apple-mail-mcp 2.10.2 → 2.10.3
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 +33 -17
- package/build/index.js +9 -10
- package/docs/IMAP-SETUP.md +27 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -147,6 +147,7 @@ tool, and troubleshooting. Verify any time by running the **`doctor`** tool.
|
|
|
147
147
|
|
|
148
148
|
- **macOS** - Apple Mail and AppleScript are macOS-only
|
|
149
149
|
- **Node.js 20+** - Required for the MCP server
|
|
150
|
+
- **Node.js 22.5+ and Full Disk Access** - Required by `search-contacts` only. It reads the Contacts database directly through Node's built-in `node:sqlite`, which does not exist before 22.5. On an older runtime, or without Full Disk Access for the Node binary, it logs one line to stderr and returns **an empty list rather than an error** — so "no contacts found" can mean "cannot read Contacts". Every other tool works on Node 20+. See [Node runtime & TCC permissions](https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md).
|
|
150
151
|
- **Apple Mail** - Must have at least one account configured (iCloud, Gmail, Exchange, etc.)
|
|
151
152
|
|
|
152
153
|
## Features
|
|
@@ -264,6 +265,8 @@ Get the full content of a message.
|
|
|
264
265
|
|-----------|------|----------|-------------|
|
|
265
266
|
| `id` | string | Yes | Message ID |
|
|
266
267
|
| `preferHtml` | boolean | No | Return HTML source instead of plain text |
|
|
268
|
+
| `mailbox` | string | No | Mailbox holding the message (e.g. `"Sent Items"`). With `account`, opens that mailbox directly instead of scanning every mailbox — this is the fix for timeouts on large folders |
|
|
269
|
+
| `account` | string | No | Account holding the message. Pair with `mailbox` to skip the cross-mailbox scan |
|
|
267
270
|
|
|
268
271
|
**Returns:** Subject line and message body (plain text by default, HTML if `preferHtml` is true and HTML content is available).
|
|
269
272
|
|
|
@@ -668,7 +671,7 @@ Return an attachment's bytes as base64 (the read counterpart to inline-base64 se
|
|
|
668
671
|
|
|
669
672
|
| Parameter | Type | Required | Description |
|
|
670
673
|
|-----------|------|----------|-------------|
|
|
671
|
-
| `id` | string | Yes |
|
|
674
|
+
| `id` | string | Yes | Message ID (numeric or `imap:…`) |
|
|
672
675
|
| `attachmentName` | string | Yes | Attachment filename (from `list-attachments`) |
|
|
673
676
|
|
|
674
677
|
**Returns:** The attachment bytes, base64-encoded (also in `structuredContent.contentBase64`).
|
|
@@ -748,6 +751,8 @@ Flag or unflag a message. `flag-message` optionally takes a flag **color**; `unf
|
|
|
748
751
|
|
|
749
752
|
**Flag colors** are an Apple Mail feature — the message's `flag index` (0 red, 1 orange, 2 yellow, 3 green, 4 blue, 5 purple, 6 gray), which is the property a Mail smart mailbox can match on. **The color is applied on both routes** (since 2.10.0): AppleScript sets the flag index directly, and for an **IMAP-routed** id (`imap:…`) the color is written as Mail.app's `$MailFlagBit0/1/2` keywords — a 3-bit field holding the same palette index. `\Flagged` on its own really is colorless, but those keywords ride alongside it in an ordinary `UID STORE`, so a smart mailbox keyed on flag color matches an IMAP-flagged message too. You do **not** need to resolve to a numeric id just to color a flag.
|
|
750
753
|
|
|
754
|
+
To **read** a color, the IMAP read path returns `flagColorIndex` in `structuredContent` — the same 0-6 palette index, omitted when the message carries no color bits. The AppleScript read path does not populate it.
|
|
755
|
+
|
|
751
756
|
---
|
|
752
757
|
|
|
753
758
|
#### `delete-message`
|
|
@@ -853,8 +858,12 @@ Get unread message count.
|
|
|
853
858
|
|
|
854
859
|
| Parameter | Type | Required | Description |
|
|
855
860
|
|-----------|------|----------|-------------|
|
|
856
|
-
| `mailbox` | string | No | Mailbox to check (omit for
|
|
857
|
-
| `account` | string | No | Account to check |
|
|
861
|
+
| `mailbox` | string | No | Mailbox to check (omit for **INBOX**) |
|
|
862
|
+
| `account` | string | No | Account to check (omit to sum each account's INBOX) |
|
|
863
|
+
|
|
864
|
+
**Returns:** The unread count for the requested scope.
|
|
865
|
+
|
|
866
|
+
> Omitting `mailbox` counts **INBOX**, not a cross-mailbox total. This changed in 2.8.15: summing every mailbox was slow and wrong on Gmail, where one message appears in INBOX, All Mail and every label it carries. For account-wide totals use [`get-mail-stats`](#get-mail-stats).
|
|
858
867
|
|
|
859
868
|
---
|
|
860
869
|
|
|
@@ -896,6 +905,8 @@ Rename a mailbox (creates new, moves messages, deletes old).
|
|
|
896
905
|
|
|
897
906
|
### Smart Mailbox Operations (intelligente Postfächer)
|
|
898
907
|
|
|
908
|
+
> **Requires Full Disk Access.** These tools read and write `~/Library/Mail/V*/MailData/SyncedSmartMailboxes.plist`, which is TCC-protected. Without Full Disk Access for the server's Node runtime the read simply finds nothing, and the tools report "no smart mailboxes" or "launch Mail at least once" rather than a permission error — see [Node runtime & TCC permissions](https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md).
|
|
909
|
+
|
|
899
910
|
Smart mailboxes are Apple Mail's **criteria-based virtual views** — not real folders, so no messages are moved. AppleScript's `smart mailbox` / `intelligentes Postfach` terms don't compile reliably on localized (e.g. German) macOS, so these tools read and edit `~/Library/Mail/V*/MailData/SyncedSmartMailboxes.plist` directly.
|
|
900
911
|
|
|
901
912
|
**How writes stay safe:** creating or deleting a smart mailbox first backs the plist up to `SyncedSmartMailboxes.plist.bak`, edits a temp copy with `plutil`/`PlistBuddy`, validates it with `plutil -lint`, and only then atomically renames it into place. Your **existing** smart mailboxes — including any with date/data criteria — are never rewritten, only the single target entry is added or removed. These tools do **not** quit or restart Mail: **quit Mail first** for reliable results, since a running Mail may not show a new smart mailbox until it's relaunched and can overwrite plist edits it didn't make.
|
|
@@ -1030,14 +1041,15 @@ Delete a mail rule by name.
|
|
|
1030
1041
|
|
|
1031
1042
|
#### `search-contacts`
|
|
1032
1043
|
|
|
1033
|
-
Search
|
|
1044
|
+
Search the macOS Contacts database by name, organization, nickname, or email substring.
|
|
1045
|
+
|
|
1046
|
+
Since 2.8.7 this reads the AddressBook SQLite files directly rather than driving Contacts.app over AppleScript, so **Contacts.app need not be running and no Automation grant is involved** — but the Node runtime does need **Full Disk Access**, and **Node 22.5+** (see [Requirements](#requirements)). Without either, the tool returns an empty list rather than an error.
|
|
1034
1047
|
|
|
1035
1048
|
| Parameter | Type | Required | Description |
|
|
1036
1049
|
|-----------|------|----------|-------------|
|
|
1037
|
-
| `query` | string | Yes |
|
|
1038
|
-
| `limit` | number | No | Max results (default: 10) |
|
|
1050
|
+
| `query` | string | Yes | Substring matched against full name, organization, nickname, or any email address |
|
|
1039
1051
|
|
|
1040
|
-
**Returns:** List of contacts with name, email addresses, and phone numbers.
|
|
1052
|
+
**Returns:** List of contacts with name, email addresses, and phone numbers. Results are **not** truncated — a broad query returns every match.
|
|
1041
1053
|
|
|
1042
1054
|
---
|
|
1043
1055
|
|
|
@@ -1130,7 +1142,9 @@ Run a full setup diagnostic: Mail.app automation permission, account state (flag
|
|
|
1130
1142
|
|
|
1131
1143
|
Get mail statistics.
|
|
1132
1144
|
|
|
1133
|
-
|
|
1145
|
+
| Parameter | Type | Required | Description |
|
|
1146
|
+
|-----------|------|----------|-------------|
|
|
1147
|
+
| `account` | string | No | Limit to one account (uses fast IMAP `STATUS` when that account is IMAP-configured). Omit to merge across all accounts. |
|
|
1134
1148
|
|
|
1135
1149
|
**Returns:** Total and per-account message/unread counts, plus recently received stats (24h, 7d, 30d).
|
|
1136
1150
|
|
|
@@ -1271,7 +1285,7 @@ The entrypoint is written as:
|
|
|
1271
1285
|
|
|
1272
1286
|
## Security and Privacy
|
|
1273
1287
|
|
|
1274
|
-
- **
|
|
1288
|
+
- **No third parties** - The server talks only to Mail.app on this Mac (AppleScript) and, when you configure them, directly to **your own** mail provider over TLS (IMAP/SMTP). Nothing is sent to this project or any other service. With the default AppleScript backend everything stays on-device; the opt-in IMAP/SMTP backends necessarily reach your provider, which is what they are for.
|
|
1275
1289
|
- **Permission required** - macOS will prompt for automation permission on first use.
|
|
1276
1290
|
- **No credential storage** - The server doesn't store any passwords or authentication tokens.
|
|
1277
1291
|
- **Email safety** - Use `create-draft` to review emails before sending.
|
|
@@ -1285,7 +1299,7 @@ The entrypoint is written as:
|
|
|
1285
1299
|
| macOS only | Apple Mail and AppleScript are macOS-specific |
|
|
1286
1300
|
| MCP `send-email` is plain-text | The `send-email` tool sends plain text (reading HTML content is supported). To send HTML, use the bundled `apple-mail-send` CLI with `--html-body-file` (sends `multipart/alternative` via SMTP) |
|
|
1287
1301
|
| Attachments require absolute paths | File attachments must use full absolute paths (e.g., `/Users/me/file.pdf`) |
|
|
1288
|
-
|
|
|
1302
|
+
| Smart mailboxes need Mail quit | Smart mailboxes are supported (see [Smart Mailbox Operations](#smart-mailbox-operations-intelligente-postfächer)), but `create-`/`delete-smart-mailbox` edit `SyncedSmartMailboxes.plist` directly — a running Mail may not show a new one until relaunched, and can overwrite plist edits it didn't make. Quit Mail first. Reading them needs Full Disk Access for the Node runtime |
|
|
1289
1303
|
| Very large mailboxes not searchable *via AppleScript* | Apple Mail's AppleScript bridge times out on mailboxes with tens of thousands of messages, so unscoped `search-messages` skips mailboxes above `APPLE_MAIL_MAX_SEARCH_MAILBOX` (default 5000) and reports them as a partial result. Scope with `mailbox` + a date window — or configure the [IMAP backend](#imap-backend--opt-in), which searches these server-side in well under a second. ([#24](https://github.com/sweetrb/apple-mail-mcp/issues/24)) |
|
|
1290
1304
|
| Can't delete/rename server-side mailboxes or mutate drafts *via AppleScript* | Mail.app's AppleScript bridge can only `delete`/`rename` **local "On My Mac"** mailboxes and cannot delete/move drafts — it throws `AppleEvent handler failed` for IMAP/Gmail/Workspace/iCloud/Exchange mailboxes (the GUI can do it). Without IMAP configured, `delete-mailbox`/`rename-mailbox`/`delete-message`/`move-message` return a clear "do it in Mail.app directly" error instead of a generic failure. With the [IMAP backend](#imap-backend--opt-in) configured for the account, these operations run via IMAP and succeed. ([#42](https://github.com/sweetrb/apple-mail-mcp/issues/42)) |
|
|
1291
1305
|
| Message ID format | Message IDs must be numeric (AppleScript ids) or `imap:…` tokens from the IMAP read path (validated by schema) |
|
|
@@ -1385,14 +1399,16 @@ The `\\\\` in JSON becomes `\\` in the actual string, which represents a single
|
|
|
1385
1399
|
|
|
1386
1400
|
## Development
|
|
1387
1401
|
|
|
1402
|
+
This repo is **pnpm-only** — `package.json`'s `preinstall` guard hard-fails an `npm install`, because npm resolves off-lockfile and the committed bundle would then mismatch CI.
|
|
1403
|
+
|
|
1388
1404
|
```bash
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1405
|
+
corepack enable && pnpm install --frozen-lockfile # Install dependencies
|
|
1406
|
+
pnpm run build # Typecheck, then bundle src/index.ts + src/cli.ts into build/ (esbuild)
|
|
1407
|
+
pnpm test # Run unit tests
|
|
1408
|
+
pnpm run test:integration # Run integration tests (requires Mail.app)
|
|
1409
|
+
pnpm run test:all # Run all tests (unit + integration)
|
|
1410
|
+
pnpm run lint # Check code style
|
|
1411
|
+
pnpm run format # Format code
|
|
1396
1412
|
```
|
|
1397
1413
|
|
|
1398
1414
|
---
|
package/build/index.js
CHANGED
|
@@ -76079,6 +76079,10 @@ function executeAppleScript(script, options = {}) {
|
|
|
76079
76079
|
return lastError;
|
|
76080
76080
|
}
|
|
76081
76081
|
|
|
76082
|
+
// src/utils/docsUrls.ts
|
|
76083
|
+
var SETUP_GUIDE_URL = "https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/IMAP-SETUP.md";
|
|
76084
|
+
var SETUP_HINT = `Setup guide: ${SETUP_GUIDE_URL} \u2014 run the "doctor" tool to check your setup.`;
|
|
76085
|
+
|
|
76082
76086
|
// src/utils/mimeParse.ts
|
|
76083
76087
|
function extractBoundary(source) {
|
|
76084
76088
|
const match = source.match(/boundary="?([^";\s\r\n]+)"?/i);
|
|
@@ -76974,7 +76978,7 @@ var AppleMailManager = class {
|
|
|
76974
76978
|
serverSideCreateGuard(account, op) {
|
|
76975
76979
|
if (this.isServerSideAccount(account) === true) {
|
|
76976
76980
|
const verb = op === "rename" ? "rename" : "create";
|
|
76977
|
-
return `Account "${account}" stores its mailboxes on the server (IMAP / iCloud / Exchange), and Mail.app cannot ${verb} server-side mailboxes via AppleScript \u2014 a ${verb} would ${op === "rename" ? "leave a half-created orphan" : "orphan a mailbox that can never be removed"}. Configure IMAP for this account (APPLE_MAIL_MCP_IMAP_*) so mailbox create/delete/rename route through the server, or manage the folder in Mail.app directly
|
|
76981
|
+
return `Account "${account}" stores its mailboxes on the server (IMAP / iCloud / Exchange), and Mail.app cannot ${verb} server-side mailboxes via AppleScript \u2014 a ${verb} would ${op === "rename" ? "leave a half-created orphan" : "orphan a mailbox that can never be removed"}. Configure IMAP for this account (APPLE_MAIL_MCP_IMAP_*) so mailbox create/delete/rename route through the server, or manage the folder in Mail.app directly. ${SETUP_HINT}`;
|
|
76978
76982
|
}
|
|
76979
76983
|
return null;
|
|
76980
76984
|
}
|
|
@@ -79739,12 +79743,6 @@ var import_nodemailer = __toESM(require_nodemailer(), 1);
|
|
|
79739
79743
|
import { execFileSync } from "child_process";
|
|
79740
79744
|
import { isAbsolute as isAbsolute2 } from "path";
|
|
79741
79745
|
import { existsSync as existsSync4 } from "fs";
|
|
79742
|
-
|
|
79743
|
-
// src/utils/docsUrls.ts
|
|
79744
|
-
var SETUP_GUIDE_URL = "https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/IMAP-SETUP.md";
|
|
79745
|
-
var SETUP_HINT = `Setup guide: ${SETUP_GUIDE_URL} \u2014 run the "doctor" tool to check your setup.`;
|
|
79746
|
-
|
|
79747
|
-
// src/services/smtpMailer.ts
|
|
79748
79746
|
var SMTP_ENV = {
|
|
79749
79747
|
host: "APPLE_MAIL_MCP_SMTP_HOST",
|
|
79750
79748
|
port: "APPLE_MAIL_MCP_SMTP_PORT",
|
|
@@ -80144,7 +80142,7 @@ function specToConfig(spec) {
|
|
|
80144
80142
|
}
|
|
80145
80143
|
if (!pass) {
|
|
80146
80144
|
throw new Error(
|
|
80147
|
-
`No IMAP password for account "${spec.accountLabel}". Set a password or a Keychain service/account
|
|
80145
|
+
`No IMAP password for account "${spec.accountLabel}". Set a password or a Keychain service/account. ${SETUP_HINT}`
|
|
80148
80146
|
);
|
|
80149
80147
|
}
|
|
80150
80148
|
return {
|
|
@@ -83252,7 +83250,7 @@ server.registerTool(
|
|
|
83252
83250
|
withErrorHandling(({ query }) => {
|
|
83253
83251
|
const contacts = mailManager.searchContacts(query);
|
|
83254
83252
|
const structured = {
|
|
83255
|
-
contacts: contacts.map((c) => ({ name: c.name, emails: c.emails })),
|
|
83253
|
+
contacts: contacts.map((c) => ({ name: c.name, emails: c.emails, phones: c.phones })),
|
|
83256
83254
|
count: contacts.length
|
|
83257
83255
|
};
|
|
83258
83256
|
if (contacts.length === 0) {
|
|
@@ -83260,7 +83258,8 @@ server.registerTool(
|
|
|
83260
83258
|
}
|
|
83261
83259
|
const contactList = contacts.map((c) => {
|
|
83262
83260
|
const emails = c.emails.length > 0 ? c.emails.join(", ") : "no email";
|
|
83263
|
-
|
|
83261
|
+
const phones = c.phones.length > 0 ? `; ${c.phones.join(", ")}` : "";
|
|
83262
|
+
return ` - ${c.name} (${emails}${phones})`;
|
|
83264
83263
|
}).join("\n");
|
|
83265
83264
|
return successResponse(`Found ${contacts.length} contact(s):
|
|
83266
83265
|
${contactList}`, structured);
|
package/docs/IMAP-SETUP.md
CHANGED
|
@@ -160,9 +160,22 @@ non-secret config goes here — passwords stay in the Keychain.**
|
|
|
160
160
|
### `APPLE_MAIL_MCP_IMAP_ACCOUNT` and routing
|
|
161
161
|
|
|
162
162
|
`APPLE_MAIL_MCP_IMAP_ACCOUNT` should equal the **Mail.app account name** (what
|
|
163
|
-
`list-accounts` shows)
|
|
164
|
-
argument
|
|
165
|
-
|
|
163
|
+
`list-accounts` shows), because that is how an **explicitly named** `account`
|
|
164
|
+
argument is matched to an IMAP config.
|
|
165
|
+
|
|
166
|
+
**Since v2.6.0 you do not need to pass `account` at all for reads.** When any
|
|
167
|
+
IMAP account is configured, the read tools (`search-messages`, `get-thread`,
|
|
168
|
+
`list-messages`, `list-mailboxes`, `get-unread-count`, `get-mail-stats`) prefer
|
|
169
|
+
IMAP automatically:
|
|
170
|
+
|
|
171
|
+
- explicit IMAP `account` → that account over IMAP;
|
|
172
|
+
- explicit non-IMAP `account` → AppleScript;
|
|
173
|
+
- **no `account` → fan out over every configured IMAP account**, with AppleScript
|
|
174
|
+
used only for the accounts no IMAP config covers.
|
|
175
|
+
|
|
176
|
+
The name still matters for targeting a specific account and for the
|
|
177
|
+
mailbox-write ops (`create`/`delete`/`rename-mailbox`), which route to IMAP only
|
|
178
|
+
for an explicitly named IMAP account.
|
|
166
179
|
|
|
167
180
|
### Multiple accounts
|
|
168
181
|
|
|
@@ -285,9 +298,17 @@ and a server started there can't read passwords either. Options:
|
|
|
285
298
|
restrict the config file's permissions to the service account.
|
|
286
299
|
|
|
287
300
|
**Calls aren't routing to IMAP even though `doctor` shows connected.**
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
301
|
+
For the six **read** tools this should not happen since v2.6.0 — they prefer IMAP
|
|
302
|
+
as soon as any account is configured, with no `account` argument needed. If reads
|
|
303
|
+
still look like AppleScript (slow, or partial-coverage warnings), the usual cause
|
|
304
|
+
is that the server never saw your settings at all: a host that strips the `env`
|
|
305
|
+
block (use the `config.json` method), or a Keychain service/account mismatch so
|
|
306
|
+
the password lookup fails. Run `doctor` — it reports each account separately.
|
|
307
|
+
|
|
308
|
+
For **targeting one specific account**, and for the mailbox-write ops
|
|
309
|
+
(`create`/`delete`/`rename-mailbox`), the `account` argument must match the
|
|
310
|
+
configured account's name/login: set `APPLE_MAIL_MCP_IMAP_ACCOUNT` to the exact
|
|
311
|
+
Mail.app account name from `list-accounts` and pass that same `account`.
|
|
291
312
|
|
|
292
313
|
**A disabled Mail account.** IMAP connects to the server directly, so it works
|
|
293
314
|
even if the account is disabled in Mail.app — useful for reaching an account the
|
package/package.json
CHANGED