apple-mail-mcp 2.10.24 → 2.10.26

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 +5 -0
  2. package/build/index.js +112 -56
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -824,6 +824,11 @@ List attachments on a message.
824
824
 
825
825
  Save a message attachment to disk.
826
826
 
827
+ The destination must not already exist: `save-attachment` fails closed instead
828
+ of overwriting an existing file. AppleScript and MIME fallback paths stage the
829
+ bytes privately, commit with an exclusive create, and leave the saved file
830
+ owner-readable/writable (`0600`).
831
+
827
832
  | Parameter | Type | Required | Description |
828
833
  |-----------|------|----------|-------------|
829
834
  | `id` | string | Yes | Message ID |
package/build/index.js CHANGED
@@ -77516,6 +77516,8 @@ var StdioServerTransport = class {
77516
77516
  // src/services/appleMailManager.ts
77517
77517
  import { spawnSync as spawnSync2 } from "child_process";
77518
77518
  import {
77519
+ constants as fsConstants,
77520
+ chmodSync,
77519
77521
  existsSync as existsSync3,
77520
77522
  writeFileSync as writeFileSync3,
77521
77523
  readFileSync as readFileSync2,
@@ -78300,6 +78302,7 @@ var DIAG_ITEM_SEP = "M";
78300
78302
  var CONTENT_MARKER = "CONTENT";
78301
78303
  var MSGID_MARKER = "MSGID";
78302
78304
  var HTML_MARKER = "HTML";
78305
+ var LOOKUP_ERROR_MARKER = "ERR";
78303
78306
  var BATCH_FATAL = "FATAL";
78304
78307
  var RECON_TAG = "RECON";
78305
78308
  var SNAP_TAG = "SNAP";
@@ -78374,8 +78377,11 @@ function resolveAttachmentSaveTarget(savePath, attachmentName) {
78374
78377
  if (!isPathWithinAllowedRoots(savedPath)) {
78375
78378
  throw new Error(`Output path "${savedPath}" is outside allowed directories`);
78376
78379
  }
78377
- if (existsSync3(savedPath) && lstatSync(savedPath).isSymbolicLink()) {
78378
- throw new Error(`Refusing to overwrite symbolic link "${savedPath}"`);
78380
+ if (existsSync3(savedPath)) {
78381
+ if (lstatSync(savedPath).isSymbolicLink()) {
78382
+ throw new Error(`Refusing to overwrite symbolic link "${savedPath}"`);
78383
+ }
78384
+ throw new Error(`Refusing to overwrite existing file "${savedPath}"`);
78379
78385
  }
78380
78386
  return { saveDirectory, savedPath };
78381
78387
  }
@@ -78653,6 +78659,8 @@ var AppleMailManager = class {
78653
78659
  * misses and falls back to the full scan, so it can never wedge a lookup.
78654
78660
  */
78655
78661
  idLocationIndex = /* @__PURE__ */ new Map();
78662
+ /** Error from the most recent numeric message read, if it was refused. */
78663
+ lastMessageLookupError;
78656
78664
  /** Cap on the id→location index so a long-lived process can't grow unbounded. */
78657
78665
  ID_LOCATION_MAX = 5e3;
78658
78666
  /** Record (or refresh) where a message id lives, evicting oldest when full. */
@@ -78681,6 +78689,12 @@ var AppleMailManager = class {
78681
78689
  noteMessageLocation(id, account, mailbox) {
78682
78690
  this.rememberLocation(id, account, mailbox);
78683
78691
  }
78692
+ /** Consume the most recent read refusal so the tool layer can preserve it. */
78693
+ consumeLastMessageLookupError() {
78694
+ const error2 = this.lastMessageLookupError;
78695
+ this.lastMessageLookupError = void 0;
78696
+ return error2;
78697
+ }
78684
78698
  /**
78685
78699
  * AppleScript fragment resolving `account` + `mailbox` into `_tmb`, leaving
78686
78700
  * `_tmb` as `missing value` when it can't be pinned down. Exact-name match
@@ -79699,6 +79713,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79699
79713
  * returned the entire raw MIME blob mislabeled as HTML (#32).
79700
79714
  */
79701
79715
  getMessageContent(id, includeHtml = false, hint) {
79716
+ this.lastMessageLookupError = void 0;
79702
79717
  const sourceFetch = includeHtml ? `set htmlSource to ""
79703
79718
  try
79704
79719
  set htmlSource to source of msg
@@ -79724,17 +79739,25 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79724
79739
  }
79725
79740
  const script = buildAppLevelScript(`
79726
79741
  try
79742
+ set _hits to {}
79743
+ set _names to ""
79727
79744
  repeat with acct in accounts
79728
79745
  repeat with mb in mailboxes of acct
79729
79746
  try
79730
79747
  set matchingMsgs to (messages of mb whose id is ${Number(id)})
79731
79748
  if (count of matchingMsgs) > 0 then
79732
- set msg to item 1 of matchingMsgs
79733
- ${innerFetch}
79749
+ set end of _hits to item 1 of matchingMsgs
79750
+ set _names to _names & (name of acct) & "/" & (name of mb) & ", "
79734
79751
  end if
79735
79752
  end try
79736
79753
  end repeat
79737
79754
  end repeat
79755
+ if (count of _hits) is 0 then return "${LOOKUP_ERROR_MARKER}Message not found"
79756
+ 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"
79757
+ if (count of _hits) is 1 then
79758
+ set msg to item 1 of _hits
79759
+ ${innerFetch}
79760
+ end if
79738
79761
  return ""
79739
79762
  on error errMsg
79740
79763
  return ""
@@ -79756,6 +79779,10 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79756
79779
  if (!result.success) console.error(`Failed to get message content: ${result.error}`);
79757
79780
  return null;
79758
79781
  }
79782
+ if (result.output.startsWith(LOOKUP_ERROR_MARKER)) {
79783
+ this.lastMessageLookupError = result.output.slice(LOOKUP_ERROR_MARKER.length).trim();
79784
+ return null;
79785
+ }
79759
79786
  const htmlSplit = result.output.split(HTML_MARKER);
79760
79787
  const contentPart = htmlSplit[0];
79761
79788
  const rawSource = htmlSplit.length > 1 ? htmlSplit[1] : "";
@@ -79783,6 +79810,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79783
79810
  * a 20MB attachment can take several seconds over Exchange/IMAP.
79784
79811
  */
79785
79812
  getRawSource(id, hint) {
79813
+ this.lastMessageLookupError = void 0;
79786
79814
  const loc = hint?.account && hint?.mailbox ? { account: hint.account, mailbox: hint.mailbox } : this.idLocationIndex.get(id.toString());
79787
79815
  if (loc) {
79788
79816
  const scopedScript = this.scopedByIdScript(
@@ -79792,21 +79820,34 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79792
79820
  "return source of msg"
79793
79821
  );
79794
79822
  const scoped = executeAppleScript(scopedScript, { timeoutMs: 12e4 });
79795
- if (scoped.success && scoped.output.trim()) return scoped.output;
79823
+ if (scoped.success && scoped.output.trim() && !scoped.output.startsWith(LOOKUP_ERROR_MARKER)) {
79824
+ return scoped.output;
79825
+ }
79826
+ if (scoped.success && scoped.output.startsWith(LOOKUP_ERROR_MARKER)) {
79827
+ this.lastMessageLookupError = scoped.output.slice(LOOKUP_ERROR_MARKER.length).trim();
79828
+ }
79796
79829
  }
79797
79830
  const script = buildAppLevelScript(`
79798
79831
  try
79832
+ set _hits to {}
79833
+ set _names to ""
79799
79834
  repeat with acct in accounts
79800
79835
  repeat with mb in mailboxes of acct
79801
79836
  try
79802
79837
  set matchingMsgs to (messages of mb whose id is ${Number(id)})
79803
79838
  if (count of matchingMsgs) > 0 then
79804
- set msg to item 1 of matchingMsgs
79805
- return source of msg
79839
+ set end of _hits to item 1 of matchingMsgs
79840
+ set _names to _names & (name of acct) & "/" & (name of mb) & ", "
79806
79841
  end if
79807
79842
  end try
79808
79843
  end repeat
79809
79844
  end repeat
79845
+ if (count of _hits) is 0 then return "${LOOKUP_ERROR_MARKER}Message not found"
79846
+ 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"
79847
+ if (count of _hits) is 1 then
79848
+ set msg to item 1 of _hits
79849
+ return source of msg
79850
+ end if
79810
79851
  return ""
79811
79852
  on error errMsg
79812
79853
  return ""
@@ -79816,6 +79857,10 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
79816
79857
  if (!result.success || !result.output.trim()) {
79817
79858
  return null;
79818
79859
  }
79860
+ if (result.output.startsWith(LOOKUP_ERROR_MARKER)) {
79861
+ this.lastMessageLookupError = result.output.slice(LOOKUP_ERROR_MARKER.length).trim();
79862
+ return null;
79863
+ }
79819
79864
  return result.output;
79820
79865
  }
79821
79866
  /**
@@ -80256,27 +80301,13 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
80256
80301
  const safeBody = escapeForAppleScriptBody(body);
80257
80302
  const replyAllClause = replyAll ? " with reply to all" : "";
80258
80303
  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
- `);
80304
+ const script = this.findMessageScript(
80305
+ id,
80306
+ `
80307
+ set theReply to reply msg without opening window${replyAllClause}
80308
+ set content of theReply to "${safeBody}"
80309
+ ${sendAction}`
80310
+ );
80280
80311
  const result = executeAppleScript(script, { timeoutMs: 6e4 });
80281
80312
  if (!result.success || result.output.startsWith("error:")) {
80282
80313
  console.error(`Failed to reply to message: ${result.error || result.output}`);
@@ -80301,28 +80332,14 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
80301
80332
  recipientCommands += `make new to recipient at end of to recipients of theForward with properties {address:"${escapeForAppleScript(addr)}"}
80302
80333
  `;
80303
80334
  }
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
- `);
80335
+ const script = this.findMessageScript(
80336
+ id,
80337
+ `
80338
+ set theForward to forward msg without opening window
80339
+ ${recipientCommands}
80340
+ ${safeBody ? `set content of theForward to "${safeBody}"` : ""}
80341
+ ${sendAction}`
80342
+ );
80326
80343
  const result = executeAppleScript(script, { timeoutMs: 6e4 });
80327
80344
  if (!result.success || result.output.startsWith("error:")) {
80328
80345
  console.error(`Failed to forward message: ${result.error || result.output}`);
@@ -81150,7 +81167,21 @@ ${this.errorEmit(" ")}
81150
81167
  return false;
81151
81168
  }
81152
81169
  const safeName = escapeForAppleScript(attachmentName);
81153
- const safePath = escapeForAppleScript(target.saveDirectory);
81170
+ let temporaryDirectory;
81171
+ try {
81172
+ temporaryDirectory = mkdtempSync2(join4(target.saveDirectory, ".apple-mail-mcp-"));
81173
+ } catch (error2) {
81174
+ console.error(`Failed to create attachment staging directory: ${error2}`);
81175
+ return false;
81176
+ }
81177
+ const temporaryPath = join4(temporaryDirectory, "attachment");
81178
+ const safeTemporaryPath = escapeForAppleScript(temporaryPath);
81179
+ const cleanupTemporaryDirectory = () => {
81180
+ try {
81181
+ rmSync2(temporaryDirectory, { recursive: true, force: true });
81182
+ } catch {
81183
+ }
81184
+ };
81154
81185
  const numericId = Number(id);
81155
81186
  const script = buildAppLevelScript(`
81156
81187
  try
@@ -81162,7 +81193,7 @@ ${this.errorEmit(" ")}
81162
81193
  set msg to item 1 of matchingMsgs
81163
81194
  repeat with att in mail attachments of msg
81164
81195
  if name of att is "${safeName}" then
81165
- set savePath to POSIX file "${safePath}/${safeName}"
81196
+ set savePath to POSIX file "${safeTemporaryPath}"
81166
81197
  save att in savePath
81167
81198
  return "ok"
81168
81199
  end if
@@ -81179,8 +81210,18 @@ ${this.errorEmit(" ")}
81179
81210
  `);
81180
81211
  const result = executeAppleScript(script, { timeoutMs: 6e4 });
81181
81212
  if (result.success && result.output === "ok") {
81182
- return true;
81213
+ try {
81214
+ copyFileSync(temporaryPath, target.savedPath, fsConstants.COPYFILE_EXCL);
81215
+ chmodSync(target.savedPath, 384);
81216
+ cleanupTemporaryDirectory();
81217
+ return true;
81218
+ } catch (err) {
81219
+ cleanupTemporaryDirectory();
81220
+ console.error(`Failed to commit attachment to disk: ${err}`);
81221
+ return false;
81222
+ }
81183
81223
  }
81224
+ cleanupTemporaryDirectory();
81184
81225
  const rawSource = this.getRawSource(id);
81185
81226
  if (!rawSource) {
81186
81227
  console.error(`Failed to save attachment: could not retrieve message source`);
@@ -81191,12 +81232,24 @@ ${this.errorEmit(" ")}
81191
81232
  console.error(`Failed to save attachment: "${attachmentName}" not found in MIME source`);
81192
81233
  return false;
81193
81234
  }
81235
+ let mimeTemporaryDirectory;
81194
81236
  try {
81195
- writeFileSync3(target.savedPath, attachment.data);
81237
+ mimeTemporaryDirectory = mkdtempSync2(join4(target.saveDirectory, ".apple-mail-mcp-"));
81238
+ const mimeTemporaryPath = join4(mimeTemporaryDirectory, "attachment");
81239
+ writeFileSync3(mimeTemporaryPath, attachment.data, { flag: "wx", mode: 384 });
81240
+ copyFileSync(mimeTemporaryPath, target.savedPath, fsConstants.COPYFILE_EXCL);
81241
+ chmodSync(target.savedPath, 384);
81196
81242
  return true;
81197
81243
  } catch (err) {
81198
81244
  console.error(`Failed to write attachment to disk: ${err}`);
81199
81245
  return false;
81246
+ } finally {
81247
+ if (mimeTemporaryDirectory) {
81248
+ try {
81249
+ rmSync2(mimeTemporaryDirectory, { recursive: true, force: true });
81250
+ } catch {
81251
+ }
81252
+ }
81200
81253
  }
81201
81254
  }
81202
81255
  /**
@@ -84735,7 +84788,10 @@ Do not use when: you don't yet have an id (use search-messages or list-messages
84735
84788
  account,
84736
84789
  mailbox
84737
84790
  });
84738
- if (!content) return errorResponse(`Message with ID "${id}" not found`);
84791
+ if (!content) {
84792
+ const lookupError = mailManager.consumeLastMessageLookupError();
84793
+ return errorResponse(lookupError ?? `Message with ID "${id}" not found`);
84794
+ }
84739
84795
  const isHtml = preferHtml === true && !!content.htmlContent;
84740
84796
  const body = isHtml ? content.htmlContent : content.plainText;
84741
84797
  return successResponse(`Subject: ${content.subject}
@@ -85661,7 +85717,7 @@ registerTool(
85661
85717
  if (!r.success || !r.base64) {
85662
85718
  return errorResponse(r.error || `Failed to fetch attachment "${attachmentName}"`);
85663
85719
  }
85664
- writeFileSync4(target.savedPath, Buffer.from(r.base64, "base64"));
85720
+ writeFileSync4(target.savedPath, Buffer.from(r.base64, "base64"), { flag: "wx", mode: 384 });
85665
85721
  return successResponse(`Attachment "${attachmentName}" saved to ${savePath}`, {
85666
85722
  ok: true,
85667
85723
  attachmentName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.10.24",
3
+ "version": "2.10.26",
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",