apple-mail-mcp 1.5.6 → 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 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,8 @@ 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";
28
+ import { createSerialGate } from "./utils/serialize.js";
27
29
  // =============================================================================
28
30
  // Shared Validation Schemas
29
31
  // =============================================================================
@@ -84,17 +86,32 @@ function errorResponse(message) {
84
86
  };
85
87
  }
86
88
  /**
87
- * Wraps a tool handler with consistent error handling.
89
+ * Serial execution gate for AppleScript-backed tool calls (issue #11).
90
+ *
91
+ * Concurrent MCP tool calls are funneled through this single gate so only one
92
+ * osascript invocation hits Mail.app's single-threaded AppleScript dispatch at
93
+ * a time, with a short settle delay between calls so the dispatch queue drains.
94
+ * Without it, a concurrent batch races into Mail.app, the later calls blow past
95
+ * their timeouts, and Mail.app is left half-recovered for the next batch.
96
+ */
97
+ const serializeAppleScript = createSerialGate();
98
+ /**
99
+ * Wraps a tool handler with consistent error handling, serialized through the
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.
88
103
  */
89
104
  function withErrorHandling(handler, errorPrefix) {
90
105
  return async (params) => {
91
- try {
92
- return handler(params);
93
- }
94
- catch (error) {
95
- const message = error instanceof Error ? error.message : "Unknown error";
96
- return errorResponse(`${errorPrefix}: ${message}`);
97
- }
106
+ return serializeAppleScript(async () => {
107
+ try {
108
+ return await handler(params);
109
+ }
110
+ catch (error) {
111
+ const message = error instanceof Error ? error.message : "Unknown error";
112
+ return errorResponse(`${errorPrefix}: ${message}`);
113
+ }
114
+ });
98
115
  };
99
116
  }
100
117
  // =============================================================================
@@ -176,12 +193,25 @@ server.tool("send-email", {
176
193
  .max(20, "Cannot attach more than 20 files")
177
194
  .optional()
178
195
  .describe("Absolute file paths to attach (e.g., ['/Users/me/report.pdf'])"),
179
- }, withErrorHandling(({ to, subject, body, cc, bcc, account, attachments }) => {
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
+ }
180
211
  const success = mailManager.sendEmail(to, subject, body, cc, bcc, account, attachments);
181
212
  if (!success) {
182
213
  return errorResponse("Failed to send email. Check Mail.app configuration.");
183
214
  }
184
- const attachInfo = attachments?.length ? ` with ${attachments.length} attachment(s)` : "";
185
215
  return successResponse(`Email sent to ${to.join(", ")}${attachInfo}`);
186
216
  }, "Error sending email"));
187
217
  // --- send-serial-email ---
