apple-mail-mcp 2.10.16 → 2.10.17
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 +208 -0
- package/build/index.js +832 -77
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -202,6 +202,7 @@ Read/list/get tools also return **structured JSON** (`structuredContent`) alongs
|
|
|
202
202
|
| **Doctor** | Diagnose Mail permission, account state, and each IMAP/SMTP backend with actionable messages |
|
|
203
203
|
| **Statistics** | Message and unread counts per account, recently received stats |
|
|
204
204
|
| **Sync Status** | Check if Mail.app is actively syncing |
|
|
205
|
+
| **Effect reconciliation** | Every delete/move reports what it actually did to the mailbox (`countDelta`), and warns when more messages left than were operated on — see [Auditing destructive operations](#auditing-destructive-operations) |
|
|
205
206
|
|
|
206
207
|
### MCP resources & prompts
|
|
207
208
|
|
|
@@ -779,6 +780,9 @@ Delete a message (move to trash).
|
|
|
779
780
|
|-----------|------|----------|-------------|
|
|
780
781
|
| `id` | string | Yes | Message ID |
|
|
781
782
|
|
|
783
|
+
`structuredContent` carries `countDelta` — what the delete actually did to the
|
|
784
|
+
source mailbox. See [Auditing destructive operations](#auditing-destructive-operations).
|
|
785
|
+
|
|
782
786
|
**⚠️ Safety:** Destructive. Requires explicit user confirmation; search/list first to confirm the message id.
|
|
783
787
|
|
|
784
788
|
---
|
|
@@ -799,6 +803,9 @@ name matches **more than one** mailbox (e.g. `Archive` under both `Work` and
|
|
|
799
803
|
the full path. The same applies to `batch-move-messages`, `delete-mailbox` and
|
|
800
804
|
`rename-mailbox`.
|
|
801
805
|
|
|
806
|
+
`structuredContent` carries `countDelta` — what the move actually did to the
|
|
807
|
+
**source** mailbox. See [Auditing destructive operations](#auditing-destructive-operations).
|
|
808
|
+
|
|
802
809
|
---
|
|
803
810
|
|
|
804
811
|
#### `list-attachments`
|
|
@@ -851,6 +858,10 @@ default account cannot be determined, the ids fail with an error asking for an e
|
|
|
851
858
|
`sourceAccount` — a scope the server can't honor is never quietly downgraded to the guess-the-copy
|
|
852
859
|
walk. `sourceAccount` by itself pins nothing, since the mailbox is what an id is scoped to.
|
|
853
860
|
|
|
861
|
+
**A repeated id is one message.** `ids` is treated as a set: a duplicate names the same message,
|
|
862
|
+
so it is operated on once, and the batch returns **one result per distinct id**. `success` is
|
|
863
|
+
therefore a count of messages, not of list positions.
|
|
864
|
+
|
|
854
865
|
#### `batch-delete-messages`
|
|
855
866
|
|
|
856
867
|
| Parameter | Type | Required | Description |
|
|
@@ -859,6 +870,9 @@ walk. `sourceAccount` by itself pins nothing, since the mailbox is what an id is
|
|
|
859
870
|
| `sourceMailbox` | string | No | Mailbox the **numeric** ids were listed from — pins them to it. Ignored for `imap:` ids. |
|
|
860
871
|
| `sourceAccount` | string | No | Account the numeric ids were listed from. Defaults to the default account; on its own it pins nothing — pair it with `sourceMailbox`. |
|
|
861
872
|
|
|
873
|
+
`structuredContent` carries `countDelta` — what the batch actually did to each
|
|
874
|
+
source mailbox. See [Auditing destructive operations](#auditing-destructive-operations).
|
|
875
|
+
|
|
862
876
|
**⚠️ Safety:** Destructive. Requires explicit user confirmation; search/list first to confirm the message ids.
|
|
863
877
|
|
|
864
878
|
#### `batch-move-messages`
|
|
@@ -871,6 +885,9 @@ walk. `sourceAccount` by itself pins nothing, since the mailbox is what an id is
|
|
|
871
885
|
| `sourceMailbox` | string | No | Mailbox the **numeric** ids were listed from — pins them to it. Ignored for `imap:` ids. |
|
|
872
886
|
| `sourceAccount` | string | No | Account the numeric ids were listed from. Defaults to the default account; on its own it pins nothing — pair it with `sourceMailbox`. |
|
|
873
887
|
|
|
888
|
+
`structuredContent` carries `countDelta` — what the batch actually did to each
|
|
889
|
+
**source** mailbox. See [Auditing destructive operations](#auditing-destructive-operations).
|
|
890
|
+
|
|
874
891
|
#### `batch-mark-as-read` / `batch-mark-as-unread`
|
|
875
892
|
|
|
876
893
|
| Parameter | Type | Required | Description |
|
|
@@ -1242,6 +1259,197 @@ Check Mail.app sync activity.
|
|
|
1242
1259
|
|
|
1243
1260
|
---
|
|
1244
1261
|
|
|
1262
|
+
## Auditing destructive operations
|
|
1263
|
+
|
|
1264
|
+
`delete-message`, `move-message`, `batch-delete-messages` and
|
|
1265
|
+
`batch-move-messages` report what they actually did, not merely that Mail.app did
|
|
1266
|
+
not raise an error. This exists because of
|
|
1267
|
+
[#155](https://github.com/sweetrb/apple-mail-mcp/issues/155): a batch delete was
|
|
1268
|
+
reported to have removed two messages whose ids were never passed, and nothing in
|
|
1269
|
+
the server recorded enough to explain it.
|
|
1270
|
+
|
|
1271
|
+
### `countDelta` — always on, no configuration
|
|
1272
|
+
|
|
1273
|
+
Every one of those four tools counts the affected **source** mailbox immediately
|
|
1274
|
+
before and immediately after the mutation, **inside the same AppleScript it was
|
|
1275
|
+
already running** (no extra `osascript` invocations, no measurable cost), and
|
|
1276
|
+
returns the comparison in `structuredContent`:
|
|
1277
|
+
|
|
1278
|
+
```json
|
|
1279
|
+
{
|
|
1280
|
+
"ok": true,
|
|
1281
|
+
"success": 2,
|
|
1282
|
+
"failed": 0,
|
|
1283
|
+
"countDelta": [
|
|
1284
|
+
{
|
|
1285
|
+
"account": "you@gmail.com",
|
|
1286
|
+
"mailbox": "INBOX",
|
|
1287
|
+
"before": 412,
|
|
1288
|
+
"after": 408,
|
|
1289
|
+
"expected": 2,
|
|
1290
|
+
"observed": 4,
|
|
1291
|
+
"status": "over"
|
|
1292
|
+
}
|
|
1293
|
+
]
|
|
1294
|
+
}
|
|
1295
|
+
```
|
|
1296
|
+
|
|
1297
|
+
`status` is deliberately four-valued rather than a pass/fail flag:
|
|
1298
|
+
|
|
1299
|
+
| `status` | Meaning | Warns? |
|
|
1300
|
+
|----------|---------|--------|
|
|
1301
|
+
| `match` | Exactly as many messages left the mailbox as the operation acted on. | No |
|
|
1302
|
+
| `over` | **More** left than were operated on. Messages are unaccounted for. | **Yes** |
|
|
1303
|
+
| `under` | **Fewer** left than expected. | No |
|
|
1304
|
+
| `unknown` | No comparison was possible: either Mail would not report a count (`before`/`after` null) or there is no predictable expectation (`expected` null — see the self-move rule below). | No |
|
|
1305
|
+
|
|
1306
|
+
#### What an `over` warning does and does not tell you
|
|
1307
|
+
|
|
1308
|
+
Only `over` produces a warning in the tool's text response. Be precise about what
|
|
1309
|
+
that warning proves, because a warning is useful only for as long as it is
|
|
1310
|
+
trusted:
|
|
1311
|
+
|
|
1312
|
+
- **It establishes** that more messages left the source mailbox across the window
|
|
1313
|
+
of the operation than the operation accounted for. That is the data-loss
|
|
1314
|
+
direction, and it is the #155 signature.
|
|
1315
|
+
- **It does not establish that this server removed them.** The reading is a
|
|
1316
|
+
before/after pair around a window, so anything else that removes mail from that
|
|
1317
|
+
mailbox inside the window reads identically: a Mail.app rule firing mid-batch,
|
|
1318
|
+
a server-side filter, another client (phone, webmail, a second Mail.app)
|
|
1319
|
+
deleting or moving, or an IMAP expunge landing between the two counts.
|
|
1320
|
+
|
|
1321
|
+
**Concurrent departure is the benign cause to rule out first**, and the warning
|
|
1322
|
+
text says so. What the asymmetry argument actually buys is the other half:
|
|
1323
|
+
concurrent *arrivals* cannot produce `over`, because a message arriving
|
|
1324
|
+
mid-operation *raises* the after-count and biases the reading toward `under`.
|
|
1325
|
+
That is why `over` is the interesting direction — a strong signal, not a proof.
|
|
1326
|
+
|
|
1327
|
+
Setting `APPLE_MAIL_MCP_AUDIT_LOG` is what settles which one you have: the
|
|
1328
|
+
collateral diff below **names** the messages that disappeared, and "the
|
|
1329
|
+
newsletter my rule files every morning" is a very different report from a message
|
|
1330
|
+
nothing should have touched.
|
|
1331
|
+
|
|
1332
|
+
`under`, by contrast, is routine: an account that flags deletions rather than
|
|
1333
|
+
removing them leaves the message in place and the count does not move, and a
|
|
1334
|
+
Gmail label mailbox can behave the same way while the delete genuinely succeeded.
|
|
1335
|
+
A warning that fires on every ordinary Gmail delete would be ignored exactly when
|
|
1336
|
+
it matters, so `under` is **reported** in `countDelta` (with a `note` explaining
|
|
1337
|
+
it) and never warned about.
|
|
1338
|
+
|
|
1339
|
+
Three more honesty rules:
|
|
1340
|
+
|
|
1341
|
+
- The **expectation is per source mailbox**. On a Gmail label store, deleting the
|
|
1342
|
+
`INBOX` copy drops the `\Inbox` label and deleting the `[Gmail]/All Mail` copy
|
|
1343
|
+
trashes the message — different operations, but either way the mailbox the ids
|
|
1344
|
+
came from loses exactly one entry per id. That is what is compared. A move's
|
|
1345
|
+
**destination** count is not checked.
|
|
1346
|
+
- A move whose destination **is** the source mailbox is **not compared at all**.
|
|
1347
|
+
No message should leave, but what Mail does to the count when a message is
|
|
1348
|
+
re-filed into the mailbox it already occupies is unspecified — so `expected` is
|
|
1349
|
+
`null`, `status` is `unknown`, `note` says why, and no warning is raised. A
|
|
1350
|
+
warning computed against a guessed expectation would fire on an operation that
|
|
1351
|
+
did exactly what it was asked to, which is the one thing this instrumentation
|
|
1352
|
+
must never do.
|
|
1353
|
+
|
|
1354
|
+
**This makes a self-move a blind spot for the always-on layer**, and the cost is
|
|
1355
|
+
worth stating plainly: if messages genuinely do disappear during a self-move,
|
|
1356
|
+
nothing warns you, because there was no expectation to compare against. `status`
|
|
1357
|
+
is `unknown` rather than `match`, so the result does not claim the operation was
|
|
1358
|
+
clean — but it does not flag it either. The collateral diff still names anything
|
|
1359
|
+
that vanished, so **enable `APPLE_MAIL_MCP_AUDIT_LOG` if you need coverage for
|
|
1360
|
+
same-mailbox moves.**
|
|
1361
|
+
- A **repeated id is one message**. The batch tools operate on each distinct id
|
|
1362
|
+
once and return one result per distinct id, so `success` counts messages rather
|
|
1363
|
+
than list positions — and `expected` stays comparable with the mailbox instead
|
|
1364
|
+
of double-counting a duplicate into a false `over`.
|
|
1365
|
+
|
|
1366
|
+
`imap:` ids are not reconciled. An IMAP UID names exactly one message in exactly
|
|
1367
|
+
one mailbox, so the mis-targeting class this exists for cannot occur there; a
|
|
1368
|
+
batch of only `imap:` ids returns no `countDelta` rather than a fabricated one.
|
|
1369
|
+
|
|
1370
|
+
### `APPLE_MAIL_MCP_AUDIT_LOG` — opt-in forensic log
|
|
1371
|
+
|
|
1372
|
+
| Variable | Default | Description |
|
|
1373
|
+
|----------|---------|-------------|
|
|
1374
|
+
| `APPLE_MAIL_MCP_AUDIT_LOG` | *(off)* | Absolute path to an NDJSON file. Setting it enables the audit log **and** the collateral diff below |
|
|
1375
|
+
| `APPLE_MAIL_MCP_AUDIT_SUBJECTS` | `0` | Set `1` to also record message **subjects**. Separate, deliberate second opt-in — see Privacy |
|
|
1376
|
+
| `APPLE_MAIL_MCP_AUDIT_SNAPSHOT_MAX` | `2000` | Skip the collateral snapshot for mailboxes larger than this many messages. `0` disables the snapshot entirely |
|
|
1377
|
+
|
|
1378
|
+
When set, each destructive operation appends **one JSON object per line**
|
|
1379
|
+
containing: timestamp, tool name, server version, the arguments it was called
|
|
1380
|
+
with, the **pre-image** of every message it resolved (account, mailbox, numeric
|
|
1381
|
+
id, RFC Message-ID, `date received`), the per-id outcome (`ok` / `notfound` /
|
|
1382
|
+
`error` + reason), the `countDelta` above, and the collateral diff.
|
|
1383
|
+
|
|
1384
|
+
The pre-image is the part that matters after the fact: a Mail.app numeric id is
|
|
1385
|
+
unique only within a mailbox and is reused, so on its own it proves nothing about
|
|
1386
|
+
which message was acted on. The RFC Message-ID does.
|
|
1387
|
+
|
|
1388
|
+
**The record is framed against its own contents.** The Message-ID and (when
|
|
1389
|
+
enabled) the subject are written by whoever sent the mail, so the control
|
|
1390
|
+
characters this server frames records with are stripped out of every such value
|
|
1391
|
+
before it is written — a Message-ID crafted to close a record and open a forged
|
|
1392
|
+
one cannot invent evidence in the log it is being recorded in. The same stripping
|
|
1393
|
+
is applied to every other value read out of Mail at runtime (`date received`,
|
|
1394
|
+
mailbox and account names, the text of an error Mail raised, the candidate list
|
|
1395
|
+
behind an "ambiguous id" refusal), so no emitter is an exception. A value that
|
|
1396
|
+
arrives with those characters in it (which a well-formed Message-ID never does)
|
|
1397
|
+
is logged with each of them replaced by `U+FFFD`, so the record shows that the
|
|
1398
|
+
value was altered rather than quietly shortening it.
|
|
1399
|
+
|
|
1400
|
+
### Collateral identification — which messages actually disappeared
|
|
1401
|
+
|
|
1402
|
+
Also gated on `APPLE_MAIL_MCP_AUDIT_LOG`. The mailbox's `(numeric id, Message-ID)`
|
|
1403
|
+
pairs are captured before and after the mutation and diffed, so the log names
|
|
1404
|
+
every message that left — **including ones the caller never listed**:
|
|
1405
|
+
|
|
1406
|
+
```json
|
|
1407
|
+
{
|
|
1408
|
+
"account": "you@gmail.com",
|
|
1409
|
+
"mailbox": "INBOX",
|
|
1410
|
+
"snapshot": "ok",
|
|
1411
|
+
"disappeared": [
|
|
1412
|
+
{ "id": "75811", "messageId": "a@example.com" },
|
|
1413
|
+
{ "id": "75814", "messageId": "d@example.com" }
|
|
1414
|
+
],
|
|
1415
|
+
"unrequested": [{ "id": "75814", "messageId": "d@example.com" }],
|
|
1416
|
+
"appeared": []
|
|
1417
|
+
}
|
|
1418
|
+
```
|
|
1419
|
+
|
|
1420
|
+
A non-empty `unrequested` **is** the #155 symptom, with names attached. Please
|
|
1421
|
+
attach that line to the issue if you ever see one.
|
|
1422
|
+
|
|
1423
|
+
`id` is always the plain decimal id you passed, even on a mailbox whose ids
|
|
1424
|
+
exceed AppleScript's 2^29 integer range (where Mail hands them back as
|
|
1425
|
+
`9.99999999E+8`). That normalisation is also what keeps `unrequested` truthful:
|
|
1426
|
+
compared in the raw form, a message you explicitly asked to delete would be
|
|
1427
|
+
reported here as collateral.
|
|
1428
|
+
|
|
1429
|
+
This costs one bulk property read per snapshot and is O(mailbox size), so it is
|
|
1430
|
+
bounded by `APPLE_MAIL_MCP_AUDIT_SNAPSHOT_MAX`. When the bound bites, the record
|
|
1431
|
+
says so explicitly (`"snapshot": "skipped"` with a reason) rather than omitting
|
|
1432
|
+
the field — a silently skipped snapshot would read as "nothing collateral
|
|
1433
|
+
happened", which is worse than no snapshot at all. `countDelta` is unaffected by
|
|
1434
|
+
the skip and still reconciles the counts.
|
|
1435
|
+
|
|
1436
|
+
### Privacy, and what the file costs you
|
|
1437
|
+
|
|
1438
|
+
- **Default:** identifying metadata only — Message-ID, date, mailbox, account,
|
|
1439
|
+
numeric id. Enough to say *which* message, nothing about what it says.
|
|
1440
|
+
- **Subjects are behind their own opt-in** (`APPLE_MAIL_MCP_AUDIT_SUBJECTS=1`)
|
|
1441
|
+
because a subject line is frequently the entire sensitive payload, and it is
|
|
1442
|
+
not needed to diagnose #155.
|
|
1443
|
+
- **Message bodies are never logged, under any setting.**
|
|
1444
|
+
- The file **grows without bound** and is never rotated or truncated by this
|
|
1445
|
+
server. Point it somewhere you control, and delete it when you are done. It is
|
|
1446
|
+
written with your user's permissions, wherever you point it; there is no
|
|
1447
|
+
default location precisely so that turning it on is a deliberate act.
|
|
1448
|
+
- Writes go to that file and nowhere else. Diagnostics go to **stderr**; nothing
|
|
1449
|
+
is ever written to stdout, which is the JSON-RPC transport.
|
|
1450
|
+
|
|
1451
|
+
---
|
|
1452
|
+
|
|
1245
1453
|
## Usage Patterns
|
|
1246
1454
|
|
|
1247
1455
|
### Basic Workflow
|