apple-mail-mcp 2.8.2 → 2.8.3

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 (65) hide show
  1. package/README.md +5 -5
  2. package/build/cli.js +12083 -138
  3. package/build/index.js +82684 -1687
  4. package/package.json +3 -4
  5. package/build/cli.d.ts +0 -24
  6. package/build/cli.d.ts.map +0 -1
  7. package/build/index.d.ts +0 -23
  8. package/build/index.d.ts.map +0 -1
  9. package/build/services/appleMailManager.d.ts +0 -680
  10. package/build/services/appleMailManager.d.ts.map +0 -1
  11. package/build/services/appleMailManager.js +0 -3143
  12. package/build/services/fileConfig.d.ts +0 -7
  13. package/build/services/fileConfig.d.ts.map +0 -1
  14. package/build/services/fileConfig.js +0 -52
  15. package/build/services/imapClient.d.ts +0 -312
  16. package/build/services/imapClient.d.ts.map +0 -1
  17. package/build/services/imapClient.js +0 -1023
  18. package/build/services/imapIdle.d.ts +0 -58
  19. package/build/services/imapIdle.d.ts.map +0 -1
  20. package/build/services/imapIdle.js +0 -151
  21. package/build/services/imapMultiAccount.d.ts +0 -124
  22. package/build/services/imapMultiAccount.d.ts.map +0 -1
  23. package/build/services/imapMultiAccount.js +0 -253
  24. package/build/services/messageRouter.d.ts +0 -24
  25. package/build/services/messageRouter.d.ts.map +0 -1
  26. package/build/services/messageRouter.js +0 -31
  27. package/build/services/replyForward.d.ts +0 -87
  28. package/build/services/replyForward.d.ts.map +0 -1
  29. package/build/services/replyForward.js +0 -150
  30. package/build/services/smtpMailer.d.ts +0 -160
  31. package/build/services/smtpMailer.d.ts.map +0 -1
  32. package/build/services/smtpMailer.js +0 -268
  33. package/build/services/templateStore.d.ts +0 -18
  34. package/build/services/templateStore.d.ts.map +0 -1
  35. package/build/services/templateStore.js +0 -91
  36. package/build/tools/doctor.d.ts +0 -23
  37. package/build/tools/doctor.d.ts.map +0 -1
  38. package/build/tools/doctor.js +0 -74
  39. package/build/tools/resourcesAndPrompts.d.ts +0 -14
  40. package/build/tools/resourcesAndPrompts.d.ts.map +0 -1
  41. package/build/tools/resourcesAndPrompts.js +0 -109
  42. package/build/tools/respond.d.ts +0 -48
  43. package/build/tools/respond.d.ts.map +0 -1
  44. package/build/tools/respond.js +0 -95
  45. package/build/tools/thread.d.ts +0 -19
  46. package/build/tools/thread.d.ts.map +0 -1
  47. package/build/tools/thread.js +0 -32
  48. package/build/types.d.ts +0 -434
  49. package/build/types.d.ts.map +0 -1
  50. package/build/types.js +0 -13
  51. package/build/utils/applescript.d.ts +0 -45
  52. package/build/utils/applescript.d.ts.map +0 -1
  53. package/build/utils/applescript.js +0 -446
  54. package/build/utils/attachmentMaterialize.d.ts +0 -9
  55. package/build/utils/attachmentMaterialize.d.ts.map +0 -1
  56. package/build/utils/attachmentMaterialize.js +0 -38
  57. package/build/utils/mimeParse.d.ts +0 -62
  58. package/build/utils/mimeParse.d.ts.map +0 -1
  59. package/build/utils/mimeParse.js +0 -317
  60. package/build/utils/orphan.d.ts +0 -25
  61. package/build/utils/orphan.d.ts.map +0 -1
  62. package/build/utils/orphan.js +0 -26
  63. package/build/utils/serialize.d.ts +0 -30
  64. package/build/utils/serialize.d.ts.map +0 -1
  65. package/build/utils/serialize.js +0 -41
