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.
Files changed (3) hide show
  1. package/README.md +208 -0
  2. package/build/index.js +832 -77
  3. 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