apple-mail-mcp 2.14.1 → 2.15.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 +17 -6
  2. package/build/index.js +78 -34
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1444,12 +1444,23 @@ Three more honesty rules:
1444
1444
  than list positions — and `expected` stays comparable with the mailbox instead
1445
1445
  of double-counting a duplicate into a false `over`.
1446
1446
 
1447
- `imap:` ids are not reconciled by `countDelta`. An IMAP UID names exactly one
1448
- message in exactly one mailbox, so the mis-targeting class this exists for cannot
1449
- occur there; a batch of only `imap:` ids returns no `countDelta` rather than a
1450
- fabricated one.
1451
-
1452
- They carry their own post-condition check instead. `delete-message` and
1447
+ **`imap:` ids are reconciled too, as of 2.15.0.** `batch-delete-messages` and
1448
+ `batch-move-messages` return the same `countDelta` structure on the IMAP path, so
1449
+ one shape covers both backends and a mixed batch reports an entry per source
1450
+ mailbox from whichever backend handled it. The entries are **concatenated, never
1451
+ summed** — Mail's own count can lag (#155) while the server's `STATUS` cannot, and
1452
+ averaging the two would hide which reading you were looking at.
1453
+
1454
+ Only the operations that actually remove messages from their source reconcile.
1455
+ `batch-mark-as-read` and the flag tools change no count, so emitting
1456
+ `expected: N, observed: 0` for them would manufacture an alarm; they report no
1457
+ `countDelta` at all.
1458
+
1459
+ Note the mis-targeting class `countDelta` was originally built for cannot occur
1460
+ on the IMAP path — a UID names exactly one message in exactly one mailbox — so
1461
+ there the value is effect confirmation rather than target confirmation.
1462
+
1463
+ Single-message tools carry a post-condition check instead. `delete-message` and
1453
1464
  `move-message` on an `imap:` id return a **`verification`** object in
1454
1465
  `structuredContent`:
1455
1466
 
package/build/index.js CHANGED
@@ -78828,6 +78828,14 @@ function auditSnapshotChunk() {
78828
78828
  if (!Number.isFinite(n) || n < 1) return DEFAULT_SNAPSHOT_CHUNK;
78829
78829
  return Math.floor(n);
78830
78830
  }
78831
+ function classifyCountStatus(readable, expected, observed) {
78832
+ if (!readable) return { status: "unknown", unknownReason: "count-unreadable" };
78833
+ if (expected === null) return { status: "unknown", unknownReason: "no-expectation" };
78834
+ if (observed === expected) return { status: "match" };
78835
+ if ((observed ?? 0) > expected) return { status: "over" };
78836
+ if (observed === 0) return { status: "unknown", unknownReason: "count-did-not-move" };
78837
+ return { status: "unknown", unknownReason: "count-partial" };
78838
+ }
78831
78839
  function writeAuditRecord(record2) {
78832
78840
  const path = auditLogPath();
78833
78841
  if (!path) return;
@@ -79771,25 +79779,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79771
79779
  const readable = m.before >= 0 && m.after >= 0;
79772
79780
  const observed = readable ? m.before - m.after : null;
79773
79781
  const note = noteFor(m.account, m.mailbox);
79774
- let status;
79775
- let unknownReason;
79776
- if (!readable) {
79777
- status = "unknown";
79778
- unknownReason = "count-unreadable";
79779
- } else if (m.expected === null) {
79780
- status = "unknown";
79781
- unknownReason = "no-expectation";
79782
- } else if (observed === m.expected) {
79783
- status = "match";
79784
- } else if ((observed ?? 0) > m.expected) {
79785
- status = "over";
79786
- } else if (observed === 0) {
79787
- status = "unknown";
79788
- unknownReason = "count-did-not-move";
79789
- } else {
79790
- status = "unknown";
79791
- unknownReason = "count-partial";
79792
- }
79782
+ const { status, unknownReason } = classifyCountStatus(readable, m.expected, observed);
79793
79783
  return {
79794
79784
  account: m.account,
79795
79785
  mailbox: m.mailbox,
@@ -84475,7 +84465,15 @@ async function imapFetchAttachment(id, attachmentName, deps = {}) {
84475
84465
  }
84476
84466
  });
