apple-mail-mcp 2.12.0 → 2.13.1

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 +29 -3
  2. package/build/index.js +94 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1430,9 +1430,35 @@ Three more honesty rules:
1430
1430
  than list positions — and `expected` stays comparable with the mailbox instead
1431
1431
  of double-counting a duplicate into a false `over`.
1432
1432
 
1433
- `imap:` ids are not reconciled. An IMAP UID names exactly one message in exactly
1434
- one mailbox, so the mis-targeting class this exists for cannot occur there; a
1435
- batch of only `imap:` ids returns no `countDelta` rather than a fabricated one.
1433
+ `imap:` ids are not reconciled by `countDelta`. An IMAP UID names exactly one
1434
+ message in exactly one mailbox, so the mis-targeting class this exists for cannot
1435
+ occur there; a batch of only `imap:` ids returns no `countDelta` rather than a
1436
+ fabricated one.
1437
+
1438
+ They carry their own post-condition check instead. `delete-message` and
1439
+ `move-message` on an `imap:` id return a **`verification`** object in
1440
+ `structuredContent`:
1441
+
1442
+ ```json
1443
+ {
1444
+ "verification": {
1445
+ "verdict": "verified",
1446
+ "how": "COPYUID: UID 5 arrived in \"Archive\" as UID 91"
1447
+ }
1448
+ }
1449
+ ```
1450
+
1451
+ | `verdict` | Meaning |
1452
+ |---|---|
1453
+ | `verified` | The effect was **observed** — either the server's UIDPLUS `COPYUID` named the message's new UID in the destination, or the UID is no longer in the source mailbox. |
1454
+ | `unverified` | The server **accepted** the command and nothing could confirm the effect. Populates `why`. |
1455
+
1456
+ `unverified` is **not a failure** and must not be rendered as one — it means
1457
+ "accepted, no observation either way". It is reported rather than hidden because
1458
+ an absent verification must never read as a successful one, the same rule the
1459
+ collateral diff follows. A message that is still in the source mailbox after an
1460
+ accepted move is reported `unverified` rather than failed, because a Gmail label
1461
+ store can legitimately keep a message visible in an all-mail view after a move.
1436
1462
 
1437
1463
  ### `APPLE_MAIL_MCP_AUDIT_LOG` — opt-in forensic log
1438
1464
 
package/build/index.js CHANGED
@@ -79503,6 +79503,35 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79503
79503
  end if
79504
79504
  set _sLo to _sHi + 1
79505
79505
  end repeat
