apple-mail-mcp 2.19.9 → 2.19.11

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,19 +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
- | `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 |
230
- | `from` | string | No | Filter by sender email address |
231
- | `subject` | string | No | Filter by subject line |
232
- | `mailbox` | string | No | Mailbox to search in (omit to search all mailboxes) |
233
- | `account` | string | No | Account to search in (omit to search all accounts) |
234
- | `isRead` | boolean | No | Filter by read status |
235
- | `isFlagged` | boolean | No | Filter by flagged status |
236
- | `dateFrom` | string | No | Start date filter (e.g., "January 1, 2026") |
237
- | `dateTo` | string | No | End date filter (e.g., "March 1, 2026") |
238
- | `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) |
239
243
 
240
244
  **Returns:** List of matching messages with ID, date, subject, sender, and read state.
241
245
 
@@ -273,16 +277,16 @@ to disable the guard and attempt every mailbox regardless of size).
273
277
 
274
278
  **Coverage diagnostics (structured fields).** The warning above is prose for a
275
279
  human reader; the same information is also returned as structured fields on
276
- `search-messages` and `list-messages`, so a caller can tell *"nothing matched"*
277
- 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:
278
282
 
279
- | Field | Type | Meaning |
280
- |---|---|---|
281
- | `partial` | boolean | Coverage was incomplete — **the result is not a confirmed "no such mail"**. True whenever any field below is non-empty. |
282
- | `skippedLargeMailboxes` | string[] | Mailboxes never scanned because their message count exceeded `APPLE_MAIL_MAX_SEARCH_MAILBOX`, formatted `"Account / Mailbox (count)"` — e.g. `"iCloud / Archive (90694)"`. |
283
- | `notSearchedMailboxes` | string[] | Mailboxes that *were* reached but timed out or errored mid-scan, formatted `"Account / Mailbox"`. Also carries the IMAP path's `failedMailboxes`. |
284
- | `timedOutAccounts` | string[] | Accounts whose whole-account AppleScript was killed by the per-account time budget — nothing from that account was searched. |
285
- | `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. |
286
290
 
287
291
  Treat a non-empty `skippedLargeMailboxes` as actionable rather than
288
292
  informational: re-run scoped to the named mailbox with a `dateFrom`/`dateTo`
@@ -296,12 +300,12 @@ coverage was complete.
296
300
 
297
301
  Get the full content of a message.
298
302
 
299
- | Parameter | Type | Required | Description |
300
- |-----------|------|----------|-------------|
301
- | `id` | string | Yes | Message ID |
302
- | `preferHtml` | boolean | No | Return HTML source instead of plain text |
303
- | `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 |
304
- | `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 |
305
309
 
306
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.
307
311
 
@@ -318,15 +322,36 @@ Get the full content of a message.
318
322
 
319
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.
320
324
 
321
- | Parameter | Type | Required | Description |
322
- |-----------|------|----------|-------------|
323
- | `id` | string | Yes | Message ID (numeric or `imap:…`) |
324
- | `mailbox` | string | No | Mailbox holding the message. With `account`, opens that mailbox directly instead of scanning every mailbox |
325
- | `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 |
326
330
 
327
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)).
328
332
 
329
- **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).
330
355
 
331
356
  ---
332
357
 
@@ -334,14 +359,14 @@ Return a message's raw RFC 5322 header block — without fetching the body or an
334
359
 
335
360
  List messages in a mailbox.
336
361
 
337
- | Parameter | Type | Required | Description |
338
- |-----------|------|----------|-------------|
339
- | `mailbox` | string | No | Mailbox name (omit to list from all mailboxes) |
340
- | `account` | string | No | Account name |
341
- | `limit` | number | No | Max messages, 1–500 (default: 50) |
342
- | `offset` | number | No | Number of messages to skip, ≥ 0 (for pagination) |
343
- | `from` | string | No | Filter by sender email address or name |
344
- | `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 |
345
370
 
346
371
  **Returns:** List of messages with ID, date, subject, and sender.
347
372
 
@@ -368,18 +393,19 @@ Send a new email immediately.
368
393
 
369
394
  **⚠️ Safety:** Sends real mail immediately and cannot be unsent. Confirm the recipients, subject, and body with the user before calling.
370
395
 
371
- | Parameter | Type | Required | Description |
372
- |-----------|------|----------|-------------|
373
- | `to` | string[] | Yes | Recipient addresses |
374
- | `subject` | string | Yes | Email subject |
375
- | `body` | string | Yes | Email body (plain text) |
376
- | `cc` | string[] | No | CC recipients |
377
- | `bcc` | string[] | No | BCC recipients |
378
- | `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` |
379
- | `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 |
380
- | `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) |
381
406
 
382
407
  **Example:**
408
+
383
409
  ```json