@@ -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":"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;IAuBH,SAAS,CACP,EAAE,EAAE,MAAM,EAAE,EACZ,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,EAAE,CAAC,EAAE,MAAM,EAAE,EACb,GAAG,CAAC,EAAE,MAAM,EAAE,EACd,OAAO,CAAC,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,MAAM,EAAE,GACrB,OAAO;IA0DV;;;;;;;;;;;;OAYG;IACH,eAAe,CACb,UAAU,EAAE,oBAAoB,EAAE,EAClC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,GAAE,MAAY,GACpB,iBAAiB,EAAE;IAgDtB;;;;;;;;;;OAUG;IACH,WAAW,CACT,EAAE,EAAE,MAAM,EAAE,EACZ,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,EAAE,CAAC,EAAE,MAAM,EAAE,EACb,GAAG,CAAC,EAAE,MAAM,EAAE,EACd,OAAO,CAAC,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,MAAM,EAAE,GACrB,OAAO;IAwDV;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,UAAQ,EAAE,IAAI,UAAO,GAAG,OAAO;IAqChF;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,UAAO,GAAG,OAAO;IA2C7E;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAsBzB;;OAEG;IACH,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAY/B;;OAEG;IACH,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAYjC;;OAEG;IACH,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAYhC;;OAEG;IACH,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAYlC;;OAEG;IACH,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAYlC;;OAEG;IACH;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,mBAAmB;IAwD3B,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO;IAYnE;;;;;OAKG;IACH,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,oBAAoB,EAAE;IAe1D;;;;;;;OAOG;IACH,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,oBAAoB,EAAE;IAe3F;;OAEG;IACH,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,oBAAoB,EAAE;IAStD;;OAEG;IACH,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,oBAAoB,EAAE;IAaxD;;OAEG;IACH,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,oBAAoB,EAAE;IASxD;;OAEG;IACH,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,oBAAoB,EAAE;IAS1D;;;;OAIG;IACH,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,EAAE;IAiEzC;;;;OAIG;IACH,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO;IAsF7E;;OAEG;IACH,aAAa,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,EAAE;IA2C1C;;OAEG;IACH,cAAc,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM;IA8B1D;;OAEG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO;IAyBtD;;OAEG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO;IA0BtD;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO;IA4C1E;;OAEG;IACH,YAAY,IAAI,OAAO,EAAE;IAIzB;;;OAGG;IACH,OAAO,CAAC,aAAa;IA2CrB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAwBzB;;OAEG;IACH,SAAS,IAAI,QAAQ,EAAE;IAiCvB;;OAEG;IACH,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO;IA+B3D;;OAEG;IACH,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,EAAE;IA4DxC,OAAO,CAAC,SAAS,CAAyC;IAC1D,OAAO,CAAC,cAAc,CAAK;IAE3B;;OAEG;IACH,aAAa,IAAI,aAAa,EAAE;IAIhC;;OAEG;IACH,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI;IAI7C;;OAEG;IACH,YAAY,CACV,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,EAAE,CAAC,EAAE,MAAM,EAAE,EACb,EAAE,CAAC,EAAE,MAAM,EAAE,EACb,EAAE,CAAC,EAAE,MAAM,GACV,aAAa;IAOhB;;OAEG;IACH,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAInC;;OAEG;IACH,WAAW,CACT,EAAE,EAAE,MAAM,EACV,SAAS,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAC5E,OAAO;IAkBV;;OAEG;IACH,WAAW,IAAI,iBAAiB;IA8EhC;;OAEG;IACH,YAAY,IAAI,SAAS;IA4CzB;;;;;;;OAOG;IACH,wBAAwB,IAAI,qBAAqB;IAyEjD;;;;;;;;;OASG;IACH,aAAa,IAAI,UAAU;CA+D5B"}
@@ -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
- // Workaround for callers who need clean emails today: use SMTP directly
793
- // (Python smtplib). See e.g. sweetrb/nhl-bracket-tracker's send_email.py.
794
- //
795
- // Proper fix is probably to abandon `make new outgoing message` and either:
796
- // 1. Build the .emlx file ourselves and drop it into a Drafts mailbox.
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
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"applescript.d.ts","sourceRoot":"","sources":["../../src/utils/applescript.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAuOxE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,kBAAuB,GAC/B,iBAAiB,CA6HnB"}
1
+ {"version":3,"file":"applescript.d.ts","sourceRoot":"","sources":["../../src/utils/applescript.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAmQxE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,kBAAuB,GAC/B,iBAAiB,CAoInB"}
@@ -65,6 +65,32 @@ function escapeForShell(script) {
65
65
  // This is the standard shell escaping pattern for single-quoted strings
66
66
  return script.replace(/'/g, "'\\''");
67
67
  }
68
+ /**
69
+ * Headroom (ms) between the in-AppleScript `with timeout` and the outer
70
+ * osascript process timeout. The script-level timeout must fire *first* so
71
+ * Mail.app aborts the operation from inside its own AppleScript dispatch —
72
+ * cleanly releasing the event queue — before Node SIGKILLs the osascript
73
+ * process. Killing osascript alone does not stop work already dispatched into
74
+ * Mail.app, which is what wedges Mail.app for subsequent calls (issue #11).
75
+ */
76
+ const SCRIPT_TIMEOUT_HEADROOM_MS = 5000;
77
+ /**
78
+ * Wraps a script body in an AppleScript `with timeout` block so any Apple
79
+ * Event that honors timeouts (most Mail.app operations do) aborts cleanly
80
+ * rather than holding Mail.app's single-threaded AppleScript dispatch queue
81
+ * open indefinitely.
82
+ *
83
+ * The timeout is set a few seconds below the osascript process timeout so the
84
+ * in-Mail abort wins the race against the outer process kill.
85
+ *
86
+ * @param script - The AppleScript body
87
+ * @param processTimeoutMs - The outer osascript process timeout
88
+ * @returns The script wrapped in `with timeout … end timeout`
89
+ */
90
+ function wrapWithTimeout(script, processTimeoutMs) {
91
+ const seconds = Math.max(1, Math.ceil((processTimeoutMs - SCRIPT_TIMEOUT_HEADROOM_MS) / 1000));
92
+ return `with timeout of ${seconds} seconds\n${script}\nend timeout`;
93
+ }
68
94
  /**
69
95
  * Checks if an error is a timeout error from execSync.
70
96
  *
@@ -270,8 +296,10 @@ export function executeAppleScript(script, options = {}) {
270
296
  // Prepare the script:
271
297
  // 1. Trim leading/trailing whitespace (cosmetic)
272
298
  // 2. Preserve internal newlines (required for AppleScript syntax)
273
- // 3. Escape for shell execution
274
- const preparedScript = escapeForShell(script.trim());
299
+ // 3. Wrap in `with timeout` so a stuck Mail.app operation aborts from
300
+ // inside its own dispatch before the process timeout kills osascript (#11)
301
+ // 4. Escape for shell execution
302
+ const preparedScript = escapeForShell(wrapWithTimeout(script.trim(), timeoutMs));
275
303
  // Build the osascript command
276
304
  // We use single quotes to wrap the script, which is why we escape
277
305
  // single quotes within the script itself
@@ -292,6 +320,11 @@ export function executeAppleScript(script, options = {}) {
292
320
  const output = execSync(command, {
293
321
  encoding: "utf8",
294
322
  timeout: timeoutMs,
323
+ // SIGKILL (not the default SIGTERM): a wedged osascript blocked on an
324
+ // unresponsive Mail.app can ignore SIGTERM, leaking processes that pile
325
+ // up and worsen the contention. SIGKILL guarantees the process is reaped
326
+ // when the timeout fires. (#11)
327
+ killSignal: "SIGKILL",
295
328
  // Capture stderr separately to get error details
296
329
  stdio: ["pipe", "pipe", "pipe"],
297
330
  });
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Serial execution gate.
3
+ *
4
+ * Mail.app's AppleScript handler is effectively single-threaded: concurrent
5
+ * `tell application "Mail"` calls queue inside Mail.app, and once enough stack
6
+ * up the later ones blow past their timeouts while earlier ones are still
7
+ * draining — the client sees a cascade of "Request timed out" errors and
8
+ * Mail.app is left half-recovered for the next batch (issue #11).
9
+ *
10
+ * A gate chains every task through a single promise so only one runs at a time,
11
+ * with an optional settle delay between tasks so Mail.app's dispatch queue can
12
+ * drain. Because tasks are chained on promises, awaiting the gate also yields
13
+ * the event loop between calls, keeping the server responsive to protocol
14
+ * traffic (pings, health-check) during a batch.
15
+ *
16
+ * @module utils/serialize
17
+ */
18
+ /** A function that serializes a task behind all previously enqueued tasks. */
19
+ export type SerialGate = <R>(task: () => R | PromiseLike<R>) => Promise<R>;
20
+ /**
21
+ * Creates a serial execution gate.
22
+ *
23
+ * @param settleMs - Delay inserted after each task before the next one starts
24
+ * (default 50ms). Set to 0 to disable.
25
+ * @returns A `serialize(task)` function. Tasks run strictly in enqueue order and
26
+ * never overlap; each call resolves/rejects with its task's own result, and a
27
+ * failing task never breaks serialization for later tasks.
28
+ */
29
+ export declare function createSerialGate(settleMs?: number): SerialGate;
30
+ //# sourceMappingURL=serialize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../../src/utils/serialize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,8EAA8E;AAC9E,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;AAE3E;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,SAAK,GAAG,UAAU,CAa1D"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Serial execution gate.
3
+ *
4
+ * Mail.app's AppleScript handler is effectively single-threaded: concurrent
5
+ * `tell application "Mail"` calls queue inside Mail.app, and once enough stack
6
+ * up the later ones blow past their timeouts while earlier ones are still
7
+ * draining — the client sees a cascade of "Request timed out" errors and
8
+ * Mail.app is left half-recovered for the next batch (issue #11).
9
+ *
10
+ * A gate chains every task through a single promise so only one runs at a time,
11
+ * with an optional settle delay between tasks so Mail.app's dispatch queue can
12
+ * drain. Because tasks are chained on promises, awaiting the gate also yields
13
+ * the event loop between calls, keeping the server responsive to protocol
14
+ * traffic (pings, health-check) during a batch.
15
+ *
16
+ * @module utils/serialize
17
+ */
18
+ /**
19
+ * Creates a serial execution gate.
20
+ *
21
+ * @param settleMs - Delay inserted after each task before the next one starts
22
+ * (default 50ms). Set to 0 to disable.
23
+ * @returns A `serialize(task)` function. Tasks run strictly in enqueue order and
24
+ * never overlap; each call resolves/rejects with its task's own result, and a
25
+ * failing task never breaks serialization for later tasks.
26
+ */
27
+ export function createSerialGate(settleMs = 50) {
28
+ let tail = Promise.resolve();
29
+ return (task) => {
30
+ const result = tail.then(task);
31
+ // Keep the chain alive regardless of this task's outcome, with a settle
32
+ // delay so the next task doesn't race straight back into Mail.app.
33
+ tail = result.then(() => settle(settleMs), () => settle(settleMs));
34
+ return result;
35
+ };
36
+ }
37
+ function settle(ms) {
38
+ if (ms <= 0)
39
+ return Promise.resolve();
40
+ return new Promise((resolve) => setTimeout(resolve, ms));
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "1.5.6",
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",