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.
Files changed (4) hide show
  1. package/README.md +109 -34
  2. package/build/cli.js +179 -27
  3. package/build/index.js +430 -85
  4. 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=(\d+)/i);
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 estimateBase64Size(base64Body) {
7289
- const cleaned = base64Body.replace(/[\s\r\n]/g, "");
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 estimateBase64Size(body);
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
- size: node.size ?? 0
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 attachments = collectAttachments(msg.bodyStructure).map((a) => ({
66693
- id: `${id}#${a.part}`,
66694
- name: a.filename,
66695
- mimeType: a.mimeType,
66696
- size: a.size
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
- if (match.size > MAX_IMAP_ATTACHMENT_BYTES) {
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 ${match.size} bytes; the maximum is ${MAX_IMAP_ATTACHMENT_BYTES} bytes (25 MiB).`
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 html = opts.htmlBody?.trim() ? opts.htmlBody : void 0;
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 script = this.findMessageScript(
84680
- id,
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
- return this.runComposeScript(script, "reply to");
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 script = this.findMessageScript(
84706
- id,
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
- return this.runComposeScript(script, "forward");
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 out = result.output.trim();
84957
- return /^\d+$/.test(out) ? out : null;
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" ? "Reply sent via SMTP" : `Message forwarded via SMTP to ${args.to.join(", ")}`,
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 and a chosen transport.\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), 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.",
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 ({ to, subject, body, cc, bcc, account, attachments, transport: transport2, replyTo }) => {
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
- if (shouldUseSmtp(transport2, account)) {
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("Account to create draft in"),
89034
- attachments: ATTACHMENTS_SCHEMA
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(({ to, subject, body, cc, bcc, account, attachments }) => {
89043
- const success = mailManager.createDraft(to, subject, body, cc, bcc, account, attachments);
89044
- if (!success) {
89045
- return errorResponse("Failed to create draft. Check Mail.app configuration.");
89046
- }
89047
- const attachmentCount = attachments?.length ?? 0;
89048
- const attachInfo = attachmentCount ? ` with ${attachmentCount} attachment(s)` : "";
89049
- return successResponse(`Draft created for ${to.join(", ")}${attachInfo}`, {
89050
- ok: true,
89051
- recipients: to,
89052
- attachmentCount
89053
- });
89054
- }, "Error creating draft")
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 sizeKb = Math.round(a.size / 1024);
89513
- return ` - ${a.name} (${a.mimeType}, ${sizeKb} KB)`;
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):