apple-mail-mcp 2.10.23 → 2.10.25

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 +10 -9
  2. package/build/index.js +89 -69
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -852,11 +852,12 @@ likely to be refused as ambiguous. Passing the source mailbox explicitly is the
852
852
  stay scoped, and it overrides the remembered location. These parameters name where the ids **came
853
853
  from**; for `batch-move-messages` that is distinct from `mailbox`, the destination.
854
854
 
855
- `sourceMailbox` is the one that scopes, and it works on its own: omitting `sourceAccount` means
856
- that mailbox **in the default account**, exactly as an omitted `account` does elsewhere. If the
857
- default account cannot be determined, the ids fail with an error asking for an explicit
858
- `sourceAccount` a scope the server can't honor is never quietly downgraded to the guess-the-copy
859
- walk. `sourceAccount` by itself pins nothing, since the mailbox is what an id is scoped to.
855
+ `sourceMailbox` and `sourceAccount` are an atomic scope pair: provide **both** for numeric ids.
856
+ The server never fills in a missing account from mutable default-send state, because the same
857
+ mailbox name can exist in more than one account and numeric ids are only unique within an account
858
+ and mailbox. A whitespace-only source field is rejected. `sourceAccount` by itself pins nothing,
859
+ since the mailbox is what an id is scoped to; `imap:…` ids ignore both fields because they carry
860
+ their own account, mailbox, and UID identity.
860
861
 
861
862
  **A repeated id is one message.** `ids` is treated as a set: a duplicate names the same message,
862
863
  so it is operated on once, and the batch returns **one result per distinct id**. `success` is
@@ -868,7 +869,7 @@ therefore a count of messages, not of list positions.
868
869
  |-----------|------|----------|-------------|
869
870
  | `ids` | string[] | Yes | Message IDs to delete (max 100) |
870
871
  | `sourceMailbox` | string | No | Mailbox the **numeric** ids were listed from — pins them to it. Ignored for `imap:` ids. |
871
- | `sourceAccount` | string | No | Account the numeric ids were listed from. Defaults to the default account; on its own it pins nothing — pair it with `sourceMailbox`. |
872
+ | `sourceAccount` | string | No | Account the numeric ids were listed from. Required when `sourceMailbox` is supplied; on its own it pins nothing. |
872
873
 
873
874
  `structuredContent` carries `countDelta` — what the batch actually did to each