84477
84467
  }
84478
- async function imapBatch(ids, deps, op) {
84468
+ async function mailboxCount(client, path) {
84469
+ try {
84470
+ const st = await client.status(path, { messages: true });
84471
+ return typeof st.messages === "number" ? st.messages : null;
84472
+ } catch {
84473
+ return null;
84474
+ }
84475
+ }
84476
+ async function imapBatch(ids, deps, op, opts = {}) {
84479
84477
  const groups = /* @__PURE__ */ new Map();
84480
84478
  const errors = [];
84481
84479
  let failed = 0;
@@ -84492,15 +84490,39 @@ async function imapBatch(ids, deps, op) {
84492
84490
  groups.set(key, g);
84493
84491
  }
84494
84492
  let success = 0;
84493
+ const countDelta = [];
84495
84494
  for (const g of groups.values()) {
84496
84495
  try {
84497
84496
  await useClient(depsForAccount(g.account, deps), async (client) => {
84497
+ const before = opts.reconcile ? await mailboxCount(client, g.path) : null;
84498
84498
  const lock = await client.getMailboxLock(g.path);
84499
84499
  try {
84500
84500
  await op(client, g.uids, g.path);
84501
84501
  } finally {
84502
84502
  lock.release();
84503
84503
  }
84504
+ if (!opts.reconcile) return;
84505
+ const after = await mailboxCount(client, g.path);
84506
+ const readable = before !== null && after !== null;
84507
+ const observed = readable ? before - after : null;
84508
+ const { status, unknownReason } = classifyCountStatus(readable, g.uids.length, observed);
84509
+ countDelta.push({
84510
+ account: g.account,
84511
+ mailbox: g.path,
84512
+ before,
84513
+ after,
84514
+ expected: g.uids.length,
84515
+ observed,
84516
+ status,
84517
+ ...unknownReason ? { unknownReason } : {},
84518
+ ...unknownReason === "count-unreadable" ? { note: "The server did not answer STATUS for this mailbox" } : {},
84519
+ ...unknownReason === "count-did-not-move" ? {
84520
+ note: `The mailbox count did not move. On a label store (Gmail) a message can stay visible in an all-mail view after being moved out of a label, so this is not by itself evidence the operation failed \u2014 check the destination.`
84521
+ } : {},
84522
+ ...unknownReason === "count-partial" ? {
84523
+ note: `Fewer messages left than were operated on. \`observed\` is a LOWER BOUND on what left, not a count of what left \u2014 a concurrent delivery to this mailbox masks departures one-for-one.`
84524
+ } : {}
84525
+ });
84504
84526
  });
84505
84527
  success += g.uids.length;
84506
84528
  } catch (e) {
@@ -84508,7 +84530,7 @@ async function imapBatch(ids, deps, op) {
84508
84530
  errors.push(`${g.path}: ${errText(e)}`);
84509
84531
  }
84510
84532
  }
84511
- return { success, failed, errors };
84533
+ return { success, failed, errors, ...countDelta.length ? { countDelta } : {} };
84512
84534
  }
84513
84535
  var imapBatchMarkRead = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids) => {
84514
84536
  assertMutated(
@@ -84543,17 +84565,27 @@ var imapBatchUnflag = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids) =
84543
84565
  `IMAP unflag of ${uids.length} message(s)`
84544
84566
  );
84545
84567
  });
