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.
- package/build/index.js +97 -38
- 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:
|
|
78078
|
-
message:
|
|
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
|
|
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
|
-
${
|
|
80964
|
+
${finalAction}`
|
|
80959
80965
|
);
|
|
80960
|
-
|
|
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
|
|
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
|
-
${
|
|
80991
|
+
${finalAction}`
|
|
80991
80992
|
);
|
|
80992
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
85981
|
-
if (
|
|
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 (!
|
|
85985
|
-
return errorResponse(`Failed to reply to message "${id}" via SMTP: ${
|
|
86031
|
+
if (!outcome2.fallback) {
|
|
86032
|
+
return errorResponse(`Failed to reply to message "${id}" via SMTP: ${outcome2.error}`);
|
|
85986
86033
|
}
|
|
85987
86034
|
}
|
|
85988
|
-
const
|
|
85989
|
-
if (!
|
|
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
|
|
86019
|
-
if (
|
|
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 (!
|
|
86028
|
-
return errorResponse(`Failed to forward message "${id}" via SMTP: ${
|
|
86080
|
+
if (!outcome2.fallback) {
|
|
86081
|
+
return errorResponse(`Failed to forward message "${id}" via SMTP: ${outcome2.error}`);
|
|
86029
86082
|
}
|
|
86030
86083
|
}
|
|
86031
|
-
const
|
|
86032
|
-
if (!
|
|
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