apple-mail-mcp 2.10.5 → 2.10.7

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
@@ -485,6 +485,7 @@ for an explicitly-named IMAP account, never on an omitted account.
485
485
  | `APPLE_MAIL_MCP_IMAP_ACCOUNTS` | No | — | JSON array of **additional** IMAP accounts for multi-account setups (see below) |
486
486
  | `APPLE_MAIL_MCP_IMAP_IDLE` | No | `0` | Set `1` to enable IMAP IDLE push notifications (new-mail alerts) for every configured account |
487
487
  | `APPLE_MAIL_MCP_IMAP_IDLE_MS` | No | `30000` | Idle timeout (ms) before a pooled IMAP connection is closed (`0` = never close) |
488
+ | `APPLE_MAIL_MCP_STATS_BUDGET_MS` | No | `25000` | Per-account wall-clock budget for `get-mail-stats` (minimum `1000`). Raise it for very large accounts |
488
489
 
489
490
  **Multiple IMAP accounts (C2):** set `APPLE_MAIL_MCP_IMAP_ACCOUNTS` to a JSON array, e.g.
490
491
  `[{"account":"Work","user":"me@co.com","host":"imap.co.com","keychainService":"imap.co.com"}]`.
@@ -1146,7 +1147,15 @@ Get mail statistics.
1146
1147
  |-----------|------|----------|-------------|
1147
1148
  | `account` | string | No | Limit to one account (uses fast IMAP `STATUS` when that account is IMAP-configured). Omit to merge across all accounts. |
1148
1149
 
1149
- **Returns:** Total and per-account message/unread counts, plus recently received stats (24h, 7d, 30d).
1150
+ **Returns:** Total and per-account message/unread counts, plus recently received stats (24h, 7d, 30d). The scoped IMAP path also returns a `perMailbox` breakdown.
1151
+
1152
+ Gathering stats costs one IMAP `STATUS` per mailbox, and Gmail lists every label
1153
+ as a mailbox, so a large account is not instant. Accounts are counted
1154
+ **concurrently**, and each is bounded by `APPLE_MAIL_MCP_STATS_BUDGET_MS`
1155
+ (default `25000`). In the merged all-accounts path an account that fails or
1156
+ overruns is reported via `partial: true` + `failedAccounts` rather than being
1157
+ folded in as a silent zero; a scoped call to a single account returns an error
1158
+ naming the budget instead. Raise the budget if you have a very large account.
1150
1159
 
1151
1160
  ---
1152
1161
 
package/build/index.js CHANGED
@@ -81653,9 +81653,8 @@ function sameImapAccount(left, right, deps) {
81653
81653
  if (aliases.has(left) && aliases.has(right)) return true;
81654
81654
  }
81655
81655
  const specs = listImapAccountSpecs();
81656
- const matches = (selector, spec) => spec.accountLabel === selector || spec.user === selector;
81657
- const leftSpec = specs.find((spec) => matches(left, spec));
81658
- const rightSpec = specs.find((spec) => matches(right, spec));
81656
+ const leftSpec = specs.find((spec) => specMatchesSelector(spec, left));
81657
+ const rightSpec = specs.find((spec) => specMatchesSelector(spec, right));
81659
81658
  return leftSpec !== void 0 && leftSpec === rightSpec;
81660
81659
  }
