apple-mail-mcp 2.4.2 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -26,7 +26,8 @@ import { z } from "zod";
26
26
  import { AppleMailManager, isPathWithinAllowedRoots } from "./services/appleMailManager.js";
27
27
  import { writeFileSync } from "fs";
28
28
  import { resolve as resolvePath, join as joinPath } from "path";
29
- import { sendViaSmtp, shouldUseSmtp } from "./services/smtpMailer.js";
29
+ import { sendViaSmtp, sendSerialViaSmtp, shouldUseSmtp, isSmtpConfigured, resolveSmtpConfig, } from "./services/smtpMailer.js";
30
+ import { buildReplyOptions, buildForwardOptions, parseOriginalHeaders, } from "./services/replyForward.js";
30
31
  import { isImapAccount, resolveImapConfigs, dropAllPools, imapSearchMessages, imapListMessages, imapUnreadCount, imapListMailboxes, imapMailStats, imapListAttachments, imapFetchAttachment, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete, imapBatchMove, imapThread, imapCreateMailbox, imapDeleteMailbox, imapRenameMailbox, imapGetMessage, imapMarkRead, imapMarkUnread, imapFlagMessage, imapUnflagMessage, imapMoveMessageById, imapDeleteMessageById, } from "./services/imapClient.js";
31
32
  import { successResponse, errorResponse, partialCoverageBlock, withErrorHandling, messageSummary, } from "./tools/respond.js";
32
33
  import { routeMessage } from "./services/messageRouter.js";
@@ -504,8 +505,13 @@ server.registerTool("send-serial-email", {
504
505
  .passthrough())
505
506
  .optional(),
506
507
  },
507
- }, withErrorHandling(({ recipients, subject, body, account, delayMs }) => {
508
- const results = mailManager.sendSerialEmail(recipients, subject, body, account, delayMs);
508
+ }, withErrorHandling(async ({ recipients, subject, body, account, delayMs }) => {
509
+ // 2.5.0: prefer direct SMTP for mail-merge when configured (and not targeting
510
+ // a bare Mail.app account label); Mail.app fallback when not configured.
511
+ const smtpCfg = shouldUseSmtp(undefined, account) ? resolveSmtpOrFallback() : null;
512
+ const results = smtpCfg
513
+ ? await sendSerialViaSmtp(recipients, subject, body, smtpCfg, { delayMs })
514
+ : mailManager.sendSerialEmail(recipients, subject, body, account, delayMs);
509
515
  const successCount = results.filter((r) => r.success).length;
510
516
  const failCount = results.length - successCount;
511
517
  const details = results
@@ -557,6 +563,64 @@ server.registerTool("create-draft", {
557
563
  attachmentCount,
558
564
  });
559
565
  }, "Error creating draft"));
566
+ /** Resolve SMTP config, falling back (host/user set but no password) to Mail.app. */
567
+ function resolveSmtpOrFallback() {
568
+ try {
569
+ return resolveSmtpConfig();
570
+ }
571
+ catch {
572
+ return null;
573
+ }
574
+ }
575
+ /** Reply to a message over direct SMTP with RFC 5322 threading headers. */
576
+ async function sendReplyViaSmtp(id, body, replyAll) {
577
+ const cfg = resolveSmtpOrFallback();
578
+ if (!cfg)
579
+ return { sent: false, fallback: true };
580
+ const raw = mailManager.getRawSource(id);
581
+ if (!raw)
582
+ return { sent: false, fallback: true };
583
+ const original = parseOriginalHeaders(raw);
584
+ // Without a Message-ID we can't thread; let Mail.app's reply handle it.
585
+ if (!original.messageId || original.from.length === 0) {
586
+ return { sent: false, fallback: true };
587
+ }
588
+ const content = mailManager.getMessageContent(id);
589
+ const opts = buildReplyOptions({
590
+ original,
591
+ originalPlainText: content?.plainText ?? "",
592
+ body,
593
+ replyAll,
594
+ self: [cfg.from, cfg.user],
595
+ from: cfg.from,
596
+ });
597
+ const result = await sendViaSmtp(opts, cfg);
598
+ if (result.success)
599
+ return { sent: true };
600
+ return { sent: false, fallback: false, error: result.error ?? "unknown SMTP error" };
601
+ }
602
+ /** Forward a message over direct SMTP (clean MIME, new thread). */
603
+ async function sendForwardViaSmtp(id, to, body) {
604
+ const cfg = resolveSmtpOrFallback();
605
+ if (!cfg)
606
+ return { sent: false, fallback: true };
607
+ const raw = mailManager.getRawSource(id);
608
+ if (!raw)
609
+ return { sent: false, fallback: true };
610
+ const original = parseOriginalHeaders(raw);
611
+ const content = mailManager.getMessageContent(id);
612
+ const opts = buildForwardOptions({
613
+ original,
614
+ originalPlainText: content?.plainText ?? "",
615
+ to,
616
+ body,
617
+ from: cfg.from,
618
+ });
619
+ const result = await sendViaSmtp(opts, cfg);
620
+ if (result.success)
621
+ return { sent: true };
622
+ return { sent: false, fallback: false, error: result.error ?? "unknown SMTP error" };
623
+ }
560
624
  // --- reply-to-message ---
