apple-mail-mcp 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -299,14 +299,17 @@ What routes to IMAP when an account is IMAP-configured:
299
299
  - **Read:** `search-messages`, `list-messages` (server-side `SEARCH`, typically sub-second), and `get-message`.
300
300
  - **Folder ops:** `create-mailbox`, `rename-mailbox`, `delete-mailbox` — IMAP's `CREATE`/`RENAME`/`DELETE` succeed on the iCloud/Gmail/Workspace/Exchange mailboxes Mail.app's AppleScript bridge can't touch (#42).
301
301
  - **Message mutations:** `mark-as-read`/`unread`, `flag-message`/`unflag-message`, `move-message`, `delete-message`.
302
+ - **Batch mutations (2.1):** `batch-mark-as-read`/`unread`, `batch-flag`/`unflag-messages`, `batch-move-messages`, `batch-delete-messages` — `imap:` ids are grouped by mailbox and applied as a single `UID STORE`/`UID MOVE`; numeric ids in the same batch still use AppleScript.
303
+ - **Counts & stats (2.1):** `get-unread-count` and `list-mailboxes` use `STATUS`; `get-mail-stats` (with an `account`) uses `STATUS` + `SEARCH SINCE` — authoritative and fast even on huge mailboxes.
304
+ - **Attachments (2.1):** `list-attachments`, `save-attachment`, `fetch-attachment` use `BODYSTRUCTURE` + `FETCH BODY[part]` for `imap:` ids — faster and able to see MIME-embedded attachments AppleScript misses.
305
+ - **Threading (2.1):** `get-thread` links a conversation via `References`/`Message-ID` (`HEADER SEARCH`) for an `imap:` seed, falling back to subject grouping otherwise.
302
306
 
303
307
  **Message ids are backend-tagged.** The IMAP read path emits self-describing ids
304
308
  of the form `imap:<token>` (the token encodes the account, mailbox path, and
305
- UID). Pass that id back to `get-message` or any message mutation and it routes to
306
- IMAP automatically; bare numeric ids continue to use AppleScript. So an agent
307
- never has to know which backend a message came from the id carries it. (Batch
308
- operations remain AppleScript-only and accept numeric ids; use the single-message
309
- tools for IMAP ids.)
309
+ UID). Pass that id back to `get-message`, a message mutation, a batch op, or the
310
+ attachment/thread tools and it routes to IMAP automatically; bare numeric ids
311
+ continue to use AppleScript. So an agent never has to know which backend a
312
+ message came from the id carries it.
310
313
 
311
314
  Routing is conservative: only a call whose explicit `account` matches the
312
315
  configured IMAP account goes to IMAP; everything else falls through to
@@ -331,25 +334,85 @@ Each entry accepts `account`, `user`, `host`, `port`, `password`, `keychainServi
331
334
  `keychainAccount`. Calls route to the account matching their `account` argument (or the
332
335
  decoded `imap:` id), and each account keeps its own pooled connection.
333
336
 
334
- **Push notifications (B5):** with `APPLE_MAIL_MCP_IMAP_IDLE=1`, the server watches each
335
- account's INBOX and emits an MCP logging message + `notifications/resources/updated` for
336
- `mail://mailboxes/{account}` when new mail arrives (real-time IDLE where the server
337
- supports it, polling fallback otherwise).
338
-
339
337
  As with SMTP, the password is read from the macOS **Keychain** by default (use
340
338
  an app-specific password for Gmail/Workspace/iCloud), so no secret goes in
341
339
  config. Gmail label semantics: common names (`All Mail`, `Sent`, `Trash`,
342
340
  `Spam`, `Important`, …) map to their `[Gmail]/…` IMAP paths automatically.
343
341
 
