apple-mail-mcp 2.3.0 → 2.4.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/README.md +51 -8
- package/build/cli.d.ts +24 -0
- package/build/cli.d.ts.map +1 -0
- package/build/cli.js +165 -0
- package/build/index.js +17 -6
- package/build/services/smtpMailer.d.ts +33 -2
- package/build/services/smtpMailer.d.ts.map +1 -1
- package/build/services/smtpMailer.js +39 -2
- package/build/tools/doctor.js +5 -5
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that e
|
|
|
6
6
|
[](https://www.npmjs.com/package/apple-mail-mcp)
|
|
7
7
|
[](https://www.npmjs.com/package/apple-mail-mcp)
|
|
8
8
|
[](https://github.com/sweetrb/apple-mail-mcp/actions/workflows/ci.yml)
|
|
9
|
+
[](https://scorecard.dev/viewer/?uri=github.com/sweetrb/apple-mail-mcp)
|
|
9
10
|
[](https://www.apple.com/macos/)
|
|
10
11
|
[](https://opensource.org/licenses/MIT)
|
|
11
12
|
[](https://modelcontextprotocol.io)
|
|
@@ -255,7 +256,7 @@ Send a new email immediately.
|
|
|
255
256
|
| `bcc` | string[] | No | BCC recipients |
|
|
256
257
|
| `account` | string | No | Send from specific account (with `transport: "smtp"`, overrides the From address) |
|
|
257
258
|
| `attachments` | (string \| {filename, contentBase64})[] | No | Up to 20 attachments: absolute file paths (e.g., `"/Users/me/report.pdf"`) and/or inline `{filename, contentBase64}` objects for content not on disk |
|
|
258
|
-
| `transport` | `"applescript"` \| `"smtp"` | No | Send transport (
|
|
259
|
+
| `transport` | `"applescript"` \| `"smtp"` | No | Send transport. If omitted, **SMTP is used automatically when configured** (otherwise AppleScript). Pass `"smtp"` to require clean MIME, or `"applescript"` to force the Mail.app path — see [SMTP transport](#smtp-transport) |
|
|
259
260
|
|
|
260
261
|
**Example:**
|
|
261
262
|
```json
|
|
@@ -274,8 +275,25 @@ On macOS 15+ (Sequoia/Tahoe), Mail.app wraps any AppleScript-injected body in
|
|
|
274
275
|
`<blockquote type="cite">` under the `Apple-Mail-URLShareWrapperClass` template,
|
|
275
276
|
so emails sent through the default `applescript` transport render to recipients
|
|
276
277
|
as if they were quoted/forwarded (Apple radar **FB11734014**, open since
|
|
277
|
-
Ventura).
|
|
278
|
-
|
|
278
|
+
Ventura). The SMTP transport bypasses Mail.app entirely and submits clean MIME
|
|
279
|
+
directly. **Once SMTP is configured, `send-email` uses it automatically** (no
|
|
280
|
+
need to pass `transport` per call); pass `transport: "applescript"` to force the
|
|
281
|
+
Mail.app path.
|
|
282
|
+
|
|
283
|
+
Two differences to know when SMTP is auto-preferred:
|
|
284
|
+
|
|
285
|
+
- **No Sent-folder copy.** SMTP submission does not file the message in Mail.app's
|
|
286
|
+
Sent mailbox (the server's own "save to Sent" may, depending on provider). Use
|
|
287
|
+
`transport: "applescript"` if you need the local Sent copy.
|
|
288
|
+
- **`account` is a From override, not account selection.** Over SMTP, `account`
|
|
289
|
+
is used as the From address only when it is an email address; a Mail.app
|
|
290
|
+
account *label* (e.g. `"Work"`) can't select an account over SMTP, so a call
|
|
291
|
+
that passes one is left on the AppleScript path automatically. To force
|
|
292
|
+
account selection, pass `transport: "applescript"` explicitly.
|
|
293
|
+
|
|
294
|
+
Both plain-text and HTML bodies are supported — over SMTP an HTML body (CLI
|
|
295
|
+
`--html-body-file`) is sent as `multipart/alternative` with the plain-text
|
|
296
|
+
fallback.
|
|
279
297
|
|
|
280
298
|
Configure SMTP via environment variables on the MCP server. The password is
|
|
281
299
|
read from the macOS **Keychain** by default, so no secret goes in config:
|
|
@@ -292,23 +310,48 @@ read from the macOS **Keychain** by default, so no secret goes in config:
|
|
|
292
310
|
| `APPLE_MAIL_MCP_SMTP_KEYCHAIN_ACCOUNT` | No | = user | Keychain item account |
|
|
293
311
|
|
|
294
312
|
Store the password in the Keychain once (an app-specific password for Gmail/
|
|
295
|
-
iCloud)
|
|
313
|
+
iCloud). A generic-password item with an explicit service name keeps it from
|
|
314
|
+
colliding with the system mail account password, and matches
|
|
315
|
+
`APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE`:
|
|
296
316
|
|
|
297
317
|
```bash
|
|
318
|
+
# Fastmail (Keychain service defaults to the host)
|
|
298
319
|
security add-internet-password -s smtp.fastmail.com -a you@example.com -w
|
|
320
|
+
|
|
321
|
+
# Gmail / Google Workspace, using a dedicated Keychain service name:
|
|
322
|
+
# APPLE_MAIL_MCP_SMTP_HOST=smtp.gmail.com
|
|
323
|
+
# APPLE_MAIL_MCP_SMTP_USER=you@gmail.com
|
|
324
|
+
# APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE=apple-mail-mcp-smtp
|
|
325
|
+
security add-generic-password -s apple-mail-mcp-smtp -a you@gmail.com -w
|
|
299
326
|
```
|
|
300
327
|
|
|
301
|
-
|
|
328
|
+
Once the env vars are set, a plain `send-email` (no `transport`) already goes
|
|
329
|
+
out clean:
|
|
302
330
|
```json
|
|
303
331
|
{
|
|
304
332
|
"to": ["colleague@company.com"],
|
|
305
333
|
"subject": "Standings",
|
|
306
|
-
"body": "Plain body — no blockquote wrapping."
|
|
307
|
-
"transport": "smtp"
|
|
334
|
+
"body": "Plain body — no blockquote wrapping."
|
|
308
335
|
}
|
|
309
336
|
```
|
|
310
337
|
|
|
311
|
-
|
|
338
|
+
###### `apple-mail-send` CLI (no MCP server required)
|
|
339
|
+
|
|
340
|
+
The package also installs an `apple-mail-send` binary — a standalone CLI over the
|
|
341
|
+
same SMTP path, for cron jobs, scheduled tasks, and scripts that can't run an MCP
|
|
342
|
+
session. It reads the identical `APPLE_MAIL_MCP_SMTP_*` env + Keychain config:
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
apple-mail-send \
|
|
346
|
+
--from you@example.com --to colleague@company.com \
|
|
347
|
+
--subject "Standings" --body-file /tmp/body.txt \
|
|
348
|
+
[--html-body-file /tmp/body.html] [--attach /tmp/report.pdf]
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Repeatable `--to`/`--cc`/`--bcc`/`--attach`; an `--html-body-file` is sent as a
|
|
352
|
+
`multipart/alternative` alongside the plain `--body-file`. Exit codes follow
|
|
353
|
+
`sysexits.h`: `0` success, `64` usage error, `66` unreadable body file, `78`
|
|
354
|
+
SMTP not configured.
|
|
312
355
|
|
|
313
356
|
##### IMAP backend — opt-in
|
|
314
357
|
|
package/build/cli.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { sendViaSmtp, resolveSmtpConfig } from "./services/smtpMailer.js";
|
|
3
|
+
/** sysexits.h codes used so callers can distinguish failure modes. */
|
|
4
|
+
export declare const EX_USAGE = 64;
|
|
5
|
+
export declare const EX_NOINPUT = 66;
|
|
6
|
+
export declare const EX_CONFIG = 78;
|
|
7
|
+
/** Injectable dependencies, defaulted to the real implementations. */
|
|
8
|
+
export interface CliDeps {
|
|
9
|
+
send?: typeof sendViaSmtp;
|
|
10
|
+
resolveConfig?: typeof resolveSmtpConfig;
|
|
11
|
+
readTextFile?: (path: string) => string;
|
|
12
|
+
stdout?: (line: string) => void;
|
|
13
|
+
stderr?: (line: string) => void;
|
|
14
|
+
env?: NodeJS.ProcessEnv;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Parses argv, sends the email, and returns a process exit code. Pure with
|
|
18
|
+
* respect to its deps (no direct process/console access) so it can be unit
|
|
19
|
+
* tested without the network or Keychain.
|
|
20
|
+
*
|
|
21
|
+
* @param argv - arguments AFTER `node cli.js` (i.e. `process.argv.slice(2)`)
|
|
22
|
+
*/
|
|
23
|
+
export declare function runCli(argv: string[], deps?: CliDeps): Promise<number>;
|
|
24
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAyBA,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAY,MAAM,0BAA0B,CAAC;AAGpF,sEAAsE;AACtE,eAAO,MAAM,QAAQ,KAAK,CAAC;AAC3B,eAAO,MAAM,UAAU,KAAK,CAAC;AAC7B,eAAO,MAAM,SAAS,KAAK,CAAC;AAqB5B,sEAAsE;AACtE,MAAM,WAAW,OAAO;IACtB,IAAI,CAAC,EAAE,OAAO,WAAW,CAAC;IAC1B,aAAa,CAAC,EAAE,OAAO,iBAAiB,CAAC;IACzC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACxC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,GAAE,OAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAwGhF"}
|
package/build/cli.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `apple-mail-send` — standalone clean-SMTP email CLI.
|
|
4
|
+
*
|
|
5
|
+
* A thin command-line front end over {@link sendViaSmtp} so scheduled tasks,
|
|
6
|
+
* cron jobs, and other scripts can send clean MIME (no Mail.app blockquote
|
|
7
|
+
* wrapping, issue #12) WITHOUT a running MCP server. It reuses the exact same
|
|
8
|
+
* SMTP config resolution as the MCP `send-email` tool — env vars first, then the
|
|
9
|
+
* macOS Keychain (see the README "SMTP transport" section).
|
|
10
|
+
*
|
|
11
|
+
* The flag surface intentionally mirrors the legacy nhl-bracket-tracker
|
|
12
|
+
* `send_email.py` (--from/--to/--cc/--bcc/--subject/--body-file/
|
|
13
|
+
* --html-body-file/--attach) and its exit codes (78 = EX_CONFIG when SMTP is
|
|
14
|
+
* not configured) so callers can drop this in as a replacement.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* apple-mail-send --from me@example.com --to you@example.com \
|
|
18
|
+
* --subject "Hi" --body-file /tmp/body.txt \
|
|
19
|
+
* [--html-body-file /tmp/body.html] [--attach /tmp/report.pdf]
|
|
20
|
+
*
|
|
21
|
+
* @module cli
|
|
22
|
+
*/
|
|
23
|
+
import { readFileSync, realpathSync } from "fs";
|
|
24
|
+
import { fileURLToPath } from "url";
|
|
25
|
+
import { parseArgs } from "util";
|
|
26
|
+
import { sendViaSmtp, resolveSmtpConfig, SMTP_ENV } from "./services/smtpMailer.js";
|
|
27
|
+
/** sysexits.h codes used so callers can distinguish failure modes. */
|
|
28
|
+
export const EX_USAGE = 64;
|
|
29
|
+
export const EX_NOINPUT = 66;
|
|
30
|
+
export const EX_CONFIG = 78;
|
|
31
|
+
const USAGE = `apple-mail-send — send a clean email via SMTP (no Mail.app blockquote wrapping).
|
|
32
|
+
|
|
33
|
+
Required:
|
|
34
|
+
--from <addr> Sender address (must be allowed by the SMTP server)
|
|
35
|
+
--to <addr> Recipient (repeatable)
|
|
36
|
+
--subject <text> Subject line
|
|
37
|
+
--body-file <path> UTF-8 file with the plain-text body
|
|
38
|
+
|
|
39
|
+
Optional:
|
|
40
|
+
--cc <addr> CC recipient (repeatable)
|
|
41
|
+
--bcc <addr> BCC recipient (repeatable)
|
|
42
|
+
--html-body-file <p> UTF-8 file with an HTML alternative body (sends
|
|
43
|
+
multipart/alternative)
|
|
44
|
+
--attach <path> Absolute path to a file to attach (repeatable)
|
|
45
|
+
--help Show this help
|
|
46
|
+
|
|
47
|
+
SMTP connection comes from ${SMTP_ENV.host} / ${SMTP_ENV.user} (+ password via
|
|
48
|
+
${SMTP_ENV.password} or the macOS Keychain). See the README "SMTP transport".`;
|
|
49
|
+
/**
|
|
50
|
+
* Parses argv, sends the email, and returns a process exit code. Pure with
|
|
51
|
+
* respect to its deps (no direct process/console access) so it can be unit
|
|
52
|
+
* tested without the network or Keychain.
|
|
53
|
+
*
|
|
54
|
+
* @param argv - arguments AFTER `node cli.js` (i.e. `process.argv.slice(2)`)
|
|
55
|
+
*/
|
|
56
|
+
export async function runCli(argv, deps = {}) {
|
|
57
|
+
const send = deps.send ?? sendViaSmtp;
|
|
58
|
+
const resolveConfig = deps.resolveConfig ?? resolveSmtpConfig;
|
|
59
|
+
const readTextFile = deps.readTextFile ?? ((p) => readFileSync(p, "utf8"));
|
|
60
|
+
const out = deps.stdout ?? ((l) => console.log(l));
|
|
61
|
+
const err = deps.stderr ?? ((l) => console.error(l));
|
|
62
|
+
const env = deps.env ?? process.env;
|
|
63
|
+
let values;
|
|
64
|
+
try {
|
|
65
|
+
({ values } = parseArgs({
|
|
66
|
+
args: argv,
|
|
67
|
+
options: {
|
|
68
|
+
from: { type: "string" },
|
|
69
|
+
to: { type: "string", multiple: true },
|
|
70
|
+
cc: { type: "string", multiple: true },
|
|
71
|
+
bcc: { type: "string", multiple: true },
|
|
72
|
+
subject: { type: "string" },
|
|
73
|
+
"body-file": { type: "string" },
|
|
74
|
+
"html-body-file": { type: "string" },
|
|
75
|
+
attach: { type: "string", multiple: true },
|
|
76
|
+
help: { type: "boolean" },
|
|
77
|
+
},
|
|
78
|
+
allowPositionals: false,
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
err(e instanceof Error ? e.message : String(e));
|
|
83
|
+
err(USAGE);
|
|
84
|
+
return EX_USAGE;
|
|
85
|
+
}
|
|
86
|
+
if (values.help) {
|
|
87
|
+
out(USAGE);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
const missing = [];
|
|
91
|
+
if (!values.from)
|
|
92
|
+
missing.push("--from");
|
|
93
|
+
if (!values.to || values.to.length === 0)
|
|
94
|
+
missing.push("--to");
|
|
95
|
+
if (!values.subject)
|
|
96
|
+
missing.push("--subject");
|
|
97
|
+
if (!values["body-file"])
|
|
98
|
+
missing.push("--body-file");
|
|
99
|
+
if (missing.length > 0) {
|
|
100
|
+
err(`Missing required argument(s): ${missing.join(", ")}`);
|
|
101
|
+
err(USAGE);
|
|
102
|
+
return EX_USAGE;
|
|
103
|
+
}
|
|
104
|
+
// Resolve the full SMTP config up front (host/user/port + Keychain password)
|
|
105
|
+
// so a missing password counts as EX_CONFIG (78) — the same actionable
|
|
106
|
+
// "needs setup" signal as a missing host/user — rather than a generic failure.
|
|
107
|
+
let config;
|
|
108
|
+
try {
|
|
109
|
+
config = resolveConfig(env);
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
err(e instanceof Error ? e.message : String(e));
|
|
113
|
+
err(`See the README "SMTP transport" section.`);
|
|
114
|
+
return EX_CONFIG;
|
|
115
|
+
}
|
|
116
|
+
let body;
|
|
117
|
+
let htmlBody;
|
|
118
|
+
try {
|
|
119
|
+
body = readTextFile(values["body-file"]);
|
|
120
|
+
if (values["html-body-file"])
|
|
121
|
+
htmlBody = readTextFile(values["html-body-file"]);
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
err(`Could not read body file: ${e instanceof Error ? e.message : String(e)}`);
|
|
125
|
+
return EX_NOINPUT;
|
|
126
|
+
}
|
|
127
|
+
const attachments = values.attach?.length
|
|
128
|
+
? values.attach
|
|
129
|
+
: undefined;
|
|
130
|
+
const result = await send({
|
|
131
|
+
from: values.from,
|
|
132
|
+
to: values.to,
|
|
133
|
+
cc: values.cc,
|
|
134
|
+
bcc: values.bcc,
|
|
135
|
+
subject: values.subject,
|
|
136
|
+
body,
|
|
137
|
+
htmlBody,
|
|
138
|
+
attachments,
|
|
139
|
+
}, config);
|
|
140
|
+
if (!result.success) {
|
|
141
|
+
err(result.error ?? "SMTP send failed.");
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
144
|
+
out(`sent to ${values.to.join(", ")} from ${values.from}`);
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
// Entry point when invoked directly (not when imported by tests). Resolve both
|
|
148
|
+
// sides through the filesystem so it still matches when launched via the npm
|
|
149
|
+
// `bin` symlink (argv[1] is the symlink; import.meta.url is the real build file).
|
|
150
|
+
function isInvokedDirectly() {
|
|
151
|
+
if (typeof process === "undefined" || !process.argv?.[1])
|
|
152
|
+
return false;
|
|
153
|
+
try {
|
|
154
|
+
return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (isInvokedDirectly()) {
|
|
161
|
+
runCli(process.argv.slice(2)).then((code) => process.exit(code), (e) => {
|
|
162
|
+
console.error(e instanceof Error ? (e.stack ?? e.message) : String(e));
|
|
163
|
+
process.exit(1);
|
|
164
|
+
});
|
|
165
|
+
}
|
package/build/index.js
CHANGED
|
@@ -26,7 +26,7 @@ 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 } from "./services/smtpMailer.js";
|
|
29
|
+
import { sendViaSmtp, shouldUseSmtp } from "./services/smtpMailer.js";
|
|
30
30
|
import { isImapAccount, resolveImapConfigs, 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
31
|
import { successResponse, errorResponse, partialCoverageBlock, withErrorHandling, messageSummary, } from "./tools/respond.js";
|
|
32
32
|
import { routeMessage } from "./services/messageRouter.js";
|
|
@@ -413,9 +413,11 @@ server.registerTool("send-email", {
|
|
|
413
413
|
transport: z
|
|
414
414
|
.enum(["applescript", "smtp"])
|
|
415
415
|
.optional()
|
|
416
|
-
.describe("Send transport. '
|
|
417
|
-
"
|
|
418
|
-
"
|
|
416
|
+
.describe("Send transport. 'smtp' submits clean MIME directly via SMTP, avoiding " +
|
|
417
|
+
"the macOS 15+ Mail.app <blockquote> wrapping (issue #12); requires " +
|
|
418
|
+
"APPLE_MAIL_MCP_SMTP_* env config. 'applescript' sends through Mail.app. " +
|
|
419
|
+
"If omitted, SMTP is used automatically when APPLE_MAIL_MCP_SMTP_* is " +
|
|
420
|
+
"configured, otherwise AppleScript."),
|
|
419
421
|
},
|
|
420
422
|
outputSchema: {
|
|
421
423
|
ok: z.boolean().optional(),
|
|
@@ -426,8 +428,17 @@ server.registerTool("send-email", {
|
|
|
426
428
|
}, withErrorHandling(async ({ to, subject, body, cc, bcc, account, attachments, transport }) => {
|
|
427
429
|
const attachInfo = attachments?.length ? ` with ${attachments.length} attachment(s)` : "";
|
|
428
430
|
const attachmentCount = attachments?.length ?? 0;
|
|
429
|
-
|
|
430
|
-
|
|
431
|
+
// Prefer SMTP when explicitly requested, or automatically when it is
|
|
432
|
+
// configured and no transport was specified — except when a non-email
|
|
433
|
+
// `account` label requests Mail.app account selection (see shouldUseSmtp).
|
|
434
|
+
// Explicit transport:"applescript" always forces the Mail.app path.
|
|
435
|
+
if (shouldUseSmtp(transport, account)) {
|
|
436
|
+
// `account` is a Mail.app account label for the AppleScript path; for SMTP
|
|
437
|
+
// it only makes sense as a From override when it is an actual address.
|
|
438
|
+
// A bare label (only possible here via explicit transport:"smtp") must not
|
|
439
|
+
// corrupt the From — fall back to the configured SMTP From in that case.
|
|
440
|
+
const smtpFrom = account?.includes("@") ? account : undefined;
|
|
441
|
+
const result = await sendViaSmtp({ to, subject, body, cc, bcc, from: smtpFrom, attachments });
|
|
431
442
|
if (!result.success) {
|
|
432
443
|
return errorResponse(result.error ?? "Failed to send email via SMTP.");
|
|
433
444
|
}
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Connection settings come from environment variables; the password is read
|
|
11
11
|
* from the macOS Keychain via the `security` CLI by default so no secret is
|
|
12
|
-
* ever placed in config.
|
|
13
|
-
* opt
|
|
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}.
|
|
14
15
|
*
|
|
15
16
|
* @module services/smtpMailer
|
|
16
17
|
*/
|
|
@@ -20,6 +21,7 @@ import type { AttachmentInput } from "../types.js";
|
|
|
20
21
|
export interface SmtpSendOptions {
|
|
21
22
|
to: string[];
|
|
22
23
|
subject: string;
|
|
24
|
+
/** Plain-text body. Always sent as the text/plain part. */
|
|
23
25
|
body: string;
|
|
24
26
|
cc?: string[];
|
|
25
27
|
bcc?: string[];
|
|
@@ -27,6 +29,13 @@ export interface SmtpSendOptions {
|
|
|
27
29
|
from?: string;
|
|
28
30
|
/** Files to attach: absolute paths and/or inline base64 content (B4). */
|
|
29
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;
|
|
30
39
|
}
|
|
31
40
|
/** Resolved SMTP connection configuration. */
|
|
32
41
|
export interface SmtpConfig {
|
|
@@ -57,6 +66,28 @@ export declare const SMTP_ENV: {
|
|
|
57
66
|
readonly keychainService: "APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE";
|
|
58
67
|
readonly keychainAccount: "APPLE_MAIL_MCP_SMTP_KEYCHAIN_ACCOUNT";
|
|
59
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* Cheap check for whether the SMTP transport is configured at all, i.e. the two
|
|
71
|
+
* required settings ({@link SMTP_ENV.host} and {@link SMTP_ENV.user}) are
|
|
72
|
+
* present. Used to auto-prefer SMTP over AppleScript when no transport is
|
|
73
|
+
* explicitly requested, and by `doctor`. Does NOT touch the Keychain or verify
|
|
74
|
+
* the password — a misconfigured password still surfaces a clear error at send
|
|
75
|
+
* time via {@link resolveSmtpConfig}.
|
|
76
|
+
*/
|
|
77
|
+
export declare function isSmtpConfigured(env?: NodeJS.ProcessEnv): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Decides whether `send-email` should use the SMTP transport for this call.
|
|
80
|
+
*
|
|
81
|
+
* - explicit `"smtp"` → always SMTP (config errors surface, no fallback);
|
|
82
|
+
* - explicit `"applescript"` → always the Mail.app path;
|
|
83
|
+
* - omitted → SMTP when configured, **except** when a non-email `account` label
|
|
84
|
+
* (a Mail.app account name such as `"Work"`) is supplied. That requests
|
|
85
|
+
* account-based sending, which only the AppleScript path can do, so we honor
|
|
86
|
+
* the caller's intent instead of silently switching their account. An
|
|
87
|
+
* `account` that is an email address is treated as a From override and does
|
|
88
|
+
* not block SMTP.
|
|
89
|
+
*/
|
|
90
|
+
export declare function shouldUseSmtp(transport: "applescript" | "smtp" | undefined, account: string | undefined, configured?: boolean): boolean;
|
|
60
91
|
/**
|
|
61
92
|
* Reads a password from the macOS login Keychain via the `security` CLI.
|
|
62
93
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"smtpMailer.d.ts","sourceRoot":"","sources":["../../src/services/smtpMailer.ts"],"names":[],"mappings":"AAAA
|
|
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"}
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Connection settings come from environment variables; the password is read
|
|
11
11
|
* from the macOS Keychain via the `security` CLI by default so no secret is
|
|
12
|
-
* ever placed in config.
|
|
13
|
-
* opt
|
|
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}.
|
|
14
15
|
*
|
|
15
16
|
* @module services/smtpMailer
|
|
16
17
|
*/
|
|
@@ -32,6 +33,39 @@ export const SMTP_ENV = {
|
|
|
32
33
|
keychainService: "APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE",
|
|
33
34
|
keychainAccount: "APPLE_MAIL_MCP_SMTP_KEYCHAIN_ACCOUNT",
|
|
34
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
|
+
}
|
|
35
69
|
/**
|
|
36
70
|
* Reads a password from the macOS login Keychain via the `security` CLI.
|
|
37
71
|
*
|
|
@@ -152,6 +186,7 @@ export async function sendViaSmtp(opts, config, createTransport = nodemailer.cre
|
|
|
152
186
|
secure: cfg.secure,
|
|
153
187
|
auth: { user: cfg.user, pass: cfg.pass },
|
|
154
188
|
});
|
|
189
|
+
const html = opts.htmlBody?.trim() ? opts.htmlBody : undefined;
|
|
155
190
|
try {
|
|
156
191
|
const info = await transporter.sendMail({
|
|
157
192
|
from: opts.from?.trim() || cfg.from,
|
|
@@ -160,6 +195,8 @@ export async function sendViaSmtp(opts, config, createTransport = nodemailer.cre
|
|
|
160
195
|
bcc: opts.bcc,
|
|
161
196
|
subject: opts.subject,
|
|
162
197
|
text: opts.body,
|
|
198
|
+
// When present, nodemailer emits multipart/alternative (text + html).
|
|
199
|
+
html,
|
|
163
200
|
attachments,
|
|
164
201
|
});
|
|
165
202
|
return { success: true, messageId: info.messageId };
|
package/build/tools/doctor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { imapHealthCheck, IMAP_ENV, listImapAccountLabels } from "../services/imapClient.js";
|
|
2
|
-
import { SMTP_ENV } from "../services/smtpMailer.js";
|
|
2
|
+
import { SMTP_ENV, isSmtpConfigured } from "../services/smtpMailer.js";
|
|
3
3
|
export async function runDoctor(mailManager) {
|
|
4
4
|
const checks = [];
|
|
5
5
|
// 1. Mail.app reachability + Automation permission (existing health check).
|
|
@@ -56,10 +56,10 @@ export async function runDoctor(mailManager) {
|
|
|
56
56
|
const smtpHost = process.env[SMTP_ENV.host]?.trim();
|
|
57
57
|
checks.push({
|
|
58
58
|
name: "SMTP transport",
|
|
59
|
-
status:
|
|
60
|
-
detail:
|
|
61
|
-
? `configured (${smtpHost}); send-email transport:"
|
|
62
|
-
: `not configured — send-email uses AppleScript (subject to macOS 15+ blockquote wrapping). Set ${SMTP_ENV.host} to enable.`,
|
|
59
|
+
status: isSmtpConfigured() ? "ok" : "warn",
|
|
60
|
+
detail: isSmtpConfigured()
|
|
61
|
+
? `configured (${smtpHost}); send-email auto-prefers clean SMTP (no Mail.app Sent-folder copy; a non-email "account" label still routes to AppleScript). Pass transport:"applescript" to force Mail.app. The apple-mail-send CLI is also available.`
|
|
62
|
+
: `not configured — send-email uses AppleScript (subject to macOS 15+ blockquote wrapping). Set ${SMTP_ENV.host} and ${SMTP_ENV.user} (+ password via Keychain) to enable.`,
|
|
63
63
|
});
|
|
64
64
|
const healthy = !checks.some((c) => c.status === "fail");
|
|
65
65
|
return { healthy, checks };
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-mail-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.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",
|
|
7
7
|
"types": "build/index.d.ts",
|
|
8
8
|
"bin": {
|
|
9
|
-
"apple-mail-mcp": "build/index.js"
|
|
9
|
+
"apple-mail-mcp": "build/index.js",
|
|
10
|
+
"apple-mail-send": "build/cli.js"
|
|
10
11
|
},
|
|
11
12
|
"files": [
|
|
12
13
|
"build",
|