apple-mail-mcp 2.13.2 → 2.14.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 +17 -3
  2. package/build/index.js +108 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -935,9 +935,23 @@ List all mailboxes for an account.
935
935
 
936
936
  | Parameter | Type | Required | Description |
937
937
  |-----------|------|----------|-------------|
938
- | `account` | string | No | Account to list from |
939
-
940
- **Returns:** List of mailbox names with message and unread counts.
938
+ | `account` | string | No | Account to list from, or `"On My Mac"` for the local store |
939
+
940
+ **Returns:** List of mailbox names with message and unread counts. A source that
941
+ could not be read is **named** (`partial: true` + `failedAccounts`) rather than
942
+ dropped, and a listing Mail refused outright returns an error naming the accounts
943
+ that do exist — never an empty list.
944
+
945
+ **Mail's local "On My Mac" mailboxes** are not children of any account — they
946
+ hang off the application — so they are reported under the synthetic account label
947
+ **`On My Mac`**. An unscoped call includes them (listed last); `account="On My
948
+ Mac"` lists only them. `on my computer`, `local` and `local folders` are accepted
949
+ as aliases.
950
+
951
+ They deliberately do **not** appear in `list-accounts`, which reports real
952
+ accounts only: the local store is a store, not an account. Nothing selects it
953
+ implicitly — omitting `account` still resolves to a real account for every other
954
+ tool.
941
955
 
942
956
  ---
943
957
 
