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 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
- }, 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
+ }
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":"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"}
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
- // 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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "1.5.7",
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",