apple-mail-mcp 2.10.33 → 2.11.0

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 +61 -12
  2. package/build/index.js +29 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1319,14 +1319,26 @@ returns the comparison in `structuredContent`:
1319
1319
  }
1320
1320
  ```
1321
1321
 
1322
- `status` is deliberately four-valued rather than a pass/fail flag:
1322
+ `status` is deliberately not a pass/fail flag:
1323
1323
 
1324
1324
  | `status` | Meaning | Warns? |
1325
1325
  |----------|---------|--------|
1326
1326
  | `match` | Exactly as many messages left the mailbox as the operation acted on. | No |
1327
1327
  | `over` | **More** left than were operated on. Messages are unaccounted for. | **Yes** |
1328
- | `under` | **Fewer** left than expected. | No |
1329
- | `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 |
1328
+ | `unknown` | No comparison this server is willing to assert. `unknownReason` says which of four situations produced it. | No |
1329
+
1330
+ `unknownReason` distinguishes four cases that are *not* interchangeable:
1331
+
1332
+ | `unknownReason` | Meaning |
1333
+ |-----------------|---------|
1334
+ | `count-unreadable` | Mail would not report a count at all (`before`/`after` null). |
1335
+ | `no-expectation` | No expectation is predictable, so no comparison exists — a move whose destination **is** the source mailbox. |
1336
+ | `count-did-not-move` | The count did not move. On a store that flags deletions instead of removing them this is the **ordinary, correct** reading for an operation that fully succeeded. |
1337
+ | `count-partial` | The count moved, but by less than the operation accounted for. A flag-only store cannot produce this, which is why it is worth telling apart. |
1338
+
1339
+ > **`under` was removed in 2.11.0.** It used to mean "fewer left than expected"
1340
+ > and was documented as routine. Field evidence retired it — see
1341
+ > [Why `observed` is a lower bound](#why-observed-is-a-lower-bound-155).
1330
1342
 
1331
1343
  #### What an `over` warning does and does not tell you
1332
1344
 
@@ -1346,7 +1358,7 @@ trusted:
1346
1358
  **Concurrent departure is the benign cause to rule out first**, and the warning
1347
1359
  text says so. What the asymmetry argument actually buys is the other half:
1348
1360
  concurrent *arrivals* cannot produce `over`, because a message arriving
1349
- mid-operation *raises* the after-count and biases the reading toward `under`.
1361
+ mid-operation *raises* the after-count and biases the reading short.
1350
1362
  That is why `over` is the interesting direction — a strong signal, not a proof.
1351
1363
 
1352
1364
  Setting `APPLE_MAIL_MCP_AUDIT_LOG` is what settles which one you have: the
@@ -1354,12 +1366,42 @@ collateral diff below **names** the messages that disappeared, and "the
1354
1366
  newsletter my rule files every morning" is a very different report from a message
1355
1367
  nothing should have touched.
1356
1368
 
1357
- `under`, by contrast, is routine: an account that flags deletions rather than
1358
- removing them leaves the message in place and the count does not move, and a
1359
- Gmail label mailbox can behave the same way while the delete genuinely succeeded.
1360
- A warning that fires on every ordinary Gmail delete would be ignored exactly when
1361
- it matters, so `under` is **reported** in `countDelta` (with a `note` explaining
1362
- it) and never warned about.
1369
+ #### Why `observed` is a lower bound (#155)
1370
+
1371
+ **`observed` is the movement of Mail's count. It is a lower bound on how many
1372
+ messages left, not a count of how many left.**
1373
+
1374
+ On iCloud, @scottstern0325 ran the check that settled this: for two batches
1375
+ reporting `observed: 0`, the messages were located **in Trash**, matched by
1376
+ `date received` + sender against the audit log's pre-image. The deletes had
1377
+ happened. Mail's count had not caught up. Across four readings — 0 of 4, 0 of 1
1378
+ (a *single-id* delete), 15 of 16, and 14 of 15 — the shortfall bore no relation
1379
+ to batch size, which is what a lagging count looks like and not what a
1380
+ store-behaviour rule looks like.
1381
+
1382
+ So a short reading is not evidence about your operation. **The per-id outcomes
1383
+ are what report success; this number is not. Do not retry on the strength of
1384
+ it** — that is how a message gets deleted twice.
1385
+
1386
+ Two things follow:
1387
+
1388
+ - A count that does not move at all is still the ordinary reading on a store that
1389
+ flags deletions instead of removing them (Gmail label mailboxes, IMAP accounts
1390
+ with "move deleted messages to Trash" off). It reports
1391
+ `unknownReason: "count-did-not-move"` and says so, and it is never warned about
1392
+ — a warning that fires on every ordinary Gmail delete would be ignored exactly
1393
+ when it matters.
1394
+ - **To confirm where messages went, match them at the destination by `date
1395
+ received` plus sender — not by the numeric ids you passed.** Ids are renumbered
1396
+ by the move and do not survive it.
1397
+
1398
+ **Removed in 2.11.0: the "reported success with no observed effect" warning.**
1399
+ Shipped in 2.10.30, it fired when the count was flat, the snapshot read cleanly
1400
+ and nothing had disappeared. Its premise was that the snapshot corroborated the
1401
+ count — but both are read back-to-back in the same script, and the record that
1402
+ prompted it turns out to have had *both* instruments stale at once. It therefore
1403
+ fired on stores that had done exactly what they were asked. It is gone rather
1404
+ than narrowed; `over` is the only surviving assertion.
1363
1405
 
1364
1406
  Three more honesty rules:
1365
1407
 
@@ -1489,8 +1531,15 @@ a hole, because a wrong name here is worse than a missing one:
1489
1531
  | both | withheld | withheld |
1490
1532
 
1491
1533
  **An absent field means "not computable", never "empty".** Check
1492
- `"snapshot": "ok"` before reading `disappeared` as a clean bill of health — the
1493
- false-ok warning does exactly that, so a `partial` snapshot never triggers it.
1534
+ `"snapshot": "ok"` before reading `disappeared` as a clean bill of health.
1535
+
1536
+ > ⚠️ **`"ok"` is necessary, not sufficient.** The enumeration's range is bounded
1537
+ > by Mail's own message count, and #155 established that count can lag the
1538
+ > mutation. A count reading **low** truncates the enumeration silently — the
1539
+ > unread tail is never requested, so it never registers as a failed slice and the
1540
+ > status still says `"ok"` — and messages past that bound would then look like
1541
+ > they disappeared. Until that is fixed, treat a `disappeared` entry as a lead to
1542
+ > check, not a proof.
1494
1543
 
1495
1544
  ### Privacy, and what the file costs you
1496
1545
 
package/build/index.js CHANGED
@@ -78854,17 +78854,8 @@ function countDeltaWarning(d) {
78854
78854
  const where = d.account ? `"${d.mailbox}" in account "${d.account}"` : `"${d.mailbox}"`;
78855
78855
  return `\u26A0\uFE0F Effect mismatch in ${where}: ${d.observed} message(s) left the mailbox but only ${d.expected} were operated on (count ${d.before} \u2192 ${d.after}). ${extra} message(s) are unaccounted for. Anything else removing mail from this mailbox at the same moment \u2014 a Mail rule, a server-side filter, another client, an IMAP expunge \u2014 reads the same way, so rule that out first. If nothing else was touching it, this is the signature of https://github.com/sweetrb/apple-mail-mcp/issues/155 \u2014 please report it there, and set ${AUDIT_LOG_ENV}=/path/to/audit.ndjson to capture which messages disappeared.`;
78856
78856
  }
78857
- function falseOkWarning(d, collateral) {
78858
- if (d.status !== "under" || d.expected === null) return null;
78859
- if (d.observed !== 0 || d.expected <= 0) return null;
78860
- if (!collateral || collateral.snapshot !== "ok") return null;
78861
- if ((collateral.disappeared?.length ?? 0) !== 0) return null;
78862
- const where = d.account ? `"${d.mailbox}" in account "${d.account}"` : `"${d.mailbox}"`;
78863
- return `\u26A0\uFE0F Reported success with no observed effect in ${where}: ${d.expected} message(s) were operated on and reported ok, but the mailbox count did not move (${d.before} \u2192 ${d.after}) and the collateral snapshot \u2014 which read this mailbox successfully \u2014 shows no message left it. Either the operation silently did nothing, or Mail's count and listing are both stale and the messages did move. Check the destination (Trash for a delete) before assuming either: if they are there, this is a measurement-timing bug; if they are still in the source mailbox minutes later, the operation failed while reporting success. Please report at https://github.com/sweetrb/apple-mail-mcp/issues/155 with ${AUDIT_LOG_ENV}=/path/to/audit.ndjson set.`;
78864
- }
78865
78857
  function reconciliationWarnings(report) {
78866
- const collateralFor = (d) => report.collateral.find((c) => c.account === d.account && c.mailbox === d.mailbox);
78867
- return report.countDeltas.flatMap((d) => [countDeltaWarning(d), falseOkWarning(d, collateralFor(d))]).filter((w) => w !== null);
78858
+ return report.countDeltas.map((d) => countDeltaWarning(d)).filter((w) => w !== null);
78868
78859
  }
78869
78860
 
78870
78861
  // src/services/appleMailManager.ts
@@ -79142,6 +79133,8 @@ function canonicalNumericId(raw) {
79142
79133
  return Number.isFinite(n) ? String(n) : trimmed;
79143
79134
  }
79144
79135
  var SELF_MOVE_NOTE = "Destination is the source mailbox, so no message should leave it. What Mail does to the count when a message is re-filed into the mailbox it already occupies is unspecified, so there is no expected delta to compare against: this mailbox is reported without a comparison and is never warned about.";
79136
+ var COUNT_UNMOVED_NOTE = `Mail's count did not move. This is the ordinary reading on a store that flags deletions instead of removing them (Gmail label mailboxes and IMAP accounts with "move deleted messages to Trash" off), where the message stays put and the operation still fully succeeded. It can also mean Mail's count simply had not caught up yet. The per-id outcomes are what report success; this number is not, so do not retry on the strength of it.`;
79137
+ var COUNT_PARTIAL_NOTE = `Mail's count moved by less than this operation accounted for. That is a LOWER BOUND on what left, not a count of what left: Mail has been observed reporting a stale count for a delete it had already performed (issue #155), and new mail arriving mid-operation reads the same way. No claim is made either way. To confirm where the messages went, match them at the destination by "date received" plus sender \u2014 NOT by the numeric ids you passed, which are renumbered by the move and do not survive it.`;
79145
79138
  function snapshotKey(entry) {
79146
79139
  return `${entry.id}\0${entry.messageId}`;
79147
79140
  }
@@ -79679,10 +79672,24 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79679
79672
  const observed = readable ? m.before - m.after : null;
79680
79673
  const note = noteFor(m.account, m.mailbox);
79681
79674
  let status;
79682
- if (!readable || m.expected === null) status = "unknown";
79683
- else if (observed === m.expected) status = "match";
79684
- else if ((observed ?? 0) > m.expected) status = "over";
79685
- else status = "under";
79675
+ let unknownReason;
79676
+ if (!readable) {
79677
+ status = "unknown";
79678
+ unknownReason = "count-unreadable";
79679
+ } else if (m.expected === null) {
79680
+ status = "unknown";
79681
+ unknownReason = "no-expectation";
79682
+ } else if (observed === m.expected) {
79683
+ status = "match";
79684
+ } else if ((observed ?? 0) > m.expected) {
79685
+ status = "over";
79686
+ } else if (observed === 0) {
79687
+ status = "unknown";
79688
+ unknownReason = "count-did-not-move";
79689
+ } else {
79690
+ status = "unknown";
79691
+ unknownReason = "count-partial";
79692
+ }
79686
79693
  return {
79687
79694
  account: m.account,
79688
79695
  mailbox: m.mailbox,
@@ -79691,11 +79698,11 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79691
79698
  expected: m.expected,
79692
79699
  observed,
79693
79700
  status,
79701
+ ...unknownReason ? { unknownReason } : {},
79694
79702
  ...note ? { note } : {},
79695
- ...status === "unknown" && readable === false ? { note: note ?? "Mail did not report a message count for this mailbox" } : {},
79696
- ...status === "under" && !note ? {
79697
- note: "Fewer messages left the mailbox than were operated on. This is normal on a store that flags deletions instead of removing them (and when new mail arrives mid-operation), so it is reported but not warned about."
79698
- } : {}
79703
+ ...unknownReason === "count-unreadable" ? { note: note ?? "Mail did not report a message count for this mailbox" } : {},
79704
+ ...unknownReason === "count-did-not-move" && !note ? { note: COUNT_UNMOVED_NOTE } : {},
79705
+ ...unknownReason === "count-partial" && !note ? { note: COUNT_PARTIAL_NOTE } : {}
79699
79706
  };
79700
79707
  });
79701
79708
  const preImages = [];
@@ -85303,7 +85310,10 @@ var COUNT_DELTA_OUTPUT_SCHEMA = external_exports.array(
85303
85310
  // `status: "unknown"`, and never with a warning.
85304
85311
  expected: external_exports.number().nullable().optional(),
85305
85312
  observed: external_exports.number().nullable().optional(),
85306
- status: external_exports.enum(["match", "over", "under", "unknown"]).optional(),
85313
+ status: external_exports.enum(["match", "over", "unknown"]).optional(),
85314
+ // Declared explicitly: the SDK stamps additionalProperties:false on a bare
85315
+ // zod shape, so an undeclared key makes the CLIENT reject the result.
85316
+ unknownReason: external_exports.enum(["count-unreadable", "no-expectation", "count-did-not-move", "count-partial"]).optional(),
85307
85317
  note: external_exports.string().optional()
85308
85318
  })
85309
85319
  ).optional();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.10.33",
3
+ "version": "2.11.0",
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",