@@ -1,87 +0,0 @@
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
@@ -1 +0,0 @@
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"}
@@ -1,150 +0,0 @@
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
- }
@@ -1,160 +0,0 @@
1
- /**
2
- * SMTP transport for sending mail (issue #12).
3
- *
4
- * Mail.app's AppleScript send path wraps any injected body in
5
- * `<blockquote type="cite">` under the Apple-Mail-URLShareWrapperClass template
6
- * on macOS 15+, so messages render to recipients as quoted/forwarded content
7
- * (Apple radar FB11734014, open since Ventura). This module bypasses Mail.app
8
- * entirely and submits clean MIME directly over SMTP via nodemailer.
9
- *
10
- * Connection settings come from environment variables; the password is read
11
- * from the macOS Keychain via the `security` CLI by default so no secret is
12
- * ever placed in config. When SMTP is configured, `send-email` auto-prefers it
13
- * over AppleScript (opt out per call with `transport: "applescript"`); see
14
- * {@link shouldUseSmtp}.
15
- *
16
- * @module services/smtpMailer
17
- */
18
- import nodemailer from "nodemailer";
19
- import type { AttachmentInput } from "../types.js";
20
- /** Options for an SMTP send, mirroring the AppleScript send-email surface. */
21
- export interface SmtpSendOptions {
22
- to: string[];
23
- subject: string;
24
- /** Plain-text body. Always sent as the text/plain part. */
25
- body: string;
26
- cc?: string[];
27
- bcc?: string[];
28
- /** Overrides the configured From address (must be allowed by the SMTP server). */
29
- from?: string;
30
- /** Files to attach: absolute paths and/or inline base64 content (B4). */
31
- attachments?: AttachmentInput[];
32
- /**
33
- * Optional HTML body. When provided, the message is sent as
34
- * multipart/alternative ({@link SmtpSendOptions.body} as the text/plain part,
35
- * this as the text/html part) so clients pick the richer rendering while
36
- * plain-text clients still get a clean fallback.
37
- */
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[];
50
- }
51
- /** Resolved SMTP connection configuration. */
52
- export interface SmtpConfig {
53
- host: string;
54
- port: number;
55
- secure: boolean;
56
- user: string;
57
- pass: string;
58
- from: string;
59
- }
60
- /** Result of an SMTP send. */
61
- export interface SmtpSendResult {
62
- success: boolean;
63
- messageId?: string;
64
- error?: string;
65
- }
66
- /**
67
- * Environment variables consumed by {@link resolveSmtpConfig}. Documented here
68
- * (and in the README) so the error path can point users at exactly what to set.
69
- */
70
- export declare const SMTP_ENV: {
71
- readonly host: "APPLE_MAIL_MCP_SMTP_HOST";
72
- readonly port: "APPLE_MAIL_MCP_SMTP_PORT";
73
- readonly secure: "APPLE_MAIL_MCP_SMTP_SECURE";
74
- readonly user: "APPLE_MAIL_MCP_SMTP_USER";
75
- readonly from: "APPLE_MAIL_MCP_SMTP_FROM";
76
- readonly password: "APPLE_MAIL_MCP_SMTP_PASSWORD";
77
- readonly keychainService: "APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE";
78
- readonly keychainAccount: "APPLE_MAIL_MCP_SMTP_KEYCHAIN_ACCOUNT";
79
- };
80
- /**
81
- * Cheap check for whether the SMTP transport is configured at all, i.e. the two
82
- * required settings ({@link SMTP_ENV.host} and {@link SMTP_ENV.user}) are
83
- * present. Used to auto-prefer SMTP over AppleScript when no transport is
84
- * explicitly requested, and by `doctor`. Does NOT touch the Keychain or verify
85
- * the password — a misconfigured password still surfaces a clear error at send
86
- * time via {@link resolveSmtpConfig}.
87
- */
88
- export declare function isSmtpConfigured(env?: NodeJS.ProcessEnv): boolean;
89
- /**
90
- * Decides whether `send-email` should use the SMTP transport for this call.
91
- *
92
- * - explicit `"smtp"` → always SMTP (config errors surface, no fallback);
93
- * - explicit `"applescript"` → always the Mail.app path;
94
- * - omitted → SMTP when configured, **except** when a non-email `account` label
95
- * (a Mail.app account name such as `"Work"`) is supplied. That requests
96
- * account-based sending, which only the AppleScript path can do, so we honor
97
- * the caller's intent instead of silently switching their account. An
98
- * `account` that is an email address is treated as a From override and does
99
- * not block SMTP.
100
- */
101
- export declare function shouldUseSmtp(transport: "applescript" | "smtp" | undefined, account: string | undefined, configured?: boolean): boolean;
102
- /**
103
- * Reads a password from the macOS login Keychain via the `security` CLI.
104
- *
105
- * Tries `find-internet-password` first (where Mail.app stores account
106
- * passwords) and falls back to `find-generic-password`. Returns null if no
107
- * matching item exists or the lookup fails for any reason — callers fall back
108
- * to the password env var and ultimately surface a clear configuration error.
109
- *
110
- * @param service - Keychain service / server name (typically the SMTP host)
111
- * @param account - Keychain account (typically the SMTP username)
112
- */
113
- export declare function readKeychainPassword(service: string, account: string): string | null;
114
- /**
115
- * Resolves SMTP connection configuration from environment + Keychain.
116
- *
117
- * @throws Error with an actionable message listing the missing settings.
118
- */
119
- export declare function resolveSmtpConfig(env?: NodeJS.ProcessEnv): SmtpConfig;
120
- /**
121
- * Sends an email over SMTP, producing clean MIME with no blockquote wrapping.
122
- *
123
- * Config is resolved via {@link resolveSmtpConfig} unless one is injected (the
124
- * `config` parameter exists for testing). The body is sent as plain text; pass
125
- * a transporter factory only in tests.
126
- */
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[]>;
160
- //# sourceMappingURL=smtpMailer.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"smtpMailer.d.ts","sourceRoot":"","sources":["../../src/services/smtpMailer.ts"],"names":[],"mappings":"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"}
@@ -1,268 +0,0 @@
1
- /**
2
- * SMTP transport for sending mail (issue #12).
3
- *
4
- * Mail.app's AppleScript send path wraps any injected body in
5
- * `<blockquote type="cite">` under the Apple-Mail-URLShareWrapperClass template
6
- * on macOS 15+, so messages render to recipients as quoted/forwarded content
7
- * (Apple radar FB11734014, open since Ventura). This module bypasses Mail.app
8
- * entirely and submits clean MIME directly over SMTP via nodemailer.
9
- *
10
- * Connection settings come from environment variables; the password is read
11
- * from the macOS Keychain via the `security` CLI by default so no secret is
12
- * ever placed in config. When SMTP is configured, `send-email` auto-prefers it
13
- * over AppleScript (opt out per call with `transport: "applescript"`); see
14
- * {@link shouldUseSmtp}.
15
- *
16
- * @module services/smtpMailer
17
- */
18
- import nodemailer from "nodemailer";
19
- import { execFileSync } from "child_process";
20
- import { isAbsolute } from "path";
21
- import { existsSync } from "fs";
22
- /**
23
- * Environment variables consumed by {@link resolveSmtpConfig}. Documented here
24
- * (and in the README) so the error path can point users at exactly what to set.
25
- */
26
- export const SMTP_ENV = {
27
- host: "APPLE_MAIL_MCP_SMTP_HOST",
28
- port: "APPLE_MAIL_MCP_SMTP_PORT",
29
- secure: "APPLE_MAIL_MCP_SMTP_SECURE",
30
- user: "APPLE_MAIL_MCP_SMTP_USER",
31
- from: "APPLE_MAIL_MCP_SMTP_FROM",
32
- password: "APPLE_MAIL_MCP_SMTP_PASSWORD",
33
- keychainService: "APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE",
34
- keychainAccount: "APPLE_MAIL_MCP_SMTP_KEYCHAIN_ACCOUNT",
35
- };
36
- /**
37
- * Cheap check for whether the SMTP transport is configured at all, i.e. the two
38
- * required settings ({@link SMTP_ENV.host} and {@link SMTP_ENV.user}) are
39
- * present. Used to auto-prefer SMTP over AppleScript when no transport is
40
- * explicitly requested, and by `doctor`. Does NOT touch the Keychain or verify
41
- * the password — a misconfigured password still surfaces a clear error at send
42
- * time via {@link resolveSmtpConfig}.
43
- */
44
- export function isSmtpConfigured(env = process.env) {
45
- return Boolean(env[SMTP_ENV.host]?.trim() && env[SMTP_ENV.user]?.trim());
46
- }
47
- /**
48
- * Decides whether `send-email` should use the SMTP transport for this call.
49
- *
50
- * - explicit `"smtp"` → always SMTP (config errors surface, no fallback);
51
- * - explicit `"applescript"` → always the Mail.app path;
52
- * - omitted → SMTP when configured, **except** when a non-email `account` label
53
- * (a Mail.app account name such as `"Work"`) is supplied. That requests
54
- * account-based sending, which only the AppleScript path can do, so we honor
55
- * the caller's intent instead of silently switching their account. An
56
- * `account` that is an email address is treated as a From override and does
57
- * not block SMTP.
58
- */
59
- export function shouldUseSmtp(transport, account, configured = isSmtpConfigured()) {
60
- if (transport === "smtp")
61
- return true;
62
- if (transport === "applescript")
63
- return false;
64
- if (!configured)
65
- return false;
66
- const isAccountLabel = Boolean(account && !account.includes("@"));
67
- return !isAccountLabel;
68
- }
69
- /**
70
- * Reads a password from the macOS login Keychain via the `security` CLI.
71
- *
72
- * Tries `find-internet-password` first (where Mail.app stores account
73
- * passwords) and falls back to `find-generic-password`. Returns null if no
74
- * matching item exists or the lookup fails for any reason — callers fall back
75
- * to the password env var and ultimately surface a clear configuration error.
76
- *
77
- * @param service - Keychain service / server name (typically the SMTP host)
78
- * @param account - Keychain account (typically the SMTP username)
79
- */
80
- export function readKeychainPassword(service, account) {
81
- for (const kind of ["find-internet-password", "find-generic-password"]) {
82
- try {
83
- const out = execFileSync("security", [kind, "-s", service, "-a", account, "-w"], {
84
- encoding: "utf8",
85
- stdio: ["ignore", "pipe", "ignore"],
86
- });
87
- const pass = out.replace(/\n$/, "");
88
- if (pass)
89
- return pass;
90
- }
91
- catch {
92
- // Not found via this kind; try the next.
93
- }
94
- }
95
- return null;
96
- }
97
- /**
98
- * Resolves SMTP connection configuration from environment + Keychain.
99
- *
100
- * @throws Error with an actionable message listing the missing settings.
101
- */
102
- export function resolveSmtpConfig(env = process.env) {
103
- const host = env[SMTP_ENV.host]?.trim();
104
- const user = env[SMTP_ENV.user]?.trim();
105
- const missing = [];
106
- if (!host)
107
- missing.push(SMTP_ENV.host);
108
- if (!user)
109
- missing.push(SMTP_ENV.user);
110
- if (missing.length > 0) {
111
- throw new Error(`SMTP transport is not configured. Set ${missing.join(" and ")} ` +
112
- `(plus a password via ${SMTP_ENV.password} or the Keychain). ` +
113
- `See the README "SMTP transport" section.`);
114
- }
115
- // secure=true => implicit TLS (port 465); otherwise STARTTLS (port 587).
116
- const secure = /^(1|true|yes)$/i.test(env[SMTP_ENV.secure]?.trim() ?? "");
117
- const port = env[SMTP_ENV.port]
118
- ? Number.parseInt(env[SMTP_ENV.port], 10)
119
- : secure
120
- ? 465
121
- : 587;
122
- if (!Number.isInteger(port) || port <= 0) {
123
- throw new Error(`Invalid ${SMTP_ENV.port}: "${env[SMTP_ENV.port]}" is not a valid port.`);
124
- }
125
- const from = env[SMTP_ENV.from]?.trim() || user;
126
- // Password: explicit env var wins, otherwise Keychain (service/account
127
- // default to the host/user but can be overridden).
128
- let pass = env[SMTP_ENV.password];
129
- if (!pass) {
130
- const service = env[SMTP_ENV.keychainService]?.trim() || host;
131
- const account = env[SMTP_ENV.keychainAccount]?.trim() || user;
132
- pass = readKeychainPassword(service, account) ?? undefined;
133
- }
134
- if (!pass) {
135
- throw new Error(`No SMTP password found. Set ${SMTP_ENV.password}, or store an internet ` +
136
- `password in the Keychain for service "${env[SMTP_ENV.keychainService]?.trim() || host}" / account "${env[SMTP_ENV.keychainAccount]?.trim() || user}".`);
137
- }
138
- return { host: host, port, secure, user: user, pass, from };
139
- }
140
- /**
141
- * Validates attachment paths the same way the AppleScript path does: absolute
142
- * and existing. Returns nodemailer attachment descriptors.
143
- */
144
- function buildAttachments(attachments) {
145
- if (!attachments || attachments.length === 0)
146
- return undefined;
147
- return attachments.map((a) => {
148
- if (typeof a === "string") {
149
- if (!isAbsolute(a))
150
- throw new Error(`Attachment path must be absolute: "${a}"`);
151
- if (!existsSync(a))
152
- throw new Error(`Attachment file not found: "${a}"`);
153
- return { path: a };
154
- }
155
- if (!a.filename || !a.contentBase64) {
156
- throw new Error("Inline attachment requires both filename and contentBase64.");
157
- }
158
- return { filename: a.filename, content: Buffer.from(a.contentBase64, "base64") };
159
- });
160
- }
161
- /**
162
- * Sends an email over SMTP, producing clean MIME with no blockquote wrapping.
163
- *
164
- * Config is resolved via {@link resolveSmtpConfig} unless one is injected (the
165
- * `config` parameter exists for testing). The body is sent as plain text; pass
166
- * a transporter factory only in tests.
167
- */
168
- export async function sendViaSmtp(opts, config, createTransport = nodemailer.createTransport) {
169
- let cfg;
170
- try {
171
- cfg = config ?? resolveSmtpConfig();
172
- }
173
- catch (error) {
174
- return { success: false, error: error instanceof Error ? error.message : String(error) };
175
- }
176
- let attachments;
177
- try {
178
- attachments = buildAttachments(opts.attachments);
179
- }
180
- catch (error) {
181
- return { success: false, error: error instanceof Error ? error.message : String(error) };
182
- }
183
- const transporter = createTransport({
184
- host: cfg.host,
185
- port: cfg.port,
186
- secure: cfg.secure,
187
- auth: { user: cfg.user, pass: cfg.pass },
188
- });
189
- const html = opts.htmlBody?.trim() ? opts.htmlBody : undefined;
190
- try {
191
- const info = await transporter.sendMail({
192
- from: opts.from?.trim() || cfg.from,
193
- to: opts.to,
194
- cc: opts.cc,
195
- bcc: opts.bcc,
196
- subject: opts.subject,
197
- text: opts.body,
198
- // When present, nodemailer emits multipart/alternative (text + html).
199
- html,
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,
204
- });
205
- return { success: true, messageId: info.messageId };
206
- }
207
- catch (error) {
208
- return {
209
- success: false,
210
- error: `SMTP send failed: ${error instanceof Error ? error.message : String(error)}`,
211
- };
212
- }
213
- finally {
214
- transporter.close();
215
- }
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
- }