384
410
  {
385
411
  "to": ["colleague@company.com"],
@@ -416,7 +442,7 @@ Two differences to know when SMTP is auto-preferred:
416
442
  Use `transport: "applescript"` if you want Mail.app itself to file the copy.
417
443
  - **`account` is a From override, not account selection.** Over SMTP, `account`
418
444
  is used as the From address only when it is an email address; a Mail.app
419
- 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
420
446
  that passes one is left on the AppleScript path automatically. To force
421
447
  account selection, pass `transport: "applescript"` explicitly. For sender
422
448
  safety, an email-form override must match the SMTP login user, the configured
@@ -437,18 +463,18 @@ trusted isolated server or test fixture; it disables the upgrade requirement and
437
463
  can expose credentials and message content. The server emits a warning when it
438
464
  is used. Keep the default unset.
439
465
 
440
- | Variable | Required | Default | Description |
441
- |----------|----------|---------|-------------|
442
- | `APPLE_MAIL_MCP_SMTP_HOST` | Yes | — | SMTP server hostname (e.g. `smtp.fastmail.com`) |
443
- | `APPLE_MAIL_MCP_SMTP_USER` | Yes | — | SMTP username |
444
- | `APPLE_MAIL_MCP_SMTP_PORT` | No | `465` if secure, else `587` | SMTP port |
445
- | `APPLE_MAIL_MCP_SMTP_SECURE` | No | `false` | `true` for implicit TLS (port 465); otherwise STARTTLS |
446
- | `APPLE_MAIL_MCP_SMTP_ALLOW_PLAINTEXT` | No | `0` | Set `1` only for an explicitly trusted plaintext test/server; otherwise STARTTLS is required |
447
- | `APPLE_MAIL_MCP_SMTP_FROM` | No | = user | From address |
448
- | `APPLE_MAIL_MCP_SMTP_ALLOWED_FROM` | No | — | Comma-separated sender aliases permitted as per-message From overrides |
449
- | `APPLE_MAIL_MCP_SMTP_PASSWORD` | No | — | Password (if set, used instead of the Keychain) |
450
- | `APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE` | No | = host | Keychain item service/server name |
451
- | `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 |
452
478
 
453
479
  Store the password in the Keychain once (an app-specific password for Gmail/
454
480
  iCloud). A generic-password item with an explicit service name keeps it from
@@ -468,6 +494,7 @@ security add-generic-password -s apple-mail-mcp-smtp -a you@gmail.com -w
468
494
 
469
495
  Once the env vars are set, a plain `send-email` (no `transport`) already goes
470
496
  out clean:
497
+
471
498
  ```json
472
499
  {
473
500
  "to": ["colleague@company.com"],
@@ -514,7 +541,7 @@ What routes to IMAP when an account is IMAP-configured:
514
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).
515
542
  - **Message mutations:** `mark-as-read`/`unread`, `flag-message`/`unflag-message`, `move-message`, `delete-message`.
516
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.
517
- - **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.
518
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.
519
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.
520
547
 
@@ -534,9 +561,9 @@ matching `account` is passed. There are three cases:
534
561
  - **Explicit IMAP account** — single-account IMAP (fast server-side path).
535
562
  - **Explicit non-IMAP account** — AppleScript (that account isn't on IMAP).
536
563
  - **No `account` given** — **merge across all accounts**: the query fans out over
537
- *every* configured IMAP account, **and** AppleScript runs **only for the
564
+ _every_ configured IMAP account, **and** AppleScript runs **only for the
538
565
  accounts no IMAP config covers** (the account list is partitioned — accounts
539
- 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
540
567
  account is IMAP-configured, AppleScript is skipped entirely. The results are
541
568
  merged so no account is dropped. Message lists still de-duplicate as a safety
542
569
  net (preferring the IMAP copy, which carries the round-trippable `imap:` id) and
@@ -555,7 +582,7 @@ matching `account` is passed. There are three cases:
555
582
  one `SEARCH` + a bounded `FETCH` per mailbox over the pooled IMAP
556
583
  connection — pin a `mailbox` to skip the fan-out when you already know
557
584
  where to look. `list-messages` (no query) still defaults an omitted
558
- mailbox to `INBOX` on every provider — only unscoped *search* scans the
585
+ mailbox to `INBOX` on every provider — only unscoped _search_ scans the
559
586
  whole account.
560
587
 
561
588
  If IMAP is **not** configured at all, every read behaves exactly as before
@@ -563,21 +590,21 @@ If IMAP is **not** configured at all, every read behaves exactly as before
563
590
  `delete-mailbox`, `rename-mailbox`) remain conservative — they route to IMAP only
564
591
  for an explicitly-named IMAP account, never on an omitted account.
565
592
 
566
- | Variable | Required | Default | Description |
567
- |----------|----------|---------|-------------|
568
- | `APPLE_MAIL_MCP_IMAP_USER` | Yes | — | Login address; setting it enables IMAP |
569
- | `APPLE_MAIL_MCP_IMAP_ACCOUNT` | No | = user | Mail account name to match for routing |
570
- | `APPLE_MAIL_MCP_IMAP_HOST` | No | `imap.gmail.com` | IMAP server hostname |
571
- | `APPLE_MAIL_MCP_IMAP_PORT` | No | `993` | IMAP port (993 = implicit TLS) |
572
- | `APPLE_MAIL_MCP_IMAP_ALLOW_PLAINTEXT` | No | `0` | Set `1` only for an explicitly trusted plaintext test/server; otherwise STARTTLS is required |
573
- | `APPLE_MAIL_MCP_IMAP_PASSWORD` | No | — | Password (if set, used instead of the Keychain) |
574
- | `APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE` | No | — | Keychain item service/server name |
575
- | `APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT` | No | = user | Keychain item account |
576
- | `APPLE_MAIL_MCP_IMAP_ACCOUNTS` | No | — | JSON array of **additional** IMAP accounts for multi-account setups (see below) |
577
- | `APPLE_MAIL_MCP_IMAP_IDLE` | No | `0` | Set `1` to enable IMAP IDLE push notifications (new-mail alerts) for every configured account |
578
- | `APPLE_MAIL_MCP_IMAP_IDLE_MS` | No | `30000` | Idle timeout (ms) before a pooled IMAP connection is closed (`0` = never close) |
579
- | `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 |
580
- | `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 |
581
608
 
582
609
  **Multiple IMAP accounts (C2):** set `APPLE_MAIL_MCP_IMAP_ACCOUNTS` to a JSON array, e.g.
583
610
  `[{"account":"Work","user":"me@co.com","host":"imap.co.com","keychainService":"imap.co.com"}]`.
@@ -623,9 +650,9 @@ those slots. This server keeps its footprint small:
623
650
  polling every 30s, so it can't linger holding sockets after its session is gone.
624
651
 
625
652
  The catch is **multiple concurrent instances**. A host like the Claude desktop
626
- 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
627
654
  them after a crash), so the footprint is **per instance × accounts**. With IDLE
628
- 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
629
656
  or IDLE on, the per-account total climbs toward Gmail's 15-connection cap and can
630
657
  starve Apple Mail of slots (→ intermittent "cannot connect"). If you hit that,
631
658
  close idle Claude conversations, keep `APPLE_MAIL_MCP_IMAP_IDLE` off unless you
@@ -702,22 +729,23 @@ Enable it in your MCP client config alongside the IMAP settings:
702
729
 
703
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.
704
731
 
705
- | Parameter | Type | Required | Description |
706
- |-----------|------|----------|-------------|
707
- | `recipients` | object[] | Yes | List of recipients, max 100 (see below) |
708
- | `subject` | string | Yes | Email subject — use `{{Key}}` for placeholders |
709
- | `body` | string | Yes | Email body — use `{{Key}}` for placeholders |
710
- | `account` | string | No | Send from specific account |
711
- | `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) |
712
739
 
713
740
  Each recipient object:
714
741
 
715
- | Field | Type | Required | Description |
716
- |-------|------|----------|-------------|
717
- | `email` | string | Yes | Recipient email address |
718
- | `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 |
719
746
 
720
747
  **Example:**
748
+
721
749
  ```json
722
750
  {
723
751
  "recipients": [
@@ -739,15 +767,15 @@ Each recipient object:
739
767
 
740
768
  Save an email to Drafts without sending.
741
769
 
742
- | Parameter | Type | Required | Description |
743
- |-----------|------|----------|-------------|
744
- | `to` | string[] | Yes | Recipient addresses |
745
- | `subject` | string | Yes | Email subject |
746
- | `body` | string | Yes | Email body (plain text) |
747
- | `cc` | string[] | No | CC recipients |
748
- | `bcc` | string[] | No | BCC recipients |
749
- | `account` | string | No | Account for draft |
750
- | `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 |
751
779
 
752
780
  **Returns:** Confirmation that draft was created.
753
781
 
@@ -755,12 +783,12 @@ Save an email to Drafts without sending.
755
783
 
756
784
  Group a conversation by normalized subject (across the AppleScript or IMAP backend).
757
785
 
758
- | Parameter | Type | Required | Description |
759
- |-----------|------|----------|-------------|
760
- | `id` | string | Yes | A message ID in the conversation (numeric or `imap:…`) |
761
- | `account` | string | No | Account to search (omit to search all) |
762
- | `mailbox` | string | No | Mailbox to search (omit to search all) |
763
- | `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) |
764
792
 
765
793
  **Returns:** The conversation's messages, oldest-first.
766
794
 
@@ -768,10 +796,10 @@ Group a conversation by normalized subject (across the AppleScript or IMAP backe
768
796
 
769
797
  Return an attachment's bytes as base64 (the read counterpart to inline-base64 send).
770
798
 
771
- | Parameter | Type | Required | Description |
772
- |-----------|------|----------|-------------|
773
- | `id` | string | Yes | Message ID (numeric or `imap:…`) |
774
- | `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`) |
775
803
 
776
804
  **Returns:** The attachment bytes, base64-encoded (also in `structuredContent.contentBase64`).
777
805
 
@@ -781,9 +809,9 @@ Return an attachment's bytes as base64 (the read counterpart to inline-base64 se
781
809
 
782
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.
783
811
 
784
- | Parameter | Type | Required | Description |
785
- |-----------|------|----------|-------------|
786
- | `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:…` |
787
815
 
788
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.
789
817
 
@@ -795,15 +823,16 @@ Map `imap:` message IDs to their numeric Mail.app IDs, via each message's RFC 53
795
823
 
796
824
  Reply to an existing message.
797
825
 
798
- | Parameter | Type | Required | Description |
799
- |-----------|------|----------|-------------|
800
- | `id` | string | Yes | Message ID to reply to |
801
- | `body` | string | Yes | Reply body |
802
- | `replyAll` | boolean | No | Reply to all recipients (default: false) |
803
- | `send` | boolean | No | Send immediately (default: true, false = save as draft) |
804
- | `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. |
805
833
 
806
834
  **Example - Reply to sender only:**
835
+
807
836
  ```json
808
837
  {
809
838
  "id": "12345",
@@ -812,6 +841,7 @@ Reply to an existing message.
812
841
  ```
813
842
 
814
843
  **Example - Reply all, save as draft:**
844
+
815
845
  ```json
816
846
  {
817
847
  "id": "12345",
@@ -837,13 +867,13 @@ Success includes `transport` and, for SMTP when returned by the server, the new
837
867
 
838
868
  Forward a message to new recipients.
839
869
 
840
- | Parameter | Type | Required | Description |
841
- |-----------|------|----------|-------------|
842
- | `id` | string | Yes | Message ID to forward |
843
- | `to` | string[] | Yes | Recipients to forward to |
844
- | `body` | string | No | Message to prepend |
845
- | `send` | boolean | No | Send immediately (default: true, false = save as draft) |
846
- | `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. |
847
877
 
848
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).
849
879
 
