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.
- package/build/index.js +159 -18
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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