apple-mail-mcp 2.15.0 → 2.16.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.
package/README.md CHANGED
@@ -953,6 +953,11 @@ accounts only: the local store is a store, not an account. Nothing selects it
953
953
  implicitly — omitting `account` still resolves to a real account for every other
954
954
  tool.
955
955
 
956
+ The mail inside them is reachable too: `list-messages` and `search-messages`
957
+ accept `account="On My Mac"`, and `get-message` resolves an id that lives only in
958
+ a local mailbox. An id present **both** in an account mailbox and locally is
959
+ reported as ambiguous rather than silently resolving to the account copy.
960
+
956
961
  ---
957
962
 
958
963
  #### `get-unread-count`
package/build/index.js CHANGED
@@ -78980,7 +78980,7 @@ function describeMailboxOpError(op, raw) {
78980
78980
  const trimmed = (raw || "").trim();
78981
78981
  if (UNSUPPORTED_APPLESCRIPT_OP.test(trimmed)) {
78982
78982
  const verb = op.charAt(0).toUpperCase() + op.slice(1);
78983
- return `Mail.app cannot ${op} server-side (IMAP / Gmail / Workspace / iCloud / Exchange) mailboxes via AppleScript \u2014 only local "On My Mac" mailboxes support this. ${verb} it in Mail.app directly. (Mail.app error: ${trimmed})`;
78983
+ return `Mail.app's scripting bridge will not ${op} this mailbox. That covers server-side (IMAP / Gmail / Workspace / iCloud / Exchange) mailboxes, and on current macOS it covers local "On My Mac" mailboxes too \u2014 measured 2026-08-16, see #193. ${verb} it in Mail.app directly; for an IMAP-configured account the IMAP path can do it instead. (Mail.app error: ${trimmed})`;
78984
78984
  }
78985
78985
  return trimmed || `Failed to ${op} mailbox`;
78986
78986
  }
@@ -79170,6 +79170,26 @@ var COUNT_PARTIAL_NOTE = `Mail's count moved by less than this operation account
79170
79170
  function snapshotKey(entry) {
79171
79171
  return `${entry.id}\0${entry.messageId}`;
79172
79172
  }
79173
+ function crossCheckRenumbered(disappeared, appeared) {
79174
+ const index = (entries) => {
79175
+ const m = /* @__PURE__ */ new Map();
79176
+ for (const e of entries) {
79177
+ if (!e.messageId) continue;
79178
+ m.set(e.messageId, m.has(e.messageId) ? null : e);
79179
+ }
79180
+ return m;
79181
+ };
79182
+ const gone = index(disappeared);
79183
+ const came = index(appeared);
79184
+ const out = [];
79185
+ for (const [mid, before] of gone) {
79186
+ const after = came.get(mid);
79187
+ if (!before || !after) continue;
79188
+ if (before.id === after.id) continue;
79189
+ out.push({ messageId: mid, before: before.id, after: after.id });
79190
+ }
79191
+ return out;
79192
+ }
79173
79193
  function buildAppLevelScript(command) {
79174
79194
  return `
79175
79195
  tell application "Mail"
@@ -79845,8 +79865,12 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79845
79865
  const afterEntries = this.parseSnapshot(a.payload);
79846
79866
  const afterKeys = new Set(afterEntries.map((e) => snapshotKey(e)));
79847
79867
  const beforeKeys = new Set(beforeEntries.map((e) => snapshotKey(e)));
79848
- const disappeared = beforeEntries.filter((e) => !afterKeys.has(snapshotKey(e)));
79849
- const appeared = afterEntries.filter((e) => !beforeKeys.has(snapshotKey(e)));
79868
+ const rawDisappeared = beforeEntries.filter((e) => !afterKeys.has(snapshotKey(e)));
79869
+ const rawAppeared = afterEntries.filter((e) => !beforeKeys.has(snapshotKey(e)));
79870
+ const renumbered = crossCheckRenumbered(rawDisappeared, rawAppeared);
79871
+ const renumberedMids = new Set(renumbered.map((r) => r.messageId));
79872
+ const disappeared = rawDisappeared.filter((e) => !renumberedMids.has(e.messageId));
79873
+ const appeared = rawAppeared.filter((e) => !renumberedMids.has(e.messageId));
79850
79874
  const unrequested = disappeared.filter(
79851
79875
  (e) => !requestedNumericIds.has(canonicalNumericId(e.id))
79852
79876
  );
@@ -79860,7 +79884,8 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79860
79884
  disappeared,
79861
79885
  unrequested,
79862
79886
  appeared,
79863
- ...countStale.length ? { countStale } : {}
79887
+ ...countStale.length ? { countStale } : {},
79888
+ ...renumbered.length ? { renumbered } : {}
79864
79889
  });
79865
79890
  continue;
79866
79891
  }
@@ -79873,6 +79898,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79873
79898
  mailbox: g.mailbox,
79874
79899
  snapshot: "partial",
79875
79900
  ...countStale.length ? { countStale } : {},