81661
81660
  function depsForAccount(account, deps) {
@@ -81667,14 +81666,21 @@ function depsForAccount(account, deps) {
81667
81666
  function depsForMessageRef(ref, deps) {
81668
81667
  return depsForAccount(ref.account, deps);
81669
81668
  }
81669
+ function specMatchesSelector(spec, selector) {
81670
+ return spec.accountLabel === selector || spec.user === selector || (spec.aliases?.includes(selector) ?? false);
81671
+ }
81670
81672
  function str(v) {
81671
81673
  return typeof v === "string" && v.trim() ? v.trim() : void 0;
81672
81674
  }
81675
+ function imapIdentityKey(spec) {
81676
+ return `${spec.host.trim().toLowerCase()}:${spec.port}:${spec.user.trim()}`;
81677
+ }
81673
81678
  function listImapAccountSpecs(env = process.env) {
81674
81679
  const specs = [];
81680
+ const seen = /* @__PURE__ */ new Set();
81675
81681
  const user = env[IMAP_ENV.user]?.trim();
81676
81682
  if (user) {
81677
- specs.push({
81683
+ const legacy = {
81678
81684
  accountLabel: env[IMAP_ENV.account]?.trim() || user,
81679
81685
  user,
81680
81686
  host: env[IMAP_ENV.host]?.trim() || "imap.gmail.com",
@@ -81682,7 +81688,9 @@ function listImapAccountSpecs(env = process.env) {
81682
81688
  password: env[IMAP_ENV.password],
81683
81689
  keychainService: env[IMAP_ENV.keychainService]?.trim(),
81684
81690
  keychainAccount: env[IMAP_ENV.keychainAccount]?.trim()
81685
- });
81691
+ };
81692
+ specs.push(legacy);
81693
+ seen.add(imapIdentityKey(legacy));
81686
81694
  }
81687
81695
  const json = env[IMAP_ENV.accounts]?.trim();
81688
81696
  if (json) {
@@ -81694,12 +81702,22 @@ function listImapAccountSpecs(env = process.env) {
81694
81702
  const u = str(a.user);
81695
81703
  if (!u) continue;
81696
81704
  const label = str(a.account) || str(a.accountLabel) || u;
81697
- if (specs.some((s) => s.accountLabel === label)) continue;
81705
+ const host = str(a.host) || "imap.gmail.com";
81698
81706
  const port = a.port ? Number(a.port) : 993;
81707
+ const key = imapIdentityKey({ host, port, user: u });
81708
+ if (seen.has(key)) {
81709
+ const owner = specs.find((s) => imapIdentityKey(s) === key);
81710
+ if (owner && owner.accountLabel !== label && !owner.aliases?.includes(label)) {
81711
+ (owner.aliases ??= []).push(label);
81712
+ }
81713
+ continue;
81714
+ }
81715
+ if (specs.some((s) => s.accountLabel === label)) continue;
81716
+ seen.add(key);
81699
81717
  specs.push({
81700
81718
  accountLabel: label,
81701
81719
  user: u,
81702
- host: str(a.host) || "imap.gmail.com",
81720
+ host,
81703
81721
  port,
81704
81722
  password: str(a.password),
81705
81723
  keychainService: str(a.keychainService),
@@ -81737,7 +81755,7 @@ function specToConfig(spec) {
81737
81755
  }
81738
81756
  function isImapAccount(account, env = process.env) {
81739
81757
  if (!account) return false;
81740
- return listImapAccountSpecs(env).some((s) => s.accountLabel === account || s.user === account);
81758
+ return listImapAccountSpecs(env).some((s) => specMatchesSelector(s, account));
81741
81759
  }
81742
81760
  function shouldUseImap(account, env = process.env) {
81743
81761
  return listImapAccountSpecs(env).length > 0 && (account === void 0 || isImapAccount(account, env));
@@ -81760,12 +81778,12 @@ function resolveImapConfig(env = process.env, account) {
81760
81778
  const specs = listImapAccountSpecs(env);
81761
81779
  if (specs.length === 0) {
81762
81780
  throw new Error(
81763
- `IMAP not configured. Set ${IMAP_ENV.user} (login address) to enable it. ${SETUP_HINT}`
81781
+ `IMAP not configured. Set ${IMAP_ENV.user} (login address), or ${IMAP_ENV.accounts} for multiple accounts, to enable it. ${SETUP_HINT}`
81764
81782
  );
81765
81783
  }
81766
81784
  let spec;
81767
81785
  if (account) {
81768
- spec = specs.find((s) => s.accountLabel === account || s.user === account);
81786
+ spec = specs.find((s) => specMatchesSelector(s, account));
81769
81787
  if (!spec) {
81770
81788
  throw new Error(
81771
81789
  `No IMAP account matching "${account}". Configured: ${specs.map((s) => s.accountLabel).join(", ")}.`
@@ -81977,7 +81995,7 @@ function errText(e) {
81977
81995
  var poolConnect = defaultConnect;
81978
81996
  var pools = /* @__PURE__ */ new Map();
81979
81997
  function poolKey(cfg) {
81980
- return `${cfg.host}:${cfg.port}:${cfg.user}`;
81998
+ return imapIdentityKey(cfg);
81981
81999
  }
81982
82000
  function imapIdleMs() {
81983
82001
  const raw = process.env.APPLE_MAIL_MCP_IMAP_IDLE_MS;
@@ -82035,7 +82053,7 @@ async function acquirePooled(cfg) {
82035
82053
  }
82036
82054
  }
82037
82055
  async function imapHealthCheck(deps = {}) {
82038
- if (!deps.config && !process.env[IMAP_ENV.user]?.trim()) {
82056
+ if (!deps.config && listImapAccountSpecs().length === 0) {
82039
82057
  return { configured: false, ok: false };
82040
82058
  }
82041
82059
  let cfg;
@@ -82773,7 +82791,11 @@ async function runDoctor(mailManager2) {
82773
82791
  checks.push({
82774
82792
  name: `IMAP: ${label}`,
82775
82793
  status: h.ok ? "ok" : "fail",
82776
- detail: h.ok ? `connected to ${h.host}` : `connection failed: ${h.error}. Check the Keychain password and host/port.`
82794
+ // `h.error` is optional on the health-check result, so interpolating it
82795
+ // bare printed the literal string "connection failed: undefined" for
82796
+ // any failure that carried no message (issue #138). Never render that:
82797
+ // an unexplained failure is still worth naming, but as words.
82798
+ detail: h.ok ? `connected to ${h.host}` : `connection failed: ${h.error ?? "the health check reported no detail"}. Check the Keychain password and host/port.`
82777
82799
  });
82778
82800
  }
82779
82801
  }
@@ -83202,6 +83224,14 @@ var server = new McpServer(
83202
83224
  // logging capability lets the IMAP IDLE watcher push new-mail notifications (B5).
83203
83225
  { capabilities: { logging: {} } }
83204
83226
  );
83227
+ function registerTool(name, config2, cb) {
83228
+ const { outputSchema, ...rest } = config2;
83229
+ return server.registerTool(
83230
+ name,
83231
+ outputSchema ? { ...rest, outputSchema: external_exports.object(outputSchema).passthrough() } : rest,
83232
+ cb
83233
+ );
83234
+ }
83205
83235
  var mailManager = new AppleMailManager();
83206
83236
  registerResourcesAndPrompts(server, mailManager);
83207
83237
  async function hybridBatchCounts(ids, appleFn, imapFn) {
@@ -83224,7 +83254,7 @@ async function hybridBatchCounts(ids, appleFn, imapFn) {
83224
83254
  }
83225
83255
  return { success, fail, errors };
83226
83256
  }
83227
- server.registerTool(
83257
+ registerTool(
83228
83258
  "search-messages",
83229
83259
  {
83230
83260
  description: "Use when: finding messages by query/sender/subject/date/read/flag filters and you need their ids for follow-up operations.\nReturns: matching messages with id, date, subject, sender, and read state (plus partial-coverage diagnostics when some mailboxes were skipped).\nDo not use when: you want a plain mailbox listing without filters (use list-messages), already have an id and want the body (use get-message), or want a whole conversation (use get-thread).\nPrefer this first to obtain the message ids that get-message/mark-as-read/delete-message/move-message and the batch tools require.",
@@ -83336,7 +83366,7 @@ ${messageList}${coverageBlock}`,
83336
83366
  "Error searching messages"
83337
83367
  )
83338
83368
  );
83339
- server.registerTool(
83369
+ registerTool(
83340
83370
  "get-message",
83341
83371
  {
83342
83372
  description: `Use when: reading the full body of one message whose id you already have (numeric or imap:\u2026); set preferHtml to get the HTML body instead of plain text.
@@ -83404,7 +83434,7 @@ ${body}`, {
83404
83434
  "Error retrieving message"
83405
83435
  )
83406
83436
  );
83407
- server.registerTool(
83437
+ registerTool(
83408
83438
  "get-thread",
83409
83439
  {
83410
83440
  description: "Use when: you have one message id and want the whole conversation it belongs to, oldest-first. With an imap: id it threads by References/Message-ID; otherwise it groups by normalized subject.\nReturns: the thread's normalized subject and its messages (id, date, subject, sender, read state).\nDo not use when: you only need the single message (use get-message) or are searching by arbitrary criteria (use search-messages).",
@@ -83523,7 +83553,7 @@ ${list}${coverageBlock}`,
83523
83553
  );
83524
83554
  }, "Error retrieving thread")
83525
83555
  );
83526
- server.registerTool(
83556
+ registerTool(
83527
83557
  "list-messages",
83528
83558
  {
83529
83559
  description: "Use when: browsing a mailbox's recent messages (optionally filtered by sender or unread-only) with pagination via limit/offset, and you need their ids.\nReturns: messages with id, date, subject, and sender (plus partial-coverage diagnostics when some mailboxes were skipped).\nDo not use when: you have specific search criteria like subject/date/flags (use search-messages) or already have an id and want the body (use get-message).\nLike search-messages, use this to obtain the ids that read/mark/delete/move and batch tools require.",
@@ -83588,7 +83618,7 @@ ${messageList}${coverageBlock}`,
83588
83618
  );
83589
83619
  }, "Error listing messages")
83590
83620
  );
83591
- server.registerTool(
83621
+ registerTool(
83592
83622
  "send-email",
83593
83623
  {
83594
83624
  description: "Use when: the user has explicitly confirmed they want to send a single email now to the given recipients (to/cc/bcc are arrays), optionally with attachments and a chosen transport.\nReturns: a confirmation naming the recipients and attachment count.\nDo not use when: the user wants to review first (use create-draft), is replying to or forwarding an existing message (use reply-to-message / forward-message), or wants per-recipient personalized copies (use send-serial-email).\nSafety: this SENDS real email immediately and it cannot be unsent \u2014 require explicit user confirmation of the exact recipients, subject, and body before calling. Prefer create-draft when there is any doubt.",
@@ -83639,7 +83669,7 @@ server.registerTool(
83639
83669
  });
83640
83670
  }, "Error sending email")
83641
83671
  );
83642
- server.registerTool(
83672
+ registerTool(
83643
83673
  "send-serial-email",
83644
83674
  {
83645
83675
  description: "Use when: the user has confirmed a mail-merge \u2014 sending individually personalized copies to many recipients (max 100), with {{Key}} placeholders in subject/body replaced per-recipient from each recipient's variables. Recipients do not see each other.\nReturns: a per-recipient sent/failed report with counts.\nDo not use when: sending one message to a shared recipient list (use send-email) or saving for review (use create-draft).\nSafety: this SENDS many real emails immediately and they cannot be unsent \u2014 require explicit user confirmation of the recipient list, the subject/body template, and the placeholder substitutions before calling.",
@@ -83695,7 +83725,7 @@ ${details}`,
83695
83725
  }
83696
83726
  }, "Error sending serial emails")
83697
83727
  );
83698
- server.registerTool(
83728
+ registerTool(
83699
83729
  "create-draft",
83700
83730
  {
83701
83731
  description: "Use when: composing an email the user should review in Mail.app before sending \u2014 the safe default for any new message (to/cc/bcc are arrays, optional attachments).\nReturns: a confirmation that the draft was created, with recipients and attachment count.\nDo not use when: the user has already confirmed they want it sent now (use send-email).\nSafety: low risk \u2014 creates a draft only and sends nothing; the user must open Mail.app and send it themselves.",
@@ -83775,7 +83805,7 @@ async function sendForwardViaSmtp(id, to, body) {
83775
83805
  if (result.success) return { sent: true };
83776
83806
  return { sent: false, fallback: false, error: result.error ?? "unknown SMTP error" };
83777
83807
  }
83778
- server.registerTool(
83808
+ registerTool(
83779
83809
  "reply-to-message",
83780
83810
  {
83781
83811
  description: "Use when: replying to an existing message by id, preserving its threading headers. Set replyAll for all recipients; set send=false to save as a draft instead of sending.\nReturns: a confirmation that the reply was sent or saved as a draft.\nDo not use when: composing a brand-new message (use send-email / create-draft) or forwarding to new recipients (use forward-message).\nSafety: with the default send=true this SENDS real email immediately and cannot be unsent \u2014 require explicit user confirmation of the recipients and body, or pass send=false to let the user review.",
@@ -83812,7 +83842,7 @@ server.registerTool(
83812
83842
  });
83813
83843
  }, "Error replying to message")
83814
83844
  );
83815
- server.registerTool(
83845
+ registerTool(
83816
83846
  "forward-message",
83817
83847
  {
83818
83848
  description: "Use when: forwarding an existing message (by id) to new recipients (to is an array), with an optional body to prepend. Set send=false to save as a draft.\nReturns: a confirmation that the message was forwarded or saved as a draft.\nDo not use when: replying to the sender/recipients (use reply-to-message) or composing a new message (use send-email / create-draft).\nSafety: with the default send=true this SENDS real email immediately and cannot be unsent \u2014 require explicit user confirmation of the recipients and any prepended body, or pass send=false to let the user review.",
@@ -83854,7 +83884,7 @@ server.registerTool(
83854
83884
  );
83855
83885
  }, "Error forwarding message")
83856
83886
  );
83857
- server.registerTool(
83887
+ registerTool(
83858
83888
  "mark-as-read",
83859
83889
  {
83860
83890
  description: "Use when: marking a single message (by id) as read.\nReturns: a confirmation that the message was marked read.\nDo not use when: marking several at once (use batch-mark-as-read) or marking unread (use mark-as-unread). Get the id from search-messages or list-messages first.",
@@ -83874,7 +83904,7 @@ server.registerTool(
83874
83904
  "Error marking message as read"
83875
83905
  )
83876
83906
  );
83877
- server.registerTool(
83907
+ registerTool(
83878
83908
  "mark-as-unread",
83879
83909
  {
83880
83910
  description: "Use when: marking a single message (by id) as unread.\nReturns: a confirmation that the message was marked unread.\nDo not use when: marking several at once (use batch-mark-as-unread) or marking read (use mark-as-read). Get the id from search-messages or list-messages first.",
@@ -83894,7 +83924,7 @@ server.registerTool(
83894
83924
  "Error marking message as unread"
83895
83925
  )
83896
83926
  );
83897
- server.registerTool(
83927
+ registerTool(
83898
83928
  "flag-message",
83899
83929
  {
83900
83930
  description: "Use when: flagging a single message (by id), optionally with a color (red/orange/yellow/green/blue/purple/gray).\nReturns: a confirmation that the message was flagged (and the color, when applied).\nDo not use when: flagging several at once (use batch-flag-messages) or removing a flag (use unflag-message). Get the id from search-messages or list-messages first.\nNote: the color is applied on both routes \u2014 AppleScript sets the flag index, IMAP writes the equivalent $MailFlagBit0/1/2 keywords Mail.app reads.",
@@ -83926,7 +83956,7 @@ server.registerTool(
83926
83956
  });
83927
83957
  }, "Error flagging message")
83928
83958
  );
83929
- server.registerTool(
83959
+ registerTool(
83930
83960
  "unflag-message",
83931
83961
  {
83932
83962
  description: "Use when: removing the flag from a single message (by id).\nReturns: a confirmation that the message was unflagged.\nDo not use when: unflagging several at once (use batch-unflag-messages) or adding a flag (use flag-message). Get the id from search-messages or list-messages first.",
@@ -83946,7 +83976,7 @@ server.registerTool(
83946
83976
  "Error unflagging message"
83947
83977
  )
83948
83978
  );
83949
- server.registerTool(
83979
+ registerTool(
83950
83980
  "delete-message",
83951
83981
  {
83952
83982
  description: "Use when: deleting a single message by id (moves it to Trash).\nReturns: a confirmation that the message was deleted.\nDo not use when: deleting several at once (use batch-delete-messages) or just filing it away (use move-message).\nSafety: destructive \u2014 require explicit user confirmation, and search-messages/list-messages first to confirm you have the right id before deleting.",
@@ -83969,7 +83999,7 @@ server.registerTool(
83969
83999
  "Error deleting message"
83970
84000
  )
83971
84001
  );
83972
- server.registerTool(
84002
+ registerTool(
83973
84003
  "move-message",
83974
84004
  {
83975
84005
  description: "Use when: moving a single message (by id) into another mailbox/folder, e.g. archiving or filing.\nReturns: a confirmation naming the destination mailbox.\nDo not use when: moving several at once (use batch-move-messages) or deleting (use delete-message). Use list-mailboxes to confirm the destination name exists.\nSafety: moves a real message between folders \u2014 confirm the destination mailbox, and search-messages/list-messages first to confirm the id.",
@@ -83998,7 +84028,7 @@ server.registerTool(
83998
84028
  "Error moving message"
83999
84029
  )
84000
84030
  );
84001
- server.registerTool(
84031
+ registerTool(
84002
84032
  "batch-delete-messages",
84003
84033
  {
84004
84034
  description: "Use when: deleting multiple messages in one call (1\u2013100 ids; moves them to Trash).\nReturns: counts of how many were deleted and how many failed.\nDo not use when: deleting just one (use delete-message) or filing messages away (use batch-move-messages).\nSafety: destructive and applies to many messages at once \u2014 require explicit user confirmation, and search-messages/list-messages first to confirm every id is correct before deleting.",
@@ -84023,7 +84053,7 @@ server.registerTool(
84023
84053
  }
84024
84054
  }, "Error batch deleting messages")
84025
84055
  );
84026
- server.registerTool(
84056
+ registerTool(
84027
84057
  "batch-move-messages",
84028
84058
  {
84029
84059
  description: "Use when: moving multiple messages (1\u2013100 ids) into the same destination mailbox/folder in one call, e.g. bulk archiving.\nReturns: counts of how many were moved and how many failed.\nDo not use when: moving just one (use move-message) or deleting (use batch-delete-messages). Use list-mailboxes to confirm the destination name exists.\nSafety: moves many real messages at once \u2014 confirm the destination mailbox, and search-messages/list-messages first to confirm the ids.",
@@ -84056,7 +84086,7 @@ server.registerTool(
84056
84086
  }
84057
84087
  }, "Error batch moving messages")
84058
84088
  );
84059
- server.registerTool(
84089
+ registerTool(
84060
84090
  "batch-mark-as-read",
84061
84091
  {
84062
84092
  description: "Use when: marking multiple messages (1\u2013100 ids) as read in one call.\nReturns: counts of how many were marked read and how many failed.\nDo not use when: marking just one (use mark-as-read) or marking unread (use batch-mark-as-unread). Get the ids from search-messages or list-messages first.",
@@ -84084,7 +84114,7 @@ server.registerTool(
84084
84114
  }
84085
84115
  }, "Error batch marking messages as read")
84086
84116
  );
84087
- server.registerTool(
84117
+ registerTool(
84088
84118
  "batch-mark-as-unread",
84089
84119
  {
84090
84120
  description: "Use when: marking multiple messages (1\u2013100 ids) as unread in one call.\nReturns: counts of how many were marked unread and how many failed.\nDo not use when: marking just one (use mark-as-unread) or marking read (use batch-mark-as-read). Get the ids from search-messages or list-messages first.",
@@ -84115,7 +84145,7 @@ server.registerTool(
84115
84145
  }
84116
84146
  }, "Error batch marking messages as unread")
84117
84147
  );
84118
- server.registerTool(
84148
+ registerTool(
84119
84149
  "batch-flag-messages",
84120
84150
  {
84121
84151
  description: "Use when: flagging multiple messages (1\u2013100 ids) in one call, optionally with a color (red/orange/yellow/green/blue/purple/gray).\nReturns: counts of how many were flagged and how many failed.\nDo not use when: flagging just one (use flag-message) or removing flags (use batch-unflag-messages). Get the ids from search-messages or list-messages first.\nNote: the color is applied on both routes \u2014 AppleScript sets the flag index, IMAP writes the equivalent $MailFlagBit0/1/2 keywords Mail.app reads \u2014 so a mixed batch of numeric and `imap:` ids all end up colored.",
@@ -84142,7 +84172,7 @@ server.registerTool(
84142
84172
  }
84143
84173
  }, "Error batch flagging messages")
84144
84174
  );
84145
- server.registerTool(
84175
+ registerTool(
84146
84176
  "batch-unflag-messages",
84147
84177
  {
84148
84178
  description: "Use when: removing flags from multiple messages (1\u2013100 ids) in one call.\nReturns: counts of how many were unflagged and how many failed.\nDo not use when: unflagging just one (use unflag-message) or adding flags (use batch-flag-messages). Get the ids from search-messages or list-messages first.",
@@ -84170,7 +84200,7 @@ server.registerTool(
84170
84200
  }
84171
84201
  }, "Error batch unflagging messages")
84172
84202
  );
84173
- server.registerTool(
84203
+ registerTool(
84174
84204
  "resolve-message-id",
84175
84205
  {
84176
84206
  description: "Use when: you have `imap:` message id(s) and genuinely need the numeric Mail.app id(s) \u2014 e.g. for reply-to-message/forward-message, which are numeric-id only. NOTE: as of 2.10.0 you no longer need this to apply a flag COLOR \u2014 flag-message/batch-flag-messages write the color over IMAP directly via Mail.app's $MailFlagBit0/1/2 keywords, so a smart mailbox keyed on flag color matches an IMAP-flagged message. Each imap: id is resolved via its RFC822 Message-ID.\nReturns: for each input id, its `numericId` (the AppleScript id) or null when it can't be resolved, plus the `messageId` used; and a `resolvedCount`.\nDo not use when: your ids are already numeric (they pass straight through), or you don't need a color \u2014 flag/move/mark tools operate on `imap:` ids directly.",
@@ -84208,7 +84238,7 @@ server.registerTool(
84208
84238
  );
84209
84239
  }, "Error resolving message ids")
84210
84240
  );
84211
- server.registerTool(
84241
+ registerTool(
84212
84242
  "list-attachments",
84213
84243
  {
84214
84244
  description: "Use when: enumerating a message's attachments (by id) to discover their names, MIME types, and sizes \u2014 typically before saving or fetching one.\nReturns: each attachment's name, MIME type, and size, plus a count.\nDo not use when: you want the bytes (use fetch-attachment for inline base64, or save-attachment to write to disk). Get the message id from search-messages or list-messages first.",
@@ -84241,7 +84271,7 @@ ${attachmentList}`,
84241
84271
  );
84242
84272
  }, "Error listing attachments")
84243
84273
  );
84244
- server.registerTool(
84274
+ registerTool(
84245
84275
  "save-attachment",
84246
84276
  {
84247
84277
  description: "Use when: writing one of a message's attachments to disk, by message id and attachmentName, into the savePath directory (saved as savePath/attachmentName).\nReturns: a confirmation of the saved file path.\nDo not use when: you don't know the attachment name (use list-attachments first) or want the bytes inline rather than on disk (use fetch-attachment).\nSafety: writes a file to disk \u2014 savePath must be a directory inside the configured allowed roots, and attachmentName may not contain path separators or '..'; calls outside those constraints are rejected.",
@@ -84289,7 +84319,7 @@ server.registerTool(
84289
84319
  });
84290
84320
  }, "Error saving attachment")
84291
84321
  );
84292
- server.registerTool(
84322
+ registerTool(
84293
84323
  "fetch-attachment",
84294
84324
  {
84295
84325
  description: "Use when: retrieving an attachment's raw bytes inline as base64 (by message id and attachmentName), e.g. to process its contents without touching disk.\nReturns: the attachment's bytes base64-encoded, with its size and (for IMAP) MIME type.\nDo not use when: you don't know the attachment name (use list-attachments first) or you just want it saved to disk (use save-attachment).",
@@ -84329,7 +84359,7 @@ ${r.base64}`,
84329
84359
  );
84330
84360
  }, "Error fetching attachment")
84331
84361
  );
84332
- server.registerTool(
84362
+ registerTool(
84333
84363
  "list-mailboxes",
84334
84364
  {
84335
84365
  description: "Use when: discovering the mailbox/folder names (and unread/message counts) available in an account, e.g. before moving messages or searching a specific mailbox.\nReturns: each mailbox's name with its unread (and, for IMAP, total message) count, plus a count.\nDo not use when: you want the messages inside a mailbox (use list-messages or search-messages) or the list of accounts (use list-accounts).",
@@ -84402,7 +84432,7 @@ ${list}`, structured2);
84402
84432
  ${mailboxList}`, structured);
84403
84433
  }, "Error listing mailboxes")
84404
84434
  );
84405
- server.registerTool(
84435
+ registerTool(
84406
84436
  "get-unread-count",
84407
84437
  {
84408
84438
  description: "Use when: you only need the number of unread messages \u2014 INBOX by default, or scoped to one mailbox and/or account \u2014 without listing the messages themselves.\nReturns: the unread count for the requested scope (INBOX when no mailbox is given). If a source cannot be read the result carries `partial: true` + `failedAccounts`, and a total AppleScript failure returns an ERROR \u2014 a plain count is never a disguised transport failure.\nDo not use when: you need the actual unread messages and their ids (use list-messages with unreadOnly, or search-messages with isRead=false) or broader totals across every mailbox (use get-mail-stats).",
@@ -84466,7 +84496,7 @@ server.registerTool(
84466
84496
  });
84467
84497
  }, "Error getting unread count")
84468
84498
  );
84469
- server.registerTool(
84499
+ registerTool(
84470
84500
  "create-mailbox",
84471
84501
  {
84472
84502
  description: "Use when: creating a new mailbox/folder in an account.\nReturns: a confirmation that the mailbox was created.\nDo not use when: renaming an existing one (use rename-mailbox) or deleting one (use delete-mailbox). Use list-mailboxes to see what already exists.\nSafety: creates a real folder in the mail account \u2014 confirm the name and target account first.",
@@ -84492,7 +84522,7 @@ server.registerTool(
84492
84522
  return successResponse(`Mailbox "${name}" created`, { ok: true, name });
84493
84523
  }, "Error creating mailbox")
84494
84524
  );
84495
- server.registerTool(
84525
+ registerTool(
84496
84526
  "delete-mailbox",
84497
84527
  {
84498
84528
  description: "Use when: deleting a mailbox/folder from an account.\nReturns: a confirmation that the mailbox was deleted.\nDo not use when: renaming it (use rename-mailbox) or deleting messages within it (use delete-message / batch-delete-messages).\nSafety: destructive \u2014 deleting a mailbox removes the folder and any messages it contains. Require explicit user confirmation and use list-mailboxes first to confirm the exact name.",
@@ -84518,7 +84548,7 @@ server.registerTool(
84518
84548
  return successResponse(`Mailbox "${name}" deleted`, { ok: true, name });
84519
84549
  }, "Error deleting mailbox")
84520
84550
  );
84521
- server.registerTool(
84551
+ registerTool(
84522
84552
  "rename-mailbox",
84523
84553
  {
84524
84554
  description: "Use when: renaming an existing mailbox/folder from oldName to newName within an account.\nReturns: a confirmation naming the old and new mailbox names.\nDo not use when: creating a new folder (use create-mailbox) or deleting one (use delete-mailbox). Use list-mailboxes to confirm the current name.\nSafety: renames a real folder in the mail account \u2014 confirm oldName matches exactly (case-sensitive) before calling.",
@@ -84556,7 +84586,7 @@ server.registerTool(
84556
84586
  });
84557
84587
  }, "Error renaming mailbox")
84558
84588
  );
84559
- server.registerTool(
84589
+ registerTool(
84560
84590
  "list-smart-mailboxes",
84561
84591
  {
84562
84592
  description: "Use when: listing Apple Mail smart mailboxes (criteria-based virtual views), including on German-localized macOS where AppleScript's smart-mailbox terms do not compile.\nReturns: each smart mailbox's name and a short criteria summary.\nDo not use when: listing real folders/mailboxes (use list-mailboxes).",
@@ -84589,7 +84619,7 @@ ${lines}`, {
84589
84619
  });
84590
84620
  }, "Error listing smart mailboxes")
84591
84621
  );
84592
- server.registerTool(
84622
+ registerTool(
84593
84623
  "create-smart-mailbox",
84594
84624
  {
84595
84625
  description: "Use when: creating an Apple Mail smart mailbox (a criteria-based virtual view) that matches a sender, subject, or body substring \u2014 works on German-localized macOS where AppleScript's smart-mailbox terms fail.\nReturns: confirmation of creation, or a note that a smart mailbox with that name already existed.\nDo not use when: creating a real folder (use create-mailbox).\nSafety: edits Apple Mail's SyncedSmartMailboxes.plist directly. It backs the file up (.bak) and writes atomically, and never rewrites your existing smart mailboxes. It does not quit Mail \u2014 quit Mail first for reliable results, since a running Mail may not show the new smart mailbox until relaunched and can overwrite plist edits it did not make.",
@@ -84632,7 +84662,7 @@ server.registerTool(
84632
84662
  });
84633
84663
  }, "Error creating smart mailbox")
84634
84664
  );
84635
- server.registerTool(
84665
+ registerTool(
84636
84666
  "delete-smart-mailbox",
84637
84667
  {
84638
84668
  description: "Use when: deleting an Apple Mail smart mailbox (virtual view) by name.\nReturns: confirmation of deletion.\nDo not use when: deleting a real folder (use delete-mailbox) or messages (use delete-message / batch-delete-messages).\nSafety: destructive \u2014 removes the smart mailbox from Apple Mail's SyncedSmartMailboxes.plist. It backs the file up (.bak) and writes atomically, preserving every other smart mailbox, but the removal is not undoable in-app. Confirm the exact name with list-smart-mailboxes first, and quit Mail first for reliable results.",
@@ -84655,7 +84685,7 @@ server.registerTool(
84655
84685
  });
84656
84686
  }, "Error deleting smart mailbox")
84657
84687
  );
84658
- server.registerTool(
84688
+ registerTool(
84659
84689
  "create-newsletter-smart-mailboxes",
84660
84690
  {
84661
84691
  description: `Use when: auto-discovering newsletter/bulk senders in your INBOX(es) and (optionally) creating a dedicated smart mailbox per sender (named "NL: <sender>"). Defaults to a safe dry run that only proposes.
@@ -84685,7 +84715,7 @@ ${lines || " (none met the threshold)"}`,
84685
84715
  );
84686
84716
  }, "Error creating newsletter smart mailboxes")
84687
84717
  );
84688
- server.registerTool(
84718
+ registerTool(
84689
84719
  "list-accounts",
84690
84720
  {
84691
84721
  description: "Use when: discovering the configured Mail accounts (e.g. iCloud, Gmail) so you can pass an exact account name to other tools.\nReturns: the account names and a count. If the AppleScript transport fails (timeout / wedged Mail / missing Automation grant) this returns an ERROR rather than an empty list \u2014 an empty list always means Mail really has no accounts.\nDo not use when: you want the folders within an account (use list-mailboxes) or messages (use list-messages / search-messages).",
@@ -84713,7 +84743,7 @@ server.registerTool(
84713
84743
  ${accountList}`, structured);
84714
84744
  }, "Error listing accounts")
84715
84745
  );
84716
- server.registerTool(
84746
+ registerTool(
84717
84747
  "list-rules",
84718
84748
  {
84719
84749
  description: "Use when: discovering the Mail rules that exist and whether each is enabled or disabled, e.g. before enabling/disabling/deleting one.\nReturns: each rule's name and enabled/disabled state.\nDo not use when: you want to change a rule (use enable-rule / disable-rule / create-rule / delete-rule).",
@@ -84737,7 +84767,7 @@ server.registerTool(
84737
84767
  ${ruleList}`, structured);
84738
84768
  }, "Error listing rules")
84739
84769
  );
84740
- server.registerTool(
84770
+ registerTool(
84741
84771
  "enable-rule",
84742
84772
  {
84743
84773
  description: "Use when: turning on an existing Mail rule by name.\nReturns: a confirmation that the rule was enabled.\nDo not use when: turning a rule off (use disable-rule), creating one (use create-rule), or deleting one (use delete-rule). Use list-rules to confirm the exact rule name.",
@@ -84758,7 +84788,7 @@ server.registerTool(
84758
84788
  return successResponse(`Rule "${name}" enabled`, { ok: true, name, enabled: true });
84759
84789
  }, "Error enabling rule")
84760
84790
  );
84761
- server.registerTool(
84791
+ registerTool(
84762
84792
  "disable-rule",
84763
84793
  {
84764
84794
  description: "Use when: turning off an existing Mail rule by name (without deleting it).\nReturns: a confirmation that the rule was disabled.\nDo not use when: turning a rule on (use enable-rule), creating one (use create-rule), or removing it permanently (use delete-rule). Use list-rules to confirm the exact rule name.",
@@ -84779,7 +84809,7 @@ server.registerTool(
84779
84809
  return successResponse(`Rule "${name}" disabled`, { ok: true, name, enabled: false });
84780
84810
  }, "Error disabling rule")
84781
84811
  );
84782
- server.registerTool(
84812
+ registerTool(
84783
84813
  "create-rule",
84784
84814
  {
84785
84815
  description: "Use when: creating a new Mail rule with one or more conditions (field/operator/value) and at least one action (markRead, markFlagged, delete, or moveTo). Set matchAll to require all conditions vs. any.\nReturns: a confirmation naming the rule and its condition count.\nDo not use when: toggling an existing rule (use enable-rule / disable-rule) or removing one (use delete-rule). Use list-rules to avoid duplicating an existing rule.\nSafety: creates a rule that automatically acts on real mail (including delete/move actions) on an ongoing basis \u2014 confirm the conditions and actions with the user before calling.",
@@ -84821,7 +84851,7 @@ server.registerTool(
84821
84851
  );
84822
84852
  }, "Error creating rule")
84823
84853
  );
84824
- server.registerTool(
84854
+ registerTool(
84825
84855
  "delete-rule",
84826
84856
  {
84827
84857
  description: "Use when: permanently removing a Mail rule by name.\nReturns: a confirmation that the rule was deleted.\nDo not use when: you only want to pause it (use disable-rule) or create one (use create-rule).\nSafety: destructive \u2014 the rule is removed permanently. Require explicit user confirmation and use list-rules first to confirm the exact name.",
@@ -84841,7 +84871,7 @@ server.registerTool(
84841
84871
  return successResponse(`Rule "${name}" deleted`, { name, deleted: true });
84842
84872
  }, "Error deleting rule")
84843
84873
  );
84844
- server.registerTool(
84874
+ registerTool(
84845
84875
  "search-contacts",
84846
84876
  {
84847
84877
  description: "Use when: looking up a person in Contacts by name, organization, nickname, or email to find their email address(es)/phone(s) before composing or sending mail. Reads the macOS Contacts database directly (needs Full Disk Access; does NOT require Contacts.app to be running or an Automation / Apple-Events grant).\nReturns: matching contacts with their names, email addresses, and phone numbers.\nDo not use when: searching email messages (use search-messages) \u2014 this queries Contacts, not the mailbox.",
@@ -84871,7 +84901,7 @@ server.registerTool(
84871
84901
  ${contactList}`, structured);
84872
84902
  }, "Error searching contacts")
84873
84903
  );
84874
- server.registerTool(
84904
+ registerTool(
84875
84905
  "save-template",
84876
84906
  {
84877
84907
  description: "Use when: creating a reusable email template (name, subject, body, optional default to/cc), or updating one by passing its existing id. Subject/body may contain placeholders for later use.\nReturns: the saved template's name and id (reuse the id with use-template / get-template / delete-template).\nDo not use when: composing a one-off message (use create-draft / send-email) or filling in a template to send (use use-template).\nSafety: writes the template to the on-disk templates store (APPLE_MAIL_MCP_TEMPLATES_FILE) and persists across restarts; passing an existing id overwrites that template.",
@@ -84898,7 +84928,7 @@ server.registerTool(
84898
84928
  });
84899
84929
  }, "Error saving template")
84900
84930
  );
84901
- server.registerTool(
84931
+ registerTool(
84902
84932
  "list-templates",
84903
84933
  {
84904
84934
  description: "Use when: discovering the saved email templates and their ids, e.g. before using or editing one.\nReturns: each template's id, name, and subject.\nDo not use when: you want a single template's full body (use get-template) or want to apply one (use use-template).",
@@ -84922,7 +84952,7 @@ server.registerTool(
84922
84952
  ${templateList}`, structured);
84923
84953
  }, "Error listing templates")
84924
84954
  );
84925
- server.registerTool(
84955
+ registerTool(
84926
84956
  "get-template",
84927
84957
  {
84928
84958
  description: "Use when: reading the full contents of one saved template by id \u2014 its name, subject, default to/cc, and body.\nReturns: the template's name, subject, default recipients, and body text.\nDo not use when: you don't have the id (use list-templates first) or want to apply the template into a draft (use use-template).",
@@ -84961,7 +84991,7 @@ ${template.body}`
84961
84991
  });
84962
84992
  }, "Error getting template")
84963
84993
  );
84964
- server.registerTool(
84994
+ registerTool(
84965
84995
  "delete-template",
84966
84996
  {
84967
84997
  description: "Use when: permanently removing a saved email template by id.\nReturns: a confirmation that the template was deleted.\nDo not use when: you only want to view it (use get-template) or update it (use save-template with the existing id).\nSafety: destructive \u2014 removes the template from the on-disk store permanently. Require explicit user confirmation and use list-templates first to confirm the id.",
@@ -84981,7 +85011,7 @@ server.registerTool(
84981
85011
  return successResponse(`Template "${id}" deleted`, { ok: true, id });
84982
85012
  }, "Error deleting template")
84983
85013
  );
84984
- server.registerTool(
85014
+ registerTool(
84985
85015
  "use-template",
84986
85016
  {
84987
85017
  description: "Use when: composing a new draft from a saved template (by id), optionally overriding the recipients, subject, or body. Creates a draft in Mail.app for the user to review and send.\nReturns: a confirmation that a draft was created from the template.\nDo not use when: you want to inspect the template without composing (use get-template) or send immediately without a draft (use send-email).",
@@ -85005,7 +85035,7 @@ server.registerTool(
85005
85035
  return successResponse(`Draft created from template "${id}"`, { ok: true, id });
85006
85036
  }, "Error using template")
85007
85037
  );
85008
- server.registerTool(
85038
+ registerTool(
85009
85039
  "health-check",
85010
85040
  {
85011
85041
  description: "Use when: doing a quick check that Mail.app is reachable and the server's basic checks pass.\nReturns: an overall healthy/unhealthy status with a pass/fail line per check.\nDo not use when: you need detailed permission/account/IMAP/SMTP diagnostics with remediation steps (use doctor).",
@@ -85028,7 +85058,7 @@ server.registerTool(
85028
85058
  ${checkLines}`, { ...result });
85029
85059
  }, "Error running health check")
85030
85060
  );
85031
- server.registerTool(
85061
+ registerTool(
85032
85062
  "doctor",
85033
85063
  {
85034
85064
  description: "Use when: troubleshooting setup problems \u2014 diagnoses Mail.app automation permissions, account state, and the IMAP/SMTP backends with actionable remediation messages.\nReturns: a detailed diagnostic report (formatted text plus structured checks).\nDo not use when: you just want a quick up/down status (use health-check) or message counts (use get-mail-stats).",
@@ -85043,7 +85073,7 @@ server.registerTool(
85043
85073
  return successResponse(formatDoctorReport(report), { ...report });
85044
85074
  }, "Error running doctor")
85045
85075
  );
85046
- server.registerTool(
85076
+ registerTool(
85047
85077
  "get-mail-stats",
85048
85078
  {
85049
85079
  description: "Use when: you want aggregate mailbox statistics \u2014 total and unread message counts, recently-received counts (last 24h/7d/30d), and (for the all-accounts path) a per-account breakdown.\nReturns: totals, unread counts, recent-activity counts, and per-account figures.\nDo not use when: you only need a single unread number (use get-unread-count) or want to list the messages themselves (use list-messages / search-messages).",
@@ -85056,12 +85086,42 @@ server.registerTool(
85056
85086
  totalUnread: external_exports.number().optional(),
85057
85087
  accounts: external_exports.array(external_exports.object({}).passthrough()).optional(),
85058
85088
  recentlyReceived: external_exports.object({}).passthrough().optional(),
85059
- recent: external_exports.object({}).passthrough().optional()
85089
+ recent: external_exports.object({}).passthrough().optional(),
85090
+ // The scoped IMAP path spreads an ImapStats, which carries per-mailbox
85091
+ // STATUS rows. Declared so the shape is documented rather than merely
85092
+ // tolerated by the permissive advertisement (#135).
85093
+ perMailbox: external_exports.array(external_exports.object({}).passthrough()).optional(),
85094
+ partial: external_exports.boolean().optional(),
85095
+ failedAccounts: external_exports.array(external_exports.string()).optional()
85060
85096
  }
85061
85097
  },
85062
85098
  withErrorHandling(async ({ account }) => {
85099
+ const budgetMs = Math.max(1e3, Number(process.env.APPLE_MAIL_MCP_STATS_BUDGET_MS ?? 25e3));
85100
+ const withBudget = async (work, label) => {
85101
+ let timer;
85102
+ try {
85103
+ return await Promise.race([
85104
+ work,
85105
+ new Promise((_, reject) => {
85106
+ timer = setTimeout(
85107
+ () => reject(new Error(`${label} timed out after ${budgetMs}ms`)),
85108
+ budgetMs
85109
+ );
85110
+ })
85111
+ ]);
85112
+ } finally {
85113
+ if (timer) clearTimeout(timer);
85114
+ }
85115
+ };
85063
85116
  if (account !== void 0 && isImapAccount(account)) {
85064
- const s = await imapMailStats({ account });
85117
+ let s;
85118
+ try {
85119
+ s = await withBudget(imapMailStats({ account }), `IMAP mail-stats for "${account}"`);
85120
+ } catch (e) {
85121
+ return errorResponse(
85122
+ `Could not read mail statistics for "${account}": ${String(e)}. Gathering stats costs one IMAP STATUS per mailbox, so a very large account can exceed the ${budgetMs}ms budget \u2014 raise APPLE_MAIL_MCP_STATS_BUDGET_MS, or run the "doctor" tool if the connection itself is the problem.`
85123
+ );
85124
+ }
85065
85125
  const lines2 = [
85066
85126
  `\u{1F4CA} Mail Statistics \u2014 ${account} (IMAP)`,
85067
85127
  `\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
@@ -85081,40 +85141,55 @@ server.registerTool(
85081
85141
  const recent = { last24h: 0, last7d: 0, last30d: 0 };
85082
85142
  const perAccount = [];
85083
85143
  const sources = planCountSources(mailManager.listAccounts(), resolveImapConfigs());
85084
- for (const src of sources) {
85085
- if (src.kind === "imap") {
85144
+ const failedAccounts = [];
85145
+ const settled = await Promise.all(
85146
+ sources.filter((s) => s.kind === "imap").map(async (src) => {
85086
85147
  try {
85087
- const s = await imapMailStats({ config: src.config });
85088
- totalMessages += s.totalMessages;
85089
- totalUnread += s.totalUnread;
85090
- recent.last24h += s.recent.last24h;
85091
- recent.last7d += s.recent.last7d;
85092
- recent.last30d += s.recent.last30d;
85093
- perAccount.push({
85094
- name: src.label,
85095
- totalMessages: s.totalMessages,
85096
- unreadMessages: s.totalUnread,
85097
- backend: "imap"
85098
- });
85148
+ const stats2 = await withBudget(
85149
+ imapMailStats({ config: src.config }),
85150
+ `IMAP mail-stats for "${src.label}"`
85151
+ );
85152
+ return { label: src.label, stats: stats2 };
85099
85153
  } catch (e) {
85100
85154
  console.error(`IMAP mail-stats failed for "${src.label}": ${String(e)}`);
85155
+ return { label: src.label, stats: void 0 };
85101
85156
  }
85102
- } else {
85103
- let m = 0;
85104
- let u = 0;
85105
- for (const mb of mailManager.listMailboxes(src.account.name)) {
85106
- m += mb.messageCount;
85107
- u += mb.unreadCount;
85108
- }
85109
- totalMessages += m;
85110
- totalUnread += u;
85111
- perAccount.push({
85112
- name: src.label,
85113
- totalMessages: m,
85114
- unreadMessages: u,
85115
- backend: "applescript"
85116
- });
85157
+ })
85158
+ );
85159
+ for (const r of settled) {
85160
+ if (!r.stats) {
85161
+ failedAccounts.push(r.label);
85162
+ continue;
85117
85163
  }
85164
+ const s = r.stats;
85165
+ totalMessages += s.totalMessages;
85166
+ totalUnread += s.totalUnread;
85167
+ recent.last24h += s.recent.last24h;
85168
+ recent.last7d += s.recent.last7d;
85169
+ recent.last30d += s.recent.last30d;
85170
+ perAccount.push({
85171
+ name: r.label,
85172
+ totalMessages: s.totalMessages,
85173
+ unreadMessages: s.totalUnread,
85174
+ backend: "imap"
85175
+ });
85176
+ }
85177
+ for (const src of sources) {
85178
+ if (src.kind === "imap") continue;
85179
+ let m = 0;
85180
+ let u = 0;
85181
+ for (const mb of mailManager.listMailboxes(src.account.name)) {
85182
+ m += mb.messageCount;
85183
+ u += mb.unreadCount;
85184
+ }
85185
+ totalMessages += m;
85186
+ totalUnread += u;
85187
+ perAccount.push({
85188
+ name: src.label,
85189
+ totalMessages: m,
85190
+ unreadMessages: u,
85191
+ backend: "applescript"
85192
+ });
85118
85193
  }
85119
85194
  const lines2 = [
85120
85195
  `\u{1F4CA} Mail Statistics (merged: IMAP + AppleScript)`,
@@ -85132,11 +85207,18 @@ server.registerTool(
85132
85207
  (a) => ` ${a.name}: ${a.totalMessages} messages (${a.unreadMessages} unread) [${a.backend}]`
85133
85208
  )
85134
85209
  ];
85210
+ if (failedAccounts.length > 0) {
85211
+ lines2.push(
85212
+ ``,
85213
+ `\u26A0\uFE0F PARTIAL: ${failedAccounts.length} account(s) could not be read (${failedAccounts.join(", ")}), so the real totals are higher. They either failed or exceeded the ${budgetMs}ms budget \u2014 raise APPLE_MAIL_MCP_STATS_BUDGET_MS if an account is simply large, or run the "doctor" tool to check the connection.`
85214
+ );
85215
+ }
85135
85216
  return successResponse(lines2.join("\n"), {
85136
85217
  totalMessages,
85137
85218
  totalUnread,
85138
85219
  accounts: perAccount,
85139
- recent
85220
+ recent,
85221
+ ...failedAccounts.length > 0 ? { partial: true, failedAccounts } : {}
85140
85222
  });
85141
85223
  }
85142
85224
  const stats = mailManager.getMailStats();
@@ -85164,7 +85246,7 @@ server.registerTool(
85164
85246
  return successResponse(lines.join("\n"), { ...stats });
85165
85247
  }, "Error getting mail statistics")
85166
85248
  );
85167
- server.registerTool(
85249
+ registerTool(
85168
85250
  "get-sync-status",
85169
85251
  {
85170
85252
  description: "Use when: checking whether Mail.app is running and actively syncing, e.g. to explain why new mail hasn't appeared yet.\nReturns: whether Mail.app is running and whether sync activity was detected.\nDo not use when: you need message counts (use get-mail-stats) or a full setup diagnosis (use doctor).",
@@ -199,6 +199,20 @@ Each array entry accepts: `account`, `user`, `host`, `port`, `password`
199
199
  (discouraged — prefer Keychain), `keychainService`, `keychainAccount`. Each
200
200
  account keeps its own pooled IMAP connection.
201
201
 
202
+ **The array alone is enough.** You do not need the legacy single-account vars:
203
+ listing every account in `APPLE_MAIL_MCP_IMAP_ACCOUNTS` and setting none of
204
+ `APPLE_MAIL_MCP_IMAP_USER` / `_ACCOUNT` / `_HOST` is fully supported, and the
205
+ first array entry becomes the default account. (Before 2.10.7 that shape worked
206
+ for every tool but made `doctor` report `connection failed: undefined` for each
207
+ account — see #138.)
208
+
209
+ **Don't declare the same mailbox twice.** If the legacy vars already describe a
210
+ mailbox, do not also give it an array entry — even under a different `account`
211
+ nickname. An account's identity is its resolved `(host, port, user)`, not its
212
+ label, so the duplicate is recognised and collapsed rather than counted twice
213
+ (the extra nickname still works as an alias). Before 2.10.7 it was counted
214
+ twice, inflating `get-unread-count` and `get-mail-stats`.
215
+
202
216
  ---
203
217
 
204
218
  ## Step 4 (optional) — SMTP sending
@@ -333,16 +347,17 @@ GUI is ignoring.
333
347
  | Variable | Purpose |
334
348
  |----------|---------|
335
349
  | `APPLE_MAIL_MCP_DEFAULT_ACCOUNT` | Account used when a tool omits `account` (name or email). |
336
- | `APPLE_MAIL_MCP_IMAP_USER` | Primary IMAP login; setting it enables IMAP. |
350
+ | `APPLE_MAIL_MCP_IMAP_USER` | Primary IMAP login. Setting it enables IMAP — but so does `APPLE_MAIL_MCP_IMAP_ACCOUNTS` on its own; either is sufficient. |
337
351
  | `APPLE_MAIL_MCP_IMAP_ACCOUNT` | Mail.app account name to match for routing (default = USER). |
338
352
  | `APPLE_MAIL_MCP_IMAP_HOST` | IMAP host (default `imap.gmail.com`). |
339
353
  | `APPLE_MAIL_MCP_IMAP_PORT` | IMAP port (default `993`, implicit TLS). |
340
354
  | `APPLE_MAIL_MCP_IMAP_PASSWORD` | Password (discouraged; prefer Keychain). |
341
355
  | `APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE` | Keychain item service/server name. |
342
356
  | `APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT` | Keychain item account (default = USER). |
343
- | `APPLE_MAIL_MCP_IMAP_ACCOUNTS` | JSON array of additional accounts (multi-account). |
357
+ | `APPLE_MAIL_MCP_IMAP_ACCOUNTS` | JSON array of accounts (multi-account). Sufficient on its own; also enables IMAP. |
344
358
  | `APPLE_MAIL_MCP_IMAP_IDLE` | `1` to enable IMAP IDLE new-mail push. |
345
359
  | `APPLE_MAIL_MCP_IMAP_IDLE_MS` | Pooled-connection idle timeout in ms (default `30000`; `0` = never close). |
360
+ | `APPLE_MAIL_MCP_STATS_BUDGET_MS` | Per-account wall-clock budget for `get-mail-stats` in ms (default `25000`, minimum `1000`). |
346
361
  | `APPLE_MAIL_MCP_SMTP_HOST` | SMTP host; setting it enables `transport:"smtp"`. |
347
362
  | `APPLE_MAIL_MCP_SMTP_PORT` | SMTP port (`465` if secure, else `587`). |
348
363
  | `APPLE_MAIL_MCP_SMTP_SECURE` | `true` for implicit TLS (465); else STARTTLS. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.10.5",
3
+ "version": "2.10.7",
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",