561
625
  server.registerTool("reply-to-message", {
562
626
  description: "Use when: replying to an existing message by id, preserving its threading headers. Set replyAll for all recipients; set send=false to save as a draft instead of sending.\nReturns: a confirmation that the reply was sent or saved as a draft.\nDo not use when: composing a brand-new message (use send-email / create-draft) or forwarding to new recipients (use forward-message).\nSafety: with the default send=true this SENDS real email immediately and cannot be unsent — require explicit user confirmation of the recipients and body, or pass send=false to let the user review.",
@@ -575,7 +639,19 @@ server.registerTool("reply-to-message", {
575
639
  sent: z.boolean().optional(),
576
640
  id: z.string().optional(),
577
641
  },
578
- }, withErrorHandling(({ id, body, replyAll, send }) => {
642
+ }, withErrorHandling(async ({ id, body, replyAll, send }) => {
643
+ // 2.5.0: prefer direct SMTP (clean, correctly threaded MIME) when configured
644
+ // and actually sending. Drafts (send=false) and the not-configured /
645
+ // unthreadable cases fall through to the Mail.app AppleScript path.
646
+ if (send && isSmtpConfigured()) {
647
+ const outcome = await sendReplyViaSmtp(id, body, replyAll);
648
+ if (outcome.sent) {
649
+ return successResponse("Reply sent", { ok: true, sent: true, id });
650
+ }
651
+ if (!outcome.fallback) {
652
+ return errorResponse(`Failed to reply to message "${id}" via SMTP: ${outcome.error}`);
653
+ }
654
+ }
579
655
  const success = mailManager.replyToMessage(id, body, replyAll, send);
580
656
  if (!success) {
581
657
  return errorResponse(`Failed to reply to message "${id}"`);
@@ -605,7 +681,22 @@ server.registerTool("forward-message", {
605
681
  recipients: z.array(z.string()).optional(),
606
682
  id: z.string().optional(),
607
683
  },
608
- }, withErrorHandling(({ id, to, body, send }) => {
684
+ }, withErrorHandling(async ({ id, to, body, send }) => {
685
+ // 2.5.0: prefer direct SMTP (clean MIME) when configured and actually sending.
686
+ if (send && isSmtpConfigured()) {
687
+ const outcome = await sendForwardViaSmtp(id, to, body);
688
+ if (outcome.sent) {
689
+ return successResponse(`Message forwarded to ${to.join(", ")}`, {
690
+ ok: true,
691
+ sent: true,
692
+ recipients: to,
693
+ id,
694
+ });
695
+ }
696
+ if (!outcome.fallback) {
697
+ return errorResponse(`Failed to forward message "${id}" via SMTP: ${outcome.error}`);
698
+ }
699
+ }
609
700
  const success = mailManager.forwardMessage(id, to, body, send);
610
701
  if (!success) {
611
702
  return errorResponse(`Failed to forward message "${id}"`);
@@ -1036,9 +1127,9 @@ server.registerTool("create-mailbox", {
1036
1127
  return errorResponse(r.error || `Failed to create mailbox "${name}"`);
1037
1128
  return successResponse(r.info || `Mailbox "${name}" created`, { ok: true, name });
1038
1129
  }
1039
- const success = mailManager.createMailbox(name, account);
1130
+ const { success, error } = mailManager.createMailbox(name, account);
1040
1131
  if (!success) {
1041
- return errorResponse(`Failed to create mailbox "${name}"`);
1132
+ return errorResponse(error || `Failed to create mailbox "${name}"`);
1042
1133
  }
1043
1134
  return successResponse(`Mailbox "${name}" created`, { ok: true, name });
1044
1135
  }, "Error creating mailbox"));
@@ -53,7 +53,7 @@ export declare function isPathWithinAllowedRoots(resolvedPath: string): boolean;
53
53
  *
54
54
  * Exported for unit testing.
55
55
  */
56
- export declare function describeMailboxOpError(op: "delete" | "rename", raw: string): string;
56
+ export declare function describeMailboxOpError(op: "create" | "delete" | "rename", raw: string): string;
57
57
  /** Env var to pin the default account (matched by account name or email). */
58
58
  export declare const DEFAULT_ACCOUNT_ENV = "APPLE_MAIL_MCP_DEFAULT_ACCOUNT";
59
59
  /**
@@ -153,6 +153,38 @@ export declare class AppleMailManager {
153
153
  * mailbox structure (create/delete/rename mailbox).
154
154
  */
155
155
  private invalidateCache;
156
+ /**
157
+ * Reads the live `enabled` flag for an account directly from Mail (bypassing
158
+ * the 60 s account cache) so a guard reflects an account that was enabled or
159
+ * disabled out-of-band. Returns true/false when known, or null when the probe
160
+ * is inconclusive — account not found, or the probe itself failed. Callers
161
+ * treat null as "can't tell, don't block".
162
+ */
163
+ private isAccountEnabled;
164
+ /**
165
+ * Guard for AppleScript-backed structural operations (create / delete / rename
166
+ * mailbox). When the target account is disabled in Mail, Mail holds no live
167
+ * server session for it, so the operation fails inside Mail with an opaque
168
+ * AppleEvent -10000 — and a multi-step op like rename can leave half-built
169
+ * state behind (an orphaned destination mailbox). Detect the disabled account
170
+ * up front and refuse with an actionable message instead of attempting the
171
+ * doomed op.
172
+ *
173
+ * Returns an error string when the account is known-disabled, else null —
174
+ * including when the state can't be determined. We fail open: an inconclusive
175
+ * probe never blocks an otherwise-valid operation.
176
+ *
177
+ * Applies only to the AppleScript backend. Direct-IMAP accounts talk to the
178
+ * server independent of Mail's enabled toggle and are routed before reaching
179
+ * the manager.
180
+ */
181
+ private disabledAccountGuard;
182
+ /**
183
+ * Best-effort rollback for a failed rename: delete a just-created destination
184
+ * mailbox, but ONLY if it is empty, so any messages that did move are never
185
+ * destroyed. Returns true if the empty orphan was removed.
186
+ */
187
+ private deleteMailboxIfEmpty;
156
188
  /**
157
189
  * Resolves the account to use for an operation when the caller omits one.
158
190
  *
@@ -479,7 +511,10 @@ export declare class AppleMailManager {
479
511
  /**
480
512
  * Create a new mailbox.
481
513
  */
482
- createMailbox(name: string, account?: string): boolean;
514
+ createMailbox(name: string, account?: string): {
515
+ success: boolean;
516
+ error?: string;
517
+ };
483
518
  /**
484
519
  * Delete a mailbox.
485
520
  */
@@ -1 +1 @@
1
- {"version":3,"file":"appleMailManager.d.ts","sourceRoot":"","sources":["../../src/services/appleMailManager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"appleMailManager.d.ts","sourceRoot":"","sources":["../../src/services/appleMailManager.ts"],"names":[],"mappings":"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"}
@@ -178,7 +178,7 @@ const UNSUPPORTED_APPLESCRIPT_OP = /AppleEvent handler failed|-10000/i;
178
178
  export function describeMailboxOpError(op, raw) {
179
179
  const trimmed = (raw || "").trim();
180
180
  if (UNSUPPORTED_APPLESCRIPT_OP.test(trimmed)) {
181
- const verb = op === "delete" ? "Delete" : "Rename";
181
+ const verb = op.charAt(0).toUpperCase() + op.slice(1);
182
182
  return `Mail.app cannot ${op} server-side (IMAP / Gmail / Workspace / iCloud / Exchange) mailboxes via AppleScript — only local "On My Mac" mailboxes support this. ${verb} it in Mail.app directly. (Mail.app error: ${trimmed})`;
183
183
  }
184
184
  return trimmed || `Failed to ${op} mailbox`;
@@ -523,6 +523,77 @@ export class AppleMailManager {
523
523
  this.cache.accounts = null;
524
524
  this.cache.mailboxNames.clear();
525
525
  }
526
+ /**
527
+ * Reads the live `enabled` flag for an account directly from Mail (bypassing
528
+ * the 60 s account cache) so a guard reflects an account that was enabled or
529
+ * disabled out-of-band. Returns true/false when known, or null when the probe
530
+ * is inconclusive — account not found, or the probe itself failed. Callers
531
+ * treat null as "can't tell, don't block".
532
+ */
533
+ isAccountEnabled(account) {
534
+ const safeAccount = escapeForAppleScript(account);
535
+ const result = executeAppleScript(buildAppLevelScript(`
536
+ try
537
+ return (enabled of account "${safeAccount}") as text
538
+ on error
539
+ return "missing"
540
+ end try
541
+ `));
542
+ if (!result.success)
543
+ return null;
544
+ const out = result.output.trim();
545
+ if (out === "true")
546
+ return true;
547
+ if (out === "false")
548
+ return false;
549
+ return null;
550
+ }
551
+ /**
552
+ * Guard for AppleScript-backed structural operations (create / delete / rename
553
+ * mailbox). When the target account is disabled in Mail, Mail holds no live
554
+ * server session for it, so the operation fails inside Mail with an opaque
555
+ * AppleEvent -10000 — and a multi-step op like rename can leave half-built
556
+ * state behind (an orphaned destination mailbox). Detect the disabled account
557
+ * up front and refuse with an actionable message instead of attempting the
558
+ * doomed op.
559
+ *
560
+ * Returns an error string when the account is known-disabled, else null —
561
+ * including when the state can't be determined. We fail open: an inconclusive
562
+ * probe never blocks an otherwise-valid operation.
563
+ *
564
+ * Applies only to the AppleScript backend. Direct-IMAP accounts talk to the
565
+ * server independent of Mail's enabled toggle and are routed before reaching
566
+ * the manager.
567
+ */
568
+ disabledAccountGuard(account) {
569
+ if (this.isAccountEnabled(account) === false) {
570
+ return `Account "${account}" is disabled in Mail, so Mail has no live connection to it — this operation would fail server-side (AppleEvent -10000) and could leave a mailbox half-changed. Enable the account (Mail ▸ Settings ▸ Accounts ▸ "Enable this account") and retry, or target an enabled account.`;
571
+ }
572
+ return null;
573
+ }
574
+ /**
575
+ * Best-effort rollback for a failed rename: delete a just-created destination
576
+ * mailbox, but ONLY if it is empty, so any messages that did move are never
577
+ * destroyed. Returns true if the empty orphan was removed.
578
+ */
579
+ deleteMailboxIfEmpty(name, account) {
580
+ const safeName = escapeForAppleScript(name);
581
+ const safeAccount = escapeForAppleScript(account);
582
+ const result = executeAppleScript(buildAppLevelScript(`
583
+ try
584
+ set mb to mailbox "${safeName}" of account "${safeAccount}"
585
+ if (count of messages of mb) is 0 then
586
+ delete mb
587
+ return "deleted"
588
+ else
589
+ return "kept"
590
+ end if
591
+ on error errMsg
592
+ return "error:" & errMsg
593
+ end try
594
+ `));
595
+ return result.success && result.output.trim() === "deleted";
596
+ }
526
597
  /**
527
598
  * Resolves the account to use for an operation when the caller omits one.
528
599
  *
@@ -2050,6 +2121,11 @@ export class AppleMailManager {
2050
2121
  */
2051
2122
  createMailbox(name, account) {
2052
2123
  const targetAccount = this.resolveAccount(account);
2124
+ const disabled = this.disabledAccountGuard(targetAccount);
2125
+ if (disabled) {
2126
+ console.error(`Refusing to create mailbox: ${disabled}`);
2127
+ return { success: false, error: disabled };
2128
+ }
2053
2129
  const safeName = escapeForAppleScript(name);
2054
2130
  const safeAccount = escapeForAppleScript(targetAccount);
2055
2131
  const script = buildAppLevelScript(`
@@ -2062,17 +2138,26 @@ export class AppleMailManager {
2062
2138
  `);
2063
2139
  const result = executeAppleScript(script);
2064
2140
  if (!result.success || result.output.startsWith("error:")) {
2065
- console.error(`Failed to create mailbox: ${result.error || result.output}`);
2066
- return false;
2141
+ const raw = result.success
2142
+ ? result.output.replace(/^error:/, "")
2143
+ : result.error || "Unknown error";
2144
+ const error = describeMailboxOpError("create", raw);
2145
+ console.error(`Failed to create mailbox: ${error}`);
2146
+ return { success: false, error };
2067
2147
  }
2068
2148
  this.invalidateCache();
2069
- return true;
2149
+ return { success: true };
2070
2150
  }
2071
2151
  /**
2072
2152
  * Delete a mailbox.
2073
2153
  */
2074
2154
  deleteMailbox(name, account) {
2075
2155
  const targetAccount = this.resolveAccount(account);
2156
+ const disabled = this.disabledAccountGuard(targetAccount);
2157
+ if (disabled) {
2158
+ console.error(`Refusing to delete mailbox: ${disabled}`);
2159
+ return { success: false, error: disabled };
2160
+ }
2076
2161
  const targetMailbox = this.resolveMailbox(name, targetAccount);
2077
2162
  const safeName = escapeForAppleScript(targetMailbox);
2078
2163
  const safeAccount = escapeForAppleScript(targetAccount);
@@ -2101,11 +2186,15 @@ export class AppleMailManager {
2101
2186
  */
2102
2187
  renameMailbox(oldName, newName, account) {
2103
2188
  const targetAccount = this.resolveAccount(account);
2104
- // Create the new mailbox
2105
- if (!this.createMailbox(newName, targetAccount)) {
2189
+ // Create the new mailbox. createMailbox runs the disabled-account guard, so
2190
+ // a disabled target is refused here before anything is built — no orphan can
2191
+ // be created. Propagate its (more specific) error rather than a generic one.
2192
+ const created = this.createMailbox(newName, targetAccount);
2193
+ if (!created.success) {
2106
2194
  return {
2107
2195
  success: false,
2108
- error: `Could not create the destination mailbox "${newName}" needed for the rename.`,
2196
+ error: created.error ??
2197
+ `Could not create the destination mailbox "${newName}" needed for the rename.`,
2109
2198
  };
2110
2199
  }
2111
2200
  // Move all messages from old to new
@@ -2155,9 +2244,16 @@ export class AppleMailManager {
2155
2244
  const raw = result.success
2156
2245
  ? result.output.replace(/^error:/, "")
2157
2246
  : result.error || "Unknown error";
2158
- // The empty destination mailbox we just created is now an orphan; the
2159
- // source is untouched (delete only runs after a verified-empty move).
2160
- const error = describeMailboxOpError("rename", raw);
2247
+ // Roll back the destination we just created so a failed rename doesn't
2248
+ // leave an orphan (as a partial-failure once did the _amcp_rename_test_*
2249
+ // ghosts). deleteMailboxIfEmpty only removes it when empty, so any
2250
+ // messages that did move are never destroyed; the source is untouched
2251
+ // (its delete only runs after a verified-empty move).
2252
+ const rolledBack = this.deleteMailboxIfEmpty(resolvedNew, targetAccount);
2253
+ let error = describeMailboxOpError("rename", raw);
2254
+ error += rolledBack
2255
+ ? ` The empty destination mailbox "${resolvedNew}" was rolled back, so no orphan was left.`
2256
+ : ` The destination mailbox "${resolvedNew}" was created and could not be auto-removed; delete it manually if it is an empty leftover.`;
2161
2257
  console.error(`Failed to rename mailbox: ${error}`);
2162
2258
  this.invalidateCache();
2163
2259
  return { success: false, error };
@@ -851,7 +851,7 @@ async function imapBatch(ids, deps, op) {
851
851
  errors.push(`Not an IMAP id: "${id}"`);
852
852
  continue;
853
853
  }
854
- const key = `${ref.account}${ref.path}`;
854
+ const key = `${ref.account}\0${ref.path}`;
855
855
  const g = groups.get(key) ?? { account: ref.account, path: ref.path, uids: [] };
856
856
  g.uids.push(ref.uid);
857
857
  groups.set(key, g);
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Pure helpers that turn an existing message (its raw RFC 5322 source plus the
3
+ * decoded plain-text body) into {@link SmtpSendOptions} for a **threaded reply**
4
+ * or a **forward** sent over SMTP.
5
+ *
6
+ * This is the 2.5.0 "prefer-direct" path: when SMTP is configured we send
7
+ * replies/forwards ourselves with correct `In-Reply-To`/`References` headers and
8
+ * a clean MIME body, instead of driving Mail.app's `reply`/`forward` AppleScript
9
+ * commands (which thread correctly but wrap the injected body in a `blockquote`
10
+ * on macOS 15+). Kept separate from {@link sendViaSmtp} so the addressing,
11
+ * subject-prefix, and quoting rules are unit-testable without a live SMTP server.
12
+ */
13
+ import type { SmtpSendOptions } from "../services/smtpMailer.js";
14
+ /** The subset of an original message's headers we need to reply/forward. */
15
+ export interface OriginalHeaders {
16
+ /** Raw `Message-ID` including angle brackets, e.g. `<abc@host>`. */
17
+ messageId?: string;
18
+ /** Thread chain: every `<id>` from `References` + `In-Reply-To`, in order. */
19
+ references: string[];
20
+ /** `From` address(es), bare (no display name). */
21
+ from: string[];
22
+ /** `Reply-To` address(es), bare. Preferred over {@link from} for replies. */
23
+ replyTo: string[];
24
+ /** `To` address(es), bare. */
25
+ to: string[];
26
+ /** `Cc` address(es), bare. */
27
+ cc: string[];
28
+ /** `Subject`, unfolded and trimmed (no `Re:`/`Fwd:` normalization). */
29
+ subject: string;
30
+ /** `Date` header verbatim, for the reply attribution line. */
31
+ date?: string;
32
+ }
33
+ /**
34
+ * Pull bare email addresses out of a header value such as
35
+ * `"Alice <a@x.com>, bob@y.com"` → `["a@x.com", "bob@y.com"]`. Splits on commas
36
+ * (address lists are comma-separated) and unwraps `<...>` when present. Anything
37
+ * without an `@` is dropped, so group syntax / junk is ignored.
38
+ */
39
+ export declare function extractAddresses(headerValue: string): string[];
40
+ /**
41
+ * Parse the header block (everything before the first blank line) of a raw
42
+ * message. Continuation lines (folded headers, leading WSP) are unfolded, and
43
+ * header names are matched case-insensitively. Only the headers in
44
+ * {@link OriginalHeaders} are extracted.
45
+ */
46
+ export declare function parseOriginalHeaders(raw: string): OriginalHeaders;
47
+ /**
48
+ * Ensure `subject` carries `prefix` exactly once. Existing `Re:`/`Fwd:`/`Fw:`
49
+ * prefixes (any case) are treated as already-present so we never stack them.
50
+ */
51
+ export declare function withSubjectPrefix(subject: string, prefix: "Re:" | "Fwd:"): string;
52
+ /** Prefix every line of the original body with `> ` for the quoted reply block. */
53
+ export declare function quoteBody(plainText: string): string;
54
+ /**
55
+ * Build {@link SmtpSendOptions} for a threaded reply.
56
+ *
57
+ * - recipients: `Reply-To` if the original had one, else `From`; with
58
+ * `replyAll`, the original `To`+`Cc` (minus our own addresses and the primary
59
+ * recipients) become `Cc`.
60
+ * - subject: `Re: ` prepended unless already present.
61
+ * - threading: `In-Reply-To` = original `Message-ID`; `References` = original
62
+ * chain + that `Message-ID`.
63
+ * - body: the new text, then an attribution line and the quoted original.
64
+ */
65
+ export declare function buildReplyOptions(args: {
66
+ original: OriginalHeaders;
67
+ originalPlainText: string;
68
+ body: string;
69
+ replyAll: boolean;
70
+ /** Our own addresses, excluded from a reply-all recipient set. */
71
+ self: string[];
72
+ /** From override (defaults to the SMTP identity at send time when omitted). */
73
+ from?: string;
74
+ }): SmtpSendOptions;
75
+ /**
76
+ * Build {@link SmtpSendOptions} for a forward to new recipients. A forward
77
+ * starts a new thread, so no `In-Reply-To`/`References` are set — the win over
78
+ * the AppleScript path is a clean, un-wrapped MIME body.
79
+ */
80
+ export declare function buildForwardOptions(args: {
81
+ original: OriginalHeaders;
82
+ originalPlainText: string;
83
+ to: string[];
84
+ body?: string;
85
+ from?: string;
86
+ }): SmtpSendOptions;
87
+ //# sourceMappingURL=replyForward.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replyForward.d.ts","sourceRoot":"","sources":["../../src/services/replyForward.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAEhE,4EAA4E;AAC5E,MAAM,WAAW,eAAe;IAC9B,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,kDAAkD;IAClD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,6EAA6E;IAC7E,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,8BAA8B;IAC9B,EAAE,EAAE,MAAM,EAAE,CAAC;IACb,8BAA8B;IAC9B,EAAE,EAAE,MAAM,EAAE,CAAC;IACb,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAS9D;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAkCjE;AAgBD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,CAIjF;AAED,mFAAmF;AACnF,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAMnD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC,QAAQ,EAAE,eAAe,CAAC;IAC1B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,kEAAkE;IAClE,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,+EAA+E;IAC/E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,eAAe,CAmClB;AAQD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,QAAQ,EAAE,eAAe,CAAC;IAC1B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,EAAE,EAAE,MAAM,EAAE,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,eAAe,CAsBlB"}
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Pull bare email addresses out of a header value such as
3
+ * `"Alice <a@x.com>, bob@y.com"` → `["a@x.com", "bob@y.com"]`. Splits on commas
4
+ * (address lists are comma-separated) and unwraps `<...>` when present. Anything
5
+ * without an `@` is dropped, so group syntax / junk is ignored.
6
+ */
7
+ export function extractAddresses(headerValue) {
8
+ if (!headerValue.trim())
9
+ return [];
10
+ return headerValue
11
+ .split(",")
12
+ .map((part) => {
13
+ const angle = part.match(/<([^>]+)>/);
14
+ return (angle ? angle[1] : part).trim();
15
+ })
16
+ .filter((addr) => addr.includes("@"));
17
+ }
18
+ /**
19
+ * Parse the header block (everything before the first blank line) of a raw
20
+ * message. Continuation lines (folded headers, leading WSP) are unfolded, and
21
+ * header names are matched case-insensitively. Only the headers in
22
+ * {@link OriginalHeaders} are extracted.
23
+ */
24
+ export function parseOriginalHeaders(raw) {
25
+ const headerBlock = raw.split(/\r?\n\r?\n/)[0] ?? "";
26
+ // Unfold: any line starting with a space/tab continues the previous header.
27
+ const lines = [];
28
+ for (const line of headerBlock.split(/\r?\n/)) {
29
+ if (/^[ \t]/.test(line) && lines.length > 0) {
30
+ lines[lines.length - 1] += " " + line.trim();
31
+ }
32
+ else {
33
+ lines.push(line);
34
+ }
35
+ }
36
+ const get = (name) => {
37
+ const prefix = `${name.toLowerCase()}:`;
38
+ const found = lines.find((l) => l.toLowerCase().startsWith(prefix));
39
+ return found ? found.slice(found.indexOf(":") + 1).trim() : undefined;
40
+ };
41
+ const rawMessageId = get("Message-ID");
42
+ const messageId = rawMessageId?.match(/<[^>]+>/)?.[0] ?? rawMessageId ?? undefined;
43
+ const references = `${get("References") ?? ""} ${get("In-Reply-To") ?? ""}`.match(/<[^>]+>/g) ?? [];
44
+ return {
45
+ messageId,
46
+ references,
47
+ from: extractAddresses(get("From") ?? ""),
48
+ replyTo: extractAddresses(get("Reply-To") ?? ""),
49
+ to: extractAddresses(get("To") ?? ""),
50
+ cc: extractAddresses(get("Cc") ?? ""),
51
+ subject: get("Subject") ?? "",
52
+ date: get("Date"),
53
+ };
54
+ }
55
+ /** Case-insensitive de-dupe that preserves first-seen order and casing. */
56
+ function dedupe(addrs) {
57
+ const seen = new Set();
58
+ const out = [];
59
+ for (const a of addrs) {
60
+ const k = a.toLowerCase();
61
+ if (!seen.has(k)) {
62
+ seen.add(k);
63
+ out.push(a);
64
+ }
65
+ }
66
+ return out;
67
+ }
68
+ /**
69
+ * Ensure `subject` carries `prefix` exactly once. Existing `Re:`/`Fwd:`/`Fw:`
70
+ * prefixes (any case) are treated as already-present so we never stack them.
71
+ */
72
+ export function withSubjectPrefix(subject, prefix) {
73
+ const s = subject.trim();
74
+ const present = prefix === "Re:" ? /^re:/i : /^(fwd?|fw):/i;
75
+ return present.test(s) ? s : `${prefix} ${s}`;
76
+ }
77
+ /** Prefix every line of the original body with `> ` for the quoted reply block. */
78
+ export function quoteBody(plainText) {
79
+ return plainText
80
+ .replace(/\s+$/, "")
81
+ .split(/\r?\n/)
82
+ .map((l) => (l ? `> ${l}` : ">"))
83
+ .join("\n");
84
+ }
85
+ /**
86
+ * Build {@link SmtpSendOptions} for a threaded reply.
87
+ *
88
+ * - recipients: `Reply-To` if the original had one, else `From`; with
89
+ * `replyAll`, the original `To`+`Cc` (minus our own addresses and the primary
90
+ * recipients) become `Cc`.
91
+ * - subject: `Re: ` prepended unless already present.
92
+ * - threading: `In-Reply-To` = original `Message-ID`; `References` = original
93
+ * chain + that `Message-ID`.
94
+ * - body: the new text, then an attribution line and the quoted original.
95
+ */
96
+ export function buildReplyOptions(args) {
97
+ const { original, originalPlainText, body, replyAll, self, from } = args;
98
+ const selfSet = new Set(self.filter(Boolean).map((s) => s.toLowerCase()));
99
+ const to = dedupe((original.replyTo.length ? original.replyTo : original.from).filter(Boolean));
100
+ const toSet = new Set(to.map((t) => t.toLowerCase()));
101
+ let cc;
102
+ if (replyAll) {
103
+ const extra = dedupe([...original.to, ...original.cc].filter((a) => !selfSet.has(a.toLowerCase()) && !toSet.has(a.toLowerCase())));
104
+ cc = extra.length ? extra : undefined;
105
+ }
106
+ const attribution = buildAttribution(original);
107
+ const quoted = originalPlainText.trim()
108
+ ? `\n\n${attribution}${quoteBody(originalPlainText)}`
109
+ : "";
110
+ const references = dedupe(original.messageId ? [...original.references, original.messageId] : original.references);
111
+ return {
112
+ to,
113
+ cc,
114
+ subject: withSubjectPrefix(original.subject, "Re:"),
115
+ body: `${body}${quoted}`,
116
+ inReplyTo: original.messageId,
117
+ references: references.length ? references : undefined,
118
+ from,
119
+ };
120
+ }
121
+ /** "On <date>, <sender> wrote:\n" — omits the date clause when unknown. */
122
+ function buildAttribution(original) {
123
+ const who = original.from[0] ?? original.replyTo[0] ?? "the sender";
124
+ return original.date ? `On ${original.date}, ${who} wrote:\n` : `${who} wrote:\n`;
125
+ }
126
+ /**
127
+ * Build {@link SmtpSendOptions} for a forward to new recipients. A forward
128
+ * starts a new thread, so no `In-Reply-To`/`References` are set — the win over
129
+ * the AppleScript path is a clean, un-wrapped MIME body.
130
+ */
131
+ export function buildForwardOptions(args) {
132
+ const { original, originalPlainText, to, body, from } = args;
133
+ const headerBlock = [
134
+ "---------- Forwarded message ----------",
135
+ original.from.length ? `From: ${original.from.join(", ")}` : "",
136
+ original.date ? `Date: ${original.date}` : "",
137
+ `Subject: ${original.subject}`,
138
+ original.to.length ? `To: ${original.to.join(", ")}` : "",
139
+ original.cc.length ? `Cc: ${original.cc.join(", ")}` : "",
140
+ ]
141
+ .filter(Boolean)
142
+ .join("\n");
143
+ const prefix = body?.trim() ? `${body}\n\n` : "";
144
+ return {
145
+ to: dedupe(to),
146
+ subject: withSubjectPrefix(original.subject, "Fwd:"),
147
+ body: `${prefix}${headerBlock}\n\n${originalPlainText}`,
148
+ from,
149
+ };
150
+ }
@@ -36,6 +36,17 @@ export interface SmtpSendOptions {
36
36
  * plain-text clients still get a clean fallback.
37
37
  */
38
38
  htmlBody?: string;
39
+ /**
40
+ * RFC 5322 threading (2.5.0): the message this is replying to — emitted as the
41
+ * `In-Reply-To` header so SMTP replies/forwards thread correctly in Gmail and
42
+ * other clients. Pass the original message's `Message-ID`.
43
+ */
44
+ inReplyTo?: string;
45
+ /**
46
+ * RFC 5322 threading (2.5.0): the `References` chain — the original message's
47
+ * existing `References` plus its `Message-ID`. nodemailer accepts an array.
48
+ */
49
+ references?: string[];
39
50
  }
40
51
  /** Resolved SMTP connection configuration. */
41
52
  export interface SmtpConfig {
@@ -114,4 +125,36 @@ export declare function resolveSmtpConfig(env?: NodeJS.ProcessEnv): SmtpConfig;
114
125
  * a transporter factory only in tests.
115
126
  */
116
127
  export declare function sendViaSmtp(opts: SmtpSendOptions, config?: SmtpConfig, createTransport?: typeof nodemailer.createTransport): Promise<SmtpSendResult>;
128
+ /** One recipient of a mail-merge batch (mirrors the AppleScript serial path). */
129
+ export interface SerialSmtpRecipient {
130
+ email: string;
131
+ variables: Record<string, string>;
132
+ }
133
+ /** Per-recipient outcome of a serial SMTP send. */
134
+ export interface SerialSmtpResult {
135
+ email: string;
136
+ success: boolean;
137
+ error?: string;
138
+ }
139
+ /**
140
+ * Replace every `{{Key}}` token in `template` with the matching value from
141
+ * `variables`. Keys are escaped so regex metacharacters in a key are literal.
142
+ * Mirrors the substitution in {@link AppleMailManager.sendSerialEmail} so the
143
+ * two transports personalize identically.
144
+ */
145
+ export declare function applyPlaceholders(template: string, variables: Record<string, string>): string;
146
+ /**
147
+ * Send a personalized mail-merge batch over SMTP — one individual message per
148
+ * recipient (recipients never see each other), with `{{Key}}` placeholders in
149
+ * the subject/body replaced per recipient. Returns a per-recipient result list;
150
+ * a single recipient's failure does not abort the batch.
151
+ *
152
+ * `opts.send` and `opts.sleep` are injectable for tests (no real SMTP / no real
153
+ * delay). The default `sleep` waits `delayMs` (clamped 0–10000) between sends.
154
+ */
155
+ export declare function sendSerialViaSmtp(recipients: SerialSmtpRecipient[], subject: string, body: string, config: SmtpConfig, opts?: {
156
+ delayMs?: number;
157
+ send?: typeof sendViaSmtp;
158
+ sleep?: (ms: number) => Promise<void>;
159
+ }): Promise<SerialSmtpResult[]>;
117
160
  //# sourceMappingURL=smtpMailer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"smtpMailer.d.ts","sourceRoot":"","sources":["../../src/services/smtpMailer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,UAAU,MAAM,YAAY,CAAC;AAIpC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,8EAA8E;AAC9E,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,EAAE,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,kFAAkF;IAClF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,8CAA8C;AAC9C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,8BAA8B;AAC9B,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;CASX,CAAC;AAEX;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAE9E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS,EAC7C,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,UAAU,GAAE,OAA4B,GACvC,OAAO,CAMT;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAcpF;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,UAAU,CA8ClF;AAqBD;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,eAAe,EACrB,MAAM,CAAC,EAAE,UAAU,EACnB,eAAe,GAAE,OAAO,UAAU,CAAC,eAA4C,GAC9E,OAAO,CAAC,cAAc,CAAC,CA6CzB"}
1
+ {"version":3,"file":"smtpMailer.d.ts","sourceRoot":"","sources":["../../src/services/smtpMailer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,UAAU,MAAM,YAAY,CAAC;AAIpC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,8EAA8E;AAC9E,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,EAAE,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,kFAAkF;IAClF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,8CAA8C;AAC9C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,8BAA8B;AAC9B,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;CASX,CAAC;AAEX;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAE9E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS,EAC7C,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,UAAU,GAAE,OAA4B,GACvC,OAAO,CAMT;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAcpF;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,UAAU,CA8ClF;AAqBD;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,eAAe,EACrB,MAAM,CAAC,EAAE,UAAU,EACnB,eAAe,GAAE,OAAO,UAAU,CAAC,eAA4C,GAC9E,OAAO,CAAC,cAAc,CAAC,CAgDzB;AAED,iFAAiF;AACjF,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACnC;AAED,mDAAmD;AACnD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAO7F;AAED;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,UAAU,EAAE,mBAAmB,EAAE,EACjC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,UAAU,EAClB,IAAI,GAAE;IACJ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,WAAW,CAAC;IAC1B,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC,GACL,OAAO,CAAC,gBAAgB,EAAE,CAAC,CA+B7B"}
@@ -198,6 +198,9 @@ export async function sendViaSmtp(opts, config, createTransport = nodemailer.cre
198
198
  // When present, nodemailer emits multipart/alternative (text + html).
199
199
  html,
200
200
  attachments,
201
+ // RFC 5322 threading for SMTP replies/forwards (2.5.0).
202
+ inReplyTo: opts.inReplyTo?.trim() || undefined,
203
+ references: opts.references?.length ? opts.references : undefined,
201
204
  });
202
205
  return { success: true, messageId: info.messageId };
203
206
  }
@@ -211,3 +214,55 @@ export async function sendViaSmtp(opts, config, createTransport = nodemailer.cre
211
214
  transporter.close();
212
215
  }
213
216
  }
217
+ /**
218
+ * Replace every `{{Key}}` token in `template` with the matching value from
219
+ * `variables`. Keys are escaped so regex metacharacters in a key are literal.
220
+ * Mirrors the substitution in {@link AppleMailManager.sendSerialEmail} so the
221
+ * two transports personalize identically.
222
+ */
223
+ export function applyPlaceholders(template, variables) {
224
+ let out = template;
225
+ for (const [key, value] of Object.entries(variables)) {
226
+ const safeKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
227
+ out = out.replace(new RegExp(`\\{\\{${safeKey}\\}\\}`, "g"), value);
228
+ }
229
+ return out;
230
+ }
231
+ /**
232
+ * Send a personalized mail-merge batch over SMTP — one individual message per
233
+ * recipient (recipients never see each other), with `{{Key}}` placeholders in
234
+ * the subject/body replaced per recipient. Returns a per-recipient result list;
235
+ * a single recipient's failure does not abort the batch.
236
+ *
237
+ * `opts.send` and `opts.sleep` are injectable for tests (no real SMTP / no real
238
+ * delay). The default `sleep` waits `delayMs` (clamped 0–10000) between sends.
239
+ */
240
+ export async function sendSerialViaSmtp(recipients, subject, body, config, opts = {}) {
241
+ const send = opts.send ?? sendViaSmtp;
242
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
243
+ const delay = Math.min(Math.max(opts.delayMs ?? 500, 0), 10000);
244
+ const results = [];
245
+ for (let i = 0; i < recipients.length; i++) {
246
+ const r = recipients[i];
247
+ try {
248
+ const res = await send({
249
+ to: [r.email],
250
+ subject: applyPlaceholders(subject, r.variables),
251
+ body: applyPlaceholders(body, r.variables),
252
+ from: config.from,
253
+ }, config);
254
+ results.push({ email: r.email, success: res.success, error: res.error });
255
+ }
256
+ catch (error) {
257
+ results.push({
258
+ email: r.email,
259
+ success: false,
260
+ error: error instanceof Error ? error.message : String(error),
261
+ });
262
+ }
263
+ if (delay > 0 && i < recipients.length - 1) {
264
+ await sleep(delay);
265
+ }
266
+ }
267
+ return results;
268
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.4.2",
3
+ "version": "2.5.0",
4
4
  "description": "MCP server for Apple Mail - read, search, send, and manage emails via Claude and other AI assistants",
5
5
  "type": "module",
6
6
  "main": "build/index.js",