apple-mail-mcp 2.8.8 → 2.8.9

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 +159 -18
  2. package/package.json +1 -1
package/build/index.js CHANGED
@@ -76095,6 +76095,14 @@ function extractTextBody(source) {
76095
76095
  const encoding = getHeader(headers, "Content-Transfer-Encoding");
76096
76096
  return decodeBody(body, encoding).toString("utf8");
76097
76097
  }
76098
+ function extractRfcMessageIdFromSource(source) {
76099
+ if (!source || !source.trim()) return "";
76100
+ const blankLineIdx = source.search(/\r?\n\r?\n/);
76101
+ const headers = blankLineIdx === -1 ? source : source.substring(0, blankLineIdx);
76102
+ const raw = getHeader(headers, "Message-ID") ?? getHeader(headers, "Message-Id");
76103
+ if (!raw) return "";
76104
+ return raw.trim().replace(/^<+/, "").replace(/>+$/, "").trim();
76105
+ }
76098
76106
  function extractMimeAttachment(source, attachmentName) {
76099
76107
  if (!source || !source.trim()) return null;
76100
76108
  const boundary = extractBoundary(source);
@@ -76341,8 +76349,12 @@ var DIAG_MARKER = "DIAG";
76341
76349
  var DIAG_FIELD_SEP = "F";
76342
76350
  var DIAG_ITEM_SEP = "M";
76343
76351
  var CONTENT_MARKER = "CONTENT";
76352
+ var MSGID_MARKER = "MSGID";
76344
76353
  var HTML_MARKER = "HTML";
76345
76354
  var BATCH_FATAL = "FATAL";
76355
+ function normalizeRfcMessageId(mid) {
76356
+ return (mid || "").trim().replace(/^<+/, "").replace(/>+$/, "").trim();
76357
+ }
76346
76358
  function mergeSearchDiagnostics(into, from) {
76347
76359
  into.timedOutAccounts.push(...from.timedOutAccounts);
76348
76360
  into.skippedLargeMailboxes.push(...from.skippedLargeMailboxes);
@@ -76621,6 +76633,32 @@ var AppleMailManager = class {
76621
76633
  };
76622
76634
  /** Cache TTL in milliseconds (60 seconds). */
76623
76635
  CACHE_TTL_MS = 6e4;
76636
+ /**
76637
+ * Remembers where each message id was last seen: id → {account, mailbox}.
76638
+ *
76639
+ * Mail.app numeric message ids are unique *per mailbox*, and by-id fetches
76640
+ * (getMessageContent/getRawSource) otherwise have to linear-scan every mailbox
76641
+ * of every account probing `whose id is N`. On a real multi-account setup that
76642
+ * is 700+ mailboxes; a message in a late-iterated folder (e.g. a large "Sent
76643
+ * Items") isn't reached before the AppleScript timeout fires, so the fetch
76644
+ * returns a false "not found" (only INBOX ids, reached early, worked). Every
76645
+ * search/list/by-id result records its id→location here so a subsequent fetch
76646
+ * opens the one right mailbox directly. A stale entry (message moved) simply
76647
+ * misses and falls back to the full scan, so it can never wedge a lookup.
76648
+ */
76649
+ idLocationIndex = /* @__PURE__ */ new Map();
76650
+ /** Cap on the id→location index so a long-lived process can't grow unbounded. */
76651
+ ID_LOCATION_MAX = 5e3;
76652
+ /** Record (or refresh) where a message id lives, evicting oldest when full. */
76653
+ rememberLocation(id, account, mailbox) {
76654
+ if (!id || !account || !mailbox) return;
76655
+ if (this.idLocationIndex.has(id)) this.idLocationIndex.delete(id);
76656
+ this.idLocationIndex.set(id, { account, mailbox });
76657
+ if (this.idLocationIndex.size > this.ID_LOCATION_MAX) {
76658
+ const oldest = this.idLocationIndex.keys().next().value;
76659
+ if (oldest !== void 0) this.idLocationIndex.delete(oldest);
76660
+ }
76661
+ }
76624
76662
  /**
76625
76663
  * Returns cached accounts or fetches fresh data if cache is expired/empty.
76626
76664
  */
@@ -77123,6 +77161,7 @@ var AppleMailManager = class {
77123
77161
  }
77124
77162
  const parts = result.output.split(FIELD_SEP);
77125
77163
  if (parts.length < 9) return null;
77164
+ this.rememberLocation(id.toString(), parts[8], parts[7]);
77126
77165
  return {
77127
77166
  id: id.toString(),
77128
77167
  subject: parts[0],
@@ -77138,6 +77177,47 @@ var AppleMailManager = class {
77138
77177
  hasAttachments: parts.length > 9 ? parts[9] === "true" : false
77139
77178
  };
77140
77179
  }
77180
+ /**
77181
+ * Build an app-level AppleScript that opens exactly one account+mailbox, finds
77182
+ * the message with numeric `id` in it, and runs `innerAction` (which may assume
77183
+ * `msg` is bound). Used by the by-id fast paths (getMessageContent/getRawSource)
77184
+ * so a message in a late-iterated large folder resolves directly instead of via
77185
+ * the timeout-prone full-mailbox scan.
77186
+ *
77187
+ * The mailbox name is resolved through `resolveMailbox` (so an alias like
77188
+ * "Sent"→"Sent Items" or a casing mismatch like "INBOX"→"Inbox" still opens the
77189
+ * right folder), and matched case-insensitively by iterating the account's
77190
+ * mailboxes — `mailbox "INBOX" of account …` throws on accounts whose inbox is
77191
+ * actually named "Inbox", which would silently drop us back to the slow scan.
77192
+ * Returns "" (found nothing) on any error, so the caller falls back safely.
77193
+ */
77194
+ scopedByIdScript(account, mailbox, id, innerAction) {
77195
+ const resolved = this.resolveMailbox(mailbox, account);
77196
+ return buildAppLevelScript(`
77197
+ try
77198
+ set acct to (first account whose name is "${escapeForAppleScript(account)}")
77199
+ set targetMb to missing value
77200
+ ignoring case
77201
+ repeat with mb in mailboxes of acct
77202
+ if (name of mb) is "${escapeForAppleScript(resolved)}" then
77203
+ set targetMb to mb
77204
+ exit repeat
77205
+ end if
77206
+ end repeat
77207
+ end ignoring
77208
+ if targetMb is not missing value then
77209
+ set matchingMsgs to (messages of targetMb whose id is ${Number(id)})
77210
+ if (count of matchingMsgs) > 0 then
77211
+ set msg to item 1 of matchingMsgs
77212
+ ${innerAction}
77213
+ end if
77214
+ end if
77215
+ return ""
77216
+ on error errMsg
77217
+ return ""
77218
+ end try
77219
+ `);
77220
+ }
77141
77221
  /**
77142
77222
  * Get the content of a message.
77143
77223
  *
@@ -77148,11 +77228,30 @@ var AppleMailManager = class {
77148
77228
  * path doesn't need it; fetching it unconditionally was both slow and, worse,
77149
77229
  * returned the entire raw MIME blob mislabeled as HTML (#32).
77150
77230
  */
77151
- getMessageContent(id, includeHtml = false) {
77231
+ getMessageContent(id, includeHtml = false, hint) {
77152
77232
  const sourceFetch = includeHtml ? `set htmlSource to ""
77153
77233
  try
77154
77234
  set htmlSource to source of msg
77155
77235
  end try` : `set htmlSource to ""`;
77236
+ const innerFetch = `
77237
+ set msgSubject to subject of msg
77238
+ set msgRfcId to ""
77239
+ try
77240
+ set msgRfcId to message id of msg
77241
+ end try
77242
+ set msgContent to content of msg
77243
+ ${sourceFetch}
77244
+ return msgSubject & "${MSGID_MARKER}" & msgRfcId & "${CONTENT_MARKER}" & msgContent & "${HTML_MARKER}" & htmlSource`;
77245
+ const loc = hint?.account && hint?.mailbox ? { account: hint.account, mailbox: hint.mailbox } : this.idLocationIndex.get(id.toString());
77246
+ if (loc) {
77247
+ const scopedScript = this.scopedByIdScript(loc.account, loc.mailbox, id, innerFetch);
77248
+ const scoped = this.parseMessageContent(
77249
+ id,
77250
+ executeAppleScript(scopedScript, { timeoutMs: 6e4 }),
77251
+ includeHtml
77252
+ );
77253
+ if (scoped) return scoped;
77254
+ }
77156
77255
  const script = buildAppLevelScript(`
77157
77256
  try
77158
77257
  repeat with acct in accounts
@@ -77161,10 +77260,7 @@ var AppleMailManager = class {
77161
77260
  set matchingMsgs to (messages of mb whose id is ${Number(id)})
77162
77261
  if (count of matchingMsgs) > 0 then
77163
77262
  set msg to item 1 of matchingMsgs
77164
- set msgSubject to subject of msg
77165
- set msgContent to content of msg
77166
- ${sourceFetch}
77167
- return msgSubject & "${CONTENT_MARKER}" & msgContent & "${HTML_MARKER}" & htmlSource
77263
+ ${innerFetch}
77168
77264
  end if
77169
77265
  end try
77170
77266
  end repeat
@@ -77174,9 +77270,20 @@ var AppleMailManager = class {
77174
77270
  return ""
77175
77271
  end try
77176
77272
  `);
77177
- const result = executeAppleScript(script, { timeoutMs: 6e4 });
77273
+ return this.parseMessageContent(
77274
+ id,
77275
+ executeAppleScript(script, { timeoutMs: 6e4 }),
77276
+ includeHtml
77277
+ );
77278
+ }
77279
+ /**
77280
+ * Parse the marker-delimited output of a getMessageContent AppleScript into a
77281
+ * MessageContent, or null when nothing was found / the fetch failed. Shared by
77282
+ * the scoped fast path and the full-mailbox-scan fallback.
77283
+ */
77284
+ parseMessageContent(id, result, includeHtml) {
77178
77285
  if (!result.success || !result.output.trim()) {
77179
- console.error(`Failed to get message content: ${result.error}`);
77286
+ if (!result.success) console.error(`Failed to get message content: ${result.error}`);
77180
77287
  return null;
77181
77288
  }
77182
77289
  const htmlSplit = result.output.split(HTML_MARKER);
@@ -77184,12 +77291,16 @@ var AppleMailManager = class {
77184
77291
  const rawSource = htmlSplit.length > 1 ? htmlSplit[1] : "";
77185
77292
  const parts = contentPart.split(CONTENT_MARKER);
77186
77293
  if (parts.length < 2) return null;
77294
+ const subjParts = parts[0].split(MSGID_MARKER);
77295
+ const subject = subjParts[0];
77296
+ const rfcMessageId = normalizeRfcMessageId(subjParts.length > 1 ? subjParts[1] : "");
77187
77297
  const htmlContent = includeHtml && rawSource ? extractHtmlBody(rawSource) || void 0 : void 0;
77188
77298
  return {
77189
77299
  id: id.toString(),
77190
- subject: parts[0],
77300
+ subject,
77191
77301
  plainText: parts[1],
77192
- htmlContent
77302
+ htmlContent,
77303
+ rfcMessageId
77193
77304
  };
77194
77305
  }
77195
77306
  /**
@@ -77201,7 +77312,18 @@ var AppleMailManager = class {
77201
77312
  * the entire raw message including base64-encoded attachments —
77202
77313
  * a 20MB attachment can take several seconds over Exchange/IMAP.
77203
77314
  */
77204
- getRawSource(id) {
77315
+ getRawSource(id, hint) {
77316
+ const loc = hint?.account && hint?.mailbox ? { account: hint.account, mailbox: hint.mailbox } : this.idLocationIndex.get(id.toString());
77317
+ if (loc) {
77318
+ const scopedScript = this.scopedByIdScript(
77319
+ loc.account,
77320
+ loc.mailbox,
77321
+ id,
77322
+ "return source of msg"
77323
+ );
77324
+ const scoped = executeAppleScript(scopedScript, { timeoutMs: 12e4 });
77325
+ if (scoped.success && scoped.output.trim()) return scoped.output;
77326
+ }
77205
77327
  const script = buildAppLevelScript(`
77206
77328
  try
77207
77329
  repeat with acct in accounts
@@ -77372,8 +77494,9 @@ var AppleMailManager = class {
77372
77494
  } else if (parts.length === 7) {
77373
77495
  hasAttachments = parts[6] === "true";
77374
77496
  }
77497
+ const msgId = parts[0].trim();
77375
77498
  messages.push({
77376
- id: parts[0].trim(),
77499
+ id: msgId,
77377
77500
  subject: parts[1],
77378
77501
  sender: parts[2],
77379
77502
  recipients: [],
@@ -77386,6 +77509,7 @@ var AppleMailManager = class {
77386
77509
  account,
77387
77510
  hasAttachments
77388
77511
  });
77512
+ this.rememberLocation(msgId, account, msgMailbox);
77389
77513
  }
77390
77514
  return messages;
77391
77515
  }
@@ -80964,20 +81088,32 @@ ${messageList}${coverageBlock}`,
80964
81088
  server.registerTool(
80965
81089
  "get-message",
80966
81090
  {
80967
- description: "Use when: reading the full body of one message whose id you already have (numeric or imap:\u2026); set preferHtml to get the HTML body instead of plain text.\nReturns: the message subject and body (plain text by default, HTML when preferHtml is true).\nDo not use when: you don't yet have an id (use search-messages or list-messages first), or you want the whole conversation (use get-thread).",
81091
+ description: `Use when: reading the full body of one message whose id you already have (numeric or imap:\u2026); set preferHtml to get the HTML body instead of plain text.
81092
+ Returns: the message subject, body (plain text by default, HTML when preferHtml is true), and its stable RFC Message-ID (rfcMessageId) for dedup/threading.
81093
+ Tip: pass the mailbox+account you got the id from (e.g. from search-messages) to fetch it directly \u2014 required for reliable reads of large folders like "Sent Items", which otherwise time out.
81094
+ Do not use when: you don't yet have an id (use search-messages or list-messages first), or you want the whole conversation (use get-thread).`,
80968
81095
  inputSchema: {
80969
81096
  id: MESSAGE_ID_SCHEMA,
80970
- preferHtml: external_exports.boolean().optional().describe("Return the HTML body (extracted from the message source) instead of plain text")
81097
+ preferHtml: external_exports.boolean().optional().describe("Return the HTML body (extracted from the message source) instead of plain text"),
81098
+ mailbox: external_exports.string().optional().describe(
81099
+ 'Mailbox that holds the message (e.g. "Sent Items"). Numeric ids are unique per mailbox; supplying this (with account) opens that mailbox directly instead of scanning every mailbox, which is required to read large folders like Sent Items without timing out.'
81100
+ ),
81101
+ account: external_exports.string().optional().describe(
81102
+ "Account that holds the message. Pair with `mailbox` for a direct, scan-free fetch."
81103
+ )
80971
81104
  },
80972
81105
  outputSchema: {
80973
81106
  id: external_exports.string().optional(),
80974
81107
  subject: external_exports.string().optional(),
80975
81108
  body: external_exports.string().optional(),
80976
- isHtml: external_exports.boolean().optional()
81109
+ isHtml: external_exports.boolean().optional(),
81110
+ rfcMessageId: external_exports.string().optional().describe(
81111
+ "Stable RFC 5322 Message-ID (angle brackets stripped); empty when the message has none"
81112
+ )
80977
81113
  }
80978
81114
  },
80979
81115
  withErrorHandling(
80980
- ({ id, preferHtml }) => routeMessage(id, {
81116
+ ({ id, preferHtml, mailbox, account }) => routeMessage(id, {
80981
81117
  // IMAP id (imap:…) → fetch via IMAP (#43 Phase 3); else AppleScript.
80982
81118
  imap: () => imapGetMessage(id, preferHtml === true),
80983
81119
  // IMAP path: parse subject/body out of the returned source so the
@@ -80989,11 +81125,15 @@ server.registerTool(
80989
81125
  id,
80990
81126
  subject: subjectFromGetMessage(r.info),
80991
81127
  body: sep2 >= 0 ? r.info.slice(sep2 + 2) : r.info,
80992
- isHtml: preferHtml === true
81128
+ isHtml: preferHtml === true,
81129
+ rfcMessageId: extractRfcMessageIdFromSource(r.info)
80993
81130
  };
80994
81131
  },
80995
81132
  apple: () => {
80996
- const content = mailManager.getMessageContent(id, preferHtml === true);
81133
+ const content = mailManager.getMessageContent(id, preferHtml === true, {
81134
+ account,
81135
+ mailbox
81136
+ });
80997
81137
  if (!content) return errorResponse(`Message with ID "${id}" not found`);
80998
81138
  const isHtml = preferHtml === true && !!content.htmlContent;
80999
81139
  const body = isHtml ? content.htmlContent : content.plainText;
@@ -81003,7 +81143,8 @@ ${body}`, {
81003
81143
  id,
81004
81144
  subject: content.subject,
81005
81145
  body,
81006
- isHtml
81146
+ isHtml,
81147
+ rfcMessageId: content.rfcMessageId ?? ""
81007
81148
  });
81008
81149
  },
81009
81150
  ok: "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.8.8",
3
+ "version": "2.8.9",
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",