@@ -857,9 +887,9 @@ SMTP forwarding requires a readable plain-text original. HTML-only IMAP messages
857
887
 
858
888
  Change read status of a message.
859
889
 
860
- | Parameter | Type | Required | Description |
861
- |-----------|------|----------|-------------|
862
- | `id` | string | Yes | Message ID |
890
+ | Parameter | Type | Required | Description |
891
+ | --------- | ------ | -------- | ----------- |
892
+ | `id` | string | Yes | Message ID |
863
893
 
864
894
  ---
865
895
 
@@ -867,10 +897,10 @@ Change read status of a message.
867
897
 
868
898
  Flag or unflag a message. `flag-message` optionally takes a flag **color**; `unflag-message` removes the flag entirely (which also clears any color).
869
899
 
870
- | Parameter | Type | Required | Description |
871
- |-----------|------|----------|-------------|
872
- | `id` | string | Yes | Message ID |
873
- | `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. |
874
904
 
875
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.
876
906
 
@@ -882,9 +912,9 @@ To **read** a color, the IMAP read path returns `flagColorIndex` in `structuredC
882
912
 
883
913
  Delete a message (move to trash).
884
914
 
885
- | Parameter | Type | Required | Description |
886
- |-----------|------|----------|-------------|
887
- | `id` | string | Yes | Message ID |
915
+ | Parameter | Type | Required | Description |
916
+ | --------- | ------ | -------- | ----------- |
917
+ | `id` | string | Yes | Message ID |
888
918
 
