apple-mail-mcp 2.8.4 → 2.8.6
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 +26 -14
- package/build/cli.js +11 -4
- package/build/index.js +45 -14
- package/docs/IMAP-SETUP.md +333 -0
- package/docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md +109 -0
- package/docs/STABILITY-PERF-AUDIT-2026-06-17.md +131 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that e
|
|
|
12
12
|
[](https://modelcontextprotocol.io)
|
|
13
13
|
|
|
14
14
|
<p align="center">
|
|
15
|
-
<img src="codex/assets/screenshot.png" alt="Apple Mail MCP — read, search, send, and organize Apple Mail from Codex, Claude, and other AI assistants" width="680">
|
|
15
|
+
<img src="https://raw.githubusercontent.com/sweetrb/apple-mail-mcp/main/codex/assets/screenshot.png" alt="Apple Mail MCP — read, search, send, and organize Apple Mail from Codex, Claude, and other AI assistants" width="680">
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
> **Note:** This is the **npm/Node.js** package — install with `npx` or `npm`. There is an unrelated Python project of the same name on PyPI ([`imdinu/apple-mail-mcp`](https://github.com/imdinu/apple-mail-mcp)) installed via `pipx`/`uvx`. If you're using `uvx` and seeing a `cyclopts` dependency error, you're looking for that project, not this one.
|
|
@@ -43,6 +43,12 @@ Install the sweetrb/apple-mail-mcp MCP server so you can help me manage my Apple
|
|
|
43
43
|
|
|
44
44
|
Claude will handle the installation and configuration automatically.
|
|
45
45
|
|
|
46
|
+
Or register it deterministically in one command:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
claude mcp add apple-mail -s user -- npx -y apple-mail-mcp
|
|
50
|
+
```
|
|
51
|
+
|
|
46
52
|
### Using the Plugin Marketplace
|
|
47
53
|
|
|
48
54
|
Install as a Claude Code plugin for automatic configuration and enhanced AI behavior:
|
|
@@ -54,6 +60,11 @@ Install as a Claude Code plugin for automatic configuration and enhanced AI beha
|
|
|
54
60
|
|
|
55
61
|
This method also installs a **skill** that teaches Claude when and how to use Apple Mail effectively.
|
|
56
62
|
|
|
63
|
+
> **Configuring IMAP/SMTP for a plugin install:** a plugin install has no editable `env` block,
|
|
64
|
+
> so supply settings via the config file at `~/Library/Application Support/apple-mail-mcp/config.json`
|
|
65
|
+
> — Method B in the [IMAP / SMTP Setup Guide](https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/IMAP-SETUP.md).
|
|
66
|
+
> Passwords stay in the macOS Keychain; run the `doctor` tool to verify.
|
|
67
|
+
|
|
57
68
|
### Using the Codex Marketplace
|
|
58
69
|
|
|
59
70
|
Install the same public marketplace in Codex:
|
|
@@ -69,14 +80,14 @@ The Codex package registers the same `apple-mail` MCP server through `npx -y app
|
|
|
69
80
|
|
|
70
81
|
Configuration for two more hosts is included — each registers the same `apple-mail` MCP server (`npx -y apple-mail-mcp`):
|
|
71
82
|
|
|
72
|
-
- **[Hermes Agent](https://hermes-agent.nousresearch.com/)** (NousResearch) — Hermes has no plugin/marketplace drop-in. Add the server with `hermes mcp add apple-mail --command npx --args -y apple-mail-mcp`, or merge [`.hermes-plugin/config.yaml`](.hermes-plugin/config.yaml) into `~/.hermes/config.yaml`. Details: [`.hermes-plugin/README.md`](.hermes-plugin/README.md).
|
|
73
|
-
- **[Antigravity](https://antigravity.google/)** (Google) — add the server entry from [`.antigravity-plugin/mcp_config.json`](.antigravity-plugin/mcp_config.json) to `~/.gemini/config/mcp_config.json` (or via Antigravity's MCP settings).
|
|
83
|
+
- **[Hermes Agent](https://hermes-agent.nousresearch.com/)** (NousResearch) — Hermes has no plugin/marketplace drop-in. Add the server with `hermes mcp add apple-mail --command npx --args -y apple-mail-mcp`, or merge [`.hermes-plugin/config.yaml`](https://github.com/sweetrb/apple-mail-mcp/blob/main/.hermes-plugin/config.yaml) into `~/.hermes/config.yaml`. Details: [`.hermes-plugin/README.md`](https://github.com/sweetrb/apple-mail-mcp/blob/main/.hermes-plugin/README.md).
|
|
84
|
+
- **[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).
|
|
74
85
|
|
|
75
86
|
### Manual Installation
|
|
76
87
|
|
|
77
88
|
**1. Install the server:**
|
|
78
89
|
```bash
|
|
79
|
-
npm install -g
|
|
90
|
+
npm install -g apple-mail-mcp
|
|
80
91
|
```
|
|
81
92
|
|
|
82
93
|
**2. Add to Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
@@ -113,7 +124,7 @@ Both are driven by non-secret `APPLE_MAIL_MCP_*` settings — supplied via an `e
|
|
|
113
124
|
block **or** a `config.json` file (for hosts like Claude Desktop that strip `env`)
|
|
114
125
|
— with passwords kept in the macOS **Keychain**, never in config.
|
|
115
126
|
|
|
116
|
-
👉 **[IMAP / SMTP Setup Guide](docs/IMAP-SETUP.md)** — step-by-step: app passwords,
|
|
127
|
+
👉 **[IMAP / SMTP Setup Guide](https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/IMAP-SETUP.md)** — step-by-step: app passwords,
|
|
117
128
|
Keychain, both config methods, multi-account, SMTP, verification with the `doctor`
|
|
118
129
|
tool, and troubleshooting. Verify any time by running the **`doctor`** tool.
|
|
119
130
|
|
|
@@ -381,7 +392,7 @@ SMTP not configured.
|
|
|
381
392
|
|
|
382
393
|
##### IMAP backend — opt-in
|
|
383
394
|
|
|
384
|
-
> 📘 **For step-by-step setup (app passwords, Keychain, config methods, multi-account, upgrading, troubleshooting), see the [IMAP / SMTP Setup Guide](docs/IMAP-SETUP.md).** The summary below is the reference; the guide is the walkthrough.
|
|
395
|
+
> 📘 **For step-by-step setup (app passwords, Keychain, config methods, multi-account, upgrading, troubleshooting), see the [IMAP / SMTP Setup Guide](https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/IMAP-SETUP.md).** The summary below is the reference; the guide is the walkthrough.
|
|
385
396
|
|
|
386
397
|
AppleScript runs `search`/`list` predicates client-side over the Apple Event
|
|
387
398
|
bridge, which is slow and can time out (false-empty) on large Gmail/IMAP
|
|
@@ -1133,7 +1144,7 @@ AI: [calls move-message for each, with mailbox="Archive"]
|
|
|
1133
1144
|
### npm (Recommended)
|
|
1134
1145
|
|
|
1135
1146
|
```bash
|
|
1136
|
-
npm install -g
|
|
1147
|
+
npm install -g apple-mail-mcp
|
|
1137
1148
|
```
|
|
1138
1149
|
|
|
1139
1150
|
### From Source
|
|
@@ -1145,6 +1156,8 @@ cd apple-mail-mcp
|
|
|
1145
1156
|
|
|
1146
1157
|
The repo ships prebuilt, dependency-free `build/index.js` and `build/cli.js` bundles, so a bare clone runs with nothing but Node installed. `npm install` and `npm run build` are only needed when you change the source.
|
|
1147
1158
|
|
|
1159
|
+
> 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.
|
|
1160
|
+
|
|
1148
1161
|
If installed from source, use this configuration:
|
|
1149
1162
|
```json
|
|
1150
1163
|
{
|
|
@@ -1189,13 +1202,12 @@ The entrypoint is written as:
|
|
|
1189
1202
|
| Limitation | Reason |
|
|
1190
1203
|
|------------|--------|
|
|
1191
1204
|
| macOS only | Apple Mail and AppleScript are macOS-specific |
|
|
1192
|
-
|
|
|
1205
|
+
| 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) |
|
|
1193
1206
|
| Attachments require absolute paths | File attachments must use full absolute paths (e.g., `/Users/me/file.pdf`) |
|
|
1194
1207
|
| No smart mailboxes | Cannot access Smart Mailboxes via AppleScript |
|
|
1195
1208
|
| 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)) |
|
|
1196
1209
|
| 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)) |
|
|
1197
|
-
|
|
|
1198
|
-
| Numeric-only message IDs | Message IDs must contain only digits (validated by schema) |
|
|
1210
|
+
| Message ID format | Message IDs must be numeric (AppleScript ids) or `imap:…` tokens from the IMAP read path (validated by schema) |
|
|
1199
1211
|
| Batch size cap | Batch operations are limited to 100 messages per request |
|
|
1200
1212
|
| 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 |
|
|
1201
1213
|
| Attachment save path restrictions | `save-attachment` only allows saving to home directory, `/tmp`, `/private/tmp`, and `/Volumes`; path traversal is blocked |
|
|
@@ -1259,7 +1271,7 @@ The `\\\\` in JSON becomes `\\` in the actual string, which represents a single
|
|
|
1259
1271
|
|
|
1260
1272
|
### "Permission denied"
|
|
1261
1273
|
- macOS needs automation permission
|
|
1262
|
-
- Go to System
|
|
1274
|
+
- Go to System Settings > Privacy & Security > Automation
|
|
1263
1275
|
- Ensure your terminal/Claude has permission to control Mail
|
|
1264
1276
|
|
|
1265
1277
|
### "Message not found"
|
|
@@ -1315,11 +1327,11 @@ A software consulting, contracting, and development company.
|
|
|
1315
1327
|
|
|
1316
1328
|
## License
|
|
1317
1329
|
|
|
1318
|
-
MIT License - see [LICENSE](LICENSE) for details.
|
|
1330
|
+
MIT License - see [LICENSE](https://github.com/sweetrb/apple-mail-mcp/blob/main/LICENSE) for details.
|
|
1319
1331
|
|
|
1320
1332
|
## Contributing
|
|
1321
1333
|
|
|
1322
|
-
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
1334
|
+
Contributions are welcome! Please see [CONTRIBUTING.md](https://github.com/sweetrb/apple-mail-mcp/blob/main/CONTRIBUTING.md) for guidelines.
|
|
1323
1335
|
|
|
1324
1336
|
## Related Projects
|
|
1325
1337
|
|
|
@@ -1331,4 +1343,4 @@ Part of a family of macOS MCP servers:
|
|
|
1331
1343
|
|
|
1332
1344
|
## Recurring macOS permission prompts
|
|
1333
1345
|
|
|
1334
|
-
If macOS keeps re-prompting for Full Disk Access or Automation for `node` (often after a `brew upgrade`), see [docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md](docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md) — the fix is to run this server under the official, Developer-ID-signed Node so the grant survives Node updates.
|
|
1346
|
+
If macOS keeps re-prompting for Full Disk Access or Automation for `node` (often after a `brew upgrade`), see [docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md](https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md) — the fix is to run this server under the official, Developer-ID-signed Node so the grant survives Node updates.
|
package/build/cli.js
CHANGED
|
@@ -11868,6 +11868,12 @@ var import_nodemailer = __toESM(require_nodemailer(), 1);
|
|
|
11868
11868
|
import { execFileSync } from "child_process";
|
|
11869
11869
|
import { isAbsolute } from "path";
|
|
11870
11870
|
import { existsSync } from "fs";
|
|
11871
|
+
|
|
11872
|
+
// src/utils/docsUrls.ts
|
|
11873
|
+
var SETUP_GUIDE_URL = "https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/IMAP-SETUP.md";
|
|
11874
|
+
var SETUP_HINT = `Setup guide: ${SETUP_GUIDE_URL} \u2014 run the "doctor" tool to check your setup.`;
|
|
11875
|
+
|
|
11876
|
+
// src/services/smtpMailer.ts
|
|
11871
11877
|
var SMTP_ENV = {
|
|
11872
11878
|
host: "APPLE_MAIL_MCP_SMTP_HOST",
|
|
11873
11879
|
port: "APPLE_MAIL_MCP_SMTP_PORT",
|
|
@@ -11900,7 +11906,7 @@ function resolveSmtpConfig(env = process.env) {
|
|
|
11900
11906
|
if (!user) missing.push(SMTP_ENV.user);
|
|
11901
11907
|
if (missing.length > 0) {
|
|
11902
11908
|
throw new Error(
|
|
11903
|
-
`SMTP transport is not configured. Set ${missing.join(" and ")} (plus a password via ${SMTP_ENV.password} or the Keychain).
|
|
11909
|
+
`SMTP transport is not configured. Set ${missing.join(" and ")} (plus a password via ${SMTP_ENV.password} or the Keychain). ` + SETUP_HINT
|
|
11904
11910
|
);
|
|
11905
11911
|
}
|
|
11906
11912
|
const secure = /^(1|true|yes)$/i.test(env[SMTP_ENV.secure]?.trim() ?? "");
|
|
@@ -11917,7 +11923,7 @@ function resolveSmtpConfig(env = process.env) {
|
|
|
11917
11923
|
}
|
|
11918
11924
|
if (!pass) {
|
|
11919
11925
|
throw new Error(
|
|
11920
|
-
`No SMTP password found. Set ${SMTP_ENV.password}, or store an internet password in the Keychain for service "${env[SMTP_ENV.keychainService]?.trim() || host}" / account "${env[SMTP_ENV.keychainAccount]?.trim() || user}"
|
|
11926
|
+
`No SMTP password found. Set ${SMTP_ENV.password}, or store an internet password in the Keychain for service "${env[SMTP_ENV.keychainService]?.trim() || host}" / account "${env[SMTP_ENV.keychainAccount]?.trim() || user}". ` + SETUP_HINT
|
|
11921
11927
|
);
|
|
11922
11928
|
}
|
|
11923
11929
|
return { host, port, secure, user, pass, from };
|
|
@@ -12003,7 +12009,8 @@ Optional:
|
|
|
12003
12009
|
--help Show this help
|
|
12004
12010
|
|
|
12005
12011
|
SMTP connection comes from ${SMTP_ENV.host} / ${SMTP_ENV.user} (+ password via
|
|
12006
|
-
${SMTP_ENV.password} or the macOS Keychain).
|
|
12012
|
+
${SMTP_ENV.password} or the macOS Keychain). Setup guide:
|
|
12013
|
+
${SETUP_GUIDE_URL}`;
|
|
12007
12014
|
async function runCli(argv, deps = {}) {
|
|
12008
12015
|
const send = deps.send ?? sendViaSmtp;
|
|
12009
12016
|
const resolveConfig = deps.resolveConfig ?? resolveSmtpConfig;
|
|
@@ -12052,7 +12059,7 @@ async function runCli(argv, deps = {}) {
|
|
|
12052
12059
|
config = resolveConfig(env);
|
|
12053
12060
|
} catch (e) {
|
|
12054
12061
|
err(e instanceof Error ? e.message : String(e));
|
|
12055
|
-
err(`
|
|
12062
|
+
err(`Setup guide: ${SETUP_GUIDE_URL}`);
|
|
12056
12063
|
return EX_CONFIG;
|
|
12057
12064
|
}
|
|
12058
12065
|
let body;
|
package/build/index.js
CHANGED
|
@@ -76219,7 +76219,7 @@ var ERROR_MAPPINGS = [
|
|
|
76219
76219
|
// Permission errors
|
|
76220
76220
|
{
|
|
76221
76221
|
pattern: /not authorized|not permitted|access.*denied/i,
|
|
76222
|
-
message: "Permission denied. Grant automation access in System
|
|
76222
|
+
message: "Permission denied. Grant automation access in System Settings > Privacy & Security > Automation."
|
|
76223
76223
|
},
|
|
76224
76224
|
// Application not running
|
|
76225
76225
|
{
|
|
@@ -79153,7 +79153,7 @@ ${actionStmts.join("\n")}
|
|
|
79153
79153
|
message: "Mail.app is accessible"
|
|
79154
79154
|
});
|
|
79155
79155
|
} else {
|
|
79156
|
-
const errorHint = mailCheck.error?.includes("not authorized") ? " (check
|
|
79156
|
+
const errorHint = mailCheck.error?.includes("not authorized") ? " (check System Settings > Privacy & Security > Automation)" : "";
|
|
79157
79157
|
checks.push({
|
|
79158
79158
|
name: "mail_app",
|
|
79159
79159
|
passed: false,
|
|
@@ -79173,7 +79173,7 @@ ${actionStmts.join("\n")}
|
|
|
79173
79173
|
checks.push({
|
|
79174
79174
|
name: "permissions",
|
|
79175
79175
|
passed: !isPermError,
|
|
79176
|
-
message: isPermError ? "AppleScript permissions denied. Grant access in System
|
|
79176
|
+
message: isPermError ? "AppleScript permissions denied. Grant access in System Settings > Privacy & Security > Automation" : `Permission check returned: ${permCheck.error}`
|
|
79177
79177
|
});
|
|
79178
79178
|
if (isPermError) {
|
|
79179
79179
|
return { healthy: false, checks };
|
|
@@ -79409,6 +79409,12 @@ var import_nodemailer = __toESM(require_nodemailer(), 1);
|
|
|
79409
79409
|
import { execFileSync } from "child_process";
|
|
79410
79410
|
import { isAbsolute as isAbsolute2 } from "path";
|
|
79411
79411
|
import { existsSync as existsSync3 } from "fs";
|
|
79412
|
+
|
|
79413
|
+
// src/utils/docsUrls.ts
|
|
79414
|
+
var SETUP_GUIDE_URL = "https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/IMAP-SETUP.md";
|
|
79415
|
+
var SETUP_HINT = `Setup guide: ${SETUP_GUIDE_URL} \u2014 run the "doctor" tool to check your setup.`;
|
|
79416
|
+
|
|
79417
|
+
// src/services/smtpMailer.ts
|
|
79412
79418
|
var SMTP_ENV = {
|
|
79413
79419
|
host: "APPLE_MAIL_MCP_SMTP_HOST",
|
|
79414
79420
|
port: "APPLE_MAIL_MCP_SMTP_PORT",
|
|
@@ -79451,7 +79457,7 @@ function resolveSmtpConfig(env = process.env) {
|
|
|
79451
79457
|
if (!user) missing.push(SMTP_ENV.user);
|
|
79452
79458
|
if (missing.length > 0) {
|
|
79453
79459
|
throw new Error(
|
|
79454
|
-
`SMTP transport is not configured. Set ${missing.join(" and ")} (plus a password via ${SMTP_ENV.password} or the Keychain).
|
|
79460
|
+
`SMTP transport is not configured. Set ${missing.join(" and ")} (plus a password via ${SMTP_ENV.password} or the Keychain). ` + SETUP_HINT
|
|
79455
79461
|
);
|
|
79456
79462
|
}
|
|
79457
79463
|
const secure = /^(1|true|yes)$/i.test(env[SMTP_ENV.secure]?.trim() ?? "");
|
|
@@ -79468,7 +79474,7 @@ function resolveSmtpConfig(env = process.env) {
|
|
|
79468
79474
|
}
|
|
79469
79475
|
if (!pass) {
|
|
79470
79476
|
throw new Error(
|
|
79471
|
-
`No SMTP password found. Set ${SMTP_ENV.password}, or store an internet password in the Keychain for service "${env[SMTP_ENV.keychainService]?.trim() || host}" / account "${env[SMTP_ENV.keychainAccount]?.trim() || user}"
|
|
79477
|
+
`No SMTP password found. Set ${SMTP_ENV.password}, or store an internet password in the Keychain for service "${env[SMTP_ENV.keychainService]?.trim() || host}" / account "${env[SMTP_ENV.keychainAccount]?.trim() || user}". ` + SETUP_HINT
|
|
79472
79478
|
);
|
|
79473
79479
|
}
|
|
79474
79480
|
return { host, port, secure, user, pass, from };
|
|
@@ -79811,7 +79817,9 @@ function resolveImapConfigs(env = process.env) {
|
|
|
79811
79817
|
function resolveImapConfig(env = process.env, account) {
|
|
79812
79818
|
const specs = listImapAccountSpecs(env);
|
|
79813
79819
|
if (specs.length === 0) {
|
|
79814
|
-
throw new Error(
|
|
79820
|
+
throw new Error(
|
|
79821
|
+
`IMAP not configured. Set ${IMAP_ENV.user} (login address) to enable it. ${SETUP_HINT}`
|
|
79822
|
+
);
|
|
79815
79823
|
}
|
|
79816
79824
|
let spec;
|
|
79817
79825
|
if (account) {
|
|
@@ -80308,6 +80316,28 @@ async function imapMoveMessageById(id, destMailbox, deps = {}) {
|
|
|
80308
80316
|
}
|
|
80309
80317
|
});
|
|
80310
80318
|
}
|
|
80319
|
+
async function resolveTrashPath(client) {
|
|
80320
|
+
try {
|
|
80321
|
+
const boxes = await client.list();
|
|
80322
|
+
const special = boxes.find((b) => b.specialUse === "\\Trash");
|
|
80323
|
+
if (special) return special.path;
|
|
80324
|
+
const named = boxes.find(
|
|
80325
|
+
(b) => /^(trash|deleted messages|deleted items|bin)$/i.test(b.name) || /(^|\/)trash$/i.test(b.path)
|
|
80326
|
+
);
|
|
80327
|
+
if (named) return named.path;
|
|
80328
|
+
} catch {
|
|
80329
|
+
}
|
|
80330
|
+
return resolveMailboxPath("trash", "list");
|
|
80331
|
+
}
|
|
80332
|
+
async function trashUids(client, uids, srcPath) {
|
|
80333
|
+
const dest = await resolveTrashPath(client);
|
|
80334
|
+
if (srcPath.trim().toLowerCase() === dest.trim().toLowerCase()) {
|
|
80335
|
+
await client.messageDelete(uids, { uid: true });
|
|
80336
|
+
return { dest, expunged: true };
|
|
80337
|
+
}
|
|
80338
|
+
await client.messageMove(uids, dest, { uid: true });
|
|
80339
|
+
return { dest, expunged: false };
|
|
80340
|
+
}
|
|
80311
80341
|
async function imapDeleteMessageById(id, deps = {}) {
|
|
80312
80342
|
const ref = decodeImapId(id);
|
|
80313
80343
|
if (!ref) return { success: false, error: `Not an IMAP message id: "${id}".` };
|
|
@@ -80316,9 +80346,11 @@ async function imapDeleteMessageById(id, deps = {}) {
|
|
|
80316
80346
|
{ ...deps, account: deps.account ?? ref.account },
|
|
80317
80347
|
async (client) => {
|
|
80318
80348
|
try {
|
|
80319
|
-
const
|
|
80320
|
-
|
|
80321
|
-
|
|
80349
|
+
const { dest, expunged } = await trashUids(client, [ref.uid], ref.path);
|
|
80350
|
+
return {
|
|
80351
|
+
success: true,
|
|
80352
|
+
info: expunged ? `Permanently deleted UID ${ref.uid} from Trash ("${ref.path}") via IMAP.` : `Moved UID ${ref.uid} to Trash ("${dest}") via IMAP.`
|
|
80353
|
+
};
|
|
80322
80354
|
} catch (e) {
|
|
80323
80355
|
return { success: false, error: `IMAP delete failed for UID ${ref.uid}: ${errText(e)}` };
|
|
80324
80356
|
}
|
|
@@ -80445,8 +80477,8 @@ var imapBatchFlag = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids) =>
|
|
|
80445
80477
|
var imapBatchUnflag = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids) => {
|
|
80446
80478
|
await c.messageFlagsRemove(uids, ["\\Flagged"], { uid: true });
|
|
80447
80479
|
});
|
|
80448
|
-
var imapBatchDelete = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids) => {
|
|
80449
|
-
await c
|
|
80480
|
+
var imapBatchDelete = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids, path) => {
|
|
80481
|
+
await trashUids(c, uids, path);
|
|
80450
80482
|
});
|
|
80451
80483
|
function imapBatchMove(ids, destMailbox, deps = {}) {
|
|
80452
80484
|
return imapBatch(ids, deps, async (c, uids) => {
|
|
@@ -80733,7 +80765,6 @@ async function routeMessage(id, opts) {
|
|
|
80733
80765
|
}
|
|
80734
80766
|
|
|
80735
80767
|
// src/tools/doctor.ts
|
|
80736
|
-
var SETUP_GUIDE = "https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/IMAP-SETUP.md";
|
|
80737
80768
|
var CONFIG_FILE_HINT = "If your MCP host ignores the server 'env' block (e.g. Claude Desktop), put these in ~/Library/Application Support/apple-mail-mcp/config.json instead";
|
|
80738
80769
|
async function runDoctor(mailManager2) {
|
|
80739
80770
|
const checks = [];
|
|
@@ -80766,7 +80797,7 @@ async function runDoctor(mailManager2) {
|
|
|
80766
80797
|
checks.push({
|
|
80767
80798
|
name: "IMAP backend",
|
|
80768
80799
|
status: "warn",
|
|
80769
|
-
detail: `not configured \u2014 AppleScript is used for all accounts. Set ${IMAP_ENV.user} (+ Keychain/password), or ${IMAP_ENV.accounts} for multiple accounts, to enable server-side search and server-mailbox ops. ${CONFIG_FILE_HINT}. Setup guide: ${
|
|
80800
|
+
detail: `not configured \u2014 AppleScript is used for all accounts. Set ${IMAP_ENV.user} (+ Keychain/password), or ${IMAP_ENV.accounts} for multiple accounts, to enable server-side search and server-mailbox ops. ${CONFIG_FILE_HINT}. Setup guide: ${SETUP_GUIDE_URL}`
|
|
80770
80801
|
});
|
|
80771
80802
|
} else {
|
|
80772
80803
|
for (const label of imapAccounts) {
|
|
@@ -80782,7 +80813,7 @@ async function runDoctor(mailManager2) {
|
|
|
80782
80813
|
checks.push({
|
|
80783
80814
|
name: "SMTP transport",
|
|
80784
80815
|
status: isSmtpConfigured() ? "ok" : "warn",
|
|
80785
|
-
detail: isSmtpConfigured() ? `configured (${smtpHost}); send-email auto-prefers clean SMTP (no Mail.app Sent-folder copy; a non-email "account" label still routes to AppleScript). Pass transport:"applescript" to force Mail.app. The apple-mail-send CLI is also available.` : `not configured \u2014 send-email uses AppleScript (subject to macOS 15+ blockquote wrapping). Set ${SMTP_ENV.host} and ${SMTP_ENV.user} (+ password via Keychain) to enable. ${CONFIG_FILE_HINT}. Setup guide: ${
|
|
80816
|
+
detail: isSmtpConfigured() ? `configured (${smtpHost}); send-email auto-prefers clean SMTP (no Mail.app Sent-folder copy; a non-email "account" label still routes to AppleScript). Pass transport:"applescript" to force Mail.app. The apple-mail-send CLI is also available.` : `not configured \u2014 send-email uses AppleScript (subject to macOS 15+ blockquote wrapping). Set ${SMTP_ENV.host} and ${SMTP_ENV.user} (+ password via Keychain) to enable. ${CONFIG_FILE_HINT}. Setup guide: ${SETUP_GUIDE_URL}`
|
|
80786
80817
|
});
|
|
80787
80818
|
const healthy = !checks.some((c) => c.status === "fail");
|
|
80788
80819
|
return { healthy, checks };
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# IMAP / SMTP Setup Guide
|
|
2
|
+
|
|
3
|
+
`apple-mail-mcp` works out of the box over AppleScript with no configuration. The
|
|
4
|
+
**IMAP backend is opt-in** and makes the server talk to your mail provider
|
|
5
|
+
directly for the operations where that's faster and more reliable than driving
|
|
6
|
+
Mail.app — most importantly **search, counts, and large mailboxes**, where
|
|
7
|
+
AppleScript times out. Configuring **SMTP** additionally lets the server send
|
|
8
|
+
clean MIME (avoiding the macOS 15+ Mail.app `<blockquote>` wrapping).
|
|
9
|
+
|
|
10
|
+
This is **additive and per-account**: any account you don't configure for IMAP
|
|
11
|
+
keeps using AppleScript exactly as before. You can mix — e.g. Gmail over IMAP,
|
|
12
|
+
everything else over AppleScript.
|
|
13
|
+
|
|
14
|
+
> **TL;DR**
|
|
15
|
+
> 1. Generate an **app-specific password** at your mail provider.
|
|
16
|
+
> 2. Store it in the **macOS Keychain** (the server reads it from there — it
|
|
17
|
+
> never goes in any config file).
|
|
18
|
+
> 3. Tell the server which account(s) to use, via an **`env` block** or a
|
|
19
|
+
> **`config.json` file** (see Step 3 below).
|
|
20
|
+
> 4. **Restart** your MCP client and run the **`doctor`** tool to verify.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## What IMAP accelerates
|
|
25
|
+
|
|
26
|
+
When an account is IMAP-configured, these route to IMAP (otherwise AppleScript):
|
|
27
|
+
|
|
28
|
+
| Capability | Tools |
|
|
29
|
+
|------------|-------|
|
|
30
|
+
| Server-side search / list | `search-messages`, `list-messages` |
|
|
31
|
+
| Read a message | `get-message` |
|
|
32
|
+
| Message mutations | `mark-as-read`/`unread`, `flag`/`unflag-message`, `move-message`, `delete-message` |
|
|
33
|
+
| Batch mutations | `batch-mark-as-read`/`unread`, `batch-flag`/`unflag-messages`, `batch-move-messages`, `batch-delete-messages` |
|
|
34
|
+
| Folder ops | `create-mailbox`, `rename-mailbox`, `delete-mailbox` |
|
|
35
|
+
| Counts & stats | `get-unread-count`, `list-mailboxes`, `get-mail-stats` |
|
|
36
|
+
| Attachments | `list-attachments`, `save-attachment`, `fetch-attachment` |
|
|
37
|
+
| Threading | `get-thread` |
|
|
38
|
+
| New-mail push | IMAP IDLE notifications (opt-in) |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Prerequisites
|
|
43
|
+
|
|
44
|
+
- macOS with Mail.app configured (the AppleScript backend still handles anything
|
|
45
|
+
not IMAP-configured).
|
|
46
|
+
- **`apple-mail-mcp` v2.1.0+** for the IMAP acceleration features, and **v2.1.1+**
|
|
47
|
+
if you need the `config.json` method (see Step 3, Method B).
|
|
48
|
+
- For each account: the ability to create an **app-specific password** (requires
|
|
49
|
+
2-factor auth enabled on that account).
|
|
50
|
+
|
|
51
|
+
Check your version with the `doctor` tool, or `npm view apple-mail-mcp version`.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Step 1 — Generate an app-specific password
|
|
56
|
+
|
|
57
|
+
Use an **app-specific password**, never your real account password. Where to get one:
|
|
58
|
+
|
|
59
|
+
- **Gmail (personal):** [myaccount.google.com](https://myaccount.google.com) →
|
|
60
|
+
**Security** → (2-Step Verification must be ON) → **App passwords** → create one,
|
|
61
|
+
name it `apple-mail-mcp`. You get a 16-character password shown **once**.
|
|
62
|
+
- **Google Workspace** (e.g. a company `you@yourdomain.com` on Google): same flow,
|
|
63
|
+
signed in as that account. ⚠️ Your Workspace **admin must allow app passwords**;
|
|
64
|
+
if the option is missing, app-password IMAP isn't available for that account.
|
|
65
|
+
- **iCloud:** [account.apple.com](https://account.apple.com) → **Sign-In and
|
|
66
|
+
Security** → **App-Specific Passwords**. Note the two different identities below.
|
|
67
|
+
- **Other IMAP providers (Fastmail, etc.):** create an app password in the
|
|
68
|
+
provider's security settings.
|
|
69
|
+
|
|
70
|
+
> **iCloud gotcha — Apple ID vs IMAP login.** Your **Apple ID** (what you sign in
|
|
71
|
+
> to account.apple.com with, possibly a Gmail address) is *not* your IMAP login.
|
|
72
|
+
> The IMAP **username** is your iCloud mailbox address, e.g. `you@icloud.com`
|
|
73
|
+
> (or a `@me.com` alias). The app password is generated under the Apple ID but
|
|
74
|
+
> authenticates the iCloud mailbox login. Also note an iCloud app password only
|
|
75
|
+
> works for iCloud — it can't log into Gmail.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Step 2 — Store the password in the Keychain
|
|
80
|
+
|
|
81
|
+
The server reads the password from the macOS login Keychain via `security
|
|
82
|
+
find-internet-password -s <service> -a <account>`. Add an entry to match. Run
|
|
83
|
+
this in **Terminal** (the `-w` flag prompts for the password silently, so it's
|
|
84
|
+
never in your shell history):
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Gmail / Google Workspace (service is always imap.gmail.com)
|
|
88
|
+
security add-internet-password -U -s imap.gmail.com -a you@gmail.com -r imap -w
|
|
89
|
+
|
|
90
|
+
# iCloud (service imap.mail.me.com; account is the iCloud MAILBOX address)
|
|
91
|
+
security add-internet-password -U -s imap.mail.me.com -a you@icloud.com -r imap -w
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Paste the app-specific password at the prompt and press Return. The `-s` (service)
|
|
95
|
+
and `-a` (account) values must match what you put in the config in Step 3
|
|
96
|
+
(`KEYCHAIN_SERVICE` / `KEYCHAIN_ACCOUNT`).
|
|
97
|
+
|
|
98
|
+
If you also want **SMTP** sending and your provider uses the same app password for
|
|
99
|
+
SMTP (Gmail does), you don't need a second Keychain entry — just point the SMTP
|
|
100
|
+
`KEYCHAIN_SERVICE`/`KEYCHAIN_ACCOUNT` at the same item.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Step 3 — Configure the server
|
|
105
|
+
|
|
106
|
+
The server reads `APPLE_MAIL_MCP_*` settings. There are two ways to supply them;
|
|
107
|
+
**use whichever your MCP client supports.**
|
|
108
|
+
|
|
109
|
+
### Method A — `env` block in your MCP client config
|
|
110
|
+
|
|
111
|
+
Works with clients that pass an `env` block through to the server (e.g. **Claude
|
|
112
|
+
Code** via `~/.claude.json`, and most standard `mcpServers` configs).
|
|
113
|
+
|
|
114
|
+
```jsonc
|
|
115
|
+
{
|
|
116
|
+
"mcpServers": {
|
|
117
|
+
"apple-mail": {
|
|
118
|
+
"command": "node",
|
|
119
|
+
"args": ["/path/to/apple-mail-mcp/build/index.js"],
|
|
120
|
+
"env": {
|
|
121
|
+
"APPLE_MAIL_MCP_IMAP_USER": "you@gmail.com",
|
|
122
|
+
"APPLE_MAIL_MCP_IMAP_ACCOUNT": "you@gmail.com",
|
|
123
|
+
"APPLE_MAIL_MCP_IMAP_HOST": "imap.gmail.com",
|
|
124
|
+
"APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE": "imap.gmail.com",
|
|
125
|
+
"APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT": "you@gmail.com"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Method B — `config.json` file (for hosts that strip `env`) *(v2.1.1+)*
|
|
133
|
+
|
|
134
|
+
Some host apps (notably **Claude Desktop**) launch MCP servers with a **scrubbed
|
|
135
|
+
environment and ignore the `env` block**, so Method A silently does nothing
|
|
136
|
+
there. For those, put the same settings in a JSON file the host doesn't manage:
|
|
137
|
+
|
|
138
|
+
- Default path: `~/Library/Application Support/apple-mail-mcp/config.json`
|
|
139
|
+
- Or set `APPLE_MAIL_MCP_CONFIG_FILE` to a path of your choice.
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"APPLE_MAIL_MCP_IMAP_USER": "you@gmail.com",
|
|
144
|
+
"APPLE_MAIL_MCP_IMAP_ACCOUNT": "you@gmail.com",
|
|
145
|
+
"APPLE_MAIL_MCP_IMAP_HOST": "imap.gmail.com",
|
|
146
|
+
"APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE": "imap.gmail.com",
|
|
147
|
+
"APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT": "you@gmail.com"
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
At startup the server merges these into its environment **without overriding**
|
|
152
|
+
anything already set, so Method A still wins if both are present. **Only
|
|
153
|
+
non-secret config goes here — passwords stay in the Keychain.**
|
|
154
|
+
|
|
155
|
+
> Not sure which method? Configure with Method A; if `doctor` still says "not
|
|
156
|
+
> configured" after a restart, your host strips `env` — switch to Method B.
|
|
157
|
+
> Plugin-marketplace installs (Claude Code `/plugin install apple-mail`) have no
|
|
158
|
+
> editable `env` block at all, so they always use Method B.
|
|
159
|
+
|
|
160
|
+
### `APPLE_MAIL_MCP_IMAP_ACCOUNT` and routing
|
|
161
|
+
|
|
162
|
+
`APPLE_MAIL_MCP_IMAP_ACCOUNT` should equal the **Mail.app account name** (what
|
|
163
|
+
`list-accounts` shows). A tool call routes to IMAP only when its `account`
|
|
164
|
+
argument matches a configured account's name or login — so this value is how the
|
|
165
|
+
server knows "calls for this account go to IMAP."
|
|
166
|
+
|
|
167
|
+
### Multiple accounts
|
|
168
|
+
|
|
169
|
+
Configure additional accounts with `APPLE_MAIL_MCP_IMAP_ACCOUNTS`, a JSON **array
|
|
170
|
+
of objects, passed as a single string value** (the whole array is one env-var /
|
|
171
|
+
config-file string, as in the example below). The legacy single-account
|
|
172
|
+
vars above define the first/default account; the array adds the rest:
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"APPLE_MAIL_MCP_IMAP_USER": "you@gmail.com",
|
|
177
|
+
"APPLE_MAIL_MCP_IMAP_ACCOUNT": "you@gmail.com",
|
|
178
|
+
"APPLE_MAIL_MCP_IMAP_HOST": "imap.gmail.com",
|
|
179
|
+
"APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE": "imap.gmail.com",
|
|
180
|
+
"APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT": "you@gmail.com",
|
|
181
|
+
"APPLE_MAIL_MCP_IMAP_ACCOUNTS": "[{\"account\":\"Work\",\"user\":\"you@company.com\",\"host\":\"imap.gmail.com\",\"keychainService\":\"imap.gmail.com\",\"keychainAccount\":\"you@company.com\"},{\"account\":\"iCloud\",\"user\":\"you@icloud.com\",\"host\":\"imap.mail.me.com\",\"keychainService\":\"imap.mail.me.com\",\"keychainAccount\":\"you@icloud.com\"}]"
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Each array entry accepts: `account`, `user`, `host`, `port`, `password`
|
|
186
|
+
(discouraged — prefer Keychain), `keychainService`, `keychainAccount`. Each
|
|
187
|
+
account keeps its own pooled IMAP connection.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Step 4 (optional) — SMTP sending
|
|
192
|
+
|
|
193
|
+
Lets `send-email` submit clean MIME directly (use `transport: "smtp"`), avoiding
|
|
194
|
+
the macOS 15+ blockquote wrapping. SMTP is single-account (the default sender):
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{
|
|
198
|
+
"APPLE_MAIL_MCP_SMTP_HOST": "smtp.gmail.com",
|
|
199
|
+
"APPLE_MAIL_MCP_SMTP_PORT": "587",
|
|
200
|
+
"APPLE_MAIL_MCP_SMTP_USER": "you@gmail.com",
|
|
201
|
+
"APPLE_MAIL_MCP_SMTP_FROM": "you@gmail.com",
|
|
202
|
+
"APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE": "imap.gmail.com",
|
|
203
|
+
"APPLE_MAIL_MCP_SMTP_KEYCHAIN_ACCOUNT": "you@gmail.com"
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
(Reusing the IMAP Keychain item is fine when the provider accepts the same app
|
|
208
|
+
password for SMTP, as Gmail does.)
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Step 5 (optional) — New-mail push (IMAP IDLE)
|
|
213
|
+
|
|
214
|
+
Add `"APPLE_MAIL_MCP_IMAP_IDLE": "1"` to get new-mail notifications for every
|
|
215
|
+
configured IMAP account. See the README's "Push notifications (IMAP IDLE)"
|
|
216
|
+
section for details. Tune the pooled-connection idle timeout with
|
|
217
|
+
`APPLE_MAIL_MCP_IMAP_IDLE_MS` (default `30000`; `0` = never close).
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Step 6 — Restart and verify
|
|
222
|
+
|
|
223
|
+
1. **Restart your MCP client** so the server picks up the new config (env and
|
|
224
|
+
config-file changes are read at startup).
|
|
225
|
+
2. Run the **`doctor`** tool. Each configured account should report:
|
|
226
|
+
`IMAP: <account> → connected to <host>`, and SMTP `configured` if you set it up.
|
|
227
|
+
|
|
228
|
+
If an account shows `connection failed`, see Troubleshooting.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Upgrading from an earlier version
|
|
233
|
+
|
|
234
|
+
- **From a version with no IMAP backend (before 1.7.0):** IMAP is purely
|
|
235
|
+
additive — nothing you had changes. Just follow Steps 1–6 for the accounts you
|
|
236
|
+
want to accelerate.
|
|
237
|
+
- **From 2.0.0 / 2.1.0:** the optimizations (attachments, batch, counts/stats,
|
|
238
|
+
threading) light up automatically for any account already IMAP-configured — no
|
|
239
|
+
new setup needed. If you weren't using IMAP yet, follow Steps 1–6.
|
|
240
|
+
- **If your host app strips `env`** (e.g. Claude Desktop): upgrade to **2.1.1+**
|
|
241
|
+
and move your settings into `config.json` (Method B). This is the most common
|
|
242
|
+
reason an upgrade "doesn't seem to use IMAP."
|
|
243
|
+
- **Already had iCloud working, now adding Gmail:** remember each provider needs
|
|
244
|
+
its **own** app password — an iCloud password can't authenticate Gmail.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Troubleshooting
|
|
249
|
+
|
|
250
|
+
**`doctor` says "IMAP backend: not configured" after a restart.**
|
|
251
|
+
The server isn't seeing your settings. Most often the host stripped the `env`
|
|
252
|
+
block — switch to the `config.json` method (Method B) and confirm the file is at
|
|
253
|
+
`~/Library/Application Support/apple-mail-mcp/config.json` (or wherever
|
|
254
|
+
`APPLE_MAIL_MCP_CONFIG_FILE` points). Verify it parses: `python3 -m json.tool < <path>`.
|
|
255
|
+
|
|
256
|
+
**`doctor` says "connection failed" for an account.**
|
|
257
|
+
- The Keychain item is missing or under a different service/account. Check:
|
|
258
|
+
`security find-internet-password -s imap.gmail.com -a you@gmail.com` (should
|
|
259
|
+
return without "could not be found"). It must match your `KEYCHAIN_SERVICE` /
|
|
260
|
+
`KEYCHAIN_ACCOUNT`.
|
|
261
|
+
- You used your real password instead of an **app-specific** password.
|
|
262
|
+
- **iCloud:** you used your Apple ID as the login instead of the iCloud mailbox
|
|
263
|
+
address; try `you@icloud.com`, the `@me.com` alias, or the bare local part.
|
|
264
|
+
- **Workspace:** your admin disabled app passwords (or requires OAuth) — IMAP via
|
|
265
|
+
app password isn't available for that account.
|
|
266
|
+
|
|
267
|
+
**Storing the Keychain password over SSH / headless fails with `User interaction is not allowed`.**
|
|
268
|
+
`security add-internet-password` — and the server *reading* the password back —
|
|
269
|
+
need the login Keychain **unlocked**, which normally only happens in a **GUI login
|
|
270
|
+
session**. From a plain `ssh` session the Keychain is locked, so the write fails
|
|
271
|
+
with `SecKeychainAddInternetPassword: User interaction is not allowed` (exit 36),
|
|
272
|
+
and a server started there can't read passwords either. Options:
|
|
273
|
+
|
|
274
|
+
- **Run it in a GUI session** — a Terminal on the Mac itself, or via Screen Sharing.
|
|
275
|
+
- **Unlock over SSH with a tty:**
|
|
276
|
+
`ssh -t you@host 'security unlock-keychain && security add-internet-password -U -r imap -s imap.gmail.com -a you@gmail.com -w'`
|
|
277
|
+
— the `-t` lets `unlock-keychain` prompt for your macOS **login** password, after
|
|
278
|
+
which the add (and later server reads) succeed. A metadata check —
|
|
279
|
+
`security find-internet-password -s … -a …` with no `-w` — does work over plain
|
|
280
|
+
ssh, so you can confirm an item *exists* even when you can't unlock it.
|
|
281
|
+
- **Truly headless** (CI, or a server with no GUI login and no unlockable Keychain):
|
|
282
|
+
skip the Keychain — set `APPLE_MAIL_MCP_SMTP_PASSWORD` / `APPLE_MAIL_MCP_IMAP_PASSWORD`
|
|
283
|
+
(or a per-account `"password"` inside `APPLE_MAIL_MCP_IMAP_ACCOUNTS`) directly, and
|
|
284
|
+
restrict the config file's permissions to the service account.
|
|
285
|
+
|
|
286
|
+
**Calls aren't routing to IMAP even though `doctor` shows connected.**
|
|
287
|
+
The tool's `account` argument must match the configured account's name/login.
|
|
288
|
+
Set `APPLE_MAIL_MCP_IMAP_ACCOUNT` to the exact Mail.app account name from
|
|
289
|
+
`list-accounts`, and pass that same `account` to tools.
|
|
290
|
+
|
|
291
|
+
**A disabled Mail account.** IMAP connects to the server directly, so it works
|
|
292
|
+
even if the account is disabled in Mail.app — useful for reaching an account the
|
|
293
|
+
GUI is ignoring.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Security notes
|
|
298
|
+
|
|
299
|
+
- **Passwords live only in the macOS Keychain.** No config file or env value
|
|
300
|
+
here holds a secret — only account names, hosts, and Keychain *references*.
|
|
301
|
+
(You *can* put a `password` directly in config, but don't.)
|
|
302
|
+
- Use **app-specific passwords** so you can revoke access per-integration without
|
|
303
|
+
changing your real password.
|
|
304
|
+
- The `config.json` and env values are non-sensitive (addresses/hosts), but
|
|
305
|
+
there's no reason to share them either.
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Full environment variable reference
|
|
310
|
+
|
|
311
|
+
| Variable | Purpose |
|
|
312
|
+
|----------|---------|
|
|
313
|
+
| `APPLE_MAIL_MCP_DEFAULT_ACCOUNT` | Account used when a tool omits `account` (name or email). |
|
|
314
|
+
| `APPLE_MAIL_MCP_IMAP_USER` | Primary IMAP login; setting it enables IMAP. |
|
|
315
|
+
| `APPLE_MAIL_MCP_IMAP_ACCOUNT` | Mail.app account name to match for routing (default = USER). |
|
|
316
|
+
| `APPLE_MAIL_MCP_IMAP_HOST` | IMAP host (default `imap.gmail.com`). |
|
|
317
|
+
| `APPLE_MAIL_MCP_IMAP_PORT` | IMAP port (default `993`, implicit TLS). |
|
|
318
|
+
| `APPLE_MAIL_MCP_IMAP_PASSWORD` | Password (discouraged; prefer Keychain). |
|
|
319
|
+
| `APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE` | Keychain item service/server name. |
|
|
320
|
+
| `APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT` | Keychain item account (default = USER). |
|
|
321
|
+
| `APPLE_MAIL_MCP_IMAP_ACCOUNTS` | JSON array of additional accounts (multi-account). |
|
|
322
|
+
| `APPLE_MAIL_MCP_IMAP_IDLE` | `1` to enable IMAP IDLE new-mail push. |
|
|
323
|
+
| `APPLE_MAIL_MCP_IMAP_IDLE_MS` | Pooled-connection idle timeout in ms (default `30000`; `0` = never close). |
|
|
324
|
+
| `APPLE_MAIL_MCP_SMTP_HOST` | SMTP host; setting it enables `transport:"smtp"`. |
|
|
325
|
+
| `APPLE_MAIL_MCP_SMTP_PORT` | SMTP port (`465` if secure, else `587`). |
|
|
326
|
+
| `APPLE_MAIL_MCP_SMTP_SECURE` | `true` for implicit TLS (465); else STARTTLS. |
|
|
327
|
+
| `APPLE_MAIL_MCP_SMTP_USER` / `_FROM` | SMTP login / From address. |
|
|
328
|
+
| `APPLE_MAIL_MCP_SMTP_PASSWORD` | Password (discouraged; prefer Keychain). |
|
|
329
|
+
| `APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE` / `_KEYCHAIN_ACCOUNT` | SMTP Keychain reference. |
|
|
330
|
+
| `APPLE_MAIL_MCP_CONFIG_FILE` | Path to the config JSON (default app-support dir). |
|
|
331
|
+
| `APPLE_MAIL_MCP_TEMPLATES_FILE` | Email-templates store path (default `~/Library/Application Support/apple-mail-mcp/templates.json`). |
|
|
332
|
+
| `APPLE_MAIL_MCP_MAX_BUFFER` | Max AppleScript (`osascript`) output buffer in bytes (default 64 MiB). |
|
|
333
|
+
| `APPLE_MAIL_MAX_SEARCH_MAILBOX` | Per-mailbox message-count guard for unscoped AppleScript search (default `5000`; `0` disables). Note: no `_MCP` in the name. |
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Node runtime & TCC permission stability
|
|
2
|
+
|
|
3
|
+
macOS gates this MCP server's access to your data behind **TCC** permissions —
|
|
4
|
+
**Full Disk Access** (to read app data such as Mail, Notes, or Photos) and
|
|
5
|
+
**Automation / Apple Events** (to drive an app like Mail.app or Notes.app via
|
|
6
|
+
AppleScript). See this repo's Full Disk Access / Automation notes for *which*
|
|
7
|
+
operations need which permission.
|
|
8
|
+
|
|
9
|
+
This page is about a **separate, recurring annoyance**: being asked to approve
|
|
10
|
+
those permissions **over and over**, often right after a routine `brew upgrade`.
|
|
11
|
+
|
|
12
|
+
## Symptom
|
|
13
|
+
|
|
14
|
+
- You granted Full Disk Access (and/or Automation) to "node", but days later
|
|
15
|
+
macOS prompts again — `"node" wants access to ...` or `"node" wants to control
|
|
16
|
+
"Mail"`.
|
|
17
|
+
- System Settings → Privacy & Security → Full Disk Access shows **several
|
|
18
|
+
identical "node" rows**, usually only one enabled.
|
|
19
|
+
- It tends to happen immediately after you update Node.
|
|
20
|
+
|
|
21
|
+
## Cause
|
|
22
|
+
|
|
23
|
+
TCC binds a permission grant to the **code identity of the binary that performs
|
|
24
|
+
the access** — here, the `node` executable that launches the MCP server. For a
|
|
25
|
+
binary that is only **ad-hoc signed** (no Developer ID / Team ID), TCC keys the
|
|
26
|
+
grant to the binary's **cdhash**, a hash of its contents.
|
|
27
|
+
|
|
28
|
+
Homebrew's `node` formula is ad-hoc signed:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
$ codesign -dvvv "$(which node)" 2>&1 | grep -E 'Signature|TeamIdentifier'
|
|
32
|
+
Signature=adhoc
|
|
33
|
+
TeamIdentifier=not set
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Every Node update **replaces the binary**, which **changes the cdhash**, so TCC
|
|
37
|
+
no longer recognizes it as the thing you approved — and re-prompts. The extra
|
|
38
|
+
"node" rows are stale cdhashes from previous versions.
|
|
39
|
+
|
|
40
|
+
By contrast, properly signed apps (Chrome, Slack, …) keep their permissions
|
|
41
|
+
across auto-updates because TCC matches them on a stable **Designated
|
|
42
|
+
Requirement** derived from their Developer ID, not on the cdhash.
|
|
43
|
+
|
|
44
|
+
## Fix: run the MCP under the official, Developer-ID-signed Node
|
|
45
|
+
|
|
46
|
+
Node binaries distributed from **nodejs.org** are signed with a real Developer
|
|
47
|
+
ID (`Node.js Foundation`, Team `HX7739G8FX`), notarized, and self-contained.
|
|
48
|
+
Pointing the MCP server at one gives TCC a **stable** identity to match, so a
|
|
49
|
+
permission you grant **persists across future Node updates**. It also decouples
|
|
50
|
+
the MCP runtime from your Homebrew/dev Node, which can keep updating freely.
|
|
51
|
+
|
|
52
|
+
### Steps (Apple Silicon shown; use `darwin-x64` on Intel)
|
|
53
|
+
|
|
54
|
+
1. Install a current LTS to a stable path (kept off `PATH` so it won't shadow
|
|
55
|
+
your dev Node):
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
VER=v24.17.0 ARCH=darwin-arm64
|
|
59
|
+
mkdir -p ~/mcp-runtime && cd ~/mcp-runtime
|
|
60
|
+
curl -O https://nodejs.org/dist/$VER/node-$VER-$ARCH.tar.gz
|
|
61
|
+
curl -O https://nodejs.org/dist/$VER/SHASUMS256.txt
|
|
62
|
+
grep " node-$VER-$ARCH.tar.gz$" SHASUMS256.txt | shasum -a 256 -c - # must print OK
|
|
63
|
+
tar -xzf node-$VER-$ARCH.tar.gz
|
|
64
|
+
ln -sfn node-$VER-$ARCH node-current
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
2. Confirm it's Developer-ID signed:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
codesign -dvvv ~/mcp-runtime/node-current/bin/node 2>&1 | grep -E 'Authority=Developer ID|TeamIdentifier'
|
|
71
|
+
# Authority=Developer ID Application: Node.js Foundation (HX7739G8FX)
|
|
72
|
+
# TeamIdentifier=HX7739G8FX
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
3. Point this MCP server's launcher at it. For Claude Desktop, edit
|
|
76
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json` and set this
|
|
77
|
+
server's `command` to the absolute path:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"mcpServers": {
|
|
82
|
+
"apple-mail": {
|
|
83
|
+
"command": "/Users/<you>/mcp-runtime/node-current/bin/node",
|
|
84
|
+
"args": ["/path/to/apple-mail-mcp/build/index.js"]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Servers launched via `npx` that don't need Full Disk Access can stay on
|
|
91
|
+
Homebrew Node.
|
|
92
|
+
|
|
93
|
+
4. **Restart your MCP client** so the server relaunches under the new Node.
|
|
94
|
+
|
|
95
|
+
5. **Grant the permissions once** to the new binary:
|
|
96
|
+
- *Full Disk Access*: System Settings → Privacy & Security → Full Disk Access
|
|
97
|
+
→ **+** → ⌘⇧G → paste `~/mcp-runtime/node-current/bin/node`.
|
|
98
|
+
- *Automation*: the first time the server drives an app you'll get a one-time
|
|
99
|
+
`"node" wants to control "<App>"` prompt — click **Allow**.
|
|
100
|
+
|
|
101
|
+
Both grants are keyed to the official Node's stable signature, so you should
|
|
102
|
+
not be asked again — including after future Node updates. You can delete the
|
|
103
|
+
stale "node" rows from the Full Disk Access list.
|
|
104
|
+
|
|
105
|
+
### Updating the dedicated Node later
|
|
106
|
+
|
|
107
|
+
Drop a newer official LTS tarball into `~/mcp-runtime/`, repoint the
|
|
108
|
+
`node-current` symlink, and restart your client. The signing identity is
|
|
109
|
+
unchanged, so existing grants carry over — no re-approval.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# apple-mail-mcp — Stability & Performance Audit
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-06-17 · **Version audited:** 1.6.1 (`main` @ `4b5610d`)
|
|
4
|
+
**Scope:** full codebase — `appleMailManager.ts` (2652 LOC), `index.ts`, `utils/{applescript,serialize,mimeParse}.ts`, CI/publish workflows.
|
|
5
|
+
|
|
6
|
+
This audit looks only at robustness and speed; feature gaps and style are out of scope. Findings are ranked by **impact × likelihood**, with effort estimates so they can be scheduled. Line numbers are against `main` @ `4b5610d`.
|
|
7
|
+
|
|
8
|
+
> **Resolution status (all items addressed 2026-06-17).** Every finding below has shipped or been explicitly deferred:
|
|
9
|
+
>
|
|
10
|
+
> | # | Finding | Status | Released |
|
|
11
|
+
> |---|---------|--------|----------|
|
|
12
|
+
> | 1 | execSync maxBuffer | Fixed ([#27](https://github.com/sweetrb/apple-mail-mcp/issues/27)) | 1.6.2 |
|
|
13
|
+
> | 2 | stats locale + swallow | Fixed ([#28](https://github.com/sweetrb/apple-mail-mcp/issues/28)) | 1.6.3 |
|
|
14
|
+
> | 3 | list-messages / by-id swallow | Fixed ([#29](https://github.com/sweetrb/apple-mail-mcp/issues/29)) | 1.6.4 |
|
|
15
|
+
> | 4 | `\|\|\|` delimiter collisions | Fixed ([#30](https://github.com/sweetrb/apple-mail-mcp/issues/30)) | 1.6.5 |
|
|
16
|
+
> | 5 | batch fan-out | Fixed ([#31](https://github.com/sweetrb/apple-mail-mcp/issues/31)) | 1.6.6 |
|
|
17
|
+
> | 6+7 | get-message source scan / preferHtml | Fixed ([#32](https://github.com/sweetrb/apple-mail-mcp/issues/32)) | 1.6.7 |
|
|
18
|
+
> | 9 | rename-mailbox partial-move | Fixed ([#33](https://github.com/sweetrb/apple-mail-mcp/issues/33)) | 1.6.8 |
|
|
19
|
+
> | 10 | escape control chars | Fixed (audit hardening) | 1.6.9 |
|
|
20
|
+
> | 8 | aggregate stat timeouts | Fixed (audit hardening) | 1.6.9 |
|
|
21
|
+
> | 12 | saveAttachment prefix bypass | Fixed (audit hardening) | 1.6.9 |
|
|
22
|
+
> | 13 | searchContacts per-person try | Fixed (audit hardening) | 1.6.9 |
|
|
23
|
+
> | 14 | useTemplate empty override | Fixed (audit hardening) | 1.6.9 |
|
|
24
|
+
> | 11 | per-message bulk reads | **Deferred** — the #24 count-guard now bounds scans to small mailboxes, so the refactor's regression risk on the hottest paths outweighs the gain | — |
|
|
25
|
+
> | 15 | CI can't run integration suite | Addressed — `npm run test:all` documented as a pre-release gate in CONTRIBUTING; the release-publish race was also fixed with a `concurrency` guard on `publish.yml` | — |
|
|
26
|
+
>
|
|
27
|
+
> Known limitation surfaced during validation: Mail.app **cannot delete iCloud mailboxes via AppleScript** ("AppleEvent handler failed"), so `delete-mailbox` / `rename-mailbox` cannot complete on iCloud-account mailboxes. This is a platform constraint, not a code defect; the rename fix ensures it fails *safely* (source preserved).
|
|
28
|
+
|
|
29
|
+
## Summary
|
|
30
|
+
|
|
31
|
+
The architecture is sound and several hard problems are already solved well (serial gate for Mail's single-threaded dispatch #11, the `with timeout`/SIGKILL executor, the `error:`-prefix protocol that surfaces AppleScript failures, the #24 search count-guard). The remaining risks cluster into four themes:
|
|
32
|
+
|
|
33
|
+
1. **Swallowed failures that masquerade as empty/zero success** — the #24 family. Several read methods still do this (`list-messages`, `get-mail-stats`, `get-recently-received`, by-id lookups). This is the single highest-value theme because it produces *confidently wrong* answers.
|
|
34
|
+
2. **The `1 MB execSync buffer** — every osascript call is capped at Node's default `maxBuffer`, so large messages/sources silently fail.
|
|
35
|
+
3. **`|||` delimiter collisions** — the string serialization corrupts on any field containing `|||`.
|
|
36
|
+
4. **N× full-tree-walk fan-out** in batch operations — correct but pathologically slow at scale.
|
|
37
|
+
|
|
38
|
+
## Priority table
|
|
39
|
+
|
|
40
|
+
| # | Sev | Type | Finding | Effort |
|
|
41
|
+
|---|-----|------|---------|--------|
|
|
42
|
+
| 1 | High | stability | No `maxBuffer` on `execSync` → 1 MB cap truncates/throws on large messages, `getRawSource`, attachment extraction | S |
|
|
43
|
+
| 2 | High | stability | `get-mail-stats` / `get-recently-received` reintroduce the #15 locale bug + swallow → silent `0/0/0` | S |
|
|
44
|
+
| 3 | High | stability | `list-messages` & by-id lookups still have the #24 unbounded-all-mailbox + swallowed-timeout pattern (false empty) | M |
|
|
45
|
+
| 4 | High | stability | `\|\|\|` / `\|\|\|ITEM\|\|\|` delimiter collisions corrupt every parser on hostile/odd field values | M |
|
|
46
|
+
| 5 | High | perf | Batch ops are N separate osascript spawns, each a full account→mailbox→`whose id` tree walk | M |
|
|
47
|
+
| 6 | Med | perf/stability | `get-message` always reads full `source of msg` for attachmentless messages (slow; trips finding #1) | S |
|
|
48
|
+
| 7 | Med | stability | `get-message preferHtml` returns raw MIME source mislabeled as `htmlContent` (dumps base64 into context) | S |
|
|
49
|
+
| 8 | Med | stability | All-mailbox stat methods (`list-mailboxes`, `get-unread-count`, `get-sync-status`) on default 30s → silent empty/zero on timeout | S |
|
|
50
|
+
| 9 | Med | stability | `rename-mailbox` move-then-delete has no rollback / partial-move accounting (data-loss risk on large mailboxes) | M |
|
|
51
|
+
| 10 | Med | stability | `escapeForAppleScript` doesn't neutralize newlines/control chars in interpolated names/subjects | S |
|
|
52
|
+
| 11 | Low | perf | Per-message property round-trips in search/list (≥6 reads/msg); could bulk-read on small mailboxes | M |
|
|
53
|
+
| 12 | Low | stability | `saveAttachment` path-prefix check uses bare `startsWith` (sibling-prefix bypass: `/Volumes` vs `/Volumes-evil`) | S |
|
|
54
|
+
| 13 | Low | stability | `searchContacts` lacks per-person `try` → one bad contact aborts the whole search | S |
|
|
55
|
+
| 14 | Low | stability | `useTemplate` `overrides.x || template.x` drops intentional empty-string overrides; templates in-memory only | S |
|
|
56
|
+
| 15 | Low | process | Integration suite can't run in CI; real-Mail regressions only caught by local `npm run test:all` | M |
|
|
57
|
+
|
|
58
|
+
Effort: S ≈ <½ day, M ≈ ½–2 days.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## High severity
|
|
63
|
+
|
|
64
|
+
### 1. No `maxBuffer` on `execSync` — 1 MB cap truncates large outputs
|
|
65
|
+
`executeAppleScript` (`utils/applescript.ts:349`) calls `execSync` with `encoding`, `timeout`, `killSignal`, `stdio` — but **no `maxBuffer`**, so Node's 1 MB default applies to every Mail operation. `getRawSource` (`appleMailManager.ts:922`) explicitly anticipates *20 MB* sources; `getMessageContent` returns full `source of msg` as `htmlContent`; large `search`/`list` outputs accumulate too. When output exceeds 1 MB, `execSync` throws `ENOBUFS`, which the executor reports as a failure, and the caller returns `null` → the user sees "message not found" / "attachment not found" or missing body for exactly the large/attachment-bearing messages where it matters most.
|
|
66
|
+
**Fix:** set a generous `maxBuffer` (e.g. 64 MB) in the `execSync` options, ideally configurable via env. Cheap and high-value.
|
|
67
|
+
|
|
68
|
+
### 2. `get-mail-stats` / `get-recently-received` — locale bug + swallow → silent `0/0/0`
|
|
69
|
+
`getRecentlyReceivedStats` (`appleMailManager.ts:2506`) builds AppleScript date thresholds with hard-coded English month names: `date "January 5, 2026"` (`formatDate`, lines 2514–2539). This is the exact construct issue **#15** replaced with the locale-independent `buildAppleScriptDate` (line ~246) because `date "May 30, 2026"` throws *"Invalid date and time (-30720)"* on non-English system locales. The throw is swallowed by the nested per-account/per-inbox `try` blocks (2542, 2546), so on any non-English locale the method returns `{last24h:0, last7d:0, last30d:0}` as a clean success — and it feeds `getMailStats`. A correctness regression of the same class as #24.
|
|
70
|
+
**Fix:** build the three thresholds with `buildAppleScriptDate`; surface failure instead of returning zero.
|
|
71
|
+
|
|
72
|
+
### 3. `list-messages` and by-id lookups still carry the #24 pattern
|
|
73
|
+
The #24 fix landed only on `searchMessages`. `listMessages` (all-mailboxes branch, `appleMailManager.ts:803–842`) still iterates every mailbox with an unbounded `messages of mb` materialization and a swallowing per-mailbox `try`, returning `[]` on timeout — a false "No messages found" on large multi-account setups. `getMessageById` / `getMessageContent` / `getRawSource` (789–942) likewise walk every mailbox of every account with no count-guard and a swallowing `on error … return ""`, so a slow account makes a real message look deleted.
|
|
74
|
+
**Fix:** extend the #24 approach (count-guard via `APPLE_MAIL_MAX_SEARCH_MAILBOX`, per-account budget, partial-result diagnostics) to `listMessages`; for by-id lookups, add the count-guard and distinguish "not found" from "timed out."
|
|
75
|
+
|
|
76
|
+
### 4. `|||` delimiter collisions corrupt parsing
|
|
77
|
+
Results are serialized by concatenating fields with the literal `"|||"` and records with `"|||ITEM|||"`, then split in TS. No field value is sanitized first. Any subject, sender, contact name, attachment filename, or mailbox name containing `|||` shifts every subsequent field. Vulnerable parsers include `parseMessageList` (~1086), `getMessageById` (843), `getMessageContent` (898), `listAttachments` (~1785), `listMailboxes` (~1938), `fetchAccounts` (~2127), `listRules` (~2193), `searchContacts` (~2285), `getSyncStatus` (~2639). Concretely, an attachment named `report|||v2.pdf` makes the parser read `v2.pdf` as the MIME type and `parseInt` the size as `0`. `fetchMailboxNames` (~2148) is a separate variant that splits on `", "`, colliding with any mailbox name containing a comma-space.
|
|
78
|
+
**Fix:** switch field/record separators to ASCII control characters that cannot appear in mail text (Unit Separator ``, Record Separator ``), or length-prefix/JSON-encode each record. Update both the AppleScript emitters and the TS parsers together.
|
|
79
|
+
|
|
80
|
+
### 5. Batch operations are N× full-tree-walk osascript spawns
|
|
81
|
+
`batchDeleteMessages` (1656), `batchMoveMessages` (1679), `batchMarkAsRead/Unread` (1697/1709), `batchFlag/Unflag` (1725+) each loop over the ids calling the single-id method. Every single-id method spawns its own osascript and walks `accounts → mailboxes of acct → messages whose id is …` to locate one message. So a 100-id batch = 100 processes, each a full breadth-first tree scan with a `whose id` query per mailbox, all serialized through the gate — minutes to hours on large accounts. (The batch APIs are capped at 100 ids, which bounds but doesn't fix it.)
|
|
82
|
+
**Fix:** one AppleScript that takes the whole id list, walks the tree once, and returns a per-id result vector; parse into `BatchOperationResult[]`.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Medium severity
|
|
87
|
+
|
|
88
|
+
### 6. `get-message` always full-source-scans attachmentless messages
|
|
89
|
+
`getMessageById` (789) computes `hasAttachments` by reading the entire `source of msg` whenever the fast attachment count is 0 (819–824), to catch MIME-embedded attachments. That's the slowest part of the common path and it interacts badly with finding #1 (1 MB cap). **Fix:** make the source-scan opt-in (e.g. a `deepAttachmentCheck` flag) or skip it when the fast count suffices for the caller.
|
|
90
|
+
|
|
91
|
+
### 7. `preferHtml` returns raw MIME, not HTML
|
|
92
|
+
`getMessageContent` (865) sets `htmlContent = source of msg` — the entire raw MIME (headers + base64 attachments), not the HTML body. `get-message preferHtml:true` (`index.ts:256`) then returns that whole blob into the model context: token blowup plus the 1 MB buffer risk. **Fix:** extract the `text/html` MIME part (you already have `mimeParse.ts`), or rename/redocument and bound the size.
|
|
93
|
+
|
|
94
|
+
### 8. All-mailbox stat methods silently degrade on 30s timeout
|
|
95
|
+
`listMailboxes` (~1913), `getUnreadCount` no-mailbox path (~1966), `getSyncStatus` (~2607) iterate all mailboxes/accounts at the **default 30s** timeout and return `[]`/`0` on timeout — a false "all read"/"no mailboxes." **Fix:** raise timeouts for these aggregate scans and surface partial/error rather than benign zeros.
|
|
96
|
+
|
|
97
|
+
### 9. `rename-mailbox` partial-move data-loss risk
|
|
98
|
+
`renameMailbox` (~2046) creates the destination, moves messages in a loop, then deletes the source — all in one `try`. A mid-loop error jumps to `on error` with no rollback and no count of how many moved, leaving messages split across two mailboxes; mutating `messages of srcMailbox` while iterating it is itself unsafe. On a large mailbox the move can also exceed the 60s timeout and be SIGKILLed before completion. **Fix:** snapshot ids, verify source/destination counts match before deleting the source, and report partial moves.
|
|
99
|
+
|
|
100
|
+
### 10. `escapeForAppleScript` doesn't handle newlines/control chars
|
|
101
|
+
`escapeForAppleScript` (155) escapes `\` and `"` but not raw newlines. AppleScript string literals can't span raw newlines, so a mailbox/template/contact name or subject containing `\n` can terminate the literal and inject a statement (or just error). Inputs are otherwise nicely escaped/validated — this is the one gap. **Fix:** strip or escape control characters (`\n`, `\r`, `\t`, line-continuation) in the escape function.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Low severity
|
|
106
|
+
|
|
107
|
+
- **11 — per-message round-trips** (search/list): each emitted message does ≥6 property reads (`subject of msg`, `sender of msg`, …); on a mailbox these are serial Apple Events. Bulk list-property reads (`subject of msgs`, `sender of msgs`) cut round-trips on the small mailboxes that survive the #24 guard. Measure first.
|
|
108
|
+
- **12 — `saveAttachment` prefix check** (~1831/1890): `startsWith(homedir())` allows `/Users/robother`; `startsWith("/Volumes")` allows `/Volumes-evil`. Compare against `prefix + path.sep` or use a `path.relative` containment check.
|
|
109
|
+
- **13 — `searchContacts`** (~2250): no per-person `try`, so one contact with no `emails` aborts the scan and returns `[]`. Wrap the per-person read.
|
|
110
|
+
- **14 — templates**: `useTemplate` `overrides.subject || template.subject` (~2358) discards an intentional `""` override; the `Map` is in-memory and `nextTemplateId` resets to 1 each start, so ids collide across restarts. Documented limitation — note in README or persist.
|
|
111
|
+
- **15 — CI can't run integration tests**: `ci.yml` runs unit tests/lint/typecheck/build + a nice "committed build/ matches source" guard, but the real-Mail `test:integration` suite needs a GUI Mail.app and so only runs locally. Real AppleScript regressions can ship. Mitigation: make `npm run test:all` a documented pre-release gate (or a self-hosted runner).
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## What's already done well (don't regress these)
|
|
116
|
+
|
|
117
|
+
- **Serial gate** (`utils/serialize.ts`) — correctly funnels all Mail calls through one promise chain with a settle delay; the right fix for Mail's single-threaded dispatch (#11).
|
|
118
|
+
- **Executor hardening** (`utils/applescript.ts`) — `with timeout` a few seconds under the process timeout so Mail aborts cleanly from inside its own dispatch, `SIGKILL` to reap wedged osascript, retry with backoff on transient errors, user-friendly error mapping.
|
|
119
|
+
- **`error:`-prefix protocol** — move/delete/reply/forward/mark/mailbox ops return `"error:" & errMsg` from `on error` and TS checks for it, surfacing AppleScript-level failures that `result.success` alone misses. The methods that *don't* use it (findings #2, #3, #8) are exactly where the swallow bugs live.
|
|
120
|
+
- **`moveMessageInternal`** — account-scoped resolution, explicit ambiguity refusal, distinct error strings, `{success,error}` return; the model the batch path should adopt.
|
|
121
|
+
- **Attachment input validation** — rejects path traversal/separators/null in names; resolves + prefix-checks save paths (modulo the `startsWith` nit in #12).
|
|
122
|
+
- **Cache stores mailbox *names* only** (not counts) with invalidation wired on the three structure-changing ops — the right call.
|
|
123
|
+
- **#24 search fix** — count-guard + per-account budget + partial diagnostics; findings #2/#3/#8 are about extending that discipline to the rest of the read surface.
|
|
124
|
+
|
|
125
|
+
## Suggested sequencing
|
|
126
|
+
|
|
127
|
+
1. **Quick wins (one small PR):** #1 maxBuffer, #2 locale/swallow, #10 escape control chars, #12 prefix check. All small, all pure stability.
|
|
128
|
+
2. **The swallow-family PR:** #3 (extend #24 guard to list/by-id) + #8 (aggregate stat timeouts) — shares code with the #24 work.
|
|
129
|
+
3. **The serialization PR:** #4 delimiters (touches many emitters/parsers; do it once, carefully, with tests).
|
|
130
|
+
4. **The perf PR:** #5 batch single-walk, then measure #11 before investing.
|
|
131
|
+
5. #6/#7/#9 as standalone medium fixes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-mail-mcp",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.6",
|
|
4
4
|
"description": "MCP server for Apple Mail - read, search, send, and manage emails via Claude and other AI assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"build",
|
|
13
|
+
"docs",
|
|
13
14
|
"README.md",
|
|
14
15
|
"LICENSE"
|
|
15
16
|
],
|