apple-mail-mcp 2.10.2 → 2.10.4

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
@@ -147,6 +147,7 @@ tool, and troubleshooting. Verify any time by running the **`doctor`** tool.
147
147
 
148
148
  - **macOS** - Apple Mail and AppleScript are macOS-only
149
149
  - **Node.js 20+** - Required for the MCP server
150
+ - **Node.js 22.5+ and Full Disk Access** - Required by `search-contacts` only. It reads the Contacts database directly through Node's built-in `node:sqlite`, which does not exist before 22.5. On an older runtime, or without Full Disk Access for the Node binary, it logs one line to stderr and returns **an empty list rather than an error** — so "no contacts found" can mean "cannot read Contacts". Every other tool works on Node 20+. See [Node runtime & TCC permissions](https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md).
150
151
  - **Apple Mail** - Must have at least one account configured (iCloud, Gmail, Exchange, etc.)
151
152
 
152
153
  ## Features
@@ -264,6 +265,8 @@ Get the full content of a message.
264
265
  |-----------|------|----------|-------------|
265
266
  | `id` | string | Yes | Message ID |
266
267
  | `preferHtml` | boolean | No | Return HTML source instead of plain text |
268
+ | `mailbox` | string | No | Mailbox holding the message (e.g. `"Sent Items"`). With `account`, opens that mailbox directly instead of scanning every mailbox — this is the fix for timeouts on large folders |
269
+ | `account` | string | No | Account holding the message. Pair with `mailbox` to skip the cross-mailbox scan |
267
270
 
268
271
  **Returns:** Subject line and message body (plain text by default, HTML if `preferHtml` is true and HTML content is available).
269
272
 
@@ -668,7 +671,7 @@ Return an attachment's bytes as base64 (the read counterpart to inline-base64 se
668
671
 
669
672
  | Parameter | Type | Required | Description |
670
673
  |-----------|------|----------|-------------|
671
- | `id` | string | Yes | Numeric message ID |
674
+ | `id` | string | Yes | Message ID (numeric or `imap:…`) |
672
675
  | `attachmentName` | string | Yes | Attachment filename (from `list-attachments`) |
673
676
 
674
677
  **Returns:** The attachment bytes, base64-encoded (also in `structuredContent.contentBase64`).
