@littlebearapps/outlook-assistant 3.13.0 → 3.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +30 -3
- package/README.md +67 -27
- package/advanced/index.js +44 -174
- package/auth/auth-errors.js +23 -1
- package/auth/oauth-server.js +7 -1
- package/auth/token-manager.js +7 -3
- package/auth/token-storage.js +28 -30
- package/auth/tools.js +61 -82
- package/calendar/attendees.js +36 -0
- package/calendar/cancel.js +9 -25
- package/calendar/create.js +42 -48
- package/calendar/decline.js +10 -25
- package/calendar/delete.js +10 -25
- package/calendar/index.js +20 -37
- package/calendar/list.js +4 -16
- package/calendar/preview.js +461 -0
- package/calendar/update.js +55 -83
- package/categories/index.js +68 -265
- package/config.js +29 -1
- package/contacts/index.js +72 -128
- package/email/attachments.js +43 -125
- package/email/conversations.js +44 -78
- package/email/delta.js +69 -46
- package/email/draft.js +170 -103
- package/email/export.js +145 -110
- package/email/folder-utils.js +3 -2
- package/email/headers.js +11 -49
- package/email/index.js +86 -110
- package/email/list.js +4 -17
- package/email/mail-tips.js +86 -57
- package/email/mark-as-read.js +13 -49
- package/email/mime.js +39 -51
- package/email/read.js +16 -50
- package/email/search.js +47 -87
- package/email/send.js +82 -48
- package/folder/create.js +6 -25
- package/folder/delete.js +117 -38
- package/folder/index.js +19 -17
- package/folder/list.js +5 -17
- package/folder/move.js +13 -42
- package/folder/resolve.js +11 -6
- package/folder/stats.js +18 -27
- package/index.js +39 -45
- package/llms-install.md +22 -4
- package/llms.txt +20 -11
- package/outlook-auth-server.js +10 -3
- package/package.json +4 -1
- package/request-handler.js +217 -116
- package/rules/create.js +28 -71
- package/rules/index.js +52 -93
- package/rules/list.js +7 -19
- package/rules/rule-builder.js +59 -22
- package/rules/update.js +27 -61
- package/server.js +41 -0
- package/settings/index.js +162 -145
- package/tools.js +30 -0
- package/utils/field-presets.js +4 -2
- package/utils/graph-api.js +65 -22
- package/utils/logger.js +251 -0
- package/utils/mock-data.js +91 -2
- package/utils/read-only.js +59 -0
- package/utils/response-formatter.js +54 -15
- package/utils/risk-classes.js +324 -0
- package/utils/safe-write.js +372 -6
- package/utils/safety.js +247 -42
- package/utils/server-instructions.js +73 -0
- package/utils/tool-error.js +33 -0
package/folder/delete.js
CHANGED
|
@@ -3,8 +3,108 @@
|
|
|
3
3
|
*/
|
|
4
4
|
const { callGraphAPI } = require('../utils/graph-api');
|
|
5
5
|
const { ensureAuthenticated } = require('../auth');
|
|
6
|
-
const { resolveFolder, WELL_KNOWN } = require('./resolve');
|
|
6
|
+
const { resolveFolder, listChildFolders, WELL_KNOWN } = require('./resolve');
|
|
7
7
|
const { buildMailboxPrefix } = require('../utils/mailbox');
|
|
8
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
9
|
+
const { dryRunResult } = require('../utils/safety');
|
|
10
|
+
|
|
11
|
+
const COUNT_FIELDS = 'totalItemCount,unreadItemCount';
|
|
12
|
+
const COUNT_SELECT = `id,displayName,childFolderCount,${COUNT_FIELDS}`;
|
|
13
|
+
/** Most subfolders a dry run counts before reporting "at least". */
|
|
14
|
+
const PREVIEW_FOLDER_LIMIT = 100;
|
|
15
|
+
|
|
16
|
+
const LOSS_NOTE =
|
|
17
|
+
"It doesn't go to Deleted Items, and Graph doesn't document a way to restore it (Outlook's \"Recover deleted items\" may work for a limited time, but don't rely on it). Move out anything you need first.";
|
|
18
|
+
|
|
19
|
+
function plural(count, word) {
|
|
20
|
+
return `${count} ${word}${count === 1 ? '' : 's'}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The folder's quoted path, or its ID if Graph gave it no name. */
|
|
24
|
+
function folderLabel(resolved, quote = "'") {
|
|
25
|
+
return resolved.path
|
|
26
|
+
? `${quote}${resolved.path}${quote}`
|
|
27
|
+
: `(unnamed, id ${resolved.id})`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Count every subfolder below `root`, and the items in them, breadth-first.
|
|
32
|
+
* Stops after PREVIEW_FOLDER_LIMIT subfolders and flags the count partial.
|
|
33
|
+
*/
|
|
34
|
+
async function countSubfolders(accessToken, root, mailbox) {
|
|
35
|
+
let subfolders = 0;
|
|
36
|
+
let items = 0;
|
|
37
|
+
let partial = false;
|
|
38
|
+
const queue = root.childFolderCount > 0 ? [root.id] : [];
|
|
39
|
+
while (queue.length > 0) {
|
|
40
|
+
if (subfolders >= PREVIEW_FOLDER_LIMIT) {
|
|
41
|
+
partial = true;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
const children = await listChildFolders(
|
|
45
|
+
accessToken,
|
|
46
|
+
queue.shift(),
|
|
47
|
+
COUNT_SELECT,
|
|
48
|
+
mailbox
|
|
49
|
+
);
|
|
50
|
+
for (const child of children) {
|
|
51
|
+
subfolders += 1;
|
|
52
|
+
items += child.totalItemCount || 0;
|
|
53
|
+
if (child.childFolderCount > 0) queue.push(child.id);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { subfolders, items, partial };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* dryRun preview for delete (#274): the folder, and the items and
|
|
61
|
+
* subfolders that would be lost with it. Reads only, and reads the folder
|
|
62
|
+
* itself only if resolving it didn't already (`resolved.fields`).
|
|
63
|
+
*/
|
|
64
|
+
async function previewDeleteFolder(accessToken, resolved, mailbox) {
|
|
65
|
+
const prefix = buildMailboxPrefix(mailbox);
|
|
66
|
+
const folder =
|
|
67
|
+
resolved.fields ||
|
|
68
|
+
(await callGraphAPI(
|
|
69
|
+
accessToken,
|
|
70
|
+
'GET',
|
|
71
|
+
`${prefix}/mailFolders/${resolved.id}`,
|
|
72
|
+
null,
|
|
73
|
+
{ $select: COUNT_SELECT }
|
|
74
|
+
));
|
|
75
|
+
const items = folder.totalItemCount || 0;
|
|
76
|
+
const unread = folder.unreadItemCount || 0;
|
|
77
|
+
const below = await countSubfolders(
|
|
78
|
+
accessToken,
|
|
79
|
+
{ ...folder, id: resolved.id },
|
|
80
|
+
mailbox
|
|
81
|
+
);
|
|
82
|
+
const where = mailbox ? ` in ${mailbox}` : '';
|
|
83
|
+
const name = `${folderLabel(resolved)}${where}`;
|
|
84
|
+
|
|
85
|
+
let summary;
|
|
86
|
+
if (items === 0 && below.subfolders === 0) {
|
|
87
|
+
summary = `Deletes folder ${name}. It's empty: no items or subfolders.`;
|
|
88
|
+
} else {
|
|
89
|
+
const atLeast = below.partial ? 'at least ' : '';
|
|
90
|
+
summary = `Deletes folder ${name} and everything in it: ${plural(items, 'item')}`;
|
|
91
|
+
if (unread > 0) summary += ` (${unread} unread)`;
|
|
92
|
+
if (below.subfolders > 0) {
|
|
93
|
+
summary += `, plus ${atLeast}${plural(below.subfolders, 'subfolder')} holding ${atLeast}${below.items} more ${below.items === 1 ? 'item' : 'items'}`;
|
|
94
|
+
}
|
|
95
|
+
summary += '.';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return dryRunResult([summary, LOSS_NOTE], {
|
|
99
|
+
action: 'delete',
|
|
100
|
+
folderId: resolved.id,
|
|
101
|
+
path: resolved.path,
|
|
102
|
+
items,
|
|
103
|
+
subfolders: below.subfolders,
|
|
104
|
+
subfolderItems: below.items,
|
|
105
|
+
partial: below.partial,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
8
108
|
|
|
9
109
|
/**
|
|
10
110
|
* Delete folder handler
|
|
@@ -22,31 +122,19 @@ const { buildMailboxPrefix } = require('../utils/mailbox');
|
|
|
22
122
|
* @returns {object} - MCP response
|
|
23
123
|
*/
|
|
24
124
|
async function handleDeleteFolder(args) {
|
|
25
|
-
const { folderId, folderName } = args;
|
|
125
|
+
const { folderId, folderName, dryRun = false } = args;
|
|
26
126
|
const sharedMailbox = args.sharedMailbox || args.email || null;
|
|
27
127
|
const prefix = buildMailboxPrefix(sharedMailbox);
|
|
28
128
|
|
|
29
129
|
if (!folderId && !folderName) {
|
|
30
|
-
return
|
|
31
|
-
content: [
|
|
32
|
-
{
|
|
33
|
-
type: 'text',
|
|
34
|
-
text: 'Either folderId or folderName is required.',
|
|
35
|
-
},
|
|
36
|
-
],
|
|
37
|
-
};
|
|
130
|
+
return toolError('Either folderId or folderName is required.');
|
|
38
131
|
}
|
|
39
132
|
|
|
40
133
|
// Name/alias guard for the common accidental case.
|
|
41
134
|
if (folderName && WELL_KNOWN[folderName.toLowerCase().trim()]) {
|
|
42
|
-
return
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
type: 'text',
|
|
46
|
-
text: `Cannot delete protected folder "${folderName}". Protected folders: Inbox, Drafts, Sent Items, Deleted Items, Junk Email, Archive, Outbox.`,
|
|
47
|
-
},
|
|
48
|
-
],
|
|
49
|
-
};
|
|
135
|
+
return toolError(
|
|
136
|
+
`Cannot delete protected folder "${folderName}". Protected folders: Inbox, Drafts, Sent Items, Deleted Items, Junk Email, Archive, Outbox.`
|
|
137
|
+
);
|
|
50
138
|
}
|
|
51
139
|
|
|
52
140
|
try {
|
|
@@ -60,11 +148,16 @@ async function handleDeleteFolder(args) {
|
|
|
60
148
|
id: folderId,
|
|
61
149
|
name: folderName,
|
|
62
150
|
mailbox: sharedMailbox,
|
|
151
|
+
// A dry run by ID reads the counts in the same request.
|
|
152
|
+
...(dryRun && { extraSelect: COUNT_FIELDS }),
|
|
63
153
|
});
|
|
64
154
|
} catch (resolveError) {
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
|
|
155
|
+
return toolError(resolveError.message);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// dryRun: say what would be lost; delete nothing.
|
|
159
|
+
if (dryRun) {
|
|
160
|
+
return await previewDeleteFolder(accessToken, resolved, sharedMailbox);
|
|
68
161
|
}
|
|
69
162
|
|
|
70
163
|
// Delete the folder
|
|
@@ -77,29 +170,15 @@ async function handleDeleteFolder(args) {
|
|
|
77
170
|
content: [
|
|
78
171
|
{
|
|
79
172
|
type: 'text',
|
|
80
|
-
text: `Folder
|
|
173
|
+
text: `Folder ${folderLabel(resolved, '"')} deleted successfully.`,
|
|
81
174
|
},
|
|
82
175
|
],
|
|
83
176
|
};
|
|
84
177
|
} catch (error) {
|
|
85
178
|
if (error.message === 'Authentication required') {
|
|
86
|
-
return
|
|
87
|
-
content: [
|
|
88
|
-
{
|
|
89
|
-
type: 'text',
|
|
90
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
91
|
-
},
|
|
92
|
-
],
|
|
93
|
-
};
|
|
179
|
+
return authRequiredError();
|
|
94
180
|
}
|
|
95
|
-
return {
|
|
96
|
-
content: [
|
|
97
|
-
{
|
|
98
|
-
type: 'text',
|
|
99
|
-
text: `Error deleting folder: ${error.message}`,
|
|
100
|
-
},
|
|
101
|
-
],
|
|
102
|
-
};
|
|
181
|
+
return toolError(`Error deleting folder: ${error.message}`);
|
|
103
182
|
}
|
|
104
183
|
}
|
|
105
184
|
|
package/folder/index.js
CHANGED
|
@@ -6,19 +6,17 @@ const handleCreateFolder = require('./create');
|
|
|
6
6
|
const handleMoveEmails = require('./move');
|
|
7
7
|
const handleGetFolderStats = require('./stats');
|
|
8
8
|
const handleDeleteFolder = require('./delete');
|
|
9
|
+
const { toolMetadata } = require('../utils/risk-classes');
|
|
10
|
+
const { toolError } = require('../utils/tool-error');
|
|
11
|
+
const { dryRunUnsupported } = require('../utils/safety');
|
|
9
12
|
|
|
10
13
|
// Consolidated folder tool definition
|
|
11
14
|
const folderTools = [
|
|
12
15
|
{
|
|
13
16
|
name: 'folders',
|
|
14
17
|
description:
|
|
15
|
-
"Manage mail folders
|
|
16
|
-
|
|
17
|
-
title: 'Mail Folders',
|
|
18
|
-
readOnlyHint: false,
|
|
19
|
-
destructiveHint: true,
|
|
20
|
-
openWorldHint: false,
|
|
21
|
-
},
|
|
18
|
+
"Manage mail folders. Address a folder by name, by slash-separated path for nested folders (e.g. `Inbox/Clients/Acme`, case-insensitive) or by ID; a bare name matches a unique top-level folder first, then nested ones (an ambiguous name returns the candidates). action=`list` (default) returns the tree with each folder's path and id (`includeItemCounts`, `includeChildren`). action=`create` makes `name` under the root or `parentFolder`/`parentFolderId`. action=`move` moves `emailIds` into `targetFolder`/`targetFolderId`. action=`stats` returns total/unread counts for `folder` or `folderId`. action=`delete` removes a folder (`folderName`/path or `folderId`) with everything in it, subfolders included. It skips Deleted Items and Graph documents no restore path, so pass `dryRun: true` first to see what would be lost. Protected folders (Inbox, Sent Items, etc.) can't be deleted. Every action accepts `sharedMailbox` (alias `email`) to work in a shared or delegated mailbox (default: your own).",
|
|
19
|
+
...toolMetadata('folders', 'Mail Folders'),
|
|
22
20
|
inputSchema: {
|
|
23
21
|
type: 'object',
|
|
24
22
|
properties: {
|
|
@@ -40,7 +38,7 @@ const folderTools = [
|
|
|
40
38
|
sharedMailbox: {
|
|
41
39
|
type: 'string',
|
|
42
40
|
description:
|
|
43
|
-
'Email address of a shared/delegated mailbox to target
|
|
41
|
+
'Email address of a shared/delegated mailbox to target (all actions; default: the signed-in account). Requires delegate access + Mail.Read.Shared (list/stats) or Mail.ReadWrite.Shared (create/move/delete). Work/school only; needs the server opt-in setting OUTLOOK_SHARED_MAILBOX (otherwise the call is refused with setup guidance).',
|
|
44
42
|
},
|
|
45
43
|
email: {
|
|
46
44
|
type: 'string',
|
|
@@ -79,7 +77,8 @@ const folderTools = [
|
|
|
79
77
|
},
|
|
80
78
|
sourceFolder: {
|
|
81
79
|
type: 'string',
|
|
82
|
-
description:
|
|
80
|
+
description:
|
|
81
|
+
'Ignored: action=move moves each email by ID from wherever it is. Accepted for older callers.',
|
|
83
82
|
},
|
|
84
83
|
// stats params
|
|
85
84
|
folder: {
|
|
@@ -102,12 +101,20 @@ const folderTools = [
|
|
|
102
101
|
description:
|
|
103
102
|
'Folder name or path to delete — resolved to ID (action=delete). Cannot delete protected folders (Inbox, Drafts, Sent, etc.)',
|
|
104
103
|
},
|
|
104
|
+
dryRun: {
|
|
105
|
+
type: 'boolean',
|
|
106
|
+
description:
|
|
107
|
+
'Preview only (action=delete): nothing is deleted. Shows the folder and how many items and subfolders would be lost. Other actions refuse dryRun and change nothing. Default false.',
|
|
108
|
+
},
|
|
105
109
|
},
|
|
106
110
|
additionalProperties: false,
|
|
107
111
|
required: [],
|
|
108
112
|
},
|
|
109
113
|
handler: async (args) => {
|
|
110
114
|
const action = args.action || 'list';
|
|
115
|
+
if (args.dryRun && action !== 'delete') {
|
|
116
|
+
return dryRunUnsupported('folders', action, 'delete');
|
|
117
|
+
}
|
|
111
118
|
switch (action) {
|
|
112
119
|
case 'create':
|
|
113
120
|
return handleCreateFolder(args);
|
|
@@ -120,14 +127,9 @@ const folderTools = [
|
|
|
120
127
|
case 'list':
|
|
121
128
|
return handleListFolders(args);
|
|
122
129
|
default:
|
|
123
|
-
return
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
type: 'text',
|
|
127
|
-
text: `Unknown action '${action}'. Valid actions: list, create, move, stats, delete.`,
|
|
128
|
-
},
|
|
129
|
-
],
|
|
130
|
-
};
|
|
130
|
+
return toolError(
|
|
131
|
+
`Unknown action '${action}'. Valid actions: list, create, move, stats, delete.`
|
|
132
|
+
);
|
|
131
133
|
}
|
|
132
134
|
},
|
|
133
135
|
},
|
package/folder/list.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
const { ensureAuthenticated } = require('../auth');
|
|
5
5
|
const { listChildFolders } = require('./resolve');
|
|
6
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
7
|
+
const { log } = require('../utils/logger');
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* List folders handler
|
|
@@ -52,24 +54,10 @@ async function handleListFolders(args) {
|
|
|
52
54
|
};
|
|
53
55
|
} catch (error) {
|
|
54
56
|
if (error.message === 'Authentication required') {
|
|
55
|
-
return
|
|
56
|
-
content: [
|
|
57
|
-
{
|
|
58
|
-
type: 'text',
|
|
59
|
-
text: "Authentication required. Please use the 'authenticate' tool first.",
|
|
60
|
-
},
|
|
61
|
-
],
|
|
62
|
-
};
|
|
57
|
+
return authRequiredError();
|
|
63
58
|
}
|
|
64
59
|
|
|
65
|
-
return {
|
|
66
|
-
content: [
|
|
67
|
-
{
|
|
68
|
-
type: 'text',
|
|
69
|
-
text: `Error listing folders: ${error.message}`,
|
|
70
|
-
},
|
|
71
|
-
],
|
|
72
|
-
};
|
|
60
|
+
return toolError(`Error listing folders: ${error.message}`);
|
|
73
61
|
}
|
|
74
62
|
}
|
|
75
63
|
|
|
@@ -133,7 +121,7 @@ async function getAllFoldersHierarchy(
|
|
|
133
121
|
sharedMailbox
|
|
134
122
|
);
|
|
135
123
|
} catch (error) {
|
|
136
|
-
|
|
124
|
+
log.debug(
|
|
137
125
|
`Error getting child folders for "${folder.displayName}": ${error.message}`
|
|
138
126
|
);
|
|
139
127
|
warnings.push(
|
package/folder/move.js
CHANGED
|
@@ -5,6 +5,8 @@ const { callGraphAPI } = require('../utils/graph-api');
|
|
|
5
5
|
const { ensureAuthenticated } = require('../auth');
|
|
6
6
|
const { resolveFolder } = require('./resolve');
|
|
7
7
|
const { buildMailboxPrefix } = require('../utils/mailbox');
|
|
8
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
9
|
+
const { log } = require('../utils/logger');
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Move emails handler
|
|
@@ -19,25 +21,15 @@ async function handleMoveEmails(args) {
|
|
|
19
21
|
const sharedMailbox = args.sharedMailbox || args.email || null;
|
|
20
22
|
|
|
21
23
|
if (!emailIds) {
|
|
22
|
-
return
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
type: 'text',
|
|
26
|
-
text: 'Email IDs are required. Please provide a comma-separated list of email IDs to move.',
|
|
27
|
-
},
|
|
28
|
-
],
|
|
29
|
-
};
|
|
24
|
+
return toolError(
|
|
25
|
+
'Email IDs are required. Please provide a comma-separated list of email IDs to move.'
|
|
26
|
+
);
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
if (!targetFolder && !targetFolderId) {
|
|
33
|
-
return
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
type: 'text',
|
|
37
|
-
text: 'Target folder is required — pass `targetFolder` (name or "Parent/Child" path) or `targetFolderId`.',
|
|
38
|
-
},
|
|
39
|
-
],
|
|
40
|
-
};
|
|
30
|
+
return toolError(
|
|
31
|
+
'Target folder is required — pass `targetFolder` (name or "Parent/Child" path) or `targetFolderId`.'
|
|
32
|
+
);
|
|
41
33
|
}
|
|
42
34
|
|
|
43
35
|
try {
|
|
@@ -51,14 +43,7 @@ async function handleMoveEmails(args) {
|
|
|
51
43
|
.filter((id) => id);
|
|
52
44
|
|
|
53
45
|
if (ids.length === 0) {
|
|
54
|
-
return
|
|
55
|
-
content: [
|
|
56
|
-
{
|
|
57
|
-
type: 'text',
|
|
58
|
-
text: 'No valid email IDs provided.',
|
|
59
|
-
},
|
|
60
|
-
],
|
|
61
|
-
};
|
|
46
|
+
return toolError('No valid email IDs provided.');
|
|
62
47
|
}
|
|
63
48
|
|
|
64
49
|
// Move emails
|
|
@@ -85,24 +70,10 @@ async function handleMoveEmails(args) {
|
|
|
85
70
|
};
|
|
86
71
|
} catch (error) {
|
|
87
72
|
if (error.message === 'Authentication required') {
|
|
88
|
-
return
|
|
89
|
-
content: [
|
|
90
|
-
{
|
|
91
|
-
type: 'text',
|
|
92
|
-
text: "Authentication required. Please use the 'authenticate' tool first.",
|
|
93
|
-
},
|
|
94
|
-
],
|
|
95
|
-
};
|
|
73
|
+
return authRequiredError();
|
|
96
74
|
}
|
|
97
75
|
|
|
98
|
-
return {
|
|
99
|
-
content: [
|
|
100
|
-
{
|
|
101
|
-
type: 'text',
|
|
102
|
-
text: `Error moving emails: ${error.message}`,
|
|
103
|
-
},
|
|
104
|
-
],
|
|
105
|
-
};
|
|
76
|
+
return toolError(`Error moving emails: ${error.message}`);
|
|
106
77
|
}
|
|
107
78
|
}
|
|
108
79
|
|
|
@@ -157,7 +128,7 @@ async function moveEmailsToFolder(
|
|
|
157
128
|
newId: moved?.id || emailId,
|
|
158
129
|
});
|
|
159
130
|
} catch (error) {
|
|
160
|
-
|
|
131
|
+
log.debug(`Error moving email ${emailId}: ${error.message}`);
|
|
161
132
|
results.failed.push({
|
|
162
133
|
id: emailId,
|
|
163
134
|
error: error.message,
|
|
@@ -203,7 +174,7 @@ async function moveEmailsToFolder(
|
|
|
203
174
|
results,
|
|
204
175
|
};
|
|
205
176
|
} catch (error) {
|
|
206
|
-
|
|
177
|
+
log.debug(`Error in moveEmailsToFolder: ${error.message}`);
|
|
207
178
|
throw error;
|
|
208
179
|
}
|
|
209
180
|
}
|
package/folder/resolve.js
CHANGED
|
@@ -132,15 +132,18 @@ async function resolveWellKnown(accessToken, alias, mailbox) {
|
|
|
132
132
|
return toRecord(resp, resp.displayName);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
async function resolveById(accessToken, id, mailbox) {
|
|
135
|
+
async function resolveById(accessToken, id, mailbox, extraSelect) {
|
|
136
136
|
const resp = await callGraphAPI(
|
|
137
137
|
accessToken,
|
|
138
138
|
'GET',
|
|
139
139
|
`${buildMailboxPrefix(mailbox)}/mailFolders/${id}`,
|
|
140
140
|
null,
|
|
141
|
-
{ $select: FOLDER_SELECT }
|
|
141
|
+
{ $select: extraSelect ? `${FOLDER_SELECT},${extraSelect}` : FOLDER_SELECT }
|
|
142
142
|
);
|
|
143
|
-
|
|
143
|
+
// The ID asked for stands in if a response leaves `id` out.
|
|
144
|
+
const folder = { ...resp, id: resp?.id || id };
|
|
145
|
+
const record = toRecord(folder, folder.displayName);
|
|
146
|
+
return extraSelect ? { ...record, fields: folder } : record;
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
/**
|
|
@@ -300,8 +303,10 @@ function looksLikeFolderId(value) {
|
|
|
300
303
|
/**
|
|
301
304
|
* Resolve a folder from a name/path and/or explicit ID.
|
|
302
305
|
* @param {string} accessToken
|
|
303
|
-
* @param {{name?: string, id?: string, mailbox?: string|null}} spec
|
|
304
|
-
*
|
|
306
|
+
* @param {{name?: string, id?: string, mailbox?: string|null, extraSelect?: string}} spec
|
|
307
|
+
* `extraSelect`: more fields to read when resolving by ID (e.g. item
|
|
308
|
+
* counts), returned as `fields` so the caller needn't read the folder again
|
|
309
|
+
* @returns {Promise<{id: string, displayName: string, parentId: string|null, path: string, fields?: object}>}
|
|
305
310
|
* `path` is the full slash-separated path when resolved by name/path/alias;
|
|
306
311
|
* when resolved by ID it is the folder's display name only (ancestors are not
|
|
307
312
|
* fetched).
|
|
@@ -310,7 +315,7 @@ async function resolveFolder(accessToken, spec = {}) {
|
|
|
310
315
|
const mailbox = spec.mailbox || null;
|
|
311
316
|
const id = (spec.id || '').trim();
|
|
312
317
|
if (id) {
|
|
313
|
-
return resolveById(accessToken, id, mailbox);
|
|
318
|
+
return resolveById(accessToken, id, mailbox, spec.extraSelect);
|
|
314
319
|
}
|
|
315
320
|
const name = (spec.name || '').trim();
|
|
316
321
|
if (!name) {
|
package/folder/stats.js
CHANGED
|
@@ -9,6 +9,8 @@ const { ensureAuthenticated } = require('../auth');
|
|
|
9
9
|
const { resolveFolder } = require('./resolve');
|
|
10
10
|
const { buildMailboxPrefix } = require('../utils/mailbox');
|
|
11
11
|
const config = require('../config');
|
|
12
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
13
|
+
const { log } = require('../utils/logger');
|
|
12
14
|
|
|
13
15
|
const { VERBOSITY, DEFAULT_LIMITS } = config;
|
|
14
16
|
|
|
@@ -36,9 +38,7 @@ async function handleGetFolderStats(args) {
|
|
|
36
38
|
mailbox: sharedMailbox,
|
|
37
39
|
});
|
|
38
40
|
} catch (resolveError) {
|
|
39
|
-
return
|
|
40
|
-
content: [{ type: 'text', text: resolveError.message }],
|
|
41
|
-
};
|
|
41
|
+
return toolError(resolveError.message);
|
|
42
42
|
}
|
|
43
43
|
const folderId = resolved.id;
|
|
44
44
|
|
|
@@ -75,24 +75,10 @@ async function handleGetFolderStats(args) {
|
|
|
75
75
|
};
|
|
76
76
|
} catch (error) {
|
|
77
77
|
if (error.message === 'Authentication required') {
|
|
78
|
-
return
|
|
79
|
-
content: [
|
|
80
|
-
{
|
|
81
|
-
type: 'text',
|
|
82
|
-
text: "Authentication required. Please use the 'authenticate' tool first.",
|
|
83
|
-
},
|
|
84
|
-
],
|
|
85
|
-
};
|
|
78
|
+
return authRequiredError();
|
|
86
79
|
}
|
|
87
80
|
|
|
88
|
-
return {
|
|
89
|
-
content: [
|
|
90
|
-
{
|
|
91
|
-
type: 'text',
|
|
92
|
-
text: `Error getting folder stats: ${error.message}`,
|
|
93
|
-
},
|
|
94
|
-
],
|
|
95
|
-
};
|
|
81
|
+
return toolError(`Error getting folder stats: ${error.message}`);
|
|
96
82
|
}
|
|
97
83
|
}
|
|
98
84
|
|
|
@@ -139,7 +125,7 @@ async function getEmailDateRange(accessToken, folderId, mailbox = null) {
|
|
|
139
125
|
return { newest, oldest };
|
|
140
126
|
}
|
|
141
127
|
} catch (error) {
|
|
142
|
-
|
|
128
|
+
log.debug(`Error getting date range: ${error.message}`);
|
|
143
129
|
}
|
|
144
130
|
|
|
145
131
|
return null;
|
|
@@ -194,8 +180,10 @@ function formatFolderStats(folder, dateRange, verbosity) {
|
|
|
194
180
|
text += `| Date Range | ${oldest} to ${newest} |\n`;
|
|
195
181
|
}
|
|
196
182
|
|
|
197
|
-
|
|
198
|
-
|
|
183
|
+
// List mode returns at most 50 with no page cursor, so anything bigger
|
|
184
|
+
// needs delta sync to read in full.
|
|
185
|
+
if (totalItems > 50) {
|
|
186
|
+
text += `\n_Hint: Use \`search-emails\` with \`deltaMode: true\` for efficient incremental sync of large folders._`;
|
|
199
187
|
}
|
|
200
188
|
|
|
201
189
|
return { text, meta };
|
|
@@ -224,7 +212,10 @@ function formatFolderStats(folder, dateRange, verbosity) {
|
|
|
224
212
|
text += `|---------|-------|\n`;
|
|
225
213
|
text += `| Page Size | ${pageSize} emails |\n`;
|
|
226
214
|
text += `| Total Pages | ${totalPages} |\n`;
|
|
227
|
-
text +=
|
|
215
|
+
text +=
|
|
216
|
+
totalItems > 50
|
|
217
|
+
? `| Estimated API Calls | ${Math.ceil(totalItems / 100)} (\`search-emails\` \`deltaMode: true\`, 100 per page; list mode stops at 50) |\n`
|
|
218
|
+
: `| Estimated API Calls | 1 (\`search-emails\` list mode, \`count\` up to 50) |\n`;
|
|
228
219
|
|
|
229
220
|
if (dateRange) {
|
|
230
221
|
const newestDate = new Date(dateRange.newest);
|
|
@@ -247,12 +238,12 @@ function formatFolderStats(folder, dateRange, verbosity) {
|
|
|
247
238
|
text += `\n## Recommendations\n\n`;
|
|
248
239
|
|
|
249
240
|
if (totalItems > 1000) {
|
|
250
|
-
text += `- **Large folder**: Use \`
|
|
241
|
+
text += `- **Large folder**: Use \`search-emails\` with \`deltaMode: true\` for incremental sync\n`;
|
|
251
242
|
text += `- **Use date filters**: \`receivedAfter\` and \`receivedBefore\` to narrow scope\n`;
|
|
252
|
-
} else if (totalItems >
|
|
253
|
-
text += `- **Medium folder**: Consider
|
|
243
|
+
} else if (totalItems > 50) {
|
|
244
|
+
text += `- **Medium folder**: Consider \`search-emails\` with \`deltaMode: true\` for efficient updates\n`;
|
|
254
245
|
} else {
|
|
255
|
-
text += `- **Small folder**: \`
|
|
246
|
+
text += `- **Small folder**: \`search-emails\` in list mode (raise \`count\` up to 50) is enough\n`;
|
|
256
247
|
}
|
|
257
248
|
|
|
258
249
|
if (unreadItems > 50) {
|