@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.
Files changed (67) hide show
  1. package/.env.example +30 -3
  2. package/README.md +67 -27
  3. package/advanced/index.js +44 -174
  4. package/auth/auth-errors.js +23 -1
  5. package/auth/oauth-server.js +7 -1
  6. package/auth/token-manager.js +7 -3
  7. package/auth/token-storage.js +28 -30
  8. package/auth/tools.js +61 -82
  9. package/calendar/attendees.js +36 -0
  10. package/calendar/cancel.js +9 -25
  11. package/calendar/create.js +42 -48
  12. package/calendar/decline.js +10 -25
  13. package/calendar/delete.js +10 -25
  14. package/calendar/index.js +20 -37
  15. package/calendar/list.js +4 -16
  16. package/calendar/preview.js +461 -0
  17. package/calendar/update.js +55 -83
  18. package/categories/index.js +68 -265
  19. package/config.js +29 -1
  20. package/contacts/index.js +72 -128
  21. package/email/attachments.js +43 -125
  22. package/email/conversations.js +44 -78
  23. package/email/delta.js +69 -46
  24. package/email/draft.js +170 -103
  25. package/email/export.js +145 -110
  26. package/email/folder-utils.js +3 -2
  27. package/email/headers.js +11 -49
  28. package/email/index.js +86 -110
  29. package/email/list.js +4 -17
  30. package/email/mail-tips.js +86 -57
  31. package/email/mark-as-read.js +13 -49
  32. package/email/mime.js +39 -51
  33. package/email/read.js +16 -50
  34. package/email/search.js +47 -87
  35. package/email/send.js +82 -48
  36. package/folder/create.js +6 -25
  37. package/folder/delete.js +117 -38
  38. package/folder/index.js +19 -17
  39. package/folder/list.js +5 -17
  40. package/folder/move.js +13 -42
  41. package/folder/resolve.js +11 -6
  42. package/folder/stats.js +18 -27
  43. package/index.js +39 -45
  44. package/llms-install.md +22 -4
  45. package/llms.txt +20 -11
  46. package/outlook-auth-server.js +10 -3
  47. package/package.json +4 -1
  48. package/request-handler.js +217 -116
  49. package/rules/create.js +28 -71
  50. package/rules/index.js +52 -93
  51. package/rules/list.js +7 -19
  52. package/rules/rule-builder.js +59 -22
  53. package/rules/update.js +27 -61
  54. package/server.js +41 -0
  55. package/settings/index.js +162 -145
  56. package/tools.js +30 -0
  57. package/utils/field-presets.js +4 -2
  58. package/utils/graph-api.js +65 -22
  59. package/utils/logger.js +251 -0
  60. package/utils/mock-data.js +91 -2
  61. package/utils/read-only.js +59 -0
  62. package/utils/response-formatter.js +54 -15
  63. package/utils/risk-classes.js +324 -0
  64. package/utils/safe-write.js +372 -6
  65. package/utils/safety.js +247 -42
  66. package/utils/server-instructions.js +73 -0
  67. package/utils/tool-error.js +33 -0
package/email/index.js CHANGED
@@ -27,20 +27,16 @@ const handleDraft = require('./draft');
27
27
 
28
28
  // Import flag handlers from advanced module
29
29
  const { handleSetMessageFlag, handleClearMessageFlag } = require('../advanced');
30
+ const { toolMetadata } = require('../utils/risk-classes');
31
+ const { toolError } = require('../utils/tool-error');
30
32
 
31
33
  // Consolidated email tool definitions (17 → 6)
