apple-mail-mcp 1.5.7 → 1.6.1

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
@@ -149,6 +149,27 @@ Search for messages matching criteria. Searches all accounts by default.
149
149
  | `dateTo` | string | No | End date filter (e.g., "March 1, 2026") |
150
150
  | `limit` | number | No | Max results (default: 50) |
151
151
 
152
+ **Large mailboxes & partial results.** Apple Mail's AppleScript bridge cannot
153
+ search very large IMAP/Gmail mailboxes (tens of thousands of messages) before
154
+ the Apple Event times out — empirically even reading the newest 20 messages of
155
+ a 44k-message mailbox takes ~45s. To avoid burning minutes only to return a
156
+ misleading empty result, an unscoped (all-mailboxes) search **skips** mailboxes
157
+ whose message count exceeds a threshold (default **5000**), enforces a
158
+ per-account time budget, and **reports** anything it skipped or that timed out
159
+ rather than silently returning nothing. When coverage is incomplete the result
160
+ includes an explicit warning, e.g.:
161
+
162
+ ```
163
+ ⚠️ Partial results — this is NOT a confirmed "no such mail":
164
+ - skipped mailbox(es) too large to search via AppleScript: Gmail / All Mail (44287) — scope the search with `mailbox` + a `dateFrom`/`dateTo` window to target them
165
+ ```
166
+
167
+ To search inside a large mailbox, scope the call with `mailbox` (and ideally a
168
+ `dateFrom`/`dateTo` window). Tune or disable the skip threshold with the
169
+ `APPLE_MAIL_MAX_SEARCH_MAILBOX` environment variable (default `5000`; set to `0`
170
+ to disable the guard and attempt every mailbox regardless of size).
171
+ ([#24](https://github.com/sweetrb/apple-mail-mcp/issues/24))
172
+
152
173
  ---
153
174
 
154
175
  #### `get-message`
@@ -192,8 +213,9 @@ Send a new email immediately.
192
213
  | `body` | string | Yes | Email body (plain text) |
193
214
  | `cc` | string[] | No | CC recipients |
194
215
  | `bcc` | string[] | No | BCC recipients |
195
- | `account` | string | No | Send from specific account |
216
+ | `account` | string | No | Send from specific account (with `transport: "smtp"`, overrides the From address) |
196
217
  | `attachments` | string[] | No | Absolute file paths to attach, max 20 files (e.g., `["/Users/me/report.pdf"]`) |
218
+ | `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
219
 
198
220
  **Example:**
199
221
  ```json
@@ -206,6 +228,48 @@ Send a new email immediately.
206
228
  }
207
229
  ```
208
230
 
231
+ ##### SMTP transport
232
+
233
+ On macOS 15+ (Sequoia/Tahoe), Mail.app wraps any AppleScript-injected body in
234
+ `<blockquote type="cite">` under the `Apple-Mail-URLShareWrapperClass` template,
235
+ so emails sent through the default `applescript` transport render to recipients
236
+ as if they were quoted/forwarded (Apple radar **FB11734014**, open since
237
+ Ventura). Passing `transport: "smtp"` bypasses Mail.app entirely and submits
238
+ clean MIME via SMTP.
239
+
240
+ Configure SMTP via environment variables on the MCP server. The password is
241
+ read from the macOS **Keychain** by default, so no secret goes in config:
242
+
243
+ | Variable | Required | Default | Description |
244
+ |----------|----------|---------|-------------|
245
+ | `APPLE_MAIL_MCP_SMTP_HOST` | Yes | — | SMTP server hostname (e.g. `smtp.fastmail.com`) |
246
+ | `APPLE_MAIL_MCP_SMTP_USER` | Yes | — | SMTP username |
247
+ | `APPLE_MAIL_MCP_SMTP_PORT` | No | `465` if secure, else `587` | SMTP port |
248
+ | `APPLE_MAIL_MCP_SMTP_SECURE` | No | `false` | `true` for implicit TLS (port 465); otherwise STARTTLS |
249
+ | `APPLE_MAIL_MCP_SMTP_FROM` | No | = user | From address |
250
+ | `APPLE_MAIL_MCP_SMTP_PASSWORD` | No | — | Password (if set, used instead of the Keychain) |
251
+ | `APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE` | No | = host | Keychain item service/server name |
252
+ | `APPLE_MAIL_MCP_SMTP_KEYCHAIN_ACCOUNT` | No | = user | Keychain item account |
253
+
254
+ Store the password in the Keychain once (an app-specific password for Gmail/
255
+ iCloud), e.g.:
256
+
257
+ ```bash
258
+ security add-internet-password -s smtp.fastmail.com -a you@example.com -w
259
+ ```
260
+
261
+ Then send:
262
+ ```json
263
+ {
264
+ "to": ["colleague@company.com"],
265
+ "subject": "Standings",
266
+ "body": "Plain body — no blockquote wrapping.",
267
+ "transport": "smtp"
268
+ }
269
+ ```
270
+
271
+ The default `applescript` transport is unchanged; SMTP is opt-in per call.
272
+
209
273
  ---
210
274
 
211
275
  #### `send-serial-email`
@@ -735,6 +799,7 @@ The entrypoint is written as:
735
799
  | No sending HTML email | Emails are sent as plain text; reading HTML content is supported |
736
800
  | Attachments require absolute paths | File attachments must use full absolute paths (e.g., `/Users/me/file.pdf`) |
737
801
  | No smart mailboxes | Cannot access Smart Mailboxes via AppleScript |
802
+ | Very large mailboxes not searchable | Apple Mail's AppleScript bridge times out on mailboxes with tens of thousands of messages, so unscoped `search-messages` skips mailboxes above `APPLE_MAIL_MAX_SEARCH_MAILBOX` (default 5000) and reports them as a partial result. Scope with `mailbox` + a date window to search inside one. ([#24](https://github.com/sweetrb/apple-mail-mcp/issues/24)) |
738
803
  | In-memory templates | Email templates are not persisted across server restarts |
739
804
  | Numeric-only message IDs | Message IDs must contain only digits (validated by schema) |
740
805
  | Batch size cap | Batch operations are limited to 100 messages per request |
@@ -742,6 +807,17 @@ The entrypoint is written as:
742
807
  | Attachment save path restrictions | `save-attachment` only allows saving to home directory, `/tmp`, `/private/tmp`, and `/Volumes`; path traversal is blocked |
743
808
  | Attachment count limit | `send-email` and `create-draft` accept a maximum of 20 file attachments |
744
809
 
810
+ ### Mail.app `<blockquote>` wrapping on macOS 15+ (workaround in v1.6.0)
811
+
812
+ On macOS 15+ Mail.app wraps AppleScript-injected message bodies in
813
+ `<blockquote type="cite">` under the `Apple-Mail-URLShareWrapperClass` template,
814
+ so mail sent via the default `applescript` transport renders to recipients as
815
+ quoted/forwarded content (Apple radar **FB11734014**, open since Ventura, no
816
+ fix). Since v1.6.0, `send-email` accepts `transport: "smtp"` to bypass Mail.app
817
+ and send clean MIME directly — see [SMTP transport](#smtp-transport). The
818
+ AppleScript path is still the default and still exhibits Apple's wrapping.
819
+ ([#12](https://github.com/sweetrb/apple-mail-mcp/issues/12))
820
+
745
821
  ### Reply / Forward from Background Processes (Fixed in v1.4.0)
746
822
 
747
823
  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).
@@ -795,6 +871,12 @@ The `\\\\` in JSON becomes `\\` in the actual string, which represents a single
795
871
  - Message IDs change if the message is moved between mailboxes
796
872
  - Use `search-messages` to find the current message ID
797
873
 
874
+ ### `search-messages` says "Partial results" or skips a mailbox
875
+ - This is expected for very large IMAP/Gmail mailboxes (e.g. Gmail's `All Mail`, `Important`): Apple Mail can't scan them via AppleScript before timing out, so they're skipped and named in the result rather than silently returning empty.
876
+ - To search inside one, scope the call with `mailbox` **and** a `dateFrom`/`dateTo` window.
877
+ - Raise or disable the threshold with `APPLE_MAIL_MAX_SEARCH_MAILBOX` (default `5000`; `0` disables the guard) — note that disabling it can make a single search take minutes.
878
+ - A `Partial results` warning means coverage was incomplete; it is **not** a confirmed "no such mail."
879
+
798
880
  ### "Account not found"
799
881
  - Account names must match exactly (case-sensitive)
800
882
  - Use `list-accounts` to see exact account names
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";
@@ -133,14 +136,35 @@ server.tool("search-messages", {
133
136
  dateTo: DATE_FILTER_SCHEMA.describe("End date filter (e.g., 'March 1, 2026')"),
134
137
  limit: z.number().optional().describe("Maximum number of results (default: 50)"),
135
138
  }, withErrorHandling(({ query, mailbox, account, limit = 50, dateFrom, dateTo, from, subject, isRead, isFlagged, }) => {
136
- const messages = mailManager.searchMessages(query, mailbox, account, limit, dateFrom, dateTo, from, subject, isRead, isFlagged);
139
+ const { messages, diagnostics } = mailManager.searchMessagesWithDiagnostics(query, mailbox, account, limit, dateFrom, dateTo, from, subject, isRead, isFlagged);
140
+ // Build a coverage note when the search couldn't reach everything, so a
141
+ // caller never mistakes an incomplete search for a confirmed "no matches"
142
+ // (issue #24).
143
+ const coverageNotes = [];
144
+ if (diagnostics.timedOutAccounts.length > 0) {
145
+ coverageNotes.push(`timed out (no results) for account(s): ${diagnostics.timedOutAccounts.join(", ")}`);
146
+ }
147
+ if (diagnostics.skippedLargeMailboxes.length > 0) {
148
+ coverageNotes.push(`skipped mailbox(es) too large to search via AppleScript: ${diagnostics.skippedLargeMailboxes.join(", ")} — scope the search with \`mailbox\` + a \`dateFrom\`/\`dateTo\` window to target them`);
149
+ }
150
+ if (diagnostics.notSearchedMailboxes.length > 0) {
151
+ coverageNotes.push(`could not finish searching mailbox(es): ${diagnostics.notSearchedMailboxes.join(", ")}`);
152
+ }
153
+ const coverageBlock = coverageNotes.length > 0
154
+ ? `\n\n⚠️ Partial results — this is NOT a confirmed "no such mail":\n${coverageNotes
155
+ .map((n) => ` - ${n}`)
156
+ .join("\n")}`
157
+ : "";
137
158
  if (messages.length === 0) {
138
- return successResponse("No messages found matching criteria");
159
+ const base = diagnostics.partial
160
+ ? "No messages found in the portions that were searched."
161
+ : "No messages found matching criteria";
162
+ return successResponse(`${base}${coverageBlock}`);
139
163
  }