package/build/index.js CHANGED
@@ -79129,6 +79129,24 @@ function buildAccountScopedScript(account, command) {
79129
79129
  end tell
79130
79130
  `;
79131
79131
  }
79132
+ var LOCAL_STORE_LABEL = "On My Mac";
79133
+ var LOCAL_STORE_ALIASES = ["on my mac", "on my computer", "local", "local folders"];
79134
+ function isLocalStoreLabel(name) {
79135
+ return name !== void 0 && LOCAL_STORE_ALIASES.includes(name.trim().toLowerCase());
79136
+ }
79137
+ function localMailboxBindingFragment() {
79138
+ return `
79139
+ set _mbs to {}
79140
+ repeat with _m in mailboxes
79141
+ set _isLoc to false
79142
+ try
79143
+ if (account of _m) is missing value then set _isLoc to true
79144
+ on error
79145
+ set _isLoc to true
79146
+ end try
79147
+ if _isLoc then set end of _mbs to (contents of _m)
79148
+ end repeat`;
79149
+ }
79132
79150
  function groupKey(account, mailbox) {
79133
79151
  return `${account}\0${mailbox}`;
79134
79152
  }
@@ -79452,16 +79470,61 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79452
79470
  set _sMiss to ""
79453
79471
  set _sPairs to {}
79454
79472
  set _sChunk to ${chunk}
79473
+ -- The mailbox's MEASURED length, emitted only when it disagrees with the
79474
+ -- count (#187). -1 = "not measured", which is the normal case: the probe
79475
+ -- only runs a binary search when the count's last position is unreadable.
79476
+ -- Initialised here, not in the else-branch, or the skipped/unavailable
79477
+ -- paths would reference an unbound variable when emitting.
79478
+ set _sTrue to -1
79455
79479
  if ${countVar} < 0 then
79456
79480
  set _sStatus to "unavailable"
79457
79481
  else if ${countVar} > ${max} then
79458
79482
  set _sStatus to "skipped"
79459
79483
  set _sPayload to "mailbox holds " & (${countVar} as string) & " messages, above ${AUDIT_SNAPSHOT_MAX_ENV}=${max}"
79460
79484
  else
79485
+ -- #187: the count can read HIGH, and an out-of-range range RAISES as
79486
+ -- a whole rather than clamping. On a mailbox smaller than one chunk
79487
+ -- there is only ONE slice, so a high count made it fail entirely:
79488
+ -- _sPairs stayed empty, the status collapsed to "unavailable", and
79489
+ -- the record carried no holes and no warning. The collateral
79490
+ -- instrument switched itself off in exactly the stale direction #155
79491
+ -- evidences, silently.
79492
+ --
79493
+ -- So establish a bound that actually EXISTS before slicing. If the
79494
+ -- last position the count claims is readable, the count is not high
79495
+ -- and this costs one probe. Otherwise binary-search the true end,
79496
+ -- which is O(log n) probes and also MEASURES how stale the count is.
79497
+ set _sBound to ${countVar}
79498
+ if _sBound > 0 then
79499
+ set _sEndOk to false
79500
+ try
79501
+ get id of message _sBound of ${mbVar}
79502
+ set _sEndOk to true
79503
+ end try
79504
+ if not _sEndOk then
79505
+ set _sLoB to 0
79506
+ set _sHiB to _sBound
79507
+ repeat while (_sHiB - _sLoB) > 1
79508
+ set _sMid to (_sLoB + _sHiB) div 2
79509
+ set _sMidOk to false
79510
+ try
79511
+ get id of message _sMid of ${mbVar}
79512
+ set _sMidOk to true
79513
+ end try
79514
+ if _sMidOk then
79515
+ set _sLoB to _sMid
79516
+ else
79517
+ set _sHiB to _sMid
79518
+ end if
79519
+ end repeat
79520
+ set _sBound to _sLoB
79521
+ set _sTrue to _sLoB
79522
+ end if
79523
+ end if
79461
79524
  set _sLo to 1
79462
- repeat while _sLo <= ${countVar}
79525
+ repeat while _sLo <= _sBound
79463
79526
  set _sHi to _sLo + _sChunk - 1
79464
- if _sHi > ${countVar} then set _sHi to ${countVar}
79527
+ if _sHi > _sBound then set _sHi to _sBound
79465
79528
  set _sGot to false
79466
79529
  repeat with _sTry from 1 to ${SNAPSHOT_SLICE_ATTEMPTS}
79467
79530
  set _sIds to {}
@@ -79519,7 +79582,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79519
79582
  -- PARTIAL under the existing rules and withholds the halves a
79520
79583
  -- truncation would poison.
79521
79584
  try
79522
- set _sOverId to ((id of message (${countVar} + 1) of ${mbVar}) as string)
79585
+ set _sOverId to ((id of message (_sBound + 1) of ${mbVar}) as string)
79523
79586
  -- A specifier that CLAMPS rather than raising hands back the LAST
79524
79587
  -- message instead of failing. That is not evidence of a truncation,
79525
79588
  -- so only an id this enumeration did not already record counts.
@@ -79529,7 +79592,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79529
79592
  end repeat
79530
79593
  if not _sSeen then
79531
79594
  if _sMiss is not "" then set _sMiss to _sMiss & ","
79532
- set _sMiss to _sMiss & ((${countVar} + 1) as string) & "-end"
79595
+ set _sMiss to _sMiss & ((_sBound + 1) as string) & "-end"
79533
79596
  end if
79534
79597
  end try
79535
79598
  if _sMiss is not "" then
@@ -79544,7 +79607,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79544
79607
  set _sPayload to _sPairs as string
79545
79608
  set AppleScript's text item delimiters to _sTid
79546
79609
  end if
79547
- set _out to _out & "${SNAP_TAG}${FIELD_SEP}" & ${acctExpr} & "${FIELD_SEP}" & ${mbExpr} & "${FIELD_SEP}${phase}${FIELD_SEP}" & _sStatus & "${FIELD_SEP}" & _sPayload & "${FIELD_SEP}" & _sMiss & "${RECORD_SEP}"`;
79610
+ set _out to _out & "${SNAP_TAG}${FIELD_SEP}" & ${acctExpr} & "${FIELD_SEP}" & ${mbExpr} & "${FIELD_SEP}${phase}${FIELD_SEP}" & _sStatus & "${FIELD_SEP}" & _sPayload & "${FIELD_SEP}" & _sMiss & "${FIELD_SEP}" & (_sTrue as string) & "${RECORD_SEP}"`;
79548
79611
  }
79549
79612
  /**
79550
79613
  * AppleScript capturing the message the op is ABOUT to touch into `_pre`,
@@ -79605,13 +79668,15 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79605
79668
  continue;
79606
79669
  }
79607
79670
  if (f[0] === SNAP_TAG) {
79671
+ const measured = f[7] !== void 0 && f[7] !== "" ? Number(f[7]) : -1;
79608
79672
  snaps.push({
79609
79673
  account: f[1] ?? "",
79610
79674
  mailbox: f[2] ?? "",
79611
79675
  phase: f[3] === "after" ? "after" : "before",
79612
79676
  status: f[4] ?? "",
79613
79677
  payload: f[5] ?? "",
79614
- miss: f[6] ?? ""
79678
+ miss: f[6] ?? "",
79679
+ ...Number.isFinite(measured) && measured >= 0 ? { measuredLength: measured } : {}
79615
79680
  });
79616
79681
  continue;
79617
79682
  }
@@ -79795,6 +79860,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79795
79860
  const unrequested = disappeared.filter(
79796
79861
  (e) => !requestedNumericIds.has(canonicalNumericId(e.id))
79797
79862
  );
79863
+ const countStale = [b, a].filter((s) => s.measuredLength !== void 0).map((s) => ({ phase: s.phase, measuredLength: s.measuredLength }));
79798
79864
  const holes = [b, a].filter((s) => s.miss !== "").map((s) => ({ phase: s.phase, ranges: s.miss }));
79799
79865
  if (holes.length === 0) {
79800
79866
  collateral.push({
@@ -79803,7 +79869,8 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79803
79869
  snapshot: "ok",
79804
79870
  disappeared,
79805
79871
  unrequested,
79806
- appeared
79872
+ appeared,
79873
+ ...countStale.length ? { countStale } : {}
79807
79874
  });
79808
79875
  continue;
79809
79876
  }
@@ -79815,6 +79882,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79815
79882
  account: g.account,
79816
79883
  mailbox: g.mailbox,
79817
79884
  snapshot: "partial",
79885
+ ...countStale.length ? { countStale } : {},
79818
79886
  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".`,
79819
79887
  unobserved: holes,
79820
79888
  ...a.miss === "" ? { disappeared, unrequested } : {},
@@ -81983,10 +82051,11 @@ ${this.errorEmit(" ")}
81983
82051
  * List all mailboxes for an account.
81984
82052
  */
81985
82053
  listMailboxes(account, options = {}) {
81986
- const targetAccount = this.resolveAccount(account);
81987
- const listCommand = `
82054
+ const local = isLocalStoreLabel(account);
82055
+ const targetAccount = local ? LOCAL_STORE_LABEL : this.resolveAccount(account);
82056
+ const listCommand = (iterExpr) => `
81988
82057
  set mailboxList to {}
81989
- repeat with mb in mailboxes
82058
+ repeat with mb in ${iterExpr}
81990
82059
  set mbName to name of mb
81991
82060
  set mbUnread to unread count of mb
81992
82061
  set mbCount to count of messages of mb
@@ -81995,7 +82064,8 @@ ${this.errorEmit(" ")}
81995
82064
  set AppleScript's text item delimiters to "${RECORD_SEP}"
81996
82065
  return mailboxList as text
81997
82066
  `;
81998
- const script = buildAccountScopedScript(targetAccount, listCommand);
82067
+ const script = local ? buildAppLevelScript(`${localMailboxBindingFragment()}
82068
+ ${listCommand("_mbs")}`) : buildAccountScopedScript(targetAccount, listCommand("mailboxes"));
81999
82069
  const result = executeAppleScript(script, { timeoutMs: options.timeoutMs ?? 6e4 });
82000
82070
  if (!result.success) {
82001
82071
  console.error(`Failed to list mailboxes: ${result.error}`);
@@ -84653,19 +84723,15 @@ function withErrorHandling(handler, errorPrefix) {
84653
84723
  }
84654
84724
 
84655
84725
  // src/tools/mailboxListing.ts
84656
- var LOCAL_STORE_NAMES = ["on my mac", "on my computer", "local", "local folders"];
84657
- function isLocalStoreName(name) {
84658
- return LOCAL_STORE_NAMES.includes(name.trim().toLowerCase());
84659
- }
84660
84726
  function unlistableStoreError(account, error2, knownAccounts) {
84661
84727
  const scope = account ? ` for "${account}"` : "";
84662
84728
  const parts = [`Could not list mailboxes${scope}: ${error2 ?? "Mail declined the request"}`];
84663
84729
  if (knownAccounts.length > 0) {
84664
84730
  parts.push(`Accounts on this Mac: ${knownAccounts.join(", ")}.`);
84665
84731
  }
84666
- if (account && isLocalStoreName(account)) {
84732
+ if (account && isLocalStoreLabel(account)) {
84667
84733
  parts.push(
84668
- `"${account}" is Mail's LOCAL store, not an account \u2014 its mailboxes are not children of any account, so they cannot be reached with an \`account\` argument. Enumerating them is not yet supported.`
84734
+ `"${account}" addresses Mail's LOCAL store, which IS listable \u2014 it is read at the application level rather than through an account, so this failure is not "no such account". Something went wrong reading the local store itself.`
84669
84735
  );
84670
84736
  }
