apple-mail-mcp 2.17.7 → 2.17.8
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/README.md +14 -4
- package/build/index.js +275 -213
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -725,7 +725,7 @@ Return an attachment's bytes as base64 (the read counterpart to inline-base64 se
|
|
|
725
725
|
|
|
726
726
|
#### `resolve-message-id`
|
|
727
727
|
|
|
728
|
-
Map `imap:` message IDs to their numeric Mail.app IDs, via each message's RFC 5322 `Message-ID` (the join key both backends share). Needed
|
|
728
|
+
Map `imap:` message IDs to their numeric Mail.app IDs, via each message's RFC 5322 `Message-ID` (the join key both backends share). Needed for the AppleScript reply/forward path; direct SMTP replies and forwards read `imap:` IDs without numeric conversion. Numeric IDs pass through unchanged.
|
|
729
729
|
|
|
730
730
|
| Parameter | Type | Required | Description |
|
|
731
731
|
|-----------|------|----------|-------------|
|
|
@@ -747,6 +747,7 @@ Reply to an existing message.
|
|
|
747
747
|
| `body` | string | Yes | Reply body |
|
|
748
748
|
| `replyAll` | boolean | No | Reply to all recipients (default: false) |
|
|
749
749
|
| `send` | boolean | No | Send immediately (default: true, false = save as draft) |
|
|
750
|
+
| `transport` | string | No | `smtp` or `applescript`; omitted prefers configured SMTP when sending. Drafts use AppleScript. |
|
|
750
751
|
|
|
751
752
|
**Example - Reply to sender only:**
|
|
752
753
|
```json
|
|
@@ -766,7 +767,13 @@ Reply to an existing message.
|
|
|
766
767
|
}
|
|
767
768
|
```
|
|
768
769
|
|
|
769
|
-
|
|
770
|
+
**Delivery:** `imap:` IDs are read directly from their encoded account, mailbox, and UID. Numeric IDs are read through Mail.app. With SMTP configured, replies use clean MIME with the original `Message-ID` in `In-Reply-To` and the full `References` chain. Only the original plain-text body is quoted; the new text is not. Pass `transport: "smtp"` to require this path, or `transport: "applescript"` to use Mail.app explicitly.
|
|
771
|
+
|
|
772
|
+
**Failure behavior:** once SMTP is selected, a missing password, unreadable source, missing reply address or `Message-ID`, or SMTP failure returns an error. It does not silently switch to AppleScript or create an unthreaded new message. With SMTP unconfigured and transport omitted, AppleScript remains available. `send: false` saves a Mail.app draft; combining it with `transport: "smtp"` is rejected before composing.
|
|
773
|
+
|
|
774
|
+
The direct IMAP source read is bounded to 25 MiB, including MIME attachments. Its account login must match the SMTP login, configured From, or an explicitly configured `APPLE_MAIL_MCP_SMTP_ALLOWED_FROM` identity; otherwise the call fails rather than sending from an unrelated account. The SMTP configuration still represents a single sending identity, not a per-account SMTP registry.
|
|
775
|
+
|
|
776
|
+
Success includes `transport` and, for SMTP when returned by the server, the new `messageId`. SMTP submission does not add a local Sent copy; server-side Sent-folder behavior is unchanged. `send-email` is for **new conversations**: adding `Re:` to its subject does not add threading headers. Use this reply tool with the original id instead.
|
|
770
777
|
|
|
771
778
|
**⚠️ Safety:** With the default `send: true`, sends real mail immediately and cannot be unsent. Confirm the recipients, subject, and body with the user before calling (or pass `send: false` to save a draft for review).
|
|
772
779
|
|
|
@@ -782,8 +789,11 @@ Forward a message to new recipients.
|
|
|
782
789
|
| `to` | string[] | Yes | Recipients to forward to |
|
|
783
790
|
| `body` | string | No | Message to prepend |
|
|
784
791
|
| `send` | boolean | No | Send immediately (default: true, false = save as draft) |
|
|
792
|
+
| `transport` | string | No | `smtp` or `applescript`; omitted prefers configured SMTP when sending. Drafts use AppleScript. |
|
|
793
|
+
|
|
794
|
+
**Delivery:** uses the same source lookup, transport selection, 25 MiB source limit, account-identity check, and failure behavior as `reply-to-message`. A forward deliberately starts a new conversation, so it has no `In-Reply-To` or `References` headers. The existing plain-text forwarding behavior is unchanged: original attachments are not reattached. See [SMTP transport](#smtp-transport).
|
|
785
795
|
|
|
786
|
-
|
|
796
|
+
SMTP forwarding requires a readable plain-text original. HTML-only IMAP messages and failed Mail.app body reads return an error before sending instead of silently omitting the original content. Explicitly select `transport: "applescript"` to forward these with Mail.app; no automatic fallback occurs. An intentionally empty plain-text message is still valid.
|
|
787
797
|
|
|
788
798
|
**⚠️ Safety:** With the default `send: true`, sends real mail immediately and cannot be unsent. Confirm the recipients, subject, and body with the user before calling (or pass `send: false` to save a draft for review).
|
|
789
799
|
|
|
@@ -1838,7 +1848,7 @@ Prior to v1.4.0, `reply-to-message` and `forward-message` would send messages wi
|
|
|
1838
1848
|
|
|
1839
1849
|
**Fix:** Replaced `with opening window` with `without opening window` for both `reply` and `forward` commands. With this approach, `set content` works immediately and reliably from background processes. `In-Reply-To` and `References` headers are still set correctly by Mail.app, and no GUI compose window is opened.
|
|
1840
1850
|
|
|
1841
|
-
**Update (v2.5.0):** when SMTP is configured, `reply-to-message` and `forward-message` now prefer **clean direct SMTP** instead of AppleScript — the same prefer-direct model as `send-email`. Replies are threaded with RFC 5322 `In-Reply-To`/`References` headers built from the original message; forwards start a new conversation.
|
|
1851
|
+
**Update (v2.5.0):** when SMTP is configured, `reply-to-message` and `forward-message` now prefer **clean direct SMTP** instead of AppleScript — the same prefer-direct model as `send-email`. Replies are threaded with RFC 5322 `In-Reply-To`/`References` headers built from the original message; forwards start a new conversation. With transport omitted, the AppleScript `without opening window` path above is used only when SMTP is not configured or a draft is requested. Since v2.17.8, a selected SMTP path returns source/configuration/threading errors instead of silently falling back; explicit `transport: "applescript"` remains available.
|
|
1842
1852
|
|
|
1843
1853
|
See [#7](https://github.com/sweetrb/apple-mail-mcp/issues/7) for full details and the list of approaches that were tested.
|
|
1844
1854
|
|
package/build/index.js
CHANGED
|
@@ -84179,123 +84179,6 @@ async function sendSerialViaSmtp(recipients, subject, body, config2, opts = {})
|
|
|
84179
84179
|
return results;
|
|
84180
84180
|
}
|
|
84181
84181
|
|
|
84182
|
-
// src/services/replyForward.ts
|
|
84183
|
-
function extractAddresses(headerValue) {
|
|
84184
|
-
if (!headerValue.trim()) return [];
|
|
84185
|
-
return headerValue.split(",").map((part) => {
|
|
84186
|
-
const angle = part.match(/<([^>]+)>/);
|
|
84187
|
-
return (angle ? angle[1] : part).trim();
|
|
84188
|
-
}).filter((addr) => addr.includes("@"));
|
|
84189
|
-
}
|
|
84190
|
-
function parseOriginalHeaders(raw) {
|
|
84191
|
-
const headerBlock = raw.split(/\r?\n\r?\n/)[0] ?? "";
|
|
84192
|
-
const lines = [];
|
|
84193
|
-
for (const line of headerBlock.split(/\r?\n/)) {
|
|
84194
|
-
if (/^[ \t]/.test(line) && lines.length > 0) {
|
|
84195
|
-
lines[lines.length - 1] += " " + line.trim();
|
|
84196
|
-
} else {
|
|
84197
|
-
lines.push(line);
|
|
84198
|
-
}
|
|
84199
|
-
}
|
|
84200
|
-
const get = (name) => {
|
|
84201
|
-
const prefix = `${name.toLowerCase()}:`;
|
|
84202
|
-
const found = lines.find((l) => l.toLowerCase().startsWith(prefix));
|
|
84203
|
-
return found ? found.slice(found.indexOf(":") + 1).trim() : void 0;
|
|
84204
|
-
};
|
|
84205
|
-
const rawMessageId = get("Message-ID");
|
|
84206
|
-
const messageId = rawMessageId?.match(/<[^>]+>/)?.[0] ?? rawMessageId ?? void 0;
|
|
84207
|
-
const references = `${get("References") ?? ""} ${get("In-Reply-To") ?? ""}`.match(/<[^>]+>/g) ?? [];
|
|
84208
|
-
return {
|
|
84209
|
-
messageId,
|
|
84210
|
-
references,
|
|
84211
|
-
from: extractAddresses(get("From") ?? ""),
|
|
84212
|
-
replyTo: extractAddresses(get("Reply-To") ?? ""),
|
|
84213
|
-
to: extractAddresses(get("To") ?? ""),
|
|
84214
|
-
cc: extractAddresses(get("Cc") ?? ""),
|
|
84215
|
-
subject: get("Subject") ?? "",
|
|
84216
|
-
date: get("Date")
|
|
84217
|
-
};
|
|
84218
|
-
}
|
|
84219
|
-
function dedupe(addrs) {
|
|
84220
|
-
const seen = /* @__PURE__ */ new Set();
|
|
84221
|
-
const out = [];
|
|
84222
|
-
for (const a of addrs) {
|
|
84223
|
-
const k = a.toLowerCase();
|
|
84224
|
-
if (!seen.has(k)) {
|
|
84225
|
-
seen.add(k);
|
|
84226
|
-
out.push(a);
|
|
84227
|
-
}
|
|
84228
|
-
}
|
|
84229
|
-
return out;
|
|
84230
|
-
}
|
|
84231
|
-
function withSubjectPrefix(subject, prefix) {
|
|
84232
|
-
const s = subject.trim();
|
|
84233
|
-
const present = prefix === "Re:" ? /^re:/i : /^(fwd?|fw):/i;
|
|
84234
|
-
return present.test(s) ? s : `${prefix} ${s}`;
|
|
84235
|
-
}
|
|
84236
|
-
function quoteBody(plainText) {
|
|
84237
|
-
return plainText.replace(/\s+$/, "").split(/\r?\n/).map((l) => l ? `> ${l}` : ">").join("\n");
|
|
84238
|
-
}
|
|
84239
|
-
function buildReplyOptions(args) {
|
|
84240
|
-
const { original, originalPlainText, body, replyAll, self, from } = args;
|
|
84241
|
-
const selfSet = new Set(self.filter(Boolean).map((s) => s.toLowerCase()));
|
|
84242
|
-
const to = dedupe((original.replyTo.length ? original.replyTo : original.from).filter(Boolean));
|
|
84243
|
-
const toSet = new Set(to.map((t) => t.toLowerCase()));
|
|
84244
|
-
let cc;
|
|
84245
|
-
if (replyAll) {
|
|
84246
|
-
const extra = dedupe(
|
|
84247
|
-
[...original.to, ...original.cc].filter(
|
|
84248
|
-
(a) => !selfSet.has(a.toLowerCase()) && !toSet.has(a.toLowerCase())
|
|
84249
|
-
)
|
|
84250
|
-
);
|
|
84251
|
-
cc = extra.length ? extra : void 0;
|
|
84252
|
-
}
|
|
84253
|
-
const attribution = buildAttribution(original);
|
|
84254
|
-
const quoted = originalPlainText.trim() ? `
|
|
84255
|
-
|
|
84256
|
-
${attribution}${quoteBody(originalPlainText)}` : "";
|
|
84257
|
-
const references = dedupe(
|
|
84258
|
-
original.messageId ? [...original.references, original.messageId] : original.references
|
|
84259
|
-
);
|
|
84260
|
-
return {
|
|
84261
|
-
to,
|
|
84262
|
-
cc,
|
|
84263
|
-
subject: withSubjectPrefix(original.subject, "Re:"),
|
|
84264
|
-
body: `${body}${quoted}`,
|
|
84265
|
-
inReplyTo: original.messageId,
|
|
84266
|
-
references: references.length ? references : void 0,
|
|
84267
|
-
from
|
|
84268
|
-
};
|
|
84269
|
-
}
|
|
84270
|
-
function buildAttribution(original) {
|
|
84271
|
-
const who = original.from[0] ?? original.replyTo[0] ?? "the sender";
|
|
84272
|
-
return original.date ? `On ${original.date}, ${who} wrote:
|
|
84273
|
-
` : `${who} wrote:
|
|
84274
|
-
`;
|
|
84275
|
-
}
|
|
84276
|
-
function buildForwardOptions(args) {
|
|
84277
|
-
const { original, originalPlainText, to, body, from } = args;
|
|
84278
|
-
const headerBlock = [
|
|
84279
|
-
"---------- Forwarded message ----------",
|
|
84280
|
-
original.from.length ? `From: ${original.from.join(", ")}` : "",
|
|
84281
|
-
original.date ? `Date: ${original.date}` : "",
|
|
84282
|
-
`Subject: ${original.subject}`,
|
|
84283
|
-
original.to.length ? `To: ${original.to.join(", ")}` : "",
|
|
84284
|
-
original.cc.length ? `Cc: ${original.cc.join(", ")}` : ""
|
|
84285
|
-
].filter(Boolean).join("\n");
|
|
84286
|
-
const prefix = body?.trim() ? `${body}
|
|
84287
|
-
|
|
84288
|
-
` : "";
|
|
84289
|
-
return {
|
|
84290
|
-
to: dedupe(to),
|
|
84291
|
-
subject: withSubjectPrefix(original.subject, "Fwd:"),
|
|
84292
|
-
body: `${prefix}${headerBlock}
|
|
84293
|
-
|
|
84294
|
-
${originalPlainText}`,
|
|
84295
|
-
from
|
|
84296
|
-
};
|
|
84297
|
-
}
|
|
84298
|
-
|
|
84299
84182
|
// src/services/imapClient.ts
|
|
84300
84183
|
var import_imapflow = __toESM(require_imap_flow(), 1);
|
|
84301
84184
|
var IMAP_ENV = {
|
|
@@ -85058,6 +84941,34 @@ async function withMailbox(path, deps, fn) {
|
|
|
85058
84941
|
}
|
|
85059
84942
|
});
|
|
85060
84943
|
}
|
|
84944
|
+
var MAX_COMPOSE_SOURCE_BYTES = 25 * 1024 * 1024;
|
|
84945
|
+
async function imapGetMessageSource(id, deps = {}) {
|
|
84946
|
+
const ref = decodeImapId(id);
|
|
84947
|
+
if (!ref) throw new Error(`Not an IMAP message id: "${id}".`);
|
|
84948
|
+
return withClient(depsForMessageRef(ref, deps), async (client, cfg) => {
|
|
84949
|
+
const lock = await client.getMailboxLock(ref.path);
|
|
84950
|
+
try {
|
|
84951
|
+
const msg = await client.fetchOne(
|
|
84952
|
+
String(ref.uid),
|
|
84953
|
+
{
|
|
84954
|
+
envelope: true,
|
|
84955
|
+
// One extra byte distinguishes an exact-limit source from truncation.
|
|
84956
|
+
source: { start: 0, maxLength: MAX_COMPOSE_SOURCE_BYTES + 1 }
|
|
84957
|
+
},
|
|
84958
|
+
{ uid: true }
|
|
84959
|
+
);
|
|
84960
|
+
if (!msg) throw new Error(`IMAP message UID ${ref.uid} not found in "${ref.path}".`);
|
|
84961
|
+
if (!msg.source || !msg.source.length)
|
|
84962
|
+
throw new Error("IMAP returned no original message source.");
|
|
84963
|
+
if (Buffer.byteLength(msg.source) > MAX_COMPOSE_SOURCE_BYTES) {
|
|
84964
|
+
throw new Error("Original message source exceeds the 25 MiB reply/forward limit.");
|
|
84965
|
+
}
|
|
84966
|
+
return { raw: msg.source.toString(), subject: msg.envelope?.subject, accountUser: cfg.user };
|
|
84967
|
+
} finally {
|
|
84968
|
+
lock.release();
|
|
84969
|
+
}
|
|
84970
|
+
});
|
|
84971
|
+
}
|
|
85061
84972
|
async function imapGetMessage(id, preferHtml, deps = {}) {
|
|
85062
84973
|
const ref = decodeImapId(id);
|
|
85063
84974
|
if (!ref) return { success: false, error: `Not an IMAP message id: "${id}".` };
|
|
@@ -85552,6 +85463,123 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
85552
85463
|
);
|
|
85553
85464
|
}
|
|
85554
85465
|
|
|
85466
|
+
// src/services/replyForward.ts
|
|
85467
|
+
function extractAddresses(headerValue) {
|
|
85468
|
+
if (!headerValue.trim()) return [];
|
|
85469
|
+
return headerValue.split(",").map((part) => {
|
|
85470
|
+
const angle = part.match(/<([^>]+)>/);
|
|
85471
|
+
return (angle ? angle[1] : part).trim();
|
|
85472
|
+
}).filter((addr) => addr.includes("@"));
|
|
85473
|
+
}
|
|
85474
|
+
function parseOriginalHeaders(raw) {
|
|
85475
|
+
const headerBlock = raw.split(/\r?\n\r?\n/)[0] ?? "";
|
|
85476
|
+
const lines = [];
|
|
85477
|
+
for (const line of headerBlock.split(/\r?\n/)) {
|
|
85478
|
+
if (/^[ \t]/.test(line) && lines.length > 0) {
|
|
85479
|
+
lines[lines.length - 1] += " " + line.trim();
|
|
85480
|
+
} else {
|
|
85481
|
+
lines.push(line);
|
|
85482
|
+
}
|
|
85483
|
+
}
|
|
85484
|
+
const get = (name) => {
|
|
85485
|
+
const prefix = `${name.toLowerCase()}:`;
|
|
85486
|
+
const found = lines.find((l) => l.toLowerCase().startsWith(prefix));
|
|
85487
|
+
return found ? found.slice(found.indexOf(":") + 1).trim() : void 0;
|
|
85488
|
+
};
|
|
85489
|
+
const rawMessageId = get("Message-ID");
|
|
85490
|
+
const messageId = rawMessageId?.match(/<[^>]+>/)?.[0] ?? rawMessageId ?? void 0;
|
|
85491
|
+
const references = `${get("References") ?? ""} ${get("In-Reply-To") ?? ""}`.match(/<[^>]+>/g) ?? [];
|
|
85492
|
+
return {
|
|
85493
|
+
messageId,
|
|
85494
|
+
references,
|
|
85495
|
+
from: extractAddresses(get("From") ?? ""),
|
|
85496
|
+
replyTo: extractAddresses(get("Reply-To") ?? ""),
|
|
85497
|
+
to: extractAddresses(get("To") ?? ""),
|
|
85498
|
+
cc: extractAddresses(get("Cc") ?? ""),
|
|
85499
|
+
subject: get("Subject") ?? "",
|
|
85500
|
+
date: get("Date")
|
|
85501
|
+
};
|
|
85502
|
+
}
|
|
85503
|
+
function dedupe(addrs) {
|
|
85504
|
+
const seen = /* @__PURE__ */ new Set();
|
|
85505
|
+
const out = [];
|
|
85506
|
+
for (const a of addrs) {
|
|
85507
|
+
const k = a.toLowerCase();
|
|
85508
|
+
if (!seen.has(k)) {
|
|
85509
|
+
seen.add(k);
|
|
85510
|
+
out.push(a);
|
|
85511
|
+
}
|
|
85512
|
+
}
|
|
85513
|
+
return out;
|
|
85514
|
+
}
|
|
85515
|
+
function withSubjectPrefix(subject, prefix) {
|
|
85516
|
+
const s = subject.trim();
|
|
85517
|
+
const present = prefix === "Re:" ? /^re:/i : /^(fwd?|fw):/i;
|
|
85518
|
+
return present.test(s) ? s : `${prefix} ${s}`;
|
|
85519
|
+
}
|
|
85520
|
+
function quoteBody(plainText) {
|
|
85521
|
+
return plainText.replace(/\s+$/, "").split(/\r?\n/).map((l) => l ? `> ${l}` : ">").join("\n");
|
|
85522
|
+
}
|
|
85523
|
+
function buildReplyOptions(args) {
|
|
85524
|
+
const { original, originalPlainText, body, replyAll, self, from } = args;
|
|
85525
|
+
const selfSet = new Set(self.filter(Boolean).map((s) => s.toLowerCase()));
|
|
85526
|
+
const to = dedupe((original.replyTo.length ? original.replyTo : original.from).filter(Boolean));
|
|
85527
|
+
const toSet = new Set(to.map((t) => t.toLowerCase()));
|
|
85528
|
+
let cc;
|
|
85529
|
+
if (replyAll) {
|
|
85530
|
+
const extra = dedupe(
|
|
85531
|
+
[...original.to, ...original.cc].filter(
|
|
85532
|
+
(a) => !selfSet.has(a.toLowerCase()) && !toSet.has(a.toLowerCase())
|
|
85533
|
+
)
|
|
85534
|
+
);
|
|
85535
|
+
cc = extra.length ? extra : void 0;
|
|
85536
|
+
}
|
|
85537
|
+
const attribution = buildAttribution(original);
|
|
85538
|
+
const quoted = originalPlainText.trim() ? `
|
|
85539
|
+
|
|
85540
|
+
${attribution}${quoteBody(originalPlainText)}` : "";
|
|
85541
|
+
const references = dedupe(
|
|
85542
|
+
original.messageId ? [...original.references, original.messageId] : original.references
|
|
85543
|
+
);
|
|
85544
|
+
return {
|
|
85545
|
+
to,
|
|
85546
|
+
cc,
|
|
85547
|
+
subject: withSubjectPrefix(original.subject, "Re:"),
|
|
85548
|
+
body: `${body}${quoted}`,
|
|
85549
|
+
inReplyTo: original.messageId,
|
|
85550
|
+
references: references.length ? references : void 0,
|
|
85551
|
+
from
|
|
85552
|
+
};
|
|
85553
|
+
}
|
|
85554
|
+
function buildAttribution(original) {
|
|
85555
|
+
const who = original.from[0] ?? original.replyTo[0] ?? "the sender";
|
|
85556
|
+
return original.date ? `On ${original.date}, ${who} wrote:
|
|
85557
|
+
` : `${who} wrote:
|
|
85558
|
+
`;
|
|
85559
|
+
}
|
|
85560
|
+
function buildForwardOptions(args) {
|
|
85561
|
+
const { original, originalPlainText, to, body, from } = args;
|
|
85562
|
+
const headerBlock = [
|
|
85563
|
+
"---------- Forwarded message ----------",
|
|
85564
|
+
original.from.length ? `From: ${original.from.join(", ")}` : "",
|
|
85565
|
+
original.date ? `Date: ${original.date}` : "",
|
|
85566
|
+
`Subject: ${original.subject}`,
|
|
85567
|
+
original.to.length ? `To: ${original.to.join(", ")}` : "",
|
|
85568
|
+
original.cc.length ? `Cc: ${original.cc.join(", ")}` : ""
|
|
85569
|
+
].filter(Boolean).join("\n");
|
|
85570
|
+
const prefix = body?.trim() ? `${body}
|
|
85571
|
+
|
|
85572
|
+
` : "";
|
|
85573
|
+
return {
|
|
85574
|
+
to: dedupe(to),
|
|
85575
|
+
subject: withSubjectPrefix(original.subject, "Fwd:"),
|
|
85576
|
+
body: `${prefix}${headerBlock}
|
|
85577
|
+
|
|
85578
|
+
${originalPlainText}`,
|
|
85579
|
+
from
|
|
85580
|
+
};
|
|
85581
|
+
}
|
|
85582
|
+
|
|
85555
85583
|
// src/tools/respond.ts
|
|
85556
85584
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
85557
85585
|
|
|
@@ -85644,6 +85672,117 @@ function withErrorHandling(handler, errorPrefix) {
|
|
|
85644
85672
|
};
|
|
85645
85673
|
}
|
|
85646
85674
|
|
|
85675
|
+
// src/tools/compose.ts
|
|
85676
|
+
async function readOriginal(deps, id, cfg) {
|
|
85677
|
+
if (decodeImapId(id)) {
|
|
85678
|
+
const source = await deps.imapSource(id);
|
|
85679
|
+
const identities = [cfg.user, cfg.from, ...cfg.allowedFrom ?? []].map(
|
|
85680
|
+
(s) => s.trim().toLowerCase()
|
|
85681
|
+
);
|
|
85682
|
+
if (!identities.includes(source.accountUser.trim().toLowerCase())) {
|
|
85683
|
+
throw new Error(
|
|
85684
|
+
"The source IMAP account does not match the configured SMTP identity. Configure SMTP for that account or explicitly select transport=applescript."
|
|
85685
|
+
);
|
|
85686
|
+
}
|
|
85687
|
+
const original = parseOriginalHeaders(source.raw);
|
|
85688
|
+
if (source.subject !== void 0) original.subject = source.subject;
|
|
85689
|
+
return { original, plainText: extractTextBody(source.raw) };
|
|
85690
|
+
}
|
|
85691
|
+
const raw = deps.mail.getRawSource(id);
|
|
85692
|
+
if (!raw)
|
|
85693
|
+
throw new Error(
|
|
85694
|
+
"Cannot read the original message source. Re-list the intended mailbox and retry with its message id."
|
|
85695
|
+
);
|
|
85696
|
+
const content = deps.mail.getMessageContent(id);
|
|
85697
|
+
return { original: parseOriginalHeaders(raw), plainText: content?.plainText ?? null };
|
|
85698
|
+
}
|
|
85699
|
+
async function runCompose(deps, args) {
|
|
85700
|
+
const { id, send, transport: transport2 } = args;
|
|
85701
|
+
const verb = args.kind === "reply" ? "reply to" : "forward";
|
|
85702
|
+
if (!send && transport2 === "smtp") {
|
|
85703
|
+
return errorResponse(
|
|
85704
|
+
"SMTP cannot save a Mail.app draft. Omit transport or use transport=applescript with send=false."
|
|
85705
|
+
);
|
|
85706
|
+
}
|
|
85707
|
+
const smtp = send && transport2 !== "applescript" && (transport2 === "smtp" || deps.smtpConfigured());
|
|
85708
|
+
if (smtp) {
|
|
85709
|
+
try {
|
|
85710
|
+
const cfg = deps.smtpConfig();
|
|
85711
|
+
const { original, plainText } = await readOriginal(deps, id, cfg);
|
|
85712
|
+
if (args.kind === "forward" && plainText === null)
|
|
85713
|
+
throw new Error(
|
|
85714
|
+
"The original message has no readable plain-text body. SMTP forwarding would omit its content; explicitly select transport=applescript to forward it with Mail.app."
|
|
85715
|
+
);
|
|
85716
|
+
if (args.kind === "reply") {
|
|
85717
|
+
if (!original.messageId)
|
|
85718
|
+
throw new Error(
|
|
85719
|
+
"The original message has no Message-ID; a threaded SMTP reply cannot be constructed."
|
|
85720
|
+
);
|
|
85721
|
+
if (!original.replyTo.length && !original.from.length)
|
|
85722
|
+
throw new Error("The original message has no reply address.");
|
|
85723
|
+
}
|
|
85724
|
+
const opts = args.kind === "reply" ? buildReplyOptions({
|
|
85725
|
+
original,
|
|
85726
|
+
originalPlainText: plainText ?? "",
|
|
85727
|
+
body: args.body,
|
|
85728
|
+
replyAll: args.replyAll,
|
|
85729
|
+
self: [cfg.from, cfg.user, ...cfg.allowedFrom ?? []],
|
|
85730
|
+
from: cfg.from
|
|
85731
|
+
}) : buildForwardOptions({
|
|
85732
|
+
original,
|
|
85733
|
+
originalPlainText: plainText ?? "",
|
|
85734
|
+
to: args.to,
|
|
85735
|
+
body: args.body,
|
|
85736
|
+
from: cfg.from
|
|
85737
|
+
});
|
|
85738
|
+
const result = await deps.smtpSend(opts, cfg);
|
|
85739
|
+
if (!result.success)
|
|
85740
|
+
return errorResponse(
|
|
85741
|
+
`Failed to ${verb} message "${id}" via SMTP: ${result.error ?? "unknown SMTP error"}`
|
|
85742
|
+
);
|
|
85743
|
+
return successResponse(
|
|
85744
|
+
args.kind === "reply" ? "Reply sent via SMTP" : `Message forwarded via SMTP to ${args.to.join(", ")}`,
|
|
85745
|
+
{
|
|
85746
|
+
ok: true,
|
|
85747
|
+
sent: true,
|
|
85748
|
+
id,
|
|
85749
|
+
transport: "smtp",
|
|
85750
|
+
messageId: result.messageId,
|
|
85751
|
+
...args.kind === "forward" ? { recipients: args.to } : {}
|
|
85752
|
+
}
|
|
85753
|
+
);
|
|
85754
|
+
} catch (error2) {
|
|
85755
|
+
return errorResponse(
|
|
85756
|
+
`Failed to ${verb} message "${id}" via SMTP: ${error2 instanceof Error ? error2.message : String(error2)} No AppleScript fallback was attempted.`
|
|
85757
|
+
);
|
|
85758
|
+
}
|
|
85759
|
+
}
|
|
85760
|
+
const resolved = await deps.numericId(id);
|
|
85761
|
+
if (!resolved.numericId)
|
|
85762
|
+
return errorResponse(
|
|
85763
|
+
`Failed to ${verb} message "${id}": ${resolved.error ?? "message not found"}`
|
|
85764
|
+
);
|
|
85765
|
+
const outcome = args.kind === "reply" ? deps.mail.replyToMessage(resolved.numericId, args.body, args.replyAll, send) : deps.mail.forwardMessage(resolved.numericId, args.to, args.body, send);
|
|
85766
|
+
if (!outcome.success)
|
|
85767
|
+
return errorResponse(
|
|
85768
|
+
`Failed to ${verb} message "${id}": ${outcome.error ?? "Mail.app compose failed"}`
|
|
85769
|
+
);
|
|
85770
|
+
const text = args.kind === "reply" ? send ? "Reply sent via AppleScript" : "Reply saved as draft" : send ? `Message forwarded to ${args.to.join(", ")}` : "Forward saved as draft";
|
|
85771
|
+
return successResponse(text, {
|
|
85772
|
+
ok: true,
|
|
85773
|
+
sent: send,
|
|
85774
|
+
id,
|
|
85775
|
+
transport: "applescript",
|
|
85776
|
+
...args.kind === "forward" ? { recipients: args.to } : {}
|
|
85777
|
+
});
|
|
85778
|
+
}
|
|
85779
|
+
function runReply(deps, args) {
|
|
85780
|
+
return runCompose(deps, { ...args, kind: "reply" });
|
|
85781
|
+
}
|
|
85782
|
+
function runForward(deps, args) {
|
|
85783
|
+
return runCompose(deps, { ...args, kind: "forward" });
|
|
85784
|
+
}
|
|
85785
|
+
|
|
85647
85786
|
// src/tools/mailboxListing.ts
|
|
85648
85787
|
function unlistableStoreError(account, error2, knownAccounts) {
|
|
85649
85788
|
const scope = account ? ` for "${account}"` : "";
|
|
@@ -87115,88 +87254,37 @@ async function toNumericMailId(id) {
|
|
|
87115
87254
|
}
|
|
87116
87255
|
return { numericId };
|
|
87117
87256
|
}
|
|
87118
|
-
|
|
87119
|
-
|
|
87120
|
-
|
|
87121
|
-
|
|
87122
|
-
|
|
87123
|
-
|
|
87124
|
-
|
|
87125
|
-
|
|
87126
|
-
|
|
87127
|
-
|
|
87128
|
-
|
|
87129
|
-
original,
|
|
87130
|
-
originalPlainText: content?.plainText ?? "",
|
|
87131
|
-
body,
|
|
87132
|
-
replyAll,
|
|
87133
|
-
self: [cfg.from, cfg.user],
|
|
87134
|
-
from: cfg.from
|
|
87135
|
-
});
|
|
87136
|
-
const result = await sendViaSmtp(opts, cfg);
|
|
87137
|
-
if (result.success) return { sent: true };
|
|
87138
|
-
return { sent: false, fallback: false, error: result.error ?? "unknown SMTP error" };
|
|
87139
|
-
}
|
|
87140
|
-
async function sendForwardViaSmtp(id, to, body) {
|
|
87141
|
-
const cfg = resolveSmtpOrFallback();
|
|
87142
|
-
if (!cfg) return { sent: false, fallback: true };
|
|
87143
|
-
const raw = mailManager.getRawSource(id);
|
|
87144
|
-
if (!raw) return { sent: false, fallback: true };
|
|
87145
|
-
const original = parseOriginalHeaders(raw);
|
|
87146
|
-
const content = mailManager.getMessageContent(id);
|
|
87147
|
-
const opts = buildForwardOptions({
|
|
87148
|
-
original,
|
|
87149
|
-
originalPlainText: content?.plainText ?? "",
|
|
87150
|
-
to,
|
|
87151
|
-
body,
|
|
87152
|
-
from: cfg.from
|
|
87153
|
-
});
|
|
87154
|
-
const result = await sendViaSmtp(opts, cfg);
|
|
87155
|
-
if (result.success) return { sent: true };
|
|
87156
|
-
return { sent: false, fallback: false, error: result.error ?? "unknown SMTP error" };
|
|
87157
|
-
}
|
|
87257
|
+
var composeDeps = {
|
|
87258
|
+
mail: mailManager,
|
|
87259
|
+
imapSource: imapGetMessageSource,
|
|
87260
|
+
numericId: toNumericMailId,
|
|
87261
|
+
smtpConfigured: isSmtpConfigured,
|
|
87262
|
+
smtpConfig: resolveSmtpConfig,
|
|
87263
|
+
smtpSend: sendViaSmtp
|
|
87264
|
+
};
|
|
87265
|
+
var COMPOSE_TRANSPORT_SCHEMA = external_exports.enum(["applescript", "smtp"]).optional().describe(
|
|
87266
|
+
"SMTP sends clean MIME and preserves reply threading; requires SMTP configuration. AppleScript uses Mail.app and may quote-wrap the new body on macOS 15+. Omit to prefer configured SMTP when sending; drafts use AppleScript. A selected SMTP transport never silently falls back. SMTP with send=false is rejected."
|
|
87267
|
+
);
|
|
87158
87268
|
registerTool(
|
|
87159
87269
|
"reply-to-message",
|
|
87160
87270
|
{
|
|
87161
87271
|
description: "Use when: replying to an existing message by id, preserving its threading headers. Set replyAll for all recipients; set send=false to save as a draft instead of sending.\nReturns: a confirmation that the reply was sent or saved as a draft.\nDo not use when: composing a brand-new message (use send-email / create-draft) or forwarding to new recipients (use forward-message).\nSafety: with the default send=true this SENDS real email immediately and cannot be unsent \u2014 require explicit user confirmation of the recipients and body, or pass send=false to let the user review.",
|
|
87162
87272
|
inputSchema: {
|
|
87163
87273
|
id: MESSAGE_ID_SCHEMA,
|
|
87274
|
+
transport: COMPOSE_TRANSPORT_SCHEMA,
|
|
87164
87275
|
body: external_exports.string().min(1, "Reply body is required"),
|
|
87165
87276
|
replyAll: external_exports.boolean().optional().default(false).describe("Reply to all recipients"),
|
|
87166
87277
|
send: external_exports.boolean().optional().default(true).describe("Send immediately (false = save as draft)")
|
|
87167
87278
|
},
|
|
87168
87279
|
outputSchema: {
|
|
87280
|
+
transport: external_exports.enum(["smtp", "applescript"]).optional(),
|
|
87281
|
+
messageId: external_exports.string().optional(),
|
|
87169
87282
|
ok: external_exports.boolean().optional(),
|
|
87170
87283
|
sent: external_exports.boolean().optional(),
|
|
87171
87284
|
id: external_exports.string().optional()
|
|
87172
87285
|
}
|
|
87173
87286
|
},
|
|
87174
|
-
withErrorHandling(
|
|
87175
|
-
if (send && isSmtpConfigured()) {
|
|
87176
|
-
const outcome2 = await sendReplyViaSmtp(id, body, replyAll);
|
|
87177
|
-
if (outcome2.sent) {
|
|
87178
|
-
return successResponse("Reply sent", { ok: true, sent: true, id });
|
|
87179
|
-
}
|
|
87180
|
-
if (!outcome2.fallback) {
|
|
87181
|
-
return errorResponse(`Failed to reply to message "${id}" via SMTP: ${outcome2.error}`);
|
|
87182
|
-
}
|
|
87183
|
-
}
|
|
87184
|
-
const resolvedReply = await toNumericMailId(id);
|
|
87185
|
-
if (!resolvedReply.numericId) {
|
|
87186
|
-
return errorResponse(`Failed to reply to message "${id}": ${resolvedReply.error}`);
|
|
87187
|
-
}
|
|
87188
|
-
const outcome = mailManager.replyToMessage(resolvedReply.numericId, body, replyAll, send);
|
|
87189
|
-
if (!outcome.success) {
|
|
87190
|
-
return errorResponse(
|
|
87191
|
-
outcome.error ? `Failed to reply to message "${id}": ${outcome.error}` : `Failed to reply to message "${id}"`
|
|
87192
|
-
);
|
|
87193
|
-
}
|
|
87194
|
-
return successResponse(send ? "Reply sent" : "Reply saved as draft", {
|
|
87195
|
-
ok: true,
|
|
87196
|
-
sent: send,
|
|
87197
|
-
id
|
|
87198
|
-
});
|
|
87199
|
-
}, "Error replying to message")
|
|
87287
|
+
withErrorHandling((args) => runReply(composeDeps, args), "Error replying to message")
|
|
87200
87288
|
);
|
|
87201
87289
|
registerTool(
|
|
87202
87290
|
"forward-message",
|
|
@@ -87204,47 +87292,21 @@ registerTool(
|
|
|
87204
87292
|
description: "Use when: forwarding an existing message (by id) to new recipients (to is an array), with an optional body to prepend. Set send=false to save as a draft.\nReturns: a confirmation that the message was forwarded or saved as a draft.\nDo not use when: replying to the sender/recipients (use reply-to-message) or composing a new message (use send-email / create-draft).\nSafety: with the default send=true this SENDS real email immediately and cannot be unsent \u2014 require explicit user confirmation of the recipients and any prepended body, or pass send=false to let the user review.",
|
|
87205
87293
|
inputSchema: {
|
|
87206
87294
|
id: MESSAGE_ID_SCHEMA,
|
|
87295
|
+
transport: COMPOSE_TRANSPORT_SCHEMA,
|
|
87207
87296
|
to: external_exports.array(external_exports.string()).min(1, "At least one recipient is required"),
|
|
87208
87297
|
body: external_exports.string().optional().describe("Optional message to prepend"),
|
|
87209
87298
|
send: external_exports.boolean().optional().default(true).describe("Send immediately (false = save as draft)")
|
|
87210
87299
|
},
|
|
87211
87300
|
outputSchema: {
|
|
87301
|
+
transport: external_exports.enum(["smtp", "applescript"]).optional(),
|
|
87302
|
+
messageId: external_exports.string().optional(),
|
|
87212
87303
|
ok: external_exports.boolean().optional(),
|
|
87213
87304
|
sent: external_exports.boolean().optional(),
|
|
87214
87305
|
recipients: external_exports.array(external_exports.string()).optional(),
|
|
87215
87306
|
id: external_exports.string().optional()
|
|
87216
87307
|
}
|
|
87217
87308
|
},
|
|
87218
|
-
withErrorHandling(
|
|
87219
|
-
if (send && isSmtpConfigured()) {
|
|
87220
|
-
const outcome2 = await sendForwardViaSmtp(id, to, body);
|
|
87221
|
-
if (outcome2.sent) {
|
|
87222
|
-
return successResponse(`Message forwarded to ${to.join(", ")}`, {
|
|
87223
|
-
ok: true,
|
|
87224
|
-
sent: true,
|
|
87225
|
-
recipients: to,
|
|
87226
|
-
id
|
|
87227
|
-
});
|
|
87228
|
-
}
|
|
87229
|
-
if (!outcome2.fallback) {
|
|
87230
|
-
return errorResponse(`Failed to forward message "${id}" via SMTP: ${outcome2.error}`);
|
|
87231
|
-
}
|
|
87232
|
-
}
|
|
87233
|
-
const resolvedFwd = await toNumericMailId(id);
|
|
87234
|
-
if (!resolvedFwd.numericId) {
|
|
87235
|
-
return errorResponse(`Failed to forward message "${id}": ${resolvedFwd.error}`);
|
|
87236
|
-
}
|
|
87237
|
-
const outcome = mailManager.forwardMessage(resolvedFwd.numericId, to, body, send);
|
|
87238
|
-
if (!outcome.success) {
|
|
87239
|
-
return errorResponse(
|
|
87240
|
-
outcome.error ? `Failed to forward message "${id}": ${outcome.error}` : `Failed to forward message "${id}"`
|
|
87241
|
-
);
|
|
87242
|
-
}
|
|
87243
|
-
return successResponse(
|
|
87244
|
-
send ? `Message forwarded to ${to.join(", ")}` : "Forward saved as draft",
|
|
87245
|
-
{ ok: true, sent: send, recipients: to, id }
|
|
87246
|
-
);
|
|
87247
|
-
}, "Error forwarding message")
|
|
87309
|
+
withErrorHandling((args) => runForward(composeDeps, args), "Error forwarding message")
|
|
87248
87310
|
);
|
|
87249
87311
|
registerTool(
|
|
87250
87312
|
"mark-as-read",
|
package/package.json
CHANGED