140
164
  const messageList = messages
141
165
  .map((m) => ` - ID: ${m.id} | ${m.dateReceived.toLocaleDateString()} | ${m.subject} (from: ${m.sender}) [${m.isRead ? "read" : "unread"}]`)
142
166
  .join("\n");
143
- return successResponse(`Found ${messages.length} message(s):\n${messageList}`);
167
+ return successResponse(`Found ${messages.length} message(s):\n${messageList}${coverageBlock}`);
144
168
  }, "Error searching messages"));
145
169
  // --- get-message ---
146
170
  server.tool("get-message", {
@@ -190,12 +214,25 @@ server.tool("send-email", {
190
214
  .max(20, "Cannot attach more than 20 files")
191
215
  .optional()
192
216
  .describe("Absolute file paths to attach (e.g., ['/Users/me/report.pdf'])"),
193
- }, withErrorHandling(({ to, subject, body, cc, bcc, account, attachments }) => {
217
+ transport: z
218
+ .enum(["applescript", "smtp"])
219
+ .optional()
220
+ .describe("Send transport. 'applescript' (default) sends through Mail.app. " +
221
+ "'smtp' submits clean MIME directly via SMTP, avoiding the macOS 15+ " +
222
+ "Mail.app <blockquote> wrapping (issue #12); requires APPLE_MAIL_MCP_SMTP_* env config."),
223
+ }, withErrorHandling(async ({ to, subject, body, cc, bcc, account, attachments, transport }) => {
224
+ const attachInfo = attachments?.length ? ` with ${attachments.length} attachment(s)` : "";
225
+ if (transport === "smtp") {
226
+ const result = await sendViaSmtp({ to, subject, body, cc, bcc, from: account, attachments });
227
+ if (!result.success) {
228
+ return errorResponse(result.error ?? "Failed to send email via SMTP.");
229
+ }
230
+ return successResponse(`Email sent via SMTP to ${to.join(", ")}${attachInfo}`);
231
+ }
194
232
  const success = mailManager.sendEmail(to, subject, body, cc, bcc, account, attachments);
195
233
  if (!success) {
196
234
  return errorResponse("Failed to send email. Check Mail.app configuration.");
197
235
  }
198
- const attachInfo = attachments?.length ? ` with ${attachments.length} attachment(s)` : "";
199
236
  return successResponse(`Email sent to ${to.join(", ")}${attachInfo}`);
200
237
  }, "Error sending email"));
201
238
  // --- send-serial-email ---
@@ -12,7 +12,29 @@
12
12
  *
13
13
  * @module services/appleMailManager
14
14
  */
15
- import type { Message, MessageContent, Mailbox, Account, Attachment, HealthCheckResult, MailStats, BatchOperationResult, SyncStatus, RecentlyReceivedStats, MailRule, Contact, EmailTemplate, SerialEmailRecipient, SerialEmailResult } from "../types.js";
15
+ import type { Message, MessageContent, Mailbox, Account, Attachment, HealthCheckResult, MailStats, BatchOperationResult, SyncStatus, RecentlyReceivedStats, MailRule, Contact, EmailTemplate, SerialEmailRecipient, SerialEmailResult, SearchDiagnostics, SearchResult } from "../types.js";
16
+ /**
17
+ * Merge a per-account SearchDiagnostics into an aggregate (all-accounts) one.
18
+ *
19
+ * Exported for unit testing.
20
+ */
21
+ export declare function mergeSearchDiagnostics(into: SearchDiagnostics, from: SearchDiagnostics): void;
22
+ /**
23
+ * Split a per-account search payload into its message-list portion and parsed
24
+ * diagnostics. The AppleScript appends a trailer of the form:
25
+ *
26
+ * <messages>|||DIAG|||timedOut=true|||F|||skipped=Foo (9000)|||M||||||F|||notSearched=Bar|||M|||
27
+ *
28
+ * `skipped`/`notSearched` are `|||M|||`-separated mailbox names, each prefixed
29
+ * with the account name on the way out so the aggregate result is unambiguous.
30
+ *
31
+ * Exported (pure, no Mail.app dependency) for unit testing — this is the logic
32
+ * that turns a swallowed timeout into a visible partial result (issue #24).
33
+ */
34
+ export declare function splitSearchDiagnostics(output: string, account: string): {
35
+ payload: string;
36
+ diagnostics: SearchDiagnostics;
37
+ };
16
38
  /**
17
39
  * Emits AppleScript that builds a date into the variable `varName` from numeric
18
40
  * components.
@@ -127,6 +149,30 @@ export declare class AppleMailManager {
127
149
  * @returns Array of matching messages
128
150
  */
129
151
  searchMessages(query?: string, mailbox?: string, account?: string, limit?: number, dateFrom?: string, dateTo?: string, from?: string, subject?: string, isRead?: boolean, isFlagged?: boolean): Message[];
152
+ /**
153
+ * Search for messages, returning both the matches and diagnostics describing
154
+ * how complete the search was.
155
+ *
156
+ * This is the correctness fix for issue #24. The previous implementation ran
157
+ * an unbounded `messages of mb whose <predicate>` over every mailbox in an
158
+ * account; on large IMAP/Gmail mailboxes (tens of thousands of messages) that
159
+ * single Apple Event exceeded the timeout, the error was swallowed by a `try`,
160
+ * and the function returned a clean — but wrong — empty result. Callers/agents
161
+ * then confidently reported "no such mail."
162
+ *
163
+ * Two changes fix that:
164
+ * 1. Cheap count-guard: mailboxes larger than the scan threshold are skipped
165
+ * (Apple Mail can't search them before timing out anyway) and reported.
166
+ * 2. Honest diagnostics: per-account/per-mailbox timeouts are surfaced as a
167
+ * `partial` result with the affected scopes named, instead of an empty
168
+ * "success."
169
+ */
170
+ searchMessagesWithDiagnostics(query?: string, mailbox?: string, account?: string, limit?: number, dateFrom?: string, dateTo?: string, from?: string, subject?: string, isRead?: boolean, isFlagged?: boolean): SearchResult;
171
+ /**
172
+ * Split a per-account search payload into its message list and the DIAG
173
+ * trailer, parse both, and return a SearchResult. See searchMessagesWithDiagnostics.
174
+ */
175
+ private parseSearchResult;
130
176
  /**
131
177
  * Get a message by ID.
132
178
  *
@@ -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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"}
@@ -19,6 +19,94 @@ import { homedir } from "os";
19
19
  import { executeAppleScript } from "../utils/applescript.js";
20
20
  import { parseMimeAttachments, extractMimeAttachment } from "../utils/mimeParse.js";
21
21
  // =============================================================================
22
+ // Search Tuning (issue #24)
23
+ // =============================================================================
24
+ /**
25
+ * Mailboxes larger than this are skipped during an unscoped (all-mailboxes)
26
+ * search rather than scanned. Apple Mail's AppleScript bridge cannot search a
27
+ * mailbox of this size before the Apple Event timeout fires — empirically even
28
+ * reading the newest 20 messages of a 44k-message Gmail mailbox took ~47s — so
29
+ * attempting it only burns the time budget and yields a misleading empty
30
+ * result. `count of messages` is cheap (Mail keeps it cached), so the guard is
31
+ * effectively free. The skipped mailboxes are reported back to the caller.
32
+ *
33
+ * Override with APPLE_MAIL_MAX_SEARCH_MAILBOX (set to 0 to disable the guard).
34
+ */
35
+ function getMailboxScanThreshold() {
36
+ const raw = process.env.APPLE_MAIL_MAX_SEARCH_MAILBOX;
37
+ if (raw !== undefined) {
38
+ const n = Number(raw);
39
+ if (Number.isFinite(n) && n >= 0)
40
+ return n;
41
+ }
42
+ return 5000;
43
+ }
44
+ /**
45
+ * Per-account wall-clock budget (seconds) enforced *inside* the AppleScript so
46
+ * a single account can't consume minutes. Kept comfortably below the osascript
47
+ * process timeout (see searchMessages) so the script exits and reports a
48
+ * partial result rather than being SIGKILLed with no diagnostics.
49
+ */
50
+ const SEARCH_ACCOUNT_BUDGET_SECONDS = 30;
51
+ /** osascript process timeout for a per-account search (ms). */
52
+ const SEARCH_ACCOUNT_TIMEOUT_MS = 45000;
53
+ /** Separator between the message payload and the diagnostics trailer. */
54
+ const DIAG_MARKER = "|||DIAG|||";
55
+ /**
56
+ * Merge a per-account SearchDiagnostics into an aggregate (all-accounts) one.
57
+ *
58
+ * Exported for unit testing.
59
+ */
60
+ export function mergeSearchDiagnostics(into, from) {
61
+ into.timedOutAccounts.push(...from.timedOutAccounts);
62
+ into.skippedLargeMailboxes.push(...from.skippedLargeMailboxes);
63
+ into.notSearchedMailboxes.push(...from.notSearchedMailboxes);
64
+ if (from.partial)
65
+ into.partial = true;
66
+ }
67
+ /**
68
+ * Split a per-account search payload into its message-list portion and parsed
69
+ * diagnostics. The AppleScript appends a trailer of the form:
70
+ *
71
+ * <messages>|||DIAG|||timedOut=true|||F|||skipped=Foo (9000)|||M||||||F|||notSearched=Bar|||M|||
72
+ *
73
+ * `skipped`/`notSearched` are `|||M|||`-separated mailbox names, each prefixed
74
+ * with the account name on the way out so the aggregate result is unambiguous.
75
+ *
76
+ * Exported (pure, no Mail.app dependency) for unit testing — this is the logic
77
+ * that turns a swallowed timeout into a visible partial result (issue #24).
78
+ */
79
+ export function splitSearchDiagnostics(output, account) {
80
+ const markerIdx = output.lastIndexOf(DIAG_MARKER);
81
+ const payload = markerIdx >= 0 ? output.slice(0, markerIdx) : output;
82
+ const trailer = markerIdx >= 0 ? output.slice(markerIdx + DIAG_MARKER.length) : "";
83
+ const diagnostics = {
84
+ partial: false,
85
+ timedOutAccounts: [],
86
+ skippedLargeMailboxes: [],
87
+ notSearchedMailboxes: [],
88
+ };
89
+ if (trailer) {
90
+ const fields = trailer.split("|||F|||");
91
+ const getField = (key) => {
92
+ const f = fields.find((x) => x.startsWith(`${key}=`));
93
+ return f ? f.slice(key.length + 1) : "";
94
+ };
95
+ const splitList = (raw) => raw
96
+ .split("|||M|||")
97
+ .map((s) => s.trim())
98
+ .filter((s) => s.length > 0);
99
+ diagnostics.skippedLargeMailboxes = splitList(getField("skipped")).map((mb) => `${account} / ${mb}`);
100
+ diagnostics.notSearchedMailboxes = splitList(getField("notSearched")).map((mb) => `${account} / ${mb}`);
101
+ if (getField("timedOut") === "true")
102
+ diagnostics.partial = true;
103
+ }
104
+ if (diagnostics.skippedLargeMailboxes.length > 0 || diagnostics.notSearchedMailboxes.length > 0) {
105
+ diagnostics.partial = true;
106
+ }
107
+ return { payload, diagnostics };
108
+ }
109
+ // =============================================================================
22
110
  // Text Processing Utilities
23
111
  // =============================================================================
24
112
  /**
@@ -336,18 +424,46 @@ export class AppleMailManager {
336
424
  * @returns Array of matching messages
337
425
  */
338
426
  searchMessages(query, mailbox, account, limit = 50, dateFrom, dateTo, from, subject, isRead, isFlagged) {
339
- // If no account specified, search across all accounts
427
+ return this.searchMessagesWithDiagnostics(query, mailbox, account, limit, dateFrom, dateTo, from, subject, isRead, isFlagged).messages;
428
+ }
429
+ /**
430
+ * Search for messages, returning both the matches and diagnostics describing
431
+ * how complete the search was.
432
+ *
433
+ * This is the correctness fix for issue #24. The previous implementation ran
434
+ * an unbounded `messages of mb whose <predicate>` over every mailbox in an
435
+ * account; on large IMAP/Gmail mailboxes (tens of thousands of messages) that
436
+ * single Apple Event exceeded the timeout, the error was swallowed by a `try`,
437
+ * and the function returned a clean — but wrong — empty result. Callers/agents
438
+ * then confidently reported "no such mail."
439
+ *
440
+ * Two changes fix that:
441
+ * 1. Cheap count-guard: mailboxes larger than the scan threshold are skipped
442
+ * (Apple Mail can't search them before timing out anyway) and reported.
443
+ * 2. Honest diagnostics: per-account/per-mailbox timeouts are surfaced as a
444
+ * `partial` result with the affected scopes named, instead of an empty
445
+ * "success."
446
+ */
447
+ searchMessagesWithDiagnostics(query, mailbox, account, limit = 50, dateFrom, dateTo, from, subject, isRead, isFlagged) {
448
+ // If no account specified, search across all accounts and merge diagnostics.
340
449
  if (!account) {
341
450
  const accounts = this.listAccounts();
342
451
  const allMessages = [];
452
+ const diagnostics = {
453
+ partial: false,
454
+ timedOutAccounts: [],
455
+ skippedLargeMailboxes: [],
456
+ notSearchedMailboxes: [],
457
+ };
343
458
  for (const acct of accounts) {
344
459
  if (allMessages.length >= limit)
345
460
  break;
346
461
  const remaining = limit - allMessages.length;
347
- const msgs = this.searchMessages(query, mailbox, acct.name, remaining, dateFrom, dateTo, from, subject, isRead, isFlagged);
348
- allMessages.push(...msgs);
462
+ const res = this.searchMessagesWithDiagnostics(query, mailbox, acct.name, remaining, dateFrom, dateTo, from, subject, isRead, isFlagged);
463
+ allMessages.push(...res.messages);
464
+ mergeSearchDiagnostics(diagnostics, res.diagnostics);
349
465
  }
350
- return allMessages.slice(0, limit);
466
+ return { messages: allMessages.slice(0, limit), diagnostics };
351
467
  }
352
468
  const targetAccount = this.resolveAccount(account);
353
469
  // `query` is a subject-OR-sender substring match; from/subject/isRead/isFlagged
@@ -379,79 +495,136 @@ export class AppleMailManager {
379
495
  }
380
496
  dateFilter = dateChecks.join(" and ");
381
497
  }
498
+ const scanThreshold = getMailboxScanThreshold();
382
499
  let searchCommand;
383
500
  if (mailbox) {
384
- // Search a specific mailbox
501
+ // Search a specific mailbox. The caller explicitly chose this mailbox, so
502
+ // we don't apply the count-guard skip — but we still wrap the scan so a
503
+ // timeout is reported as a partial result rather than a false empty.
385
504
  const targetMailbox = this.resolveMailbox(mailbox, targetAccount);
386
505
  searchCommand = `
387
506
  ${dateSetup}set outputText to ""
507
+ set _timedOut to false
508
+ set _notSearched to ""
388
509
  set theMailbox to mailbox "${escapeForAppleScript(targetMailbox)}"
389
- set allMessages to messages of theMailbox ${searchCondition}
390
510
  set msgCount to 0
391
- repeat with msg in allMessages
392
- if msgCount >= ${limit} then exit repeat
393
- try
394
- ${dateFilter ? `set msgDate to date received of msg\n if not (${dateFilter}) then\n -- skip message outside date range\n else` : ""}
395
- set msgId to id of msg as string
396
- set msgSubject to subject of msg
397
- set msgSender to sender of msg
398
- set d to date received of msg
399
- set msgDateStr to ${AS_DATE_TO_STRING}
400
- set msgRead to read status of msg as string
401
- set msgFlagged to flagged status of msg as string
402
- if msgCount > 0 then set outputText to outputText & "|||ITEM|||"
403
- set outputText to outputText & msgId & "|||" & msgSubject & "|||" & msgSender & "|||" & msgDateStr & "|||" & msgRead & "|||" & msgFlagged
404
- set msgCount to msgCount + 1
405
- ${dateFilter ? "end if" : ""}
406
- end try
407
- end repeat
408
- return outputText
511
+ try
512
+ set allMessages to messages of theMailbox ${searchCondition}
513
+ repeat with msg in allMessages
514
+ if msgCount >= ${limit} then exit repeat
515
+ try
516
+ ${dateFilter ? `set msgDate to date received of msg\n if not (${dateFilter}) then\n -- skip message outside date range\n else` : ""}
517
+ set msgId to id of msg as string
518
+ set msgSubject to subject of msg
519
+ set msgSender to sender of msg
520
+ set d to date received of msg
521
+ set msgDateStr to ${AS_DATE_TO_STRING}
522
+ set msgRead to read status of msg as string
523
+ set msgFlagged to flagged status of msg as string
524
+ if msgCount > 0 then set outputText to outputText & "|||ITEM|||"
525
+ set outputText to outputText & msgId & "|||" & msgSubject & "|||" & msgSender & "|||" & msgDateStr & "|||" & msgRead & "|||" & msgFlagged
526
+ set msgCount to msgCount + 1
527
+ ${dateFilter ? "end if" : ""}
528
+ end try
529
+ end repeat
530
+ on error _errMsg number _errNum
531
+ set _timedOut to true
532
+ set _notSearched to "${escapeForAppleScript(targetMailbox)}|||M|||"
533
+ end try
534
+ return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "|||F|||skipped=|||F|||notSearched=" & _notSearched
409
535
  `;
410
536
  }
411
537
  else {
412
- // Search ALL mailboxes — iterate every mailbox in the account, dedup by message ID
538
+ // Search ALL mailboxes — iterate every mailbox in the account, dedup by
539
+ // message ID. Skip mailboxes that exceed the scan threshold (they can't be
540
+ // searched before timing out), enforce a per-account wall-clock budget, and
541
+ // capture per-mailbox timeouts. All three are reported via the DIAG trailer.
542
+ const scanGuard = scanThreshold > 0 ? `mbCount > ${scanThreshold}` : "false";
413
543
  searchCommand = `
414
544
  ${dateSetup}set outputText to ""
415
545
  set msgCount to 0
416
546
  set seenIds to {}
547
+ set _timedOut to false
548
+ set _skipped to ""
549
+ set _notSearched to ""
550
+ set _startedAt to current date
417
551
  repeat with mb in mailboxes
418
552
  if msgCount >= ${limit} then exit repeat
553
+ set mbName to ""
419
554
  try
420
- set allMessages to messages of mb ${searchCondition}
421
- repeat with msg in allMessages
422
- if msgCount >= ${limit} then exit repeat
555
+ set mbName to name of mb
556
+ end try
557
+ if ((current date) - _startedAt) > ${SEARCH_ACCOUNT_BUDGET_SECONDS} then
558
+ set _timedOut to true
559
+ set _notSearched to _notSearched & mbName & "|||M|||"
560
+ else
561
+ set mbCount to 0
562
+ try
563
+ set mbCount to count of messages of mb
564
+ end try
565
+ if (${scanGuard}) then
566
+ set _timedOut to true
567
+ set _skipped to _skipped & mbName & " (" & (mbCount as string) & ")|||M|||"
568
+ else
423
569
  try
424
- set msgId to id of msg as string
425
- if seenIds does not contain msgId then
426
- set end of seenIds to msgId
427
- ${dateFilter ? `set msgDate to date received of msg\n if not (${dateFilter}) then\n -- skip message outside date range\n else` : ""}
428
- set msgSubject to subject of msg
429
- set msgSender to sender of msg
430
- set d to date received of msg
431
- set msgDateStr to ${AS_DATE_TO_STRING}
432
- set msgRead to read status of msg as string
433
- set msgFlagged to flagged status of msg as string
434
- if msgCount > 0 then set outputText to outputText & "|||ITEM|||"
435
- set outputText to outputText & msgId & "|||" & msgSubject & "|||" & msgSender & "|||" & msgDateStr & "|||" & msgRead & "|||" & msgFlagged & "|||" & name of mb
436
- set msgCount to msgCount + 1
437
- ${dateFilter ? "end if" : ""}
438
- end if
570
+ set allMessages to messages of mb ${searchCondition}
571
+ repeat with msg in allMessages
572
+ if msgCount >= ${limit} then exit repeat
573
+ try
574
+ set msgId to id of msg as string
575
+ if seenIds does not contain msgId then
576
+ set end of seenIds to msgId
577
+ ${dateFilter ? `set msgDate to date received of msg\n if not (${dateFilter}) then\n -- skip message outside date range\n else` : ""}
578
+ set msgSubject to subject of msg
579
+ set msgSender to sender of msg
580
+ set d to date received of msg
581
+ set msgDateStr to ${AS_DATE_TO_STRING}
582
+ set msgRead to read status of msg as string
583
+ set msgFlagged to flagged status of msg as string
584
+ if msgCount > 0 then set outputText to outputText & "|||ITEM|||"
585
+ set outputText to outputText & msgId & "|||" & msgSubject & "|||" & msgSender & "|||" & msgDateStr & "|||" & msgRead & "|||" & msgFlagged & "|||" & mbName
586
+ set msgCount to msgCount + 1
587
+ ${dateFilter ? "end if" : ""}
588
+ end if
589
+ end try
590
+ end repeat
591
+ on error _errMsg number _errNum
592
+ set _timedOut to true
593
+ set _notSearched to _notSearched & mbName & "|||M|||"
439
594
  end try
440
- end repeat
441
- end try
595
+ end if
596
+ end if
442
597
  end repeat
443
- return outputText
598
+ return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "|||F|||skipped=" & _skipped & "|||F|||notSearched=" & _notSearched
444
599
  `;
445
600
  }
446
601
  const script = buildAccountScopedScript(targetAccount, searchCommand);
447
- const result = executeAppleScript(script, { timeoutMs: 60000 });
602
+ const result = executeAppleScript(script, { timeoutMs: SEARCH_ACCOUNT_TIMEOUT_MS });
448
603
  if (!result.success) {
449
- console.error(`Failed to search messages: ${result.error}`);
450
- return [];
604
+ // Whole-account script failed (most often the osascript process timeout /
605
+ // SIGKILL on an unresponsive account). Surface it as a timeout rather than
606
+ // a false empty result — that confusion is the heart of issue #24.
607
+ console.error(`Failed to search messages in "${targetAccount}": ${result.error}`);
608
+ return {
609
+ messages: [],
610
+ diagnostics: {
611
+ partial: true,
612
+ timedOutAccounts: [targetAccount],
613
+ skippedLargeMailboxes: [],
614
+ notSearchedMailboxes: [],
615
+ },
616
+ };
451
617
  }
452
- if (!result.output.trim())
453
- return [];
454
- return this.parseMessageList(result.output, mailbox || "INBOX", targetAccount);
618
+ return this.parseSearchResult(result.output, mailbox || "INBOX", targetAccount);
619
+ }
620
+ /**
621
+ * Split a per-account search payload into its message list and the DIAG
622
+ * trailer, parse both, and return a SearchResult. See searchMessagesWithDiagnostics.
623
+ */
624
+ parseSearchResult(output, mailbox, account) {
625
+ const { payload, diagnostics } = splitSearchDiagnostics(output, account);
626
+ const messages = payload.trim() ? this.parseMessageList(payload, mailbox, account) : [];
627
+ return { messages, diagnostics };
455
628
  }
456
629
  /**
457
630
  * Get a message by ID.
@@ -789,13 +962,11 @@ export class AppleMailManager {
789
962
  // Discussion: https://forums.macrumors.com/threads/applescript-creating-a-
790
963
  // new-message-in-mail-app-is-causing-weird-formatting-issues.2385052/
791
964
  //
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.
965
+ // FIX (v1.6.0): send-email now accepts `transport: "smtp"`, which bypasses
966
+ // Mail.app and submits clean MIME directly via nodemailer (creds from the
967
+ // Keychain). See src/services/smtpMailer.ts. This AppleScript path remains
968
+ // the default for back-compat and for users who don't configure SMTP, so the
969
+ // wrapping behavior below is unchanged for them.
799
970
  // Tracking issue: https://github.com/sweetrb/apple-mail-mcp/issues/12
800
971
  // ───────────────────────────────────────────────────────────────────
801
972
  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/build/types.d.ts CHANGED
@@ -47,6 +47,39 @@ export interface Message {
47
47
  /** Whether the message has attachments */
48
48
  hasAttachments: boolean;
49
49
  }
50
+ /**
51
+ * Per-account / per-mailbox diagnostics for a search, so callers can tell a
52
+ * genuine "no matches" apart from "we couldn't finish searching."
53
+ *
54
+ * Apple Mail's AppleScript bridge is pathologically slow on large IMAP/Gmail
55
+ * mailboxes (tens of thousands of messages): a single `whose` predicate — or
56
+ * even reading the newest 20 messages by index — can blow past the Apple Event
57
+ * timeout. Before this was tracked, those timeouts were swallowed and the
58
+ * search returned a clean (but wrong) empty result. See issue #24.
59
+ */
60
+ export interface SearchDiagnostics {
61
+ /** True if any account/mailbox could not be fully searched (timeout or skip). */
62
+ partial: boolean;
63
+ /** Accounts that timed out entirely (whole-account AppleScript was killed). */
64
+ timedOutAccounts: string[];
65
+ /**
66
+ * Mailboxes skipped because their message count exceeded the scan threshold,
67
+ * formatted as "Account / Mailbox (count)".
68
+ */
69
+ skippedLargeMailboxes: string[];
70
+ /**
71
+ * Mailboxes that were reached but timed out or errored mid-scan, formatted as
72
+ * "Account / Mailbox".
73
+ */
74
+ notSearchedMailboxes: string[];
75
+ }
76
+ /**
77
+ * A search result paired with diagnostics about coverage.
78
+ */
79
+ export interface SearchResult {
80
+ messages: Message[];
81
+ diagnostics: SearchDiagnostics;
82
+ }
50
83
  /**
51
84
  * Represents the content of an email message.
52
85
  */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
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.1",
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",