apple-mail-mcp 2.5.0 → 2.6.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
@@ -373,7 +373,7 @@ What routes to IMAP when an account is IMAP-configured:
373
373
  - **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).
374
374
  - **Message mutations:** `mark-as-read`/`unread`, `flag-message`/`unflag-message`, `move-message`, `delete-message`.
375
375
  - **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.
376
- - **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.
376
+ - **Counts & stats (2.1):** `get-unread-count` and `list-mailboxes` use `STATUS`; `get-mail-stats` uses `STATUS` + `SEARCH SINCE` — authoritative and fast even on huge mailboxes. As of v2.6.0 these prefer IMAP whenever it's configured (see *Read routing* below), merging across accounts when no `account` is given.
377
377
  - **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.
378
378
  - **Threading (2.1):** `get-thread` links a conversation via `References`/`Message-ID` (`HEADER SEARCH`) for an `imap:` seed, falling back to subject grouping otherwise.
379
379
 
@@ -384,9 +384,34 @@ attachment/thread tools and it routes to IMAP automatically; bare numeric ids
384
384
  continue to use AppleScript. So an agent never has to know which backend a
385
385
  message came from — the id carries it.
386
386
 
387
- Routing is conservative: only a call whose explicit `account` matches the
388
- configured IMAP account goes to IMAP; everything else falls through to
389
- AppleScript.
387
+ **Read routing (v2.6.0): reads PREFER direct IMAP whenever IMAP is configured.**
388
+ The read tools `search-messages`, `get-thread`, `list-messages`,
389
+ `list-mailboxes`, `get-unread-count`, `get-mail-stats` — now go to IMAP whenever
390
+ any `APPLE_MAIL_MCP_IMAP_*` account is configured, not just when an explicit
391
+ matching `account` is passed. There are three cases:
392
+
393
+ - **Explicit IMAP account** — single-account IMAP (fast server-side path).
394
+ - **Explicit non-IMAP account** — AppleScript (that account isn't on IMAP).
395
+ - **No `account` given** — **merge across all accounts**: the query fans out over
396
+ *every* configured IMAP account, **and** AppleScript runs **only for the
397
+ accounts no IMAP config covers** (the account list is partitioned — accounts
398
+ already served by IMAP are *not* re-scanned via AppleScript). If every Mail
399
+ account is IMAP-configured, AppleScript is skipped entirely. The results are
400
+ merged so no account is dropped. Message lists still de-duplicate as a safety
401
+ net (preferring the IMAP copy, which carries the round-trippable `imap:` id) and
402
+ sort newest-first; count tools (`get-unread-count`, `get-mail-stats`) count each
403
+ account via exactly one backend so a coverage mismatch can never double- (or
404
+ under-) count.
405
+ - **Default mailbox is resolved per account.** When you don't pin a `mailbox`, a
406
+ fan-out search scopes each account to its own default — Gmail/Workspace to
407
+ `[Gmail]/All Mail`, every other IMAP host (iCloud, etc.) to `INBOX` (since
408
+ `[Gmail]/All Mail` is Gmail-only and selecting it elsewhere would silently drop
409
+ that account). Pin a `mailbox` to search a wider scope on non-Gmail accounts.
410
+
411
+ If IMAP is **not** configured at all, every read behaves exactly as before
412
+ (pure AppleScript). The three mailbox-**write** ops (`create-mailbox`,
413
+ `delete-mailbox`, `rename-mailbox`) remain conservative — they route to IMAP only
414
+ for an explicitly-named IMAP account, never on an omitted account.
390
415
 
391
416
  | Variable | Required | Default | Description |
392
417
  |----------|----------|---------|-------------|
@@ -399,7 +424,7 @@ AppleScript.
399
424
  | `APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT` | No | = user | Keychain item account |
400
425
  | `APPLE_MAIL_MCP_IMAP_ACCOUNTS` | No | — | JSON array of **additional** IMAP accounts for multi-account setups (see below) |
401
426
  | `APPLE_MAIL_MCP_IMAP_IDLE` | No | `0` | Set `1` to enable IMAP IDLE push notifications (new-mail alerts) for every configured account |
402
- | `APPLE_MAIL_MCP_IMAP_IDLE_MS` | No | `60000` | Idle timeout (ms) before a pooled IMAP connection is closed |
427
+ | `APPLE_MAIL_MCP_IMAP_IDLE_MS` | No | `30000` | Idle timeout (ms) before a pooled IMAP connection is closed (`0` = never close) |
403
428
 
404
429
  **Multiple IMAP accounts (C2):** set `APPLE_MAIL_MCP_IMAP_ACCOUNTS` to a JSON array, e.g.
405
430
  `[{"account":"Work","user":"me@co.com","host":"imap.co.com","keychainService":"imap.co.com"}]`.
@@ -421,6 +446,32 @@ config. Gmail label semantics: common names (`All Mail`, `Sent`, `Trash`,
421
446
  > (e.g. `iCloud`), and use an **app-specific password** (from appleid.apple.com)
422
447
  > stored in the Keychain.
423
448
 
449
+ ##### Connection footprint (playing nice with Gmail)
450
+
451
+ IMAP connections are a shared, capped resource: **Gmail allows at most 15
452
+ simultaneous IMAP connections per account**, and Apple Mail itself needs some of
453
+ those slots. This server keeps its footprint small:
454
+
455
+ - **One pooled connection per account**, reused across calls and **closed after
456
+ ~30s idle** (tune with `APPLE_MAIL_MCP_IMAP_IDLE_MS`; `0` = never close). So a
457
+ server that isn't actively serving IMAP calls holds **zero** connections.
458
+ - **IMAP IDLE is opt-in** (`APPLE_MAIL_MCP_IMAP_IDLE=1`). When on, it adds **one
459
+ persistent connection per account** (a long-lived watcher), on top of the
460
+ pooled request connection — leave it off if you don't need push notifications.
461
+ - **Connections are dropped on shutdown** — SIGINT/SIGTERM and stdin-EOF (the
462
+ MCP client/parent going away). As of **v2.6.1** the server also **self-exits if
463
+ it becomes orphaned** (parent force-quit/crashed → reparented to launchd),
464
+ polling every 30s, so it can't linger holding sockets after its session is gone.
465
+
466
+ The catch is **multiple concurrent instances**. A host like the Claude desktop
467
+ app spawns a *separate* set of MCP servers per open conversation (and respawns
468
+ them after a crash), so the footprint is **per instance × accounts**. With IDLE
469
+ off, an idle instance trends to 0 connections; with many *active* conversations
470
+ or IDLE on, the per-account total climbs toward Gmail's 15-connection cap and can
471
+ starve Apple Mail of slots (→ intermittent "cannot connect"). If you hit that,
472
+ close idle Claude conversations, keep `APPLE_MAIL_MCP_IMAP_IDLE` off unless you
473
+ need push, and/or lower `APPLE_MAIL_MCP_IMAP_IDLE_MS`.
474
+
424
475
  ##### Configuration file (when the host strips `env`)
425
476
 
426
477
  Some host apps (e.g. Claude Desktop) launch the MCP server with a scrubbed
@@ -596,6 +647,8 @@ Reply to an existing message.
596
647
  }
597
648
  ```
598
649
 
650
+ > **Transport (v2.5.0):** when SMTP is configured, `reply-to-message` sends via **clean SMTP**, threading the reply with proper RFC 5322 `In-Reply-To`/`References` headers (built from the original message) so it lands in the same conversation. When SMTP is not configured (or the original lacks the headers needed to thread), it falls back to Mail.app's AppleScript `reply … without opening window` — same reliable-from-background-process path as before. See [SMTP transport](#smtp-transport).
651
+
599
652
  **⚠️ Safety:** With the default `send: true`, sends real mail immediately and cannot be unsent. Confirm the recipients, subject, and body with the user before calling (or pass `send: false` to save a draft for review).
600
653
 
601
654
  ---
@@ -611,6 +664,8 @@ Forward a message to new recipients.
611
664
  | `body` | string | No | Message to prepend |
612
665
  | `send` | boolean | No | Send immediately (default: true, false = save as draft) |
613
666
 
667
+ > **Transport (v2.5.0):** when SMTP is configured, `forward-message` sends via **clean SMTP** (a fresh message with the original quoted, no threading headers — a forward starts a new conversation). When SMTP is not configured it falls back to Mail.app's AppleScript `forward … without opening window`. See [SMTP transport](#smtp-transport).
668
+
614
669
  **⚠️ Safety:** With the default `send: true`, sends real mail immediately and cannot be unsent. Confirm the recipients, subject, and body with the user before calling (or pass `send: false` to save a draft for review).
615
670
 
616
671
  ---
@@ -1128,6 +1183,8 @@ Prior to v1.4.0, `reply-to-message` and `forward-message` would send messages wi
1128
1183
 
1129
1184
  **Fix:** Replaced `with opening window` with `without opening window` for both `reply` and `forward` commands. With this approach, `set content` works immediately and reliably from background processes. `In-Reply-To` and `References` headers are still set correctly by Mail.app, and no GUI compose window is opened.
1130
1185
 
1186
+ **Update (v2.5.0):** when SMTP is configured, `reply-to-message` and `forward-message` now prefer **clean direct SMTP** instead of AppleScript — the same prefer-direct model as `send-email`. Replies are threaded with RFC 5322 `In-Reply-To`/`References` headers built from the original message; forwards start a new conversation. The AppleScript `without opening window` path above remains the fallback when SMTP is not configured (or, for replies, when the original message lacks the headers needed to thread).
1187
+
1131
1188
  See [#7](https://github.com/sweetrb/apple-mail-mcp/issues/7) for full details and the list of approaches that were tested.
1132
1189
 
1133
1190
  ### Backslash Escaping (Important for AI Agents)
package/build/index.js CHANGED
@@ -28,14 +28,16 @@ import { writeFileSync } from "fs";
28
28
  import { resolve as resolvePath, join as joinPath } from "path";
29
29
  import { sendViaSmtp, sendSerialViaSmtp, shouldUseSmtp, isSmtpConfigured, resolveSmtpConfig, } from "./services/smtpMailer.js";
30
30
  import { buildReplyOptions, buildForwardOptions, parseOriginalHeaders, } from "./services/replyForward.js";
31
- import { isImapAccount, resolveImapConfigs, dropAllPools, 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";
31
+ import { isImapAccount, shouldUseImap, resolveImapConfigs, dropAllPools, 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";
32
32
  import { successResponse, errorResponse, partialCoverageBlock, withErrorHandling, messageSummary, } from "./tools/respond.js";
33
+ import { fanOutImapMessages, mergeMessages, formatMergedRows, partitionAccountsForCounts, planCountSources, } from "./services/imapMultiAccount.js";
33
34
  import { routeMessage } from "./services/messageRouter.js";
34
35
  import { runDoctor, formatDoctorReport } from "./tools/doctor.js";
35
36
  import { registerResourcesAndPrompts } from "./tools/resourcesAndPrompts.js";
36
37
  import { normalizeSubject, subjectFromGetMessage } from "./tools/thread.js";
37
38
  import { ImapIdleWatcher } from "./services/imapIdle.js";
38
39
  import { loadFileConfig } from "./services/fileConfig.js";
40
+ import { isOrphaned } from "./utils/orphan.js";
39
41
  // Load file-based config FIRST (2.1.1) — before anything reads APPLE_MAIL_MCP_*.
40
42
  // Lets users configure the server when the host app strips the MCP env block.
41
43
  loadFileConfig();
@@ -116,6 +118,83 @@ const CHECK_ITEM_SCHEMA = z.object({}).passthrough();
116
118
  // Read version from package.json to keep it in sync
117
119
  const require = createRequire(import.meta.url);
118
120
  const { version } = require("../package.json");
121
+ /** An empty SearchDiagnostics (complete coverage). */
122
+ function emptyDiagnostics() {
123
+ return {
124
+ partial: false,
125
+ timedOutAccounts: [],
126
+ skippedLargeMailboxes: [],
127
+ notSearchedMailboxes: [],
128
+ };
129
+ }
130
+ /** Merge two diagnostics objects (union the lists, OR the partial flag). */
131
+ function mergeDiagnostics(a, b) {
132
+ return {
133
+ partial: a.partial || b.partial,
134
+ timedOutAccounts: [...a.timedOutAccounts, ...b.timedOutAccounts],
135
+ skippedLargeMailboxes: [...a.skippedLargeMailboxes, ...b.skippedLargeMailboxes],
136
+ notSearchedMailboxes: [...a.notSearchedMailboxes, ...b.notSearchedMailboxes],
137
+ };
138
+ }
139
+ /**
140
+ * Run a per-account AppleScript scan over ONLY the given accounts (the ones no
141
+ * IMAP config covers), concatenating their rows and merging diagnostics. The IMAP
142
+ * fan-out already covers the IMAP accounts, so this avoids scanning them on the
143
+ * AppleScript side — which, for an all-IMAP user, means ZERO AppleScript work and
144
+ * therefore no reliance on the fragile composite dedup. `scan` runs one account.
145
+ */
146
+ function appleScanForAccounts(accounts, scan) {
147
+ const rows = [];
148
+ let diagnostics = emptyDiagnostics();
149
+ for (const acct of accounts) {
150
+ const res = scan(acct.name);
151
+ rows.push(...res.messages.map(messageSummary));
152
+ diagnostics = mergeDiagnostics(diagnostics, res.diagnostics);
153
+ }
154
+ return { rows, diagnostics };
155
+ }
156
+ /**
157
+ * Build the success response for a no-account, prefer-IMAP MERGED message list
158
+ * (search-messages / list-messages — v2.6.0). Concatenates the IMAP fan-out rows
159
+ * with the (already partitioned) AppleScript rows, de-dups (IMAP copy wins — a
160
+ * safety net for heuristic misses + IMAP-vs-IMAP dupes, no longer load-bearing
161
+ * for matched accounts), sorts newest-first, applies `limit`, and preserves the
162
+ * partial-coverage diagnostics (AppleScript scan + any failed IMAP fan-out).
163
+ *
164
+ * @param verb "matched" (search) or "listed" (list) — only affects empty-state text.
165
+ */
166
+ function mergedMessageResponse(fan, apple, limit, verb) {
167
+ const merged = mergeMessages(fan.rows, apple.rows, limit);
168
+ // Surface IMAP fan-out failures alongside the AppleScript diagnostics so a
169
+ // partial merge is never mistaken for a confirmed "no such mail".
170
+ const diagnostics = {
171
+ ...apple.diagnostics,
172
+ partial: apple.diagnostics.partial || fan.accountsFailed.length > 0,
173
+ timedOutAccounts: [...apple.diagnostics.timedOutAccounts, ...fan.accountsFailed],
174
+ };
175
+ const structured = {
176
+ messages: merged,
177
+ count: merged.length,
178
+ partial: diagnostics.partial,
179
+ skippedLargeMailboxes: diagnostics.skippedLargeMailboxes,
180
+ notSearchedMailboxes: diagnostics.notSearchedMailboxes,
181
+ timedOutAccounts: diagnostics.timedOutAccounts,
182
+ };
183
+ const coverageBlock = partialCoverageBlock(diagnostics);
184
+ if (merged.length === 0) {
185
+ const base = diagnostics.partial
186
+ ? `No messages found in the portions that were ${verb === "matched" ? "searched" : "listed"}.`
187
+ : "No messages found";
188
+ return successResponse(`${base}${coverageBlock}`, structured);
189
+ }
190
+ const parts = [];
191
+ if (fan.accountsQueried.length > 0)
192
+ parts.push(`IMAP account(s): ${fan.accountsQueried.join(", ")}`);
193
+ if (apple.rows.length > 0)
194
+ parts.push("AppleScript");
195
+ const accountsNote = parts.length > 0 ? ` (merged across ${parts.join(" + ")})` : "";
196
+ return successResponse(`Found ${merged.length} message(s)${accountsNote}:\n${formatMergedRows(merged)}${coverageBlock}`, structured);
197
+ }
119
198
  // =============================================================================
120
199
  // Server Initialization
121
200
  // =============================================================================
@@ -190,13 +269,17 @@ server.registerTool("search-messages", {
190
269
  },
191
270
  outputSchema: LIST_OUTPUT_SCHEMA,
192
271
  }, withErrorHandling(async ({ query, mailbox, account, limit = 50, dateFrom, dateTo, from, subject, isRead, isFlagged, }) => {
193
- // IMAP backend (issue #43): server-side search when this account is
194
- // explicitly configured for IMAP; otherwise fall through to AppleScript.
195
- if (isImapAccount(account)) {
196
- const r = await imapSearchMessages({
272
+ // IMAP backend: prefer direct IMAP whenever IMAP is configured (v2.6.0).
273
+ // - explicit IMAP account single-account IMAP (fast path);
274
+ // - no account + IMAP configured → MERGE: IMAP fans out over every
275
+ // configured account; AppleScript scans ONLY the accounts no IMAP
276
+ // config covers (partitioned — so an all-IMAP user runs ZERO
277
+ // AppleScript and never relies on the composite dedup);
278
+ // - explicit non-IMAP account (or IMAP unconfigured) → AppleScript below.
279
+ if (shouldUseImap(account)) {
280
+ const imapArgs = {
197
281
  query,
198
282
  mailbox,
199
- account,
200
283
  limit,
201
284
  dateFrom,
202
285
  dateTo,
@@ -204,12 +287,19 @@ server.registerTool("search-messages", {
204
287
  subject,
205
288
  isRead,
206
289
  isFlagged,
207
- });
208
- return successResponse(r.text, {
209
- messages: r.messages,
210
- count: r.count,
211
- partial: r.partial,
212
- });
290
+ };
291
+ if (account !== undefined) {
292
+ const r = await imapSearchMessages({ ...imapArgs, account });
293
+ return successResponse(r.text, {
294
+ messages: r.messages,
295
+ count: r.count,
296
+ partial: r.partial,
297
+ });
298
+ }
299
+ const fan = await fanOutImapMessages(imapArgs, "search");
300
+ const { appleScriptOnly } = partitionAccountsForCounts(mailManager.listAccounts(), resolveImapConfigs());
301
+ const apple = appleScanForAccounts(appleScriptOnly, (acctName) => mailManager.searchMessagesWithDiagnostics(query, mailbox, acctName, limit, dateFrom, dateTo, from, subject, isRead, isFlagged));
302
+ return mergedMessageResponse(fan, apple, limit, "matched");
213
303
  }
214
304
  const { messages, diagnostics } = mailManager.searchMessagesWithDiagnostics(query, mailbox, account, limit, dateFrom, dateTo, from, subject, isRead, isFlagged);
215
305
  const coverageBlock = partialCoverageBlock(diagnostics);
@@ -323,15 +413,48 @@ server.registerTool("get-thread", {
323
413
  if (!seedSubject)
324
414
  return errorResponse(`Could not determine the subject of message "${id}"`);
325
415
  const base = normalizeSubject(seedSubject);
326
- // IMAP backend: server-side subject search.
327
- if (isImapAccount(account)) {
328
- const r = await imapSearchMessages({ subject: base, mailbox, account, limit });
329
- return successResponse(`Thread "${base}":\n${r.text}`, {
330
- subject: base,
331
- messages: r.messages,
332
- count: r.count,
333
- partial: r.partial,
416
+ // IMAP backend: server-side subject search (prefer-IMAP, v2.6.0).
417
+ // - explicit IMAP account → single-account IMAP subject search;
418
+ // - no account + IMAP configured → MERGE: IMAP fan-out subject search over
419
+ // every configured account + AppleScript subject search over ONLY the
420
+ // accounts no IMAP config covers (partitioned), then re-order oldest-
421
+ // first for natural thread reading.
422
+ if (shouldUseImap(account)) {
423
+ if (account !== undefined) {
424
+ const r = await imapSearchMessages({ subject: base, mailbox, account, limit });
425
+ return successResponse(`Thread "${base}":\n${r.text}`, {
426
+ subject: base,
427
+ messages: r.messages,
428
+ count: r.count,
429
+ partial: r.partial,
430
+ });
431
+ }
432
+ const fan = await fanOutImapMessages({ subject: base, mailbox, limit }, "search");
433
+ const { appleScriptOnly } = partitionAccountsForCounts(mailManager.listAccounts(), resolveImapConfigs());
434
+ const apple = appleScanForAccounts(appleScriptOnly, (acctName) => mailManager.searchMessagesWithDiagnostics(undefined, mailbox, acctName, limit, undefined, undefined, undefined, base));
435
+ // Merge + de-dup (IMAP wins), then sort OLDEST-first for thread order.
436
+ const mergedNewestFirst = mergeMessages(fan.rows, apple.rows, limit);
437
+ const orderedRows = mergedNewestFirst
438
+ .slice()
439
+ .reverse() // mergeMessages returns newest-first; threads read oldest-first
440
+ .sort((a, b) => (a.dateReceived ? new Date(a.dateReceived).getTime() : 0) -
441
+ (b.dateReceived ? new Date(b.dateReceived).getTime() : 0));
442
+ const partial = apple.diagnostics.partial || fan.accountsFailed.length > 0;
443
+ const coverage = partialCoverageBlock({
444
+ ...apple.diagnostics,
445
+ partial,
446
+ timedOutAccounts: [...apple.diagnostics.timedOutAccounts, ...fan.accountsFailed],
334
447
  });
448
+ const structured = {
449
+ subject: base,
450
+ messages: orderedRows,
451
+ count: orderedRows.length,
452
+ partial,
453
+ };
454
+ if (orderedRows.length === 0) {
455
+ return successResponse(`No messages found in thread "${base}".${coverage}`, structured);
456
+ }
457
+ return successResponse(`Thread "${base}" — ${orderedRows.length} message(s), oldest first:\n${formatMergedRows(orderedRows)}${coverage}`, structured);
335
458
  }
336
459
  const { messages, diagnostics } = mailManager.searchMessagesWithDiagnostics(undefined, mailbox, account, limit, undefined, undefined, undefined, base);
337
460
  // Oldest-first is the natural reading order for a conversation.
@@ -369,15 +492,27 @@ server.registerTool("list-messages", {
369
492
  },
370
493
  outputSchema: LIST_OUTPUT_SCHEMA,
371
494
  }, withErrorHandling(async ({ mailbox, account, limit = 50, offset = 0, from, unreadOnly }) => {
372
- // IMAP backend (issue #43): server-side listing when this account is
373
- // explicitly configured for IMAP; otherwise fall through to AppleScript.
374
- if (isImapAccount(account)) {
375
- const r = await imapListMessages({ mailbox, account, limit, offset, from, unreadOnly });
376
- return successResponse(r.text, {
377
- messages: r.messages,
378
- count: r.count,
379
- partial: r.partial,
380
- });
495
+ // IMAP backend: prefer direct IMAP whenever IMAP is configured (v2.6.0).
496
+ // - explicit IMAP account single-account IMAP listing (fast path);
497
+ // - no account + IMAP configured → MERGE: IMAP fans out over every
498
+ // configured account; AppleScript lists ONLY the accounts no IMAP config
499
+ // covers (partitioned — all-IMAP user runs ZERO AppleScript). NOTE:
500
+ // pagination via `offset` is applied PER-BACKEND before the merge, so
501
+ // deep offsets in a merged multi-account list are approximate — recommend
502
+ // scoping with an `account` for exact pagination.
503
+ if (shouldUseImap(account)) {
504
+ if (account !== undefined) {
505
+ const r = await imapListMessages({ mailbox, account, limit, offset, from, unreadOnly });
506
+ return successResponse(r.text, {
507
+ messages: r.messages,
508
+ count: r.count,
509
+ partial: r.partial,
510
+ });
511
+ }
512
+ const fan = await fanOutImapMessages({ mailbox, limit, offset, from, unreadOnly }, "list");
513
+ const { appleScriptOnly } = partitionAccountsForCounts(mailManager.listAccounts(), resolveImapConfigs());
514
+ const apple = appleScanForAccounts(appleScriptOnly, (acctName) => mailManager.listMessagesWithDiagnostics(mailbox, acctName, limit, from, offset));
515
+ return mergedMessageResponse(fan, apple, limit, "listed");
381
516
  }
382
517
  const { messages, diagnostics } = mailManager.listMessagesWithDiagnostics(mailbox, account, limit, from, offset);
383
518
  const coverageBlock = partialCoverageBlock(diagnostics);
@@ -1058,21 +1193,65 @@ server.registerTool("list-mailboxes", {
1058
1193
  },
1059
1194
  }, withErrorHandling(async ({ account }) => {
1060
1195
  // IMAP (I6): LIST + per-mailbox STATUS — sees the true server hierarchy and
1061
- // authoritative counts; falls back to AppleScript for non-IMAP accounts.
1062
- if (isImapAccount(account)) {
1063
- const boxes = await imapListMailboxes({ account });
1064
- const structured = {
1065
- mailboxes: boxes.map((b) => ({
1066
- name: b.path,
1067
- unreadCount: b.unseen,
1068
- messageCount: b.messages,
1069
- })),
1070
- count: boxes.length,
1071
- };
1072
- if (boxes.length === 0)
1196
+ // authoritative counts. Prefer-IMAP (v2.6.0):
1197
+ // - explicit IMAP account → that account's IMAP mailboxes;
1198
+ // - no account + IMAP configured → concatenate every configured IMAP
1199
+ // account's mailboxes (each name prefixed with its account label to
1200
+ // disambiguate identical mailbox names across accounts) PLUS the
1201
+ // AppleScript mailboxes of every account NOT covered by IMAP.
1202
+ if (shouldUseImap(account)) {
1203
+ if (account !== undefined) {
1204
+ const boxes = await imapListMailboxes({ account });
1205
+ const structured = {
1206
+ mailboxes: boxes.map((b) => ({
1207
+ name: b.path,
1208
+ unreadCount: b.unseen,
1209
+ messageCount: b.messages,
1210
+ })),
1211
+ count: boxes.length,
1212
+ };
1213
+ if (boxes.length === 0)
1214
+ return successResponse("No mailboxes found", structured);
1215
+ const list = boxes.map((b) => ` - ${b.path} (${b.unseen} unread)`).join("\n");
1216
+ return successResponse(`Found ${boxes.length} mailbox(es):\n${list}`, structured);
1217
+ }
1218
+ const configs = resolveImapConfigs();
1219
+ const rows = [];
1220
+ for (const config of configs) {
1221
+ try {
1222
+ const boxes = await imapListMailboxes({ config });
1223
+ for (const b of boxes) {
1224
+ // Prefix with the account label so "INBOX" from two accounts is
1225
+ // distinguishable; keep the raw path available via the structured row.
1226
+ rows.push({
1227
+ name: `${config.accountLabel}/${b.path}`,
1228
+ account: config.accountLabel,
1229
+ unreadCount: b.unseen,
1230
+ messageCount: b.messages,
1231
+ });
1232
+ }
1233
+ }
1234
+ catch (e) {
1235
+ console.error(`IMAP list-mailboxes failed for "${config.accountLabel}": ${String(e)}`);
1236
+ }
1237
+ }
1238
+ // AppleScript for the accounts IMAP doesn't cover (no double-listing).
1239
+ const { appleScriptOnly } = partitionAccountsForCounts(mailManager.listAccounts(), configs);
1240
+ for (const acct of appleScriptOnly) {
1241
+ for (const mb of mailManager.listMailboxes(acct.name)) {
1242
+ rows.push({
1243
+ name: `${acct.name}/${mb.name}`,
1244
+ account: acct.name,
1245
+ unreadCount: mb.unreadCount,
1246
+ messageCount: mb.messageCount,
1247
+ });
1248
+ }
1249
+ }
1250
+ const structured = { mailboxes: rows, count: rows.length };
1251
+ if (rows.length === 0)
1073
1252
  return successResponse("No mailboxes found", structured);
1074
- const list = boxes.map((b) => ` - ${b.path} (${b.unseen} unread)`).join("\n");
1075
- return successResponse(`Found ${boxes.length} mailbox(es):\n${list}`, structured);
1253
+ const list = rows.map((b) => ` - ${b.name} (${b.unreadCount} unread)`).join("\n");
1254
+ return successResponse(`Found ${rows.length} mailbox(es):\n${list}`, structured);
1076
1255
  }
1077
1256
  const mailboxes = mailManager.listMailboxes(account);
1078
1257
  const structured = { mailboxes, count: mailboxes.length };
@@ -1096,10 +1275,40 @@ server.registerTool("get-unread-count", {
1096
1275
  },
1097
1276
  }, withErrorHandling(async ({ mailbox, account }) => {
1098
1277
  // IMAP (I4): STATUS (UNSEEN) is authoritative and fast even on huge
1099
- // mailboxes; falls back to AppleScript for non-IMAP accounts.
1100
- const count = isImapAccount(account)
1101
- ? await imapUnreadCount(mailbox, { account })
1102
- : mailManager.getUnreadCount(mailbox, account);
1278
+ // mailboxes. Prefer-IMAP (v2.6.0). Counts are ACCOUNT-CENTRIC so each account
1279
+ // is counted exactly once even if the coverage heuristic mis-matches:
1280
+ // - explicit IMAP account → IMAP UNSEEN for that account;
1281
+ // - no account + IMAP configured → planCountSources assigns each account
1282
+ // ONE source (its matching IMAP config, else AppleScript) and counts any
1283
+ // config that matched no account once via IMAP — no double-counting;
1284
+ // - explicit non-IMAP account (or IMAP unconfigured) → AppleScript.
1285
+ let count;
1286
+ if (shouldUseImap(account)) {
1287
+ if (account !== undefined) {
1288
+ count = await imapUnreadCount(mailbox, { account });
1289
+ }
1290
+ else {
1291
+ const sources = planCountSources(mailManager.listAccounts(), resolveImapConfigs());
1292
+ let total = 0;
1293
+ for (const src of sources) {
1294
+ if (src.kind === "imap") {
1295
+ try {
1296
+ total += await imapUnreadCount(mailbox, { config: src.config });
1297
+ }
1298
+ catch (e) {
1299
+ console.error(`IMAP unread-count failed for "${src.label}": ${String(e)}`);
1300
+ }
1301
+ }
1302
+ else {
1303
+ total += mailManager.getUnreadCount(mailbox, src.account.name);
1304
+ }
1305
+ }
1306
+ count = total;
1307
+ }
1308
+ }
1309
+ else {
1310
+ count = mailManager.getUnreadCount(mailbox, account);
1311
+ }
1103
1312
  const location = mailbox ? ` in "${mailbox}"` : "";
1104
1313
  return successResponse(`${count} unread message(s)${location}`, {
1105
1314
  unread: count,
@@ -1538,7 +1747,12 @@ server.registerTool("get-mail-stats", {
1538
1747
  }, withErrorHandling(async ({ account }) => {
1539
1748
  // IMAP (I3): for a named IMAP account, STATUS gives authoritative counts and
1540
1749
  // SEARCH SINCE gives recent activity — fast even on huge mailboxes.
1541
- if (account && isImapAccount(account)) {
1750
+ // - explicit IMAP account IMAP STATUS for that account (today's path);
1751
+ // - explicit non-IMAP account → AppleScript all-accounts stats below;
1752
+ // - no account + IMAP configured → MERGE: sum IMAP STATUS over every
1753
+ // configured account + AppleScript per-account stats for the accounts
1754
+ // IMAP does NOT cover (partitioned so no account is double-counted).
1755
+ if (account !== undefined && isImapAccount(account)) {
1542
1756
  const s = await imapMailStats({ account });
1543
1757
  const lines = [
1544
1758
  `📊 Mail Statistics — ${account} (IMAP)`,
@@ -1553,6 +1767,77 @@ server.registerTool("get-mail-stats", {
1553
1767
  ];
1554
1768
  return successResponse(lines.join("\n"), { account, ...s });
1555
1769
  }
1770
+ if (account === undefined && shouldUseImap(account)) {
1771
+ let totalMessages = 0;
1772
+ let totalUnread = 0;
1773
+ const recent = { last24h: 0, last7d: 0, last30d: 0 };
1774
+ const perAccount = [];
1775
+ // ACCOUNT-CENTRIC: each account counted via exactly ONE source so a
1776
+ // heuristic mis-match can't double-count. IMAP sources use STATUS; the
1777
+ // AppleScript sources are built from listMailboxes (same source
1778
+ // getMailStats uses) — never getMailStats(), which is all-accounts and
1779
+ // would re-count the IMAP-covered ones. Recently-received from AppleScript
1780
+ // is INBOX-wide (not per-account), so it's omitted for AppleScript sources;
1781
+ // IMAP's per-account recent IS included.
1782
+ const sources = planCountSources(mailManager.listAccounts(), resolveImapConfigs());
1783
+ for (const src of sources) {
1784
+ if (src.kind === "imap") {
1785
+ try {
1786
+ const s = await imapMailStats({ config: src.config });
1787
+ totalMessages += s.totalMessages;
1788
+ totalUnread += s.totalUnread;
1789
+ recent.last24h += s.recent.last24h;
1790
+ recent.last7d += s.recent.last7d;
1791
+ recent.last30d += s.recent.last30d;
1792
+ perAccount.push({
1793
+ name: src.label,
1794
+ totalMessages: s.totalMessages,
1795
+ unreadMessages: s.totalUnread,
1796
+ backend: "imap",
1797
+ });
1798
+ }
1799
+ catch (e) {
1800
+ console.error(`IMAP mail-stats failed for "${src.label}": ${String(e)}`);
1801
+ }
1802
+ }
1803
+ else {
1804
+ let m = 0;
1805
+ let u = 0;
1806
+ for (const mb of mailManager.listMailboxes(src.account.name)) {
1807
+ m += mb.messageCount;
1808
+ u += mb.unreadCount;
1809
+ }
1810
+ totalMessages += m;
1811
+ totalUnread += u;
1812
+ perAccount.push({
1813
+ name: src.label,
1814
+ totalMessages: m,
1815
+ unreadMessages: u,
1816
+ backend: "applescript",
1817
+ });
1818
+ }
1819
+ }
1820
+ const lines = [
1821
+ `📊 Mail Statistics (merged: IMAP + AppleScript)`,
1822
+ `══════════════════`,
1823
+ `Total messages: ${totalMessages}`,
1824
+ `Unread messages: ${totalUnread}`,
1825
+ ``,
1826
+ `📥 Recently Received (IMAP INBOXes):`,
1827
+ ` Last 24 hours: ${recent.last24h}`,
1828
+ ` Last 7 days: ${recent.last7d}`,
1829
+ ` Last 30 days: ${recent.last30d}`,
1830
+ ``,
1831
+ `📁 By Account:`,
1832
+ ...perAccount.map((a) => ` ${a.name}: ${a.totalMessages} messages (${a.unreadMessages} unread) [${a.backend}]`),
1833
+ ];
1834
+ return successResponse(lines.join("\n"), {
1835
+ totalMessages,
1836
+ totalUnread,
1837
+ accounts: perAccount,
1838
+ recent,
1839
+ });
1840
+ }
1556
1841
  const stats = mailManager.getMailStats();
1557
1842
  const lines = [];
1558
1843
  lines.push(`📊 Mail Statistics`);
@@ -1662,8 +1947,14 @@ const shutdown = () => {
1662
1947
  if (_shuttingDown)
1663
1948
  return;
1664
1949
  _shuttingDown = true;
1950
+ // Stop the parent-death watchdog (defined just below) so it can't re-enter.
1951
+ clearInterval(orphanWatchdog);
1665
1952
  const force = setTimeout(() => process.exit(0), 2000);
1666
1953
  force.unref?.();
1954
+ // Closing the IDLE watcher logs out its dedicated per-account connections
1955
+ // (one persistent socket per account when APPLE_MAIL_MCP_IMAP_IDLE=1) and
1956
+ // dropAllPools() closes the request pool — together this releases EVERY IMAP
1957
+ // socket this instance holds, which is the whole point of the orphan check.
1667
1958
  void Promise.allSettled([idleWatcher?.stop() ?? Promise.resolve(), dropAllPools()]).finally(() => process.exit(0));
1668
1959
  };
1669
1960
  for (const sig of ["SIGINT", "SIGTERM"]) {
@@ -1671,3 +1962,19 @@ for (const sig of ["SIGINT", "SIGTERM"]) {
1671
1962
  }
1672
1963
  process.stdin.on("end", shutdown);
1673
1964
  process.stdin.on("close", shutdown);
1965
+ // Parent-death watchdog (connection-footprint hardening, v2.6.1). The exit
1966
+ // paths above all rely on a signal or stdin-EOF, but a host (claude-code) that
1967
+ // is force-quit or crashes delivers neither — leaving this process orphaned and
1968
+ // still holding its IMAP sockets (the IDLE watcher's per-account connections
1969
+ // and/or pooled request connections) against Gmail's ~15-per-account cap, which
1970
+ // can starve Apple Mail of connection slots. On macOS an orphan is reparented to
1971
+ // launchd (ppid 1), so poll for that and self-shutdown. At normal startup ppid
1972
+ // is the real parent, so this never misfires; it's unref'd (never keeps the
1973
+ // event loop alive) and is cleared in shutdown(). `shutdown` only reads this
1974
+ // binding at runtime (long after it's initialized), so the forward reference is
1975
+ // safe.
1976
+ const orphanWatchdog = setInterval(() => {
1977
+ if (isOrphaned())
1978
+ shutdown();
1979
+ }, 30_000);
1980
+ orphanWatchdog.unref?.();
@@ -139,6 +139,20 @@ export interface ImapDeps {
139
139
  }
140
140
  /** True when `account` matches any configured IMAP account (label or user). */
141
141
  export declare function isImapAccount(account: string | undefined, env?: NodeJS.ProcessEnv): boolean;
142
+ /**
143
+ * Read-side routing gate (v2.6.0 — prefer-IMAP reads). Returns true when a read
144
+ * tool should go to IMAP rather than AppleScript:
145
+ * - IMAP is configured at all, AND
146
+ * - either the caller named no account (→ merge across all accounts), or the
147
+ * named account is itself a configured IMAP account.
148
+ * An explicitly-named NON-IMAP account returns false → AppleScript. When IMAP is
149
+ * not configured at all this is always false, so behavior is unchanged.
150
+ *
151
+ * NOTE: the 3 mailbox-WRITE ops (create/delete/rename-mailbox) deliberately keep
152
+ * using `isImapAccount` — they only route to IMAP for an explicitly-named IMAP
153
+ * account, never on an omitted account.
154
+ */
155
+ export declare function shouldUseImap(account: string | undefined, env?: NodeJS.ProcessEnv): boolean;
142
156
  /** Account labels of every configured IMAP account (C2), for diagnostics. */
143
157
  export declare function listImapAccountLabels(env?: NodeJS.ProcessEnv): string[];
144
158
  /**
@@ -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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"}
@@ -142,6 +142,22 @@ export function isImapAccount(account, env = process.env) {
142
142
  return false;
143
143
  return listImapAccountSpecs(env).some((s) => s.accountLabel === account || s.user === account);
144
144
  }
145
+ /**
146
+ * Read-side routing gate (v2.6.0 — prefer-IMAP reads). Returns true when a read
147
+ * tool should go to IMAP rather than AppleScript:
148
+ * - IMAP is configured at all, AND
149
+ * - either the caller named no account (→ merge across all accounts), or the
150
+ * named account is itself a configured IMAP account.
151
+ * An explicitly-named NON-IMAP account returns false → AppleScript. When IMAP is
152
+ * not configured at all this is always false, so behavior is unchanged.
153
+ *
154
+ * NOTE: the 3 mailbox-WRITE ops (create/delete/rename-mailbox) deliberately keep
155
+ * using `isImapAccount` — they only route to IMAP for an explicitly-named IMAP
156
+ * account, never on an omitted account.
157
+ */
158
+ export function shouldUseImap(account, env = process.env) {
159
+ return (listImapAccountSpecs(env).length > 0 && (account === undefined || isImapAccount(account, env)));
160
+ }
145
161
  /** Account labels of every configured IMAP account (C2), for diagnostics. */
146
162
  export function listImapAccountLabels(env = process.env) {
147
163
  return listImapAccountSpecs(env).map((s) => s.accountLabel);
@@ -278,6 +294,11 @@ function structuredRow(m, account, path) {
278
294
  mailbox: path,
279
295
  account,
280
296
  hasAttachments: false,
297
+ // Message-ID (when the envelope carries it) is the strongest cross-/intra-
298
+ // backend dedup key for the multi-account merge (imapMultiAccount.ts). The
299
+ // AppleScript path does not expose it, so cross-backend dedup falls back to
300
+ // the subject|sender|date composite key.
301
+ ...(env.messageId ? { messageId: env.messageId } : {}),
281
302
  };
282
303
  }
283
304
  async function run(args, listMode, deps) {
@@ -448,7 +469,11 @@ function imapIdleMs() {
448
469
  if (Number.isFinite(n) && n >= 0)
449
470
  return n;
450
471
  }
451
- return 60_000;
472
+ // Default 30s (v2.6.1): close the pooled connection sooner so this instance
473
+ // gives its IMAP slot back quickly — important when several instances coexist
474
+ // against Gmail's ~15-per-account cap and Apple Mail also needs slots. Tune
475
+ // with APPLE_MAIL_MCP_IMAP_IDLE_MS (0 = never close).
476
+ return 30_000;
452
477
  }
453
478
  async function dropPool(key) {
454
479
  const e = pools.get(key);
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Multi-account read merge (v2.6.0 — prefer-IMAP reads).
3
+ *
4
+ * v2.6.0 flips the read tools to PREFER direct IMAP whenever IMAP is configured
5
+ * (see `shouldUseImap` in imapClient.ts). When a read is issued with NO explicit
6
+ * account, results must still cover EVERY account the user has — so we:
7
+ *
8
+ * 1. fan the IMAP query out over every configured IMAP account
9
+ * (`resolveImapConfigs()`), and
10
+ * 2. ALSO run the existing AppleScript all-accounts path,
11
+ *
12
+ * then merge the two, de-duplicating any message that appears in both backends
13
+ * (e.g. a Gmail account that is both IMAP-configured AND visible to Mail.app's
14
+ * AppleScript scan) and preferring the IMAP copy (it carries the round-trippable
15
+ * `imap:` id that the mutation tools need).
16
+ *
17
+ * This module holds the backend-agnostic merge/dedup/sort + the per-account IMAP
18
+ * fan-out so the three message-list sites (search-messages, get-thread,
19
+ * list-messages) don't each re-implement it.
20
+ *
21
+ * @module services/imapMultiAccount
22
+ */
23
+ import { type ImapSearchArgs, type ImapConfig, type ImapDeps } from "../services/imapClient.js";
24
+ import type { Account } from "../types.js";
25
+ /** A structured message row as emitted by either backend (permissive on keys). */
26
+ export type MessageRow = Record<string, unknown>;
27
+ /**
28
+ * Cross-backend dedup key for a message row.
29
+ *
30
+ * - PREFER the normalized Message-ID when present (`mid:<id>`). It's the only
31
+ * globally-unique identity, so two IMAP accounts that both hold the very
32
+ * same message (rare, but possible with multi-delivery) collapse to one.
33
+ * - FALL BACK to `normalizedSubject|sender|dateReceivedEpoch` when no
34
+ * Message-ID is available. The AppleScript backend never exposes a
35
+ * Message-ID, so this composite is what actually dedups the common case: a
36
+ * Gmail account surfaced by BOTH the IMAP fan-out and the AppleScript
37
+ * all-accounts scan. The IMAP and AppleScript copies of one message share
38
+ * the same subject, sender, and received timestamp, so they collide here.
39
+ *
40
+ * Limitation (note for live testing): the composite key assumes the two backends
41
+ * report the SAME received timestamp to the second. If Mail.app and the IMAP
42
+ * server disagree on `dateReceived` (timezone/rounding), a message could escape
43
+ * dedup and appear twice. Message-ID dedup (IMAP-vs-IMAP) is exact; the
44
+ * composite is best-effort.
45
+ */
46
+ export declare function dedupKey(row: MessageRow): string;
47
+ /**
48
+ * Merge IMAP rows with AppleScript rows: concatenate, de-dup by {@link dedupKey}
49
+ * preferring the IMAP copy (IMAP rows are passed first and win on collision),
50
+ * sort newest-first by dateReceived, then apply `limit`.
51
+ */
52
+ export declare function mergeMessages(imapRows: MessageRow[], appleRows: MessageRow[], limit: number): MessageRow[];
53
+ /** Gmail/Workspace IMAP host? Its `[Gmail]/All Mail` virtual mailbox is Gmail-only. */
54
+ export declare function isGmailHost(host: string): boolean;
55
+ /**
56
+ * Fan an IMAP message query out over EVERY configured IMAP account and return
57
+ * the concatenated structured rows (not yet merged with AppleScript, not yet
58
+ * limited — the caller merges + limits). `kind` picks the underlying query so
59
+ * the mailbox-default and unreadOnly semantics match the single-account path.
60
+ *
61
+ * Per-account failures are swallowed (logged) so one unreachable account doesn't
62
+ * sink the whole read; the returned `accountsQueried`/`accountsFailed` let the
63
+ * caller surface partial-coverage diagnostics.
64
+ */
65
+ export declare function fanOutImapMessages(args: ImapSearchArgs, kind: "search" | "list", deps?: Omit<ImapDeps, "config" | "account">, configs?: ImapConfig[]): Promise<{
66
+ rows: MessageRow[];
67
+ accountsQueried: string[];
68
+ accountsFailed: string[];
69
+ }>;
70
+ /**
71
+ * Per-pair matcher: does this IMAP config correspond to this Mail.app account?
72
+ * Compared case-insensitively, the config's `accountLabel` AND `user` against the
73
+ * Mail account's `name` AND `email`. An empty email can't match (avoids
74
+ * `"" === ""` false positives). This is the single source of truth for coverage;
75
+ * everything else delegates here.
76
+ */
77
+ export declare function configMatchesAccount(config: ImapConfig, account: Account): boolean;
78
+ /** True when ANY config matches this Mail account (delegates to the per-pair matcher). */
79
+ export declare function isAccountCoveredByImap(account: Account, configs: ImapConfig[]): boolean;
80
+ /**
81
+ * Split Mail.app accounts into those covered by IMAP (counted via IMAP) and the
82
+ * rest (counted via AppleScript), so reads/counts skip the AppleScript scan for
83
+ * IMAP-covered accounts.
84
+ */
85
+ export declare function partitionAccountsForCounts(accounts: Account[], configs: ImapConfig[]): {
86
+ imapCovered: Account[];
87
+ appleScriptOnly: Account[];
88
+ };
89
+ /**
90
+ * One unit to count for the count tools (get-unread-count / get-mail-stats),
91
+ * carrying exactly ONE source so every account is counted once and only once:
92
+ * - `{ kind: "imap", config }` — count this account via IMAP STATUS;
93
+ * - `{ kind: "applescript", account }`— count this Mail account via AppleScript.
94
+ */
95
+ export type CountSource = {
96
+ kind: "imap";
97
+ config: ImapConfig;
98
+ label: string;
99
+ } | {
100
+ kind: "applescript";
101
+ account: Account;
102
+ label: string;
103
+ };
104
+ /**
105
+ * Plan the count sources so NO account is double-counted even when the coverage
106
+ * heuristic mis-matches (the failure mode the naive `Σimap(all configs) +
107
+ * Σapple(uncovered)` had: a config that fails to match its Mail account lands in
108
+ * BOTH sums).
109
+ *
110
+ * Account-centric: walk the Mail.app accounts; each is counted via its FIRST
111
+ * matching config (IMAP) or, if none matches, via AppleScript. Then any IMAP
112
+ * config that matched NO Mail account (configured-but-not-present-in-Mail.app) is
113
+ * added once as an IMAP source. A config is consumed by at most one Mail account,
114
+ * so two Mail accounts can't both claim the same config and inflate the total.
115
+ */
116
+ export declare function planCountSources(accounts: Account[], configs: ImapConfig[]): CountSource[];
117
+ /**
118
+ * Render the structured message rows into the human text block the read tools
119
+ * emit (` - ID: … | date | subject (from: sender) [read|unread]`). Shared so the
120
+ * merged path matches the single-backend formatting. `showReadState` mirrors
121
+ * search/list (which show [read]) vs. plain list rows.
122
+ */
123
+ export declare function formatMergedRows(rows: MessageRow[], showReadState?: boolean): string;
124
+ //# sourceMappingURL=imapMultiAccount.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"imapMultiAccount.d.ts","sourceRoot":"","sources":["../../src/services/imapMultiAccount.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,QAAQ,EACd,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,kFAAkF;AAClF,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAuCjD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAMhD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,UAAU,EAAE,EACtB,SAAS,EAAE,UAAU,EAAE,EACvB,KAAK,EAAE,MAAM,GACZ,UAAU,EAAE,CAcd;AAED,uFAAuF;AACvF,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,cAAc,EACpB,IAAI,EAAE,QAAQ,GAAG,MAAM,EACvB,IAAI,GAAE,IAAI,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAM,EAC/C,OAAO,GAAE,UAAU,EAAyB,GAC3C,OAAO,CAAC;IAAE,IAAI,EAAE,UAAU,EAAE,CAAC;IAAC,eAAe,EAAE,MAAM,EAAE,CAAC;IAAC,cAAc,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CA2BtF;AAmBD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAQlF;AAED,0FAA0F;AAC1F,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,OAAO,CAEvF;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,UAAU,EAAE,GACpB;IAAE,WAAW,EAAE,OAAO,EAAE,CAAC;IAAC,eAAe,EAAE,OAAO,EAAE,CAAA;CAAE,CAQxD;AAED;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnD;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7D;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,WAAW,EAAE,CAmB1F;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,aAAa,UAAO,GAAG,MAAM,CAajF"}
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Multi-account read merge (v2.6.0 — prefer-IMAP reads).
3
+ *
4
+ * v2.6.0 flips the read tools to PREFER direct IMAP whenever IMAP is configured
5
+ * (see `shouldUseImap` in imapClient.ts). When a read is issued with NO explicit
6
+ * account, results must still cover EVERY account the user has — so we:
7
+ *
8
+ * 1. fan the IMAP query out over every configured IMAP account
9
+ * (`resolveImapConfigs()`), and
10
+ * 2. ALSO run the existing AppleScript all-accounts path,
11
+ *
12
+ * then merge the two, de-duplicating any message that appears in both backends
13
+ * (e.g. a Gmail account that is both IMAP-configured AND visible to Mail.app's
14
+ * AppleScript scan) and preferring the IMAP copy (it carries the round-trippable
15
+ * `imap:` id that the mutation tools need).
16
+ *
17
+ * This module holds the backend-agnostic merge/dedup/sort + the per-account IMAP
18
+ * fan-out so the three message-list sites (search-messages, get-thread,
19
+ * list-messages) don't each re-implement it.
20
+ *
21
+ * @module services/imapMultiAccount
22
+ */
23
+ import { imapSearchMessages, imapListMessages, resolveImapConfigs, } from "../services/imapClient.js";
24
+ /**
25
+ * Normalize a Message-ID for comparison: strip surrounding angle brackets and
26
+ * whitespace, lowercase. Returns undefined when the row has no usable id.
27
+ */
28
+ function normalizeMessageId(row) {
29
+ const raw = typeof row.messageId === "string" ? row.messageId.trim() : "";
30
+ if (!raw)
31
+ return undefined;
32
+ const inner = raw.replace(/^<+|>+$/g, "").trim();
33
+ return inner ? inner.toLowerCase() : undefined;
34
+ }
35
+ /**
36
+ * Normalize a subject the same way the thread tool does for grouping: drop
37
+ * leading Re:/Fwd: prefixes, collapse whitespace, lowercase. Kept local (and
38
+ * deliberately simple) so the merge has no dependency cycle with the thread
39
+ * module; exactness isn't required — this only feeds the fallback dedup key.
40
+ */
41
+ function normalizeSubjectForKey(subject) {
42
+ const s = typeof subject === "string" ? subject : "";
43
+ return s
44
+ .replace(/^(\s*(re|fwd|fw|aw|sv|antw)\s*:\s*)+/i, "")
45
+ .replace(/\s+/g, " ")
46
+ .trim()
47
+ .toLowerCase();
48
+ }
49
+ /** Epoch ms of the row's dateReceived (ISO string or Date), or 0 when absent. */
50
+ function dateEpoch(row) {
51
+ const d = row.dateReceived;
52
+ if (d instanceof Date)
53
+ return d.getTime();
54
+ if (typeof d === "string" && d) {
55
+ const t = new Date(d).getTime();
56
+ return Number.isNaN(t) ? 0 : t;
57
+ }
58
+ return 0;
59
+ }
60
+ /**
61
+ * Cross-backend dedup key for a message row.
62
+ *
63
+ * - PREFER the normalized Message-ID when present (`mid:<id>`). It's the only
64
+ * globally-unique identity, so two IMAP accounts that both hold the very
65
+ * same message (rare, but possible with multi-delivery) collapse to one.
66
+ * - FALL BACK to `normalizedSubject|sender|dateReceivedEpoch` when no
67
+ * Message-ID is available. The AppleScript backend never exposes a
68
+ * Message-ID, so this composite is what actually dedups the common case: a
69
+ * Gmail account surfaced by BOTH the IMAP fan-out and the AppleScript
70
+ * all-accounts scan. The IMAP and AppleScript copies of one message share
71
+ * the same subject, sender, and received timestamp, so they collide here.
72
+ *
73
+ * Limitation (note for live testing): the composite key assumes the two backends
74
+ * report the SAME received timestamp to the second. If Mail.app and the IMAP
75
+ * server disagree on `dateReceived` (timezone/rounding), a message could escape
76
+ * dedup and appear twice. Message-ID dedup (IMAP-vs-IMAP) is exact; the
77
+ * composite is best-effort.
78
+ */
79
+ export function dedupKey(row) {
80
+ const mid = normalizeMessageId(row);
81
+ if (mid)
82
+ return `mid:${mid}`;
83
+ const subject = normalizeSubjectForKey(row.subject);
84
+ const sender = (typeof row.sender === "string" ? row.sender : "").trim().toLowerCase();
85
+ return `k:${subject}|${sender}|${dateEpoch(row)}`;
86
+ }
87
+ /**
88
+ * Merge IMAP rows with AppleScript rows: concatenate, de-dup by {@link dedupKey}
89
+ * preferring the IMAP copy (IMAP rows are passed first and win on collision),
90
+ * sort newest-first by dateReceived, then apply `limit`.
91
+ */
92
+ export function mergeMessages(imapRows, appleRows, limit) {
93
+ const byKey = new Map();
94
+ // IMAP first so its copy wins; AppleScript only fills keys IMAP didn't supply.
95
+ for (const r of imapRows) {
96
+ const k = dedupKey(r);
97
+ if (!byKey.has(k))
98
+ byKey.set(k, r);
99
+ }
100
+ for (const r of appleRows) {
101
+ const k = dedupKey(r);
102
+ if (!byKey.has(k))
103
+ byKey.set(k, r);
104
+ }
105
+ const merged = [...byKey.values()];
106
+ merged.sort((a, b) => dateEpoch(b) - dateEpoch(a)); // newest first
107
+ return limit >= 0 ? merged.slice(0, limit) : merged;
108
+ }
109
+ /** Gmail/Workspace IMAP host? Its `[Gmail]/All Mail` virtual mailbox is Gmail-only. */
110
+ export function isGmailHost(host) {
111
+ return /(^|\.)gmail\.com$/i.test(host.trim());
112
+ }
113
+ /**
114
+ * Fan an IMAP message query out over EVERY configured IMAP account and return
115
+ * the concatenated structured rows (not yet merged with AppleScript, not yet
116
+ * limited — the caller merges + limits). `kind` picks the underlying query so
117
+ * the mailbox-default and unreadOnly semantics match the single-account path.
118
+ *
119
+ * Per-account failures are swallowed (logged) so one unreachable account doesn't
120
+ * sink the whole read; the returned `accountsQueried`/`accountsFailed` let the
121
+ * caller surface partial-coverage diagnostics.
122
+ */
123
+ export async function fanOutImapMessages(args, kind, deps = {}, configs = resolveImapConfigs()) {
124
+ const rows = [];
125
+ const accountsQueried = [];
126
+ const accountsFailed = [];
127
+ for (const config of configs) {
128
+ // Default-mailbox resolution is PER-ACCOUNT: the single-account search default
129
+ // ("[Gmail]/All Mail") is Gmail-only, so fanning it out to a non-Gmail account
130
+ // (e.g. iCloud) makes that SELECT fail and the account silently drops from the
131
+ // merged results. When the caller pinned no mailbox, give Gmail hosts their
132
+ // All-Mail default (undefined → resolved downstream) and every other host a
133
+ // universal "INBOX". (limit is applied AFTER the cross-account merge so the
134
+ // global newest-N is correct even when one account dominates.)
135
+ const mailbox = args.mailbox ?? (isGmailHost(config.host) ? undefined : "INBOX");
136
+ const perAccountArgs = { ...args, account: undefined, mailbox };
137
+ try {
138
+ const res = kind === "search"
139
+ ? await imapSearchMessages(perAccountArgs, { ...deps, config })
140
+ : await imapListMessages(perAccountArgs, { ...deps, config });
141
+ rows.push(...res.messages);
142
+ accountsQueried.push(config.accountLabel);
143
+ }
144
+ catch (e) {
145
+ accountsFailed.push(config.accountLabel);
146
+ console.error(`IMAP fan-out failed for account "${config.accountLabel}": ${String(e)}`);
147
+ }
148
+ }
149
+ return { rows, accountsQueried, accountsFailed };
150
+ }
151
+ // ---------------------------------------------------------------------------
152
+ // Account coverage + count partitioning (reads, get-unread-count, get-mail-stats)
153
+ //
154
+ // Coverage is decided per (config, Mail-account) PAIR by configMatchesAccount.
155
+ // - Message-list reads use partitionAccountsForCounts to run AppleScript ONLY
156
+ // for accounts no IMAP config covers (IMAP fans out over the configs), so an
157
+ // all-IMAP user runs zero AppleScript and never depends on composite dedup.
158
+ // - Count tools use planCountSources, which assigns each account EXACTLY ONE
159
+ // source (its matching config, else AppleScript) and adds any config that
160
+ // matched no account once — so even a heuristic MISS can't double-count.
161
+ //
162
+ // Matching is case-insensitive on the config's accountLabel/user vs. the Mail
163
+ // account's name/email. accountLabel defaults to the login address and users
164
+ // typically set it to the Mail.app account NAME, so checking both against both
165
+ // catches the common setups (label=accountName, label=email, login=email).
166
+ // ---------------------------------------------------------------------------
167
+ /**
168
+ * Per-pair matcher: does this IMAP config correspond to this Mail.app account?
169
+ * Compared case-insensitively, the config's `accountLabel` AND `user` against the
170
+ * Mail account's `name` AND `email`. An empty email can't match (avoids
171
+ * `"" === ""` false positives). This is the single source of truth for coverage;
172
+ * everything else delegates here.
173
+ */
174
+ export function configMatchesAccount(config, account) {
175
+ const name = account.name.trim().toLowerCase();
176
+ const email = (account.email ?? "").trim().toLowerCase();
177
+ const label = config.accountLabel.trim().toLowerCase();
178
+ const user = config.user.trim().toLowerCase();
179
+ return (label === name || (!!email && label === email) || user === name || (!!email && user === email));
180
+ }
181
+ /** True when ANY config matches this Mail account (delegates to the per-pair matcher). */
182
+ export function isAccountCoveredByImap(account, configs) {
183
+ return configs.some((c) => configMatchesAccount(c, account));
184
+ }
185
+ /**
186
+ * Split Mail.app accounts into those covered by IMAP (counted via IMAP) and the
187
+ * rest (counted via AppleScript), so reads/counts skip the AppleScript scan for
188
+ * IMAP-covered accounts.
189
+ */
190
+ export function partitionAccountsForCounts(accounts, configs) {
191
+ const imapCovered = [];
192
+ const appleScriptOnly = [];
193
+ for (const a of accounts) {
194
+ if (isAccountCoveredByImap(a, configs))
195
+ imapCovered.push(a);
196
+ else
197
+ appleScriptOnly.push(a);
198
+ }
199
+ return { imapCovered, appleScriptOnly };
200
+ }
201
+ /**
202
+ * Plan the count sources so NO account is double-counted even when the coverage
203
+ * heuristic mis-matches (the failure mode the naive `Σimap(all configs) +
204
+ * Σapple(uncovered)` had: a config that fails to match its Mail account lands in
205
+ * BOTH sums).
206
+ *
207
+ * Account-centric: walk the Mail.app accounts; each is counted via its FIRST
208
+ * matching config (IMAP) or, if none matches, via AppleScript. Then any IMAP
209
+ * config that matched NO Mail account (configured-but-not-present-in-Mail.app) is
210
+ * added once as an IMAP source. A config is consumed by at most one Mail account,
211
+ * so two Mail accounts can't both claim the same config and inflate the total.
212
+ */
213
+ export function planCountSources(accounts, configs) {
214
+ const sources = [];
215
+ const usedConfigs = new Set();
216
+ for (const account of accounts) {
217
+ const match = configs.find((c) => !usedConfigs.has(c) && configMatchesAccount(c, account));
218
+ if (match) {
219
+ usedConfigs.add(match);
220
+ sources.push({ kind: "imap", config: match, label: account.name });
221
+ }
222
+ else {
223
+ sources.push({ kind: "applescript", account, label: account.name });
224
+ }
225
+ }
226
+ // IMAP accounts that exist in config but not in Mail.app — count them once.
227
+ for (const config of configs) {
228
+ if (!usedConfigs.has(config)) {
229
+ sources.push({ kind: "imap", config, label: config.accountLabel });
230
+ }
231
+ }
232
+ return sources;
233
+ }
234
+ /**
235
+ * Render the structured message rows into the human text block the read tools
236
+ * emit (` - ID: … | date | subject (from: sender) [read|unread]`). Shared so the
237
+ * merged path matches the single-backend formatting. `showReadState` mirrors
238
+ * search/list (which show [read]) vs. plain list rows.
239
+ */
240
+ export function formatMergedRows(rows, showReadState = true) {
241
+ return rows
242
+ .map((m) => {
243
+ const date = (() => {
244
+ const e = dateEpoch(m);
245
+ return e ? new Date(e).toLocaleDateString() : "";
246
+ })();
247
+ const subject = typeof m.subject === "string" ? m.subject : "(no subject)";
248
+ const sender = typeof m.sender === "string" ? m.sender : "(unknown)";
249
+ const state = showReadState ? ` [${m.isRead ? "read" : "unread"}]` : "";
250
+ return ` - ID: ${String(m.id)} | ${date} | ${subject} (from: ${sender})${state}`;
251
+ })
252
+ .join("\n");
253
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Orphaned-process detection (connection-footprint hardening, v2.6.1).
3
+ *
4
+ * The MCP server tears down its pooled IMAP connections on the normal exit
5
+ * paths — SIGINT/SIGTERM and stdin EOF (parent/MCP-client went away). But a
6
+ * claude-code parent that is *force-quit* or crashes never delivers stdin-EOF,
7
+ * so the server would linger as an orphan holding IMAP sockets against Gmail's
8
+ * ~15-per-account connection cap (and starving Apple Mail of slots).
9
+ *
10
+ * On macOS a process whose parent has died is reparented to launchd (pid 1), so
11
+ * `process.ppid === 1` is a reliable "my parent is gone" signal for a server
12
+ * that was never itself launched directly by launchd (this one is always a child
13
+ * of the host app). index.ts polls this on an interval and self-exits when true.
14
+ *
15
+ * Pulled out as a tiny pure function so the decision is unit-testable without
16
+ * importing index.ts (which connects a transport at import time).
17
+ *
18
+ * @module utils/orphan
19
+ */
20
+ /**
21
+ * True when this process appears orphaned (its parent has died and it was
22
+ * reparented to launchd/init). Defaults to the live `process.ppid`.
23
+ */
24
+ export declare function isOrphaned(ppid?: number): boolean;
25
+ //# sourceMappingURL=orphan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orphan.d.ts","sourceRoot":"","sources":["../../src/utils/orphan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,GAAE,MAAqB,GAAG,OAAO,CAE/D"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Orphaned-process detection (connection-footprint hardening, v2.6.1).
3
+ *
4
+ * The MCP server tears down its pooled IMAP connections on the normal exit
5
+ * paths — SIGINT/SIGTERM and stdin EOF (parent/MCP-client went away). But a
6
+ * claude-code parent that is *force-quit* or crashes never delivers stdin-EOF,
7
+ * so the server would linger as an orphan holding IMAP sockets against Gmail's
8
+ * ~15-per-account connection cap (and starving Apple Mail of slots).
9
+ *
10
+ * On macOS a process whose parent has died is reparented to launchd (pid 1), so
11
+ * `process.ppid === 1` is a reliable "my parent is gone" signal for a server
12
+ * that was never itself launched directly by launchd (this one is always a child
13
+ * of the host app). index.ts polls this on an interval and self-exits when true.
14
+ *
15
+ * Pulled out as a tiny pure function so the decision is unit-testable without
16
+ * importing index.ts (which connects a transport at import time).
17
+ *
18
+ * @module utils/orphan
19
+ */
20
+ /**
21
+ * True when this process appears orphaned (its parent has died and it was
22
+ * reparented to launchd/init). Defaults to the live `process.ppid`.
23
+ */
24
+ export function isOrphaned(ppid = process.ppid) {
25
+ return ppid === 1;
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
4
4
  "description": "MCP server for Apple Mail - read, search, send, and manage emails via Claude and other AI assistants",
5
5
  "type": "module",
6
6
  "main": "build/index.js",
@@ -14,25 +14,6 @@
14
14
  "README.md",
15
15
  "LICENSE"
16
16
  ],
17
- "scripts": {
18
- "build": "tsc && tsc-alias",
19
- "start": "node build/index.js",
20
- "dev": "tsc --watch",
21
- "test": "vitest run",
22
- "test:integration": "vitest run --config vitest.integration.config.ts",
23
- "test:imap": "vitest run --config vitest.imap.config.ts",
24
- "test:all": "vitest run && vitest run --config vitest.integration.config.ts",
25
- "test:watch": "vitest",
26
- "test:coverage": "vitest run --coverage",
27
- "lint": "eslint src",
28
- "lint:fix": "eslint src --fix",
29
- "format": "prettier --write src",
30
- "format:check": "prettier --check src",
31
- "typecheck": "tsc --noEmit",
32
- "version": "node scripts/sync-plugin-version.mjs && git add .claude-plugin .agents/plugins codex .hermes-plugin .antigravity-plugin",
33
- "prepublishOnly": "npm run lint && npm run test && npm run build",
34
- "prepare": "husky; npm run build"
35
- },
36
17
  "keywords": [
37
18
  "mcp",
38
19
  "apple-mail",
@@ -67,6 +48,7 @@
67
48
  "zod": "^3.22.4"
68
49
  },
69
50
  "devDependencies": {
51
+ "@eslint/js": "^9.0.0",
70
52
  "@types/node": "^20.0.0",
71
53
  "@types/nodemailer": "^8.0.1",
72
54
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -95,5 +77,22 @@
95
77
  "overrides": {
96
78
  "js-yaml": "^4.2.0",
97
79
  "postcss": "^8.5.15"
80
+ },
81
+ "scripts": {
82
+ "build": "tsc && tsc-alias",
83
+ "start": "node build/index.js",
84
+ "dev": "tsc --watch",
85
+ "test": "vitest run",
86
+ "test:integration": "vitest run --config vitest.integration.config.ts",
87
+ "test:imap": "vitest run --config vitest.imap.config.ts",
88
+ "test:all": "vitest run && vitest run --config vitest.integration.config.ts",
89
+ "test:watch": "vitest",
90
+ "test:coverage": "vitest run --coverage",
91
+ "lint": "eslint src",
92
+ "lint:fix": "eslint src --fix",
93
+ "format": "prettier --write src",
94
+ "format:check": "prettier --check src",
95
+ "typecheck": "tsc --noEmit",
96
+ "version": "node scripts/sync-plugin-version.mjs && git add .claude-plugin .agents/plugins codex .hermes-plugin .antigravity-plugin"
98
97
  }
99
- }
98
+ }