79901
+ ...renumbered.length ? { renumbered } : {},
79876
79902
  skipReason: `Mail would not read ${holes.map((h) => `${h.ranges} (${h.phase})`).join(", ")} of this mailbox, so the snapshot has a hole in it. ` + (derivable.length > 0 ? `Still derivable and reported: ${derivable.join(" and ")}. ` : `Neither half of the diff is derivable from it. `) + `Anything the unread range could refute is omitted rather than guessed \u2014 an absent field here means "not computable", not "empty".`,
79877
79903
  unobserved: holes,
79878
79904
  ...a.miss === "" ? { disappeared, unrequested } : {},
@@ -80201,7 +80227,9 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
80201
80227
  }
80202
80228
  return { messages: allMessages.slice(0, limit), diagnostics };
80203
80229
  }
80204
- const targetAccount = this.resolveAccount(account);
80230
+ const local = isLocalStoreLabel(account);
80231
+ const targetAccount = local ? LOCAL_STORE_LABEL : this.resolveAccount(account);
80232
+ const mbIter = local ? "_mbs" : "mailboxes";
80205
80233
  const searchCondition = buildSearchCondition({ query, from, subject, isRead, isFlagged });
80206
80234
  let dateSetup = "";
80207
80235
  let dateFilter = "";
@@ -80233,7 +80261,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
80233
80261
  set _wantNames to ${nameList}
80234
80262
  set msgCount to 0
80235
80263
  set seenIds to {}
80236
- repeat with mb in mailboxes
80264
+ repeat with mb in ${mbIter}
80237
80265
  if msgCount >= ${limit} then exit repeat
80238
80266
  set mbName to ""
80239
80267
  try
@@ -80278,7 +80306,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
80278
80306
  set _skipped to ""
80279
80307
  set _notSearched to ""
80280
80308
  set _startedAt to current date
80281
- repeat with mb in mailboxes
80309
+ repeat with mb in ${mbIter}
80282
80310
  if msgCount >= ${limit} then exit repeat
80283
80311
  set mbName to ""
80284
80312
  try
@@ -80308,7 +80336,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
80308
80336
  return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "${DIAG_FIELD_SEP}skipped=" & _skipped & "${DIAG_FIELD_SEP}notSearched=" & _notSearched