@@ -748,6 +751,8 @@ Flag or unflag a message. `flag-message` optionally takes a flag **color**; `unf
748
751
 
749
752
  **Flag colors** are an Apple Mail feature — the message's `flag index` (0 red, 1 orange, 2 yellow, 3 green, 4 blue, 5 purple, 6 gray), which is the property a Mail smart mailbox can match on. **The color is applied on both routes** (since 2.10.0): AppleScript sets the flag index directly, and for an **IMAP-routed** id (`imap:…`) the color is written as Mail.app's `$MailFlagBit0/1/2` keywords — a 3-bit field holding the same palette index. `\Flagged` on its own really is colorless, but those keywords ride alongside it in an ordinary `UID STORE`, so a smart mailbox keyed on flag color matches an IMAP-flagged message too. You do **not** need to resolve to a numeric id just to color a flag.
750
753
 
754
+ To **read** a color, the IMAP read path returns `flagColorIndex` in `structuredContent` — the same 0-6 palette index, omitted when the message carries no color bits. The AppleScript read path does not populate it.
755
+
751
756
  ---
752
757
 
753
758
  #### `delete-message`
@@ -853,8 +858,12 @@ Get unread message count.
853
858
 
854
859
  | Parameter | Type | Required | Description |
855
860
  |-----------|------|----------|-------------|
856
- | `mailbox` | string | No | Mailbox to check (omit for total) |
857
- | `account` | string | No | Account to check |
861
+ | `mailbox` | string | No | Mailbox to check (omit for **INBOX**) |
862
+ | `account` | string | No | Account to check (omit to sum each account's INBOX) |
863
+
864
+ **Returns:** The unread count for the requested scope.
865
+
866
+ > Omitting `mailbox` counts **INBOX**, not a cross-mailbox total. This changed in 2.8.15: summing every mailbox was slow and wrong on Gmail, where one message appears in INBOX, All Mail and every label it carries. For account-wide totals use [`get-mail-stats`](#get-mail-stats).
858
867
 
859
868
  ---
860
869
 
@@ -896,6 +905,8 @@ Rename a mailbox (creates new, moves messages, deletes old).
896
905
 
897
906
  ### Smart Mailbox Operations (intelligente Postfächer)
898
907
 
908
+ > **Requires Full Disk Access.** These tools read and write `~/Library/Mail/V*/MailData/SyncedSmartMailboxes.plist`, which is TCC-protected. Without Full Disk Access for the server's Node runtime the read simply finds nothing, and the tools report "no smart mailboxes" or "launch Mail at least once" rather than a permission error — see [Node runtime & TCC permissions](https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md).
909
+
899
910
  Smart mailboxes are Apple Mail's **criteria-based virtual views** — not real folders, so no messages are moved. AppleScript's `smart mailbox` / `intelligentes Postfach` terms don't compile reliably on localized (e.g. German) macOS, so these tools read and edit `~/Library/Mail/V*/MailData/SyncedSmartMailboxes.plist` directly.
900
911
 
901
912
  **How writes stay safe:** creating or deleting a smart mailbox first backs the plist up to `SyncedSmartMailboxes.plist.bak`, edits a temp copy with `plutil`/`PlistBuddy`, validates it with `plutil -lint`, and only then atomically renames it into place. Your **existing** smart mailboxes — including any with date/data criteria — are never rewritten, only the single target entry is added or removed. These tools do **not** quit or restart Mail: **quit Mail first** for reliable results, since a running Mail may not show a new smart mailbox until it's relaunched and can overwrite plist edits it didn't make.
@@ -1030,14 +1041,15 @@ Delete a mail rule by name.
1030
1041
 
1031
1042
  #### `search-contacts`
1032
1043
 
1033
- Search contacts in Contacts.app.
1044
+ Search the macOS Contacts database by name, organization, nickname, or email substring.
1045
+
1046
+ Since 2.8.7 this reads the AddressBook SQLite files directly rather than driving Contacts.app over AppleScript, so **Contacts.app need not be running and no Automation grant is involved** — but the Node runtime does need **Full Disk Access**, and **Node 22.5+** (see [Requirements](#requirements)). Without either, the tool returns an empty list rather than an error.
1034
1047
 
1035
1048
  | Parameter | Type | Required | Description |
1036
1049
  |-----------|------|----------|-------------|
1037
- | `query` | string | Yes | Name to search for |
1038
- | `limit` | number | No | Max results (default: 10) |
1050
+ | `query` | string | Yes | Substring matched against full name, organization, nickname, or any email address |
1039
1051
 
1040
- **Returns:** List of contacts with name, email addresses, and phone numbers.
1052
+ **Returns:** List of contacts with name, email addresses, and phone numbers. Results are **not** truncated — a broad query returns every match.
1041
1053
 
1042
1054
  ---
1043
1055
 
@@ -1130,7 +1142,9 @@ Run a full setup diagnostic: Mail.app automation permission, account state (flag
1130
1142
 
1131
1143
  Get mail statistics.
1132
1144
 
1133
- **Parameters:** None
1145
+ | Parameter | Type | Required | Description |
1146
+ |-----------|------|----------|-------------|
1147
+ | `account` | string | No | Limit to one account (uses fast IMAP `STATUS` when that account is IMAP-configured). Omit to merge across all accounts. |
1134
1148
 
1135
1149
  **Returns:** Total and per-account message/unread counts, plus recently received stats (24h, 7d, 30d).
1136
1150
 
@@ -1271,7 +1285,7 @@ The entrypoint is written as:
1271
1285
 
1272
1286
  ## Security and Privacy
1273
1287
 
1274
- - **Local only** - All operations happen locally via AppleScript. No data is sent to external servers.
1288
+ - **No third parties** - The server talks only to Mail.app on this Mac (AppleScript) and, when you configure them, directly to **your own** mail provider over TLS (IMAP/SMTP). Nothing is sent to this project or any other service. With the default AppleScript backend everything stays on-device; the opt-in IMAP/SMTP backends necessarily reach your provider, which is what they are for.
1275
1289
  - **Permission required** - macOS will prompt for automation permission on first use.
1276
1290
  - **No credential storage** - The server doesn't store any passwords or authentication tokens.
1277
1291
  - **Email safety** - Use `create-draft` to review emails before sending.
@@ -1285,7 +1299,7 @@ The entrypoint is written as:
1285
1299
  | macOS only | Apple Mail and AppleScript are macOS-specific |
1286
1300
  | MCP `send-email` is plain-text | The `send-email` tool sends plain text (reading HTML content is supported). To send HTML, use the bundled `apple-mail-send` CLI with `--html-body-file` (sends `multipart/alternative` via SMTP) |
1287
1301
  | Attachments require absolute paths | File attachments must use full absolute paths (e.g., `/Users/me/file.pdf`) |
1288
- | No smart mailboxes | Cannot access Smart Mailboxes via AppleScript |
1302
+ | Smart mailboxes need Mail quit | Smart mailboxes are supported (see [Smart Mailbox Operations](#smart-mailbox-operations-intelligente-postfächer)), but `create-`/`delete-smart-mailbox` edit `SyncedSmartMailboxes.plist` directly — a running Mail may not show a new one until relaunched, and can overwrite plist edits it didn't make. Quit Mail first. Reading them needs Full Disk Access for the Node runtime |
1289
1303
  | Very large mailboxes not searchable *via AppleScript* | Apple Mail's AppleScript bridge times out on mailboxes with tens of thousands of messages, so unscoped `search-messages` skips mailboxes above `APPLE_MAIL_MAX_SEARCH_MAILBOX` (default 5000) and reports them as a partial result. Scope with `mailbox` + a date window — or configure the [IMAP backend](#imap-backend--opt-in), which searches these server-side in well under a second. ([#24](https://github.com/sweetrb/apple-mail-mcp/issues/24)) |
1290
1304
  | Can't delete/rename server-side mailboxes or mutate drafts *via AppleScript* | Mail.app's AppleScript bridge can only `delete`/`rename` **local "On My Mac"** mailboxes and cannot delete/move drafts — it throws `AppleEvent handler failed` for IMAP/Gmail/Workspace/iCloud/Exchange mailboxes (the GUI can do it). Without IMAP configured, `delete-mailbox`/`rename-mailbox`/`delete-message`/`move-message` return a clear "do it in Mail.app directly" error instead of a generic failure. With the [IMAP backend](#imap-backend--opt-in) configured for the account, these operations run via IMAP and succeed. ([#42](https://github.com/sweetrb/apple-mail-mcp/issues/42)) |
1291
1305
  | Message ID format | Message IDs must be numeric (AppleScript ids) or `imap:…` tokens from the IMAP read path (validated by schema) |
@@ -1385,14 +1399,16 @@ The `\\\\` in JSON becomes `\\` in the actual string, which represents a single
1385
1399
 
1386
1400
  ## Development
1387
1401
 
1402
+ This repo is **pnpm-only** — `package.json`'s `preinstall` guard hard-fails an `npm install`, because npm resolves off-lockfile and the committed bundle would then mismatch CI.
1403
+
1388
1404
  ```bash