84671
84737
  return parts.join("\n\n");
@@ -86709,10 +86775,26 @@ ${r.base64}`,
86709
86775
  );
86710
86776
  }, "Error fetching attachment")
86711
86777
  );
86778
+ function appendLocalStoreRows(rows, failedAccounts) {
86779
+ const checked = mailManager.listMailboxesChecked(LOCAL_STORE_LABEL);
86780
+ if (checked.failed) {
86781
+ console.error(`list-mailboxes failed for "${LOCAL_STORE_LABEL}": ${checked.error}`);
86782
+ failedAccounts.push(LOCAL_STORE_LABEL);
86783
+ return;
86784
+ }
86785
+ for (const mb of checked.mailboxes) {
86786
+ rows.push({
86787
+ name: `${LOCAL_STORE_LABEL}/${mb.name}`,
86788
+ account: LOCAL_STORE_LABEL,
86789
+ unreadCount: mb.unreadCount,
86790
+ messageCount: mb.messageCount
86791
+ });
86792
+ }
86793
+ }
86712
86794
  registerTool(
86713
86795
  "list-mailboxes",
86714
86796
  {
86715
- description: "Use when: discovering the mailbox/folder names (and unread/message counts) available in an account, e.g. before moving messages or searching a specific mailbox.\nReturns: each mailbox's name with its unread (and, for IMAP, total message) count, plus a count. A source that could not be read is NAMED \u2014 the result carries `partial: true` + `failedAccounts` and the list is a floor, not the complete set \u2014 and a listing Mail refused outright (e.g. an account that does not exist) returns an ERROR naming the accounts that do exist, never an empty list.\nDo not use when: you want the messages inside a mailbox (use list-messages or search-messages) or the list of accounts (use list-accounts).\nNote: Mail's local \"On My Mac\" mailboxes are not part of any account and are not currently enumerated by this tool.",
86797
+ description: 'Use when: discovering the mailbox/folder names (and unread/message counts) available in an account, e.g. before moving messages or searching a specific mailbox.\nReturns: each mailbox\'s name with its unread (and, for IMAP, total message) count, plus a count. A source that could not be read is NAMED \u2014 the result carries `partial: true` + `failedAccounts` and the list is a floor, not the complete set \u2014 and a listing Mail refused outright (e.g. an account that does not exist) returns an ERROR naming the accounts that do exist, never an empty list.\nDo not use when: you want the messages inside a mailbox (use list-messages or search-messages) or the list of accounts (use list-accounts).\nNote: Mail\'s local "On My Mac" mailboxes are not part of any account, so they are reported under the synthetic account label "On My Mac" \u2014 an unscoped call includes them, and `account: "On My Mac"` lists only them. They will not appear in list-accounts, which reports real accounts only.',
86716
86798
  inputSchema: {
86717
86799
  account: external_exports.string().optional().describe("Account to list mailboxes from")
86718
86800
  },
@@ -86778,6 +86860,7 @@ ${list2}`, structured3);
86778
86860
  });
86779
86861
  }
86780
86862
  }
86863
+ appendLocalStoreRows(rows, failedAccounts);
86781
86864
  const partial2 = failedAccounts.length > 0;
86782
86865
  const structured2 = {
86783
86866
  mailboxes: rows,
@@ -86806,7 +86889,13 @@ ${list}${caveat}`, structured2);
86806
86889
  )
86807
86890
  );
86808
86891
  }
86809
- const structured = { mailboxes, count: mailboxes.length };
86892
+ const localFailures = [];
86893
+ if (account === void 0) appendLocalStoreRows(mailboxes, localFailures);
86894
+ const structured = {
86895
+ mailboxes,
86896
+ count: mailboxes.length,
86897
+ ...localFailures.length ? { partial: true, failedAccounts: localFailures } : {}
86898
+ };
86810
86899
  if (mailboxes.length === 0) {
86811
86900
  return successResponse("No mailboxes found", structured);
86812
86901
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.13.2",
3
+ "version": "2.14.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",