889
919
  `structuredContent` carries `countDelta` — what the delete actually did to the
890
920
  source mailbox. See [Auditing destructive operations](#auditing-destructive-operations).
@@ -897,11 +927,11 @@ source mailbox. See [Auditing destructive operations](#auditing-destructive-oper
897
927
 
898
928
  Move a message to a different mailbox.
899
929
 
900
- | Parameter | Type | Required | Description |
901
- |-----------|------|----------|-------------|
902
- | `id` | string | Yes | Message ID |
903
- | `mailbox` | string | Yes | Destination mailbox — full path (`Work/Archive`) or a leaf name that is unique on the account |
904
- | `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 |
905
935
 
906
936
  A destination is matched first as a full path, then as a leaf name. If a leaf
907
937
  name matches **more than one** mailbox (e.g. `Archive` under both `Work` and
@@ -918,9 +948,9 @@ the full path. The same applies to `batch-move-messages`, `delete-mailbox` and
918
948
 
919
949
  List attachments on a message.
920
950
 
921
- | Parameter | Type | Required | Description |
922
- |-----------|------|----------|-------------|
923
- | `id` | string | Yes | Message ID |
951
+ | Parameter | Type | Required | Description |
952
+ | --------- | ------ | -------- | ----------- |
953
+ | `id` | string | Yes | Message ID |
924
954
 
925
955
  **Returns:** List of attachments with name, MIME type, and size.
926
956
 
@@ -935,11 +965,11 @@ of overwriting an existing file. AppleScript and MIME fallback paths stage the
935
965
  bytes privately, commit with an exclusive create, and leave the saved file
936
966
  owner-readable/writable (`0600`).
937
967
 
938
- | Parameter | Type | Required | Description |
939
- |-----------|------|----------|-------------|
940
- | `id` | string | Yes | Message ID |
941
- | `attachmentName` | string | Yes | Filename of the attachment |
942
- | `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 |
943
973
 
944
974
  ---
945
975
 
@@ -976,11 +1006,11 @@ therefore a count of messages, not of list positions.
976
1006
 
977
1007
  #### `batch-delete-messages`
978
1008
 
979
- | Parameter | Type | Required | Description |
980
- |-----------|------|----------|-------------|
981
- | `ids` | string[] | Yes | Message IDs to delete (max 100) |
982
- | `sourceMailbox` | string | No | Mailbox the **numeric** ids were listed from — pins them to it. Ignored for `imap:` ids. |
983
- | `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. |
984
1014
 
985
1015
  `structuredContent` carries `countDelta` — what the batch actually did to each
986
1016
  source mailbox. See [Auditing destructive operations](#auditing-destructive-operations).
@@ -989,33 +1019,33 @@ source mailbox. See [Auditing destructive operations](#auditing-destructive-oper
989
1019
 
990
1020
  #### `batch-move-messages`
991
1021
 
992
- | Parameter | Type | Required | Description |
993
- |-----------|------|----------|-------------|
994
- | `ids` | string[] | Yes | Message IDs to move (max 100) |
995
- | `mailbox` | string | Yes | Destination mailbox |
996
- | `account` | string | No | Account containing mailbox |
997
- | `sourceMailbox` | string | No | Mailbox the **numeric** ids were listed from — pins them to it. Ignored for `imap:` ids. |
998
- | `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. |
999
1029
 
1000
1030
  `structuredContent` carries `countDelta` — what the batch actually did to each
1001
1031
  **source** mailbox. See [Auditing destructive operations](#auditing-destructive-operations).
1002
1032
 
1003
1033
  #### `batch-mark-as-read` / `batch-mark-as-unread`
1004
1034
 
1005
- | Parameter | Type | Required | Description |
1006
- |-----------|------|----------|-------------|
1007
- | `ids` | string[] | Yes | Message IDs (max 100) |
1008
- | `sourceMailbox` | string | No | Mailbox the **numeric** ids were listed from — pins them to it. Ignored for `imap:` ids. |
1009
- | `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. |
1010
1040
 
1011
1041
  #### `batch-flag-messages` / `batch-unflag-messages`
1012
1042
 
1013
- | Parameter | Type | Required | Description |
1014
- |-----------|------|----------|-------------|
1015
- | `ids` | string[] | Yes | Message IDs (max 100) |
1016
- | `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. |
1017
- | `sourceMailbox` | string | No | Mailbox the **numeric** ids were listed from — pins them to it. Ignored for `imap:` ids. |
1018
- | `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. |
1019
1049
 
1020
1050
  ---
1021
1051
 
@@ -1025,9 +1055,9 @@ source mailbox. See [Auditing destructive operations](#auditing-destructive-oper
1025
1055
 
1026
1056
  List all mailboxes for an account.
1027
1057
 
1028
- | Parameter | Type | Required | Description |
1029
- |-----------|------|----------|-------------|
1030
- | `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 |
1031
1061
 
1032
1062
  **Returns:** List of mailbox **paths** (account-relative, e.g. `Archive/Inbox` for
1033
1063
  a nested mailbox — a top-level `Inbox` stays `Inbox`) with message and unread
@@ -1067,10 +1097,10 @@ reported as ambiguous rather than silently resolving to the account copy.
1067
1097
 
1068
1098
  Get unread message count.
1069
1099
 
1070
- | Parameter | Type | Required | Description |
1071
- |-----------|------|----------|-------------|
1072
- | `mailbox` | string | No | Mailbox to check (omit for **INBOX**) |
1073
- | `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) |
1074
1104
 
1075
1105
  **Returns:** The unread count for the requested scope.
1076
1106
 
@@ -1082,10 +1112,10 @@ Get unread message count.
1082
1112
 
1083
1113
  Create a new mailbox.
1084
1114
 
1085
- | Parameter | Type | Required | Description |
1086
- |-----------|------|----------|-------------|
1087
- | `name` | string | Yes | Mailbox name |
1088
- | `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 |
1089
1119
 
1090
1120
  ---
1091
1121
 
@@ -1093,10 +1123,10 @@ Create a new mailbox.
1093
1123
 
1094
1124
  Delete a mailbox.
1095
1125
 
1096
- | Parameter | Type | Required | Description |
1097
- |-----------|------|----------|-------------|
1098
- | `name` | string | Yes | Mailbox name |
1099
- | `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 |
1100
1130
 
1101
1131
  **⚠️ Safety:** Destructive — deletes the mailbox and its contents. Requires explicit user confirmation; list mailboxes first to confirm the name.
1102
1132
 
@@ -1106,11 +1136,11 @@ Delete a mailbox.
1106
1136
 
1107
1137
  Rename a mailbox (creates new, moves messages, deletes old).
1108
1138
 
1109
- | Parameter | Type | Required | Description |
1110
- |-----------|------|----------|-------------|
1111
- | `oldName` | string | Yes | Current mailbox name |
1112
- | `newName` | string | Yes | New mailbox name |
1113
- | `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 |
1114
1144
 
1115
1145
  ---
1116
1146
 
@@ -1136,12 +1166,12 @@ List existing smart mailboxes.
1136
1166
 
1137
1167
  Create a smart mailbox with a simple contains rule.
1138
1168
 
1139
- | Parameter | Type | Required | Description |
1140
- |-----------|------|----------|-------------|
1141
- | `name` | string | Yes | Name for the smart mailbox |
1142
- | `fromContains` | string | No | Match if From contains this |
1143
- | `subjectContains` | string | No | Match if Subject contains this |
1144
- | `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 |
1145
1175
 
1146
1176
  Provide at least one of the three `*Contains` fields.
1147
1177
 
@@ -1153,9 +1183,9 @@ Provide at least one of the three `*Contains` fields.
1153
1183
 
1154
1184
  Delete a smart mailbox by name.
1155
1185
 
1156
- | Parameter | Type | Required | Description |
1157
- |-----------|------|----------|-------------|
1158
- | `name` | string | Yes | Smart mailbox name |
1186
+ | Parameter | Type | Required | Description |
1187
+ | --------- | ------ | -------- | ------------------ |
1188
+ | `name` | string | Yes | Smart mailbox name |
1159
1189
 
1160
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.
1161
1191
 
@@ -1165,11 +1195,11 @@ Delete a smart mailbox by name.
1165
1195
 
1166
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: ...").
1167
1197
 
1168
- | Parameter | Type | Required | Description |
1169
- |-----------|------|----------|-------------|
1170
- | `dryRun` | boolean | No | Default true — only propose, do not create |
1171
- | `minCount` | number | No | Min messages from a sender (default 3) |
1172
- | `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) |
1173
1203
 
1174
1204
  Defaults to a **safe dry run** that only proposes. Pass `dryRun: false` to actually create the smart mailboxes for newsletters cluttering your Inbox.
1175
1205
 
@@ -1205,9 +1235,9 @@ List all mail rules.
1205
1235
 
1206
1236
  Enable or disable a mail rule.
1207
1237
 
1208
- | Parameter | Type | Required | Description |
1209
- |-----------|------|----------|-------------|
1210
- | `name` | string | Yes | Rule name |
1238
+ | Parameter | Type | Required | Description |
1239
+ | --------- | ------ | -------- | ----------- |
1240
+ | `name` | string | Yes | Rule name |
1211
1241
 
1212
1242
  ---
1213
1243
 
@@ -1215,13 +1245,13 @@ Enable or disable a mail rule.
1215
1245
 
1216
1246
  Create a Mail rule with one or more conditions and actions.
1217
1247
 
1218
- | Parameter | Type | Required | Description |
1219
- |-----------|------|----------|-------------|
1220
- | `name` | string | Yes | Rule name (must be unique) |
1221
- | `conditions` | object[] | Yes | One or more `{field, operator, value}` (see below) |
1222
- | `actions` | object | Yes | At least one of `markRead`, `markFlagged`, `delete`, `moveTo` |
1223
- | `matchAll` | boolean | No | `true` (default) = all conditions must match; `false` = any |
1224
- | `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`) |
1225
1255
 
1226
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`.
1227
1257
 
@@ -1231,6 +1261,7 @@ then call `enable-rule` explicitly when the rule is approved. Set
1231
1261
  `enabled: true` only when immediate activation is deliberate.
1232
1262
 
1233
1263
  **Example:**
1264
+
1234
1265
  ```json
1235
1266
  {
1236
1267
  "name": "Newsletters",
@@ -1245,9 +1276,9 @@ then call `enable-rule` explicitly when the rule is approved. Set
1245
1276
 
1246
1277
  Delete a mail rule by name.
1247
1278
 
1248
- | Parameter | Type | Required | Description |
1249
- |-----------|------|----------|-------------|
1250
- | `name` | string | Yes | Rule name |
1279
+ | Parameter | Type | Required | Description |
1280
+ | --------- | ------ | -------- | ----------- |
1281
+ | `name` | string | Yes | Rule name |
1251
1282
 
1252
1283
  **⚠️ Safety:** Destructive. Requires explicit user confirmation; list rules first to confirm the name.
1253
1284
 
@@ -1261,9 +1292,9 @@ Search the macOS Contacts database by name, organization, nickname, or email sub
1261
1292
 
1262
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.
1263
1294
 
1264
- | Parameter | Type | Required | Description |
1265
- |-----------|------|----------|-------------|
1266
- | `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 |
1267
1298
 
1268
1299
  **Returns:** List of contacts with name, email addresses, and phone numbers. Results are **not** truncated — a broad query returns every match.
1269
1300
 
@@ -1277,14 +1308,14 @@ Email templates are **persisted to disk** so they survive server restarts, store
1277
1308
 
1278
1309
  Save or update an email template.
1279
1310
 
1280
- | Parameter | Type | Required | Description |
1281
- |-----------|------|----------|-------------|
1282
- | `name` | string | Yes | Template name |
1283
- | `subject` | string | Yes | Default subject line |
1284
- | `body` | string | Yes | Template body |
1285
- | `to` | string[] | No | Default recipients |
1286
- | `cc` | string[] | No | Default CC recipients |
1287
- | `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) |
1288
1319
 
1289
1320
  ---
1290
1321
 
@@ -1300,9 +1331,9 @@ List all saved templates.
1300
1331
 
1301
1332
  Get a template by ID.
1302
1333
 
1303
- | Parameter | Type | Required | Description |
1304
- |-----------|------|----------|-------------|
1305
- | `id` | string | Yes | Template ID |
1334
+ | Parameter | Type | Required | Description |
1335
+ | --------- | ------ | -------- | ----------- |
1336
+ | `id` | string | Yes | Template ID |
1306
1337
 
1307
1338
  ---
1308
1339
 
@@ -1310,9 +1341,9 @@ Get a template by ID.
1310
1341
 
1311
1342
  Delete a template.
1312
1343
 
1313
- | Parameter | Type | Required | Description |
1314
- |-----------|------|----------|-------------|
1315
- | `id` | string | Yes | Template ID |
1344
+ | Parameter | Type | Required | Description |
1345
+ | --------- | ------ | -------- | ----------- |
1346
+ | `id` | string | Yes | Template ID |
1316
1347
 
1317
1348
  **⚠️ Safety:** Destructive — removes the template from the on-disk store. Requires explicit user confirmation; list templates first to confirm the id.
1318
1349
 
@@ -1322,13 +1353,13 @@ Delete a template.
1322
1353
 
1323
1354
  Create a draft from a template, with optional overrides.
1324
1355
 
1325
- | Parameter | Type | Required | Description |
1326
- |-----------|------|----------|-------------|
1327
- | `id` | string | Yes | Template ID |
1328
- | `to` | string[] | No | Override recipients |
1329
- | `cc` | string[] | No | Override CC |
1330
- | `subject` | string | No | Override subject |
1331
- | `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 |
1332
1363
 
1333
1364
  ---
1334
1365
 
@@ -1358,9 +1389,9 @@ Run a full setup diagnostic: Mail.app automation permission, account state (flag
1358
1389
 
1359
1390
  Get mail statistics.
1360
1391
 
1361
- | Parameter | Type | Required | Description |
1362
- |-----------|------|----------|-------------|
1363
- | `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. |
1364
1395
 
1365
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.
1366
1397
 
@@ -1443,20 +1474,20 @@ returns the comparison in `structuredContent`:
1443
1474
 
1444
1475
  `status` is deliberately not a pass/fail flag:
1445
1476
 
1446
- | `status` | Meaning | Warns? |
1447
- |----------|---------|--------|
1448
- | `match` | Exactly as many messages left the mailbox as the operation acted on. | No |
1449
- | `over` | **More** left than were operated on. Messages are unaccounted for. | **Yes** |
1450
- | `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 |
1451
1482
 
1452
- `unknownReason` distinguishes four cases that are *not* interchangeable:
1483
+ `unknownReason` distinguishes four cases that are _not_ interchangeable:
1453
1484
 
1454
- | `unknownReason` | Meaning |
1455
- |-----------------|---------|
1456
- | `count-unreadable` | Mail would not report a count at all (`before`/`after` null). |
1457
- | `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. |
1458
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. |
1459
- | `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. |
1460
1491
 
1461
1492
  > **`under` was removed in 2.11.0.** It used to mean "fewer left than expected"
1462
1493
  > and was documented as routine. Field evidence retired it — see
@@ -1479,8 +1510,8 @@ trusted:
1479
1510
 
1480
1511
  **Concurrent departure is the benign cause to rule out first**, and the warning
1481
1512
  text says so. What the asymmetry argument actually buys is the other half:
1482
- concurrent *arrivals* cannot produce `over`, because a message arriving
1483
- 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.
1484
1515
  That is why `over` is the interesting direction — a strong signal, not a proof.
1485
1516
 
1486
1517
  Setting `APPLE_MAIL_MCP_AUDIT_LOG` is what settles which one you have: the
@@ -1497,7 +1528,7 @@ On iCloud, @scottstern0325 ran the check that settled this: for two batches
1497
1528
  reporting `observed: 0`, the messages were located **in Trash**, matched by
1498
1529
  `date received` + sender against the audit log's pre-image. The deletes had
1499
1530
  happened. Mail's count had not caught up. Across four readings — 0 of 4, 0 of 1
1500
- (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
1501
1532
  to batch size, which is what a lagging count looks like and not what a
1502
1533
  store-behaviour rule looks like.
1503
1534
 
@@ -1514,14 +1545,14 @@ Two things follow:
1514
1545
  — a warning that fires on every ordinary Gmail delete would be ignored exactly
1515
1546
  when it matters.
1516
1547
  - **To confirm where messages went, match them at the destination by `date
1517
- 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
1518
1549
  by the move and do not survive it.
1519
1550
 
1520
1551
  **Removed in 2.11.0: the "reported success with no observed effect" warning.**
1521
1552
  Shipped in 2.10.30, it fired when the count was flat, the snapshot read cleanly
1522
1553
  and nothing had disappeared. Its premise was that the snapshot corroborated the
1523
1554
  count — but both are read back-to-back in the same script, and the record that
1524
- 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
1525
1556
  fired on stores that had done exactly what they were asked. It is gone rather
1526
1557
  than narrowed; `over` is the only surviving assertion.
1527
1558
 
@@ -1547,6 +1578,7 @@ Three more honesty rules:
1547
1578
  clean — but it does not flag it either. The collateral diff still names anything
1548
1579
  that vanished, so **enable `APPLE_MAIL_MCP_AUDIT_LOG` if you need coverage for
1549
1580
  same-mailbox moves.**
1581
+
1550
1582
  - A **repeated id is one message**. The batch tools operate on each distinct id
1551
1583
  once and return one result per distinct id, so `success` counts messages rather
1552
1584
  than list positions — and `expected` stays comparable with the mailbox instead
@@ -1581,10 +1613,10 @@ Single-message tools carry a post-condition check instead. `delete-message` and
1581
1613
  }