874
875
  source mailbox. See [Auditing destructive operations](#auditing-destructive-operations).
@@ -883,7 +884,7 @@ source mailbox. See [Auditing destructive operations](#auditing-destructive-oper
883
884
  | `mailbox` | string | Yes | Destination mailbox |
884
885
  | `account` | string | No | Account containing mailbox |
885
886
  | `sourceMailbox` | string | No | Mailbox the **numeric** ids were listed from — pins them to it. Ignored for `imap:` ids. |
886
- | `sourceAccount` | string | No | Account the numeric ids were listed from. Defaults to the default account; on its own it pins nothing — pair it with `sourceMailbox`. |
887
+ | `sourceAccount` | string | No | Account the numeric ids were listed from. Required when `sourceMailbox` is supplied; on its own it pins nothing. |
887
888
 
888
889
  `structuredContent` carries `countDelta` — what the batch actually did to each
889
890
  **source** mailbox. See [Auditing destructive operations](#auditing-destructive-operations).
@@ -894,7 +895,7 @@ source mailbox. See [Auditing destructive operations](#auditing-destructive-oper
894
895
  |-----------|------|----------|-------------|
895
896
  | `ids` | string[] | Yes | Message IDs (max 100) |
896
897
  | `sourceMailbox` | string | No | Mailbox the **numeric** ids were listed from — pins them to it. Ignored for `imap:` ids. |
897
- | `sourceAccount` | string | No | Account the numeric ids were listed from. Defaults to the default account; on its own it pins nothing — pair it with `sourceMailbox`. |
898
+ | `sourceAccount` | string | No | Account the numeric ids were listed from. Required when `sourceMailbox` is supplied; on its own it pins nothing. |
898
899
 
899
900
  #### `batch-flag-messages` / `batch-unflag-messages`
900
901
 
@@ -903,7 +904,7 @@ source mailbox. See [Auditing destructive operations](#auditing-destructive-oper
903
904
  | `ids` | string[] | Yes | Message IDs (max 100) |
904
905
  | `color` | string | No | (`batch-flag-messages` only) Flag color — see [`flag-message`](#flag-message--unflag-message). Applied on both routes, so a mixed batch of numeric and `imap:` ids all end up colored. |
905
906
  | `sourceMailbox` | string | No | Mailbox the **numeric** ids were listed from — pins them to it. Ignored for `imap:` ids. |
906
- | `sourceAccount` | string | No | Account the numeric ids were listed from. Defaults to the default account; on its own it pins nothing — pair it with `sourceMailbox`. |
907
+ | `sourceAccount` | string | No | Account the numeric ids were listed from. Required when `sourceMailbox` is supplied; on its own it pins nothing. |
907
908
 
908
909
  ---
909
910
 
package/build/index.js CHANGED
@@ -78300,6 +78300,7 @@ var DIAG_ITEM_SEP = "M";
78300
78300
  var CONTENT_MARKER = "CONTENT";
78301
78301
  var MSGID_MARKER = "MSGID";
78302
78302
  var HTML_MARKER = "HTML";
78303
+ var LOOKUP_ERROR_MARKER = "ERR";
78303
78304
  var BATCH_FATAL = "FATAL";
78304
78305
  var RECON_TAG = "RECON";
78305
78306
  var SNAP_TAG = "SNAP";
@@ -78653,6 +78654,8 @@ var AppleMailManager = class {
78653
78654
  * misses and falls back to the full scan, so it can never wedge a lookup.
78654
78655
  */
78655
78656
  idLocationIndex = /* @__PURE__ */ new Map();
78657
+ /** Error from the most recent numeric message read, if it was refused. */
78658
+ lastMessageLookupError;
78656
78659
  /** Cap on the id→location index so a long-lived process can't grow unbounded. */
78657
78660
  ID_LOCATION_MAX = 5e3;
78658
78661
  /** Record (or refresh) where a message id lives, evicting oldest when full. */
@@ -78681,6 +78684,12 @@ var AppleMailManager = class {
78681
78684
  noteMessageLocation(id, account, mailbox) {
78682
78685
  this.rememberLocation(id, account, mailbox);
78683
78686
  }
78687
+ /** Consume the most recent read refusal so the tool layer can preserve it. */
78688
+ consumeLastMessageLookupError() {
78689
+ const error2 = this.lastMessageLookupError;
78690
+ this.lastMessageLookupError = void 0;
78691
+ return error2;
78692
+ }
78684
78693
  /**
78685
78694
  * AppleScript fragment resolving `account` + `mailbox` into `_tmb`, leaving
78686
78695
  * `_tmb` as `missing value` when it can't be pinned down. Exact-name match
@@ -79699,6 +79708,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79699
79708
  * returned the entire raw MIME blob mislabeled as HTML (#32).
79700
79709
  */
79701
79710
  getMessageContent(id, includeHtml = false, hint) {
79711
+ this.lastMessageLookupError = void 0;
79702
79712
  const sourceFetch = includeHtml ? `set htmlSource to ""
79703
79713
  try
79704
79714
  set htmlSource to source of msg
@@ -79724,17 +79734,25 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79724
79734
  }
79725
79735
  const script = buildAppLevelScript(`
79726
79736
  try
79737
+ set _hits to {}
79738
+ set _names to ""
79727
79739
  repeat with acct in accounts
79728
79740
  repeat with mb in mailboxes of acct
79729
79741
  try
79730
79742
  set matchingMsgs to (messages of mb whose id is ${Number(id)})
79731
79743
  if (count of matchingMsgs) > 0 then
79732
- set msg to item 1 of matchingMsgs
79733
- ${innerFetch}
79744
+ set end of _hits to item 1 of matchingMsgs
79745
+ set _names to _names & (name of acct) & "/" & (name of mb) & ", "
79734
79746
  end if
79735
79747
  end try
79736
79748
  end repeat
79737
79749
  end repeat
79750
+ if (count of _hits) is 0 then return "${LOOKUP_ERROR_MARKER}Message not found"
79751
+ 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"
79752
+ if (count of _hits) is 1 then
79753
+ set msg to item 1 of _hits
79754
+ ${innerFetch}
79755
+ end if
79738
79756
  return ""
79739
79757
  on error errMsg
79740
79758
  return ""
@@ -79756,6 +79774,10 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79756
79774
  if (!result.success) console.error(`Failed to get message content: ${result.error}`);
79757
79775
  return null;
79758
79776
  }
79777
+ if (result.output.startsWith(LOOKUP_ERROR_MARKER)) {
79778
+ this.lastMessageLookupError = result.output.slice(LOOKUP_ERROR_MARKER.length).trim();
79779
+ return null;
79780
+ }
79759
79781
  const htmlSplit = result.output.split(HTML_MARKER);
79760
79782
  const contentPart = htmlSplit[0];
79761
79783
  const rawSource = htmlSplit.length > 1 ? htmlSplit[1] : "";
@@ -79783,6 +79805,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79783
79805
  * a 20MB attachment can take several seconds over Exchange/IMAP.
79784
79806
  */
79785
79807
  getRawSource(id, hint) {
79808
+ this.lastMessageLookupError = void 0;
79786
79809
  const loc = hint?.account && hint?.mailbox ? { account: hint.account, mailbox: hint.mailbox } : this.idLocationIndex.get(id.toString());
79787
79810
  if (loc) {
79788
79811
  const scopedScript = this.scopedByIdScript(
@@ -79792,21 +79815,34 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79792
79815
  "return source of msg"
79793
79816
  );
79794
79817
  const scoped = executeAppleScript(scopedScript, { timeoutMs: 12e4 });
79795
- if (scoped.success && scoped.output.trim()) return scoped.output;
79818
+ if (scoped.success && scoped.output.trim() && !scoped.output.startsWith(LOOKUP_ERROR_MARKER)) {
79819
+ return scoped.output;
79820
+ }
79821
+ if (scoped.success && scoped.output.startsWith(LOOKUP_ERROR_MARKER)) {
79822
+ this.lastMessageLookupError = scoped.output.slice(LOOKUP_ERROR_MARKER.length).trim();
79823
+ }
79796
79824
  }
79797
79825
  const script = buildAppLevelScript(`
79798
79826
  try
79827
+ set _hits to {}
79828
+ set _names to ""
79799
79829
  repeat with acct in accounts
79800
79830
  repeat with mb in mailboxes of acct
79801
79831
  try
79802
79832
  set matchingMsgs to (messages of mb whose id is ${Number(id)})
79803
79833
  if (count of matchingMsgs) > 0 then
79804
- set msg to item 1 of matchingMsgs
79805
- return source of msg
79834
+ set end of _hits to item 1 of matchingMsgs
79835
+ set _names to _names & (name of acct) & "/" & (name of mb) & ", "
79806
79836
  end if
79807
79837
  end try
79808
79838
  end repeat
79809
79839
  end repeat
79840
+ if (count of _hits) is 0 then return "${LOOKUP_ERROR_MARKER}Message not found"
79841
+ 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"
79842
+ if (count of _hits) is 1 then
79843
+ set msg to item 1 of _hits
79844
+ return source of msg
79845
+ end if
79810
79846
  return ""
79811
79847
  on error errMsg
79812
79848
  return ""
@@ -79816,6 +79852,10 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79816
79852
  if (!result.success || !result.output.trim()) {
79817
79853
  return null;
79818
79854
  }
79855
+ if (result.output.startsWith(LOOKUP_ERROR_MARKER)) {
79856
+ this.lastMessageLookupError = result.output.slice(LOOKUP_ERROR_MARKER.length).trim();
79857
+ return null;
79858
+ }
79819
79859
  return result.output;
79820
79860
  }
79821
79861
  /**
@@ -80256,27 +80296,13 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
80256
80296
  const safeBody = escapeForAppleScriptBody(body);
80257
80297
  const replyAllClause = replyAll ? " with reply to all" : "";
80258
80298
  const sendAction = send ? "send theReply" : "";
80259
- const script = buildAppLevelScript(`
80260
- try
80261
- repeat with acct in accounts
80262
- repeat with mb in mailboxes of acct
80263
- try
80264
- set matchingMsgs to (messages of mb whose id is ${Number(id)})
80265
- if (count of matchingMsgs) > 0 then
80266
- set msg to item 1 of matchingMsgs
80267
- set theReply to reply msg without opening window${replyAllClause}
80268
- set content of theReply to "${safeBody}"
80269
- ${sendAction}
80270
- return "ok"
80271
- end if
80272
- end try
80273
- end repeat
80274
- end repeat
80275
- return "error:Message not found"
80276
- on error errMsg
80277
- return "error:" & errMsg
80278
- end try
80279
- `);
80299
+ const script = this.findMessageScript(
80300
+ id,
80301
+ `
80302
+ set theReply to reply msg without opening window${replyAllClause}
80303
+ set content of theReply to "${safeBody}"
80304
+ ${sendAction}`
80305
+ );
80280
80306
  const result = executeAppleScript(script, { timeoutMs: 6e4 });
80281
80307
  if (!result.success || result.output.startsWith("error:")) {
80282
80308
  console.error(`Failed to reply to message: ${result.error || result.output}`);
@@ -80301,28 +80327,14 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
80301
80327
  recipientCommands += `make new to recipient at end of to recipients of theForward with properties {address:"${escapeForAppleScript(addr)}"}
80302
80328
  `;
80303
80329
  }
80304
- const script = buildAppLevelScript(`
80305
- try
80306
- repeat with acct in accounts
80307
- repeat with mb in mailboxes of acct
80308
- try
80309
- set matchingMsgs to (messages of mb whose id is ${Number(id)})
80310
- if (count of matchingMsgs) > 0 then
80311
- set msg to item 1 of matchingMsgs
80312
- set theForward to forward msg without opening window
80313
- ${recipientCommands}
80314
- ${safeBody ? `set content of theForward to "${safeBody}"` : ""}
80315
- ${sendAction}
80316
- return "ok"
80317
- end if
80318
- end try
80319
- end repeat
80320
- end repeat
80321
- return "error:Message not found"
80322
- on error errMsg
80323
- return "error:" & errMsg
80324
- end try
80325
- `);
80330
+ const script = this.findMessageScript(
80331
+ id,
80332
+ `
80333
+ set theForward to forward msg without opening window
80334
+ ${recipientCommands}
80335
+ ${safeBody ? `set content of theForward to "${safeBody}"` : ""}
80336
+ ${sendAction}`
80337
+ );
80326
80338
  const result = executeAppleScript(script, { timeoutMs: 6e4 });
80327
80339
  if (!result.success || result.output.startsWith("error:")) {
80328
80340
  console.error(`Failed to forward message: ${result.error || result.output}`);
@@ -80734,39 +80746,44 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
80734
80746
  /**
80735
80747
  * Turn a caller-supplied batch source scope into an account+mailbox pair.
80736
80748
  *
80737
- * `mailbox` is what does the scoping, and it works ON ITS OWN: an omitted
80738
- * `account` means "the default account" here exactly as it does for every
80739
- * other tool in this server (see resolveAccount), never "ignore the argument
80740
- * you were given". Requiring both used to make a lone `sourceMailbox` a silent
80741
- * no-op the ids fell back to the whole-tree path and were then refused as
80742
- * ambiguous, which is the very failure the parameter exists to prevent.
80749
+ * A numeric source scope is an account+mailbox pair. A mailbox name alone is
80750
+ * not an identity: the same mailbox can exist in several accounts, and a
80751
+ * numeric Mail id is not globally unique. Resolving a missing account from
80752
+ * mutable default-send state can therefore target the wrong account. Require
80753
+ * both fields so a caller cannot silently cross that account boundary.
80743
80754
  *
80744
80755
  * The safety property is absolute: when the account cannot be determined there
80745
80756
  * is NO fallback to the scan-and-guess walk. The caller gets an error naming
80746
80757
  * the mailbox it asked for, so it can retry with an explicit `sourceAccount`.
80747
80758
  * (An `account` with no `mailbox` cannot pin anything, so it scopes nothing —
80748
- * those ids still go through the index / ambiguity-checked path.)
80759
+ * those ids still go through the index / ambiguity-checked path. A supplied
80760
+ * whitespace-only field is rejected rather than silently discarded.)
80749
80761
  */
80750
80762
  resolveBatchScope(scope) {
80751
- const mailbox = scope?.mailbox?.trim();
80752
- if (!mailbox) return { kind: "none" };
80753
- const account = scope?.account?.trim();
80754
- if (account) return { kind: "scoped", account, mailbox };
80755
- const known = this.getCachedAccounts();
80756
- if (known.length === 0) {
80763
+ const rawMailbox = scope?.mailbox;
80764
+ const rawAccount = scope?.account;
80765
+ const mailbox = rawMailbox?.trim();
80766
+ const account = rawAccount?.trim();
80767
+ if (rawMailbox !== void 0 && !mailbox) {
80768
+ return {
80769
+ kind: "unresolvable",
80770
+ error: "sourceMailbox must contain a mailbox name; whitespace-only scope is not allowed."
80771
+ };
80772
+ }
80773
+ if (rawAccount !== void 0 && !account) {
80757
80774
  return {
80758
80775
  kind: "unresolvable",
80759
- error: `Cannot scope to source mailbox "${mailbox}": no sourceAccount was given and no Mail account could be read (Mail returned none, or the AppleScript transport failed). Retry with an explicit sourceAccount.`
80776
+ error: "sourceAccount must contain an account name; whitespace-only scope is not allowed."
80760
80777
  };
80761
80778
  }
80762
- const chosen = this.resolveAccount();
80763
- if (!known.some((a) => a.name === chosen)) {
80779
+ if (!mailbox) return { kind: "none" };
80780
+ if (!account) {
80764
80781
  return {
80765
80782
  kind: "unresolvable",
80766
- error: `Cannot scope to source mailbox "${mailbox}": no sourceAccount was given and the default account could not be determined. Retry with an explicit sourceAccount (available: ${known.map((a) => a.name).join(", ")}).`
80783
+ error: `Cannot scope to source mailbox "${mailbox}" without sourceAccount: numeric Mail ids are only unique within an account and mailbox. Retry with both sourceAccount and sourceMailbox explicitly set.`
80767
80784
  };
80768
80785
  }
80769
- return { kind: "scoped", account: chosen, mailbox };
80786
+ return { kind: "scoped", account, mailbox };
80770
80787
  }
80771
80788
  /**
80772
80789
  * Run one operation over many message IDs in a SINGLE osascript invocation.
@@ -84420,10 +84437,10 @@ function withJsonSchema2020_12(transport2) {
84420
84437
  // src/index.ts
84421
84438
  loadFileConfig();
84422
84439
  var BATCH_SOURCE_MAILBOX_SCHEMA = external_exports.string().optional().describe(
84423
- "Mailbox the numeric ids were listed from (e.g. 'INBOX'). Pins each id to that mailbox \u2014 strongly recommended, since one numeric id can match in several mailboxes. Works on its own: without sourceAccount it means that mailbox in the default account. Ignored for imap: ids."
84440
+ "Mailbox the numeric ids were listed from (e.g. 'INBOX'). Must be paired with sourceAccount to form an unambiguous scope. Ignored for imap: ids."
84424
84441
  );
84425
84442
  var BATCH_SOURCE_ACCOUNT_SCHEMA = external_exports.string().optional().describe(
84426
- "Account the numeric ids were listed from. Defaults to the default account. On its own it pins nothing \u2014 pair it with sourceMailbox."
84443
+ "Account the numeric ids were listed from. Required when sourceMailbox is supplied; on its own it pins nothing."
84427
84444
  );
84428
84445
  var FLAG_COLOR_INDEX = {
84429
84446
  red: 0,
@@ -84730,7 +84747,10 @@ Do not use when: you don't yet have an id (use search-messages or list-messages
84730
84747
  account,
84731
84748
  mailbox
84732
84749
  });
84733
- if (!content) return errorResponse(`Message with ID "${id}" not found`);
84750
+ if (!content) {
84751
+ const lookupError = mailManager.consumeLastMessageLookupError();
84752
+ return errorResponse(lookupError ?? `Message with ID "${id}" not found`);
84753
+ }
84734
84754
  const isHtml = preferHtml === true && !!content.htmlContent;
84735
84755
  const body = isHtml ? content.htmlContent : content.plainText;
84736
84756
  return successResponse(`Subject: ${content.subject}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.10.23",
3
+ "version": "2.10.25",
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",