80309
80337
  `;
80310
80338
  }
80311
- const script = buildAccountScopedScript(targetAccount, searchCommand);
80339
+ const script = local ? buildAppLevelScript(`${localMailboxBindingFragment()}${searchCommand}`) : buildAccountScopedScript(targetAccount, searchCommand);
80312
80340
  const result = executeAppleScript(script, { timeoutMs: SEARCH_ACCOUNT_TIMEOUT_MS });
80313
80341
  if (!result.success) {
80314
80342
  console.error(`Failed to search messages in "${targetAccount}": ${result.error}`);
@@ -80419,9 +80447,16 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
80419
80447
  */
80420
80448
  scopedByIdScript(account, mailbox, id, innerAction) {
80421
80449
  const resolved = this.resolveMailbox(mailbox, account);
80422
- return buildAppLevelScript(`
80423
- try
80424
- set acct to (first account whose name is "${escapeForAppleScript(account)}")
80450
+ const bind = isLocalStoreLabel(account) ? `${localMailboxBindingFragment()}
80451
+ set targetMb to missing value
80452
+ ignoring case
80453
+ repeat with mb in _mbs
80454
+ if (name of mb) is "${escapeForAppleScript(resolved)}" then
80455
+ set targetMb to mb
80456
+ exit repeat
80457
+ end if
80458
+ end repeat
80459
+ end ignoring` : `set acct to (first account whose name is "${escapeForAppleScript(account)}")
80425
80460
  set targetMb to missing value
80426
80461
  ignoring case
80427
80462
  repeat with mb in mailboxes of acct
@@ -80430,7 +80465,10 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
80430
80465
  exit repeat
80431
80466
  end if
80432
80467
  end repeat
80433
- end ignoring
80468
+ end ignoring`;
80469
+ return buildAppLevelScript(`
80470
+ try
80471
+ ${bind}
80434
80472
  if targetMb is not missing value then
80435
80473
  set matchingMsgs to (messages of targetMb whose id is ${Number(id)})
80436
80474
  if (count of matchingMsgs) > 0 then
@@ -80494,6 +80532,19 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
80494
80532
  end try
80495
80533
  end repeat
80496
80534
  end repeat
80535
+ -- #183: local mailboxes belong to no account, so the walk above cannot
80536
+ -- reach them. Collect into the SAME _hits/_names, which means an id
80537
+ -- present both in an account and locally is now correctly reported as
80538
+ -- ambiguous rather than silently resolving to the account copy.${localMailboxBindingFragment()}
80539
+ repeat with mb in _mbs
80540
+ try
80541
+ set matchingMsgs to (messages of mb whose id is ${Number(id)})
80542
+ if (count of matchingMsgs) > 0 then
80543
+ set end of _hits to item 1 of matchingMsgs
80544
+ set _names to _names & "${LOCAL_STORE_LABEL}/" & (name of mb) & ", "
80545
+ end if
80546
+ end try
80547
+ end repeat
80497
80548
  if (count of _hits) is 0 then return "${LOOKUP_ERROR_MARKER}Message not found"
80498
80549
  if (count of _hits) > 1 then return "${LOOKUP_ERROR_MARKER}${AMBIGUOUS_ID_PREFIX}${Number(id)} is present in more than one mailbox (" & _names & "); list or search that mailbox first so the read targets the right copy"
80499
80550
  if (count of _hits) is 1 then
@@ -80584,6 +80635,19 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
80584
80635
  end try
80585
80636
  end repeat
80586
80637
  end repeat
80638
+ -- #183: local mailboxes belong to no account, so the walk above cannot
80639
+ -- reach them. Collect into the SAME _hits/_names, which means an id
80640
+ -- present both in an account and locally is now correctly reported as
80641
+ -- ambiguous rather than silently resolving to the account copy.${localMailboxBindingFragment()}
80642
+ repeat with mb in _mbs
80643
+ try
80644
+ set matchingMsgs to (messages of mb whose id is ${Number(id)})
80645
+ if (count of matchingMsgs) > 0 then
80646
+ set end of _hits to item 1 of matchingMsgs
80647
+ set _names to _names & "${LOCAL_STORE_LABEL}/" & (name of mb) & ", "
80648
+ end if
80649
+ end try
80650
+ end repeat
80587
80651
  if (count of _hits) is 0 then return "${LOOKUP_ERROR_MARKER}Message not found"
80588
80652
  if (count of _hits) > 1 then return "${LOOKUP_ERROR_MARKER}${AMBIGUOUS_ID_PREFIX}${Number(id)} is present in more than one mailbox (" & _names & "); list or search that mailbox first so the read targets the right copy"
80589
80653
  if (count of _hits) is 1 then
@@ -80647,10 +80711,12 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
80647
80711
  }
80648
80712
  return { messages: allMessages.slice(0, limit), diagnostics };
80649
80713
  }
80650
- const targetAccount = this.resolveAccount(account);
80714
+ const local = isLocalStoreLabel(account);
80715
+ const targetAccount = local ? LOCAL_STORE_LABEL : this.resolveAccount(account);
80651
80716
  const safeFrom = from ? escapeForAppleScript(from) : "";
80652
80717
  const fromFilter = from ? `whose sender contains "${safeFrom}"` : "";
80653
80718
  const scanThreshold = getMailboxScanThreshold();
80719
+ const mbIter = local ? "_mbs" : "mailboxes";
80654
80720
  let listCommand;
80655
80721
  if (mailbox) {
80656
80722
  const targetMailbox = this.resolveMailbox(mailbox, targetAccount);
@@ -80665,7 +80731,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
80665
80731
  set msgCount to 0
80666
80732
  set skipped to 0
80667
80733
  set seenIds to {}
80668
- repeat with mb in mailboxes
80734
+ repeat with mb in ${mbIter}
80669
80735
  if msgCount >= ${limit} then exit repeat
80670
80736
  set mbName to ""
80671
80737
  try
@@ -80712,7 +80778,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
80712
80778
  set _skipped to ""
80713
80779
  set _notSearched to ""
80714
80780
  set _startedAt to current date
80715
- repeat with mb in mailboxes
80781
+ repeat with mb in ${mbIter}
80716
80782
  if msgCount >= ${limit} then exit repeat
80717
80783
  set mbName to ""
80718
80784
  try
@@ -80742,7 +80808,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
80742
80808
  return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "${DIAG_FIELD_SEP}skipped=" & _skipped & "${DIAG_FIELD_SEP}notSearched=" & _notSearched
80743
80809
  `;
80744
80810
  }
80745
- const script = buildAccountScopedScript(targetAccount, listCommand);
80811
+ const script = local ? buildAppLevelScript(`${localMailboxBindingFragment()}${listCommand}`) : buildAccountScopedScript(targetAccount, listCommand);
80746
80812
  const result = executeAppleScript(script, { timeoutMs: SEARCH_ACCOUNT_TIMEOUT_MS });
80747
80813
  if (!result.success) {
80748
80814
  console.error(`Failed to list messages in "${targetAccount}": ${result.error}`);
@@ -82710,16 +82776,14 @@ end tell`;
82710
82776
  * Used internally by the cache; prefer getCachedMailboxNames().
82711
82777
  */
82712
82778
  fetchMailboxNames(account) {
82713
- const script = buildAccountScopedScript(
82714
- account,
82715
- `
82779
+ const body = `
82716
82780
  set mbNames to {}
82717
- repeat with mb in mailboxes
82781
+ repeat with mb in ${isLocalStoreLabel(account) ? "_mbs" : "mailboxes"}
82718
82782
  set end of mbNames to name of mb
82719
82783
  end repeat
82720
82784
  return mbNames
82721
- `
82722
- );
82785
+ `;
82786
+ const script = isLocalStoreLabel(account) ? buildAppLevelScript(`${localMailboxBindingFragment()}${body}`) : buildAccountScopedScript(account, body);
82723
82787
  const result = executeAppleScript(script);
82724
82788
  if (!result.success || !result.output) {
82725
82789
  return [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.15.0",
3
+ "version": "2.16.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",