apple-mail-mcp 2.11.0 → 2.11.2

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 (2) hide show
  1. package/build/index.js +97 -38
  2. package/package.json +1 -1
package/build/index.js CHANGED
@@ -78071,11 +78071,17 @@ function sleep(ms) {
78071
78071
  }
78072
78072
  }
78073
78073
  }
78074
+ var PERMISSION_DENIED_PATTERN = /not authorized|not permitted|access.*denied/i;
78075
+ var PERMISSION_DENIED_MESSAGE = "Permission denied. Grant automation access in System Settings > Privacy & Security > Automation.";
78076
+ function isPermissionDenied(error2) {
78077
+ if (!error2) return false;
78078
+ return PERMISSION_DENIED_PATTERN.test(error2) || error2.includes(PERMISSION_DENIED_MESSAGE);
78079
+ }
78074
78080
  var ERROR_MAPPINGS = [
78075
78081
  // Permission errors
78076
78082
  {
78077
- pattern: /not authorized|not permitted|access.*denied/i,
78078
- message: "Permission denied. Grant automation access in System Settings > Privacy & Security > Automation."
78083
+ pattern: PERMISSION_DENIED_PATTERN,
78084
+ message: PERMISSION_DENIED_MESSAGE
78079
78085
  },
78080
78086
  // Application not running
78081
78087
  {
@@ -80949,20 +80955,15 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
80949
80955
  replyToMessage(id, body, replyAll = false, send = true) {
80950
80956
  const safeBody = escapeForAppleScriptBody(body);
80951
80957
  const replyAllClause = replyAll ? " with reply to all" : "";
80952
- const sendAction = send ? "send theReply" : "";
80958
+ const finalAction = send ? "send theReply" : "save theReply";
80953
80959
  const script = this.findMessageScript(
80954
80960
  id,
80955
80961
  `
80956
80962
  set theReply to reply msg without opening window${replyAllClause}
80957
80963
  set content of theReply to "${safeBody}"
80958
- ${sendAction}`
80964
+ ${finalAction}`
80959
80965
  );
80960
- const result = executeAppleScript(script, { timeoutMs: 6e4 });
80961
- if (!result.success || result.output.startsWith("error:")) {
80962
- console.error(`Failed to reply to message: ${result.error || result.output}`);
80963
- return false;
80964
- }
80965
- return true;
80966
+ return this.runComposeScript(script, "reply to");
80966
80967
  }
80967
80968
  /**
80968
80969
  * Forward a message.
@@ -80975,7 +80976,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
80975
80976
  */
80976
80977
  forwardMessage(id, to, body, send = true) {
80977
80978
  const safeBody = body ? escapeForAppleScriptBody(body) : "";
80978
- const sendAction = send ? "send theForward" : "";
80979
+ const finalAction = send ? "send theForward" : "save theForward";
80979
80980
  let recipientCommands = "";
80980
80981
  for (const addr of to) {
80981
80982
  recipientCommands += `make new to recipient at end of to recipients of theForward with properties {address:"${escapeForAppleScript(addr)}"}
@@ -80987,14 +80988,9 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
80987
80988
  set theForward to forward msg without opening window
80988
80989
  ${recipientCommands}
80989
80990
  ${safeBody ? `set content of theForward to "${safeBody}"` : ""}
80990
- ${sendAction}`
80991
+ ${finalAction}`
80991
80992
  );
80992
- const result = executeAppleScript(script, { timeoutMs: 6e4 });
80993
- if (!result.success || result.output.startsWith("error:")) {
80994
- console.error(`Failed to forward message: ${result.error || result.output}`);
80995
- return false;
80996
- }
80997
- return true;
80993
+ return this.runComposeScript(script, "forward");
80998
80994
  }
80999
80995
  /**
81000
80996
  * Helper to find and operate on a message by ID, scoped to the mailbox the id
@@ -81019,6 +81015,25 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
81019
81015
  * it immediately, so this is also where the previous operation's forensic
81020
81016
  * report is invalidated — see `beginMutation()`.
81021
81017
  */
81018
+ /**
81019
+ * Run a reply/forward compose script and surface Mail's OWN error text.
81020
+ *
81021
+ * These used to return a bare boolean and log the reason to stderr, so the
81022
+ * tool layer could only say "Failed to reply to message X". That hid the two
81023
+ * failures a caller can actually act on — an id present in several mailboxes
81024
+ * (which names the candidates and tells you to re-list) and a missing id —
81025
+ * behind one indistinguishable message.
81026
+ */
81027
+ runComposeScript(script, verb) {
81028
+ const result = executeAppleScript(script, { timeoutMs: 6e4 });
81029
+ if (!result.success || result.output.startsWith("error:")) {
81030
+ const raw = result.error || result.output;
81031
+ const error2 = raw.startsWith("error:") ? raw.slice("error:".length) : raw;
81032
+ console.error(`Failed to ${verb} message: ${error2}`);
81033
+ return { success: false, error: error2 };
81034
+ }
81035
+ return { success: true };
81036
+ }
81022
81037
  findMessageScript(id, operation, instrument = false) {
81023
81038
  this.beginMutation();
81024
81039
  const loc = this.locationFor(id);
@@ -82841,7 +82856,7 @@ ${actionStmts.join("\n")}
82841
82856
  message: "Mail.app is accessible"
82842
82857
  });
82843
82858
  } else {
82844
- const errorHint = mailCheck.error?.includes("not authorized") ? " (check System Settings > Privacy & Security > Automation)" : "";
82859
+ const errorHint = isPermissionDenied(mailCheck.error) ? " (check System Settings > Privacy & Security > Automation)" : "";
82845
82860
  checks.push({
82846
82861
  name: "mail_app",
82847
82862
  passed: false,
@@ -82857,7 +82872,7 @@ ${actionStmts.join("\n")}
82857
82872
  message: "AppleScript automation permissions granted"
82858
82873
  });
82859
82874
  } else {
82860
- const isPermError = permCheck.error?.includes("not authorized") || permCheck.error?.includes("not permitted");
82875
+ const isPermError = isPermissionDenied(permCheck.error);
82861
82876
  checks.push({
82862
82877
  name: "permissions",
82863
82878
  passed: !isPermError,
@@ -83681,7 +83696,12 @@ function structuredRow(m, account, path) {
83681
83696
  flagColorIndex: mailFlagColorIndex(m.flags),
83682
83697
  mailbox: path,
83683
83698
  account,
83684
- hasAttachments: false,
83699
+ // Derived from BODYSTRUCTURE, which the list/search fetch now requests.
83700
+ // This was hardcoded `false` from 2.2.0 until 2.11.1 — indistinguishable to
83701
+ // a caller from "no attachments", so every IMAP-sourced message claimed to
83702
+ // have none. Falls back to false only when the fetch carried no
83703
+ // BODYSTRUCTURE at all.
83704
+ hasAttachments: bodyStructureHasAttachments(m.bodyStructure),
83685
83705
  // Message-ID (when the envelope carries it) is the strongest cross-/intra-
83686
83706
  // backend dedup key for the multi-account merge (imapMultiAccount.ts). The
83687
83707
  // AppleScript path does not expose it, so cross-backend dedup falls back to
@@ -83712,7 +83732,10 @@ async function run(args, listMode, deps) {
83712
83732
  const byUid = /* @__PURE__ */ new Map();
83713
83733
  for await (const msg of client.fetch(
83714
83734
  newest.join(","),
83715
- { envelope: true, flags: true },
83735
+ // BODYSTRUCTURE rides along so `hasAttachments` is computed rather
83736
+ // than assumed. Measured on 50 real messages: ~390ms -> ~465ms for
83737
+ // the fetch (~17%), same single round trip, no extra request.
83738
+ { envelope: true, flags: true, bodyStructure: true },
83716
83739
  { uid: true }
83717
83740
  )) {
83718
83741
  byUid.set(msg.uid, msg);
@@ -84206,7 +84229,8 @@ function collectAttachments(node, out = []) {
84206
84229
  if (!node) return out;
84207
84230
  const filename = node.dispositionParameters?.filename || node.parameters?.name;
84208
84231
  const disposition = node.disposition?.toLowerCase();
84209
- const isAttachment = !!node.part && (disposition === "attachment" || !!filename && disposition !== "inline");
84232
+ const isEmbeddedByReference = disposition === "inline" && !!node.id;
84233
+ const isAttachment = !!node.part && (disposition === "attachment" || !!filename && !isEmbeddedByReference);
84210
84234
  if (isAttachment) {
84211
84235
  out.push({
84212
84236
  part: node.part,
@@ -84218,6 +84242,9 @@ function collectAttachments(node, out = []) {
84218
84242
  for (const child of node.childNodes ?? []) collectAttachments(child, out);
84219
84243
  return out;
84220
84244
  }
84245
+ function bodyStructureHasAttachments(node) {
84246
+ return !!node && collectAttachments(node).length > 0;
84247
+ }
84221
84248
  async function streamToBuffer(content, maxBytes) {
84222
84249
  const chunks = [];
84223
84250
  let total = 0;
@@ -84389,7 +84416,10 @@ async function imapThread(id, deps = {}, limit = 50) {
84389
84416
  const msgs = [];
84390
84417
  for await (const msg of client.fetch(
84391
84418
  uids.join(","),
84392
- { envelope: true, flags: true },
84419
+ // Same reason as the list/search fetch: get-thread emits structured
84420
+ // rows too, so it needs BODYSTRUCTURE or its hasAttachments would
84421
+ // silently disagree with the same message seen via search.
84422
+ { envelope: true, flags: true, bodyStructure: true },
84393
84423
  { uid: true }
84394
84424
  )) {
84395
84425
  msgs.push(msg);
@@ -85919,6 +85949,23 @@ function resolveSmtpOrFallback() {
85919
85949
  return null;
85920
85950
  }
85921
85951
  }
85952
+ async function toNumericMailId(id) {
85953
+ const ref = decodeImapId(id);
85954
+ if (!ref) return { numericId: id };
85955
+ const messageId = await imapFetchMessageId(id);
85956
+ if (!messageId) {
85957
+ return {
85958
+ error: `could not read the RFC Message-ID for "${id}" over IMAP, which is what maps it to a Mail.app id. Pass the numeric id instead (see the resolve-message-id tool).`
85959
+ };
85960
+ }
85961
+ const numericId = mailManager.findNumericIdByMessageId(messageId, ref.account);
85962
+ if (!numericId) {
85963
+ return {
85964
+ error: `Mail.app has no message with Message-ID <${messageId}> in account "${ref.account}", so this IMAP message has no numeric id to reply to or forward. It may not have synced to Mail.app yet.`
85965
+ };
85966
+ }
85967
+ return { numericId };
85968
+ }
85922
85969
  async function sendReplyViaSmtp(id, body, replyAll) {
85923
85970
  const cfg = resolveSmtpOrFallback();
85924
85971
  if (!cfg) return { sent: false, fallback: true };
@@ -85977,17 +86024,23 @@ registerTool(
85977
86024
  },
85978
86025
  withErrorHandling(async ({ id, body, replyAll, send }) => {
85979
86026
  if (send && isSmtpConfigured()) {
85980
- const outcome = await sendReplyViaSmtp(id, body, replyAll);
85981
- if (outcome.sent) {
86027
+ const outcome2 = await sendReplyViaSmtp(id, body, replyAll);
86028
+ if (outcome2.sent) {
85982
86029
  return successResponse("Reply sent", { ok: true, sent: true, id });
85983
86030
  }
85984
- if (!outcome.fallback) {
85985
- return errorResponse(`Failed to reply to message "${id}" via SMTP: ${outcome.error}`);
86031
+ if (!outcome2.fallback) {
86032
+ return errorResponse(`Failed to reply to message "${id}" via SMTP: ${outcome2.error}`);
85986
86033
  }
85987
86034
  }
85988
- const success = mailManager.replyToMessage(id, body, replyAll, send);
85989
- if (!success) {
85990
- return errorResponse(`Failed to reply to message "${id}"`);
86035
+ const resolvedReply = await toNumericMailId(id);
86036
+ if (!resolvedReply.numericId) {
86037
+ return errorResponse(`Failed to reply to message "${id}": ${resolvedReply.error}`);
86038
+ }
86039
+ const outcome = mailManager.replyToMessage(resolvedReply.numericId, body, replyAll, send);
86040
+ if (!outcome.success) {
86041
+ return errorResponse(
86042
+ outcome.error ? `Failed to reply to message "${id}": ${outcome.error}` : `Failed to reply to message "${id}"`
86043
+ );
85991
86044
  }
85992
86045
  return successResponse(send ? "Reply sent" : "Reply saved as draft", {
85993
86046
  ok: true,
@@ -86015,8 +86068,8 @@ registerTool(
86015
86068
  },
86016
86069
  withErrorHandling(async ({ id, to, body, send }) => {
86017
86070
  if (send && isSmtpConfigured()) {
86018
- const outcome = await sendForwardViaSmtp(id, to, body);
86019
- if (outcome.sent) {
86071
+ const outcome2 = await sendForwardViaSmtp(id, to, body);
86072
+ if (outcome2.sent) {
86020
86073
  return successResponse(`Message forwarded to ${to.join(", ")}`, {
86021
86074
  ok: true,
86022
86075
  sent: true,
@@ -86024,13 +86077,19 @@ registerTool(
86024
86077
  id
86025
86078
  });
86026
86079
  }
86027
- if (!outcome.fallback) {
86028
- return errorResponse(`Failed to forward message "${id}" via SMTP: ${outcome.error}`);
86080
+ if (!outcome2.fallback) {
86081
+ return errorResponse(`Failed to forward message "${id}" via SMTP: ${outcome2.error}`);
86029
86082
  }
86030
86083
  }
86031
- const success = mailManager.forwardMessage(id, to, body, send);
86032
- if (!success) {
86033
- return errorResponse(`Failed to forward message "${id}"`);
86084
+ const resolvedFwd = await toNumericMailId(id);
86085
+ if (!resolvedFwd.numericId) {
86086
+ return errorResponse(`Failed to forward message "${id}": ${resolvedFwd.error}`);
86087
+ }
86088
+ const outcome = mailManager.forwardMessage(resolvedFwd.numericId, to, body, send);
86089
+ if (!outcome.success) {
86090
+ return errorResponse(
86091
+ outcome.error ? `Failed to forward message "${id}": ${outcome.error}` : `Failed to forward message "${id}"`
86092
+ );
86034
86093
  }
86035
86094
  return successResponse(
86036
86095
  send ? `Message forwarded to ${to.join(", ")}` : "Forward saved as draft",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.11.0",
3
+ "version": "2.11.2",
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",