@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/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: "Email folder (default: 'inbox')",
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({ id: args.id, isRead: false });
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(accessToken, folder);
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);
@@ -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 = `me/messages/${emailId}`;
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 = `me/messages/${emailId}`;
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 = 'me/messages';
105
- console.error('Searching across all mail folders');
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
- 'me/messages',
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
- ? `me/mailFolders/${parent.id}/childFolders`
115
- : 'me/mailFolders';
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(accessToken, 'DELETE', `me/mailFolders/${resolved.id}`);
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
- // If including children, format as hierarchy
27
- if (includeChildren) {
28
- return {
29
- content: [
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
- * @returns {Promise<Array>} - Array of folder objects with hierarchy
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(accessToken, includeItemCounts) {
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(accessToken, null, selectFields);
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(accessToken, folder.id, selectFields);
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;