1582
1614
  ```
1583
1615
 
1584
- | `verdict` | Meaning |
1585
- |---|---|
1586
- | `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. |
1587
- | `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`. |
1588
1620
 
1589
1621
  `unverified` is **not a failure** and must not be rendered as one — it means
1590
1622
  "accepted, no observation either way". It is reported rather than hidden because
@@ -1595,12 +1627,12 @@ store can legitimately keep a message visible in an all-mail view after a move.
1595
1627
 
1596
1628
  ### `APPLE_MAIL_MCP_AUDIT_LOG` — opt-in forensic log
1597
1629
 
1598
- | Variable | Default | Description |
1599
- |----------|---------|-------------|
1600
- | `APPLE_MAIL_MCP_AUDIT_LOG` | *(off)* | Absolute path to an NDJSON file. Setting it enables the audit log **and** the collateral diff below |
1601
- | `APPLE_MAIL_MCP_AUDIT_SUBJECTS` | `0` | Set `1` to also record message **subjects**. Separate, deliberate second opt-in — see Privacy |
1602
- | `APPLE_MAIL_MCP_AUDIT_SNAPSHOT_MAX` | `2000` | Skip the collateral snapshot for mailboxes larger than this many messages. `0` disables the snapshot entirely |
1603
- | `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 |
1604
1636
 
1605
1637
  When set, each destructive operation appends **one JSON object per line**
1606
1638
  containing: timestamp, tool name, server version, the arguments it was called
@@ -1682,12 +1714,12 @@ When a slice still will not read, the snapshot is reported as `partial` and it
1682
1714
  Each half of the diff is withheld when the snapshot that could **refute** it has
1683
1715
  a hole, because a wrong name here is worse than a missing one:
1684
1716
 
1685
- | Hole in | `disappeared` / `unrequested` | `appeared` |
1686
- |---------|-------------------------------|------------|
1687
- | neither (`"ok"`) | reported | reported |
1688
- | `before` | reported (an undercount — a message never read before cannot be missed after) | withheld |
1689
- | `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 |
1690
- | 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 |
1691
1723
 