84546
- var imapBatchDelete = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids, path) => {
84547
- await trashUids(c, uids, path);
84548
- });
84568
+ var imapBatchDelete = (ids, deps = {}) => imapBatch(
84569
+ ids,
84570
+ deps,
84571
+ async (c, uids, path) => {
84572
+ await trashUids(c, uids, path);
84573
+ },
84574
+ { reconcile: true }
84575
+ );
84549
84576
  function imapBatchMove(ids, destMailbox, deps = {}) {
84550
- return imapBatch(ids, deps, async (c, uids) => {
84551
- const dest = await findMailboxPathOrThrow(c, destMailbox) ?? resolveMailboxPath(destMailbox, "list");
84552
- assertMutated(
84553
- await c.messageMove(uids, dest, { uid: true }),
84554
- `IMAP move of ${uids.length} message(s) to "${dest}"`
84555
- );
84556
- });
84577
+ return imapBatch(
84578
+ ids,
84579
+ deps,
84580
+ async (c, uids) => {
84581
+ const dest = await findMailboxPathOrThrow(c, destMailbox) ?? resolveMailboxPath(destMailbox, "list");
84582
+ assertMutated(
84583
+ await c.messageMove(uids, dest, { uid: true }),
84584
+ `IMAP move of ${uids.length} message(s) to "${dest}"`
84585
+ );
84586
+ },
84587
+ { reconcile: true }
84588
+ );
84557
84589
  }
84558
84590
  function senderName(from) {
84559
84591
  const a = from?.[0];
@@ -84752,13 +84784,15 @@ async function hybridBatchCounts(ids, appleFn, imapFn) {
84752
84784
  fail += res.length - s;
84753
84785
  errors.push(...res.filter((r) => !r.success && r.error).map((r) => r.error));
84754
84786
  }
84787
+ let countDelta;
84755
84788
  if (imapIds.length > 0) {
84756
84789
  const r = await imapFn(imapIds);
84757
84790
  success += r.success;
84758
84791
  fail += r.failed;
84759
84792
  errors.push(...r.errors);
84793
+ if (r.countDelta?.length) countDelta = r.countDelta;
84760
84794
  }
84761
- return { success, fail, errors };
84795
+ return { success, fail, errors, ...countDelta ? { countDelta } : {} };
84762
84796
  }
84763
84797
  function distinctErrors(errors) {
84764
84798
  return [...new Set(errors.filter(Boolean))];
@@ -84797,6 +84831,10 @@ ${warnings.join("\n")}` : "";
84797
84831
  function toManagerScope(args) {
84798
84832
  return { account: args.sourceAccount, mailbox: args.sourceMailbox };
84799
84833
  }
84834
+ function mergeCountDeltas(apple, imap) {
84835
+ const all = [...apple ?? [], ...imap ?? []];
84836
+ return all.length ? { countDelta: all } : {};
84837
+ }
84800
84838
  async function runBatchDelete(deps, args) {
84801
84839
  const { ids, sourceMailbox, sourceAccount } = args;
84802
84840
  let forensics = { warnings: [] };
@@ -84820,7 +84858,10 @@ async function runBatchDelete(deps, args) {
84820
84858
  allFailed: (n) => `Failed to delete all ${n} message(s)`,
84821
84859
  partial: (ok, failed) => `Deleted ${ok} message(s), ${failed} failed`
84822
84860
  },
84823
- forensics.countDelta ? { countDelta: forensics.countDelta } : {},
84861
+ // #181: merge both backends' reconciliation. An AppleScript-only batch is
84862
+ // unchanged; an IMAP-only one now reports a delta where it previously
84863
+ // reported nothing; a mixed batch reports both, per source mailbox.
84864
+ mergeCountDeltas(forensics.countDelta, counts.countDelta),
84824
84865
  forensics.warnings
84825
84866
  );
84826
84867
  }
@@ -84854,7 +84895,10 @@ async function runBatchMove(deps, args) {
84854
84895
  allFailed: (n) => `Failed to move all ${n} message(s)`,
84855
84896
  partial: (ok, failed) => `Moved ${ok} message(s) to "${mailbox}", ${failed} failed`
84856
84897
  },
84857
- { mailbox, ...forensics.countDelta ? { countDelta: forensics.countDelta } : {} },
84898
+ {
84899
+ mailbox,
84900
+ ...mergeCountDeltas(forensics.countDelta, counts.countDelta)
84901
+ },
84858
84902
  forensics.warnings
84859
84903
  );
84860
84904
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.14.1",
3
+ "version": "2.15.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",