79506
+ -- #179: the loop above is bounded by the count Mail JUST reported,
79507
+ -- and that count can lag the mailbox (#155). Positions past the bound
79508
+ -- are never requested, so \u2014 unlike a slice that failed \u2014 they leave
79509
+ -- no trace in _sMiss, and the record would claim a complete
79510
+ -- observation while every message past the bound looks like it
79511
+ -- disappeared. That is a FABRICATED finding with names attached,
79512
+ -- which is worse than the gap it papers over.
79513
+ --
79514
+ -- Probe exactly ONE position past the bound. One, not a slice: an
79515
+ -- out-of-range RANGE raises as a whole, so an over-requested slice
79516
+ -- could not distinguish "nothing there" from "count was low by more
79517
+ -- than a chunk". If a message is there, the count was low and the
79518
+ -- unread tail is recorded as a hole, which makes this snapshot
79519
+ -- PARTIAL under the existing rules and withholds the halves a
79520
+ -- truncation would poison.
79521
+ try
79522
+ set _sOverId to ((id of message (${countVar} + 1) of ${mbVar}) as string)
79523
+ -- A specifier that CLAMPS rather than raising hands back the LAST
79524
+ -- message instead of failing. That is not evidence of a truncation,
79525
+ -- so only an id this enumeration did not already record counts.
79526
+ set _sSeen to false
79527
+ repeat with _sP in _sPairs
79528
+ if (contents of _sP) starts with (_sOverId & "${SNAP_PAIR}") then set _sSeen to true
79529
+ end repeat
79530
+ if not _sSeen then
79531
+ if _sMiss is not "" then set _sMiss to _sMiss & ","
79532
+ set _sMiss to _sMiss & ((${countVar} + 1) as string) & "-end"
79533
+ end if
79534
+ end try
79506
79535
  if _sMiss is not "" then
79507
79536
  if (count of _sPairs) is 0 then
79508
79537
  set _sStatus to "unavailable"
@@ -83842,6 +83871,27 @@ function assertMutated(result, what) {
83842
83871
  if (!result) throw new Error(`${what}: server rejected the command (IMAP NO/BAD)`);
83843
83872
  return result;
83844
83873
  }
83874
+ async function verifyMoved(client, moved, uid, srcPath, destPath) {
83875
+ const newUid = moved.uidMap?.get(uid);
83876
+ if (newUid !== void 0) {
83877
+ return {
83878
+ verdict: "verified",
83879
+ how: `COPYUID: UID ${uid} arrived in "${destPath}" as UID ${newUid}`
83880
+ };
83881
+ }
83882
+ try {
83883
+ const stillThere = await client.fetchOne(String(uid), { uid: true }, { uid: true });
83884
+ if (!stillThere) {
83885
+ return { verdict: "verified", how: `UID ${uid} is no longer present in "${srcPath}"` };
83886
+ }
83887
+ return {
83888
+ verdict: "unverified",
83889
+ why: `the server accepted the MOVE, but UID ${uid} is still present in "${srcPath}" and this server does not advertise UIDPLUS, so arrival in "${destPath}" could not be confirmed. A Gmail label store can legitimately keep a message in an all-mail view after a move, so this is not reported as a failure`
83890
+ };
83891
+ } catch (e) {
83892
+ return { verdict: "unverified", why: `the post-move check could not run: ${errText(e)}` };
83893
+ }
83894
+ }
83845
83895
  var poolConnect = defaultConnect;
83846
83896
  var pools = /* @__PURE__ */ new Map();
83847
83897
  function poolKey(cfg) {
@@ -84180,11 +84230,16 @@ async function imapMoveMessageById(id, destMailbox, deps = {}) {
84180
84230
  const destPath = dest.kind === "found" ? dest.path : resolveMailboxPath(destMailbox, "list");
84181
84231
  const lock = await client.getMailboxLock(ref.path);
84182
84232
  try {
84183
- assertMutated(
84233
+ const moved = assertMutated(
84184
84234
  await client.messageMove([ref.uid], destPath, { uid: true }),
84185
84235
  `IMAP move of UID ${ref.uid} to "${destPath}"`
84186
84236
  );
84187
- return { success: true, info: `Moved UID ${ref.uid} to "${destPath}" via IMAP.` };
84237
+ const verification = await verifyMoved(client, moved, ref.uid, ref.path, destPath);
84238
+ return {
84239
+ success: true,
84240
+ info: verification.verdict === "verified" ? `Moved UID ${ref.uid} to "${destPath}" via IMAP (verified: ${verification.how}).` : `Moved UID ${ref.uid} to "${destPath}" via IMAP \u2014 UNVERIFIED: ${verification.why}.`,
84241
+ verification
84242
+ };
84188
84243
  } catch (e) {
84189
84244
  return {
84190
84245
  success: false,
@@ -84226,21 +84281,38 @@ async function trashUids(client, uids, srcPath) {
84226
84281
  );
84227
84282
  return { dest, expunged: true };
84228
84283
  }
84229
- assertMutated(
84284
+ const moved = assertMutated(
84230
84285
  await client.messageMove(uids, dest, { uid: true }),
84231
84286
  `IMAP move of ${uids.length} message(s) from "${srcPath}" to "${dest}"`
84232
84287
  );
84233
- return { dest, expunged: false };
84288
+ return { dest, expunged: false, moved };
84289
+ }
84290
+ async function verifyExpunged(client, uid, path) {
84291
+ try {
84292
+ const stillThere = await client.fetchOne(String(uid), { uid: true }, { uid: true });
84293
+ if (!stillThere) {
84294
+ return { verdict: "verified", how: `UID ${uid} is no longer present in "${path}"` };
84295
+ }
84296
+ return {
84297
+ verdict: "unverified",
84298
+ why: `the server accepted the EXPUNGE but UID ${uid} is still present in "${path}"`
84299
+ };
84300
+ } catch (e) {
84301
+ return { verdict: "unverified", why: `the post-delete check could not run: ${errText(e)}` };
84302
+ }
84234
84303
  }
84235
84304
  async function imapDeleteMessageById(id, deps = {}) {
84236
84305
  const ref = decodeImapId(id);
84237
84306
  if (!ref) return { success: false, error: `Not an IMAP message id: "${id}".` };
84238
84307
  return withMailbox(ref.path, depsForMessageRef(ref, deps), async (client) => {
84239
84308
  try {
84240
- const { dest, expunged } = await trashUids(client, [ref.uid], ref.path);
84309
+ const { dest, expunged, moved } = await trashUids(client, [ref.uid], ref.path);
84310
+ const verification = expunged || !moved ? await verifyExpunged(client, ref.uid, ref.path) : await verifyMoved(client, moved, ref.uid, ref.path, dest);
84311
+ const what = expunged ? `Permanently deleted UID ${ref.uid} from Trash ("${ref.path}") via IMAP` : `Moved UID ${ref.uid} to Trash ("${dest}") via IMAP`;
84241
84312
  return {
84242
84313
  success: true,
84243
- info: expunged ? `Permanently deleted UID ${ref.uid} from Trash ("${ref.path}") via IMAP.` : `Moved UID ${ref.uid} to Trash ("${dest}") via IMAP.`
84314
+ info: verification.verdict === "verified" ? `${what} (verified: ${verification.how}).` : `${what} \u2014 UNVERIFIED: ${verification.why}.`,
84315
+ verification
84244
84316
  };
84245
84317
  } catch (e) {
84246
84318
  return { success: false, error: `IMAP delete failed for UID ${ref.uid}: ${errText(e)}` };
@@ -84820,10 +84892,12 @@ function formatMergedRows(rows, showReadState = true) {
84820
84892
  async function routeMessage(id, opts) {
84821
84893
  if (decodeImapId(id)) {
84822
84894
  const r = await opts.imap();
84823
- return r.success ? successResponse(
84895
+ if (!r.success) return errorResponse(r.error ?? opts.fail);
84896
+ const structured = opts.structuredFromResult ? opts.structuredFromResult(r) : opts.structured;
84897
+ return successResponse(
84824
84898
  r.info ?? opts.ok,
84825
- opts.structuredFromResult ? opts.structuredFromResult(r) : opts.structured
84826
- ) : errorResponse(r.error ?? opts.fail);
84899
+ r.verification && structured ? { ...structured, verification: r.verification } : structured
84900
+ );
84827
84901
  }
84828
84902
  return opts.apple();
84829
84903
  }
@@ -85387,6 +85461,13 @@ var COUNT_DELTA_OUTPUT_SCHEMA = external_exports.array(
85387
85461
  note: external_exports.string().optional()
85388
85462
  })
85389
85463
  ).optional();
85464
+ var VERIFICATION_OUTPUT_SCHEMA = external_exports.object({
85465
+ verdict: external_exports.enum(["verified", "unverified"]),
85466
+ /** Present on `verified`: what was observed. */
85467
+ how: external_exports.string().optional(),
85468
+ /** Present on `unverified`: why no observation was possible. */
85469
+ why: external_exports.string().optional()
85470
+ }).optional();
85390
85471
  var CHECK_ITEM_SCHEMA = external_exports.object({}).passthrough();
85391
85472
  var require2 = createRequire(import.meta.url);
85392
85473
  var { version: version2 } = require2("../package.json");
@@ -86239,7 +86320,8 @@ registerTool(
86239
86320
  outputSchema: {
86240
86321
  ok: external_exports.boolean().optional(),
86241
86322
  id: external_exports.string().optional(),
86242
- countDelta: COUNT_DELTA_OUTPUT_SCHEMA
86323
+ countDelta: COUNT_DELTA_OUTPUT_SCHEMA,
86324
+ verification: VERIFICATION_OUTPUT_SCHEMA
86243
86325
  }
86244
86326
  },
86245
86327
  withErrorHandling(
@@ -86279,7 +86361,8 @@ registerTool(
86279
86361
  ok: external_exports.boolean().optional(),
86280
86362
  id: external_exports.string().optional(),
86281
86363
  mailbox: external_exports.string().optional(),
86282
- countDelta: COUNT_DELTA_OUTPUT_SCHEMA
86364
+ countDelta: COUNT_DELTA_OUTPUT_SCHEMA,
86365
+ verification: VERIFICATION_OUTPUT_SCHEMA
86283
86366
  }
86284
86367
  },
86285
86368
  withErrorHandling(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.12.0",
3
+ "version": "2.13.1",
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",