apple-mail-mcp 1.5.7 → 1.6.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 +55 -1
- package/build/index.js +20 -4
- package/build/services/appleMailManager.d.ts.map +1 -1
- package/build/services/appleMailManager.js +5 -7
- package/build/services/smtpMailer.d.ts +85 -0
- package/build/services/smtpMailer.d.ts.map +1 -0
- package/build/services/smtpMailer.js +172 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -192,8 +192,9 @@ Send a new email immediately.
|
|
|
192
192
|
| `body` | string | Yes | Email body (plain text) |
|
|
193
193
|
| `cc` | string[] | No | CC recipients |
|
|
194
194
|
| `bcc` | string[] | No | BCC recipients |
|
|
195
|
-
| `account` | string | No | Send from specific account |
|
|
195
|
+
| `account` | string | No | Send from specific account (with `transport: "smtp"`, overrides the From address) |
|
|
196
196
|
| `attachments` | string[] | No | Absolute file paths to attach, max 20 files (e.g., `["/Users/me/report.pdf"]`) |
|
|
197
|
+
| `transport` | `"applescript"` \| `"smtp"` | No | Send transport (default `"applescript"`). Use `"smtp"` to send clean MIME directly, avoiding the macOS 15+ Mail.app `<blockquote>` wrapping — see [SMTP transport](#smtp-transport) |
|
|
197
198
|
|
|
198
199
|
**Example:**
|
|
199
200
|
```json
|
|
@@ -206,6 +207,48 @@ Send a new email immediately.
|
|
|
206
207
|
}
|
|
207
208
|
```
|
|
208
209
|
|
|
210
|
+
##### SMTP transport
|
|
211
|
+
|
|
212
|
+
On macOS 15+ (Sequoia/Tahoe), Mail.app wraps any AppleScript-injected body in
|
|
213
|
+
`<blockquote type="cite">` under the `Apple-Mail-URLShareWrapperClass` template,
|
|
214
|
+
so emails sent through the default `applescript` transport render to recipients
|
|
215
|
+
as if they were quoted/forwarded (Apple radar **FB11734014**, open since
|
|
216
|
+
Ventura). Passing `transport: "smtp"` bypasses Mail.app entirely and submits
|
|
217
|
+
clean MIME via SMTP.
|
|
218
|
+
|
|
219
|
+
Configure SMTP via environment variables on the MCP server. The password is
|
|
220
|
+
read from the macOS **Keychain** by default, so no secret goes in config:
|
|
221
|
+
|
|
222
|
+
| Variable | Required | Default | Description |
|
|
223
|
+
|----------|----------|---------|-------------|
|
|
224
|
+
| `APPLE_MAIL_MCP_SMTP_HOST` | Yes | — | SMTP server hostname (e.g. `smtp.fastmail.com`) |
|
|
225
|
+
| `APPLE_MAIL_MCP_SMTP_USER` | Yes | — | SMTP username |
|
|
226
|
+
| `APPLE_MAIL_MCP_SMTP_PORT` | No | `465` if secure, else `587` | SMTP port |
|
|
227
|
+
| `APPLE_MAIL_MCP_SMTP_SECURE` | No | `false` | `true` for implicit TLS (port 465); otherwise STARTTLS |
|
|
228
|
+
| `APPLE_MAIL_MCP_SMTP_FROM` | No | = user | From address |
|
|
229
|
+
| `APPLE_MAIL_MCP_SMTP_PASSWORD` | No | — | Password (if set, used instead of the Keychain) |
|
|
230
|
+
| `APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE` | No | = host | Keychain item service/server name |
|
|
231
|
+
| `APPLE_MAIL_MCP_SMTP_KEYCHAIN_ACCOUNT` | No | = user | Keychain item account |
|
|
232
|
+
|
|
233
|
+
Store the password in the Keychain once (an app-specific password for Gmail/
|
|
234
|
+
iCloud), e.g.:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
security add-internet-password -s smtp.fastmail.com -a you@example.com -w
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Then send:
|
|
241
|
+
```json
|
|
242
|
+
{
|
|
243
|
+
"to": ["colleague@company.com"],
|
|
244
|
+
"subject": "Standings",
|
|
245
|
+
"body": "Plain body — no blockquote wrapping.",
|
|
246
|
+
"transport": "smtp"
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The default `applescript` transport is unchanged; SMTP is opt-in per call.
|
|
251
|
+
|
|
209
252
|
---
|
|
210
253
|
|
|
211
254
|
#### `send-serial-email`
|
|
@@ -742,6 +785,17 @@ The entrypoint is written as:
|
|
|
742
785
|
| Attachment save path restrictions | `save-attachment` only allows saving to home directory, `/tmp`, `/private/tmp`, and `/Volumes`; path traversal is blocked |
|
|
743
786
|
| Attachment count limit | `send-email` and `create-draft` accept a maximum of 20 file attachments |
|
|
744
787
|
|
|
788
|
+
### Mail.app `<blockquote>` wrapping on macOS 15+ (workaround in v1.6.0)
|
|
789
|
+
|
|
790
|
+
On macOS 15+ Mail.app wraps AppleScript-injected message bodies in
|
|
791
|
+
`<blockquote type="cite">` under the `Apple-Mail-URLShareWrapperClass` template,
|
|
792
|
+
so mail sent via the default `applescript` transport renders to recipients as
|
|
793
|
+
quoted/forwarded content (Apple radar **FB11734014**, open since Ventura, no
|
|
794
|
+
fix). Since v1.6.0, `send-email` accepts `transport: "smtp"` to bypass Mail.app
|
|
795
|
+
and send clean MIME directly — see [SMTP transport](#smtp-transport). The
|
|
796
|
+
AppleScript path is still the default and still exhibits Apple's wrapping.
|
|
797
|
+
([#12](https://github.com/sweetrb/apple-mail-mcp/issues/12))
|
|
798
|
+
|
|
745
799
|
### Reply / Forward from Background Processes (Fixed in v1.4.0)
|
|
746
800
|
|
|
747
801
|
Prior to v1.4.0, `reply-to-message` and `forward-message` would send messages with **empty body text** when the MCP server ran as a background process (e.g., spawned via `execSync` from Node.js, which is how Claude Code invokes it).
|
package/build/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
24
24
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
25
25
|
import { z } from "zod";
|
|
26
26
|
import { AppleMailManager } from "./services/appleMailManager.js";
|
|
27
|
+
import { sendViaSmtp } from "./services/smtpMailer.js";
|
|
27
28
|
import { createSerialGate } from "./utils/serialize.js";
|
|
28
29
|
// =============================================================================
|
|
29
30
|
// Shared Validation Schemas
|
|
@@ -97,12 +98,14 @@ const serializeAppleScript = createSerialGate();
|
|
|
97
98
|
/**
|
|
98
99
|
* Wraps a tool handler with consistent error handling, serialized through the
|
|
99
100
|
* AppleScript gate so concurrent MCP tool calls don't race into Mail.app (#11).
|
|
101
|
+
* Handlers may be synchronous or async (the SMTP send path in send-email is
|
|
102
|
+
* async), so the handler result is awaited inside the gate.
|
|
100
103
|
*/
|
|
101
104
|
function withErrorHandling(handler, errorPrefix) {
|
|
102
105
|
return async (params) => {
|
|
103
|
-
return serializeAppleScript(() => {
|
|
106
|
+
return serializeAppleScript(async () => {
|
|
104
107
|
try {
|
|
105
|
-
return handler(params);
|
|
108
|
+
return await handler(params);
|
|
106
109
|
}
|
|
107
110
|
catch (error) {
|
|
108
111
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
@@ -190,12 +193,25 @@ server.tool("send-email", {
|
|
|
190
193
|
.max(20, "Cannot attach more than 20 files")
|
|
191
194
|
.optional()
|
|
192
195
|
.describe("Absolute file paths to attach (e.g., ['/Users/me/report.pdf'])"),
|
|
193
|
-
|
|
196
|
+
transport: z
|
|
197
|
+
.enum(["applescript", "smtp"])
|
|
198
|
+
.optional()
|
|
199
|
+
.describe("Send transport. 'applescript' (default) sends through Mail.app. " +
|
|
200
|
+
"'smtp' submits clean MIME directly via SMTP, avoiding the macOS 15+ " +
|
|
201
|
+
"Mail.app <blockquote> wrapping (issue #12); requires APPLE_MAIL_MCP_SMTP_* env config."),
|
|
202
|
+
}, withErrorHandling(async ({ to, subject, body, cc, bcc, account, attachments, transport }) => {
|
|
203
|
+
const attachInfo = attachments?.length ? ` with ${attachments.length} attachment(s)` : "";
|
|
204
|
+
if (transport === "smtp") {
|
|
205
|
+
const result = await sendViaSmtp({ to, subject, body, cc, bcc, from: account, attachments });
|
|
206
|
+
if (!result.success) {
|
|
207
|
+
return errorResponse(result.error ?? "Failed to send email via SMTP.");
|
|
208
|
+
}
|
|
209
|
+
return successResponse(`Email sent via SMTP to ${to.join(", ")}${attachInfo}`);
|
|
210
|
+
}
|
|
194
211
|
const success = mailManager.sendEmail(to, subject, body, cc, bcc, account, attachments);
|
|
195
212
|
if (!success) {
|
|
196
213
|
return errorResponse("Failed to send email. Check Mail.app configuration.");
|
|
197
214
|
}
|
|
198
|
-
const attachInfo = attachments?.length ? ` with ${attachments.length} attachment(s)` : "";
|
|
199
215
|
return successResponse(`Email sent to ${to.join(", ")}${attachInfo}`);
|
|
200
216
|
}, "Error sending email"));
|
|
201
217
|
// --- send-serial-email ---
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"appleMailManager.d.ts","sourceRoot":"","sources":["../../src/services/appleMailManager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAQH,OAAO,KAAK,EACV,OAAO,EACP,cAAc,EACd,OAAO,EACP,OAAO,EACP,UAAU,EACV,iBAAiB,EACjB,SAAS,EAET,oBAAoB,EACpB,UAAU,EACV,qBAAqB,EACrB,QAAQ,EACR,OAAO,EACP,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAoFpB;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,GAAG,MAAM,CAWrE;AA2CD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,sBAAsB;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CAoB5E;AAED,qBAAa,gBAAgB;IAC3B;;OAEG;IACH,OAAO,CAAC,cAAc,CAAuB;IAE7C;;;;OAIG;IACH,OAAO,CAAC,KAAK,CAGX;IAEF,8CAA8C;IAC9C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;IAEvC;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAUzB;;;;OAIG;IACH,OAAO,CAAC,qBAAqB;IAW7B;;;OAGG;IACH,OAAO,CAAC,eAAe;IAKvB;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAwCtB;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,cAAc;IAyCtB;;;;;;;;OAQG;IACH,cAAc,CACZ,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,EAChB,KAAK,SAAK,EACV,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,OAAO,EAChB,SAAS,CAAC,EAAE,OAAO,GAClB,OAAO,EAAE;IAwIZ;;;;;OAKG;IACH,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI;IAyE1C;;OAEG;IACH,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI;IAgDpD;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IA6BvC;;;;;;;OAOG;IACH,YAAY,CACV,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,EAChB,KAAK,SAAK,EACV,IAAI,CAAC,EAAE,MAAM,EACb,MAAM,SAAI,GACT,OAAO,EAAE;IA8GZ;;;;;;;;;;OAUG;IACH,OAAO,CAAC,gBAAgB;IAoCxB;;;;;;;;;;OAUG;
|
|
1
|
+
{"version":3,"file":"appleMailManager.d.ts","sourceRoot":"","sources":["../../src/services/appleMailManager.ts"],"names":[],"mappings":"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"}
|
|
@@ -789,13 +789,11 @@ export class AppleMailManager {
|
|
|
789
789
|
// Discussion: https://forums.macrumors.com/threads/applescript-creating-a-
|
|
790
790
|
// new-message-in-mail-app-is-causing-weird-formatting-issues.2385052/
|
|
791
791
|
//
|
|
792
|
-
//
|
|
793
|
-
//
|
|
794
|
-
//
|
|
795
|
-
//
|
|
796
|
-
//
|
|
797
|
-
// 2. Switch to smtplib-style direct send with Keychain-stored creds.
|
|
798
|
-
// 3. Use Mail.app's NSSharingService rather than AppleScript.
|
|
792
|
+
// FIX (v1.6.0): send-email now accepts `transport: "smtp"`, which bypasses
|
|
793
|
+
// Mail.app and submits clean MIME directly via nodemailer (creds from the
|
|
794
|
+
// Keychain). See src/services/smtpMailer.ts. This AppleScript path remains
|
|
795
|
+
// the default for back-compat and for users who don't configure SMTP, so the
|
|
796
|
+
// wrapping behavior below is unchanged for them.
|
|
799
797
|
// Tracking issue: https://github.com/sweetrb/apple-mail-mcp/issues/12
|
|
800
798
|
// ───────────────────────────────────────────────────────────────────
|
|
801
799
|
sendEmail(to, subject, body, cc, bcc, account, attachments) {
|
|
@@ -0,0 +1,85 @@
|
|
|
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. AppleScript remains the default transport — SMTP is
|
|
13
|
+
* opt-in per call (`transport: "smtp"`).
|
|
14
|
+
*
|
|
15
|
+
* @module services/smtpMailer
|
|
16
|
+
*/
|
|
17
|
+
import nodemailer from "nodemailer";
|
|
18
|
+
/** Options for an SMTP send, mirroring the AppleScript send-email surface. */
|
|
19
|
+
export interface SmtpSendOptions {
|
|
20
|
+
to: string[];
|
|
21
|
+
subject: string;
|
|
22
|
+
body: string;
|
|
23
|
+
cc?: string[];
|
|
24
|
+
bcc?: string[];
|
|
25
|
+
/** Overrides the configured From address (must be allowed by the SMTP server). */
|
|
26
|
+
from?: string;
|
|
27
|
+
/** Absolute paths to files to attach. */
|
|
28
|
+
attachments?: string[];
|
|
29
|
+
}
|
|
30
|
+
/** Resolved SMTP connection configuration. */
|
|
31
|
+
export interface SmtpConfig {
|
|
32
|
+
host: string;
|
|
33
|
+
port: number;
|
|
34
|
+
secure: boolean;
|
|
35
|
+
user: string;
|
|
36
|
+
pass: string;
|
|
37
|
+
from: string;
|
|
38
|
+
}
|
|
39
|
+
/** Result of an SMTP send. */
|
|
40
|
+
export interface SmtpSendResult {
|
|
41
|
+
success: boolean;
|
|
42
|
+
messageId?: string;
|
|
43
|
+
error?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Environment variables consumed by {@link resolveSmtpConfig}. Documented here
|
|
47
|
+
* (and in the README) so the error path can point users at exactly what to set.
|
|
48
|
+
*/
|
|
49
|
+
export declare const SMTP_ENV: {
|
|
50
|
+
readonly host: "APPLE_MAIL_MCP_SMTP_HOST";
|
|
51
|
+
readonly port: "APPLE_MAIL_MCP_SMTP_PORT";
|
|
52
|
+
readonly secure: "APPLE_MAIL_MCP_SMTP_SECURE";
|
|
53
|
+
readonly user: "APPLE_MAIL_MCP_SMTP_USER";
|
|
54
|
+
readonly from: "APPLE_MAIL_MCP_SMTP_FROM";
|
|
55
|
+
readonly password: "APPLE_MAIL_MCP_SMTP_PASSWORD";
|
|
56
|
+
readonly keychainService: "APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE";
|
|
57
|
+
readonly keychainAccount: "APPLE_MAIL_MCP_SMTP_KEYCHAIN_ACCOUNT";
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Reads a password from the macOS login Keychain via the `security` CLI.
|
|
61
|
+
*
|
|
62
|
+
* Tries `find-internet-password` first (where Mail.app stores account
|
|
63
|
+
* passwords) and falls back to `find-generic-password`. Returns null if no
|
|
64
|
+
* matching item exists or the lookup fails for any reason — callers fall back
|
|
65
|
+
* to the password env var and ultimately surface a clear configuration error.
|
|
66
|
+
*
|
|
67
|
+
* @param service - Keychain service / server name (typically the SMTP host)
|
|
68
|
+
* @param account - Keychain account (typically the SMTP username)
|
|
69
|
+
*/
|
|
70
|
+
export declare function readKeychainPassword(service: string, account: string): string | null;
|
|
71
|
+
/**
|
|
72
|
+
* Resolves SMTP connection configuration from environment + Keychain.
|
|
73
|
+
*
|
|
74
|
+
* @throws Error with an actionable message listing the missing settings.
|
|
75
|
+
*/
|
|
76
|
+
export declare function resolveSmtpConfig(env?: NodeJS.ProcessEnv): SmtpConfig;
|
|
77
|
+
/**
|
|
78
|
+
* Sends an email over SMTP, producing clean MIME with no blockquote wrapping.
|
|
79
|
+
*
|
|
80
|
+
* Config is resolved via {@link resolveSmtpConfig} unless one is injected (the
|
|
81
|
+
* `config` parameter exists for testing). The body is sent as plain text; pass
|
|
82
|
+
* a transporter factory only in tests.
|
|
83
|
+
*/
|
|
84
|
+
export declare function sendViaSmtp(opts: SmtpSendOptions, config?: SmtpConfig, createTransport?: typeof nodemailer.createTransport): Promise<SmtpSendResult>;
|
|
85
|
+
//# sourceMappingURL=smtpMailer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smtpMailer.d.ts","sourceRoot":"","sources":["../../src/services/smtpMailer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,UAAU,MAAM,YAAY,CAAC;AAKpC,8EAA8E;AAC9E,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,EAAE,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,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,yCAAyC;IACzC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;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;;;;;;;;;;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;AAmBD;;;;;;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,CAyCzB"}
|
|
@@ -0,0 +1,172 @@
|
|
|
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. AppleScript remains the default transport — SMTP is
|
|
13
|
+
* opt-in per call (`transport: "smtp"`).
|
|
14
|
+
*
|
|
15
|
+
* @module services/smtpMailer
|
|
16
|
+
*/
|
|
17
|
+
import nodemailer from "nodemailer";
|
|
18
|
+
import { execFileSync } from "child_process";
|
|
19
|
+
import { isAbsolute } from "path";
|
|
20
|
+
import { existsSync } from "fs";
|
|
21
|
+
/**
|
|
22
|
+
* Environment variables consumed by {@link resolveSmtpConfig}. Documented here
|
|
23
|
+
* (and in the README) so the error path can point users at exactly what to set.
|
|
24
|
+
*/
|
|
25
|
+
export const SMTP_ENV = {
|
|
26
|
+
host: "APPLE_MAIL_MCP_SMTP_HOST",
|
|
27
|
+
port: "APPLE_MAIL_MCP_SMTP_PORT",
|
|
28
|
+
secure: "APPLE_MAIL_MCP_SMTP_SECURE",
|
|
29
|
+
user: "APPLE_MAIL_MCP_SMTP_USER",
|
|
30
|
+
from: "APPLE_MAIL_MCP_SMTP_FROM",
|
|
31
|
+
password: "APPLE_MAIL_MCP_SMTP_PASSWORD",
|
|
32
|
+
keychainService: "APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE",
|
|
33
|
+
keychainAccount: "APPLE_MAIL_MCP_SMTP_KEYCHAIN_ACCOUNT",
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Reads a password from the macOS login Keychain via the `security` CLI.
|
|
37
|
+
*
|
|
38
|
+
* Tries `find-internet-password` first (where Mail.app stores account
|
|
39
|
+
* passwords) and falls back to `find-generic-password`. Returns null if no
|
|
40
|
+
* matching item exists or the lookup fails for any reason — callers fall back
|
|
41
|
+
* to the password env var and ultimately surface a clear configuration error.
|
|
42
|
+
*
|
|
43
|
+
* @param service - Keychain service / server name (typically the SMTP host)
|
|
44
|
+
* @param account - Keychain account (typically the SMTP username)
|
|
45
|
+
*/
|
|
46
|
+
export function readKeychainPassword(service, account) {
|
|
47
|
+
for (const kind of ["find-internet-password", "find-generic-password"]) {
|
|
48
|
+
try {
|
|
49
|
+
const out = execFileSync("security", [kind, "-s", service, "-a", account, "-w"], {
|
|
50
|
+
encoding: "utf8",
|
|
51
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
52
|
+
});
|
|
53
|
+
const pass = out.replace(/\n$/, "");
|
|
54
|
+
if (pass)
|
|
55
|
+
return pass;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Not found via this kind; try the next.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolves SMTP connection configuration from environment + Keychain.
|
|
65
|
+
*
|
|
66
|
+
* @throws Error with an actionable message listing the missing settings.
|
|
67
|
+
*/
|
|
68
|
+
export function resolveSmtpConfig(env = process.env) {
|
|
69
|
+
const host = env[SMTP_ENV.host]?.trim();
|
|
70
|
+
const user = env[SMTP_ENV.user]?.trim();
|
|
71
|
+
const missing = [];
|
|
72
|
+
if (!host)
|
|
73
|
+
missing.push(SMTP_ENV.host);
|
|
74
|
+
if (!user)
|
|
75
|
+
missing.push(SMTP_ENV.user);
|
|
76
|
+
if (missing.length > 0) {
|
|
77
|
+
throw new Error(`SMTP transport is not configured. Set ${missing.join(" and ")} ` +
|
|
78
|
+
`(plus a password via ${SMTP_ENV.password} or the Keychain). ` +
|
|
79
|
+
`See the README "SMTP transport" section.`);
|
|
80
|
+
}
|
|
81
|
+
// secure=true => implicit TLS (port 465); otherwise STARTTLS (port 587).
|
|
82
|
+
const secure = /^(1|true|yes)$/i.test(env[SMTP_ENV.secure]?.trim() ?? "");
|
|
83
|
+
const port = env[SMTP_ENV.port]
|
|
84
|
+
? Number.parseInt(env[SMTP_ENV.port], 10)
|
|
85
|
+
: secure
|
|
86
|
+
? 465
|
|
87
|
+
: 587;
|
|
88
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
89
|
+
throw new Error(`Invalid ${SMTP_ENV.port}: "${env[SMTP_ENV.port]}" is not a valid port.`);
|
|
90
|
+
}
|
|
91
|
+
const from = env[SMTP_ENV.from]?.trim() || user;
|
|
92
|
+
// Password: explicit env var wins, otherwise Keychain (service/account
|
|
93
|
+
// default to the host/user but can be overridden).
|
|
94
|
+
let pass = env[SMTP_ENV.password];
|
|
95
|
+
if (!pass) {
|
|
96
|
+
const service = env[SMTP_ENV.keychainService]?.trim() || host;
|
|
97
|
+
const account = env[SMTP_ENV.keychainAccount]?.trim() || user;
|
|
98
|
+
pass = readKeychainPassword(service, account) ?? undefined;
|
|
99
|
+
}
|
|
100
|
+
if (!pass) {
|
|
101
|
+
throw new Error(`No SMTP password found. Set ${SMTP_ENV.password}, or store an internet ` +
|
|
102
|
+
`password in the Keychain for service "${env[SMTP_ENV.keychainService]?.trim() || host}" / account "${env[SMTP_ENV.keychainAccount]?.trim() || user}".`);
|
|
103
|
+
}
|
|
104
|
+
return { host: host, port, secure, user: user, pass, from };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Validates attachment paths the same way the AppleScript path does: absolute
|
|
108
|
+
* and existing. Returns nodemailer attachment descriptors.
|
|
109
|
+
*/
|
|
110
|
+
function buildAttachments(attachments) {
|
|
111
|
+
if (!attachments || attachments.length === 0)
|
|
112
|
+
return undefined;
|
|
113
|
+
for (const filePath of attachments) {
|
|
114
|
+
if (!isAbsolute(filePath)) {
|
|
115
|
+
throw new Error(`Attachment path must be absolute: "${filePath}"`);
|
|
116
|
+
}
|
|
117
|
+
if (!existsSync(filePath)) {
|
|
118
|
+
throw new Error(`Attachment file not found: "${filePath}"`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return attachments.map((path) => ({ path }));
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Sends an email over SMTP, producing clean MIME with no blockquote wrapping.
|
|
125
|
+
*
|
|
126
|
+
* Config is resolved via {@link resolveSmtpConfig} unless one is injected (the
|
|
127
|
+
* `config` parameter exists for testing). The body is sent as plain text; pass
|
|
128
|
+
* a transporter factory only in tests.
|
|
129
|
+
*/
|
|
130
|
+
export async function sendViaSmtp(opts, config, createTransport = nodemailer.createTransport) {
|
|
131
|
+
let cfg;
|
|
132
|
+
try {
|
|
133
|
+
cfg = config ?? resolveSmtpConfig();
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
137
|
+
}
|
|
138
|
+
let attachments;
|
|
139
|
+
try {
|
|
140
|
+
attachments = buildAttachments(opts.attachments);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
144
|
+
}
|
|
145
|
+
const transporter = createTransport({
|
|
146
|
+
host: cfg.host,
|
|
147
|
+
port: cfg.port,
|
|
148
|
+
secure: cfg.secure,
|
|
149
|
+
auth: { user: cfg.user, pass: cfg.pass },
|
|
150
|
+
});
|
|
151
|
+
try {
|
|
152
|
+
const info = await transporter.sendMail({
|
|
153
|
+
from: opts.from?.trim() || cfg.from,
|
|
154
|
+
to: opts.to,
|
|
155
|
+
cc: opts.cc,
|
|
156
|
+
bcc: opts.bcc,
|
|
157
|
+
subject: opts.subject,
|
|
158
|
+
text: opts.body,
|
|
159
|
+
attachments,
|
|
160
|
+
});
|
|
161
|
+
return { success: true, messageId: info.messageId };
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
return {
|
|
165
|
+
success: false,
|
|
166
|
+
error: `SMTP send failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
transporter.close();
|
|
171
|
+
}
|
|
172
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-mail-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "MCP server for Apple Mail - read, search, send, and manage emails via Claude",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -59,10 +59,12 @@
|
|
|
59
59
|
],
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
62
|
+
"nodemailer": "^9.0.0",
|
|
62
63
|
"zod": "^3.22.4"
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
65
66
|
"@types/node": "^20.0.0",
|
|
67
|
+
"@types/nodemailer": "^8.0.1",
|
|
66
68
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
67
69
|
"@typescript-eslint/parser": "^8.0.0",
|
|
68
70
|
"@vitest/coverage-v8": "^4.1.9",
|