apple-mail-mcp 2.8.5 → 2.8.7
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 +175 -104
- 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
|
@@ -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.7",
|
|
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
|
],
|