32
34
  const emailTools = [
33
35
  {
34
36
  name: 'search-emails',
35
37
  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. 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
- annotations: {
38
- title: 'Search Emails',
39
- readOnlyHint: true,
40
- // openWorldHint: output includes email content authored by external
41
- // senders (bodies/previews/threads) — may contain prompt-injection. (#92)
42
- openWorldHint: true,
43
- },
38
+ 'Search, list, delta-sync or thread-group emails (read-only); parameters set the mode. No params: recent emails in `folder` (default `inbox`). `query`/`from`/`to`/`subject`/date filters: search, combined as an OData filter. `searchExpression` (deprecated alias `kqlQuery`): a raw Graph `$search` expression. `deltaMode: true`: current state plus a `deltaToken` to pass back next time for changes only. `groupByConversation: true`: conversation threads. `conversationId`: every message in one thread. `internetMessageId`: the message with that RFC Message-ID. `sharedMailbox` (alias `email`) searches a shared/delegated mailbox, custom folders and nested paths included. Personal Outlook.com accounts have limited `$search`, so the tool falls back to OData filters and a recent listing automatically; structured filters (`from`/`subject`/`receivedAfter`/`hasAttachments`/`unreadOnly`) give cleaner results there. Returns up to `count` messages (id/subject/from/receivedDateTime/preview); `outputVerbosity` expands them.',
39
+ ...toolMetadata('search-emails', 'Search Emails'),
44
40
  inputSchema: {
45
41
  type: 'object',
46
42
  properties: {
@@ -63,7 +59,7 @@ const emailTools = [
63
59
  groupByConversation: {
64
60
  type: 'boolean',
65
61
  description:
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.',
62
+ 'List conversations (threads) grouped by conversationId, not individual emails. Honors `sharedMailbox`/`email` (and custom `folder` paths) to group within a shared/delegated mailbox.',
67
63
  },
68
64
  // Search/list params
69
65
  query: {
@@ -79,7 +75,7 @@ const emailTools = [
79
75
  kqlQuery: {
80
76
  type: 'string',
81
77
  description:
82
- 'DEPRECATED alias for `searchExpression` (this was never full KQL — it is a Graph `$search` expression). Prefer `searchExpression`.',
78
+ 'DEPRECATED alias for `searchExpression` (this was never full KQL — it is a Graph `$search` expression).',
83
79
  },
84
80
  folder: {
85
81
  type: 'string',
@@ -89,7 +85,7 @@ const emailTools = [
89
85
  sharedMailbox: {
90
86
  type: 'string',
91
87
  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).',
88
+ 'Email address of a shared/delegated mailbox to search (default: 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
89
  },
94
90
  email: {
95
91
  type: 'string',
@@ -131,7 +127,7 @@ const emailTools = [
131
127
  count: {
132
128
  type: 'number',
133
129
  description:
134
- 'Number of results (list default: 25, search default: 10, max: 50)',
130
+ 'Number of results (list default: 25, search default: 10, max: 50). There is no page cursor: when the result says more emails are available, raise `count` or narrow `receivedAfter`/`receivedBefore`.',
135
131
  },
136
132
  outputVerbosity: {
137
133
  type: 'string',
@@ -200,13 +196,8 @@ const emailTools = [
200
196
  {
201
197
  name: 'read-email',
202
198
  description:
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.',
204
- annotations: {
205
- title: 'Read Email',
206
- readOnlyHint: true,
207
- // openWorldHint: returns full message body from external senders. (#92)
208
- openWorldHint: true,
209
- },
199
+ 'Read a single email by id (read-only). Returns subject, from/to/cc, date and the body as Markdown (HTML stripped to text): up to 2,000 characters by default, up to 40,000 with `outputVerbosity: full`; a cut body ends with a note on how to get the rest. With `headersMode: true`: returns RFC-822 forensic headers in place of the body (DKIM, SPF, DMARC, Received chain, Message-ID, Authentication-Results) — `importantOnly: true` for the security-relevant subset, `groupByType: true` for a category-bucketed view, `raw: true` for JSON. With `includeHeaders: true` (non-headers-mode): adds basic headers alongside the body. **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.',
200
+ ...toolMetadata('read-email', 'Read Email'),
210
201
  inputSchema: {
211
202
  type: 'object',
212
203
  properties: {
@@ -226,7 +217,7 @@ const emailTools = [
226
217
  headersMode: {
227
218
  type: 'boolean',
228
219
  description:
229
- 'Return forensic headers instead of email content (default: false)',
220
+ 'Return forensic headers in place of the email content (default: false)',
230
221
  },
231
222
  includeHeaders: {
232
223
  type: 'boolean',
@@ -236,7 +227,8 @@ const emailTools = [
236
227
  outputVerbosity: {
237
228
  type: 'string',
238
229
  enum: ['minimal', 'standard', 'full'],
239
- description: 'Output detail level (default: standard)',
230
+ description:
231
+ 'Output detail level (default: standard). minimal: body preview only; standard: body up to 2,000 characters; full: adds IDs, body up to 40,000 characters. For a longer body, export it with `export` target=message.',
240
232
  },
241
233
  // Headers mode params
242
234
  groupByType: {
@@ -252,7 +244,7 @@ const emailTools = [
252
244
  raw: {
253
245
  type: 'boolean',
254
246
  description:
255
- 'Return raw JSON instead of Markdown (headersMode only, default: false)',
247
+ 'Return the headers as raw JSON, not Markdown (headersMode only, default: false)',
256
248
  },
257
249
  },
258
250
  additionalProperties: false,
@@ -268,14 +260,8 @@ const emailTools = [
268
260
  {
269
261
  name: 'send-email',
270
262
  description:
271
- 'Compose and send an email immediately (destructive: sends external comms). Returns a confirmation with the saved-message id. Safety controls: `dryRun: true` returns the composed message for review without sending; `checkRecipients: true` runs `get-mail-tips` first to flag out-of-office / mailbox-full / delivery-restricted / external recipients; combine both for a full pre-send review. Subject to session rate limits (`OUTLOOK_MAX_EMAILS_PER_SESSION` env) and recipient allowlist (`OUTLOOK_ALLOWED_RECIPIENTS` env) when configured — calls outside the allowlist fail before any Graph request. For multi-step compose/review workflows prefer `draft` (action=`create` → `update` → `send`) since drafts can be inspected in Outlook before sending. Comma-separated recipient strings or arrays both accepted.',
272
- annotations: {
273
- title: 'Send Email',
274
- readOnlyHint: false,
275
- destructiveHint: true,
276
- idempotentHint: false,
277
- openWorldHint: true,
278
- },
263
+ 'Compose and send an email immediately (destructive: sends external comms). Returns a confirmation. Safety controls: `dryRun: true` returns the composed message for review without sending; `checkRecipients: true` runs `get-mail-tips` first and returns its warnings. If the tips show an out-of-office reply, a full mailbox, a delivery restriction or external recipients, the send is refused until repeated with `acknowledgeWarnings: true`. Personal Outlook.com accounts return no tips, and no warnings is not proof of delivery. Subject to the session limit (`OUTLOOK_MAX_SEND_EMAIL_PER_SESSION`, else `OUTLOOK_MAX_EMAILS_PER_SESSION`; 0 refuses every send) and recipient allowlist (`OUTLOOK_ALLOWED_RECIPIENTS`) when configured; both refuse before any Graph request. For a review-before-send workflow, use `draft` (action=`create` → `update` → `send`); a draft can be checked in Outlook before it goes. Comma-separated recipient strings or arrays both accepted.',
264
+ ...toolMetadata('send-email', 'Send Email'),
279
265
  inputSchema: {
280
266
  type: 'object',
281
267
  properties: {
@@ -316,7 +302,13 @@ const emailTools = [
316
302
  checkRecipients: {
317
303
  type: 'boolean',
318
304
  description:
319
- 'Check recipients for out-of-office, mailbox full, delivery restrictions before sending (default: false). Combine with dryRun=true for pre-send review.',
305
+ 'Check recipients with mail tips before sending (default: false). Out-of-office, mailbox full, delivery restrictions or external recipients refuse the send unless acknowledgeWarnings=true. Combine with dryRun=true for pre-send review.',
306
+ },
307
+ acknowledgeWarnings: {
308
+ type: 'boolean',
309
+ default: false,
310
+ description:
311
+ 'Send even though checkRecipients flagged an out-of-office reply, a full mailbox, a delivery restriction or external recipients (default: false). Without it those warnings refuse the send. Pass only after the user has seen the warnings. No effect without checkRecipients.',
320
312
  },
321
313
  },
322
314
  additionalProperties: false,
@@ -327,14 +319,8 @@ const emailTools = [
327
319
  {
328
320
  name: 'draft',
329
321
  description:
330
- 'Full draft lifecycle for review-before-send workflows (destructive: covers `send` and `delete`). action=`create` saves a new draft in the Drafts folder and returns its id (use `dryRun: true` to preview without saving; `checkRecipients: true` runs mail-tips first). action=`update` patches an existing draft by `id` (only fields passed are changed). action=`send` dispatches an existing draft — shares the rate limit with `send-email`. action=`delete` deletes a draft: it skips Deleted Items and goes to Recoverable Items (restorable for a limited time, depending on your account, via "Recover deleted items" in Outlook). update/send/delete refuse any `id` that is not an unsent draft (received or sent messages are never changed). action=`reply`/`reply-all` creates a reply draft from a message `id` (use `comment` to prepend text — mutually exclusive with `body`). action=`forward` creates a forward draft (requires `id` and `to`). Recipient allowlist applies to create/update/forward. Returns the draft object on create/update/reply/forward; status confirmation on send/delete.',
331
- annotations: {
332
- title: 'Draft Operations',
333
- readOnlyHint: false,
334
- destructiveHint: true,
335
- idempotentHint: false,
336
- openWorldHint: true,
337
- },
322
+ "Draft lifecycle for review-before-send workflows (destructive: covers `send` and `delete`). action=`create` saves a new draft and returns its id (`dryRun: true` previews without saving; `checkRecipients: true` runs mail-tips first). action=`update` patches a draft by `id` (only fields passed change). action=`send` sends a draft and counts against the `send-email` session limit (0 refuses every send). action=`delete` moves a draft to Recoverable Items (restorable for a limited time). update/send/delete refuse any `id` that is not an unsent draft. action=`reply`/`reply-all` creates a reply draft from a message `id` (`comment` prepends text; not with `body`). action=`forward` creates a forward draft (needs `id` and `to`). The recipient allowlist (OUTLOOK_ALLOWED_RECIPIENTS) applies to create/update/forward, to the draft's current to/cc/bcc on send, and to reply/reply-all, whose draft is deleted if a recipient is not allowed. Returns the draft on create/update/reply/forward; a status on send/delete.",
323
+ ...toolMetadata('draft', 'Draft Operations'),
338
324
  inputSchema: {
339
325
  type: 'object',
340
326
  properties: {
@@ -390,7 +376,7 @@ const emailTools = [
390
376
  dryRun: {
391
377
  type: 'boolean',
392
378
  description:
393
- 'Preview draft without saving (action=create only, default: false)',
379
+ 'Preview only (action=create): shows the draft without saving it. Other actions refuse dryRun and change nothing. Default false.',
394
380
  },
395
381
  checkRecipients: {
396
382
  type: 'boolean',
@@ -406,14 +392,8 @@ const emailTools = [
406
392
  {
407
393
  name: 'update-email',
408
394
  description:
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 with a time: a value with `Z` or a ±hh:mm offset is kept as that exact instant; a value without one is read in the configured default timezone (OUTLOOK_DEFAULT_TIMEZONE); date-only or unparseable values are refused before any change). With only `dueDateTime`, the start defaults to 09:00 on the due date in the default timezone, or to the due time if that is earlier. action=`unflag` clears the flag. action=`complete` marks the flag as done. Flag/unflag/complete accept either `id` (single) or `ids` (batch array) — messages in a batch are updated one at a time (one PATCH each, not Graph `$batch`). 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.',
410
- annotations: {
411
- title: 'Update Email',
412
- readOnlyHint: false,
413
- destructiveHint: false,
414
- idempotentHint: true,
415
- openWorldHint: false,
416
- },
395
+ 'Update message state without modifying content (idempotent — safe to retry). action=`mark-read`/`mark-unread` sets `isRead` on a single message by `id`. action=`flag` sets a follow-up flag with optional `dueDateTime`/`startDateTime`: ISO 8601 with a time, kept as that exact instant when it has `Z` or a ±hh:mm offset and read in OUTLOOK_DEFAULT_TIMEZONE when it has none; date-only or unparseable values are refused before any change. With only `dueDateTime`, the start is 09:00 on the due date, or the due time if earlier. action=`unflag` clears the flag; action=`complete` marks it done. Flag/unflag/complete take `id` (single) or `ids` (batch, updated one at a time: one PATCH each, not Graph `$batch`). `sharedMailbox` (alias `email`) updates messages in a shared/delegated mailbox (default: the signed-in account; needs Mail.ReadWrite.Shared and delegate access). Returns a status per message.',
396
+ ...toolMetadata('update-email', 'Update Email'),
417
397
  inputSchema: {
418
398
  type: 'object',
419
399
  properties: {
@@ -425,7 +405,7 @@ const emailTools = [
425
405
  id: {
426
406
  type: 'string',
427
407
  description:
428
- 'Single message ID (required for mark-read/mark-unread, or use instead of ids for flag actions)',
408
+ 'Single message ID (required for mark-read/mark-unread; flag actions take `id` or `ids`)',
429
409
  },
430
410
  ids: {
431
411
  type: 'array',
@@ -447,7 +427,7 @@ const emailTools = [
447
427
  sharedMailbox: {
448
428
  type: 'string',
449
429
  description:
450
- '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).',
430
+ 'Email address of the shared/delegated mailbox whose message(s) to update (default: 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).',
451
431
  },
452
432
  email: {
453
433
  type: 'string',
@@ -491,29 +471,17 @@ const emailTools = [
491
471
  sharedMailbox,
492
472
  });
493
473
  default:
494
- return {
495
- content: [
496
- {
497
- type: 'text',
498
- text: "Invalid action. Use 'mark-read', 'mark-unread', 'flag', 'unflag', or 'complete'.",
499
- },
500
- ],
501
- };
474
+ return toolError(
475
+ "Invalid action. Use 'mark-read', 'mark-unread', 'flag', 'unflag', or 'complete'."
476
+ );
502
477
  }
503
478
  },
504
479
  },
505
480
  {
506
481
  name: 'attachments',
507
482
  description:
508
- '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.',
509
- annotations: {
510
- title: 'Attachments',
511
- readOnlyHint: false,
512
- destructiveHint: false,
513
- // openWorldHint: action=view returns attachment content supplied by
514
- // external senders. (#92)
515
- openWorldHint: true,
516
- },
483
+ '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 under a new, unique name in `outputDir` (default system temp directory, auto-created; must be inside the temp directory, ~/Downloads, ~/Documents or OUTLOOK_EXPORT_DIR) 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.',
484
+ ...toolMetadata('attachments', 'Attachments'),
517
485
  inputSchema: {
518
486
  type: 'object',
519
487
  properties: {
@@ -542,7 +510,7 @@ const emailTools = [
542
510
  outputDir: {
543
511
  type: 'string',
544
512
  description:
545
- 'Directory to save file (action=download, default: system tmpdir). Auto-created if missing.',
513
+ 'Absolute directory (or ~/…) to save the file in (action=download, default: system temp directory). Auto-created if missing. Must be inside the system temp directory, ~/Downloads, ~/Documents or OUTLOOK_EXPORT_DIR, with no dot-prefixed folder names.',
546
514
  },
547
515
  savePath: {
548
516
  type: 'string',
@@ -563,29 +531,17 @@ const emailTools = [
563
531
  case 'list':
564
532
  return handleListAttachments(args);
565
533
  default:
566
- return {
567
- content: [
568
- {
569
- type: 'text',
570
- text: `Unknown action '${action}'. Valid actions: list, view, download.`,
571
- },
572
- ],
573
- };
534
+ return toolError(
535
+ `Unknown action '${action}'. Valid actions: list, view, download.`
536
+ );
574
537
  }
575
538
  },
576
539
  },
577
540
  {
578
541
  name: 'export',
579
542
  description:
580
- '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 (up to 1000 messages) 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.',
581
- annotations: {
582
- title: 'Export Emails',
583
- readOnlyHint: false,
584
- destructiveHint: false,
585
- // openWorldHint: exports full message/MIME/conversation content from
586
- // external senders. (#92)
587
- openWorldHint: true,
588
- },
543
+ 'Export emails to files. target=`message` (default) exports one email by `id` (mime/eml/markdown/json/csv) to `savePath`: a directory (or a path ending in `/`, created if missing) gets a new, unique file name; a file path is created new and an existing file is replaced only with `overwrite: true`. target=`messages` batch-exports `emailIds`, or matches for `searchQuery`/`query`, into `outputDir`, at most 100 messages per call. target=`conversation` exports a thread (up to 1000 messages) by `conversationId` into `outputDir` (eml/mbox/markdown/json/html/csv; `order: "reverse"` for newest first). target=`mime` returns raw RFC-822 MIME for `id` (`headersOnly`, `base64`, `maxSize`, default 1MB). Files are written only inside the system temp directory (the default), ~/Downloads, ~/Documents or OUTLOOK_EXPORT_DIR, never to dot-prefixed names. Pass `sharedMailbox` (alias `email`) when the ids come from a shared mailbox. `includeAttachments` defaults to true for one message, false for batch.',
544
+ ...toolMetadata('export', 'Export Emails'),
589
545
  inputSchema: {
590
546
  type: 'object',
591
547
  properties: {
@@ -603,11 +559,17 @@ const emailTools = [
603
559
  type: 'string',
604
560
  enum: ['mime', 'eml', 'markdown', 'json', 'mbox', 'html', 'csv'],
605
561
  description:
606
- 'Export format. Valid values vary by target: target=message accepts mime/eml/markdown/json/csv (mbox and html are conversation-only). target=conversation accepts eml/mbox/markdown/json/html/csv. target=messages (batch) accepts markdown/json/csv. mime is an alias for eml (same RFC822 bytes, .eml extension on disk).',
562
+ 'Export format. Valid values vary by target: target=message accepts mime/eml/markdown/json/csv (mbox and html are conversation-only). target=conversation accepts eml/mbox/markdown/json/html/csv. target=messages (batch) accepts mime/eml/markdown/json (one file per message) or csv (one file). mime is an alias for eml (same RFC822 bytes, .eml extension on disk).',
607
563
  },
608
564
  savePath: {
609
565
  type: 'string',
610
- description: 'File path or directory (target=message)',
566
+ description:
567
+ 'Absolute file path or directory, or one starting with ~/ (target=message). Relative paths are refused. Must be inside the system temp directory, ~/Downloads, ~/Documents or OUTLOOK_EXPORT_DIR. An existing file is not replaced unless overwrite is true.',
568
+ },
569
+ overwrite: {
570
+ type: 'boolean',
571
+ description:
572
+ 'Replace an existing file at savePath (target=message, default: false). Never replaces a symlink, a hard-linked file, a dotfile, or a file in a dot-directory below the allowed folder.',
611
573
  },
612
574
  includeAttachments: {
613
575
  type: 'boolean',
@@ -618,20 +580,43 @@ const emailTools = [
618
580
  emailIds: {
619
581
  type: 'array',
620
582
  items: { type: 'string' },
621
- description: 'Email IDs to export (target=messages)',
583
+ description:
584
+ 'Email IDs to export (target=messages). At most 100 per call: any beyond the first 100 are left out, and the result says how many.',
622
585
  },
623
586
  searchQuery: {
624
587
  type: 'object',
625
588
  properties: {
626
- folder: { type: 'string' },
627
- from: { type: 'string' },
628
- subject: { type: 'string' },
629
- receivedAfter: { type: 'string' },
630
- receivedBefore: { type: 'string' },
631
- maxResults: { type: 'number' },
589
+ folder: {
590
+ type: 'string',
591
+ description:
592
+ 'Folder to search (default: inbox): a well-known name, display name, `Parent/Child` path or folder ID',
593
+ },
594
+ from: {
595
+ type: 'string',
596
+ description: 'Sender address or name to match (Graph `$search`)',
597
+ },
598
+ subject: {
599
+ type: 'string',
600
+ description: 'Subject text to match (Graph `$search`)',
601
+ },
602
+ receivedAfter: {
603
+ type: 'string',
604
+ description:
605
+ 'Only messages received at or after this date/time (ISO 8601)',
606
+ },
607
+ receivedBefore: {
608
+ type: 'string',
609
+ description:
610
+ 'Only messages received at or before this date/time (ISO 8601)',
611
+ },
612
+ maxResults: {
613
+ type: 'number',
614
+ description:
615
+ 'Most messages to export (default: 25, max: 100 per call). Newest first, or by relevance when `from`/`subject` is set.',
616
+ },
632
617
  },
633
618
  description:
634
- 'Search query to find emails (target=messages, alternative to emailIds)',
619
+ 'Search to find emails (target=messages, alternative to emailIds)',
635
620
  },
636
621
  query: {
637
622
  type: 'string',
@@ -641,7 +626,7 @@ const emailTools = [
641
626
  outputDir: {
642
627
  type: 'string',
643
628
  description:
644
- 'Output directory (target=messages/conversation, required)',
629
+ 'Absolute output directory, or one starting with ~/ (target=messages, required; target=message/conversation, default: system temp directory). Must be inside the system temp directory, ~/Downloads, ~/Documents or OUTLOOK_EXPORT_DIR.',
645
630
  },
646
631
  // Conversation export
647
632
  conversationId: {
@@ -657,7 +642,7 @@ const emailTools = [
657
642
  sharedMailbox: {
658
643
  type: 'string',
659
644
  description:
660
- '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).',
645
+ 'Email address of a shared/delegated mailbox to export from (default: 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).',
661
646
  },
662
647
  email: {
663
648
  type: 'string',
@@ -692,14 +677,9 @@ const emailTools = [
692
677
  case 'message':
693
678
  return handleExportEmail(args);
694
679
  default:
695
- return {
696
- content: [
697
- {
698
- type: 'text',
699
- text: `Unknown export target '${target}'. Valid targets: message, messages, conversation, mime.`,
700
- },
701
- ],
702
- };
680
+ return toolError(
681
+ `Unknown export target '${target}'. Valid targets: message, messages, conversation, mime.`
682
+ );
703
683
  }
704
684
  },
705
685
  },
@@ -707,11 +687,7 @@ const emailTools = [
707
687
  name: 'get-mail-tips',
708
688
  description:
709
689
  'Pre-send recipient validation via Graph `POST /me/getMailTips` (read-only; uses the existing `Mail.Read` scope — no extra permissions). Returns per-recipient tips covering automatic replies (out-of-office), mailbox full status, custom admin mail tips, delivery restrictions, moderation requirements, external-vs-internal scope, max message size, and group member counts (total + external). Use ahead of `send-email` or `draft` action=`create` to catch issues like OOO replies or external-recipient warnings before the message goes out; `send-email`/`draft` accept `checkRecipients: true` to invoke this automatically. Accepts either a comma-separated string or an array of addresses; `tipTypes` filters which tips are requested (defaults to all).',
710
- annotations: {
711
- title: 'Mail Tips',
712
- readOnlyHint: true,
713
- openWorldHint: false,
714
- },
690
+ ...toolMetadata('get-mail-tips', 'Mail Tips'),
715
691
  inputSchema: {
716
692
  type: 'object',
717
693
  properties: {
package/email/list.js CHANGED
@@ -16,6 +16,7 @@ const {
16
16
  DEFAULT_LIMITS,
17
17
  } = require('../utils/response-formatter');
18
18
  const { getEmailFields } = require('../utils/field-presets');
19
+ const { toolError, authRequiredError } = require('../utils/tool-error');
19
20
 
20
21
  /**
21
22
  * Maps verbosity level to field preset
@@ -101,7 +102,7 @@ async function handleListEmails(args) {
101
102
  const meta = {
102
103
  returned: response.value.length,
103
104
  totalAvailable: response['@odata.count'] || null,
104
- hasMore: Boolean(response['@odata.nextLink']),
105
+ hasMore: Boolean(response.hasMore || response['@odata.nextLink']),
105
106
  verbosity: verbosity,
106
107
  };
107
108
 
@@ -124,24 +125,10 @@ async function handleListEmails(args) {
124
125
  };
125
126
  } catch (error) {
126
127
  if (error.message === 'Authentication required') {
127
- return {
128
- content: [
129
- {
130
- type: 'text',
131
- text: "Authentication required. Please use the 'authenticate' tool first.",
132
- },
133
- ],
134
- };
128
+ return authRequiredError();
135
129
  }
136
130
 
137
- return {
138
- content: [
139
- {
140
- type: 'text',
141
- text: `Error listing emails: ${error.message}`,
142
- },
143
- ],
144
- };
131
+ return toolError(`Error listing emails: ${error.message}`);
145
132
  }
146
133
  }
147
134