apple-mail-mcp 2.19.22 → 2.20.1
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 +109 -34
- package/build/cli.js +179 -27
- package/build/index.js +430 -85
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -7256,7 +7256,7 @@ function isInlineDisposition(headers) {
|
|
|
7256
7256
|
function extractSize(headers) {
|
|
7257
7257
|
const dispHeader = getHeader(headers, "Content-Disposition");
|
|
7258
7258
|
if (dispHeader) {
|
|
7259
|
-
const sizeMatch = dispHeader.match(/size
|
|
7259
|
+
const sizeMatch = dispHeader.match(/(?:^|;)\s*size\s*=\s*"?(\d+)/i);
|
|
7260
7260
|
if (sizeMatch) return parseInt(sizeMatch[1], 10);
|
|
7261
7261
|
}
|
|
7262
7262
|
return 0;
|
|
@@ -7285,8 +7285,8 @@ function decodePartText(bytes, charset) {
|
|
|
7285
7285
|
return new TextDecoder("windows-1252").decode(bytes);
|
|
7286
7286
|
}
|
|
7287
7287
|
}
|
|
7288
|
-
function
|
|
7289
|
-
const cleaned = base64Body.replace(/[
|
|
7288
|
+
function base64DecodedSize(base64Body) {
|
|
7289
|
+
const cleaned = base64Body.replace(/[^A-Za-z0-9+/]/g, "");
|
|
7290
7290
|
return Math.floor(cleaned.length * 3 / 4);
|
|
7291
7291
|
}
|
|
7292
7292
|
function splitMimeParts(source, boundary) {
|
|
@@ -7349,7 +7349,8 @@ function decodeQuotedPrintable(body) {
|
|
|
7349
7349
|
}
|
|
7350
7350
|
function estimateSize(body, encoding) {
|
|
7351
7351
|
const enc = (encoding || "").toLowerCase().trim();
|
|
7352
|
-
if (enc === "base64") return
|
|
7352
|
+
if (enc === "base64") return base64DecodedSize(body);
|
|
7353
|
+
if (enc === "quoted-printable") return decodeQuotedPrintable(body).length;
|
|
7353
7354
|
return body.length;
|
|
7354
7355
|
}
|
|
7355
7356
|
function parseMimeAttachments(source) {
|
|
@@ -65182,11 +65183,15 @@ __export(imapClient_exports, {
|
|
|
65182
65183
|
MAX_UNSCOPED_SEARCH_DEPTH: () => MAX_UNSCOPED_SEARCH_DEPTH,
|
|
65183
65184
|
__resetPool: () => __resetPool,
|
|
65184
65185
|
__setPoolConnect: () => __setPoolConnect,
|
|
65186
|
+
base64DecodedLength: () => base64DecodedLength,
|
|
65187
|
+
base64DecodedSizeFromTail: () => base64DecodedSizeFromTail,
|
|
65185
65188
|
bodyStructureHasAttachments: () => bodyStructureHasAttachments,
|
|
65186
65189
|
buildImapConnectionOptions: () => buildImapConnectionOptions,
|
|
65187
65190
|
decodeImapId: () => decodeImapId,
|
|
65191
|
+
decodedSizeWithoutBody: () => decodedSizeWithoutBody,
|
|
65188
65192
|
dropAllPools: () => dropAllPools,
|
|
65189
65193
|
encodeImapId: () => encodeImapId,
|
|
65194
|
+
imapAppendDraft: () => imapAppendDraft,
|
|
65190
65195
|
imapAppendSentCopy: () => imapAppendSentCopy,
|
|
65191
65196
|
imapBatchDelete: () => imapBatchDelete,
|
|
65192
65197
|
imapBatchFlag: () => imapBatchFlag,
|
|
@@ -65224,6 +65229,8 @@ __export(imapClient_exports, {
|
|
|
65224
65229
|
matchMailbox: () => matchMailbox,
|
|
65225
65230
|
normalizeMessageId: () => normalizeMessageId,
|
|
65226
65231
|
omittedNote: () => omittedNote,
|
|
65232
|
+
quotedPrintableDecodedLength: () => quotedPrintableDecodedLength,
|
|
65233
|
+
resolveDraftImapAccount: () => resolveDraftImapAccount,
|
|
65227
65234
|
resolveImapConfig: () => resolveImapConfig,
|
|
65228
65235
|
resolveImapConfigs: () => resolveImapConfigs,
|
|
65229
65236
|
resolveMailboxPath: () => resolveMailboxPath,
|
|
@@ -66281,6 +66288,40 @@ async function imapAppendSentCopy(smtpUser, raw, deps = {}) {
|
|
|
66281
66288
|
return { attempted: true, success: false, error: errText(e) };
|
|
66282
66289
|
}
|
|
66283
66290
|
}
|
|
66291
|
+
function resolveDraftImapAccount(account, smtpUser, env = process.env) {
|
|
66292
|
+
const specs = listImapAccountSpecs(env);
|
|
66293
|
+
const why = "Mail.app's AppleScript cannot set In-Reply-To/References on a draft, so a threaded draft is filed over IMAP into the account's Drafts mailbox";
|
|
66294
|
+
if (specs.length === 0) {
|
|
66295
|
+
throw new Error(
|
|
66296
|
+
`${why}, and IMAP is not configured. Configure IMAP, omit inReplyTo/references, or use reply-to-message with send=false. ${SETUP_HINT}`
|
|
66297
|
+
);
|
|
66298
|
+
}
|
|
66299
|
+
let spec;
|
|
66300
|
+
if (account) {
|
|
66301
|
+
spec = specs.find((s) => specMatchesSelector(s, account));
|
|
66302
|
+
if (!spec) {
|
|
66303
|
+
throw new Error(
|
|
66304
|
+
`${why}, and "${account}" is not a configured IMAP account. Configured: ${specs.map((s) => s.accountLabel).join(", ")}.`
|
|
66305
|
+
);
|
|
66306
|
+
}
|
|
66307
|
+
} else {
|
|
66308
|
+
spec = (smtpUser ? specs.find((s) => specMatchesSelector(s, smtpUser)) : void 0) ?? (specs.length === 1 ? specs[0] : void 0);
|
|
66309
|
+
if (!spec) {
|
|
66310
|
+
throw new Error(
|
|
66311
|
+
`${why}; several IMAP accounts are configured, so pass \`account\` to choose one: ${specs.map((s) => s.accountLabel).join(", ")}.`
|
|
66312
|
+
);
|
|
66313
|
+
}
|
|
66314
|
+
}
|
|
66315
|
+
return { label: spec.accountLabel, user: spec.user };
|
|
66316
|
+
}
|
|
66317
|
+
async function imapAppendDraft(account, raw, deps = {}) {
|
|
66318
|
+
return withClient({ ...deps, account: deps.account ?? account }, async (client, cfg) => {
|
|
66319
|
+
const path3 = await resolveMailboxPath(client, "drafts", "list");
|
|
66320
|
+
const res = await client.append(path3, raw, ["\\Draft", "\\Seen"]);
|
|
66321
|
+
if (!res) throw new Error(`server rejected the APPEND to "${path3}" (IMAP NO/BAD)`);
|
|
66322
|
+
return { mailbox: path3, account: cfg.accountLabel };
|
|
66323
|
+
});
|
|
66324
|
+
}
|
|
66284
66325
|
async function withMailbox(path3, deps, fn) {
|
|
66285
66326
|
return withClient(deps, async (client) => {
|
|
66286
66327
|
const lock = await client.getMailboxLock(path3);
|
|
@@ -66649,6 +66690,56 @@ async function imapDeleteMessageById(id, deps = {}) {
|
|
|
66649
66690
|
}
|
|
66650
66691
|
});
|
|
66651
66692
|
}
|
|
66693
|
+
function dispositionSize(params) {
|
|
66694
|
+
const raw = params?.size?.trim();
|
|
66695
|
+
return raw && /^\d+$/.test(raw) ? Number(raw) : void 0;
|
|
66696
|
+
}
|
|
66697
|
+
function base64DecodedLength(body) {
|
|
66698
|
+
const s = typeof body === "string" ? body : body.toString("latin1");
|
|
66699
|
+
const n = s.replace(/[^A-Za-z0-9+/]/g, "").length;
|
|
66700
|
+
return Math.floor(n * 3 / 4);
|
|
66701
|
+
}
|
|
66702
|
+
function base64DecodedSizeFromTail(total, tail) {
|
|
66703
|
+
if (tail.length >= total) return base64DecodedLength(tail);
|
|
66704
|
+
const s = tail.toString("latin1");
|
|
66705
|
+
const term = s.includes("\r\n") ? "\r\n" : "\n";
|
|
66706
|
+
const segs = s.split(term);
|
|
66707
|
+
if (segs.length < 3) return void 0;
|
|
66708
|
+
let trailing = 0;
|
|
66709
|
+
while (segs.length > 1 && segs[segs.length - 1] === "") {
|
|
66710
|
+
segs.pop();
|
|
66711
|
+
trailing++;
|
|
66712
|
+
}
|
|
66713
|
+
const last = segs[segs.length - 1];
|
|
66714
|
+
const full = segs.slice(1, -1);
|
|
66715
|
+
if (full.length === 0 || !last) return void 0;
|
|
66716
|
+
const L = full[0].length;
|
|
66717
|
+
const alphabet = /^[A-Za-z0-9+/]+$/;
|
|
66718
|
+
if (L === 0 || L % 4 !== 0 || !full.every((l) => l.length === L && alphabet.test(l))) {
|
|
66719
|
+
return void 0;
|
|
66720
|
+
}
|
|
66721
|
+
const m = /^([A-Za-z0-9+/]*)(=*)$/.exec(last);
|
|
66722
|
+
if (!m || m[2].length > 2 || last.length > L) return void 0;
|
|
66723
|
+
const before = total - trailing * term.length - last.length;
|
|
66724
|
+
if (before < 0 || before % (L + term.length) !== 0) return void 0;
|
|
66725
|
+
const chars = before / (L + term.length) * L + last.length;
|
|
66726
|
+
if (chars % 4 !== 0) return void 0;
|
|
66727
|
+
return chars / 4 * 3 - m[2].length;
|
|
66728
|
+
}
|
|
66729
|
+
function quotedPrintableDecodedLength(body) {
|
|
66730
|
+
const s = (typeof body === "string" ? body : body.toString("latin1")).replace(/=\r?\n/g, "");
|
|
66731
|
+
const escapes = s.match(/=[0-9A-Fa-f]{2}/g)?.length ?? 0;
|
|
66732
|
+
return s.length - escapes * 2;
|
|
66733
|
+
}
|
|
66734
|
+
function decodedSizeWithoutBody(a) {
|
|
66735
|
+
if (a.declaredSize !== void 0) return { size: a.declaredSize, exact: true };
|
|
66736
|
+
if (a.encoding === "base64") {
|
|
66737
|
+
const chars = a.encodedSize - 2 * Math.floor(a.encodedSize / 78);
|
|
66738
|
+
return { size: Math.floor(chars * 3 / 4), exact: false };
|
|
66739
|
+
}
|
|
66740
|
+
if (a.encoding === "quoted-printable") return { size: a.encodedSize, exact: false };
|
|
66741
|
+
return { size: a.encodedSize, exact: true };
|
|
66742
|
+
}
|
|
66652
66743
|
function collectAttachments(node, out = []) {
|
|
66653
66744
|
if (!node) return out;
|
|
66654
66745
|
const filename = node.dispositionParameters?.filename || node.parameters?.name;
|
|
@@ -66660,7 +66751,9 @@ function collectAttachments(node, out = []) {
|
|
|
66660
66751
|
part: node.part,
|
|
66661
66752
|
filename: filename || `part-${node.part}`,
|
|
66662
66753
|
mimeType: node.type || "application/octet-stream",
|
|
66663
|
-
|
|
66754
|
+
encodedSize: node.size ?? 0,
|
|
66755
|
+
encoding: (node.encoding || "").toLowerCase().trim(),
|
|
66756
|
+
declaredSize: dispositionSize(node.dispositionParameters)
|
|
66664
66757
|
});
|
|
66665
66758
|
}
|
|
66666
66759
|
for (const child of node.childNodes ?? []) collectAttachments(child, out);
|
|
@@ -66681,6 +66774,52 @@ async function streamToBuffer(content, maxBytes) {
|
|
|
66681
66774
|
}
|
|
66682
66775
|
return Buffer.concat(chunks);
|
|
66683
66776
|
}
|
|
66777
|
+
async function resolveDecodedSizes(client, uid, atts) {
|
|
66778
|
+
const out = atts.map(decodedSizeWithoutBody);
|
|
66779
|
+
const probes = [];
|
|
66780
|
+
atts.forEach((a, i) => {
|
|
66781
|
+
if (a.declaredSize !== void 0 || a.encodedSize <= 0) return;
|
|
66782
|
+
if (a.encoding === "base64") {
|
|
66783
|
+
probes.push({ i, start: Math.max(0, a.encodedSize - BASE64_TAIL_WINDOW), kind: "b64" });
|
|
66784
|
+
} else if (a.encoding === "quoted-printable" && a.encodedSize <= QP_EXACT_MAX_BYTES) {
|
|
66785
|
+
probes.push({ i, start: 0, kind: "qp" });
|
|
66786
|
+
}
|
|
66787
|
+
});
|
|
66788
|
+
if (probes.length === 0) return out;
|
|
66789
|
+
let parts;
|
|
66790
|
+
try {
|
|
66791
|
+
const msg = await client.fetchOne(
|
|
66792
|
+
String(uid),
|
|
66793
|
+
{
|
|
66794
|
+
uid: true,
|
|
66795
|
+
bodyParts: probes.map((p) => ({
|
|
66796
|
+
key: atts[p.i].part,
|
|
66797
|
+
start: p.start,
|
|
66798
|
+
maxLength: atts[p.i].encodedSize - p.start
|
|
66799
|
+
}))
|
|
66800
|
+
},
|
|
66801
|
+
{ uid: true }
|
|
66802
|
+
);
|
|
66803
|
+
parts = msg ? msg.bodyParts : void 0;
|
|
66804
|
+
} catch {
|
|
66805
|
+
return out;
|
|
66806
|
+
}
|
|
66807
|
+
for (const p of probes) {
|
|
66808
|
+
const a = atts[p.i];
|
|
66809
|
+
const buf = parts?.get(a.part.toLowerCase());
|
|
66810
|
+
if (!buf) continue;
|
|
66811
|
+
let exact;
|
|
66812
|
+
if (p.kind === "qp") {
|
|
66813
|
+
exact = quotedPrintableDecodedLength(buf);
|
|
66814
|
+
} else {
|
|
66815
|
+
const requested = a.encodedSize - p.start;
|
|
66816
|
+
const total = buf.length < requested ? p.start + buf.length : a.encodedSize;
|
|
66817
|
+
exact = base64DecodedSizeFromTail(total, buf);
|
|
66818
|
+
}
|
|
66819
|
+
if (exact !== void 0) out[p.i] = { size: exact, exact: true };
|
|
66820
|
+
}
|
|
66821
|
+
return out;
|
|
66822
|
+
}
|
|
66684
66823
|
async function imapListAttachments(id, deps = {}) {
|
|
66685
66824
|
const ref = decodeImapId(id);
|
|
66686
66825
|
if (!ref) return { success: false, error: `Not an IMAP message id: "${id}".` };
|
|
@@ -66689,12 +66828,18 @@ async function imapListAttachments(id, deps = {}) {
|
|
|
66689
66828
|
if (!msg || !msg.bodyStructure) {
|
|
66690
66829
|
return { success: false, error: `IMAP message UID ${ref.uid} not found in "${ref.path}".` };
|
|
66691
66830
|
}
|
|
66692
|
-
const
|
|
66693
|
-
|
|
66694
|
-
|
|
66695
|
-
|
|
66696
|
-
|
|
66697
|
-
|
|
66831
|
+
const atts = collectAttachments(msg.bodyStructure);
|
|
66832
|
+
const sizes = await resolveDecodedSizes(client, ref.uid, atts);
|
|
66833
|
+
const attachments = atts.map((a, i) => {
|
|
66834
|
+
const info = {
|
|
66835
|
+
id: `${id}#${a.part}`,
|
|
66836
|
+
name: a.filename,
|
|
66837
|
+
mimeType: a.mimeType,
|
|
66838
|
+
size: sizes[i].size
|
|
66839
|
+
};
|
|
66840
|
+
if (!sizes[i].exact) info.sizeApproximate = true;
|
|
66841
|
+
return info;
|
|
66842
|
+
});
|
|
66698
66843
|
return { success: true, attachments };
|
|
66699
66844
|
});
|
|
66700
66845
|
}
|
|
@@ -66715,10 +66860,11 @@ async function imapFetchAttachment(id, attachmentName, deps = {}) {
|
|
|
66715
66860
|
error: `Attachment "${attachmentName}" not found on UID ${ref.uid}. Available: ${names}.`
|
|
66716
66861
|
};
|
|
66717
66862
|
}
|
|
66718
|
-
|
|
66863
|
+
const expected = decodedSizeWithoutBody(match).size;
|
|
66864
|
+
if (expected > MAX_IMAP_ATTACHMENT_BYTES) {
|
|
66719
66865
|
return {
|
|
66720
66866
|
success: false,
|
|
66721
|
-
error: `IMAP attachment "${attachmentName}" is ${
|
|
66867
|
+
error: `IMAP attachment "${attachmentName}" is ${expected} bytes; the maximum is ${MAX_IMAP_ATTACHMENT_BYTES} bytes (25 MiB).`
|
|
66722
66868
|
};
|
|
66723
66869
|
}
|
|
66724
66870
|
try {
|
|
@@ -66906,7 +67052,7 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
66906
67052
|
true
|
|
66907
67053
|
);
|
|
66908
67054
|
}
|
|
66909
|
-
var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, NOT_DELETED, LARGE_MAILBOX_MESSAGES, FIRST_SEARCH_WINDOW, MAX_WINDOW, MAX_UNSCOPED_SEARCH_DEPTH, ROW_QUERY, ROW_QUERY_NO_STRUCTURE, ROW_QUERY_HEADERS_ONLY, DELETED_MARGIN, poolConnect, pools, connecting, MAX_COMPOSE_SOURCE_BYTES, MAX_RFC822_INLINE_BYTES, MAX_RFC822_FILE_BYTES, HEADER_WINDOW_BYTES, MAIL_FLAG_BITS, imapMarkRead, imapMarkUnread, FALLBACK_TRASH_PATH, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete;
|
|
67055
|
+
var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, NOT_DELETED, LARGE_MAILBOX_MESSAGES, FIRST_SEARCH_WINDOW, MAX_WINDOW, MAX_UNSCOPED_SEARCH_DEPTH, ROW_QUERY, ROW_QUERY_NO_STRUCTURE, ROW_QUERY_HEADERS_ONLY, DELETED_MARGIN, poolConnect, pools, connecting, MAX_COMPOSE_SOURCE_BYTES, MAX_RFC822_INLINE_BYTES, MAX_RFC822_FILE_BYTES, HEADER_WINDOW_BYTES, MAIL_FLAG_BITS, imapMarkRead, imapMarkUnread, FALLBACK_TRASH_PATH, BASE64_TAIL_WINDOW, QP_EXACT_MAX_BYTES, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete;
|
|
66910
67056
|
var init_imapClient = __esm({
|
|
66911
67057
|
"src/services/imapClient.ts"() {
|
|
66912
67058
|
"use strict";
|
|
@@ -67011,6 +67157,8 @@ var init_imapClient = __esm({
|
|
|
67011
67157
|
imapMarkRead = (id, deps = {}) => flagOp(id, "\\Seen", true, deps);
|
|
67012
67158
|
imapMarkUnread = (id, deps = {}) => flagOp(id, "\\Seen", false, deps);
|
|
67013
67159
|
FALLBACK_TRASH_PATH = "Trash";
|
|
67160
|
+
BASE64_TAIL_WINDOW = 1024;
|
|
67161
|
+
QP_EXACT_MAX_BYTES = 64 * 1024;
|
|
67014
67162
|
imapBatchMarkRead = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids) => {
|
|
67015
67163
|
assertMutated(
|
|
67016
67164
|
await c.messageFlagsAdd(uids, ["\\Seen"], { uid: true }),
|
|
@@ -67146,6 +67294,34 @@ function buildAttachments(attachments) {
|
|
|
67146
67294
|
return { filename: a.filename, content: decodeInlineAttachment(a.contentBase64) };
|
|
67147
67295
|
});
|
|
67148
67296
|
}
|
|
67297
|
+
function buildMailOptions(opts, from, attachments) {
|
|
67298
|
+
const html = opts.htmlBody?.trim() ? opts.htmlBody : void 0;
|
|
67299
|
+
return {
|
|
67300
|
+
from,
|
|
67301
|
+
to: opts.to,
|
|
67302
|
+
cc: opts.cc,
|
|
67303
|
+
bcc: opts.bcc,
|
|
67304
|
+
subject: opts.subject,
|
|
67305
|
+
text: opts.body,
|
|
67306
|
+
// When present, nodemailer emits multipart/alternative (text + html).
|
|
67307
|
+
html,
|
|
67308
|
+
attachments,
|
|
67309
|
+
// RFC 5322 threading for SMTP replies/forwards (2.5.0). nodemailer treats
|
|
67310
|
+
// In-Reply-To / References as structured msg-id headers and never
|
|
67311
|
+
// RFC 2047-encodes them, however long the id (pinned by a test, #267).
|
|
67312
|
+
inReplyTo: opts.inReplyTo?.trim() || void 0,
|
|
67313
|
+
references: opts.references?.length ? opts.references : void 0,
|
|
67314
|
+
// Reply-To header (2.18.0, issue #220): nodemailer already supports this.
|
|
67315
|
+
replyTo: opts.replyTo?.trim() || void 0
|
|
67316
|
+
};
|
|
67317
|
+
}
|
|
67318
|
+
function composeRawMime(opts, from) {
|
|
67319
|
+
return buildRawMime({
|
|
67320
|
+
...buildMailOptions(opts, from, buildAttachments(opts.attachments)),
|
|
67321
|
+
keepBcc: true,
|
|
67322
|
+
date: /* @__PURE__ */ new Date()
|
|
67323
|
+
});
|
|
67324
|
+
}
|
|
67149
67325
|
async function sendViaSmtp(opts, config2, createTransport2 = nodemailer_default.createTransport, appendSentCopy = defaultAppendSentCopy) {
|
|
67150
67326
|
let cfg;
|
|
67151
67327
|
try {
|
|
@@ -67184,23 +67360,7 @@ async function sendViaSmtp(opts, config2, createTransport2 = nodemailer_default.
|
|
|
67184
67360
|
requireTLS,
|
|
67185
67361
|
auth: { user: cfg.user, pass: cfg.pass }
|
|
67186
67362
|
});
|
|
67187
|
-
const
|
|
67188
|
-
const mailOptions = {
|
|
67189
|
-
from: requestedFrom || cfg.from,
|
|
67190
|
-
to: opts.to,
|
|
67191
|
-
cc: opts.cc,
|
|
67192
|
-
bcc: opts.bcc,
|
|
67193
|
-
subject: opts.subject,
|
|
67194
|
-
text: opts.body,
|
|
67195
|
-
// When present, nodemailer emits multipart/alternative (text + html).
|
|
67196
|
-
html,
|
|
67197
|
-
attachments,
|
|
67198
|
-
// RFC 5322 threading for SMTP replies/forwards (2.5.0).
|
|
67199
|
-
inReplyTo: opts.inReplyTo?.trim() || void 0,
|
|
67200
|
-
references: opts.references?.length ? opts.references : void 0,
|
|
67201
|
-
// Reply-To header (2.18.0, issue #220): nodemailer already supports this.
|
|
67202
|
-
replyTo: opts.replyTo?.trim() || void 0
|
|
67203
|
-
};
|
|
67363
|
+
const mailOptions = buildMailOptions(opts, requestedFrom || cfg.from, attachments);
|
|
67204
67364
|
try {
|
|
67205
67365
|
const info = await transporter.sendMail(mailOptions);
|
|
67206
67366
|
let copyFields = {};
|
|
@@ -82144,7 +82304,7 @@ function materializeAttachments(attachments) {
|
|
|
82144
82304
|
if (!dir) dir = mkdtempSync(join3(tmpdir2(), "amcp-att-"));
|
|
82145
82305
|
const safeName = a.filename.replace(/[/\\]/g, "_");
|
|
82146
82306
|
const p = join3(dir, safeName);
|
|
82147
|
-
writeFileSync2(p, decodeInlineAttachment(a.contentBase64));
|
|
82307
|
+
writeFileSync2(p, decodeInlineAttachment(a.contentBase64), { mode: 384 });
|
|
82148
82308
|
return p;
|
|
82149
82309
|
});
|
|
82150
82310
|
} catch (error3) {
|
|
@@ -82158,6 +82318,18 @@ function materializeAttachments(attachments) {
|
|
|
82158
82318
|
}
|
|
82159
82319
|
};
|
|
82160
82320
|
}
|
|
82321
|
+
function preflightAttachments(attachments) {
|
|
82322
|
+
for (const a of attachments ?? []) {
|
|
82323
|
+
if (typeof a === "string") {
|
|
82324
|
+
resolveAttachmentReadPath(a);
|
|
82325
|
+
continue;
|
|
82326
|
+
}
|
|
82327
|
+
if (!a.filename || !a.contentBase64) {
|
|
82328
|
+
throw new Error("Inline attachment requires both filename and contentBase64.");
|
|
82329
|
+
}
|
|
82330
|
+
decodeInlineAttachment(a.contentBase64);
|
|
82331
|
+
}
|
|
82332
|
+
}
|
|
82161
82333
|
|
|
82162
82334
|
// src/services/appleMailManager.ts
|
|
82163
82335
|
init_attachmentReadPolicy();
|
|
@@ -82421,6 +82593,11 @@ function buildAttachmentCommands(attachments) {
|
|
|
82421
82593
|
}
|
|
82422
82594
|
return commands;
|
|
82423
82595
|
}
|
|
82596
|
+
function composeAttachmentBlock(msgVar, paths) {
|
|
82597
|
+
const commands = buildAttachmentCommands(paths);
|
|
82598
|
+
return commands ? `tell ${msgVar}
|
|
82599
|
+
${commands}end tell` : "";
|
|
82600
|
+
}
|
|
82424
82601
|
function asDateToString(varName) {
|
|
82425
82602
|
return `((year of ${varName}) as string) & "-" & ((month of ${varName} as integer) as string) & "-" & ((day of ${varName}) as string) & "-" & ((hours of ${varName}) as string) & "-" & ((minutes of ${varName}) as string) & "-" & ((seconds of ${varName}) as string)`;
|
|
82426
82603
|
}
|
|
@@ -84670,20 +84847,28 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84670
84847
|
* @param body - Reply body
|
|
84671
84848
|
* @param replyAll - If true, reply to all recipients
|
|
84672
84849
|
* @param send - If true, send immediately; if false, save as draft
|
|
84850
|
+
* @param attachments - Files to attach (#267): allowlisted paths and/or inline
|
|
84851
|
+
* base64 items (materialized to 0600 temp files, removed afterwards)
|
|
84673
84852
|
* @returns true if reply created/sent successfully
|
|
84674
84853
|
*/
|
|
84675
|
-
replyToMessage(id, body, replyAll = false, send = true) {
|
|
84854
|
+
replyToMessage(id, body, replyAll = false, send = true, attachments) {
|
|
84676
84855
|
const safeBody = escapeForAppleScriptBody(body);
|
|
84677
84856
|
const replyAllClause = replyAll ? " with reply to all" : "";
|
|
84678
84857
|
const finalAction = send ? "send theReply" : "save theReply\n close theReply saving yes";
|
|
84679
|
-
const
|
|
84680
|
-
|
|
84681
|
-
|
|
84858
|
+
const mat = materializeAttachments(attachments);
|
|
84859
|
+
try {
|
|
84860
|
+
const script = this.findMessageScript(
|
|
84861
|
+
id,
|
|
84862
|
+
`
|
|
84682
84863
|
set theReply to reply msg without opening window${replyAllClause}
|
|
84683
84864
|
set content of theReply to "${safeBody}"
|
|
84865
|
+
${composeAttachmentBlock("theReply", mat.paths)}
|
|
84684
84866
|
${finalAction}`
|
|
84685
|
-
|
|
84686
|
-
|
|
84867
|
+
);
|
|
84868
|
+
return this.runComposeScript(script, "reply to");
|
|
84869
|
+
} finally {
|
|
84870
|
+
mat.cleanup();
|
|
84871
|
+
}
|
|
84687
84872
|
}
|
|
84688
84873
|
/**
|
|
84689
84874
|
* Forward a message.
|
|
@@ -84692,9 +84877,10 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84692
84877
|
* @param to - Recipients to forward to
|
|
84693
84878
|
* @param body - Optional body to prepend
|
|
84694
84879
|
* @param send - If true, send immediately; if false, save as draft
|
|
84880
|
+
* @param attachments - Additional files to attach (#267), as for replyToMessage
|
|
84695
84881
|
* @returns true if forward created/sent successfully
|
|
84696
84882
|
*/
|
|
84697
|
-
forwardMessage(id, to, body, send = true) {
|
|
84883
|
+
forwardMessage(id, to, body, send = true, attachments) {
|
|
84698
84884
|
const safeBody = body ? escapeForAppleScriptBody(body) : "";
|
|
84699
84885
|
const finalAction = send ? "send theForward" : "save theForward\n close theForward saving yes";
|
|
84700
84886
|
let recipientCommands = "";
|
|
@@ -84702,15 +84888,21 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84702
84888
|
recipientCommands += `make new to recipient at end of to recipients of theForward with properties {address:"${escapeForAppleScript(addr)}"}
|
|
84703
84889
|
`;
|
|
84704
84890
|
}
|
|
84705
|
-
const
|
|
84706
|
-
|
|
84707
|
-
|
|
84891
|
+
const mat = materializeAttachments(attachments);
|
|
84892
|
+
try {
|
|
84893
|
+
const script = this.findMessageScript(
|
|
84894
|
+
id,
|
|
84895
|
+
`
|
|
84708
84896
|
set theForward to forward msg without opening window
|
|
84709
84897
|
${recipientCommands}
|
|
84710
84898
|
${safeBody ? `set content of theForward to "${safeBody}"` : ""}
|
|
84899
|
+
${composeAttachmentBlock("theForward", mat.paths)}
|
|
84711
84900
|
${finalAction}`
|
|
84712
|
-
|
|
84713
|
-
|
|
84901
|
+
);
|
|
84902
|
+
return this.runComposeScript(script, "forward");
|
|
84903
|
+
} finally {
|
|
84904
|
+
mat.cleanup();
|
|
84905
|
+
}
|
|
84714
84906
|
}
|
|
84715
84907
|
/**
|
|
84716
84908
|
* Helper to find and operate on a message by ID, scoped to the mailbox the id
|
|
@@ -84745,7 +84937,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84745
84937
|
* behind one indistinguishable message.
|
|
84746
84938
|
*/
|
|
84747
84939
|
runComposeScript(script, verb) {
|
|
84748
|
-
const result = executeAppleScript(script, { timeoutMs: 6e4 });
|
|
84940
|
+
const result = executeAppleScript(script, { timeoutMs: 6e4, maxRetries: 1 });
|
|
84749
84941
|
if (!result.success || result.output.startsWith("error:")) {
|
|
84750
84942
|
const raw = result.error || result.output;
|
|
84751
84943
|
const error3 = raw.startsWith("error:") ? raw.slice("error:".length) : raw;
|
|
@@ -84935,13 +85127,13 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84935
85127
|
try
|
|
84936
85128
|
set inMb to mailbox "INBOX" of acct
|
|
84937
85129
|
set inHits to ${matchClause("inMb")}
|
|
84938
|
-
if (count of inHits) > 0 then return (id of (item 1 of inHits)) as string
|
|
85130
|
+
if (count of inHits) > 0 then return ((id of (item 1 of inHits)) as string) & tab & (name of inMb) & tab & (name of acct)
|
|
84939
85131
|
end try
|
|
84940
85132
|
repeat with mb in mailboxes of acct
|
|
84941
85133
|
try
|
|
84942
85134
|
set matchingMsgs to ${matchClause("mb")}
|
|
84943
85135
|
if (count of matchingMsgs) > 0 then
|
|
84944
|
-
return (id of (item 1 of matchingMsgs)) as string
|
|
85136
|
+
return ((id of (item 1 of matchingMsgs)) as string) & tab & (name of mb) & tab & (name of acct)
|
|
84945
85137
|
end if
|
|
84946
85138
|
end try
|
|
84947
85139
|
end repeat
|
|
@@ -84953,8 +85145,10 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84953
85145
|
`);
|
|
84954
85146
|
const result = executeAppleScript(script, { timeoutMs: 6e4 });
|
|
84955
85147
|
if (!result.success || result.output.startsWith("error:")) return null;
|
|
84956
|
-
const
|
|
84957
|
-
|
|
85148
|
+
const [numericId, mailbox, account] = result.output.trim().split(" ");
|
|
85149
|
+
if (!/^\d+$/.test(numericId ?? "")) return null;
|
|
85150
|
+
if (mailbox && account) this.rememberLocation(numericId, account, mailbox);
|
|
85151
|
+
return numericId;
|
|
84958
85152
|
}
|
|
84959
85153
|
/**
|
|
84960
85154
|
* Unflag a message.
|
|
@@ -87195,19 +87389,28 @@ async function replyComposeBody(deps, id, body) {
|
|
|
87195
87389
|
const source = await readOriginalForQuote(deps, id);
|
|
87196
87390
|
return source ? buildReplyBody(body, source.original, source.plainText) : body;
|
|
87197
87391
|
}
|
|
87198
|
-
async function forwardComposeBody(deps, id, body) {
|
|
87199
|
-
if (!body) return body;
|
|
87392
|
+
async function forwardComposeBody(deps, id, body, hasAttachments = false) {
|
|
87393
|
+
if (!body && !hasAttachments) return body;
|
|
87200
87394
|
const source = await readOriginalForQuote(deps, id);
|
|
87201
87395
|
return source ? buildForwardBody(source.original, source.plainText, body) : body;
|
|
87202
87396
|
}
|
|
87203
87397
|
async function runCompose(deps, args) {
|
|
87204
|
-
const { id, send, transport: transport2 } = args;
|
|
87398
|
+
const { id, send, transport: transport2, attachments } = args;
|
|
87205
87399
|
const verb = args.kind === "reply" ? "reply to" : "forward";
|
|
87206
87400
|
if (!send && transport2 === "smtp") {
|
|
87207
87401
|
return errorResponse(
|
|
87208
87402
|
"SMTP cannot save a Mail.app draft. Omit transport or use transport=applescript with send=false."
|
|
87209
87403
|
);
|
|
87210
87404
|
}
|
|
87405
|
+
try {
|
|
87406
|
+
preflightAttachments(attachments);
|
|
87407
|
+
} catch (error3) {
|
|
87408
|
+
return errorResponse(
|
|
87409
|
+
`Failed to ${verb} message "${id}": ${error3 instanceof Error ? error3.message : String(error3)}`
|
|
87410
|
+
);
|
|
87411
|
+
}
|
|
87412
|
+
const attachmentCount = attachments?.length ?? 0;
|
|
87413
|
+
const withFiles = attachmentCount ? ` with ${attachmentCount} attachment(s)` : "";
|
|
87211
87414
|
const smtp = send && transport2 !== "applescript" && (transport2 === "smtp" || deps.smtpConfigured());
|
|
87212
87415
|
if (smtp) {
|
|
87213
87416
|
try {
|
|
@@ -87239,19 +87442,21 @@ async function runCompose(deps, args) {
|
|
|
87239
87442
|
body: args.body,
|
|
87240
87443
|
from: cfg.from
|
|
87241
87444
|
});
|
|
87445
|
+
if (attachmentCount) opts.attachments = attachments;
|
|
87242
87446
|
const result = await deps.smtpSend(opts, cfg);
|
|
87243
87447
|
if (!result.success)
|
|
87244
87448
|
return errorResponse(
|
|
87245
87449
|
`Failed to ${verb} message "${id}" via SMTP: ${result.error ?? "unknown SMTP error"}`
|
|
87246
87450
|
);
|
|
87247
87451
|
return successResponse(
|
|
87248
|
-
args.kind === "reply" ?
|
|
87452
|
+
args.kind === "reply" ? `Reply sent via SMTP${withFiles}` : `Message forwarded via SMTP to ${args.to.join(", ")}${withFiles}`,
|
|
87249
87453
|
{
|
|
87250
87454
|
ok: true,
|
|
87251
87455
|
sent: true,
|
|
87252
87456
|
id,
|
|
87253
87457
|
transport: "smtp",
|
|
87254
87458
|
messageId: result.messageId,
|
|
87459
|
+
attachmentCount,
|
|
87255
87460
|
...args.kind === "forward" ? { recipients: args.to } : {},
|
|
87256
87461
|
// Best-effort Sent-folder copy (issue #220) — same field shape as
|
|
87257
87462
|
// send-email, since it's the same sendViaSmtp underneath.
|
|
@@ -87274,23 +87479,26 @@ async function runCompose(deps, args) {
|
|
|
87274
87479
|
resolved.numericId,
|
|
87275
87480
|
await replyComposeBody(deps, id, args.body),
|
|
87276
87481
|
args.replyAll,
|
|
87277
|
-
send
|
|
87482
|
+
send,
|
|
87483
|
+
attachmentCount ? attachments : void 0
|
|
87278
87484
|
) : deps.mail.forwardMessage(
|
|
87279
87485
|
resolved.numericId,
|
|
87280
87486
|
args.to,
|
|
87281
|
-
await forwardComposeBody(deps, id, args.body),
|
|
87282
|
-
send
|
|
87487
|
+
await forwardComposeBody(deps, id, args.body, attachmentCount > 0),
|
|
87488
|
+
send,
|
|
87489
|
+
attachmentCount ? attachments : void 0
|
|
87283
87490
|
);
|
|
87284
87491
|
if (!outcome.success)
|
|
87285
87492
|
return errorResponse(
|
|
87286
87493
|
`Failed to ${verb} message "${id}": ${outcome.error ?? "Mail.app compose failed"}`
|
|
87287
87494
|
);
|
|
87288
|
-
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";
|
|
87495
|
+
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") + withFiles;
|
|
87289
87496
|
return successResponse(text, {
|
|
87290
87497
|
ok: true,
|
|
87291
87498
|
sent: send,
|
|
87292
87499
|
id,
|
|
87293
87500
|
transport: "applescript",
|
|
87501
|
+
attachmentCount,
|
|
87294
87502
|
...args.kind === "forward" ? { recipients: args.to } : {}
|
|
87295
87503
|
});
|
|
87296
87504
|
}
|
|
@@ -87301,6 +87509,70 @@ function runForward(deps, args) {
|
|
|
87301
87509
|
return runCompose(deps, { ...args, kind: "forward" });
|
|
87302
87510
|
}
|
|
87303
87511
|
|
|
87512
|
+
// src/tools/threading.ts
|
|
87513
|
+
function threadingHeaders(args) {
|
|
87514
|
+
const inReplyTo = args.inReplyTo?.trim() || void 0;
|
|
87515
|
+
const references = args.references?.length ? args.references : void 0;
|
|
87516
|
+
if (!inReplyTo && !references) return void 0;
|
|
87517
|
+
return { inReplyTo, references: references ?? (inReplyTo ? [inReplyTo] : void 0) };
|
|
87518
|
+
}
|
|
87519
|
+
var SEND_THREADING_NEEDS_SMTP = "inReplyTo/references require the SMTP transport: Mail.app's AppleScript cannot set In-Reply-To or References, so the message would go out unthreaded. Configure SMTP (and do not pass transport=applescript or a Mail.app account label), or use reply-to-message, which threads on both transports.";
|
|
87520
|
+
function sendEmailThreading(args, useSmtp) {
|
|
87521
|
+
const headers = threadingHeaders(args);
|
|
87522
|
+
if (headers && !useSmtp) return { ok: false, error: SEND_THREADING_NEEDS_SMTP };
|
|
87523
|
+
return { ok: true, headers };
|
|
87524
|
+
}
|
|
87525
|
+
function messageIdOf(raw) {
|
|
87526
|
+
const head = raw.toString("utf8").split(/\r?\n\r?\n/)[0] ?? "";
|
|
87527
|
+
return head.replace(/\r?\n[ \t]+/g, " ").match(/^Message-ID:\s*(<[^>\s]+>)/im)?.[1];
|
|
87528
|
+
}
|
|
87529
|
+
async function runThreadedDraft(deps, args) {
|
|
87530
|
+
const threading = threadingHeaders(args);
|
|
87531
|
+
if (!threading) throw new Error("runThreadedDraft called without threading headers");
|
|
87532
|
+
const fail = (e) => errorResponse(
|
|
87533
|
+
`Failed to create threaded draft: ${e instanceof Error ? e.message : String(e)} Nothing was filed; no Mail.app fallback was attempted (it would drop the threading headers).`
|
|
87534
|
+
);
|
|
87535
|
+
try {
|
|
87536
|
+
preflightAttachments(args.attachments);
|
|
87537
|
+
const smtp = deps.smtpIdentity();
|
|
87538
|
+
const acct = deps.resolveAccount(args.account, smtp.user);
|
|
87539
|
+
const from = smtp.user && smtp.user.toLowerCase() === acct.user.toLowerCase() && smtp.from ? smtp.from : acct.user;
|
|
87540
|
+
const raw = await deps.compose(
|
|
87541
|
+
{
|
|
87542
|
+
to: args.to,
|
|
87543
|
+
cc: args.cc,
|
|
87544
|
+
bcc: args.bcc,
|
|
87545
|
+
subject: args.subject,
|
|
87546
|
+
body: args.body,
|
|
87547
|
+
attachments: args.attachments,
|
|
87548
|
+
inReplyTo: threading.inReplyTo,
|
|
87549
|
+
references: threading.references
|
|
87550
|
+
},
|
|
87551
|
+
from
|
|
87552
|
+
);
|
|
87553
|
+
const filed = await deps.append(acct.label, raw);
|
|
87554
|
+
const attachmentCount = args.attachments?.length ?? 0;
|
|
87555
|
+
const attachInfo = attachmentCount ? ` with ${attachmentCount} attachment(s)` : "";
|
|
87556
|
+
const messageId = messageIdOf(raw);
|
|
87557
|
+
return successResponse(
|
|
87558
|
+
`Threaded draft for ${args.to.join(", ")}${attachInfo} filed over IMAP in "${filed.mailbox}" (account ${filed.account}). Review and send it from Mail.app.`,
|
|
87559
|
+
{
|
|
87560
|
+
ok: true,
|
|
87561
|
+
recipients: args.to,
|
|
87562
|
+
attachmentCount,
|
|
87563
|
+
transport: "imap",
|
|
87564
|
+
account: filed.account,
|
|
87565
|
+
mailbox: filed.mailbox,
|
|
87566
|
+
...messageId ? { messageId } : {},
|
|
87567
|
+
...threading.inReplyTo ? { inReplyTo: threading.inReplyTo } : {},
|
|
87568
|
+
references: threading.references
|
|
87569
|
+
}
|
|
87570
|
+
);
|
|
87571
|
+
} catch (e) {
|
|
87572
|
+
return fail(e);
|
|
87573
|
+
}
|
|
87574
|
+
}
|
|
87575
|
+
|
|
87304
87576
|
// src/index.ts
|
|
87305
87577
|
init_imapClient();
|
|
87306
87578
|
|
|
@@ -87812,6 +88084,17 @@ var ATTACHMENTS_SCHEMA = external_exports.array(
|
|
|
87812
88084
|
).max(20, "Cannot attach more than 20 files").optional().describe(
|
|
87813
88085
|
"Files to attach: absolute paths in the configured attachment read roots (e.g. '/Users/me/Documents/report.pdf') and/or inline {filename, contentBase64} objects up to 25 MiB decoded each."
|
|
87814
88086
|
);
|
|
88087
|
+
var RFC_MESSAGE_ID_TOKEN = /^<[^<>\s@]+@[^<>\s@]+>$/;
|
|
88088
|
+
var MESSAGE_ID_TOKEN_SCHEMA = external_exports.string().max(998, "Message-ID is longer than an RFC 5322 header line (998 chars)").regex(
|
|
88089
|
+
RFC_MESSAGE_ID_TOKEN,
|
|
88090
|
+
"Must be an RFC 5322 Message-ID token including angle brackets, e.g. <abc123@mail.example.com>"
|
|
88091
|
+
);
|
|
88092
|
+
var IN_REPLY_TO_SCHEMA = MESSAGE_ID_TOKEN_SCHEMA.optional().describe(
|
|
88093
|
+
"Thread this message as a reply: the parent's Message-ID token, angle brackets included (e.g. '<abc123@mail.example.com>'). Emitted verbatim as In-Reply-To, never RFC 2047-encoded. When `references` is omitted, References defaults to [inReplyTo]. Prefer reply-to-message, which derives recipients, threading and the quote from the original for you."
|
|
88094
|
+
);
|
|
88095
|
+
var REFERENCES_SCHEMA = external_exports.array(MESSAGE_ID_TOKEN_SCHEMA).min(1, "references must contain at least one Message-ID").max(100, "Cannot list more than 100 Message-IDs in references").optional().describe(
|
|
88096
|
+
"Thread chain, oldest first: the parent's own References followed by the parent's Message-ID (each an angle-bracketed token). Emitted verbatim, never RFC 2047-encoded."
|
|
88097
|
+
);
|
|
87815
88098
|
|
|
87816
88099
|
// src/tools/thread.ts
|
|
87817
88100
|
function normalizeSubject(subject) {
|
|
@@ -88896,7 +89179,7 @@ var SENT_COPY_ERROR_SCHEMA = external_exports.string().optional().describe("Pres
|
|
|
88896
89179
|
registerTool(
|
|
88897
89180
|
"send-email",
|
|
88898
89181
|
{
|
|
88899
|
-
description: "Use when: the user has explicitly confirmed they want to send a single email now to the given recipients (to/cc/bcc are arrays), optionally with attachments
|
|
89182
|
+
description: "Use when: the user has explicitly confirmed they want to send a single email now to the given recipients (to/cc/bcc are arrays), optionally with attachments, a chosen transport, and caller-built threading headers (inReplyTo/references, SMTP only).\nReturns: a confirmation naming the recipients and attachment count; over SMTP, also whether a Sent-folder copy was filed (sentCopy).\nDo not use when: the user wants to review first (use create-draft), is replying to or forwarding an existing message (use reply-to-message / forward-message \u2014 they derive recipients, threading and the quote, and also take attachments), or wants per-recipient personalized copies (use send-serial-email).\nSafety: this SENDS real email immediately and it cannot be unsent \u2014 require explicit user confirmation of the exact recipients, subject, and body before calling. Prefer create-draft when there is any doubt.",
|
|
88900
89183
|
inputSchema: {
|
|
88901
89184
|
to: external_exports.array(external_exports.string()).min(1, "At least one recipient is required"),
|
|
88902
89185
|
subject: external_exports.string().min(1, "Subject is required"),
|
|
@@ -88905,6 +89188,8 @@ registerTool(
|
|
|
88905
89188
|
bcc: external_exports.array(external_exports.string()).optional().describe("BCC recipients"),
|
|
88906
89189
|
account: external_exports.string().optional().describe("Account to send from"),
|
|
88907
89190
|
attachments: ATTACHMENTS_SCHEMA,
|
|
89191
|
+
inReplyTo: IN_REPLY_TO_SCHEMA,
|
|
89192
|
+
references: REFERENCES_SCHEMA,
|
|
88908
89193
|
replyTo: external_exports.string().optional().describe(
|
|
88909
89194
|
"SMTP only (issue #220): sets the Reply-To header when replies should go somewhere other than the From/login address, e.g. a domain-alias setup where APPLE_MAIL_MCP_SMTP_FROM differs from APPLE_MAIL_MCP_SMTP_USER. Ignored on the AppleScript transport."
|
|
88910
89195
|
),
|
|
@@ -88917,15 +89202,31 @@ registerTool(
|
|
|
88917
89202
|
recipients: external_exports.array(external_exports.string()).optional(),
|
|
88918
89203
|
attachmentCount: external_exports.number().optional(),
|
|
88919
89204
|
transport: external_exports.string().optional(),
|
|
89205
|
+
messageId: external_exports.string().optional(),
|
|
88920
89206
|
sentCopy: SENT_COPY_SCHEMA,
|
|
88921
89207
|
sentCopyError: SENT_COPY_ERROR_SCHEMA
|
|
88922
89208
|
}
|
|
88923
89209
|
},
|
|
88924
89210
|
withErrorHandling(
|
|
88925
|
-
async ({
|
|
89211
|
+
async ({
|
|
89212
|
+
to,
|
|
89213
|
+
subject,
|
|
89214
|
+
body,
|
|
89215
|
+
cc,
|
|
89216
|
+
bcc,
|
|
89217
|
+
account,
|
|
89218
|
+
attachments,
|
|
89219
|
+
transport: transport2,
|
|
89220
|
+
replyTo,
|
|
89221
|
+
inReplyTo,
|
|
89222
|
+
references
|
|
89223
|
+
}) => {
|
|
88926
89224
|
const attachInfo = attachments?.length ? ` with ${attachments.length} attachment(s)` : "";
|
|
88927
89225
|
const attachmentCount = attachments?.length ?? 0;
|
|
88928
|
-
|
|
89226
|
+
const useSmtp = shouldUseSmtp(transport2, account);
|
|
89227
|
+
const threading = sendEmailThreading({ inReplyTo, references }, useSmtp);
|
|
89228
|
+
if (!threading.ok) return errorResponse(threading.error);
|
|
89229
|
+
if (useSmtp) {
|
|
88929
89230
|
const smtpFrom = account?.includes("@") ? account : void 0;
|
|
88930
89231
|
const result = await sendViaSmtp({
|
|
88931
89232
|
to,
|
|
@@ -88935,7 +89236,8 @@ registerTool(
|
|
|
88935
89236
|
bcc,
|
|
88936
89237
|
from: smtpFrom,
|
|
88937
89238
|
attachments,
|
|
88938
|
-
replyTo
|
|
89239
|
+
replyTo,
|
|
89240
|
+
...threading.headers
|
|
88939
89241
|
});
|
|
88940
89242
|
if (!result.success) {
|
|
88941
89243
|
return errorResponse(result.error ?? "Failed to send email via SMTP.");
|
|
@@ -88946,6 +89248,7 @@ registerTool(
|
|
|
88946
89248
|
recipients: to,
|
|
88947
89249
|
attachmentCount,
|
|
88948
89250
|
transport: "smtp",
|
|
89251
|
+
...result.messageId ? { messageId: result.messageId } : {},
|
|
88949
89252
|
...result.sentCopy !== void 0 ? { sentCopy: result.sentCopy } : {},
|
|
88950
89253
|
...result.sentCopyError !== void 0 ? { sentCopyError: result.sentCopyError } : {}
|
|
88951
89254
|
});
|
|
@@ -89023,36 +89326,73 @@ ${details}`,
|
|
|
89023
89326
|
registerTool(
|
|
89024
89327
|
"create-draft",
|
|
89025
89328
|
{
|
|
89026
|
-
description: "Use when: composing an email the user should review in Mail.app before sending \u2014 the safe default for any new message (to/cc/bcc are arrays, optional attachments).\nReturns: a confirmation that the draft was created, with recipients and attachment count.\nDo not use when: the user has already confirmed they want it sent now (use send-email).\nSafety: low risk \u2014 creates a draft only and sends nothing; the user must open Mail.app and send it themselves.",
|
|
89329
|
+
description: "Use when: composing an email the user should review in Mail.app before sending \u2014 the safe default for any new message (to/cc/bcc are arrays, optional attachments). With inReplyTo/references the draft is threaded: it is composed by this server and filed over IMAP into the account's Drafts mailbox (Mail.app's AppleScript cannot set those headers), so an IMAP account is required.\nReturns: a confirmation that the draft was created, with recipients and attachment count; for a threaded draft also its Drafts mailbox, account and Message-ID (transport: imap).\nDo not use when: the user has already confirmed they want it sent now (use send-email), or is replying to an existing message (use reply-to-message with send=false \u2014 it derives recipients, threading and the quote and takes attachments).\nSafety: low risk \u2014 creates a draft only and sends nothing; the user must open Mail.app and send it themselves.",
|
|
89027
89330
|
inputSchema: {
|
|
89028
89331
|
to: external_exports.array(external_exports.string()).min(1, "At least one recipient is required"),
|
|
89029
89332
|
subject: external_exports.string().min(1, "Subject is required"),
|
|
89030
89333
|
body: external_exports.string().min(1, "Body is required"),
|
|
89031
89334
|
cc: external_exports.array(external_exports.string()).optional().describe("CC recipients"),
|
|
89032
89335
|
bcc: external_exports.array(external_exports.string()).optional().describe("BCC recipients"),
|
|
89033
|
-
account: external_exports.string().optional().describe(
|
|
89034
|
-
|
|
89336
|
+
account: external_exports.string().optional().describe(
|
|
89337
|
+
"Account to create draft in. With inReplyTo/references this must name a configured IMAP account (label or login); when omitted, the IMAP account matching the SMTP identity, else the only IMAP account, is used."
|
|
89338
|
+
),
|
|
89339
|
+
attachments: ATTACHMENTS_SCHEMA,
|
|
89340
|
+
inReplyTo: IN_REPLY_TO_SCHEMA,
|
|
89341
|
+
references: REFERENCES_SCHEMA
|
|
89035
89342
|
},
|
|
89036
89343
|
outputSchema: {
|
|
89037
89344
|
ok: external_exports.boolean().optional(),
|
|
89038
89345
|
recipients: external_exports.array(external_exports.string()).optional(),
|
|
89039
|
-
attachmentCount: external_exports.number().optional()
|
|
89346
|
+
attachmentCount: external_exports.number().optional(),
|
|
89347
|
+
transport: external_exports.enum(["applescript", "imap"]).optional(),
|
|
89348
|
+
account: external_exports.string().optional().describe("Threaded draft only: IMAP account it was filed in"),
|
|
89349
|
+
mailbox: external_exports.string().optional().describe("Threaded draft only: Drafts mailbox path"),
|
|
89350
|
+
messageId: external_exports.string().optional().describe("Threaded draft only: the draft's Message-ID"),
|
|
89351
|
+
inReplyTo: external_exports.string().optional(),
|
|
89352
|
+
references: external_exports.array(external_exports.string()).optional()
|
|
89040
89353
|
}
|
|
89041
89354
|
},
|
|
89042
|
-
withErrorHandling(
|
|
89043
|
-
|
|
89044
|
-
|
|
89045
|
-
|
|
89046
|
-
|
|
89047
|
-
|
|
89048
|
-
|
|
89049
|
-
|
|
89050
|
-
|
|
89051
|
-
|
|
89052
|
-
|
|
89053
|
-
|
|
89054
|
-
|
|
89355
|
+
withErrorHandling(
|
|
89356
|
+
async ({ to, subject, body, cc, bcc, account, attachments, inReplyTo, references }) => {
|
|
89357
|
+
if (threadingHeaders({ inReplyTo, references })) {
|
|
89358
|
+
return runThreadedDraft(threadedDraftDeps, {
|
|
89359
|
+
to,
|
|
89360
|
+
subject,
|
|
89361
|
+
body,
|
|
89362
|
+
cc,
|
|
89363
|
+
bcc,
|
|
89364
|
+
account,
|
|
89365
|
+
attachments,
|
|
89366
|
+
inReplyTo,
|
|
89367
|
+
references
|
|
89368
|
+
});
|
|
89369
|
+
}
|
|
89370
|
+
const success = mailManager.createDraft(to, subject, body, cc, bcc, account, attachments);
|
|
89371
|
+
if (!success) {
|
|
89372
|
+
return errorResponse("Failed to create draft. Check Mail.app configuration.");
|
|
89373
|
+
}
|
|
89374
|
+
const attachmentCount = attachments?.length ?? 0;
|
|
89375
|
+
const attachInfo = attachmentCount ? ` with ${attachmentCount} attachment(s)` : "";
|
|
89376
|
+
return successResponse(`Draft created for ${to.join(", ")}${attachInfo}`, {
|
|
89377
|
+
ok: true,
|
|
89378
|
+
recipients: to,
|
|
89379
|
+
attachmentCount,
|
|
89380
|
+
transport: "applescript"
|
|
89381
|
+
});
|
|
89382
|
+
},
|
|
89383
|
+
"Error creating draft"
|
|
89384
|
+
)
|
|
89055
89385
|
);
|
|
89386
|
+
var threadedDraftDeps = {
|
|
89387
|
+
resolveAccount: (account, smtpUser) => resolveDraftImapAccount(account, smtpUser),
|
|
89388
|
+
smtpIdentity: () => {
|
|
89389
|
+
if (!isSmtpConfigured()) return {};
|
|
89390
|
+
const user = process.env[SMTP_ENV.user]?.trim();
|
|
89391
|
+
return { user, from: process.env[SMTP_ENV.from]?.trim() || user };
|
|
89392
|
+
},
|
|
89393
|
+
compose: composeRawMime,
|
|
89394
|
+
append: (account, raw) => imapAppendDraft(account, raw)
|
|
89395
|
+
};
|
|
89056
89396
|
function resolveSmtpOrFallback() {
|
|
89057
89397
|
try {
|
|
89058
89398
|
return resolveSmtpConfig();
|
|
@@ -89091,13 +89431,14 @@ var COMPOSE_TRANSPORT_SCHEMA = external_exports.enum(["applescript", "smtp"]).op
|
|
|
89091
89431
|
registerTool(
|
|
89092
89432
|
"reply-to-message",
|
|
89093
89433
|
{
|
|
89094
|
-
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; over SMTP, also whether a Sent-folder copy was filed (sentCopy).\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.",
|
|
89434
|
+
description: "Use when: replying to an existing message by id, preserving its threading headers and quoting the original, optionally with attachments (same rules as send-email). 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, with the attachment count; over SMTP, also whether a Sent-folder copy was filed (sentCopy).\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.",
|
|
89095
89435
|
inputSchema: {
|
|
89096
89436
|
id: MESSAGE_ID_SCHEMA,
|
|
89097
89437
|
transport: COMPOSE_TRANSPORT_SCHEMA,
|
|
89098
89438
|
body: external_exports.string().min(1, "Reply body is required").describe("Reply body (plain text; HTML tags such as <br> are not rendered)"),
|
|
89099
89439
|
replyAll: external_exports.boolean().optional().default(false).describe("Reply to all recipients"),
|
|
89100
|
-
send: external_exports.boolean().optional().default(true).describe("Send immediately (false = save as draft)")
|
|
89440
|
+
send: external_exports.boolean().optional().default(true).describe("Send immediately (false = save as draft)"),
|
|
89441
|
+
attachments: ATTACHMENTS_SCHEMA
|
|
89101
89442
|
},
|
|
89102
89443
|
outputSchema: {
|
|
89103
89444
|
transport: external_exports.enum(["smtp", "applescript"]).optional(),
|
|
@@ -89105,6 +89446,7 @@ registerTool(
|
|
|
89105
89446
|
ok: external_exports.boolean().optional(),
|
|
89106
89447
|
sent: external_exports.boolean().optional(),
|
|
89107
89448
|
id: external_exports.string().optional(),
|
|
89449
|
+
attachmentCount: external_exports.number().optional(),
|
|
89108
89450
|
sentCopy: SENT_COPY_SCHEMA,
|
|
89109
89451
|
sentCopyError: SENT_COPY_ERROR_SCHEMA
|
|
89110
89452
|
}
|
|
@@ -89114,13 +89456,14 @@ registerTool(
|
|
|
89114
89456
|
registerTool(
|
|
89115
89457
|
"forward-message",
|
|
89116
89458
|
{
|
|
89117
|
-
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; over SMTP, also whether a Sent-folder copy was filed (sentCopy).\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.",
|
|
89459
|
+
description: "Use when: forwarding an existing message (by id) to new recipients (to is an array), with an optional body to prepend and optional extra attachments (same rules as send-email). Set send=false to save as a draft.\nReturns: a confirmation that the message was forwarded or saved as a draft, with the attachment count; over SMTP, also whether a Sent-folder copy was filed (sentCopy).\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.",
|
|
89118
89460
|
inputSchema: {
|
|
89119
89461
|
id: MESSAGE_ID_SCHEMA,
|
|
89120
89462
|
transport: COMPOSE_TRANSPORT_SCHEMA,
|
|
89121
89463
|
to: external_exports.array(external_exports.string()).min(1, "At least one recipient is required"),
|
|
89122
89464
|
body: external_exports.string().optional().describe("Optional message to prepend (plain text)"),
|
|
89123
|
-
send: external_exports.boolean().optional().default(true).describe("Send immediately (false = save as draft)")
|
|
89465
|
+
send: external_exports.boolean().optional().default(true).describe("Send immediately (false = save as draft)"),
|
|
89466
|
+
attachments: ATTACHMENTS_SCHEMA
|
|
89124
89467
|
},
|
|
89125
89468
|
outputSchema: {
|
|
89126
89469
|
transport: external_exports.enum(["smtp", "applescript"]).optional(),
|
|
@@ -89129,6 +89472,7 @@ registerTool(
|
|
|
89129
89472
|
sent: external_exports.boolean().optional(),
|
|
89130
89473
|
recipients: external_exports.array(external_exports.string()).optional(),
|
|
89131
89474
|
id: external_exports.string().optional(),
|
|
89475
|
+
attachmentCount: external_exports.number().optional(),
|
|
89132
89476
|
sentCopy: SENT_COPY_SCHEMA,
|
|
89133
89477
|
sentCopyError: SENT_COPY_ERROR_SCHEMA
|
|
89134
89478
|
}
|
|
@@ -89489,7 +89833,7 @@ registerTool(
|
|
|
89489
89833
|
registerTool(
|
|
89490
89834
|
"list-attachments",
|
|
89491
89835
|
{
|
|
89492
|
-
description: "Use when: enumerating a message's attachments (by id) to discover their names, MIME types, and sizes \u2014 typically before saving or fetching one.\nReturns: each attachment's name, MIME type, and size, plus a count.\nDo not use when: you want the bytes (use fetch-attachment for inline base64, or save-attachment to write to disk). Get the message id from search-messages or list-messages first.",
|
|
89836
|
+
description: "Use when: enumerating a message's attachments (by id) to discover their names, MIME types, and sizes \u2014 typically before saving or fetching one.\nReturns: each attachment's name, MIME type, and decoded size in bytes (what fetch-attachment returns), plus a count; sizeApproximate: true marks the rare IMAP size that is an estimate.\nDo not use when: you want the bytes (use fetch-attachment for inline base64, or save-attachment to write to disk). Get the message id from search-messages or list-messages first.",
|
|
89493
89837
|
inputSchema: {
|
|
89494
89838
|
id: MESSAGE_ID_SCHEMA
|
|
89495
89839
|
},
|
|
@@ -89509,8 +89853,9 @@ registerTool(
|
|
|
89509
89853
|
return successResponse("No attachments found", structured);
|
|
89510
89854
|
}
|
|
89511
89855
|
const attachmentList = attachments.map((a) => {
|
|
89512
|
-
const
|
|
89513
|
-
|
|
89856
|
+
const approx = "sizeApproximate" in a && a.sizeApproximate ? "~" : "";
|
|
89857
|
+
const size = a.size < 1024 ? `${a.size} bytes` : `${Math.round(a.size / 1024)} KB`;
|
|
89858
|
+
return ` - ${a.name} (${a.mimeType}, ${approx}${size})`;
|
|
89514
89859
|
}).join("\n");
|
|
89515
89860
|
return successResponse(
|
|
89516
89861
|
`Found ${attachments.length} attachment(s):
|