1389
- npm install # Install dependencies
1390
- npm run build # Typecheck, then bundle src/index.ts + src/cli.ts into build/ (esbuild)
1391
- npm test # Run unit tests
1392
- npm run test:integration # Run integration tests (requires Mail.app)
1393
- npm run test:all # Run all tests (unit + integration)
1394
- npm run lint # Check code style
1395
- npm run format # Format code
1405
+ corepack enable && pnpm install --frozen-lockfile # Install dependencies
1406
+ pnpm run build # Typecheck, then bundle src/index.ts + src/cli.ts into build/ (esbuild)
1407
+ pnpm test # Run unit tests
1408
+ pnpm run test:integration # Run integration tests (requires Mail.app)
1409
+ pnpm run test:all # Run all tests (unit + integration)
1410
+ pnpm run lint # Check code style
1411
+ pnpm run format # Format code
1396
1412
  ```
1397
1413
 
1398
1414
  ---
package/build/index.js CHANGED
@@ -76079,6 +76079,10 @@ function executeAppleScript(script, options = {}) {
76079
76079
  return lastError;
76080
76080
  }
76081
76081
 
76082
+ // src/utils/docsUrls.ts
76083
+ var SETUP_GUIDE_URL = "https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/IMAP-SETUP.md";
76084
+ var SETUP_HINT = `Setup guide: ${SETUP_GUIDE_URL} \u2014 run the "doctor" tool to check your setup.`;
76085
+
76082
76086
  // src/utils/mimeParse.ts
76083
76087
  function extractBoundary(source) {
76084
76088
  const match = source.match(/boundary="?([^";\s\r\n]+)"?/i);
@@ -76838,6 +76842,12 @@ var AppleMailManager = class {
76838
76842
  };
76839
76843
  /** Cache TTL in milliseconds (60 seconds). */
76840
76844
  CACHE_TTL_MS = 6e4;
76845
+ /**
76846
+ * Last AppleScript transport error from an account/count read, or null when the
76847
+ * last one succeeded. Lets a tool report "the transport failed" instead of
76848
+ * presenting a fallback zero/empty as a real answer. (#130)
76849
+ */
76850
+ lastAccountsError = null;
76841
76851
  /**
76842
76852
  * Remembers where each message id was last seen: id → {account, mailbox}.
76843
76853
  *
@@ -76873,6 +76883,10 @@ var AppleMailManager = class {
76873
76883
  return this.cache.accounts.data;
76874
76884
  }
76875
76885
  const accounts = this.fetchAccounts();
76886
+ if (accounts === null) {
76887
+ return this.cache.accounts?.data ?? [];
76888
+ }
76889
+ this.lastAccountsError = null;
76876
76890
  this.cache.accounts = { data: accounts, expiry: now + this.CACHE_TTL_MS };
76877
76891
  return accounts;
76878
76892
  }
@@ -76974,7 +76988,7 @@ var AppleMailManager = class {
76974
76988
  serverSideCreateGuard(account, op) {
76975
76989
  if (this.isServerSideAccount(account) === true) {
76976
76990
  const verb = op === "rename" ? "rename" : "create";
76977
- return `Account "${account}" stores its mailboxes on the server (IMAP / iCloud / Exchange), and Mail.app cannot ${verb} server-side mailboxes via AppleScript \u2014 a ${verb} would ${op === "rename" ? "leave a half-created orphan" : "orphan a mailbox that can never be removed"}. Configure IMAP for this account (APPLE_MAIL_MCP_IMAP_*) so mailbox create/delete/rename route through the server, or manage the folder in Mail.app directly.`;
76991
+ return `Account "${account}" stores its mailboxes on the server (IMAP / iCloud / Exchange), and Mail.app cannot ${verb} server-side mailboxes via AppleScript \u2014 a ${verb} would ${op === "rename" ? "leave a half-created orphan" : "orphan a mailbox that can never be removed"}. Configure IMAP for this account (APPLE_MAIL_MCP_IMAP_*) so mailbox create/delete/rename route through the server, or manage the folder in Mail.app directly. ${SETUP_HINT}`;
76978
76992
  }
76979
76993
  return null;
76980
76994
  }
@@ -78679,6 +78693,7 @@ var AppleMailManager = class {
78679
78693
  const result = executeAppleScript(script, { timeoutMs: 6e4 });
78680
78694
  if (!result.success) {
78681
78695
  console.error(`Failed to get unread count: ${result.error}`);
78696
+ this.lastAccountsError = result.error ?? "AppleScript transport failed";
78682
78697
  return 0;
78683
78698
  }
78684
78699
  return parseInt(result.output) || 0;
@@ -79204,9 +79219,38 @@ end tell`;
79204
79219
  listAccounts() {
79205
79220
  return this.getCachedAccounts();
79206
79221
  }
