@littlebearapps/outlook-assistant 3.11.1 → 3.12.0
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 +12 -0
- package/README.md +46 -28
- package/advanced/index.js +239 -10
- package/auth/device-code.js +100 -3
- package/auth/token-storage.js +44 -2
- package/auth/tools.js +196 -14
- package/calendar/index.js +19 -1
- package/calendar/list.js +154 -2
- package/categories/index.js +17 -3
- package/config.js +73 -17
- package/email/attachments.js +79 -5
- package/email/conversations.js +29 -15
- package/email/delta.js +94 -4
- package/email/export.js +164 -54
- package/email/folder-utils.js +29 -6
- package/email/headers.js +5 -1
- package/email/index.js +68 -13
- package/email/list.js +8 -1
- package/email/mark-as-read.js +3 -1
- package/email/mime.js +4 -1
- package/email/read.js +5 -1
- package/email/search.js +14 -5
- package/folder/create.js +11 -4
- package/folder/delete.js +9 -1
- package/folder/index.js +11 -1
- package/folder/list.js +61 -27
- package/folder/move.js +32 -7
- package/folder/resolve.js +62 -23
- package/folder/stats.js +11 -5
- package/llms-install.md +28 -9
- package/llms.txt +12 -8
- package/package.json +5 -5
- package/utils/graph-api.js +109 -3
- package/utils/mailbox.js +77 -0
- package/utils/response-formatter.js +44 -10
package/email/index.js
CHANGED
|
@@ -33,7 +33,7 @@ const emailTools = [
|
|
|
33
33
|
{
|
|
34
34
|
name: 'search-emails',
|
|
35
35
|
description:
|
|
36
|
-
'Search, list, delta-sync, or thread-group emails — six modes selected by parameters (read-only). With no params: lists recent emails in `folder` (default `inbox`). With `query`/`from`/`to`/`subject`/date filters: full search (combines via OData filter). With `searchExpression` (deprecated alias `kqlQuery`): a raw Microsoft Graph `$search` expression for advanced server-side search. With `deltaMode: true`: returns current state plus a `deltaToken`; pass the token back on the next call for incremental changes only — ideal for inbox monitoring. With `groupByConversation: true`: returns conversation threads. With `conversationId`: returns all messages in a single thread. With `internetMessageId`: looks up a message by its RFC Message-ID header. Personal Outlook.com accounts have limited `$search` support — this tool falls back through OData filters / boolean filters / recent listing automatically, but structured filters (`from`/`subject`/`receivedAfter`/`hasAttachments`/`unreadOnly`) return cleaner results. Returns paged messages with id/subject/from/receivedDateTime/preview by default; use `outputVerbosity` to expand.',
|
|
36
|
+
'Search, list, delta-sync, or thread-group emails — six modes selected by parameters (read-only). With no params: lists recent emails in `folder` (default `inbox`). With `query`/`from`/`to`/`subject`/date filters: full search (combines via OData filter). With `searchExpression` (deprecated alias `kqlQuery`): a raw Microsoft Graph `$search` expression for advanced server-side search. With `deltaMode: true`: returns current state plus a `deltaToken`; pass the token back on the next call for incremental changes only — ideal for inbox monitoring. With `groupByConversation: true`: returns conversation threads. With `conversationId`: returns all messages in a single thread. With `internetMessageId`: looks up a message by its RFC Message-ID header. Set `sharedMailbox` (or alias `email`) to search a shared/delegated mailbox instead of the signed-in account — works with custom folders and nested folder paths. Personal Outlook.com accounts have limited `$search` support — this tool falls back through OData filters / boolean filters / recent listing automatically, but structured filters (`from`/`subject`/`receivedAfter`/`hasAttachments`/`unreadOnly`) return cleaner results. Returns paged messages with id/subject/from/receivedDateTime/preview by default; use `outputVerbosity` to expand.',
|
|
37
37
|
annotations: {
|
|
38
38
|
title: 'Search Emails',
|
|
39
39
|
readOnlyHint: true,
|
|
@@ -48,22 +48,22 @@ const emailTools = [
|
|
|
48
48
|
deltaMode: {
|
|
49
49
|
type: 'boolean',
|
|
50
50
|
description:
|
|
51
|
-
'Enable delta sync mode. Returns only changes since last sync. Use deltaToken for subsequent calls.',
|
|
51
|
+
'Enable delta sync mode. Returns only changes since last sync. Use deltaToken for subsequent calls. Honors `sharedMailbox`/`email` (and custom `folder` paths) to sync within a shared/delegated mailbox.',
|
|
52
52
|
},
|
|
53
53
|
internetMessageId: {
|
|
54
54
|
type: 'string',
|
|
55
55
|
description:
|
|
56
|
-
'Look up email by Message-ID header (e.g. <abc123@example.com>). For threading/deduplication.',
|
|
56
|
+
'Look up email by Message-ID header (e.g. <abc123@example.com>). For threading/deduplication. Honors `sharedMailbox`/`email` to look up within a shared/delegated mailbox.',
|
|
57
57
|
},
|
|
58
58
|
conversationId: {
|
|
59
59
|
type: 'string',
|
|
60
60
|
description:
|
|
61
|
-
'Get all messages in a conversation thread by conversationId.',
|
|
61
|
+
'Get all messages in a conversation thread by conversationId. Honors `sharedMailbox`/`email` to thread within a shared/delegated mailbox.',
|
|
62
62
|
},
|
|
63
63
|
groupByConversation: {
|
|
64
64
|
type: 'boolean',
|
|
65
65
|
description:
|
|
66
|
-
'List conversations (threads) grouped by conversationId instead of individual emails.',
|
|
66
|
+
'List conversations (threads) grouped by conversationId instead of individual emails. Honors `sharedMailbox`/`email` (and custom `folder` paths) to group within a shared/delegated mailbox.',
|
|
67
67
|
},
|
|
68
68
|
// Search/list params
|
|
69
69
|
query: {
|
|
@@ -83,7 +83,17 @@ const emailTools = [
|
|
|
83
83
|
},
|
|
84
84
|
folder: {
|
|
85
85
|
type: 'string',
|
|
86
|
-
description:
|
|
86
|
+
description:
|
|
87
|
+
"Email folder (default: 'inbox'). Accepts a well-known name, a custom/localized display name, or a nested path like `Inbox/Subfolder`.",
|
|
88
|
+
},
|
|
89
|
+
sharedMailbox: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
description:
|
|
92
|
+
'Email address of a shared/delegated mailbox to search instead of the signed-in account. Combine with `folder` (incl. custom subfolders/paths) or `searchAllFolders`. Work/school only; needs the server opt-in setting OUTLOOK_SHARED_MAILBOX (otherwise the call is refused with setup guidance).',
|
|
93
|
+
},
|
|
94
|
+
email: {
|
|
95
|
+
type: 'string',
|
|
96
|
+
description: 'Alias for `sharedMailbox`.',
|
|
87
97
|
},
|
|
88
98
|
from: {
|
|
89
99
|
type: 'string',
|
|
@@ -132,7 +142,7 @@ const emailTools = [
|
|
|
132
142
|
deltaToken: {
|
|
133
143
|
type: 'string',
|
|
134
144
|
description:
|
|
135
|
-
'Token from previous delta call for incremental sync (deltaMode only)',
|
|
145
|
+
'Token from previous delta call for incremental sync (deltaMode only). The token is authoritative — it encodes its own mailbox and folder, so `folder`/`sharedMailbox` are ignored and a token from a different mailbox is rejected.',
|
|
136
146
|
},
|
|
137
147
|
maxResults: {
|
|
138
148
|
type: 'number',
|
|
@@ -158,6 +168,7 @@ const emailTools = [
|
|
|
158
168
|
return handleSearchByMessageId({
|
|
159
169
|
messageId: args.internetMessageId,
|
|
160
170
|
outputVerbosity: args.outputVerbosity,
|
|
171
|
+
sharedMailbox: args.sharedMailbox || args.email || null,
|
|
161
172
|
});
|
|
162
173
|
}
|
|
163
174
|
if (args.conversationId) {
|
|
@@ -189,7 +200,7 @@ const emailTools = [
|
|
|
189
200
|
{
|
|
190
201
|
name: 'read-email',
|
|
191
202
|
description:
|
|
192
|
-
'Read a single email by id (read-only). Default: returns the full message body (HTML stripped to text by default), subject, from/to/cc, receivedDateTime, conversationId, attachments metadata, and webLink as Markdown. With `headersMode: true`: returns RFC-822 forensic headers instead (DKIM, SPF, DMARC, Received chain, Message-ID, Authentication-Results) — pair with `importantOnly: true` for the security-relevant subset, `groupByType: true` for category-bucketed view, or `raw: true` for JSON instead of Markdown. With `includeHeaders: true` (non-headers-mode): adds basic headers alongside body. Use `outputVerbosity` (minimal/standard/full) to control field count.',
|
|
203
|
+
'Read a single email by id (read-only). Default: returns the full message body (HTML stripped to text by default), subject, from/to/cc, receivedDateTime, conversationId, attachments metadata, and webLink as Markdown. With `headersMode: true`: returns RFC-822 forensic headers instead (DKIM, SPF, DMARC, Received chain, Message-ID, Authentication-Results) — pair with `importantOnly: true` for the security-relevant subset, `groupByType: true` for category-bucketed view, or `raw: true` for JSON instead of Markdown. With `includeHeaders: true` (non-headers-mode): adds basic headers alongside body. Use `outputVerbosity` (minimal/standard/full) to control field count. **If the id came from a shared/delegated mailbox (e.g. via `search-emails` or `access-shared-mailbox` with `sharedMailbox` set), you MUST pass the same `sharedMailbox` (or alias `email`) here** — message IDs are mailbox-scoped, and reading a shared-mailbox id without it fails with 404 ErrorInvalidMailboxItemId.',
|
|
193
204
|
annotations: {
|
|
194
205
|
title: 'Read Email',
|
|
195
206
|
readOnlyHint: true,
|
|
@@ -203,6 +214,15 @@ const emailTools = [
|
|
|
203
214
|
type: 'string',
|
|
204
215
|
description: 'ID of the email to read',
|
|
205
216
|
},
|
|
217
|
+
sharedMailbox: {
|
|
218
|
+
type: 'string',
|
|
219
|
+
description:
|
|
220
|
+
'Email address of the shared/delegated mailbox the id belongs to. Required when the id was obtained from a shared mailbox — message IDs are mailbox-scoped and reading without it returns 404 ErrorInvalidMailboxItemId. Requires delegate access + Mail.Read.Shared. Work/school only; needs the server opt-in setting OUTLOOK_SHARED_MAILBOX (otherwise the call is refused with setup guidance).',
|
|
221
|
+
},
|
|
222
|
+
email: {
|
|
223
|
+
type: 'string',
|
|
224
|
+
description: 'Alias for `sharedMailbox`.',
|
|
225
|
+
},
|
|
206
226
|
headersMode: {
|
|
207
227
|
type: 'boolean',
|
|
208
228
|
description:
|
|
@@ -386,7 +406,7 @@ const emailTools = [
|
|
|
386
406
|
{
|
|
387
407
|
name: 'update-email',
|
|
388
408
|
description:
|
|
389
|
-
'Update message state without modifying content (idempotent — safe to retry). action=`mark-read`/`mark-unread` toggles the `isRead` flag on a single message by `id`. action=`flag` sets a follow-up flag with optional `dueDateTime`/`startDateTime` (ISO 8601). action=`unflag` clears the flag. action=`complete` marks the flag as done. Flag/unflag/complete accept either `id` (single) or `ids` (batch array) — batch operations use Graph `$batch` for efficiency. Returns status confirmation per message.',
|
|
409
|
+
'Update message state without modifying content (idempotent — safe to retry). action=`mark-read`/`mark-unread` toggles the `isRead` flag on a single message by `id`. action=`flag` sets a follow-up flag with optional `dueDateTime`/`startDateTime` (ISO 8601). action=`unflag` clears the flag. action=`complete` marks the flag as done. Flag/unflag/complete accept either `id` (single) or `ids` (batch array) — batch operations use Graph `$batch` for efficiency. Pass `sharedMailbox` (or alias `email`) to update messages in a shared/delegated mailbox instead of the signed-in account (requires Mail.ReadWrite.Shared + delegate access). Returns status confirmation per message.',
|
|
390
410
|
annotations: {
|
|
391
411
|
title: 'Update Email',
|
|
392
412
|
readOnlyHint: false,
|
|
@@ -422,34 +442,51 @@ const emailTools = [
|
|
|
422
442
|
type: 'string',
|
|
423
443
|
description: 'Start date/time for follow-up, ISO 8601 (action=flag)',
|
|
424
444
|
},
|
|
445
|
+
sharedMailbox: {
|
|
446
|
+
type: 'string',
|
|
447
|
+
description:
|
|
448
|
+
'Email address of a shared/delegated mailbox whose message(s) to update instead of the signed-in account. Requires delegate access + Mail.ReadWrite.Shared. Work/school only; needs the server opt-in setting OUTLOOK_SHARED_MAILBOX (otherwise the call is refused with setup guidance).',
|
|
449
|
+
},
|
|
450
|
+
email: {
|
|
451
|
+
type: 'string',
|
|
452
|
+
description: 'Alias for `sharedMailbox`.',
|
|
453
|
+
},
|
|
425
454
|
},
|
|
426
455
|
additionalProperties: false,
|
|
427
456
|
required: ['action'],
|
|
428
457
|
},
|
|
429
458
|
handler: async (args) => {
|
|
459
|
+
const sharedMailbox = args.sharedMailbox || args.email || null;
|
|
430
460
|
switch (args.action) {
|
|
431
461
|
case 'mark-read':
|
|
432
|
-
return handleMarkAsRead({ id: args.id, isRead: true });
|
|
462
|
+
return handleMarkAsRead({ id: args.id, isRead: true, sharedMailbox });
|
|
433
463
|
case 'mark-unread':
|
|
434
|
-
return handleMarkAsRead({
|
|
464
|
+
return handleMarkAsRead({
|
|
465
|
+
id: args.id,
|
|
466
|
+
isRead: false,
|
|
467
|
+
sharedMailbox,
|
|
468
|
+
});
|
|
435
469
|
case 'flag':
|
|
436
470
|
return handleSetMessageFlag({
|
|
437
471
|
messageId: args.id,
|
|
438
472
|
messageIds: args.ids,
|
|
439
473
|
dueDateTime: args.dueDateTime,
|
|
440
474
|
startDateTime: args.startDateTime,
|
|
475
|
+
sharedMailbox,
|
|
441
476
|
});
|
|
442
477
|
case 'unflag':
|
|
443
478
|
return handleClearMessageFlag({
|
|
444
479
|
messageId: args.id,
|
|
445
480
|
messageIds: args.ids,
|
|
446
481
|
markComplete: false,
|
|
482
|
+
sharedMailbox,
|
|
447
483
|
});
|
|
448
484
|
case 'complete':
|
|
449
485
|
return handleClearMessageFlag({
|
|
450
486
|
messageId: args.id,
|
|
451
487
|
messageIds: args.ids,
|
|
452
488
|
markComplete: true,
|
|
489
|
+
sharedMailbox,
|
|
453
490
|
});
|
|
454
491
|
default:
|
|
455
492
|
return {
|
|
@@ -466,7 +503,7 @@ const emailTools = [
|
|
|
466
503
|
{
|
|
467
504
|
name: 'attachments',
|
|
468
505
|
description:
|
|
469
|
-
'Inspect or retrieve email attachments. action=`list` (default) returns metadata for all attachments on `messageId` (id, name, contentType, size, isInline) — read-only. action=`view` returns inline content for text/JSON/XML attachments via `attachmentId`; binary types require download. action=`download` saves the attachment to disk at `outputDir` (default system tmpdir, auto-created) and returns the saved file path. `messageId` is required for all actions; `attachmentId` is required for view/download. Use `outputVerbosity` to control list field count.',
|
|
506
|
+
'Inspect or retrieve email attachments. action=`list` (default) returns metadata for all attachments on `messageId` (id, name, contentType, size, isInline) — read-only. action=`view` returns inline content for text/JSON/XML attachments via `attachmentId`; binary types require download. action=`download` saves the attachment to disk at `outputDir` (default system tmpdir, auto-created) and returns the saved file path. `messageId` is required for all actions; `attachmentId` is required for view/download. If `messageId` came from a shared/delegated mailbox, pass the same `sharedMailbox` (or alias `email`) — attachment IDs are scoped to the message and fail under /me otherwise. Use `outputVerbosity` to control list field count.',
|
|
470
507
|
annotations: {
|
|
471
508
|
title: 'Attachments',
|
|
472
509
|
readOnlyHint: false,
|
|
@@ -491,6 +528,15 @@ const emailTools = [
|
|
|
491
528
|
type: 'string',
|
|
492
529
|
description: 'Attachment ID (action=view/download, required)',
|
|
493
530
|
},
|
|
531
|
+
sharedMailbox: {
|
|
532
|
+
type: 'string',
|
|
533
|
+
description:
|
|
534
|
+
'Email address of the shared/delegated mailbox the messageId belongs to. Required when the message came from a shared mailbox. Requires delegate access + Mail.Read.Shared. Work/school only; needs the server opt-in setting OUTLOOK_SHARED_MAILBOX (otherwise the call is refused with setup guidance).',
|
|
535
|
+
},
|
|
536
|
+
email: {
|
|
537
|
+
type: 'string',
|
|
538
|
+
description: 'Alias for `sharedMailbox`.',
|
|
539
|
+
},
|
|
494
540
|
outputDir: {
|
|
495
541
|
type: 'string',
|
|
496
542
|
description:
|
|
@@ -529,7 +575,7 @@ const emailTools = [
|
|
|
529
575
|
{
|
|
530
576
|
name: 'export',
|
|
531
577
|
description:
|
|
532
|
-
'Export emails to file formats for archival, forensics, or programmatic processing. target=`message` (default) exports a single email by `id` to `savePath` — accepts `mime`/`eml`/`markdown`/`json`/`csv`. target=`messages` batch-exports either an explicit `emailIds` array or messages matching `searchQuery` (or `query` shortcut) into `outputDir` — accepts `markdown`/`json`/`csv`. target=`conversation` exports a full thread by `conversationId` into `outputDir` (chronological by default; pass `order: "reverse"` for newest-first) — accepts `eml`/`mbox`/`markdown`/`json`/`html`/`csv`. target=`mime` returns raw RFC-822 MIME bytes for `id` (use `headersOnly` for just headers, `base64` for encoded transport, `maxSize` to cap at default 1MB). `includeAttachments` defaults to true for single-message exports and false for batch. Format support varies by target — see the format param enum.',
|
|
578
|
+
'Export emails to file formats for archival, forensics, or programmatic processing. target=`message` (default) exports a single email by `id` to `savePath` — accepts `mime`/`eml`/`markdown`/`json`/`csv`. target=`messages` batch-exports either an explicit `emailIds` array or messages matching `searchQuery` (or `query` shortcut) into `outputDir` — accepts `markdown`/`json`/`csv`. target=`conversation` exports a full thread by `conversationId` into `outputDir` (chronological by default; pass `order: "reverse"` for newest-first) — accepts `eml`/`mbox`/`markdown`/`json`/`html`/`csv`. target=`mime` returns raw RFC-822 MIME bytes for `id` (use `headersOnly` for just headers, `base64` for encoded transport, `maxSize` to cap at default 1MB). All targets accept `sharedMailbox` (alias `email`) to export from a shared/delegated mailbox instead of the signed-in account — pass it whenever the id(s)/conversationId/searchQuery come from a shared mailbox, or exports fail with 404 ErrorInvalidMailboxItemId. `includeAttachments` defaults to true for single-message exports and false for batch. Format support varies by target — see the format param enum.',
|
|
533
579
|
annotations: {
|
|
534
580
|
title: 'Export Emails',
|
|
535
581
|
readOnlyHint: false,
|
|
@@ -606,6 +652,15 @@ const emailTools = [
|
|
|
606
652
|
description:
|
|
607
653
|
'Message order (target=conversation, default: chronological)',
|
|
608
654
|
},
|
|
655
|
+
sharedMailbox: {
|
|
656
|
+
type: 'string',
|
|
657
|
+
description:
|
|
658
|
+
'Email address of a shared/delegated mailbox to export from instead of the signed-in account. Applies to all targets (message/messages/conversation/mime) — pass it whenever the id(s)/conversationId/searchQuery belong to a shared mailbox. Requires delegate access + Mail.Read.Shared. Work/school only; needs the server opt-in setting OUTLOOK_SHARED_MAILBOX (otherwise the call is refused with setup guidance).',
|
|
659
|
+
},
|
|
660
|
+
email: {
|
|
661
|
+
type: 'string',
|
|
662
|
+
description: 'Alias for `sharedMailbox`.',
|
|
663
|
+
},
|
|
609
664
|
// MIME params
|
|
610
665
|
headersOnly: {
|
|
611
666
|
type: 'boolean',
|
package/email/list.js
CHANGED
|
@@ -51,13 +51,20 @@ async function handleListEmails(args) {
|
|
|
51
51
|
const requestedCount =
|
|
52
52
|
args.count ?? args.maxResults ?? DEFAULT_LIMITS.listEmails;
|
|
53
53
|
const verbosity = args.outputVerbosity || VERBOSITY.STANDARD;
|
|
54
|
+
// `search-emails` falls through to list mode when no filters are supplied,
|
|
55
|
+
// so the shared-mailbox scope has to be honoured here too.
|
|
56
|
+
const sharedMailbox = args.sharedMailbox || args.email || null;
|
|
54
57
|
|
|
55
58
|
try {
|
|
56
59
|
// Get access token
|
|
57
60
|
const accessToken = await ensureAuthenticated();
|
|
58
61
|
|
|
59
62
|
// Resolve the folder path
|
|
60
|
-
const endpoint = await resolveFolderPath(
|
|
63
|
+
const endpoint = await resolveFolderPath(
|
|
64
|
+
accessToken,
|
|
65
|
+
folder,
|
|
66
|
+
sharedMailbox
|
|
67
|
+
);
|
|
61
68
|
|
|
62
69
|
// Select fields based on verbosity level
|
|
63
70
|
const fieldPreset = getFieldPresetForVerbosity(verbosity);
|
package/email/mark-as-read.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const _config = require('../config'); // Reserved for future use
|
|
5
5
|
const { callGraphAPI } = require('../utils/graph-api');
|
|
6
6
|
const { ensureAuthenticated } = require('../auth');
|
|
7
|
+
const { buildMailboxPrefix } = require('../utils/mailbox');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Mark email as read handler
|
|
@@ -13,6 +14,7 @@ const { ensureAuthenticated } = require('../auth');
|
|
|
13
14
|
async function handleMarkAsRead(args) {
|
|
14
15
|
const emailId = args.id;
|
|
15
16
|
const isRead = args.isRead !== undefined ? args.isRead : true; // Default to true
|
|
17
|
+
const prefix = buildMailboxPrefix(args.sharedMailbox || args.email || null);
|
|
16
18
|
|
|
17
19
|
if (!emailId) {
|
|
18
20
|
return {
|
|
@@ -30,7 +32,7 @@ async function handleMarkAsRead(args) {
|
|
|
30
32
|
const accessToken = await ensureAuthenticated();
|
|
31
33
|
|
|
32
34
|
// Make API call to update email read status
|
|
33
|
-
const endpoint =
|
|
35
|
+
const endpoint = `${prefix}/messages/${emailId}`;
|
|
34
36
|
const updateData = {
|
|
35
37
|
isRead: isRead,
|
|
36
38
|
};
|
package/email/mime.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
const { callGraphAPIRaw } = require('../utils/graph-api');
|
|
8
8
|
const { ensureAuthenticated } = require('../auth');
|
|
9
|
+
const { buildMailboxPrefix } = require('../utils/mailbox');
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Parse MIME headers from raw content
|
|
@@ -93,6 +94,8 @@ async function handleGetMimeContent(args) {
|
|
|
93
94
|
const headersOnly = args.headersOnly || false;
|
|
94
95
|
const returnBase64 = args.base64 || false;
|
|
95
96
|
const maxSize = args.maxSize !== undefined ? args.maxSize : 1024 * 1024; // 1MB default
|
|
97
|
+
// Mailbox-scoped IDs: route to /users/{mailbox} for a shared/delegated mailbox.
|
|
98
|
+
const prefix = buildMailboxPrefix(args.sharedMailbox || args.email || null);
|
|
96
99
|
|
|
97
100
|
if (!emailId) {
|
|
98
101
|
return {
|
|
@@ -111,7 +114,7 @@ async function handleGetMimeContent(args) {
|
|
|
111
114
|
|
|
112
115
|
try {
|
|
113
116
|
// Fetch raw MIME content
|
|
114
|
-
const mimeContent = await callGraphAPIRaw(accessToken, emailId);
|
|
117
|
+
const mimeContent = await callGraphAPIRaw(accessToken, emailId, prefix);
|
|
115
118
|
|
|
116
119
|
if (!mimeContent) {
|
|
117
120
|
return {
|
package/email/read.js
CHANGED
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
VERBOSITY,
|
|
12
12
|
} = require('../utils/response-formatter');
|
|
13
13
|
const { getEmailFields } = require('../utils/field-presets');
|
|
14
|
+
const { buildMailboxPrefix } = require('../utils/mailbox');
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Get field preset based on verbosity and options
|
|
@@ -45,6 +46,9 @@ async function handleReadEmail(args) {
|
|
|
45
46
|
const emailId = args.id;
|
|
46
47
|
const verbosity = args.outputVerbosity || VERBOSITY.STANDARD;
|
|
47
48
|
const includeHeaders = args.includeHeaders || false;
|
|
49
|
+
// Message IDs are mailbox-scoped: an ID issued by a shared/delegated mailbox
|
|
50
|
+
// is not resolvable under /me. Route to /users/{mailbox} when supplied.
|
|
51
|
+
const prefix = buildMailboxPrefix(args.sharedMailbox || args.email || null);
|
|
48
52
|
|
|
49
53
|
if (!emailId) {
|
|
50
54
|
return {
|
|
@@ -66,7 +70,7 @@ async function handleReadEmail(args) {
|
|
|
66
70
|
const selectFields = getEmailFields(fieldPreset);
|
|
67
71
|
|
|
68
72
|
// Make API call to get email details
|
|
69
|
-
const endpoint =
|
|
73
|
+
const endpoint = `${prefix}/messages/${emailId}`;
|
|
70
74
|
const queryParams = {
|
|
71
75
|
$select: selectFields,
|
|
72
76
|
};
|
package/email/search.js
CHANGED
|
@@ -7,6 +7,7 @@ const _config = require('../config'); // Reserved for future use
|
|
|
7
7
|
const { callGraphAPI, callGraphAPIPaginated } = require('../utils/graph-api');
|
|
8
8
|
const { ensureAuthenticated } = require('../auth');
|
|
9
9
|
const { resolveFolderPath } = require('./folder-utils');
|
|
10
|
+
const { buildMailboxPrefix } = require('../utils/mailbox');
|
|
10
11
|
const {
|
|
11
12
|
formatEmailList,
|
|
12
13
|
VERBOSITY,
|
|
@@ -71,6 +72,9 @@ async function handleSearchEmails(args) {
|
|
|
71
72
|
// Trim so a whitespace-only value doesn't send `$search: '""'`. (#169)
|
|
72
73
|
const kqlQuery =
|
|
73
74
|
(args.searchExpression || '').trim() || (args.kqlQuery || '').trim();
|
|
75
|
+
// Optional: scope the search to a shared/delegated mailbox rather than the
|
|
76
|
+
// signed-in account. Accepts a custom/localized folder name or nested path.
|
|
77
|
+
const sharedMailbox = args.sharedMailbox || args.email || null;
|
|
74
78
|
|
|
75
79
|
// Select fields based on verbosity — but never at the cost of correctness.
|
|
76
80
|
// When more than one search term is supplied, the ladder may satisfy one
|
|
@@ -98,13 +102,16 @@ async function handleSearchEmails(args) {
|
|
|
98
102
|
// Get access token
|
|
99
103
|
const accessToken = await ensureAuthenticated();
|
|
100
104
|
|
|
101
|
-
// Determine endpoint - search all folders or specific folder
|
|
105
|
+
// Determine endpoint - search all folders or specific folder, scoped to
|
|
106
|
+
// the signed-in account or a shared mailbox.
|
|
102
107
|
let endpoint;
|
|
103
108
|
if (searchAllFolders) {
|
|
104
|
-
endpoint =
|
|
105
|
-
console.error(
|
|
109
|
+
endpoint = `${buildMailboxPrefix(sharedMailbox)}/messages`;
|
|
110
|
+
console.error(
|
|
111
|
+
`Searching across all mail folders${sharedMailbox ? ` in ${sharedMailbox}` : ''}`
|
|
112
|
+
);
|
|
106
113
|
} else {
|
|
107
|
-
endpoint = await resolveFolderPath(accessToken, folder);
|
|
114
|
+
endpoint = await resolveFolderPath(accessToken, folder, sharedMailbox);
|
|
108
115
|
console.error(`Using endpoint: ${endpoint} for folder: ${folder}`);
|
|
109
116
|
}
|
|
110
117
|
|
|
@@ -1569,6 +1576,8 @@ function formatSearchResults(response, folder, verbosity, searchAllFolders) {
|
|
|
1569
1576
|
async function handleSearchByMessageId(args) {
|
|
1570
1577
|
const messageId = args.messageId;
|
|
1571
1578
|
const verbosity = args.outputVerbosity || VERBOSITY.STANDARD;
|
|
1579
|
+
// Optional: scope the lookup to a shared/delegated mailbox.
|
|
1580
|
+
const prefix = buildMailboxPrefix(args.sharedMailbox || args.email || null);
|
|
1572
1581
|
|
|
1573
1582
|
if (!messageId) {
|
|
1574
1583
|
return {
|
|
@@ -1604,7 +1613,7 @@ async function handleSearchByMessageId(args) {
|
|
|
1604
1613
|
const response = await callGraphAPI(
|
|
1605
1614
|
accessToken,
|
|
1606
1615
|
'GET',
|
|
1607
|
-
|
|
1616
|
+
`${prefix}/messages`,
|
|
1608
1617
|
null,
|
|
1609
1618
|
params
|
|
1610
1619
|
);
|
package/folder/create.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const { callGraphAPI } = require('../utils/graph-api');
|
|
5
5
|
const { ensureAuthenticated } = require('../auth');
|
|
6
6
|
const { resolveFolder, listChildFolders } = require('./resolve');
|
|
7
|
+
const { buildMailboxPrefix } = require('../utils/mailbox');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Create folder handler
|
|
@@ -14,6 +15,7 @@ async function handleCreateFolder(args) {
|
|
|
14
15
|
const folderName = (args.name || '').trim();
|
|
15
16
|
const parentFolder = args.parentFolder || '';
|
|
16
17
|
const parentFolderId = args.parentFolderId || '';
|
|
18
|
+
const sharedMailbox = args.sharedMailbox || args.email || null;
|
|
17
19
|
|
|
18
20
|
if (!folderName) {
|
|
19
21
|
return {
|
|
@@ -34,6 +36,7 @@ async function handleCreateFolder(args) {
|
|
|
34
36
|
const result = await createMailFolder(accessToken, folderName, {
|
|
35
37
|
name: parentFolder,
|
|
36
38
|
id: parentFolderId,
|
|
39
|
+
mailbox: sharedMailbox,
|
|
37
40
|
});
|
|
38
41
|
|
|
39
42
|
return {
|
|
@@ -74,10 +77,12 @@ async function handleCreateFolder(args) {
|
|
|
74
77
|
* Create a new mail folder
|
|
75
78
|
* @param {string} accessToken - Access token
|
|
76
79
|
* @param {string} folderName - Name of the folder to create
|
|
77
|
-
* @param {{name?: string, id?: string}} parentSpec - Parent folder name/path or ID
|
|
80
|
+
* @param {{name?: string, id?: string, mailbox?: string|null}} parentSpec - Parent folder name/path or ID, plus optional shared mailbox
|
|
78
81
|
* @returns {Promise<object>} - Result object with status and message
|
|
79
82
|
*/
|
|
80
83
|
async function createMailFolder(accessToken, folderName, parentSpec) {
|
|
84
|
+
const mailbox = parentSpec.mailbox || null;
|
|
85
|
+
const prefix = buildMailboxPrefix(mailbox);
|
|
81
86
|
try {
|
|
82
87
|
// Resolve the parent folder if one was specified (supports "Parent/Child"
|
|
83
88
|
// paths and explicit IDs). Leaf name (folderName) is created, not
|
|
@@ -98,7 +103,9 @@ async function createMailFolder(accessToken, folderName, parentSpec) {
|
|
|
98
103
|
// mailbox — a name may legitimately exist under a different parent. (#216)
|
|
99
104
|
const siblings = await listChildFolders(
|
|
100
105
|
accessToken,
|
|
101
|
-
parent ? parent.id : null
|
|
106
|
+
parent ? parent.id : null,
|
|
107
|
+
undefined,
|
|
108
|
+
mailbox
|
|
102
109
|
);
|
|
103
110
|
const lower = folderName.toLowerCase();
|
|
104
111
|
if (siblings.some((f) => f.displayName.toLowerCase() === lower)) {
|
|
@@ -111,8 +118,8 @@ async function createMailFolder(accessToken, folderName, parentSpec) {
|
|
|
111
118
|
}
|
|
112
119
|
|
|
113
120
|
const endpoint = parent
|
|
114
|
-
?
|
|
115
|
-
:
|
|
121
|
+
? `${prefix}/mailFolders/${parent.id}/childFolders`
|
|
122
|
+
: `${prefix}/mailFolders`;
|
|
116
123
|
|
|
117
124
|
// Create the folder
|
|
118
125
|
const folderData = {
|
package/folder/delete.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const { callGraphAPI } = require('../utils/graph-api');
|
|
5
5
|
const { ensureAuthenticated } = require('../auth');
|
|
6
6
|
const { resolveFolder, WELL_KNOWN } = require('./resolve');
|
|
7
|
+
const { buildMailboxPrefix } = require('../utils/mailbox');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Delete folder handler
|
|
@@ -22,6 +23,8 @@ const { resolveFolder, WELL_KNOWN } = require('./resolve');
|
|
|
22
23
|
*/
|
|
23
24
|
async function handleDeleteFolder(args) {
|
|
24
25
|
const { folderId, folderName } = args;
|
|
26
|
+
const sharedMailbox = args.sharedMailbox || args.email || null;
|
|
27
|
+
const prefix = buildMailboxPrefix(sharedMailbox);
|
|
25
28
|
|
|
26
29
|
if (!folderId && !folderName) {
|
|
27
30
|
return {
|
|
@@ -56,6 +59,7 @@ async function handleDeleteFolder(args) {
|
|
|
56
59
|
resolved = await resolveFolder(accessToken, {
|
|
57
60
|
id: folderId,
|
|
58
61
|
name: folderName,
|
|
62
|
+
mailbox: sharedMailbox,
|
|
59
63
|
});
|
|
60
64
|
} catch (resolveError) {
|
|
61
65
|
return {
|
|
@@ -64,7 +68,11 @@ async function handleDeleteFolder(args) {
|
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
// Delete the folder
|
|
67
|
-
await callGraphAPI(
|
|
71
|
+
await callGraphAPI(
|
|
72
|
+
accessToken,
|
|
73
|
+
'DELETE',
|
|
74
|
+
`${prefix}/mailFolders/${resolved.id}`
|
|
75
|
+
);
|
|
68
76
|
return {
|
|
69
77
|
content: [
|
|
70
78
|
{
|
package/folder/index.js
CHANGED
|
@@ -12,7 +12,7 @@ const folderTools = [
|
|
|
12
12
|
{
|
|
13
13
|
name: 'folders',
|
|
14
14
|
description:
|
|
15
|
-
"Manage mail folders (tool-level destructiveHint=true because `delete` permanently removes a folder; `list` and `stats` are read-only sub-actions despite the annotation). Folders can be addressed by name, by a slash-separated PATH for nested folders (e.g. `Triage/Delete`, `Inbox/Clients/Acme`, case-insensitive), or by explicit ID; `list` output includes each folder's full path and `[id: …]`. A bare name resolves a unique top-level folder first, then searches nested folders (ambiguous names return the candidates — disambiguate with a path or ID). action=`list` (default) returns the folder tree (toggle `includeItemCounts` for unread/total, `includeChildren` for hierarchy). action=`create` makes a new folder under the root, or under `parentFolder` (name/path) / `parentFolderId`, and returns its id. action=`move` relocates emails (`emailIds`) into `targetFolder` (name/path) or `targetFolderId`. action=`stats` returns counts (totalItemCount/unreadItemCount) for `folder` (name/path) or `folderId`, suitable for pagination planning. action=`delete` removes a folder (by `folderName`/path or `folderId`) and its contents — on Outlook.com the folder is moved to Deleted Items (recoverable until you empty it); M365/Exchange accounts may hard-delete per retention policy.",
|
|
15
|
+
"Manage mail folders (tool-level destructiveHint=true because `delete` permanently removes a folder; `list` and `stats` are read-only sub-actions despite the annotation). Folders can be addressed by name, by a slash-separated PATH for nested folders (e.g. `Triage/Delete`, `Inbox/Clients/Acme`, case-insensitive), or by explicit ID; `list` output includes each folder's full path and `[id: …]`. A bare name resolves a unique top-level folder first, then searches nested folders (ambiguous names return the candidates — disambiguate with a path or ID). action=`list` (default) returns the folder tree (toggle `includeItemCounts` for unread/total, `includeChildren` for hierarchy). action=`create` makes a new folder under the root, or under `parentFolder` (name/path) / `parentFolderId`, and returns its id. action=`move` relocates emails (`emailIds`) into `targetFolder` (name/path) or `targetFolderId`. action=`stats` returns counts (totalItemCount/unreadItemCount) for `folder` (name/path) or `folderId`, suitable for pagination planning. action=`delete` removes a folder (by `folderName`/path or `folderId`) and its contents — on Outlook.com the folder is moved to Deleted Items (recoverable until you empty it); M365/Exchange accounts may hard-delete per retention policy. Every action accepts `sharedMailbox` (alias `email`) to target a shared/delegated mailbox instead of the signed-in account — folder names, paths, and IDs are then resolved inside that mailbox. Protected folders cannot be deleted in any mailbox.",
|
|
16
16
|
annotations: {
|
|
17
17
|
title: 'Mail Folders',
|
|
18
18
|
readOnlyHint: false,
|
|
@@ -36,6 +36,16 @@ const folderTools = [
|
|
|
36
36
|
type: 'boolean',
|
|
37
37
|
description: 'Include child folders in hierarchy (action=list)',
|
|
38
38
|
},
|
|
39
|
+
// shared-mailbox scoping (all actions)
|
|
40
|
+
sharedMailbox: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
description:
|
|
43
|
+
'Email address of a shared/delegated mailbox to target instead of the signed-in account (all actions). 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
|
+
},
|
|
45
|
+
email: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
description: 'Alias for `sharedMailbox`.',
|
|
48
|
+
},
|
|
39
49
|
// create params
|
|
40
50
|
name: {
|
|
41
51
|
type: 'string',
|
package/folder/list.js
CHANGED
|
@@ -12,38 +12,44 @@ const { listChildFolders } = require('./resolve');
|
|
|
12
12
|
async function handleListFolders(args) {
|
|
13
13
|
const includeItemCounts = args.includeItemCounts === true;
|
|
14
14
|
const includeChildren = args.includeChildren === true;
|
|
15
|
+
// Target a shared/delegated mailbox instead of the signed-in account.
|
|
16
|
+
const sharedMailbox = args.sharedMailbox || args.email || null;
|
|
15
17
|
|
|
16
18
|
try {
|
|
17
19
|
// Get access token
|
|
18
20
|
const accessToken = await ensureAuthenticated();
|
|
19
21
|
|
|
20
22
|
// Get all mail folders
|
|
21
|
-
const folders = await getAllFoldersHierarchy(
|
|
23
|
+
const { folders, warnings } = await getAllFoldersHierarchy(
|
|
22
24
|
accessToken,
|
|
23
|
-
includeItemCounts
|
|
25
|
+
includeItemCounts,
|
|
26
|
+
sharedMailbox
|
|
24
27
|
);
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
type: 'text',
|
|
32
|
-
text: formatFolderHierarchy(folders, includeItemCounts),
|
|
33
|
-
},
|
|
34
|
-
],
|
|
35
|
-
};
|
|
36
|
-
} else {
|
|
37
|
-
// Otherwise, format as flat list
|
|
38
|
-
return {
|
|
39
|
-
content: [
|
|
40
|
-
{
|
|
41
|
-
type: 'text',
|
|
42
|
-
text: formatFolderList(folders, includeItemCounts),
|
|
43
|
-
},
|
|
44
|
-
],
|
|
45
|
-
};
|
|
29
|
+
let heading = sharedMailbox ? `\n\nMailbox: ${sharedMailbox}` : '';
|
|
30
|
+
// The walk can skip branches (permission errors, depth cap) — say so
|
|
31
|
+
// instead of presenting a partial tree as complete.
|
|
32
|
+
if (warnings.length > 0) {
|
|
33
|
+
heading += `\n\n**Partial listing — ${warnings.length} branch(es) incomplete:**\n${warnings.map((w) => `- ${w}`).join('\n')}`;
|
|
46
34
|
}
|
|
35
|
+
|
|
36
|
+
const body = includeChildren
|
|
37
|
+
? formatFolderHierarchy(folders, includeItemCounts)
|
|
38
|
+
: formatFolderList(folders, includeItemCounts);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
content: [
|
|
42
|
+
{
|
|
43
|
+
type: 'text',
|
|
44
|
+
text: body + heading,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
_meta: {
|
|
48
|
+
folderCount: folders.length,
|
|
49
|
+
partial: warnings.length > 0,
|
|
50
|
+
warnings,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
47
53
|
} catch (error) {
|
|
48
54
|
if (error.message === 'Authentication required') {
|
|
49
55
|
return {
|
|
@@ -71,9 +77,14 @@ async function handleListFolders(args) {
|
|
|
71
77
|
* Get all mail folders with hierarchy information
|
|
72
78
|
* @param {string} accessToken - Access token
|
|
73
79
|
* @param {boolean} includeItemCounts - Include item counts in response
|
|
74
|
-
* @
|
|
80
|
+
* @param {string|null} [sharedMailbox] - Shared mailbox email, or null for the signed-in account
|
|
81
|
+
* @returns {Promise<{folders: Array, warnings: Array<string>}>} - Folders plus any reasons the tree is incomplete
|
|
75
82
|
*/
|
|
76
|
-
async function getAllFoldersHierarchy(
|
|
83
|
+
async function getAllFoldersHierarchy(
|
|
84
|
+
accessToken,
|
|
85
|
+
includeItemCounts,
|
|
86
|
+
sharedMailbox = null
|
|
87
|
+
) {
|
|
77
88
|
// Determine select fields based on whether to include counts
|
|
78
89
|
const selectFields = includeItemCounts
|
|
79
90
|
? 'id,displayName,parentFolderId,childFolderCount,totalItemCount,unreadItemCount'
|
|
@@ -81,8 +92,14 @@ async function getAllFoldersHierarchy(accessToken, includeItemCounts) {
|
|
|
81
92
|
|
|
82
93
|
// Full recursive, paginated walk so nested folders at ANY depth appear with
|
|
83
94
|
// their complete path (not just one level). (#216 review)
|
|
84
|
-
const top = await listChildFolders(
|
|
95
|
+
const top = await listChildFolders(
|
|
96
|
+
accessToken,
|
|
97
|
+
null,
|
|
98
|
+
selectFields,
|
|
99
|
+
sharedMailbox
|
|
100
|
+
);
|
|
85
101
|
const all = [];
|
|
102
|
+
const warnings = [];
|
|
86
103
|
const visited = new Set();
|
|
87
104
|
const queue = top.map((folder) => ({
|
|
88
105
|
folder,
|
|
@@ -100,14 +117,28 @@ async function getAllFoldersHierarchy(accessToken, includeItemCounts) {
|
|
|
100
117
|
visited.add(folder.id);
|
|
101
118
|
all.push({ ...folder, path, parentFolder: parentPath, isTopLevel });
|
|
102
119
|
|
|
120
|
+
if (folder.childFolderCount > 0 && depth >= 20) {
|
|
121
|
+
warnings.push(
|
|
122
|
+
`Depth limit (20) reached at "${path}" [id: ${folder.id}] — its subfolders were not listed.`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
103
126
|
if (folder.childFolderCount > 0 && depth < 20) {
|
|
104
127
|
let children;
|
|
105
128
|
try {
|
|
106
|
-
children = await listChildFolders(
|
|
129
|
+
children = await listChildFolders(
|
|
130
|
+
accessToken,
|
|
131
|
+
folder.id,
|
|
132
|
+
selectFields,
|
|
133
|
+
sharedMailbox
|
|
134
|
+
);
|
|
107
135
|
} catch (error) {
|
|
108
136
|
console.error(
|
|
109
137
|
`Error getting child folders for "${folder.displayName}": ${error.message}`
|
|
110
138
|
);
|
|
139
|
+
warnings.push(
|
|
140
|
+
`Could not list subfolders of "${path}" [id: ${folder.id}]: ${error.message}`
|
|
141
|
+
);
|
|
111
142
|
continue;
|
|
112
143
|
}
|
|
113
144
|
for (const child of children) {
|
|
@@ -121,7 +152,7 @@ async function getAllFoldersHierarchy(accessToken, includeItemCounts) {
|
|
|
121
152
|
}
|
|
122
153
|
}
|
|
123
154
|
}
|
|
124
|
-
return all;
|
|
155
|
+
return { folders: all, warnings };
|
|
125
156
|
}
|
|
126
157
|
|
|
127
158
|
/**
|
|
@@ -272,3 +303,6 @@ function formatFolderHierarchy(folders, includeItemCounts) {
|
|
|
272
303
|
}
|
|
273
304
|
|
|
274
305
|
module.exports = handleListFolders;
|
|
306
|
+
// Named export so the shared-mailbox folder listing in `access-shared-mailbox`
|
|
307
|
+
// reuses this walk instead of carrying its own copy.
|
|
308
|
+
module.exports.getAllFoldersHierarchy = getAllFoldersHierarchy;
|