apple-mail-mcp 2.19.8 → 2.19.10

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
@@ -96,16 +96,19 @@ Two more hosts can run the same `apple-mail` MCP server (`npx -y apple-mail-mcp`
96
96
  ```
97
97
 
98
98
  Restart your Hermes session afterward so the tools load.
99
+
99
100
  - **[Antigravity](https://antigravity.google/)** (Google) — add the server entry from [`.antigravity-plugin/mcp_config.json`](https://github.com/sweetrb/apple-mail-mcp/blob/main/.antigravity-plugin/mcp_config.json) to `~/.gemini/config/mcp_config.json` (or via Antigravity's MCP settings).
100
101
 
101
102
  ### Manual Installation
102
103
 
103
104
  **1. Install the server:**
105
+
104
106
  ```bash
105
107
  npm install -g apple-mail-mcp
106
108
  ```
107
109
 
108
110
  **2. Add to Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
111
+
109
112
  ```json
110
113
  {
111
114
  "mcpServers": {
@@ -118,6 +121,7 @@ npm install -g apple-mail-mcp
118
121
  ```
119
122
 
120
123
  **3. Restart Claude Desktop** and start using natural language:
124
+
121
125
  ```
122
126
  "Show me my unread emails"
123
127
  ```
@@ -154,55 +158,55 @@ tool, and troubleshooting. Verify any time by running the **`doctor`** tool.
154
158
 
155
159
  ### Messages
156
160
 
157
- | Feature | Description |
158
- |---------|-------------|
159
- | **List Messages** | List messages with pagination, sender filter, date display |
160
- | **Search Messages** | Search by sender, subject, content, date range, read/flagged status — across all accounts |
161
- | **Read Messages** | Get full email content (plain text or HTML) |
162
- | **Read Headers** | Get a message's raw RFC 5322 headers — the author's `Date:`, Message-ID, threading ids, `Received:` trace — without downloading the body |
163
- | **Send Email** | Compose and send new emails (attach by file path or inline base64 content) |
164
- | **Send Serial Email** | Mail merge — send personalized emails to a list of recipients with {{placeholder}} support |
165
- | **Create Draft** | Save emails to Drafts folder (attach by file path or inline base64 content) |
166
- | **Reply** | Reply to messages (with reply-all support) |
167
- | **Forward** | Forward messages to new recipients |
168
- | **Get Thread** | Group a conversation by normalized subject (across AppleScript or IMAP) |
169
- | **Mark Read/Unread** | Change read status (single or batch) |
170
- | **Flag/Unflag** | Flag or unflag messages (single or batch) |
171
- | **Delete Messages** | Move messages to trash (single or batch) |
172
- | **Move Messages** | Organize into mailboxes (single or batch) |
173
- | **List Attachments** | View attachment metadata (name, type, size) |
174
- | **Save Attachment** | Save attachments to disk |
175
- | **Fetch Attachment** | Get an attachment's bytes as base64 (no disk write) |
161
+ | Feature | Description |
162
+ | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
163
+ | **List Messages** | List messages with pagination, sender filter, date display |
164
+ | **Search Messages** | Search by sender, subject, content, date range, read/flagged status — across all accounts |
165
+ | **Read Messages** | Get full email content (plain text or HTML) |
166
+ | **Read Headers** | Get a message's raw RFC 5322 headers — the author's `Date:`, Message-ID, threading ids, `Received:` trace — without downloading the body |
167
+ | **Send Email** | Compose and send new emails (attach by file path or inline base64 content) |
168
+ | **Send Serial Email** | Mail merge — send personalized emails to a list of recipients with {{placeholder}} support |
169
+ | **Create Draft** | Save emails to Drafts folder (attach by file path or inline base64 content) |
170
+ | **Reply** | Reply to messages (with reply-all support) |
171
+ | **Forward** | Forward messages to new recipients |
172
+ | **Get Thread** | Group a conversation by normalized subject (across AppleScript or IMAP) |
173
+ | **Mark Read/Unread** | Change read status (single or batch) |
174
+ | **Flag/Unflag** | Flag or unflag messages (single or batch) |
175
+ | **Delete Messages** | Move messages to trash (single or batch) |
176
+ | **Move Messages** | Organize into mailboxes (single or batch) |
177
+ | **List Attachments** | View attachment metadata (name, type, size) |
178
+ | **Save Attachment** | Save attachments to disk |
179
+ | **Fetch Attachment** | Get an attachment's bytes as base64 (no disk write) |
176
180
 
177
181
  Read/list/get tools also return **structured JSON** (`structuredContent`) alongside the text, so agents can consume results without parsing prose.
178
182
 
179
183
  ### Mailbox & Account Management
180
184
 
181
- | Feature | Description |
182
- |---------|-------------|
183
- | **List Mailboxes** | Show all folders with message/unread counts |
184
- | **Create/Delete/Rename Mailbox** | Full mailbox lifecycle management |
185
- | **List Accounts** | Show configured accounts |
186
- | **Unread Count** | Get unread counts per mailbox |
185
+ | Feature | Description |
186
+ | -------------------------------- | ------------------------------------------- |
187
+ | **List Mailboxes** | Show all folders with message/unread counts |
188
+ | **Create/Delete/Rename Mailbox** | Full mailbox lifecycle management |
189
+ | **List Accounts** | Show configured accounts |
190
+ | **Unread Count** | Get unread counts per mailbox |
187
191
 
188
192
  ### Rules, Contacts & Templates
189
193
 
190
- | Feature | Description |
191
- |---------|-------------|
192
- | **List Rules** | View all mail rules and their enabled status |
193
- | **Enable/Disable Rules** | Toggle mail rules on or off |
194
- | **Create/Delete Rules** | Create rules with conditions + actions, or delete by name |
195
- | **Search Contacts** | Look up contacts from Contacts.app by name |
196
- | **Email Templates** | Save, list, use, and delete reusable email templates (persisted to disk across restarts) |
194
+ | Feature | Description |
195
+ | ------------------------ | ---------------------------------------------------------------------------------------- |
196
+ | **List Rules** | View all mail rules and their enabled status |
197
+ | **Enable/Disable Rules** | Toggle mail rules on or off |
198
+ | **Create/Delete Rules** | Create rules with conditions + actions, or delete by name |
199
+ | **Search Contacts** | Look up contacts from Contacts.app by name |
200
+ | **Email Templates** | Save, list, use, and delete reusable email templates (persisted to disk across restarts) |
197
201
 
198
202
  ### Diagnostics
199
203
 
200
- | Feature | Description |
201
- |---------|-------------|
202
- | **Health Check** | Verify Mail.app connectivity |
203
- | **Doctor** | Diagnose Mail permission, account state, and each IMAP/SMTP backend with actionable messages |
204
- | **Statistics** | Message and unread counts per account, recently received stats |
205
- | **Sync Status** | Check if Mail.app is actively syncing |
204
+ | Feature | Description |
205
+ | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
206
+ | **Health Check** | Verify Mail.app connectivity |
207
+ | **Doctor** | Diagnose Mail permission, account state, and each IMAP/SMTP backend with actionable messages |
208
+ | **Statistics** | Message and unread counts per account, recently received stats |
209
+ | **Sync Status** | Check if Mail.app is actively syncing |
206
210
  | **Effect reconciliation** | Every delete/move reports what it actually did to the mailbox (`countDelta`), and warns when more messages left than were operated on — see [Auditing destructive operations](#auditing-destructive-operations) |
207
211
 
208
212
  ### MCP resources & prompts
@@ -223,18 +227,19 @@ This section documents all available tools. AI agents should use these tool name
223
227
 
224
228
  Search for messages matching criteria. Searches all accounts by default.
225
229
 
226
- | Parameter | Type | Required | Description |
227
- |-----------|------|----------|-------------|
228
- | `query` | string | No | Text to search in subject/sender |
229
- | `from` | string | No | Filter by sender email address |
230
- | `subject` | string | No | Filter by subject line |
231
- | `mailbox` | string | No | Mailbox to search in (omit to search all mailboxes) |
232
- | `account` | string | No | Account to search in (omit to search all accounts) |
233
- | `isRead` | boolean | No | Filter by read status |
234
- | `isFlagged` | boolean | No | Filter by flagged status |
235
- | `dateFrom` | string | No | Start date filter (e.g., "January 1, 2026") |
236
- | `dateTo` | string | No | End date filter (e.g., "March 1, 2026") |
237
- | `limit` | number | No | Max results, 1–500 (default: 50) |
230
+ | Parameter | Type | Required | Description |
231
+ | ----------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
232
+ | `query` | string | No | Text to search in subject/sender |
233
+ | `body` | string | No | Text to search in the message body. IMAP backend only (server-side `BODY` search); AppleScript-only accounts are reported as not searched |
234
+ | `from` | string | No | Filter by sender email address |
235
+ | `subject` | string | No | Filter by subject line |
236
+ | `mailbox` | string | No | Mailbox to search in (omit to search all mailboxes) |
237
+ | `account` | string | No | Account to search in (omit to search all accounts) |
238
+ | `isRead` | boolean | No | Filter by read status |
239
+ | `isFlagged` | boolean | No | Filter by flagged status |
240
+ | `dateFrom` | string | No | Start date filter (e.g., "January 1, 2026") |
241
+ | `dateTo` | string | No | End date filter (e.g., "March 1, 2026") |
242
+ | `limit` | number | No | Max results, 1–500 (default: 50) |
238
243
 
239
244
  **Returns:** List of matching messages with ID, date, subject, sender, and read state.
240
245
 
@@ -272,16 +277,16 @@ to disable the guard and attempt every mailbox regardless of size).
272
277
 
273
278
  **Coverage diagnostics (structured fields).** The warning above is prose for a
274
279
  human reader; the same information is also returned as structured fields on
275
- `search-messages` and `list-messages`, so a caller can tell *"nothing matched"*
276
- apart from *"I did not look everywhere"* without parsing the text:
280
+ `search-messages` and `list-messages`, so a caller can tell _"nothing matched"_
281
+ apart from _"I did not look everywhere"_ without parsing the text:
277
282
 
278
- | Field | Type | Meaning |
279
- |---|---|---|
280
- | `partial` | boolean | Coverage was incomplete — **the result is not a confirmed "no such mail"**. True whenever any field below is non-empty. |
281
- | `skippedLargeMailboxes` | string[] | Mailboxes never scanned because their message count exceeded `APPLE_MAIL_MAX_SEARCH_MAILBOX`, formatted `"Account / Mailbox (count)"` — e.g. `"iCloud / Archive (90694)"`. |
282
- | `notSearchedMailboxes` | string[] | Mailboxes that *were* reached but timed out or errored mid-scan, formatted `"Account / Mailbox"`. Also carries the IMAP path's `failedMailboxes`. |
283
- | `timedOutAccounts` | string[] | Accounts whose whole-account AppleScript was killed by the per-account time budget — nothing from that account was searched. |
284
- | `failedMailboxes` | string[] | IMAP-backend mailboxes that errored. These are merged into `notSearchedMailboxes` as well; read that field unless you need to attribute the failure to the IMAP path specifically. |
283
+ | Field | Type | Meaning |
284
+ | ----------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
285
+ | `partial` | boolean | Coverage was incomplete — **the result is not a confirmed "no such mail"**. True whenever any field below is non-empty. |
286
+ | `skippedLargeMailboxes` | string[] | Mailboxes never scanned because their message count exceeded `APPLE_MAIL_MAX_SEARCH_MAILBOX`, formatted `"Account / Mailbox (count)"` — e.g. `"iCloud / Archive (90694)"`. |
287
+ | `notSearchedMailboxes` | string[] | Mailboxes that _were_ reached but timed out or errored mid-scan, formatted `"Account / Mailbox"`. Also carries the IMAP path's `failedMailboxes`. |
288
+ | `timedOutAccounts` | string[] | Accounts whose whole-account AppleScript was killed by the per-account time budget — nothing from that account was searched. |
289
+ | `failedMailboxes` | string[] | IMAP-backend mailboxes that errored. These are merged into `notSearchedMailboxes` as well; read that field unless you need to attribute the failure to the IMAP path specifically. |
285
290
 
286
291
  Treat a non-empty `skippedLargeMailboxes` as actionable rather than
287
292
  informational: re-run scoped to the named mailbox with a `dateFrom`/`dateTo`
@@ -295,12 +300,12 @@ coverage was complete.
295
300
 
296
301
  Get the full content of a message.
297
302
 
298
- | Parameter | Type | Required | Description |
299
- |-----------|------|----------|-------------|
300
- | `id` | string | Yes | Message ID |
301
- | `preferHtml` | boolean | No | Return HTML source instead of plain text |
302
- | `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 |
303
- | `account` | string | No | Account holding the message. Pair with `mailbox` to skip the cross-mailbox scan |
303
+ | Parameter | Type | Required | Description |
304
+ | ------------ | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
305
+ | `id` | string | Yes | Message ID |
306
+ | `preferHtml` | boolean | No | Return HTML source instead of plain text |
307
+ | `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 |
308
+ | `account` | string | No | Account holding the message. Pair with `mailbox` to skip the cross-mailbox scan |
304
309
 
305
310
  **Returns:** Subject line and message body (plain text by default, HTML if `preferHtml` is true and HTML content is available). `structuredContent` also carries `rfcMessageId` and, since 2.19.0, two dates: `dateSent` (the message's `Date:` header — Mail's `date sent`) and `dateReceived` (arrival in the mailbox — Mail's `date received` / IMAP `INTERNALDATE`). They differ legitimately by transit time; when they differ by **years**, the mailbox was migrated or re-imported and the arrival timestamp was reset — trust `dateSent` for chronology ([#224](https://github.com/sweetrb/apple-mail-mcp/issues/224)). Since 2.19.6 `dateSent` is **omitted** when it is more than 7 days later than `dateReceived`: a message cannot be sent after it arrived, and Mail.app substitutes a timestamp of its own for a `Date:` header it cannot parse ([#234](https://github.com/sweetrb/apple-mail-mcp/issues/234)). `isHtml` reports what was actually returned — a message with no `text/plain` part returns its HTML part with `isHtml: true`. Bodies are decoded by each part's declared `charset`, falling back to windows-1252 for bytes that are not valid UTF-8.
306
311
 
@@ -317,15 +322,36 @@ Get the full content of a message.
317
322
 
318
323
  Return a message's raw RFC 5322 header block — without fetching the body or any attachment — plus the parsed fields chronological and threading work needs. Added in 2.19.0 for mailboxes whose arrival timestamps were reset by a migration ([#224](https://github.com/sweetrb/apple-mail-mcp/issues/224)): the `Date:` header is the author's send time and survives such moves; `INTERNALDATE` / `date received` does not.
319
324
 
320
- | Parameter | Type | Required | Description |
321
- |-----------|------|----------|-------------|
322
- | `id` | string | Yes | Message ID (numeric or `imap:…`) |
323
- | `mailbox` | string | No | Mailbox holding the message. With `account`, opens that mailbox directly instead of scanning every mailbox |
324
- | `account` | string | No | Account holding the message. Pair with `mailbox` to skip the cross-mailbox scan |
325
+ | Parameter | Type | Required | Description |
326
+ | --------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------- |
327
+ | `id` | string | Yes | Message ID (numeric or `imap:…`) |
328
+ | `mailbox` | string | No | Mailbox holding the message. With `account`, opens that mailbox directly instead of scanning every mailbox |
329
+ | `account` | string | No | Account holding the message. Pair with `mailbox` to skip the cross-mailbox scan |
325
330
 
326
331
  **Returns:** The raw header block as text. `structuredContent` carries `raw`, every header as ordered `headers[]` (`{name, value}`, folded lines joined, duplicates such as `Received:` kept in wire order, values left RFC 2047-encoded), `headerCount`, and the decoded key fields: `date` (ISO 8601, from the `Date:` header), `dateHeader` (verbatim), `dateReceived` (mailbox arrival time — IMAP `INTERNALDATE` or Mail's `date received`), `messageId`, `subject`, `from`, `to`, `cc`, `replyTo`, `inReplyTo`, `references[]` and `received[]` (first entry = last hop). Fields the message does not carry are omitted. `backend` says which backend read the block (`"imap"` or `"applescript"`), and `warnings[]` appears when a malformed block was repaired ([#234](https://github.com/sweetrb/apple-mail-mcp/issues/234)).
327
332
 
328
- **Backends:** an `imap:` id reads the first 64 KiB of `BODY.PEEK[]` over IMAP and cuts the header block out of it (cheap even for a 20 MB message). That is deliberate: iCloud rewrites 8-bit header bytes to `*` in ENVELOPE and `BODY[HEADER]`, and only `BODY[]` returns them as stored; each line is decoded as UTF-8, falling back to windows-1252, so a raw latin-1 display name survives. A header block larger than the window falls back to `BODY.PEEK[HEADER]`. A numeric id reads Mail's `all headers` property over AppleScript, with the same mailbox-scoped fast path as `get-message`. ⚠️ That property is Mail's own rendering, not the stored bytes: for a `Date:` value Mail cannot parse it drops the value and joins the next header onto the name (`Date: Subject: …`). The tool splits that back apart, reports the date as absent rather than as a Subject string, and says so in `warnings[]`; the `imap:` id for the same message has the real `Date:`.
333
+ **Backends (`get-message-headers`):** an `imap:` id reads the first 64 KiB of `BODY.PEEK[]` over IMAP and cuts the header block out of it (cheap even for a 20 MB message). That is deliberate: iCloud rewrites 8-bit header bytes to `*` in ENVELOPE and `BODY[HEADER]`, and only `BODY[]` returns them as stored; each line is decoded as UTF-8, falling back to windows-1252, so a raw latin-1 display name survives. A header block larger than the window falls back to `BODY.PEEK[HEADER]`. A numeric id reads Mail's `all headers` property over AppleScript, with the same mailbox-scoped fast path as `get-message`. ⚠️ That property is Mail's own rendering, not the stored bytes: for a `Date:` value Mail cannot parse it drops the value and joins the next header onto the name (`Date: Subject: …`). The tool splits that back apart, reports the date as absent rather than as a Subject string, and says so in `warnings[]`; the `imap:` id for the same message has the real `Date:`.
334
+
335
+ ---
336
+
337
+ #### `get-message-rfc822`
338
+
339
+ Acquire a message's **complete original RFC 822 bytes exactly as the IMAP server stores them**, together with the IMAP identity that links the copy back to its source ([#244](https://github.com/sweetrb/apple-mail-mcp/issues/244)). Built for archival, forensic review and evidence preservation: nothing is decoded, charset-converted, line-ending-normalized or MIME-re-serialized, and the SHA-256 is computed over exactly the bytes returned. **IMAP-only** — it needs an `imap:` id, i.e. an IMAP-configured account ([docs/IMAP-SETUP.md](docs/IMAP-SETUP.md)). There is no AppleScript path on purpose: Mail's bridge exposes its own rendering of a message, not the stored bytes.
340
+
341
+ | Parameter | Type | Required | Description |
342
+ | ---------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
343
+ | `id` | string | Yes | An `imap:` message id (numeric Mail.app ids are refused with an explanation) |
344
+ | `savePath` | string | No | Directory inside the configured allowed roots. Writes the bytes to a **new** `.eml` there instead of returning them inline, and raises the size ceiling to 25 MiB |
345
+ | `fileName` | string | No | File name to use with `savePath` (no path separators, no `..`). Default `<account>-<mailbox>-uidv<UIDVALIDITY>-uid<UID>.eml` |
346
+ | `maxBytes` | number | No | Refuse — never truncate — a message larger than this many raw bytes. Default and inline maximum 6 MiB (`6291456`); with `savePath` up to 25 MiB (`26214400`) |
347
+
348
+ **Returns:** a one-line summary as text. `structuredContent` carries the acquisition record: `account`, `mailbox`, `uid`, `uidValidity` (decimal string), `internalDate` (ISO 8601 `INTERNALDATE` — arrival, not the `Date:` header), `flags[]`, `size` (the server's `RFC822.SIZE`), `bytes` (the count actually acquired and hashed), `sha256` (hex, over exactly those bytes), `messageId` (from `ENVELOPE`, for convenience — the authoritative copy is in the bytes), `readMethod` (the IMAP commands issued), `backend: "imap"` and `warnings[]` — plus **`contentBase64`** in inline mode or **`savedPath`** in file mode. The base64 is carried **only** in `structuredContent`; the text block never repeats it.
349
+
350
+ **Read-only by construction.** The mailbox is opened with `EXAMINE` and the body fetched with `BODY.PEEK[]`, so `\Seen` is not set and no `STORE`, `COPY`, `MOVE`, `APPEND` or `EXPUNGE` is issued. No Mail.app, AppleScript or osascript is involved. The only side effect is the optional file, created with `wx` (an existing file or symlink is refused, never overwritten) and mode `0600`, and only inside the allowed roots — the same policy as `save-attachment`.
351
+
352
+ **Sizes.** The inline ceiling exists because the MCP stdio transport drops any single message over 10 MB with no error text: 6 MiB of raw bytes is 8 MiB of base64. `maxBytes` is a ceiling, not a cut — an oversize message is refused with its `RFC822.SIZE`, so you can choose between raising `maxBytes` (inline, up to 6 MiB) and `savePath` (up to 25 MiB). `warnings[]` reports an `RFC822.SIZE` that disagrees with the byte count actually received (the hash still covers what was received) and a server that reported no `UIDVALIDITY`.
353
+
354
+ **Verifying a derivative.** `shasum -a 256 file.eml` must equal `sha256`. `account` + `mailbox` + `uidValidity` + `uid` is the durable pointer back to the source message — a UID on its own is meaningful only for one `UIDVALIDITY`, which the server may change (typically on a mailbox rebuild).
329
355
 
330
356
  ---
331
357
 
@@ -333,14 +359,14 @@ Return a message's raw RFC 5322 header block — without fetching the body or an
333
359
 
334
360
  List messages in a mailbox.
335
361
 
336
- | Parameter | Type | Required | Description |
337
- |-----------|------|----------|-------------|
338
- | `mailbox` | string | No | Mailbox name (omit to list from all mailboxes) |
339
- | `account` | string | No | Account name |
340
- | `limit` | number | No | Max messages, 1–500 (default: 50) |
341
- | `offset` | number | No | Number of messages to skip, ≥ 0 (for pagination) |
342
- | `from` | string | No | Filter by sender email address or name |
343
- | `unreadOnly` | boolean | No | Only show unread messages |
362
+ | Parameter | Type | Required | Description |
363
+ | ------------ | ------- | -------- | ------------------------------------------------ |
364
+ | `mailbox` | string | No | Mailbox name (omit to list from all mailboxes) |
365
+ | `account` | string | No | Account name |
366
+ | `limit` | number | No | Max messages, 1–500 (default: 50) |
367
+ | `offset` | number | No | Number of messages to skip, ≥ 0 (for pagination) |
368
+ | `from` | string | No | Filter by sender email address or name |
369
+ | `unreadOnly` | boolean | No | Only show unread messages |
344
370
 
345
371
  **Returns:** List of messages with ID, date, subject, and sender.
346
372
 
@@ -367,18 +393,19 @@ Send a new email immediately.
367
393
 
368
394
  **⚠️ Safety:** Sends real mail immediately and cannot be unsent. Confirm the recipients, subject, and body with the user before calling.
369
395
 
370
- | Parameter | Type | Required | Description |
371
- |-----------|------|----------|-------------|
372
- | `to` | string[] | Yes | Recipient addresses |
373
- | `subject` | string | Yes | Email subject |
374
- | `body` | string | Yes | Email body (plain text) |
375
- | `cc` | string[] | No | CC recipients |
376
- | `bcc` | string[] | No | BCC recipients |
377
- | `account` | string | No | Mail.app account label, or an email-form SMTP From override. An SMTP override must match `APPLE_MAIL_MCP_SMTP_USER`, `APPLE_MAIL_MCP_SMTP_FROM`, or an address in `APPLE_MAIL_MCP_SMTP_ALLOWED_FROM` |
378
- | `attachments` | (string \| {filename, contentBase64})[] | No | Up to 20 attachments: absolute file paths inside the configured read roots (e.g., `"/Users/me/Documents/report.pdf"`) and/or inline `{filename, contentBase64}` objects up to 25 MiB decoded each |
379
- | `transport` | `"applescript"` \| `"smtp"` | No | Send transport. If omitted, **SMTP is used automatically when configured** (otherwise AppleScript). Pass `"smtp"` to require clean MIME, or `"applescript"` to force the Mail.app path — see [SMTP transport](#smtp-transport) |
396
+ | Parameter | Type | Required | Description |
397
+ | ------------- | --------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
398
+ | `to` | string[] | Yes | Recipient addresses |
399
+ | `subject` | string | Yes | Email subject |
400
+ | `body` | string | Yes | Email body (plain text) |
401
+ | `cc` | string[] | No | CC recipients |
402
+ | `bcc` | string[] | No | BCC recipients |
403
+ | `account` | string | No | Mail.app account label, or an email-form SMTP From override. An SMTP override must match `APPLE_MAIL_MCP_SMTP_USER`, `APPLE_MAIL_MCP_SMTP_FROM`, or an address in `APPLE_MAIL_MCP_SMTP_ALLOWED_FROM` |
404
+ | `attachments` | (string \| {filename, contentBase64})[] | No | Up to 20 attachments: absolute file paths inside the configured read roots (e.g., `"/Users/me/Documents/report.pdf"`) and/or inline `{filename, contentBase64}` objects up to 25 MiB decoded each |
405
+ | `transport` | `"applescript"` \| `"smtp"` | No | Send transport. If omitted, **SMTP is used automatically when configured** (otherwise AppleScript). Pass `"smtp"` to require clean MIME, or `"applescript"` to force the Mail.app path — see [SMTP transport](#smtp-transport) |
380
406
 
381
407
  **Example:**
408
+
382
409
  ```json
383
410
  {
384
411
  "to": ["colleague@company.com"],
@@ -415,7 +442,7 @@ Two differences to know when SMTP is auto-preferred:
415
442
  Use `transport: "applescript"` if you want Mail.app itself to file the copy.
416
443
  - **`account` is a From override, not account selection.** Over SMTP, `account`
417
444
  is used as the From address only when it is an email address; a Mail.app
418
- account *label* (e.g. `"Work"`) can't select an account over SMTP, so a call
445
+ account _label_ (e.g. `"Work"`) can't select an account over SMTP, so a call
419
446
  that passes one is left on the AppleScript path automatically. To force
420
447
  account selection, pass `transport: "applescript"` explicitly. For sender
421
448
  safety, an email-form override must match the SMTP login user, the configured
@@ -436,18 +463,18 @@ trusted isolated server or test fixture; it disables the upgrade requirement and
436
463
  can expose credentials and message content. The server emits a warning when it
437
464
  is used. Keep the default unset.
438
465
 
439
- | Variable | Required | Default | Description |
440
- |----------|----------|---------|-------------|
441
- | `APPLE_MAIL_MCP_SMTP_HOST` | Yes | — | SMTP server hostname (e.g. `smtp.fastmail.com`) |
442
- | `APPLE_MAIL_MCP_SMTP_USER` | Yes | — | SMTP username |
443
- | `APPLE_MAIL_MCP_SMTP_PORT` | No | `465` if secure, else `587` | SMTP port |
444
- | `APPLE_MAIL_MCP_SMTP_SECURE` | No | `false` | `true` for implicit TLS (port 465); otherwise STARTTLS |
445
- | `APPLE_MAIL_MCP_SMTP_ALLOW_PLAINTEXT` | No | `0` | Set `1` only for an explicitly trusted plaintext test/server; otherwise STARTTLS is required |
446
- | `APPLE_MAIL_MCP_SMTP_FROM` | No | = user | From address |
447
- | `APPLE_MAIL_MCP_SMTP_ALLOWED_FROM` | No | — | Comma-separated sender aliases permitted as per-message From overrides |
448
- | `APPLE_MAIL_MCP_SMTP_PASSWORD` | No | — | Password (if set, used instead of the Keychain) |
449
- | `APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE` | No | = host | Keychain item service/server name |
450
- | `APPLE_MAIL_MCP_SMTP_KEYCHAIN_ACCOUNT` | No | = user | Keychain item account |
466
+ | Variable | Required | Default | Description |
467
+ | -------------------------------------- | -------- | --------------------------- | -------------------------------------------------------------------------------------------- |
468
+ | `APPLE_MAIL_MCP_SMTP_HOST` | Yes | — | SMTP server hostname (e.g. `smtp.fastmail.com`) |
469
+ | `APPLE_MAIL_MCP_SMTP_USER` | Yes | — | SMTP username |
470
+ | `APPLE_MAIL_MCP_SMTP_PORT` | No | `465` if secure, else `587` | SMTP port |
471
+ | `APPLE_MAIL_MCP_SMTP_SECURE` | No | `false` | `true` for implicit TLS (port 465); otherwise STARTTLS |
472
+ | `APPLE_MAIL_MCP_SMTP_ALLOW_PLAINTEXT` | No | `0` | Set `1` only for an explicitly trusted plaintext test/server; otherwise STARTTLS is required |
473
+ | `APPLE_MAIL_MCP_SMTP_FROM` | No | = user | From address |
474
+ | `APPLE_MAIL_MCP_SMTP_ALLOWED_FROM` | No | — | Comma-separated sender aliases permitted as per-message From overrides |
475
+ | `APPLE_MAIL_MCP_SMTP_PASSWORD` | No | — | Password (if set, used instead of the Keychain) |
476
+ | `APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE` | No | = host | Keychain item service/server name |
477
+ | `APPLE_MAIL_MCP_SMTP_KEYCHAIN_ACCOUNT` | No | = user | Keychain item account |
451
478
 
452
479
  Store the password in the Keychain once (an app-specific password for Gmail/
453
480
  iCloud). A generic-password item with an explicit service name keeps it from
@@ -467,6 +494,7 @@ security add-generic-password -s apple-mail-mcp-smtp -a you@gmail.com -w
467
494
 
468
495
  Once the env vars are set, a plain `send-email` (no `transport`) already goes
469
496
  out clean:
497
+
470
498
  ```json
471
499
  {
472
500
  "to": ["colleague@company.com"],
@@ -513,7 +541,7 @@ What routes to IMAP when an account is IMAP-configured:
513
541
  - **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).
514
542
  - **Message mutations:** `mark-as-read`/`unread`, `flag-message`/`unflag-message`, `move-message`, `delete-message`.
515
543
  - **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.
516
- - **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.
544
+ - **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.
517
545
  - **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.
518
546
  - **Threading (2.1):** `get-thread` links a conversation via `References`/`Message-ID` (`HEADER SEARCH`) for an `imap:` seed, falling back to subject grouping otherwise.
519
547
 
@@ -533,9 +561,9 @@ matching `account` is passed. There are three cases:
533
561
  - **Explicit IMAP account** — single-account IMAP (fast server-side path).
534
562
  - **Explicit non-IMAP account** — AppleScript (that account isn't on IMAP).
535
563
  - **No `account` given** — **merge across all accounts**: the query fans out over
536
- *every* configured IMAP account, **and** AppleScript runs **only for the
564
+ _every_ configured IMAP account, **and** AppleScript runs **only for the
537
565
  accounts no IMAP config covers** (the account list is partitioned — accounts
538
- already served by IMAP are *not* re-scanned via AppleScript). If every Mail
566
+ already served by IMAP are _not_ re-scanned via AppleScript). If every Mail
539
567
  account is IMAP-configured, AppleScript is skipped entirely. The results are
540
568
  merged so no account is dropped. Message lists still de-duplicate as a safety
541
569
  net (preferring the IMAP copy, which carries the round-trippable `imap:` id) and
@@ -554,7 +582,7 @@ matching `account` is passed. There are three cases:
554
582
  one `SEARCH` + a bounded `FETCH` per mailbox over the pooled IMAP
555
583
  connection — pin a `mailbox` to skip the fan-out when you already know
556
584
  where to look. `list-messages` (no query) still defaults an omitted
557
- mailbox to `INBOX` on every provider — only unscoped *search* scans the
585
+ mailbox to `INBOX` on every provider — only unscoped _search_ scans the
558
586
  whole account.
559
587
 
560
588
  If IMAP is **not** configured at all, every read behaves exactly as before
@@ -562,21 +590,21 @@ If IMAP is **not** configured at all, every read behaves exactly as before
562
590
  `delete-mailbox`, `rename-mailbox`) remain conservative — they route to IMAP only
563
591
  for an explicitly-named IMAP account, never on an omitted account.
564
592
 
565
- | Variable | Required | Default | Description |
566
- |----------|----------|---------|-------------|
567
- | `APPLE_MAIL_MCP_IMAP_USER` | Yes | — | Login address; setting it enables IMAP |
568
- | `APPLE_MAIL_MCP_IMAP_ACCOUNT` | No | = user | Mail account name to match for routing |
569
- | `APPLE_MAIL_MCP_IMAP_HOST` | No | `imap.gmail.com` | IMAP server hostname |
570
- | `APPLE_MAIL_MCP_IMAP_PORT` | No | `993` | IMAP port (993 = implicit TLS) |
571
- | `APPLE_MAIL_MCP_IMAP_ALLOW_PLAINTEXT` | No | `0` | Set `1` only for an explicitly trusted plaintext test/server; otherwise STARTTLS is required |
572
- | `APPLE_MAIL_MCP_IMAP_PASSWORD` | No | — | Password (if set, used instead of the Keychain) |
573
- | `APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE` | No | — | Keychain item service/server name |
574
- | `APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT` | No | = user | Keychain item account |
575
- | `APPLE_MAIL_MCP_IMAP_ACCOUNTS` | No | — | JSON array of **additional** IMAP accounts for multi-account setups (see below) |
576
- | `APPLE_MAIL_MCP_IMAP_IDLE` | No | `0` | Set `1` to enable IMAP IDLE push notifications (new-mail alerts) for every configured account |
577
- | `APPLE_MAIL_MCP_IMAP_IDLE_MS` | No | `30000` | Idle timeout (ms) before a pooled IMAP connection is closed (`0` = never close) |
578
- | `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 |
579
- | `APPLE_MAIL_MCP_STATS_DEADLINE_MS` | No | `50000` | Overall wall-clock deadline for one `get-mail-stats` call (minimum `2000`), measured from when the request arrived and covering time queued behind other tool calls, account enumeration **and** every per-account read. Keep it below your client's request timeout |
593
+ | Variable | Required | Default | Description |
594
+ | -------------------------------------- | -------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
595
+ | `APPLE_MAIL_MCP_IMAP_USER` | Yes | — | Login address; setting it enables IMAP |
596
+ | `APPLE_MAIL_MCP_IMAP_ACCOUNT` | No | = user | Mail account name to match for routing |
597
+ | `APPLE_MAIL_MCP_IMAP_HOST` | No | `imap.gmail.com` | IMAP server hostname |
598
+ | `APPLE_MAIL_MCP_IMAP_PORT` | No | `993` | IMAP port (993 = implicit TLS) |
599
+ | `APPLE_MAIL_MCP_IMAP_ALLOW_PLAINTEXT` | No | `0` | Set `1` only for an explicitly trusted plaintext test/server; otherwise STARTTLS is required |
600
+ | `APPLE_MAIL_MCP_IMAP_PASSWORD` | No | — | Password (if set, used instead of the Keychain) |
601
+ | `APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE` | No | — | Keychain item service/server name |
602
+ | `APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT` | No | = user | Keychain item account |
603
+ | `APPLE_MAIL_MCP_IMAP_ACCOUNTS` | No | — | JSON array of **additional** IMAP accounts for multi-account setups (see below) |
604
+ | `APPLE_MAIL_MCP_IMAP_IDLE` | No | `0` | Set `1` to enable IMAP IDLE push notifications (new-mail alerts) for every configured account |
605
+ | `APPLE_MAIL_MCP_IMAP_IDLE_MS` | No | `30000` | Idle timeout (ms) before a pooled IMAP connection is closed (`0` = never close) |
606
+ | `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 |
607
+ | `APPLE_MAIL_MCP_STATS_DEADLINE_MS` | No | `50000` | Overall wall-clock deadline for one `get-mail-stats` call (minimum `2000`), measured from when the request arrived and covering time queued behind other tool calls, account enumeration **and** every per-account read. Keep it below your client's request timeout |
580
608
 
581
609
  **Multiple IMAP accounts (C2):** set `APPLE_MAIL_MCP_IMAP_ACCOUNTS` to a JSON array, e.g.
582
610
  `[{"account":"Work","user":"me@co.com","host":"imap.co.com","keychainService":"imap.co.com"}]`.
@@ -622,9 +650,9 @@ those slots. This server keeps its footprint small:
622
650
  polling every 30s, so it can't linger holding sockets after its session is gone.
623
651
 
624
652
  The catch is **multiple concurrent instances**. A host like the Claude desktop
625
- app spawns a *separate* set of MCP servers per open conversation (and respawns
653
+ app spawns a _separate_ set of MCP servers per open conversation (and respawns
626
654
  them after a crash), so the footprint is **per instance × accounts**. With IDLE
627
- off, an idle instance trends to 0 connections; with many *active* conversations
655
+ off, an idle instance trends to 0 connections; with many _active_ conversations
628
656
  or IDLE on, the per-account total climbs toward Gmail's 15-connection cap and can
629
657
  starve Apple Mail of slots (→ intermittent "cannot connect"). If you hit that,
630
658
  close idle Claude conversations, keep `APPLE_MAIL_MCP_IMAP_IDLE` off unless you
@@ -701,22 +729,23 @@ Enable it in your MCP client config alongside the IMAP settings:
701
729
 
702
730
  Send individual personalized emails to a list of recipients (mail merge). Each recipient receives their own email — recipients don't see each other. Supports `{{placeholder}}` tokens in both subject and body.
703
731
 
704
- | Parameter | Type | Required | Description |
705
- |-----------|------|----------|-------------|
706
- | `recipients` | object[] | Yes | List of recipients, max 100 (see below) |
707
- | `subject` | string | Yes | Email subject — use `{{Key}}` for placeholders |
708
- | `body` | string | Yes | Email body — use `{{Key}}` for placeholders |
709
- | `account` | string | No | Send from specific account |
710
- | `delayMs` | number | No | Delay between sends in ms (default: 500, max 10000) |
732
+ | Parameter | Type | Required | Description |
733
+ | ------------ | -------- | -------- | --------------------------------------------------- |
734
+ | `recipients` | object[] | Yes | List of recipients, max 100 (see below) |
735
+ | `subject` | string | Yes | Email subject — use `{{Key}}` for placeholders |
736
+ | `body` | string | Yes | Email body — use `{{Key}}` for placeholders |
737
+ | `account` | string | No | Send from specific account |
738
+ | `delayMs` | number | No | Delay between sends in ms (default: 500, max 10000) |
711
739
 
712
740
  Each recipient object:
713
741
 
714
- | Field | Type | Required | Description |
715
- |-------|------|----------|-------------|
716
- | `email` | string | Yes | Recipient email address |
717
- | `variables` | object | Yes | Key-value pairs for placeholder replacement |
742
+ | Field | Type | Required | Description |
743
+ | ----------- | ------ | -------- | ------------------------------------------- |
744
+ | `email` | string | Yes | Recipient email address |
745
+ | `variables` | object | Yes | Key-value pairs for placeholder replacement |
718
746
 
719
747
  **Example:**
748
+
720
749
  ```json
721
750
  {
722
751
  "recipients": [
@@ -738,15 +767,15 @@ Each recipient object:
738
767
 
739
768
  Save an email to Drafts without sending.
740
769
 
741
- | Parameter | Type | Required | Description |
742
- |-----------|------|----------|-------------|
743
- | `to` | string[] | Yes | Recipient addresses |
744
- | `subject` | string | Yes | Email subject |
745
- | `body` | string | Yes | Email body (plain text) |
746
- | `cc` | string[] | No | CC recipients |
747
- | `bcc` | string[] | No | BCC recipients |
748
- | `account` | string | No | Account for draft |
749
- | `attachments` | (string \| {filename, contentBase64})[] | No | Up to 20 attachments: absolute file paths inside the configured read roots and/or inline `{filename, contentBase64}` objects up to 25 MiB decoded each |
770
+ | Parameter | Type | Required | Description |
771
+ | ------------- | --------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
772
+ | `to` | string[] | Yes | Recipient addresses |
773
+ | `subject` | string | Yes | Email subject |
774
+ | `body` | string | Yes | Email body (plain text) |
775
+ | `cc` | string[] | No | CC recipients |
776
+ | `bcc` | string[] | No | BCC recipients |
777
+ | `account` | string | No | Account for draft |
778
+ | `attachments` | (string \| {filename, contentBase64})[] | No | Up to 20 attachments: absolute file paths inside the configured read roots and/or inline `{filename, contentBase64}` objects up to 25 MiB decoded each |
750
779
 
751
780
  **Returns:** Confirmation that draft was created.
752
781
 
@@ -754,12 +783,12 @@ Save an email to Drafts without sending.
754
783
 
755
784
  Group a conversation by normalized subject (across the AppleScript or IMAP backend).
756
785
 
757
- | Parameter | Type | Required | Description |
758
- |-----------|------|----------|-------------|
759
- | `id` | string | Yes | A message ID in the conversation (numeric or `imap:…`) |
760
- | `account` | string | No | Account to search (omit to search all) |
761
- | `mailbox` | string | No | Mailbox to search (omit to search all) |
762
- | `limit` | number | No | Max messages in the thread (default 50) |
786
+ | Parameter | Type | Required | Description |
787
+ | --------- | ------ | -------- | ------------------------------------------------------ |
788
+ | `id` | string | Yes | A message ID in the conversation (numeric or `imap:…`) |
789
+ | `account` | string | No | Account to search (omit to search all) |
790
+ | `mailbox` | string | No | Mailbox to search (omit to search all) |
791
+ | `limit` | number | No | Max messages in the thread (default 50) |
763
792
 
764
793
  **Returns:** The conversation's messages, oldest-first.
765
794
 
@@ -767,10 +796,10 @@ Group a conversation by normalized subject (across the AppleScript or IMAP backe
767
796
 
768
797
  Return an attachment's bytes as base64 (the read counterpart to inline-base64 send).
769
798
 
770
- | Parameter | Type | Required | Description |
771
- |-----------|------|----------|-------------|
772
- | `id` | string | Yes | Message ID (numeric or `imap:…`) |
773
- | `attachmentName` | string | Yes | Attachment filename (from `list-attachments`) |
799
+ | Parameter | Type | Required | Description |
800
+ | ---------------- | ------ | -------- | --------------------------------------------- |
801
+ | `id` | string | Yes | Message ID (numeric or `imap:…`) |
802
+ | `attachmentName` | string | Yes | Attachment filename (from `list-attachments`) |
774
803
 
775
804
  **Returns:** The attachment bytes, base64-encoded (also in `structuredContent.contentBase64`).
776
805
 
@@ -780,9 +809,9 @@ Return an attachment's bytes as base64 (the read counterpart to inline-base64 se
780
809
 
781
810
  Map `imap:` message IDs to their numeric Mail.app IDs, via each message's RFC 5322 `Message-ID` (the join key both backends share). Needed for the AppleScript reply/forward path; direct SMTP replies and forwards read `imap:` IDs without numeric conversion. Numeric IDs pass through unchanged.
782
811
 
783
- | Parameter | Type | Required | Description |
784
- |-----------|------|----------|-------------|
785
- | `ids` | string[] | Yes | 1–100 message IDs, each numeric or `imap:…` |
812
+ | Parameter | Type | Required | Description |
813
+ | --------- | -------- | -------- | ------------------------------------------- |
814
+ | `ids` | string[] | Yes | 1–100 message IDs, each numeric or `imap:…` |
786
815
 
787
816
  **Returns:** For each input ID, its `numericId` (or `null` when it can't be resolved) and the `messageId` used, plus `count` and `resolvedCount`. The lookup scopes to the message's account and checks its INBOX first, to avoid scanning a large All Mail/Archive mailbox.
788
817
 
@@ -794,15 +823,16 @@ Map `imap:` message IDs to their numeric Mail.app IDs, via each message's RFC 53
794
823
 
795
824
  Reply to an existing message.
796
825
 
797
- | Parameter | Type | Required | Description |
798
- |-----------|------|----------|-------------|
799
- | `id` | string | Yes | Message ID to reply to |
800
- | `body` | string | Yes | Reply body |
801
- | `replyAll` | boolean | No | Reply to all recipients (default: false) |
802
- | `send` | boolean | No | Send immediately (default: true, false = save as draft) |
803
- | `transport` | string | No | `smtp` or `applescript`; omitted prefers configured SMTP when sending. Drafts use AppleScript. |
826
+ | Parameter | Type | Required | Description |
827
+ | ----------- | ------- | -------- | ---------------------------------------------------------------------------------------------- |
828
+ | `id` | string | Yes | Message ID to reply to |
829
+ | `body` | string | Yes | Reply body |
830
+ | `replyAll` | boolean | No | Reply to all recipients (default: false) |
831
+ | `send` | boolean | No | Send immediately (default: true, false = save as draft) |
832
+ | `transport` | string | No | `smtp` or `applescript`; omitted prefers configured SMTP when sending. Drafts use AppleScript. |
804
833
 
805
834
  **Example - Reply to sender only:**
835
+
806
836
  ```json
807
837
  {
808
838
  "id": "12345",
@@ -811,6 +841,7 @@ Reply to an existing message.
811
841
  ```
812
842
 
813
843
  **Example - Reply all, save as draft:**
844
+
814
845
  ```json
815
846
  {
816
847
  "id": "12345",
@@ -836,13 +867,13 @@ Success includes `transport` and, for SMTP when returned by the server, the new
836
867
 
837
868
  Forward a message to new recipients.
838
869
 
839
- | Parameter | Type | Required | Description |
840
- |-----------|------|----------|-------------|
841
- | `id` | string | Yes | Message ID to forward |
842
- | `to` | string[] | Yes | Recipients to forward to |
843
- | `body` | string | No | Message to prepend |
844
- | `send` | boolean | No | Send immediately (default: true, false = save as draft) |
845
- | `transport` | string | No | `smtp` or `applescript`; omitted prefers configured SMTP when sending. Drafts use AppleScript. |
870
+ | Parameter | Type | Required | Description |
871
+ | ----------- | -------- | -------- | ---------------------------------------------------------------------------------------------- |
872
+ | `id` | string | Yes | Message ID to forward |
873
+ | `to` | string[] | Yes | Recipients to forward to |
874
+ | `body` | string | No | Message to prepend |
875
+ | `send` | boolean | No | Send immediately (default: true, false = save as draft) |
876
+ | `transport` | string | No | `smtp` or `applescript`; omitted prefers configured SMTP when sending. Drafts use AppleScript. |
846
877
 
847
878
  **Delivery:** uses the same source lookup, transport selection, 25 MiB source limit, account-identity check, and failure behavior as `reply-to-message`. A forward deliberately starts a new conversation, so it has no `In-Reply-To` or `References` headers. The existing plain-text forwarding behavior is unchanged: original attachments are not reattached. See [SMTP transport](#smtp-transport).
848
879
 
@@ -856,9 +887,9 @@ SMTP forwarding requires a readable plain-text original. HTML-only IMAP messages
856
887
 
857
888
  Change read status of a message.
858
889
 
859
- | Parameter | Type | Required | Description |
860
- |-----------|------|----------|-------------|
861
- | `id` | string | Yes | Message ID |
890
+ | Parameter | Type | Required | Description |
891
+ | --------- | ------ | -------- | ----------- |
892
+ | `id` | string | Yes | Message ID |
862
893
 
863
894
  ---
864
895
 
@@ -866,10 +897,10 @@ Change read status of a message.
866
897
 
867
898
  Flag or unflag a message. `flag-message` optionally takes a flag **color**; `unflag-message` removes the flag entirely (which also clears any color).
868
899
 
869
- | Parameter | Type | Required | Description |
870
- |-----------|------|----------|-------------|
871
- | `id` | string | Yes | Message ID |
872
- | `color` | string | No | (`flag-message` only) Flag color: `red`, `orange`, `yellow`, `green`, `blue`, `purple`, `gray` (`grey` accepted). Omit for Mail's default flag. |
900
+ | Parameter | Type | Required | Description |
901
+ | --------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
902
+ | `id` | string | Yes | Message ID |
903
+ | `color` | string | No | (`flag-message` only) Flag color: `red`, `orange`, `yellow`, `green`, `blue`, `purple`, `gray` (`grey` accepted). Omit for Mail's default flag. |
873
904
 
874
905
  **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.
875
906
 
@@ -881,9 +912,9 @@ To **read** a color, the IMAP read path returns `flagColorIndex` in `structuredC
881
912
 
882
913
  Delete a message (move to trash).
883
914
 
884
- | Parameter | Type | Required | Description |
885
- |-----------|------|----------|-------------|
886
- | `id` | string | Yes | Message ID |
915
+ | Parameter | Type | Required | Description |
916
+ | --------- | ------ | -------- | ----------- |
917
+ | `id` | string | Yes | Message ID |
887
918
 
888
919
  `structuredContent` carries `countDelta` — what the delete actually did to the
889
920
  source mailbox. See [Auditing destructive operations](#auditing-destructive-operations).
@@ -896,11 +927,11 @@ source mailbox. See [Auditing destructive operations](#auditing-destructive-oper
896
927
 
897
928
  Move a message to a different mailbox.
898
929
 
899
- | Parameter | Type | Required | Description |
900
- |-----------|------|----------|-------------|
901
- | `id` | string | Yes | Message ID |
902
- | `mailbox` | string | Yes | Destination mailbox — full path (`Work/Archive`) or a leaf name that is unique on the account |
903
- | `account` | string | No | Account containing mailbox |
930
+ | Parameter | Type | Required | Description |
931
+ | --------- | ------ | -------- | --------------------------------------------------------------------------------------------- |
932
+ | `id` | string | Yes | Message ID |
933
+ | `mailbox` | string | Yes | Destination mailbox — full path (`Work/Archive`) or a leaf name that is unique on the account |
934
+ | `account` | string | No | Account containing mailbox |
904
935
 
905
936
  A destination is matched first as a full path, then as a leaf name. If a leaf
906
937
  name matches **more than one** mailbox (e.g. `Archive` under both `Work` and
@@ -917,9 +948,9 @@ the full path. The same applies to `batch-move-messages`, `delete-mailbox` and
917
948
 
918
949
  List attachments on a message.
919
950
 
920
- | Parameter | Type | Required | Description |
921
- |-----------|------|----------|-------------|
922
- | `id` | string | Yes | Message ID |
951
+ | Parameter | Type | Required | Description |
952
+ | --------- | ------ | -------- | ----------- |
953
+ | `id` | string | Yes | Message ID |
923
954
 
924
955
  **Returns:** List of attachments with name, MIME type, and size.
925
956
 
@@ -934,11 +965,11 @@ of overwriting an existing file. AppleScript and MIME fallback paths stage the
934
965
  bytes privately, commit with an exclusive create, and leave the saved file
935
966
  owner-readable/writable (`0600`).
936
967
 
937
- | Parameter | Type | Required | Description |
938
- |-----------|------|----------|-------------|
939
- | `id` | string | Yes | Message ID |
940
- | `attachmentName` | string | Yes | Filename of the attachment |
941
- | `savePath` | string | Yes | Directory to save to |
968
+ | Parameter | Type | Required | Description |
969
+ | ---------------- | ------ | -------- | -------------------------- |
970
+ | `id` | string | Yes | Message ID |
971
+ | `attachmentName` | string | Yes | Filename of the attachment |
972
+ | `savePath` | string | Yes | Directory to save to |
942
973
 
943
974
  ---
944
975
 
@@ -975,11 +1006,11 @@ therefore a count of messages, not of list positions.
975
1006
 
976
1007
  #### `batch-delete-messages`
977
1008
 
978
- | Parameter | Type | Required | Description |
979
- |-----------|------|----------|-------------|
980
- | `ids` | string[] | Yes | Message IDs to delete (max 100) |
981
- | `sourceMailbox` | string | No | Mailbox the **numeric** ids were listed from — pins them to it. Ignored for `imap:` ids. |
982
- | `sourceAccount` | string | No | Account the numeric ids were listed from. Required when `sourceMailbox` is supplied; on its own it pins nothing. |
1009
+ | Parameter | Type | Required | Description |
1010
+ | --------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
1011
+ | `ids` | string[] | Yes | Message IDs to delete (max 100) |
1012
+ | `sourceMailbox` | string | No | Mailbox the **numeric** ids were listed from — pins them to it. Ignored for `imap:` ids. |
1013
+ | `sourceAccount` | string | No | Account the numeric ids were listed from. Required when `sourceMailbox` is supplied; on its own it pins nothing. |
983
1014
 
984
1015
  `structuredContent` carries `countDelta` — what the batch actually did to each
985
1016
  source mailbox. See [Auditing destructive operations](#auditing-destructive-operations).
@@ -988,33 +1019,33 @@ source mailbox. See [Auditing destructive operations](#auditing-destructive-oper
988
1019
 
989
1020
  #### `batch-move-messages`
990
1021
 
991
- | Parameter | Type | Required | Description |
992
- |-----------|------|----------|-------------|
993
- | `ids` | string[] | Yes | Message IDs to move (max 100) |
994
- | `mailbox` | string | Yes | Destination mailbox |
995
- | `account` | string | No | Account containing mailbox |
996
- | `sourceMailbox` | string | No | Mailbox the **numeric** ids were listed from — pins them to it. Ignored for `imap:` ids. |
997
- | `sourceAccount` | string | No | Account the numeric ids were listed from. Required when `sourceMailbox` is supplied; on its own it pins nothing. |
1022
+ | Parameter | Type | Required | Description |
1023
+ | --------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
1024
+ | `ids` | string[] | Yes | Message IDs to move (max 100) |
1025
+ | `mailbox` | string | Yes | Destination mailbox |
1026
+ | `account` | string | No | Account containing mailbox |
1027
+ | `sourceMailbox` | string | No | Mailbox the **numeric** ids were listed from — pins them to it. Ignored for `imap:` ids. |
1028
+ | `sourceAccount` | string | No | Account the numeric ids were listed from. Required when `sourceMailbox` is supplied; on its own it pins nothing. |
998
1029
 
999
1030
  `structuredContent` carries `countDelta` — what the batch actually did to each
1000
1031
  **source** mailbox. See [Auditing destructive operations](#auditing-destructive-operations).
1001
1032
 
1002
1033
  #### `batch-mark-as-read` / `batch-mark-as-unread`
1003
1034
 
1004
- | Parameter | Type | Required | Description |
1005
- |-----------|------|----------|-------------|
1006
- | `ids` | string[] | Yes | Message IDs (max 100) |
1007
- | `sourceMailbox` | string | No | Mailbox the **numeric** ids were listed from — pins them to it. Ignored for `imap:` ids. |
1008
- | `sourceAccount` | string | No | Account the numeric ids were listed from. Required when `sourceMailbox` is supplied; on its own it pins nothing. |
1035
+ | Parameter | Type | Required | Description |
1036
+ | --------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
1037
+ | `ids` | string[] | Yes | Message IDs (max 100) |
1038
+ | `sourceMailbox` | string | No | Mailbox the **numeric** ids were listed from — pins them to it. Ignored for `imap:` ids. |
1039
+ | `sourceAccount` | string | No | Account the numeric ids were listed from. Required when `sourceMailbox` is supplied; on its own it pins nothing. |
1009
1040
 
1010
1041
  #### `batch-flag-messages` / `batch-unflag-messages`
1011
1042
 
1012
- | Parameter | Type | Required | Description |
1013
- |-----------|------|----------|-------------|
1014
- | `ids` | string[] | Yes | Message IDs (max 100) |
1015
- | `color` | string | No | (`batch-flag-messages` only) Flag color — see [`flag-message`](#flag-message--unflag-message). Applied on both routes, so a mixed batch of numeric and `imap:` ids all end up colored. |
1016
- | `sourceMailbox` | string | No | Mailbox the **numeric** ids were listed from — pins them to it. Ignored for `imap:` ids. |
1017
- | `sourceAccount` | string | No | Account the numeric ids were listed from. Required when `sourceMailbox` is supplied; on its own it pins nothing. |
1043
+ | Parameter | Type | Required | Description |
1044
+ | --------------- | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1045
+ | `ids` | string[] | Yes | Message IDs (max 100) |
1046
+ | `color` | string | No | (`batch-flag-messages` only) Flag color — see [`flag-message`](#flag-message--unflag-message). Applied on both routes, so a mixed batch of numeric and `imap:` ids all end up colored. |
1047
+ | `sourceMailbox` | string | No | Mailbox the **numeric** ids were listed from — pins them to it. Ignored for `imap:` ids. |
1048
+ | `sourceAccount` | string | No | Account the numeric ids were listed from. Required when `sourceMailbox` is supplied; on its own it pins nothing. |
1018
1049
 
1019
1050
  ---
1020
1051
 
@@ -1024,9 +1055,9 @@ source mailbox. See [Auditing destructive operations](#auditing-destructive-oper
1024
1055
 
1025
1056
  List all mailboxes for an account.
1026
1057
 
1027
- | Parameter | Type | Required | Description |
1028
- |-----------|------|----------|-------------|
1029
- | `account` | string | No | Account to list from, or `"On My Mac"` for the local store |
1058
+ | Parameter | Type | Required | Description |
1059
+ | --------- | ------ | -------- | ---------------------------------------------------------- |
1060
+ | `account` | string | No | Account to list from, or `"On My Mac"` for the local store |
1030
1061
 
1031
1062
  **Returns:** List of mailbox **paths** (account-relative, e.g. `Archive/Inbox` for
1032
1063
  a nested mailbox — a top-level `Inbox` stays `Inbox`) with message and unread
@@ -1066,10 +1097,10 @@ reported as ambiguous rather than silently resolving to the account copy.
1066
1097
 
1067
1098
  Get unread message count.
1068
1099
 
1069
- | Parameter | Type | Required | Description |
1070
- |-----------|------|----------|-------------|
1071
- | `mailbox` | string | No | Mailbox to check (omit for **INBOX**) |
1072
- | `account` | string | No | Account to check (omit to sum each account's INBOX) |
1100
+ | Parameter | Type | Required | Description |
1101
+ | --------- | ------ | -------- | --------------------------------------------------- |
1102
+ | `mailbox` | string | No | Mailbox to check (omit for **INBOX**) |
1103
+ | `account` | string | No | Account to check (omit to sum each account's INBOX) |
1073
1104
 
1074
1105
  **Returns:** The unread count for the requested scope.
1075
1106
 
@@ -1081,10 +1112,10 @@ Get unread message count.
1081
1112
 
1082
1113
  Create a new mailbox.
1083
1114
 
1084
- | Parameter | Type | Required | Description |
1085
- |-----------|------|----------|-------------|
1086
- | `name` | string | Yes | Mailbox name |
1087
- | `account` | string | No | Account to create in |
1115
+ | Parameter | Type | Required | Description |
1116
+ | --------- | ------ | -------- | -------------------- |
1117
+ | `name` | string | Yes | Mailbox name |
1118
+ | `account` | string | No | Account to create in |
1088
1119
 
1089
1120
  ---
1090
1121
 
@@ -1092,10 +1123,10 @@ Create a new mailbox.
1092
1123
 
1093
1124
  Delete a mailbox.
1094
1125
 
1095
- | Parameter | Type | Required | Description |
1096
- |-----------|------|----------|-------------|
1097
- | `name` | string | Yes | Mailbox name |
1098
- | `account` | string | No | Account containing mailbox |
1126
+ | Parameter | Type | Required | Description |
1127
+ | --------- | ------ | -------- | -------------------------- |
1128
+ | `name` | string | Yes | Mailbox name |
1129
+ | `account` | string | No | Account containing mailbox |
1099
1130
 
1100
1131
  **⚠️ Safety:** Destructive — deletes the mailbox and its contents. Requires explicit user confirmation; list mailboxes first to confirm the name.
1101
1132
 
@@ -1105,11 +1136,11 @@ Delete a mailbox.
1105
1136
 
1106
1137
  Rename a mailbox (creates new, moves messages, deletes old).
1107
1138
 
1108
- | Parameter | Type | Required | Description |
1109
- |-----------|------|----------|-------------|
1110
- | `oldName` | string | Yes | Current mailbox name |
1111
- | `newName` | string | Yes | New mailbox name |
1112
- | `account` | string | No | Account containing mailbox |
1139
+ | Parameter | Type | Required | Description |
1140
+ | --------- | ------ | -------- | -------------------------- |
1141
+ | `oldName` | string | Yes | Current mailbox name |
1142
+ | `newName` | string | Yes | New mailbox name |
1143
+ | `account` | string | No | Account containing mailbox |
1113
1144
 
1114
1145
  ---
1115
1146
 
@@ -1135,12 +1166,12 @@ List existing smart mailboxes.
1135
1166
 
1136
1167
  Create a smart mailbox with a simple contains rule.
1137
1168
 
1138
- | Parameter | Type | Required | Description |
1139
- |-----------|------|----------|-------------|
1140
- | `name` | string | Yes | Name for the smart mailbox |
1141
- | `fromContains` | string | No | Match if From contains this |
1142
- | `subjectContains` | string | No | Match if Subject contains this |
1143
- | `bodyContains` | string | No | Match if Body contains this |
1169
+ | Parameter | Type | Required | Description |
1170
+ | ----------------- | ------ | -------- | ------------------------------ |
1171
+ | `name` | string | Yes | Name for the smart mailbox |
1172
+ | `fromContains` | string | No | Match if From contains this |
1173
+ | `subjectContains` | string | No | Match if Subject contains this |
1174
+ | `bodyContains` | string | No | Match if Body contains this |
1144
1175
 
1145
1176
  Provide at least one of the three `*Contains` fields.
1146
1177
 
@@ -1152,9 +1183,9 @@ Provide at least one of the three `*Contains` fields.
1152
1183
 
1153
1184
  Delete a smart mailbox by name.
1154
1185
 
1155
- | Parameter | Type | Required | Description |
1156
- |-----------|------|----------|-------------|
1157
- | `name` | string | Yes | Smart mailbox name |
1186
+ | Parameter | Type | Required | Description |
1187
+ | --------- | ------ | -------- | ------------------ |
1188
+ | `name` | string | Yes | Smart mailbox name |
1158
1189
 
1159
1190
  **⚠️ Safety:** destructive — removes the smart mailbox from `SyncedSmartMailboxes.plist` (backed up + atomic; every other smart mailbox is preserved). Not undoable in-app. Confirm the exact name with `list-smart-mailboxes` first, and quit Mail first for reliable results.
1160
1191
 
@@ -1164,11 +1195,11 @@ Delete a smart mailbox by name.
1164
1195
 
1165
1196
  High-level tool: scan recent messages in your INBOXes, detect likely newsletters (volume + signals like List-Unsubscribe, noreply, repetitive subjects), and create smart mailboxes for them (names prefixed "NL: ...").
1166
1197
 
1167
- | Parameter | Type | Required | Description |
1168
- |-----------|------|----------|-------------|
1169
- | `dryRun` | boolean | No | Default true — only propose, do not create |
1170
- | `minCount` | number | No | Min messages from a sender (default 3) |
1171
- | `days` | number | No | Lookback window in days (default 90) |
1198
+ | Parameter | Type | Required | Description |
1199
+ | ---------- | ------- | -------- | ------------------------------------------ |
1200
+ | `dryRun` | boolean | No | Default true — only propose, do not create |
1201
+ | `minCount` | number | No | Min messages from a sender (default 3) |
1202
+ | `days` | number | No | Lookback window in days (default 90) |
1172
1203
 
1173
1204
  Defaults to a **safe dry run** that only proposes. Pass `dryRun: false` to actually create the smart mailboxes for newsletters cluttering your Inbox.
1174
1205
 
@@ -1204,9 +1235,9 @@ List all mail rules.
1204
1235
 
1205
1236
  Enable or disable a mail rule.
1206
1237
 
1207
- | Parameter | Type | Required | Description |
1208
- |-----------|------|----------|-------------|
1209
- | `name` | string | Yes | Rule name |
1238
+ | Parameter | Type | Required | Description |
1239
+ | --------- | ------ | -------- | ----------- |
1240
+ | `name` | string | Yes | Rule name |
1210
1241
 
1211
1242
  ---
1212
1243
 
@@ -1214,13 +1245,13 @@ Enable or disable a mail rule.
1214
1245
 
1215
1246
  Create a Mail rule with one or more conditions and actions.
1216
1247
 
1217
- | Parameter | Type | Required | Description |
1218
- |-----------|------|----------|-------------|
1219
- | `name` | string | Yes | Rule name (must be unique) |
1220
- | `conditions` | object[] | Yes | One or more `{field, operator, value}` (see below) |
1221
- | `actions` | object | Yes | At least one of `markRead`, `markFlagged`, `delete`, `moveTo` |
1222
- | `matchAll` | boolean | No | `true` (default) = all conditions must match; `false` = any |
1223
- | `enabled` | boolean | No | Whether the rule is enabled on creation (default `false`) |
1248
+ | Parameter | Type | Required | Description |
1249
+ | ------------ | -------- | -------- | ------------------------------------------------------------- |
1250
+ | `name` | string | Yes | Rule name (must be unique) |
1251
+ | `conditions` | object[] | Yes | One or more `{field, operator, value}` (see below) |
1252
+ | `actions` | object | Yes | At least one of `markRead`, `markFlagged`, `delete`, `moveTo` |
1253
+ | `matchAll` | boolean | No | `true` (default) = all conditions must match; `false` = any |
1254
+ | `enabled` | boolean | No | Whether the rule is enabled on creation (default `false`) |
1224
1255
 
1225
1256
  Each condition is `{ field, operator, value }` where `field` is one of `from`, `to`, `cc`, `subject`, `content` and `operator` is one of `contains`, `notContains`, `equals`, `beginsWith`, `endsWith`. Actions: `markRead` / `markFlagged` / `delete` (booleans), `moveTo` (mailbox name) with optional `moveToAccount`.
1226
1257
 
@@ -1230,6 +1261,7 @@ then call `enable-rule` explicitly when the rule is approved. Set
1230
1261
  `enabled: true` only when immediate activation is deliberate.
1231
1262
 
1232
1263
  **Example:**
1264
+
1233
1265
  ```json
1234
1266
  {
1235
1267
  "name": "Newsletters",
@@ -1244,9 +1276,9 @@ then call `enable-rule` explicitly when the rule is approved. Set
1244
1276
 
1245
1277
  Delete a mail rule by name.
1246
1278
 
1247
- | Parameter | Type | Required | Description |
1248
- |-----------|------|----------|-------------|
1249
- | `name` | string | Yes | Rule name |
1279
+ | Parameter | Type | Required | Description |
1280
+ | --------- | ------ | -------- | ----------- |
1281
+ | `name` | string | Yes | Rule name |
1250
1282
 
1251
1283
  **⚠️ Safety:** Destructive. Requires explicit user confirmation; list rules first to confirm the name.
1252
1284
 
@@ -1260,9 +1292,9 @@ Search the macOS Contacts database by name, organization, nickname, or email sub
1260
1292
 
1261
1293
  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.
1262
1294
 
1263
- | Parameter | Type | Required | Description |
1264
- |-----------|------|----------|-------------|
1265
- | `query` | string | Yes | Substring matched against full name, organization, nickname, or any email address |
1295
+ | Parameter | Type | Required | Description |
1296
+ | --------- | ------ | -------- | --------------------------------------------------------------------------------- |
1297
+ | `query` | string | Yes | Substring matched against full name, organization, nickname, or any email address |
1266
1298
 
1267
1299
  **Returns:** List of contacts with name, email addresses, and phone numbers. Results are **not** truncated — a broad query returns every match.
1268
1300
 
@@ -1276,14 +1308,14 @@ Email templates are **persisted to disk** so they survive server restarts, store
1276
1308
 
1277
1309
  Save or update an email template.
1278
1310
 
1279
- | Parameter | Type | Required | Description |
1280
- |-----------|------|----------|-------------|
1281
- | `name` | string | Yes | Template name |
1282
- | `subject` | string | Yes | Default subject line |
1283
- | `body` | string | Yes | Template body |
1284
- | `to` | string[] | No | Default recipients |
1285
- | `cc` | string[] | No | Default CC recipients |
1286
- | `id` | string | No | Template ID (for updating) |
1311
+ | Parameter | Type | Required | Description |
1312
+ | --------- | -------- | -------- | -------------------------- |
1313
+ | `name` | string | Yes | Template name |
1314
+ | `subject` | string | Yes | Default subject line |
1315
+ | `body` | string | Yes | Template body |
1316
+ | `to` | string[] | No | Default recipients |
1317
+ | `cc` | string[] | No | Default CC recipients |
1318
+ | `id` | string | No | Template ID (for updating) |
1287
1319
 
1288
1320
  ---
1289
1321
 
@@ -1299,9 +1331,9 @@ List all saved templates.
1299
1331
 
1300
1332
  Get a template by ID.
1301
1333
 
1302
- | Parameter | Type | Required | Description |
1303
- |-----------|------|----------|-------------|
1304
- | `id` | string | Yes | Template ID |
1334
+ | Parameter | Type | Required | Description |
1335
+ | --------- | ------ | -------- | ----------- |
1336
+ | `id` | string | Yes | Template ID |
1305
1337
 
1306
1338
  ---
1307
1339
 
@@ -1309,9 +1341,9 @@ Get a template by ID.
1309
1341
 
1310
1342
  Delete a template.
1311
1343
 
1312
- | Parameter | Type | Required | Description |
1313
- |-----------|------|----------|-------------|
1314
- | `id` | string | Yes | Template ID |
1344
+ | Parameter | Type | Required | Description |
1345
+ | --------- | ------ | -------- | ----------- |
1346
+ | `id` | string | Yes | Template ID |
1315
1347
 
1316
1348
  **⚠️ Safety:** Destructive — removes the template from the on-disk store. Requires explicit user confirmation; list templates first to confirm the id.
1317
1349
 
@@ -1321,13 +1353,13 @@ Delete a template.
1321
1353
 
1322
1354
  Create a draft from a template, with optional overrides.
1323
1355
 
1324
- | Parameter | Type | Required | Description |
1325
- |-----------|------|----------|-------------|
1326
- | `id` | string | Yes | Template ID |
1327
- | `to` | string[] | No | Override recipients |
1328
- | `cc` | string[] | No | Override CC |
1329
- | `subject` | string | No | Override subject |
1330
- | `body` | string | No | Override body |
1356
+ | Parameter | Type | Required | Description |
1357
+ | --------- | -------- | -------- | ------------------- |
1358
+ | `id` | string | Yes | Template ID |
1359
+ | `to` | string[] | No | Override recipients |
1360
+ | `cc` | string[] | No | Override CC |
1361
+ | `subject` | string | No | Override subject |
1362
+ | `body` | string | No | Override body |
1331
1363
 
1332
1364
  ---
1333
1365
 
@@ -1357,9 +1389,9 @@ Run a full setup diagnostic: Mail.app automation permission, account state (flag
1357
1389
 
1358
1390
  Get mail statistics.
1359
1391
 
1360
- | Parameter | Type | Required | Description |
1361
- |-----------|------|----------|-------------|
1362
- | `account` | string | No | Limit to one account (uses fast IMAP `STATUS` when that account is IMAP-configured). Omit to merge across all accounts. |
1392
+ | Parameter | Type | Required | Description |
1393
+ | --------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------- |
1394
+ | `account` | string | No | Limit to one account (uses fast IMAP `STATUS` when that account is IMAP-configured). Omit to merge across all accounts. |
1363
1395
 
1364
1396
  **Returns:** Total and per-account message/unread counts, plus recently received stats (24h, 7d, 30d). The scoped IMAP path also returns a `perMailbox` breakdown.
1365
1397
 
@@ -1442,20 +1474,20 @@ returns the comparison in `structuredContent`:
1442
1474
 
1443
1475
  `status` is deliberately not a pass/fail flag:
1444
1476
 
1445
- | `status` | Meaning | Warns? |
1446
- |----------|---------|--------|
1447
- | `match` | Exactly as many messages left the mailbox as the operation acted on. | No |
1448
- | `over` | **More** left than were operated on. Messages are unaccounted for. | **Yes** |
1449
- | `unknown` | No comparison this server is willing to assert. `unknownReason` says which of four situations produced it. | No |
1477
+ | `status` | Meaning | Warns? |
1478
+ | --------- | ---------------------------------------------------------------------------------------------------------- | ------- |
1479
+ | `match` | Exactly as many messages left the mailbox as the operation acted on. | No |
1480
+ | `over` | **More** left than were operated on. Messages are unaccounted for. | **Yes** |
1481
+ | `unknown` | No comparison this server is willing to assert. `unknownReason` says which of four situations produced it. | No |
1450
1482
 
1451
- `unknownReason` distinguishes four cases that are *not* interchangeable:
1483
+ `unknownReason` distinguishes four cases that are _not_ interchangeable:
1452
1484
 
1453
- | `unknownReason` | Meaning |
1454
- |-----------------|---------|
1455
- | `count-unreadable` | Mail would not report a count at all (`before`/`after` null). |
1456
- | `no-expectation` | No expectation is predictable, so no comparison exists — a move whose destination **is** the source mailbox. |
1485
+ | `unknownReason` | Meaning |
1486
+ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1487
+ | `count-unreadable` | Mail would not report a count at all (`before`/`after` null). |
1488
+ | `no-expectation` | No expectation is predictable, so no comparison exists — a move whose destination **is** the source mailbox. |
1457
1489
  | `count-did-not-move` | The count did not move. On a store that flags deletions instead of removing them this is the **ordinary, correct** reading for an operation that fully succeeded. |
1458
- | `count-partial` | The count moved, but by less than the operation accounted for. A flag-only store cannot produce this, which is why it is worth telling apart. |
1490
+ | `count-partial` | The count moved, but by less than the operation accounted for. A flag-only store cannot produce this, which is why it is worth telling apart. |
1459
1491
 
1460
1492
  > **`under` was removed in 2.11.0.** It used to mean "fewer left than expected"
1461
1493
  > and was documented as routine. Field evidence retired it — see
@@ -1478,8 +1510,8 @@ trusted:
1478
1510
 
1479
1511
  **Concurrent departure is the benign cause to rule out first**, and the warning
1480
1512
  text says so. What the asymmetry argument actually buys is the other half:
1481
- concurrent *arrivals* cannot produce `over`, because a message arriving
1482
- mid-operation *raises* the after-count and biases the reading short.
1513
+ concurrent _arrivals_ cannot produce `over`, because a message arriving
1514
+ mid-operation _raises_ the after-count and biases the reading short.
1483
1515
  That is why `over` is the interesting direction — a strong signal, not a proof.
1484
1516
 
1485
1517
  Setting `APPLE_MAIL_MCP_AUDIT_LOG` is what settles which one you have: the
@@ -1496,7 +1528,7 @@ On iCloud, @scottstern0325 ran the check that settled this: for two batches
1496
1528
  reporting `observed: 0`, the messages were located **in Trash**, matched by
1497
1529
  `date received` + sender against the audit log's pre-image. The deletes had
1498
1530
  happened. Mail's count had not caught up. Across four readings — 0 of 4, 0 of 1
1499
- (a *single-id* delete), 15 of 16, and 14 of 15 — the shortfall bore no relation
1531
+ (a _single-id_ delete), 15 of 16, and 14 of 15 — the shortfall bore no relation
1500
1532
  to batch size, which is what a lagging count looks like and not what a
1501
1533
  store-behaviour rule looks like.
1502
1534
 
@@ -1513,14 +1545,14 @@ Two things follow:
1513
1545
  — a warning that fires on every ordinary Gmail delete would be ignored exactly
1514
1546
  when it matters.
1515
1547
  - **To confirm where messages went, match them at the destination by `date
1516
- received` plus sender — not by the numeric ids you passed.** Ids are renumbered
1548
+ received` plus sender — not by the numeric ids you passed.** Ids are renumbered
1517
1549
  by the move and do not survive it.
1518
1550
 
1519
1551
  **Removed in 2.11.0: the "reported success with no observed effect" warning.**
1520
1552
  Shipped in 2.10.30, it fired when the count was flat, the snapshot read cleanly
1521
1553
  and nothing had disappeared. Its premise was that the snapshot corroborated the
1522
1554
  count — but both are read back-to-back in the same script, and the record that
1523
- prompted it turns out to have had *both* instruments stale at once. It therefore
1555
+ prompted it turns out to have had _both_ instruments stale at once. It therefore
1524
1556
  fired on stores that had done exactly what they were asked. It is gone rather
1525
1557
  than narrowed; `over` is the only surviving assertion.
1526
1558
 
@@ -1546,6 +1578,7 @@ Three more honesty rules:
1546
1578
  clean — but it does not flag it either. The collateral diff still names anything
1547
1579
  that vanished, so **enable `APPLE_MAIL_MCP_AUDIT_LOG` if you need coverage for
1548
1580
  same-mailbox moves.**
1581
+
1549
1582
  - A **repeated id is one message**. The batch tools operate on each distinct id
1550
1583
  once and return one result per distinct id, so `success` counts messages rather
1551
1584
  than list positions — and `expected` stays comparable with the mailbox instead
@@ -1580,10 +1613,10 @@ Single-message tools carry a post-condition check instead. `delete-message` and
1580
1613
  }
1581
1614
  ```
1582
1615
 
1583
- | `verdict` | Meaning |
1584
- |---|---|
1585
- | `verified` | The effect was **observed** — either the server's UIDPLUS `COPYUID` named the message's new UID in the destination, or the UID is no longer in the source mailbox. |
1586
- | `unverified` | The server **accepted** the command and nothing could confirm the effect. Populates `why`. |
1616
+ | `verdict` | Meaning |
1617
+ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1618
+ | `verified` | The effect was **observed** — either the server's UIDPLUS `COPYUID` named the message's new UID in the destination, or the UID is no longer in the source mailbox. |
1619
+ | `unverified` | The server **accepted** the command and nothing could confirm the effect. Populates `why`. |
1587
1620
 
1588
1621
  `unverified` is **not a failure** and must not be rendered as one — it means
1589
1622
  "accepted, no observation either way". It is reported rather than hidden because
@@ -1594,12 +1627,12 @@ store can legitimately keep a message visible in an all-mail view after a move.
1594
1627
 
1595
1628
  ### `APPLE_MAIL_MCP_AUDIT_LOG` — opt-in forensic log
1596
1629
 
1597
- | Variable | Default | Description |
1598
- |----------|---------|-------------|
1599
- | `APPLE_MAIL_MCP_AUDIT_LOG` | *(off)* | Absolute path to an NDJSON file. Setting it enables the audit log **and** the collateral diff below |
1600
- | `APPLE_MAIL_MCP_AUDIT_SUBJECTS` | `0` | Set `1` to also record message **subjects**. Separate, deliberate second opt-in — see Privacy |
1601
- | `APPLE_MAIL_MCP_AUDIT_SNAPSHOT_MAX` | `2000` | Skip the collateral snapshot for mailboxes larger than this many messages. `0` disables the snapshot entirely |
1602
- | `APPLE_MAIL_MCP_AUDIT_SNAPSHOT_CHUNK` | `250` | How many messages the collateral snapshot reads from Mail per request. Lower it if Mail declines slices on a very large mailbox |
1630
+ | Variable | Default | Description |
1631
+ | ------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------- |
1632
+ | `APPLE_MAIL_MCP_AUDIT_LOG` | _(off)_ | Absolute path to an NDJSON file. Setting it enables the audit log **and** the collateral diff below |
1633
+ | `APPLE_MAIL_MCP_AUDIT_SUBJECTS` | `0` | Set `1` to also record message **subjects**. Separate, deliberate second opt-in — see Privacy |
1634
+ | `APPLE_MAIL_MCP_AUDIT_SNAPSHOT_MAX` | `2000` | Skip the collateral snapshot for mailboxes larger than this many messages. `0` disables the snapshot entirely |
1635
+ | `APPLE_MAIL_MCP_AUDIT_SNAPSHOT_CHUNK` | `250` | How many messages the collateral snapshot reads from Mail per request. Lower it if Mail declines slices on a very large mailbox |
1603
1636
 
1604
1637
  When set, each destructive operation appends **one JSON object per line**
1605
1638
  containing: timestamp, tool name, server version, the arguments it was called
@@ -1681,12 +1714,12 @@ When a slice still will not read, the snapshot is reported as `partial` and it
1681
1714
  Each half of the diff is withheld when the snapshot that could **refute** it has
1682
1715
  a hole, because a wrong name here is worse than a missing one:
1683
1716
 
1684
- | Hole in | `disappeared` / `unrequested` | `appeared` |
1685
- |---------|-------------------------------|------------|
1686
- | neither (`"ok"`) | reported | reported |
1687
- | `before` | reported (an undercount — a message never read before cannot be missed after) | withheld |
1688
- | `after` | **withheld** — a message absent from a partial `after` may merely be unread, and naming it would present an innocent message as evidence of data loss | reported |
1689
- | both | withheld | withheld |
1717
+ | Hole in | `disappeared` / `unrequested` | `appeared` |
1718
+ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
1719
+ | neither (`"ok"`) | reported | reported |
1720
+ | `before` | reported (an undercount — a message never read before cannot be missed after) | withheld |
1721
+ | `after` | **withheld** — a message absent from a partial `after` may merely be unread, and naming it would present an innocent message as evidence of data loss | reported |
1722
+ | both | withheld | withheld |
1690
1723
 
1691
1724
  **An absent field means "not computable", never "empty".** Check
1692
1725
  `"snapshot": "ok"` before reading `disappeared` as a clean bill of health.
@@ -1702,7 +1735,7 @@ a hole, because a wrong name here is worse than a missing one:
1702
1735
  ### Privacy, and what the file costs you
1703
1736
 
1704
1737
  - **Default:** identifying metadata only — Message-ID, date, mailbox, account,
1705
- numeric id. Enough to say *which* message, nothing about what it says.
1738
+ numeric id. Enough to say _which_ message, nothing about what it says.
1706
1739
  - **Subjects are behind their own opt-in** (`APPLE_MAIL_MCP_AUDIT_SUBJECTS=1`)
1707
1740
  because a subject line is frequently the entire sensitive payload, and it is
1708
1741
  not needed to diagnose #155.
@@ -1817,6 +1850,7 @@ The repo ships prebuilt, dependency-free `build/index.js` and `build/cli.js` bun
1817
1850
  > You can also install straight from GitHub with `npm install -g github:sweetrb/apple-mail-mcp`, but that builds from source (requires pnpm) — prefer the registry package above.
1818
1851
 
1819
1852
  If installed from source, use this configuration:
1853
+
1820
1854
  ```json
1821
1855
  {
1822
1856
  "mcpServers": {
@@ -1838,11 +1872,11 @@ The entrypoint is written as:
1838
1872
  "args": ["${CLAUDE_PROJECT_DIR:-.}/build/index.js"]
1839
1873
  ```
1840
1874
 
1841
- `CLAUDE_PROJECT_DIR` is the variable Claude Code injects into a project/user-scoped server's environment, and it resolves to the repo root. **You must launch `claude` from inside the repo** for this to work — the bare `.` fallback is only a last resort and is *not* reliable, because it resolves against the launching process's working directory, not the repo.
1875
+ `CLAUDE_PROJECT_DIR` is the variable Claude Code injects into a project/user-scoped server's environment, and it resolves to the repo root. **You must launch `claude` from inside the repo** for this to work — the bare `.` fallback is only a last resort and is _not_ reliable, because it resolves against the launching process's working directory, not the repo.
1842
1876
 
1843
- > **Why not `${CLAUDE_PLUGIN_ROOT}`?** `CLAUDE_PLUGIN_ROOT` is set **only** for marketplace plugin installs, never for a project-scope clone, so it can't drive the clone workflow. Conversely, a plugin install can't use `CLAUDE_PROJECT_DIR` (in a plugin, that points at the *user's* project, not the plugin's own directory). Claude Code does **not** support nested defaults like `${CLAUDE_PLUGIN_ROOT:-${CLAUDE_PROJECT_DIR:-.}}`, so a single entrypoint string cannot serve both contexts. The two distribution paths are therefore decoupled: the **plugin** carries its own MCP config in `.claude-plugin/plugin.json` (using `${CLAUDE_PLUGIN_ROOT}`), while the root `.mcp.json` is dedicated to the **clone** workflow (using `${CLAUDE_PROJECT_DIR:-.}`). Because `plugin.json` declares its own `mcpServers`, the plugin does not also auto-load the root `.mcp.json`, so there is no double-registration.
1877
+ > **Why not `${CLAUDE_PLUGIN_ROOT}`?** `CLAUDE_PLUGIN_ROOT` is set **only** for marketplace plugin installs, never for a project-scope clone, so it can't drive the clone workflow. Conversely, a plugin install can't use `CLAUDE_PROJECT_DIR` (in a plugin, that points at the _user's_ project, not the plugin's own directory). Claude Code does **not** support nested defaults like `${CLAUDE_PLUGIN_ROOT:-${CLAUDE_PROJECT_DIR:-.}}`, so a single entrypoint string cannot serve both contexts. The two distribution paths are therefore decoupled: the **plugin** carries its own MCP config in `.claude-plugin/plugin.json` (using `${CLAUDE_PLUGIN_ROOT}`), while the root `.mcp.json` is dedicated to the **clone** workflow (using `${CLAUDE_PROJECT_DIR:-.}`). Because `plugin.json` declares its own `mcpServers`, the plugin does not also auto-load the root `.mcp.json`, so there is no double-registration.
1844
1878
 
1845
- > **Heads-up on scope precedence:** project-scope (`.mcp.json`) outranks user-scope. If you *also* have an `apple-mail` entry registered at user scope (e.g. an absolute path in `~/.claude.json`), the project-scope entry wins and the user-scope one is ignored entirely. Pick one — for local development on this repo, the project-scope `.mcp.json` is the intended source. To pin a specific local build instead, register it at **local** scope (`claude mcp add apple-mail -s local -- node /abs/path/build/index.js`), which outranks project scope.
1879
+ > **Heads-up on scope precedence:** project-scope (`.mcp.json`) outranks user-scope. If you _also_ have an `apple-mail` entry registered at user scope (e.g. an absolute path in `~/.claude.json`), the project-scope entry wins and the user-scope one is ignored entirely. Pick one — for local development on this repo, the project-scope `.mcp.json` is the intended source. To pin a specific local build instead, register it at **local** scope (`claude mcp add apple-mail -s local -- node /abs/path/build/index.js`), which outranks project scope.
1846
1880
 
1847
1881
  ---
1848
1882
 
@@ -1867,20 +1901,20 @@ The entrypoint is written as:
1867
1901
 
1868
1902
  ## Known Limitations
1869
1903
 
1870
- | Limitation | Reason |
1871
- |------------|--------|
1872
- | macOS only | Apple Mail and AppleScript are macOS-specific |
1873
- | 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) |
1874
- | Attachment read path restrictions | Outbound file attachments must use full absolute paths inside the default home-directory, `/Volumes`, or temporary roots, except hidden files and protected credential/configuration locations. Set `APPLE_MAIL_MCP_ATTACHMENT_READ_ROOTS` to add an explicit absolute root for another deliberate location; symlink escapes are rejected. |
1875
- | 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 |
1876
- | 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)) |
1877
- | 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)) |
1878
- | Message ID format | Message IDs must be numeric (AppleScript ids) or `imap:…` tokens from the IMAP read path (validated by schema) |
1879
- | Batch size cap | Batch operations are limited to 100 messages per request |
1880
- | Date filter format | Date filters must be valid parseable dates (e.g., "January 1, 2026" or "2026-03-15"); bare numbers or non-date strings are rejected |
1881
- | Attachment save path restrictions | `save-attachment` only allows saving to home directory, `/tmp`, `/private/tmp`, and `/Volumes`; path traversal is blocked |
1882
- | Attachment count limit | `send-email` and `create-draft` accept a maximum of 20 file attachments |
1883
- | IMAP attachment fetch size | `fetch-attachment` / `save-attachment` over IMAP refuse a part larger than 25 MiB — rejected before download when the server declares the size, and the stream is cut off at the limit when it does not |
1904
+ | Limitation | Reason |
1905
+ | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1906
+ | macOS only | Apple Mail and AppleScript are macOS-specific |
1907
+ | 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) |
1908
+ | Attachment read path restrictions | Outbound file attachments must use full absolute paths inside the default home-directory, `/Volumes`, or temporary roots, except hidden files and protected credential/configuration locations. Set `APPLE_MAIL_MCP_ATTACHMENT_READ_ROOTS` to add an explicit absolute root for another deliberate location; symlink escapes are rejected. |
1909
+ | 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 |
1910
+ | 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)) |
1911
+ | 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)) |
1912
+ | Message ID format | Message IDs must be numeric (AppleScript ids) or `imap:…` tokens from the IMAP read path (validated by schema) |
1913
+ | Batch size cap | Batch operations are limited to 100 messages per request |
1914
+ | Date filter format | Date filters must be valid parseable dates (e.g., "January 1, 2026" or "2026-03-15"); bare numbers or non-date strings are rejected |
1915
+ | Attachment save path restrictions | `save-attachment` only allows saving to home directory, `/tmp`, `/private/tmp`, and `/Volumes`; path traversal is blocked |
1916
+ | Attachment count limit | `send-email` and `create-draft` accept a maximum of 20 file attachments |
1917
+ | IMAP attachment fetch size | `fetch-attachment` / `save-attachment` over IMAP refuse a part larger than 25 MiB — rejected before download when the server declares the size, and the stream is cut off at the limit when it does not |
1884
1918
 
1885
1919
  ### Mail.app `<blockquote>` wrapping on macOS 15+ (workaround in v1.6.0)
1886
1920
 
@@ -1946,11 +1980,13 @@ In a JSON string literal, `\\` — two characters — denotes **one** literal ba
1946
1980
  ## Troubleshooting
1947
1981
 
1948
1982
  ### "Mail.app not responding"
1983
+
1949
1984
  - Ensure Mail.app is not frozen
1950
1985
  - Try opening Mail.app manually
1951
1986
  - Restart the MCP server
1952
1987
 
1953
1988
  ### "Permission denied"
1989
+
1954
1990
  - macOS needs automation permission
1955
1991
  - Go to System Settings > Privacy & Security > Automation
1956
1992
  - Ensure your terminal/Claude has permission to control Mail
@@ -1958,17 +1994,19 @@ In a JSON string literal, `\\` — two characters — denotes **one** literal ba
1958
1994
  language — `Not authorised to send Apple events to Mail. (-1743)` on en-GB/en-AU/en-IE,
1959
1995
  and fully translated on fr/de/es. Before **v2.17.10** the server didn't recognise
1960
1996
  those spellings, so `health-check`/`doctor` reported `permissions: ok` and then blamed
1961
- missing accounts (*"No Mail accounts found. Set up an account in Mail.app first."*).
1997
+ missing accounts (_"No Mail accounts found. Set up an account in Mail.app first."_).
1962
1998
  If you see that on a Mac whose Mail accounts are already configured, upgrade and
1963
1999
  re-run `doctor`.
1964
2000
 
1965
2001
  ### "Message not found"
2002
+
1966
2003
  - Message may have been deleted or moved
1967
2004
  - Message IDs change if the message is moved between mailboxes
1968
2005
  - Use `search-messages` to find the current message ID
1969
2006
 
1970
2007
  ### "... is present in more than one mailbox"
1971
- - A bare numeric ID identifies a message only *within a mailbox*, and a label store (Gmail, iCloud)
2008
+
2009
+ - A bare numeric ID identifies a message only _within a mailbox_, and a label store (Gmail, iCloud)
1972
2010
  reports the same message under the same ID in `INBOX`, `Important` and `All Mail` at once. The
1973
2011
  server refuses rather than guessing which copy you meant.
1974
2012
  - Fix it by running `list-messages`/`search-messages` on the mailbox you actually want to act on,
@@ -1977,29 +2015,34 @@ In a JSON string literal, `\\` — two characters — denotes **one** literal ba
1977
2015
  by hand). `imap:…` IDs encode their own mailbox and never hit this.
1978
2016
 
1979
2017
  ### `search-messages` says "Partial results" or skips a mailbox
2018
+
1980
2019
  - This is expected for very large IMAP/Gmail mailboxes (e.g. Gmail's `All Mail`, `Important`): Apple Mail can't scan them via AppleScript before timing out, so they're skipped and named in the result rather than silently returning empty.
1981
2020
  - To search inside one, scope the call with `mailbox` **and** a `dateFrom`/`dateTo` window.
1982
2021
  - Raise or disable the threshold with `APPLE_MAIL_MAX_SEARCH_MAILBOX` (default `5000`; `0` disables the guard) — note that disabling it can make a single search take minutes.
1983
2022
  - A `Partial results` warning means coverage was incomplete; it is **not** a confirmed "no such mail."
1984
2023
 
1985
2024
  ### "Account not found"
2025
+
1986
2026
  - Account names must match exactly (case-sensitive)
1987
2027
  - Use `list-accounts` to see exact account names
1988
2028
 
1989
2029
  ### "Failed to send email"
2030
+
1990
2031
  - Check your network connection
1991
2032
  - Verify Mail.app can send emails manually
1992
2033
  - Check if the account is configured correctly in Mail.app
1993
2034
 
1994
2035
  ### "invalid outputSchema … unsupported dialect" — every tool is refused
2036
+
1995
2037
  - Full text: `Tool '<name>' has an invalid outputSchema: JSON Schema declares an unsupported dialect ("$schema": "http://json-schema.org/draft-07/schema#"). The default validator supports JSON Schema 2020-12 only.` The server connects, but **no tool is usable**.
1996
2038
  - **Upgrade to 2.10.12 or later.** Earlier versions advertised their tool schemas in JSON Schema **draft-07** (the MCP SDK's converter default); MCP has since standardized on **2020-12** and clients reject anything else. 2.10.12 normalizes every advertised `inputSchema`/`outputSchema` to 2020-12 on the way out. See [issue #147](https://github.com/sweetrb/apple-mail-mcp/issues/147).
1997
2039
  - Nothing to configure — restart your host app after upgrading so it re-reads the tool list.
1998
2040
 
1999
2041
  ### `apple-mail` server fails to connect when run from a clone
2042
+
2000
2043
  - The root `.mcp.json` resolves its entrypoint via `${CLAUDE_PROJECT_DIR:-.}/build/index.js`. **Launch `claude` from inside the repo directory** — `CLAUDE_PROJECT_DIR` only resolves to the repo root in that case; the bare `.` fallback uses the launching shell's working directory and will point at the wrong place otherwise.
2001
2044
  - If you've been editing the source, rerun `npm run build` — the server is `build/index.js`, and the committed bundle only reflects your changes after a rebuild.
2002
- - Run `claude mcp list` to check status. If you see a *conflicting scopes* warning for `apple-mail`, you have it registered at more than one scope; project-scope wins. See [Running from a clone](#running-from-a-clone-in-claude-code-project-scope-mcpjson) for how scope precedence resolves.
2045
+ - Run `claude mcp list` to check status. If you see a _conflicting scopes_ warning for `apple-mail`, you have it registered at more than one scope; project-scope wins. See [Running from a clone](#running-from-a-clone-in-claude-code-project-scope-mcpjson) for how scope precedence resolves.
2003
2046
  - If `claude mcp get apple-mail` shows **⏸ Pending approval**, approve the project-scope server (Claude Code prompts on startup, or run it again after approving).
2004
2047
 
2005
2048
  ---