79222
+ /**
79223
+ * listAccounts() plus whether the underlying AppleScript read actually worked.
79224
+ *
79225
+ * `failed: true` means the list is a fallback (stale cache or empty) because the
79226
+ * transport errored — NOT that Mail has no accounts. (#130)
79227
+ */
79228
+ listAccountsChecked() {
79229
+ this.lastAccountsError = null;
79230
+ const accounts = this.getCachedAccounts();
79231
+ const error2 = this.lastAccountsError;
79232
+ return error2 ? { accounts, failed: true, error: error2 } : { accounts, failed: false };
79233
+ }
79234
+ /**
79235
+ * getUnreadCount() plus whether the AppleScript read actually worked.
79236
+ *
79237
+ * On failure the count is `null` rather than 0, so a caller can never mistake a
79238
+ * wedged transport for an empty inbox. (#130)
79239
+ */
79240
+ getUnreadCountChecked(mailbox, account) {
79241
+ this.lastAccountsError = null;
79242
+ const count = this.getUnreadCount(mailbox, account);
79243
+ const error2 = this.lastAccountsError;
79244
+ return error2 ? { count: null, failed: true, error: error2 } : { count, failed: false };
79245
+ }
79207
79246
  /**
79208
79247
  * Fetches account list directly from Mail.app via AppleScript.
79209
79248
  * Used internally by the cache; prefer getCachedAccounts() or listAccounts().
79249
+ *
79250
+ * Returns `null` when the AppleScript transport itself failed (timeout, wedged
79251
+ * Mail, denied Automation). That is deliberately distinct from `[]`, which means
79252
+ * "Mail answered, and there genuinely are no accounts" — collapsing the two is
79253
+ * what let a wedged transport report a confident "No Mail accounts found". (#130)
79210
79254
  */