344
- > Note: each call currently opens its own IMAP connection (no pooling yet), so
345
- > expect a few seconds of connection overhead per call — the one remaining
346
- > follow-up on [#43](https://github.com/sweetrb/apple-mail-mcp/issues/43).
342
+ > Note: IMAP connections are pooled one kept-alive connection per account is
343
+ > reused across calls (verified with a NOOP, closed after `APPLE_MAIL_MCP_IMAP_IDLE_MS`
344
+ > of inactivity), so there's no per-call connection overhead ([#50](https://github.com/sweetrb/apple-mail-mcp/issues/50)).
347
345
  >
348
346
  > **iCloud:** set `APPLE_MAIL_MCP_IMAP_HOST=imap.mail.me.com`, `APPLE_MAIL_MCP_IMAP_USER`
349
347
  > to your iCloud address, `APPLE_MAIL_MCP_IMAP_ACCOUNT` to the Mail account name
350
348
  > (e.g. `iCloud`), and use an **app-specific password** (from appleid.apple.com)
351
349
  > stored in the Keychain.
352
350
 
351
+ ##### Configuration file (when the host strips `env`)
352
+
353
+ Some host apps (e.g. Claude Desktop) launch the MCP server with a scrubbed
354
+ environment and ignore the `env` block in their server config, so there's no way
355
+ to pass `APPLE_MAIL_MCP_*` settings through it. In that case, put them in a JSON
356
+ file the host doesn't manage — `APPLE_MAIL_MCP_CONFIG_FILE`, or by default
357
+ `~/Library/Application Support/apple-mail-mcp/config.json`:
358
+
359
+ ```json
360
+ {
361
+ "APPLE_MAIL_MCP_IMAP_USER": "you@gmail.com",
362
+ "APPLE_MAIL_MCP_IMAP_HOST": "imap.gmail.com",
363
+ "APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE": "imap.gmail.com",
364
+ "APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT": "you@gmail.com",
365
+ "APPLE_MAIL_MCP_IMAP_IDLE": "1"
366
+ }
367
+ ```
368
+
369
+ The server reads it at startup and merges values into the environment **without
370
+ overriding** anything already set there (so an explicit `env` still wins). Store
371
+ only non-secret config here — **passwords belong in the Keychain**, never in this
372
+ file.
373
+
374
+ ##### Push notifications (IMAP IDLE) — opt-in
375
+
376
+ When `APPLE_MAIL_MCP_IMAP_IDLE=1`, the server opens a dedicated, long-lived
377
+ connection to **each configured IMAP account** and watches its INBOX for new
378
+ mail. On arrival it pushes two MCP notifications to the client (no polling by the
379
+ client required):
380
+
381
+ 1. **`notifications/message`** (logging) — a human-readable line, e.g.
382
+ `New mail in "Work": 2 new message(s) (INBOX now 1843).`
383
+ 2. **`notifications/resources/updated`** — for the affected account's resource
384
+ `mail://mailboxes/{account}`, so a client subscribed to that resource knows to
385
+ re-read it.
386
+
387
+ This requires an IMAP account to be configured (single-account env or
388
+ `APPLE_MAIL_MCP_IMAP_ACCOUNTS`); accounts that only use AppleScript aren't
389
+ watched. Detection is **real-time** via the IMAP IDLE `EXISTS` event where the
390
+ server pushes it, with an automatic **polling fallback** for servers that don't.
391
+ Dropped connections reconnect with backoff, and the watchers shut down cleanly on
392
+ `SIGINT`/`SIGTERM`.
393
+
394
+ Enable it in your MCP client config alongside the IMAP settings:
395
+
396
+ ```jsonc
397
+ {
398
+ "mcpServers": {
399
+ "apple-mail": {
400
+ "command": "node",
401
+ "args": ["/path/to/apple-mail-mcp/build/index.js"],
402
+ "env": {
403
+ "APPLE_MAIL_MCP_IMAP_USER": "you@gmail.com",
404
+ "APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE": "imap.gmail.com",
405
+ "APPLE_MAIL_MCP_IMAP_IDLE": "1"
406
+ }
407
+ }
408
+ }
409
+ }
410
+ ```
411
+
412
+ > Note: this is most useful with clients that surface MCP logging messages or
413
+ > subscribe to resource-update notifications. Clients that ignore notifications
414
+ > are unaffected — the feature is opt-in and adds no behavior unless enabled.
415
+
353
416
  ---
354
417
 
355
418
  #### `send-serial-email`
@@ -427,16 +490,6 @@ Return an attachment's bytes as base64 (the read counterpart to inline-base64 se
427
490
 
428
491
  **Returns:** The attachment bytes, base64-encoded (also in `structuredContent.contentBase64`).
429
492
 
430
- #### `create-rule` / `delete-rule`
431
-
432
- Create a Mail rule with conditions and actions, or delete a rule by name.
433
-
434
- `create-rule` parameters: `name` (string), `conditions` (array of `{field: from|to|cc|subject|content, operator: contains|notContains|equals|beginsWith|endsWith, value}`), `actions` (`{markRead?, markFlagged?, delete?, moveTo?, moveToAccount?}`), `matchAll` (default true), `enabled` (default true). `delete-rule` parameters: `name`.
435
-
436
- #### `doctor`
437
-
438
- Run a full setup diagnostic: Mail.app automation permission, account state (flags disabled accounts), and each configured IMAP/SMTP backend, each reported as ok / warn / fail with an actionable message. No parameters.
439
-
440
493
  ---
441
494
 
442
495
  #### `reply-to-message`
@@ -674,6 +727,41 @@ Enable or disable a mail rule.
674
727
 
675
728
  ---
676
729
 
730
+ #### `create-rule`
731
+
732
+ Create a Mail rule with one or more conditions and actions.
733
+
734
+ | Parameter | Type | Required | Description |
735
+ |-----------|------|----------|-------------|
736
+ | `name` | string | Yes | Rule name (must be unique) |
737
+ | `conditions` | object[] | Yes | One or more `{field, operator, value}` (see below) |
738
+ | `actions` | object | Yes | At least one of `markRead`, `markFlagged`, `delete`, `moveTo` |
739
+ | `matchAll` | boolean | No | `true` (default) = all conditions must match; `false` = any |
740
+ | `enabled` | boolean | No | Whether the rule is enabled on creation (default `true`) |
741
+
742
+ Each condition is `{ field, operator, value }` where `field` is one of `from`, `to`, `cc`, `subject`, `content` and `operator` is one of `contains`, `notContains`, `equals`, `beginsWith`, `endsWith`. Actions: `markRead` / `markFlagged` / `delete` (booleans), `moveTo` (mailbox name) with optional `moveToAccount`.
743
+
744
+ **Example:**
745
+ ```json
746
+ {
747
+ "name": "Newsletters",
748
+ "conditions": [{ "field": "from", "operator": "contains", "value": "newsletter" }],
749
+ "actions": { "markRead": true, "moveTo": "Reading" }
750
+ }
751
+ ```
752
+
753
+ ---
754
+
755
+ #### `delete-rule`
756
+
757
+ Delete a mail rule by name.
758
+
759
+ | Parameter | Type | Required | Description |
760
+ |-----------|------|----------|-------------|
761
+ | `name` | string | Yes | Rule name |
762
+
763
+ ---
764
+
677
765
  ### Contacts
678
766
 
679
767
  #### `search-contacts`
@@ -691,7 +779,7 @@ Search contacts in Contacts.app.
691
779
 
692
780
  ### Templates
693
781
 
694
- Email templates are stored in memory for the duration of the server session.
782
+ Email templates are **persisted to disk** so they survive server restarts, stored as JSON at `APPLE_MAIL_MCP_TEMPLATES_FILE` (default `~/Library/Application Support/apple-mail-mcp/templates.json`).
695
783
 
696
784
  #### `save-template`
697
785
 
@@ -762,6 +850,16 @@ Verify Mail.app connectivity and permissions.
762
850
 
763
851
  ---
764
852
 
853
+ #### `doctor`
854
+
855
+ Run a full setup diagnostic: Mail.app automation permission, account state (flagging disabled accounts), and each configured IMAP/SMTP backend — each reported as ok / warn / fail with an actionable message.
856
+
857
+ **Parameters:** None
858
+
859
+ **Returns:** A per-check report (`structuredContent` carries the raw `{healthy, checks[]}`).
860
+
861
+ ---
862
+
765
863
  #### `get-mail-stats`
766
864
 
767
865
  Get mail statistics.
package/build/index.js CHANGED
@@ -23,21 +23,24 @@ import { createRequire } from "module";
23
23
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
24
24
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
25
25
  import { z } from "zod";
26
- import { AppleMailManager } from "./services/appleMailManager.js";
26
+ import { AppleMailManager, isPathWithinAllowedRoots } from "./services/appleMailManager.js";
27
+ import { writeFileSync } from "fs";
28
+ import { resolve as resolvePath, join as joinPath } from "path";
27
29
  import { sendViaSmtp } from "./services/smtpMailer.js";
28
- import { isImapAccount, resolveImapConfigs, imapSearchMessages, imapListMessages, imapCreateMailbox, imapDeleteMailbox, imapRenameMailbox, imapGetMessage, imapMarkRead, imapMarkUnread, imapFlagMessage, imapUnflagMessage, imapMoveMessageById, imapDeleteMessageById, } from "./services/imapClient.js";
30
+ import { isImapAccount, resolveImapConfigs, imapSearchMessages, imapListMessages, imapUnreadCount, imapListMailboxes, imapMailStats, imapListAttachments, imapFetchAttachment, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete, imapBatchMove, imapThread, imapCreateMailbox, imapDeleteMailbox, imapRenameMailbox, imapGetMessage, imapMarkRead, imapMarkUnread, imapFlagMessage, imapUnflagMessage, imapMoveMessageById, imapDeleteMessageById, } from "./services/imapClient.js";
29
31
  import { successResponse, errorResponse, partialCoverageBlock, withErrorHandling, messageSummary, } from "./tools/respond.js";
30
32
  import { routeMessage } from "./services/messageRouter.js";
31
33
  import { runDoctor, formatDoctorReport } from "./tools/doctor.js";
32
34
  import { registerResourcesAndPrompts } from "./tools/resourcesAndPrompts.js";
33
35
  import { normalizeSubject, subjectFromGetMessage } from "./tools/thread.js";
34
36
  import { ImapIdleWatcher } from "./services/imapIdle.js";
37
+ import { loadFileConfig } from "./services/fileConfig.js";
38
+ // Load file-based config FIRST (2.1.1) — before anything reads APPLE_MAIL_MCP_*.
39
+ // Lets users configure the server when the host app strips the MCP env block.
40
+ loadFileConfig();
35
41
  // =============================================================================
36
42
  // Shared Validation Schemas
37
43
  // =============================================================================
38
- /** AppleScript message IDs are always numeric. Enforced to prevent AppleScript
39
- * injection via the `whose id is ${id}` interpolation. */
40
- const NUMERIC_MESSAGE_ID_SCHEMA = z.string().regex(/^\d+$/, "Message ID must be numeric");
41
44
  /** A single-message id is EITHER an AppleScript numeric id OR an IMAP composite
42
45
  * token (`imap:<base64url>`, emitted by the IMAP read path). The IMAP form is
43
46
  * base64url so it stays injection-safe; it never reaches AppleScript (it's
@@ -45,10 +48,11 @@ const NUMERIC_MESSAGE_ID_SCHEMA = z.string().regex(/^\d+$/, "Message ID must be
45
48
  const MESSAGE_ID_SCHEMA = z
46
49
  .string()
47
50
  .regex(/^(\d+|imap:[A-Za-z0-9_-]+)$/, "Message ID must be numeric or an IMAP id (imap:…)");
48
- /** Batch operations are AppleScript-only and capped to prevent unbounded loops /
49
- * DoS. IMAP ids are rejected here use the single-message tools for those. */
51
+ /** Batch operations accept numeric (AppleScript) and/or imap: ids (I2) and are
52
+ * capped to prevent unbounded loops / DoS. Numeric ids run via AppleScript;
53
+ * imap: ids are grouped by mailbox and applied in a single UID command. */
50
54
  const BATCH_IDS_SCHEMA = z
51
- .array(NUMERIC_MESSAGE_ID_SCHEMA)
55
+ .array(MESSAGE_ID_SCHEMA)
52
56
  .min(1, "At least one message ID is required")
53
57
  .max(100, "Cannot process more than 100 messages in a single batch");
54
58
  /** Date filter strings must look like natural-language dates (e.g. "March 1, 2026").
@@ -99,6 +103,31 @@ const mailManager = new AppleMailManager();
99
103
  registerResourcesAndPrompts(server, mailManager);
100
104
  // Response helpers, the AppleScript serial gate, withErrorHandling, and the
101
105
  // message backend router now live in @/tools/respond and @/services/messageRouter.
106
+ /**
107
+ * Split a batch of ids into numeric (AppleScript) and imap: (IMAP) groups, run
108
+ * each path, and merge into success/fail counts (I2). imap: ids apply in a
109
+ * single UID command per mailbox; numeric ids use the existing AppleScript batch.
110
+ */
111
+ async function hybridBatchCounts(ids, appleFn, imapFn) {
112
+ const imapIds = ids.filter((i) => i.startsWith("imap:"));
113
+ const numericIds = ids.filter((i) => !i.startsWith("imap:"));
114
+ let success = 0;
115
+ let fail = 0;
116
+ const errors = [];
117
+ if (numericIds.length > 0) {
118
+ const res = appleFn(numericIds);
119
+ const s = res.filter((r) => r.success).length;
120
+ success += s;
121
+ fail += res.length - s;
122
+ }
123
+ if (imapIds.length > 0) {
124
+ const r = await imapFn(imapIds);
125
+ success += r.success;
126
+ fail += r.failed;
127
+ errors.push(...r.errors);
128
+ }
129
+ return { success, fail, errors };
130
+ }
102
131
  // =============================================================================
103
132
  // Message Tools
104
133
  // =============================================================================
@@ -192,6 +221,14 @@ server.tool("get-thread", {
192
221
  mailbox: z.string().optional().describe("Mailbox to search (omit to search all)"),
193
222
  limit: z.number().optional().describe("Max messages in the thread (default 50)"),
194
223
  }, withErrorHandling(async ({ id, account, mailbox, limit = 50 }) => {
224
+ // True threading via References/Message-ID when we have an imap: id (I5);
225
+ // falls through to subject grouping if the server lacks HEADER search or
226
+ // nothing References-linked is found.
227
+ if (id.startsWith("imap:")) {
228
+ const t = await imapThread(id, { account }, limit);
229
+ if (t && t.count > 1)
230
+ return successResponse(t.text, { ...t.structured });
231
+ }
195
232
  // Resolve the seed message's subject, then gather the conversation by
196
233
  // normalized subject (B1). Works across the AppleScript and IMAP backends.
197
234
  let seedSubject = null;
@@ -467,10 +504,8 @@ server.tool("move-message", {
467
504
  // --- batch-delete-messages ---
468
505
  server.tool("batch-delete-messages", {
469
506
  ids: BATCH_IDS_SCHEMA,
470
- }, withErrorHandling(({ ids }) => {
471
- const results = mailManager.batchDeleteMessages(ids);
472
- const successCount = results.filter((r) => r.success).length;
473
- const failCount = results.length - successCount;
507
+ }, withErrorHandling(async ({ ids }) => {
508
+ const { success: successCount, fail: failCount } = await hybridBatchCounts(ids, (n) => mailManager.batchDeleteMessages(n), (im) => imapBatchDelete(im));
474
509
  if (failCount === 0) {
475
510
  return successResponse(`Successfully deleted ${successCount} message(s)`);
476
511
  }
@@ -486,10 +521,8 @@ server.tool("batch-move-messages", {
486
521
  ids: BATCH_IDS_SCHEMA,
487
522
  mailbox: z.string().min(1, "Destination mailbox is required"),
488
523
  account: z.string().optional().describe("Account containing the destination mailbox"),
489
- }, withErrorHandling(({ ids, mailbox, account }) => {
490
- const results = mailManager.batchMoveMessages(ids, mailbox, account);
491
- const successCount = results.filter((r) => r.success).length;
492
- const failCount = results.length - successCount;
524
+ }, withErrorHandling(async ({ ids, mailbox, account }) => {
525
+ const { success: successCount, fail: failCount } = await hybridBatchCounts(ids, (n) => mailManager.batchMoveMessages(n, mailbox, account), (im) => imapBatchMove(im, mailbox, { account }));
493
526
  if (failCount === 0) {
494
527
  return successResponse(`Successfully moved ${successCount} message(s) to "${mailbox}"`);
495
528
  }
@@ -503,10 +536,8 @@ server.tool("batch-move-messages", {
503
536
  // --- batch-mark-as-read ---
504
537
  server.tool("batch-mark-as-read", {
505
538
  ids: BATCH_IDS_SCHEMA,
506
- }, withErrorHandling(({ ids }) => {
507
- const results = mailManager.batchMarkAsRead(ids);
508
- const successCount = results.filter((r) => r.success).length;
509
- const failCount = results.length - successCount;
539
+ }, withErrorHandling(async ({ ids }) => {
540
+ const { success: successCount, fail: failCount } = await hybridBatchCounts(ids, (n) => mailManager.batchMarkAsRead(n), (im) => imapBatchMarkRead(im));
510
541
  if (failCount === 0) {
511
542
  return successResponse(`Successfully marked ${successCount} message(s) as read`);
512
543
  }
@@ -520,10 +551,8 @@ server.tool("batch-mark-as-read", {
520
551
  // --- batch-mark-as-unread ---
521
552
  server.tool("batch-mark-as-unread", {
522
553
  ids: BATCH_IDS_SCHEMA,
523
- }, withErrorHandling(({ ids }) => {
524
- const results = mailManager.batchMarkAsUnread(ids);
525
- const successCount = results.filter((r) => r.success).length;
526
- const failCount = results.length - successCount;
554
+ }, withErrorHandling(async ({ ids }) => {
555
+ const { success: successCount, fail: failCount } = await hybridBatchCounts(ids, (n) => mailManager.batchMarkAsUnread(n), (im) => imapBatchMarkUnread(im));
527
556
  if (failCount === 0) {
528
557
  return successResponse(`Successfully marked ${successCount} message(s) as unread`);
529
558
  }
@@ -537,10 +566,8 @@ server.tool("batch-mark-as-unread", {
537
566
  // --- batch-flag-messages ---
538
567
  server.tool("batch-flag-messages", {
539
568
  ids: BATCH_IDS_SCHEMA,
540
- }, withErrorHandling(({ ids }) => {
541
- const results = mailManager.batchFlagMessages(ids);
542
- const successCount = results.filter((r) => r.success).length;
543
- const failCount = results.length - successCount;
569
+ }, withErrorHandling(async ({ ids }) => {
570
+ const { success: successCount, fail: failCount } = await hybridBatchCounts(ids, (n) => mailManager.batchFlagMessages(n), (im) => imapBatchFlag(im));
544
571
  if (failCount === 0) {
545
572
  return successResponse(`Successfully flagged ${successCount} message(s)`);
546
573
  }
@@ -554,10 +581,8 @@ server.tool("batch-flag-messages", {
554
581
  // --- batch-unflag-messages ---
555
582
  server.tool("batch-unflag-messages", {
556
583
  ids: BATCH_IDS_SCHEMA,
557
- }, withErrorHandling(({ ids }) => {
558
- const results = mailManager.batchUnflagMessages(ids);
559
- const successCount = results.filter((r) => r.success).length;
560
- const failCount = results.length - successCount;
584
+ }, withErrorHandling(async ({ ids }) => {
585
+ const { success: successCount, fail: failCount } = await hybridBatchCounts(ids, (n) => mailManager.batchUnflagMessages(n), (im) => imapBatchUnflag(im));
561
586
  if (failCount === 0) {
562
587
  return successResponse(`Successfully unflagged ${successCount} message(s)`);
563
588
  }
@@ -571,8 +596,17 @@ server.tool("batch-unflag-messages", {
571
596
  // --- list-attachments ---
572
597
  server.tool("list-attachments", {
573
598
  id: MESSAGE_ID_SCHEMA,
574
- }, withErrorHandling(({ id }) => {
575
- const attachments = mailManager.listAttachments(id);
599
+ }, withErrorHandling(async ({ id }) => {
600
+ // IMAP (I1): BODYSTRUCTURE enumerates parts (incl. MIME attachments
601
+ // AppleScript can't see) without downloading the message.
602
+ const attachments = id.startsWith("imap:")
603
+ ? await (async () => {
604
+ const r = await imapListAttachments(id);
605
+ if (!r.success)
606
+ throw new Error(r.error || "Failed to list attachments via IMAP");
607
+ return r.attachments ?? [];
608
+ })()
609
+ : mailManager.listAttachments(id);
576
610
  const structured = { attachments, count: attachments.length };
577
611
  if (attachments.length === 0) {
578
612
  return successResponse("No attachments found", structured);
@@ -590,7 +624,25 @@ server.tool("save-attachment", {
590
624
  id: MESSAGE_ID_SCHEMA,
591
625
  attachmentName: z.string().min(1, "Attachment name is required"),
592
626
  savePath: z.string().min(1, "Save directory path is required"),
593
- }, withErrorHandling(({ id, attachmentName, savePath }) => {
627
+ }, withErrorHandling(async ({ id, attachmentName, savePath }) => {
628
+ // IMAP (I1): fetch the part's bytes via IMAP, then write into savePath (a
629
+ // directory) as savePath/attachmentName — mirroring the AppleScript path,
630
+ // with the same name + allowed-roots validation.
631
+ if (id.startsWith("imap:")) {
632
+ if (/[/\\\0]/.test(attachmentName) || attachmentName.includes("..")) {
633
+ return errorResponse(`Invalid attachment name: "${attachmentName}"`);
634
+ }
635
+ const resolvedDir = resolvePath(savePath);
636
+ if (!isPathWithinAllowedRoots(resolvedDir)) {
637
+ return errorResponse(`Save path "${savePath}" is outside allowed directories`);
638
+ }
639
+ const r = await imapFetchAttachment(id, attachmentName);
640
+ if (!r.success || !r.base64) {
641
+ return errorResponse(r.error || `Failed to fetch attachment "${attachmentName}"`);
642
+ }
643
+ writeFileSync(joinPath(resolvedDir, attachmentName), Buffer.from(r.base64, "base64"));
644
+ return successResponse(`Attachment "${attachmentName}" saved to ${savePath}`);
645
+ }
594
646
  const success = mailManager.saveAttachment(id, attachmentName, savePath);
595
647
  if (!success) {
596
648
  return errorResponse(`Failed to save attachment "${attachmentName}"`);
@@ -599,12 +651,19 @@ server.tool("save-attachment", {
599
651
  }, "Error saving attachment"));
600
652
  // --- fetch-attachment ---
601
653
  server.tool("fetch-attachment", {
602
- id: NUMERIC_MESSAGE_ID_SCHEMA,
654
+ id: MESSAGE_ID_SCHEMA,
603
655
  attachmentName: z.string().min(1, "Attachment name is required"),
604
- }, withErrorHandling(({ id, attachmentName }) => {
656
+ }, withErrorHandling(async ({ id, attachmentName }) => {
605
657
  // Returns the attachment bytes as base64 (B4) — the read counterpart to
606
- // sending inline base64 content, for clients that want the bytes directly
607
- // rather than writing to disk via save-attachment.
658
+ // sending inline base64 content. IMAP (I1) fetches the part directly; numeric
659
+ // ids fall back to the AppleScript/MIME path.
660
+ if (id.startsWith("imap:")) {
661
+ const r = await imapFetchAttachment(id, attachmentName);
662
+ if (!r.success || !r.base64) {
663
+ return errorResponse(r.error || `Failed to fetch attachment "${attachmentName}"`);
664
+ }
665
+ return successResponse(`Fetched "${attachmentName}" (${r.bytes} bytes, base64-encoded below).\n\n${r.base64}`, { attachmentName, bytes: r.bytes, mimeType: r.mimeType, contentBase64: r.base64 });
666
+ }
608
667
  const r = mailManager.getAttachmentBase64(id, attachmentName);
609
668
  if (!r.success) {
610
669
  return errorResponse(r.error || `Failed to fetch attachment "${attachmentName}"`);
@@ -617,7 +676,24 @@ server.tool("fetch-attachment", {
617
676
  // --- list-mailboxes ---
618
677
  server.tool("list-mailboxes", {
619
678
  account: z.string().optional().describe("Account to list mailboxes from"),
620
- }, withErrorHandling(({ account }) => {
679
+ }, withErrorHandling(async ({ account }) => {
680
+ // IMAP (I6): LIST + per-mailbox STATUS — sees the true server hierarchy and
681
+ // authoritative counts; falls back to AppleScript for non-IMAP accounts.
682
+ if (isImapAccount(account)) {
683
+ const boxes = await imapListMailboxes({ account });
684
+ const structured = {
685
+ mailboxes: boxes.map((b) => ({
686
+ name: b.path,
687
+ unreadCount: b.unseen,
688
+ messageCount: b.messages,
689
+ })),
690
+ count: boxes.length,
691
+ };
692
+ if (boxes.length === 0)
693
+ return successResponse("No mailboxes found", structured);
694
+ const list = boxes.map((b) => ` - ${b.path} (${b.unseen} unread)`).join("\n");
695
+ return successResponse(`Found ${boxes.length} mailbox(es):\n${list}`, structured);
696
+ }
621
697
  const mailboxes = mailManager.listMailboxes(account);
622
698
  const structured = { mailboxes, count: mailboxes.length };
623
699
  if (mailboxes.length === 0) {
@@ -630,8 +706,12 @@ server.tool("list-mailboxes", {
630
706
  server.tool("get-unread-count", {
631
707
  mailbox: z.string().optional().describe("Mailbox to check (default: all)"),
632
708
  account: z.string().optional().describe("Account to check"),
633
- }, withErrorHandling(({ mailbox, account }) => {
634
- const count = mailManager.getUnreadCount(mailbox, account);
709
+ }, withErrorHandling(async ({ mailbox, account }) => {
710
+ // IMAP (I4): STATUS (UNSEEN) is authoritative and fast even on huge
711
+ // mailboxes; falls back to AppleScript for non-IMAP accounts.
712
+ const count = isImapAccount(account)
713
+ ? await imapUnreadCount(mailbox, { account })
714
+ : mailManager.getUnreadCount(mailbox, account);
635
715
  const location = mailbox ? ` in "${mailbox}"` : "";
636
716
  return successResponse(`${count} unread message(s)${location}`, {
637
717
  unread: count,
@@ -893,7 +973,29 @@ server.tool("doctor", {}, withErrorHandling(async () => {
893
973
  return successResponse(formatDoctorReport(report), { ...report });
894
974
  }, "Error running doctor"));
895
975
  // --- get-mail-stats ---
896
- server.tool("get-mail-stats", {}, withErrorHandling(() => {
976
+ server.tool("get-mail-stats", {
977
+ account: z
978
+ .string()
979
+ .optional()
980
+ .describe("Limit to one account; uses fast IMAP STATUS if that account is IMAP-configured"),
981
+ }, withErrorHandling(async ({ account }) => {
982
+ // IMAP (I3): for a named IMAP account, STATUS gives authoritative counts and
983
+ // SEARCH SINCE gives recent activity — fast even on huge mailboxes.
984
+ if (account && isImapAccount(account)) {
985
+ const s = await imapMailStats({ account });
986
+ const lines = [
987
+ `📊 Mail Statistics — ${account} (IMAP)`,
988
+ `══════════════════`,
989
+ `Total messages: ${s.totalMessages}`,
990
+ `Unread messages: ${s.totalUnread}`,
991
+ ``,
992
+ `📥 Recently Received (INBOX):`,
993
+ ` Last 24 hours: ${s.recent.last24h}`,
994
+ ` Last 7 days: ${s.recent.last7d}`,
995
+ ` Last 30 days: ${s.recent.last30d}`,
996
+ ];
997
+ return successResponse(lines.join("\n"), { account, ...s });
998
+ }
897
999
  const stats = mailManager.getMailStats();
898
1000
  const lines = [];
899
1001
  lines.push(`📊 Mail Statistics`);
@@ -0,0 +1,7 @@
1
+ export declare function fileConfigPath(env?: NodeJS.ProcessEnv): string;
2
+ /**
3
+ * Merge a JSON config file's string values into `env` for keys not already set.
4
+ * Returns the list of keys actually applied. Tolerates a missing/corrupt file.
5
+ */
6
+ export declare function loadFileConfig(env?: NodeJS.ProcessEnv, path?: string): string[];
7
+ //# sourceMappingURL=fileConfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileConfig.d.ts","sourceRoot":"","sources":["../../src/services/fileConfig.ts"],"names":[],"mappings":"AAqBA,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAI3E;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,IAAI,GAAE,MAA4B,GACjC,MAAM,EAAE,CAiBV"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * File-based configuration loader (2.1.1).
3
+ *
4
+ * Some host apps (e.g. Claude Desktop) spawn the MCP server with a scrubbed
5
+ * environment and ignore/strip the `env` block in their server config, so
6
+ * there's no way to pass `APPLE_MAIL_MCP_*` settings in. This loads them from a
7
+ * JSON file the host doesn't manage, merging into `process.env` WITHOUT
8
+ * overriding anything already set (so an explicit env still wins).
9
+ *
10
+ * The file holds only non-secret config — account/host/Keychain-service names
11
+ * and flags. Passwords are NEVER stored here; they stay in the macOS Keychain.
12
+ *
13
+ * Path: `APPLE_MAIL_MCP_CONFIG_FILE`, else
14
+ * `~/Library/Application Support/apple-mail-mcp/config.json`.
15
+ *
16
+ * @module services/fileConfig
17
+ */
18
+ import { existsSync, readFileSync } from "fs";
19
+ import { join } from "path";
20
+ import { homedir } from "os";
21
+ export function fileConfigPath(env = process.env) {
22
+ const override = env.APPLE_MAIL_MCP_CONFIG_FILE;
23
+ if (override && override.trim())
24
+ return override.trim();
25
+ return join(homedir(), "Library", "Application Support", "apple-mail-mcp", "config.json");
26
+ }
27
+ /**
28
+ * Merge a JSON config file's string values into `env` for keys not already set.
29
+ * Returns the list of keys actually applied. Tolerates a missing/corrupt file.
30
+ */
31
+ export function loadFileConfig(env = process.env, path = fileConfigPath(env)) {
32
+ const applied = [];
33
+ try {
34
+ if (!existsSync(path))
35
+ return applied;
36
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
37
+ if (!parsed || typeof parsed !== "object")
38
+ return applied;
39
+ for (const [k, v] of Object.entries(parsed)) {
40
+ if (typeof v !== "string")
41
+ continue;
42
+ if (env[k] === undefined || env[k] === "") {
43
+ env[k] = v;
44
+ applied.push(k);
45
+ }
46
+ }
47
+ }
48
+ catch (e) {
49
+ console.error(`Failed to load apple-mail-mcp config file ${path}: ${String(e)}`);
50
+ }
51
+ return applied;
52
+ }
@@ -38,12 +38,33 @@ interface ImapEnvelope {
38
38
  subject?: string;
39
39
  date?: Date | string;
40
40
  from?: ImapAddress[];
41
+ messageId?: string;
42
+ inReplyTo?: string;
43
+ }
44
+ export interface ImapBodyStructure {
45
+ part?: string;
46
+ type?: string;
47
+ disposition?: string;
48
+ dispositionParameters?: Record<string, string>;
49
+ parameters?: Record<string, string>;
50
+ size?: number;
51
+ encoding?: string;
52
+ childNodes?: ImapBodyStructure[];
41
53
  }
42
54
  interface ImapMessage {
43
55
  uid: number;
44
56
  envelope?: ImapEnvelope;
45
57
  flags?: Set<string>;
46
58
  source?: Buffer | string;
59
+ bodyStructure?: ImapBodyStructure;
60
+ headers?: Buffer | string;
61
+ }
62
+ interface ImapDownload {
63
+ meta?: {
64
+ filename?: string;
65
+ contentType?: string;
66
+ };
67
+ content: AsyncIterable<Uint8Array>;
47
68
  }
48
69
  interface MailboxLock {
49
70
  release: () => void;
@@ -68,6 +89,19 @@ export interface ImapClientLike {
68
89
  uid: true;
69
90
  }): Promise<ImapMessage | false>;
70
91
  list(): Promise<ImapMailboxListing[]>;
92
+ status(path: string, query: {
93
+ messages?: boolean;
94
+ unseen?: boolean;
95
+ recent?: boolean;
96
+ }): Promise<{
97
+ path: string;
98
+ messages?: number;
99
+ unseen?: number;
100
+ recent?: number;
101
+ }>;
102
+ download(range: string, part: string, opts: {
103
+ uid: true;
104
+ }): Promise<ImapDownload>;
71
105
  mailboxCreate(path: string): Promise<{
72
106
  path: string;
73
107
  created: boolean;
@@ -123,6 +157,32 @@ export declare function resolveImapConfig(env?: NodeJS.ProcessEnv, account?: str
123
157
  export declare function resolveMailboxPath(mailbox: string | undefined, mode: "search" | "list"): string;
124
158
  export declare function imapSearchMessages(args: ImapSearchArgs, deps?: ImapDeps): Promise<string>;
125
159
  export declare function imapListMessages(args: ImapSearchArgs, deps?: ImapDeps): Promise<string>;
160
+ /** Unread count via IMAP STATUS (UNSEEN). No mailbox → sum across all mailboxes. */
161
+ export declare function imapUnreadCount(mailbox: string | undefined, deps?: ImapDeps): Promise<number>;
162
+ export interface ImapMailboxInfo {
163
+ path: string;
164
+ name: string;
165
+ messages: number;
166
+ unseen: number;
167
+ }
168
+ /** List mailboxes with per-mailbox message/unseen counts via LIST + STATUS (I6). */
169
+ export declare function imapListMailboxes(deps?: ImapDeps): Promise<ImapMailboxInfo[]>;
170
+ export interface ImapStats {
171
+ totalMessages: number;
172
+ totalUnread: number;
173
+ perMailbox: {
174
+ mailbox: string;
175
+ messages: number;
176
+ unseen: number;
177
+ }[];
178
+ recent: {
179
+ last24h: number;
180
+ last7d: number;
181
+ last30d: number;
182
+ };
183
+ }
184
+ /** Aggregate stats via STATUS (counts) + INBOX SEARCH SINCE (recent) (I3). */
185
+ export declare function imapMailStats(deps?: ImapDeps): Promise<ImapStats>;
126
186
  export interface ImapOpResult {
127
187
  success: boolean;
128
188
  error?: string;
@@ -154,5 +214,53 @@ export declare const imapFlagMessage: (id: string, deps?: {}) => Promise<ImapOpR
154
214
  export declare const imapUnflagMessage: (id: string, deps?: {}) => Promise<ImapOpResult>;
155
215
  export declare function imapMoveMessageById(id: string, destMailbox: string, deps?: ImapDeps): Promise<ImapOpResult>;
156
216
  export declare function imapDeleteMessageById(id: string, deps?: ImapDeps): Promise<ImapOpResult>;
217
+ export interface ImapAttachmentInfo {
218
+ id: string;
219
+ name: string;
220
+ mimeType: string;
221
+ size: number;
222
+ }
223
+ /** List a message's attachments via IMAP BODYSTRUCTURE (no full download). */
224
+ export declare function imapListAttachments(id: string, deps?: ImapDeps): Promise<{
225
+ success: boolean;
226
+ attachments?: ImapAttachmentInfo[];
227
+ error?: string;
228
+ }>;
229
+ /** Fetch one attachment's bytes (base64) via IMAP, matched by filename. */
230
+ export declare function imapFetchAttachment(id: string, attachmentName: string, deps?: ImapDeps): Promise<{
231
+ success: boolean;
232
+ base64?: string;
233
+ bytes?: number;
234
+ mimeType?: string;
235
+ error?: string;
236
+ }>;
237
+ export interface ImapBatchResult {
238
+ success: number;
239
+ failed: number;
240
+ errors: string[];
241
+ }
242
+ export declare const imapBatchMarkRead: (ids: string[], deps?: ImapDeps) => Promise<ImapBatchResult>;
243
+ export declare const imapBatchMarkUnread: (ids: string[], deps?: ImapDeps) => Promise<ImapBatchResult>;
244
+ export declare const imapBatchFlag: (ids: string[], deps?: ImapDeps) => Promise<ImapBatchResult>;
245
+ export declare const imapBatchUnflag: (ids: string[], deps?: ImapDeps) => Promise<ImapBatchResult>;
246
+ export declare const imapBatchDelete: (ids: string[], deps?: ImapDeps) => Promise<ImapBatchResult>;
247
+ export declare function imapBatchMove(ids: string[], destMailbox: string, deps?: ImapDeps): Promise<ImapBatchResult>;
248
+ export interface ImapThreadMessage {
249
+ id: string;
250
+ subject: string;
251
+ sender: string;
252
+ date: string;
253
+ isRead: boolean;
254
+ }
255
+ export interface ImapThreadResult {
256
+ count: number;
257
+ text: string;
258
+ structured: {
259
+ subject: string;
260
+ messages: ImapThreadMessage[];
261
+ count: number;
262
+ };
263
+ }
264
+ export declare function imapThread(id: string, deps?: ImapDeps, limit?: number): Promise<ImapThreadResult | null>;
157
265
  export {};
158
266
  //# sourceMappingURL=imapClient.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"imapClient.d.ts","sourceRoot":"","sources":["../../src/services/imapClient.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"imapClient.d.ts","sourceRoot":"","sources":["../../src/services/imapClient.ts"],"names":[],"mappings":"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"}
@@ -294,6 +294,97 @@ export function imapSearchMessages(args, deps = {}) {
294
294
  export function imapListMessages(args, deps = {}) {
295
295
  return run(args, true, deps);
296
296
  }
297
+ // ===========================================================================
298
+ // Counts & stats via IMAP STATUS (2.1 optimizations I3/I4/I6)
299
+ //
300
+ // STATUS is a single server round-trip that returns authoritative message/unseen
301
+ // counts without enumerating messages — far faster and more reliable than
302
+ // AppleScript on large mailboxes (where the per-message walk times out, #8/#24).
303
+ // ===========================================================================
304
+ /** Unread count via IMAP STATUS (UNSEEN). No mailbox → sum across all mailboxes. */
305
+ export function imapUnreadCount(mailbox, deps = {}) {
306
+ return useClient(deps, async (client) => {
307
+ if (mailbox) {
308
+ const s = await client.status(resolveMailboxPath(mailbox, "list"), { unseen: true });
309
+ return s.unseen ?? 0;
310
+ }
311
+ let total = 0;
312
+ for (const b of await client.list()) {
313
+ try {
314
+ const s = await client.status(b.path, { unseen: true });
315
+ total += s.unseen ?? 0;
316
+ }
317
+ catch {
318
+ // skip mailboxes that can't be STATUS'd (e.g. \Noselect parents)
319
+ }
320
+ }
321
+ return total;
322
+ }, true);
323
+ }
324
+ /** List mailboxes with per-mailbox message/unseen counts via LIST + STATUS (I6). */
325
+ export function imapListMailboxes(deps = {}) {
326
+ return useClient(deps, async (client) => {
327
+ const out = [];
328
+ for (const b of await client.list()) {
329
+ let messages = 0;
330
+ let unseen = 0;
331
+ try {
332
+ const s = await client.status(b.path, { messages: true, unseen: true });
333
+ messages = s.messages ?? 0;
334
+ unseen = s.unseen ?? 0;
335
+ }
336
+ catch {
337
+ // \Noselect or otherwise un-status-able mailbox → report zeros
338
+ }
339
+ out.push({ path: b.path, name: b.name, messages, unseen });
340
+ }
341
+ return out;
342
+ }, true);
343
+ }
344
+ /** Aggregate stats via STATUS (counts) + INBOX SEARCH SINCE (recent) (I3). */
345
+ export function imapMailStats(deps = {}) {
346
+ return useClient(deps, async (client) => {
347
+ const perMailbox = [];
348
+ let totalMessages = 0;
349
+ let totalUnread = 0;
350
+ for (const b of await client.list()) {
351
+ try {
352
+ const s = await client.status(b.path, { messages: true, unseen: true });
353
+ const messages = s.messages ?? 0;
354
+ const unseen = s.unseen ?? 0;
355
+ totalMessages += messages;
356
+ totalUnread += unseen;
357
+ perMailbox.push({ mailbox: b.path, messages, unseen });
358
+ }
359
+ catch {
360
+ // skip un-status-able mailbox
361
+ }
362
+ }
363
+ // Recent counts against INBOX (the meaningful "received" surface).
364
+ const since = (days) => new Date(Date.now() - days * 86_400_000);
365
+ const countSince = async (days) => {
366
+ try {
367
+ const lock = await client.getMailboxLock("INBOX");
368
+ try {
369
+ const found = await client.search({ since: since(days) }, { uid: true });
370
+ return Array.isArray(found) ? found.length : 0;
371
+ }
372
+ finally {
373
+ lock.release();
374
+ }
375
+ }
376
+ catch {
377
+ return 0;
378
+ }
379
+ };
380
+ const [last24h, last7d, last30d] = await Promise.all([
381
+ countSince(1),
382
+ countSince(7),
383
+ countSince(30),
384
+ ]);
385
+ return { totalMessages, totalUnread, perMailbox, recent: { last24h, last7d, last30d } };
386
+ }, true);
387
+ }
297
388
  function errText(e) {
298
389
  return e instanceof Error ? e.message : String(e);
299
390
  }
@@ -618,3 +709,203 @@ export async function imapDeleteMessageById(id, deps = {}) {
618
709
  }
619
710
  });
620
711
  }
712
+ /** Walk a BODYSTRUCTURE tree collecting attachment parts (disposition or filename). */
713
+ function collectAttachments(node, out = []) {
714
+ if (!node)
715
+ return out;
716
+ const filename = node.dispositionParameters?.filename || node.parameters?.name;
717
+ const disposition = node.disposition?.toLowerCase();
718
+ const isAttachment = !!node.part && (disposition === "attachment" || (!!filename && disposition !== "inline"));
719
+ if (isAttachment) {
720
+ out.push({
721
+ part: node.part,
722
+ filename: filename || `part-${node.part}`,
723
+ mimeType: node.type || "application/octet-stream",
724
+ size: node.size ?? 0,
725
+ });
726
+ }
727
+ for (const child of node.childNodes ?? [])
728
+ collectAttachments(child, out);
729
+ return out;
730
+ }
731
+ async function streamToBuffer(content) {
732
+ const chunks = [];
733
+ for await (const chunk of content)
734
+ chunks.push(Buffer.from(chunk));
735
+ return Buffer.concat(chunks);
736
+ }
737
+ /** List a message's attachments via IMAP BODYSTRUCTURE (no full download). */
738
+ export async function imapListAttachments(id, deps = {}) {
739
+ const ref = decodeImapId(id);
740
+ if (!ref)
741
+ return { success: false, error: `Not an IMAP message id: "${id}".` };
742
+ return withMailbox(ref.path, { ...deps, account: deps.account ?? ref.account }, async (client) => {
743
+ const msg = await client.fetchOne(String(ref.uid), { bodyStructure: true }, { uid: true });
744
+ if (!msg || !msg.bodyStructure) {
745
+ return { success: false, error: `IMAP message UID ${ref.uid} not found in "${ref.path}".` };
746
+ }
747
+ const attachments = collectAttachments(msg.bodyStructure).map((a) => ({
748
+ id: `${id}#${a.part}`,
749
+ name: a.filename,
750
+ mimeType: a.mimeType,
751
+ size: a.size,
752
+ }));
753
+ return { success: true, attachments };
754
+ });
755
+ }
756
+ /** Fetch one attachment's bytes (base64) via IMAP, matched by filename. */
757
+ export async function imapFetchAttachment(id, attachmentName, deps = {}) {
758
+ const ref = decodeImapId(id);
759
+ if (!ref)
760
+ return { success: false, error: `Not an IMAP message id: "${id}".` };
761
+ return withMailbox(ref.path, { ...deps, account: deps.account ?? ref.account }, async (client) => {
762
+ const msg = await client.fetchOne(String(ref.uid), { bodyStructure: true }, { uid: true });
763
+ if (!msg || !msg.bodyStructure) {
764
+ return { success: false, error: `IMAP message UID ${ref.uid} not found in "${ref.path}".` };
765
+ }
766
+ const atts = collectAttachments(msg.bodyStructure);
767
+ const match = atts.find((a) => a.filename === attachmentName);
768
+ if (!match) {
769
+ const names = atts.map((a) => a.filename).join(", ") || "none";
770
+ return {
771
+ success: false,
772
+ error: `Attachment "${attachmentName}" not found on UID ${ref.uid}. Available: ${names}.`,
773
+ };
774
+ }
775
+ const dl = await client.download(String(ref.uid), match.part, { uid: true });
776
+ const buf = await streamToBuffer(dl.content);
777
+ return {
778
+ success: true,
779
+ base64: buf.toString("base64"),
780
+ bytes: buf.length,
781
+ mimeType: match.mimeType,
782
+ };
783
+ });
784
+ }
785
+ async function imapBatch(ids, deps, op) {
786
+ const groups = new Map();
787
+ const errors = [];
788
+ let failed = 0;
789
+ for (const id of ids) {
790
+ const ref = decodeImapId(id);
791
+ if (!ref) {
792
+ failed++;
793
+ errors.push(`Not an IMAP id: "${id}"`);
794
+ continue;
795
+ }
796
+ const key = `${ref.account}${ref.path}`;
797
+ const g = groups.get(key) ?? { account: ref.account, path: ref.path, uids: [] };
798
+ g.uids.push(ref.uid);
799
+ groups.set(key, g);
800
+ }
801
+ let success = 0;
802
+ for (const g of groups.values()) {
803
+ try {
804
+ await useClient({ ...deps, account: deps.account ?? g.account }, async (client) => {
805
+ const lock = await client.getMailboxLock(g.path);
806
+ try {
807
+ await op(client, g.uids, g.path);
808
+ }
809
+ finally {
810
+ lock.release();
811
+ }
812
+ });
813
+ success += g.uids.length;
814
+ }
815
+ catch (e) {
816
+ failed += g.uids.length;
817
+ errors.push(`${g.path}: ${errText(e)}`);
818
+ }
819
+ }
820
+ return { success, failed, errors };
821
+ }
822
+ export const imapBatchMarkRead = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids) => {
823
+ await c.messageFlagsAdd(uids, ["\\Seen"], { uid: true });
824
+ });
825
+ export const imapBatchMarkUnread = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids) => {
826
+ await c.messageFlagsRemove(uids, ["\\Seen"], { uid: true });
827
+ });
828
+ export const imapBatchFlag = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids) => {
829
+ await c.messageFlagsAdd(uids, ["\\Flagged"], { uid: true });
830
+ });
831
+ export const imapBatchUnflag = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids) => {
832
+ await c.messageFlagsRemove(uids, ["\\Flagged"], { uid: true });
833
+ });
834
+ export const imapBatchDelete = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids) => {
835
+ await c.messageDelete(uids, { uid: true });
836
+ });
837
+ export function imapBatchMove(ids, destMailbox, deps = {}) {
838
+ return imapBatch(ids, deps, async (c, uids) => {
839
+ const dest = (await findMailboxPath(c, destMailbox)) ?? resolveMailboxPath(destMailbox, "list");
840
+ await c.messageMove(uids, dest, { uid: true });
841
+ });
842
+ }
843
+ function senderName(from) {
844
+ const a = from?.[0];
845
+ if (!a)
846
+ return "(unknown)";
847
+ return a.name ? `${a.name} <${a.address ?? ""}>` : (a.address ?? "(unknown)");
848
+ }
849
+ function dateMs(m) {
850
+ return m.envelope?.date ? new Date(m.envelope.date).getTime() : 0;
851
+ }
852
+ export async function imapThread(id, deps = {}, limit = 50) {
853
+ const ref = decodeImapId(id);
854
+ if (!ref)
855
+ return null;
856
+ return useClient({ ...deps, account: deps.account ?? ref.account }, async (client) => {
857
+ const lock = await client.getMailboxLock(ref.path);
858
+ try {
859
+ const seed = await client.fetchOne(String(ref.uid), { envelope: true, headers: ["references", "in-reply-to", "message-id"] }, { uid: true });
860
+ if (!seed)
861
+ return null;
862
+ const seedMsgId = seed.envelope?.messageId;
863
+ const refIds = new Set();
864
+ const hdr = seed.headers ? seed.headers.toString() : "";
865
+ for (const m of hdr.matchAll(/<[^>]+>/g))
866
+ refIds.add(m[0]);
867
+ if (seed.envelope?.inReplyTo)
868
+ refIds.add(seed.envelope.inReplyTo);
869
+ const uidSet = new Set([ref.uid]);
870
+ const addFound = (found) => {
871
+ if (Array.isArray(found))
872
+ found.forEach((u) => uidSet.add(u));
873
+ };
874
+ // Descendants: anything referencing the seed.
875
+ if (seedMsgId) {
876
+ addFound(await client.search({ header: { references: seedMsgId } }, { uid: true }));
877
+ addFound(await client.search({ header: { "in-reply-to": seedMsgId } }, { uid: true }));
878
+ }
879
+ // Ancestors: messages whose Message-ID is in the seed's References (bounded).
880
+ for (const mid of [...refIds].slice(0, 20)) {
881
+ addFound(await client.search({ header: { "message-id": mid } }, { uid: true }));
882
+ }
883
+ if (uidSet.size <= 1)
884
+ return null; // only the seed → caller falls back to subject
885
+ const uids = [...uidSet].slice(0, limit);
886
+ const msgs = [];
887
+ for await (const msg of client.fetch(uids.join(","), { envelope: true, flags: true }, { uid: true })) {
888
+ msgs.push(msg);
889
+ }
890
+ msgs.sort((a, b) => dateMs(a) - dateMs(b)); // oldest first
891
+ const subject = seed.envelope?.subject || "(no subject)";
892
+ const structured = {
893
+ subject,
894
+ count: msgs.length,
895
+ messages: msgs.map((m) => ({
896
+ id: encodeImapId(ref.account, ref.path, m.uid),
897
+ subject: m.envelope?.subject || "(no subject)",
898
+ sender: senderName(m.envelope?.from),
899
+ date: m.envelope?.date ? new Date(m.envelope.date).toISOString() : "",
900
+ isRead: m.flags?.has("\\Seen") ?? false,
901
+ })),
902
+ };
903
+ const text = `Thread "${subject}" — ${msgs.length} message(s) via IMAP (References-linked, oldest first):\n` +
904
+ msgs.map((m) => formatRow(m, ref.account, ref.path)).join("\n");
905
+ return { count: msgs.length, text, structured };
906
+ }
907
+ finally {
908
+ lock.release();
909
+ }
910
+ }, true);
911
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "description": "MCP server for Apple Mail - read, search, send, and manage emails via Claude",
5
5
  "type": "module",
6
6
  "main": "build/index.js",