1692
1724
  **An absent field means "not computable", never "empty".** Check
1693
1725
  `"snapshot": "ok"` before reading `disappeared` as a clean bill of health.
@@ -1703,7 +1735,7 @@ a hole, because a wrong name here is worse than a missing one:
1703
1735
  ### Privacy, and what the file costs you
1704
1736
 
1705
1737
  - **Default:** identifying metadata only — Message-ID, date, mailbox, account,
1706
- 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.
1707
1739
  - **Subjects are behind their own opt-in** (`APPLE_MAIL_MCP_AUDIT_SUBJECTS=1`)
1708
1740
  because a subject line is frequently the entire sensitive payload, and it is
1709
1741
  not needed to diagnose #155.
@@ -1818,6 +1850,7 @@ The repo ships prebuilt, dependency-free `build/index.js` and `build/cli.js` bun
1818
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.
1819
1851
 
1820
1852
  If installed from source, use this configuration:
1853
+
1821
1854
  ```json
1822
1855
  {
1823
1856
  "mcpServers": {
@@ -1839,11 +1872,11 @@ The entrypoint is written as:
1839
1872
  "args": ["${CLAUDE_PROJECT_DIR:-.}/build/index.js"]
1840
1873
  ```
1841
1874
 
1842
- `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.
1843
1876
 
1844
- > **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.
1845
1878
 
1846
- > **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.
1847
1880
 
1848
1881
  ---
1849
1882
 
@@ -1868,20 +1901,20 @@ The entrypoint is written as:
1868
1901
 
1869
1902
  ## Known Limitations
1870
1903
 
1871
- | Limitation | Reason |
1872
- |------------|--------|
1873
- | macOS only | Apple Mail and AppleScript are macOS-specific |
1874
- | 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) |
1875
- | 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. |
1876
- | 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 |
1877
- | 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)) |
1878
- | 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)) |
1879
- | Message ID format | Message IDs must be numeric (AppleScript ids) or `imap:…` tokens from the IMAP read path (validated by schema) |
1880
- | Batch size cap | Batch operations are limited to 100 messages per request |
1881
- | 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 |
1882
- | Attachment save path restrictions | `save-attachment` only allows saving to home directory, `/tmp`, `/private/tmp`, and `/Volumes`; path traversal is blocked |
1883
- | Attachment count limit | `send-email` and `create-draft` accept a maximum of 20 file attachments |
1884
- | 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 |
1885
1918
 
1886
1919
  ### Mail.app `<blockquote>` wrapping on macOS 15+ (workaround in v1.6.0)
1887
1920
 
@@ -1947,11 +1980,13 @@ In a JSON string literal, `\\` — two characters — denotes **one** literal ba
1947
1980
  ## Troubleshooting
1948
1981
 
1949
1982
  ### "Mail.app not responding"
1983
+
1950
1984
  - Ensure Mail.app is not frozen
1951
1985
  - Try opening Mail.app manually
1952
1986
  - Restart the MCP server
1953
1987
 
1954
1988
  ### "Permission denied"
1989
+
1955
1990
  - macOS needs automation permission
1956
1991
  - Go to System Settings > Privacy & Security > Automation
1957
1992
  - Ensure your terminal/Claude has permission to control Mail
@@ -1959,17 +1994,19 @@ In a JSON string literal, `\\` — two characters — denotes **one** literal ba
1959
1994
  language — `Not authorised to send Apple events to Mail. (-1743)` on en-GB/en-AU/en-IE,
1960
1995
  and fully translated on fr/de/es. Before **v2.17.10** the server didn't recognise
1961
1996
  those spellings, so `health-check`/`doctor` reported `permissions: ok` and then blamed
1962
- 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."_).
1963
1998
  If you see that on a Mac whose Mail accounts are already configured, upgrade and
1964
1999
  re-run `doctor`.
1965
2000
 
1966
2001
  ### "Message not found"
2002
+
1967
2003
  - Message may have been deleted or moved
1968
2004
  - Message IDs change if the message is moved between mailboxes
1969
2005
  - Use `search-messages` to find the current message ID
1970
2006
 
1971
2007
  ### "... is present in more than one mailbox"
1972
- - 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)
1973
2010
  reports the same message under the same ID in `INBOX`, `Important` and `All Mail` at once. The
1974
2011
  server refuses rather than guessing which copy you meant.
1975
2012
  - Fix it by running `list-messages`/`search-messages` on the mailbox you actually want to act on,
@@ -1978,29 +2015,34 @@ In a JSON string literal, `\\` — two characters — denotes **one** literal ba
1978
2015
  by hand). `imap:…` IDs encode their own mailbox and never hit this.
1979
2016
 
1980
2017
  ### `search-messages` says "Partial results" or skips a mailbox
2018
+
1981
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.
1982
2020
  - To search inside one, scope the call with `mailbox` **and** a `dateFrom`/`dateTo` window.
1983
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.
1984
2022
  - A `Partial results` warning means coverage was incomplete; it is **not** a confirmed "no such mail."
1985
2023
 
1986
2024
  ### "Account not found"
2025
+
1987
2026
  - Account names must match exactly (case-sensitive)
1988
2027
  - Use `list-accounts` to see exact account names
1989
2028
 
1990
2029
  ### "Failed to send email"
2030
+
1991
2031
  - Check your network connection
1992
2032
  - Verify Mail.app can send emails manually
1993
2033
  - Check if the account is configured correctly in Mail.app
1994
2034
 
1995
2035
  ### "invalid outputSchema … unsupported dialect" — every tool is refused
2036
+
1996
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**.
1997
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).
1998
2039
  - Nothing to configure — restart your host app after upgrading so it re-reads the tool list.
1999
2040
 
2000
2041
  ### `apple-mail` server fails to connect when run from a clone
2042
+
2001
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.
2002
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.
2003
- - 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.
2004
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).
2005
2047
 
2006
2048
  ---