79211
79255
  fetchAccounts() {
79212
79256
  const script = buildAppLevelScript(`
@@ -79227,7 +79271,8 @@ end tell`;
79227
79271
  const result = executeAppleScript(script);
79228
79272
  if (!result.success) {
79229
79273
  console.error(`Failed to list accounts: ${result.error}`);
79230
- return [];
79274
+ this.lastAccountsError = result.error ?? "AppleScript transport failed";
79275
+ return null;
79231
79276
  }
79232
79277
  if (!result.output.trim()) return [];
79233
79278
  const items = result.output.split(RECORD_SEP);
@@ -79739,12 +79784,6 @@ var import_nodemailer = __toESM(require_nodemailer(), 1);
79739
79784
  import { execFileSync } from "child_process";
79740
79785
  import { isAbsolute as isAbsolute2 } from "path";
79741
79786
  import { existsSync as existsSync4 } from "fs";
79742
-
79743
- // src/utils/docsUrls.ts
79744
- var SETUP_GUIDE_URL = "https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/IMAP-SETUP.md";
79745
- var SETUP_HINT = `Setup guide: ${SETUP_GUIDE_URL} \u2014 run the "doctor" tool to check your setup.`;
79746
-
79747
- // src/services/smtpMailer.ts
79748
79787
  var SMTP_ENV = {
79749
79788
  host: "APPLE_MAIL_MCP_SMTP_HOST",
79750
79789
  port: "APPLE_MAIL_MCP_SMTP_PORT",
@@ -80144,7 +80183,7 @@ function specToConfig(spec) {
80144
80183
  }
80145
80184
  if (!pass) {
80146
80185
  throw new Error(
80147
- `No IMAP password for account "${spec.accountLabel}". Set a password or a Keychain service/account.`
80186
+ `No IMAP password for account "${spec.accountLabel}". Set a password or a Keychain service/account. ${SETUP_HINT}`
80148
80187
  );
80149
80188
  }
80150
80189
  return {
@@ -82826,7 +82865,7 @@ ${mailboxList}`, structured);
82826
82865
  server.registerTool(
82827
82866
  "get-unread-count",
82828
82867
  {
82829
- 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).\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).",
82868
+ 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).",
82830
82869
  inputSchema: {
82831
82870
  mailbox: external_exports.string().optional().describe("Mailbox to check (default: INBOX)"),
82832
82871
  account: external_exports.string().optional().describe("Account to check")
@@ -82834,11 +82873,14 @@ server.registerTool(
82834
82873
  outputSchema: {
82835
82874
  unread: external_exports.number().optional(),
82836
82875
  mailbox: external_exports.string().optional(),
82837
- account: external_exports.string().optional()
82876
+ account: external_exports.string().optional(),
82877
+ partial: external_exports.boolean().optional(),
82878
+ failedAccounts: external_exports.array(external_exports.string()).optional()
82838
82879
  }
82839
82880
  },
82840
82881
  withErrorHandling(async ({ mailbox, account }) => {
82841
82882
  let count;
82883
+ const failedAccounts = [];
82842
82884
  if (shouldUseImap(account)) {
82843
82885
  if (account !== void 0) {
82844
82886
  count = await imapUnreadCount(mailbox, { account });
@@ -82851,17 +82893,32 @@ server.registerTool(
82851
82893
  total += await imapUnreadCount(mailbox, { config: src.config });
82852
82894
  } catch (e) {
82853
82895
  console.error(`IMAP unread-count failed for "${src.label}": ${String(e)}`);
82896
+ failedAccounts.push(src.label);
82854
82897
  }
82855
82898
  } else {
82856
- total += mailManager.getUnreadCount(mailbox, src.account.name);
82899
+ const r = mailManager.getUnreadCountChecked(mailbox, src.account.name);
82900
+ if (r.failed) failedAccounts.push(src.account.name);
82901
+ else total += r.count ?? 0;
82857
82902
  }
82858
82903
  }
82859
82904
  count = total;
82860
82905
  }
82861
82906
  } else {
82862
- count = mailManager.getUnreadCount(mailbox, account);
82907
+ const r = mailManager.getUnreadCountChecked(mailbox, account);
82908
+ if (r.failed) {
82909
+ return errorResponse(
82910
+ `Could not read the unread count \u2014 the AppleScript transport failed: ${r.error}. This is NOT the same as zero unread. Mail may be busy, wedged, or missing an Automation grant; run the "doctor" tool to check.`
82911
+ );
82912
+ }
82913
+ count = r.count ?? 0;
82863
82914
  }
82864
82915
  const location = mailbox ? ` in "${mailbox}"` : "";
82916
+ if (failedAccounts.length > 0) {
82917
+ return successResponse(
82918
+ `${count} unread message(s)${location} \u2014 PARTIAL: ${failedAccounts.length} account(s) could not be read (${failedAccounts.join(", ")}), so the real total is higher. Run the "doctor" tool to check.`,
82919
+ { unread: count, mailbox, account, partial: true, failedAccounts }
82920
+ );
82921
+ }
82865
82922
  return successResponse(`${count} unread message(s)${location}`, {
82866
82923
  unread: count,
82867
82924
  mailbox,
@@ -83091,16 +83148,23 @@ ${lines || " (none met the threshold)"}`,
83091
83148
  server.registerTool(
83092
83149
  "list-accounts",
83093
83150
  {
83094
- 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.\nDo not use when: you want the folders within an account (use list-mailboxes) or messages (use list-messages / search-messages).",
83151
+ 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).",
83095
83152
  inputSchema: {},
83096
83153
  outputSchema: {
83097
83154
  accounts: external_exports.array(external_exports.object({}).passthrough()).optional(),
83098
- count: external_exports.number().optional()
83155
+ count: external_exports.number().optional(),
83156
+ partial: external_exports.boolean().optional(),
83157
+ error: external_exports.string().optional()
83099
83158
  }
83100
83159
  },
83101
83160
  withErrorHandling(() => {
83102
- const accounts = mailManager.listAccounts();
83103
- const structured = { accounts, count: accounts.length };
83161
+ const { accounts, failed, error: error2 } = mailManager.listAccountsChecked();
83162
+ const structured = failed ? { accounts, count: accounts.length, partial: true, error: error2 } : { accounts, count: accounts.length };
83163
+ if (failed) {
83164
+ return errorResponse(
83165
+ `Could not read the Mail account list \u2014 the AppleScript transport failed: ${error2}. This is NOT the same as having no accounts. Mail may be busy, wedged, or missing an Automation grant; run the "doctor" tool to check.`
83166
+ );
83167
+ }
83104
83168
  if (accounts.length === 0) {
83105
83169
  return successResponse("No Mail accounts found", structured);
83106
83170
  }
@@ -83252,7 +83316,7 @@ server.registerTool(
83252
83316
  withErrorHandling(({ query }) => {
83253
83317
  const contacts = mailManager.searchContacts(query);
83254
83318
  const structured = {
83255
- contacts: contacts.map((c) => ({ name: c.name, emails: c.emails })),
83319
+ contacts: contacts.map((c) => ({ name: c.name, emails: c.emails, phones: c.phones })),
83256
83320
  count: contacts.length
83257
83321
  };
83258
83322
  if (contacts.length === 0) {
@@ -83260,7 +83324,8 @@ server.registerTool(
83260
83324
  }
83261
83325
  const contactList = contacts.map((c) => {
83262
83326
  const emails = c.emails.length > 0 ? c.emails.join(", ") : "no email";
83263
- return ` - ${c.name} (${emails})`;
83327
+ const phones = c.phones.length > 0 ? `; ${c.phones.join(", ")}` : "";
83328
+ return ` - ${c.name} (${emails}${phones})`;
83264
83329
  }).join("\n");
83265
83330
  return successResponse(`Found ${contacts.length} contact(s):
83266
83331
  ${contactList}`, structured);
@@ -160,9 +160,22 @@ non-secret config goes here — passwords stay in the Keychain.**
160
160
  ### `APPLE_MAIL_MCP_IMAP_ACCOUNT` and routing
161
161
 
162
162
  `APPLE_MAIL_MCP_IMAP_ACCOUNT` should equal the **Mail.app account name** (what
163
- `list-accounts` shows). A tool call routes to IMAP only when its `account`
164
- argument matches a configured account's name or login — so this value is how the
165
- server knows "calls for this account go to IMAP."
163
+ `list-accounts` shows), because that is how an **explicitly named** `account`
164
+ argument is matched to an IMAP config.
165
+
166
+ **Since v2.6.0 you do not need to pass `account` at all for reads.** When any
167
+ IMAP account is configured, the read tools (`search-messages`, `get-thread`,
168
+ `list-messages`, `list-mailboxes`, `get-unread-count`, `get-mail-stats`) prefer
169
+ IMAP automatically:
170
+
171
+ - explicit IMAP `account` → that account over IMAP;
172
+ - explicit non-IMAP `account` → AppleScript;
173
+ - **no `account` → fan out over every configured IMAP account**, with AppleScript
174
+ used only for the accounts no IMAP config covers.
175
+
176
+ The name still matters for targeting a specific account and for the
177
+ mailbox-write ops (`create`/`delete`/`rename-mailbox`), which route to IMAP only
178
+ for an explicitly named IMAP account.
166
179
 
167
180
  ### Multiple accounts
168
181
 
@@ -285,9 +298,17 @@ and a server started there can't read passwords either. Options:
285
298
  restrict the config file's permissions to the service account.
286
299
 
287
300
  **Calls aren't routing to IMAP even though `doctor` shows connected.**
288
- The tool's `account` argument must match the configured account's name/login.
289
- Set `APPLE_MAIL_MCP_IMAP_ACCOUNT` to the exact Mail.app account name from
290
- `list-accounts`, and pass that same `account` to tools.
301
+ For the six **read** tools this should not happen since v2.6.0 — they prefer IMAP
302
+ as soon as any account is configured, with no `account` argument needed. If reads
303
+ still look like AppleScript (slow, or partial-coverage warnings), the usual cause
304
+ is that the server never saw your settings at all: a host that strips the `env`
305
+ block (use the `config.json` method), or a Keychain service/account mismatch so
306
+ the password lookup fails. Run `doctor` — it reports each account separately.
307
+
308
+ For **targeting one specific account**, and for the mailbox-write ops
309
+ (`create`/`delete`/`rename-mailbox`), the `account` argument must match the
310
+ configured account's name/login: set `APPLE_MAIL_MCP_IMAP_ACCOUNT` to the exact
311
+ Mail.app account name from `list-accounts` and pass that same `account`.
291
312
 
292
313
  **A disabled Mail account.** IMAP connects to the server directly, so it works
293
314
  even if the account is disabled in Mail.app — useful for reaching an account the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.10.2",
3
+ "version": "2.10.4",
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",