apple-mail-mcp 1.9.0 → 2.1.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.
Files changed (38) hide show
  1. package/README.md +149 -14
  2. package/build/index.js +421 -228
  3. package/build/services/appleMailManager.d.ts +32 -7
  4. package/build/services/appleMailManager.d.ts.map +1 -1
  5. package/build/services/appleMailManager.js +256 -104
  6. package/build/services/imapClient.d.ts +158 -34
  7. package/build/services/imapClient.d.ts.map +1 -1
  8. package/build/services/imapClient.js +567 -45
  9. package/build/services/imapIdle.d.ts +58 -0
  10. package/build/services/imapIdle.d.ts.map +1 -0
  11. package/build/services/imapIdle.js +145 -0
  12. package/build/services/messageRouter.d.ts +16 -0
  13. package/build/services/messageRouter.d.ts.map +1 -0
  14. package/build/services/messageRouter.js +29 -0
  15. package/build/services/smtpMailer.d.ts +3 -2
  16. package/build/services/smtpMailer.d.ts.map +1 -1
  17. package/build/services/smtpMailer.js +11 -7
  18. package/build/services/templateStore.d.ts +18 -0
  19. package/build/services/templateStore.d.ts.map +1 -0
  20. package/build/services/templateStore.js +91 -0
  21. package/build/tools/doctor.d.ts +23 -0
  22. package/build/tools/doctor.d.ts.map +1 -0
  23. package/build/tools/doctor.js +74 -0
  24. package/build/tools/resourcesAndPrompts.d.ts +14 -0
  25. package/build/tools/resourcesAndPrompts.d.ts.map +1 -0
  26. package/build/tools/resourcesAndPrompts.js +109 -0
  27. package/build/tools/respond.d.ts +48 -0
  28. package/build/tools/respond.d.ts.map +1 -0
  29. package/build/tools/respond.js +95 -0
  30. package/build/tools/thread.d.ts +19 -0
  31. package/build/tools/thread.d.ts.map +1 -0
  32. package/build/tools/thread.js +32 -0
  33. package/build/types.d.ts +38 -0
  34. package/build/types.d.ts.map +1 -1
  35. package/build/utils/attachmentMaterialize.d.ts +9 -0
  36. package/build/utils/attachmentMaterialize.d.ts.map +1 -0
  37. package/build/utils/attachmentMaterialize.js +38 -0
  38. package/package.json +2 -1
package/README.md CHANGED
@@ -86,17 +86,21 @@ On first use, macOS will ask for permission to automate Mail.app. Click "OK" to
86
86
  | **List Messages** | List messages with pagination, sender filter, date display |
87
87
  | **Search Messages** | Search by sender, subject, content, date range, read/flagged status — across all accounts |
88
88
  | **Read Messages** | Get full email content (plain text or HTML) |
89
- | **Send Email** | Compose and send new emails (with optional file attachments) |
89
+ | **Send Email** | Compose and send new emails (attach by file path or inline base64 content) |
90
90
  | **Send Serial Email** | Mail merge — send personalized emails to a list of recipients with {{placeholder}} support |
91
- | **Create Draft** | Save emails to Drafts folder (with optional file attachments) |
91
+ | **Create Draft** | Save emails to Drafts folder (attach by file path or inline base64 content) |
92
92
  | **Reply** | Reply to messages (with reply-all support) |
93
93
  | **Forward** | Forward messages to new recipients |
94
+ | **Get Thread** | Group a conversation by normalized subject (across AppleScript or IMAP) |
94
95
  | **Mark Read/Unread** | Change read status (single or batch) |
95
96
  | **Flag/Unflag** | Flag or unflag messages (single or batch) |
96
97
  | **Delete Messages** | Move messages to trash (single or batch) |
97
98
  | **Move Messages** | Organize into mailboxes (single or batch) |
98
99
  | **List Attachments** | View attachment metadata (name, type, size) |
99
100
  | **Save Attachment** | Save attachments to disk |
101
+ | **Fetch Attachment** | Get an attachment's bytes as base64 (no disk write) |
102
+
103
+ Read/list/get tools also return **structured JSON** (`structuredContent`) alongside the text, so agents can consume results without parsing prose.
100
104
 
101
105
  ### Mailbox & Account Management
102
106
 
@@ -113,17 +117,25 @@ On first use, macOS will ask for permission to automate Mail.app. Click "OK" to
113
117
  |---------|-------------|
114
118
  | **List Rules** | View all mail rules and their enabled status |
115
119
  | **Enable/Disable Rules** | Toggle mail rules on or off |
120
+ | **Create/Delete Rules** | Create rules with conditions + actions, or delete by name |
116
121
  | **Search Contacts** | Look up contacts from Contacts.app by name |
117
- | **Email Templates** | Save, list, use, and delete reusable email templates |
122
+ | **Email Templates** | Save, list, use, and delete reusable email templates (persisted to disk across restarts) |
118
123
 
119
124
  ### Diagnostics
120
125
 
121
126
  | Feature | Description |
122
127
  |---------|-------------|
123
128
  | **Health Check** | Verify Mail.app connectivity |
129
+ | **Doctor** | Diagnose Mail permission, account state, and each IMAP/SMTP backend with actionable messages |
124
130
  | **Statistics** | Message and unread counts per account, recently received stats |
125
131
  | **Sync Status** | Check if Mail.app is actively syncing |
126
132
 
133
+ ### MCP resources & prompts
134
+
135
+ Resources expose read-only context the client can attach without a tool call:
136
+ `mail://accounts`, `mail://templates`, and `mail://mailboxes/{account}`. Prompts
137
+ package common workflows: `triage-inbox`, `compose-reply`, `weekly-summary`.
138
+
127
139
  ---
128
140
 
129
141
  ## Tool Reference
@@ -214,7 +226,7 @@ Send a new email immediately.
214
226
  | `cc` | string[] | No | CC recipients |
215
227
  | `bcc` | string[] | No | BCC recipients |
216
228
  | `account` | string | No | Send from specific account (with `transport: "smtp"`, overrides the From address) |
217
- | `attachments` | string[] | No | Absolute file paths to attach, max 20 files (e.g., `["/Users/me/report.pdf"]`) |
229
+ | `attachments` | (string \| {filename, contentBase64})[] | No | Up to 20 attachments: absolute file paths (e.g., `"/Users/me/report.pdf"`) and/or inline `{filename, contentBase64}` objects for content not on disk |
218
230
  | `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) |
219
231
 
220
232
  **Example:**
@@ -287,14 +299,17 @@ What routes to IMAP when an account is IMAP-configured:
287
299
  - **Read:** `search-messages`, `list-messages` (server-side `SEARCH`, typically sub-second), and `get-message`.
288
300
  - **Folder ops:** `create-mailbox`, `rename-mailbox`, `delete-mailbox` — IMAP's `CREATE`/`RENAME`/`DELETE` succeed on the iCloud/Gmail/Workspace/Exchange mailboxes Mail.app's AppleScript bridge can't touch (#42).
289
301
  - **Message mutations:** `mark-as-read`/`unread`, `flag-message`/`unflag-message`, `move-message`, `delete-message`.
302
+ - **Batch mutations (2.1):** `batch-mark-as-read`/`unread`, `batch-flag`/`unflag-messages`, `batch-move-messages`, `batch-delete-messages` — `imap:` ids are grouped by mailbox and applied as a single `UID STORE`/`UID MOVE`; numeric ids in the same batch still use AppleScript.
303
+ - **Counts & stats (2.1):** `get-unread-count` and `list-mailboxes` use `STATUS`; `get-mail-stats` (with an `account`) uses `STATUS` + `SEARCH SINCE` — authoritative and fast even on huge mailboxes.
304
+ - **Attachments (2.1):** `list-attachments`, `save-attachment`, `fetch-attachment` use `BODYSTRUCTURE` + `FETCH BODY[part]` for `imap:` ids — faster and able to see MIME-embedded attachments AppleScript misses.
305
+ - **Threading (2.1):** `get-thread` links a conversation via `References`/`Message-ID` (`HEADER SEARCH`) for an `imap:` seed, falling back to subject grouping otherwise.
290
306
 
291
307
  **Message ids are backend-tagged.** The IMAP read path emits self-describing ids
292
308
  of the form `imap:<token>` (the token encodes the account, mailbox path, and
293
- UID). Pass that id back to `get-message` or any message mutation and it routes to
294
- IMAP automatically; bare numeric ids continue to use AppleScript. So an agent
295
- never has to know which backend a message came from the id carries it. (Batch
296
- operations remain AppleScript-only and accept numeric ids; use the single-message
297
- tools for IMAP ids.)
309
+ UID). Pass that id back to `get-message`, a message mutation, a batch op, or the
310
+ attachment/thread tools and it routes to IMAP automatically; bare numeric ids
311
+ continue to use AppleScript. So an agent never has to know which backend a
312
+ message came from the id carries it.
298
313
 
299
314
  Routing is conservative: only a call whose explicit `account` matches the
300
315
  configured IMAP account goes to IMAP; everything else falls through to
@@ -309,21 +324,72 @@ AppleScript.
309
324
  | `APPLE_MAIL_MCP_IMAP_PASSWORD` | No | — | Password (if set, used instead of the Keychain) |
310
325
  | `APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE` | No | — | Keychain item service/server name |
311
326
  | `APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT` | No | = user | Keychain item account |
327
+ | `APPLE_MAIL_MCP_IMAP_ACCOUNTS` | No | — | JSON array of **additional** IMAP accounts for multi-account setups (see below) |
328
+ | `APPLE_MAIL_MCP_IMAP_IDLE` | No | `0` | Set `1` to enable IMAP IDLE push notifications (new-mail alerts) for every configured account |
329
+ | `APPLE_MAIL_MCP_IMAP_IDLE_MS` | No | `60000` | Idle timeout (ms) before a pooled IMAP connection is closed |
330
+
331
+ **Multiple IMAP accounts (C2):** set `APPLE_MAIL_MCP_IMAP_ACCOUNTS` to a JSON array, e.g.
332
+ `[{"account":"Work","user":"me@co.com","host":"imap.co.com","keychainService":"imap.co.com"}]`.
333
+ Each entry accepts `account`, `user`, `host`, `port`, `password`, `keychainService`,
334
+ `keychainAccount`. Calls route to the account matching their `account` argument (or the
335
+ decoded `imap:` id), and each account keeps its own pooled connection.
312
336
 
313
337
  As with SMTP, the password is read from the macOS **Keychain** by default (use
314
338
  an app-specific password for Gmail/Workspace/iCloud), so no secret goes in
315
339
  config. Gmail label semantics: common names (`All Mail`, `Sent`, `Trash`,
316
340
  `Spam`, `Important`, …) map to their `[Gmail]/…` IMAP paths automatically.
317
341
 
318
- > Note: each call currently opens its own IMAP connection (no pooling yet), so
319
- > expect a few seconds of connection overhead per call — the one remaining
320
- > follow-up on [#43](https://github.com/sweetrb/apple-mail-mcp/issues/43).
342
+ > Note: IMAP connections are pooled one kept-alive connection per account is
343
+ > reused across calls (verified with a NOOP, closed after `APPLE_MAIL_MCP_IMAP_IDLE_MS`
344
+ > of inactivity), so there's no per-call connection overhead ([#50](https://github.com/sweetrb/apple-mail-mcp/issues/50)).
321
345
  >
322
346
  > **iCloud:** set `APPLE_MAIL_MCP_IMAP_HOST=imap.mail.me.com`, `APPLE_MAIL_MCP_IMAP_USER`
323
347
  > to your iCloud address, `APPLE_MAIL_MCP_IMAP_ACCOUNT` to the Mail account name
324
348
  > (e.g. `iCloud`), and use an **app-specific password** (from appleid.apple.com)
325
349
  > stored in the Keychain.
326
350
 
351
+ ##### Push notifications (IMAP IDLE) — opt-in
352
+
353
+ When `APPLE_MAIL_MCP_IMAP_IDLE=1`, the server opens a dedicated, long-lived
354
+ connection to **each configured IMAP account** and watches its INBOX for new
355
+ mail. On arrival it pushes two MCP notifications to the client (no polling by the
356
+ client required):
357
+
358
+ 1. **`notifications/message`** (logging) — a human-readable line, e.g.
359
+ `New mail in "Work": 2 new message(s) (INBOX now 1843).`
360
+ 2. **`notifications/resources/updated`** — for the affected account's resource
361
+ `mail://mailboxes/{account}`, so a client subscribed to that resource knows to
362
+ re-read it.
363
+
364
+ This requires an IMAP account to be configured (single-account env or
365
+ `APPLE_MAIL_MCP_IMAP_ACCOUNTS`); accounts that only use AppleScript aren't
366
+ watched. Detection is **real-time** via the IMAP IDLE `EXISTS` event where the
367
+ server pushes it, with an automatic **polling fallback** for servers that don't.
368
+ Dropped connections reconnect with backoff, and the watchers shut down cleanly on
369
+ `SIGINT`/`SIGTERM`.
370
+
371
+ Enable it in your MCP client config alongside the IMAP settings:
372
+
373
+ ```jsonc
374
+ {
375
+ "mcpServers": {
376
+ "apple-mail": {
377
+ "command": "node",
378
+ "args": ["/path/to/apple-mail-mcp/build/index.js"],
379
+ "env": {
380
+ "APPLE_MAIL_MCP_IMAP_USER": "you@gmail.com",
381
+ "APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE": "imap.gmail.com",
382
+ "APPLE_MAIL_MCP_IMAP_IDLE": "1"
383
+ }
384
+ }
385
+ }
386
+ }
387
+ ```
388
+
389
+ > Note: this is most useful with clients that surface MCP logging messages or
390
+ > subscribe to resource-update notifications. Clients that ignore notifications
391
+ > are unaffected — the feature is opt-in and adds no behavior unless enabled.
392
+
327
393
  ---
328
394
 
329
395
  #### `send-serial-email`
@@ -373,10 +439,34 @@ Save an email to Drafts without sending.
373
439
  | `cc` | string[] | No | CC recipients |
374
440
  | `bcc` | string[] | No | BCC recipients |
375
441
  | `account` | string | No | Account for draft |
376
- | `attachments` | string[] | No | Absolute file paths to attach, max 20 files |
442
+ | `attachments` | (string \| {filename, contentBase64})[] | No | Up to 20 attachments: absolute file paths and/or inline `{filename, contentBase64}` objects |
377
443
 
378
444
  **Returns:** Confirmation that draft was created.
379
445
 
446
+ #### `get-thread`
447
+
448
+ Group a conversation by normalized subject (across the AppleScript or IMAP backend).
449
+
450
+ | Parameter | Type | Required | Description |
451
+ |-----------|------|----------|-------------|
452
+ | `id` | string | Yes | A message ID in the conversation (numeric or `imap:…`) |
453
+ | `account` | string | No | Account to search (omit to search all) |
454
+ | `mailbox` | string | No | Mailbox to search (omit to search all) |
455
+ | `limit` | number | No | Max messages in the thread (default 50) |
456
+
457
+ **Returns:** The conversation's messages, oldest-first.
458
+
459
+ #### `fetch-attachment`
460
+
461
+ Return an attachment's bytes as base64 (the read counterpart to inline-base64 send).
462
+
463
+ | Parameter | Type | Required | Description |
464
+ |-----------|------|----------|-------------|
465
+ | `id` | string | Yes | Numeric message ID |
466
+ | `attachmentName` | string | Yes | Attachment filename (from `list-attachments`) |
467
+
468
+ **Returns:** The attachment bytes, base64-encoded (also in `structuredContent.contentBase64`).
469
+
380
470
  ---
381
471
 
382
472
  #### `reply-to-message`
@@ -614,6 +704,41 @@ Enable or disable a mail rule.
614
704
 
615
705
  ---
616
706
 
707
+ #### `create-rule`
708
+
709
+ Create a Mail rule with one or more conditions and actions.
710
+
711
+ | Parameter | Type | Required | Description |
712
+ |-----------|------|----------|-------------|
713
+ | `name` | string | Yes | Rule name (must be unique) |
714
+ | `conditions` | object[] | Yes | One or more `{field, operator, value}` (see below) |
715
+ | `actions` | object | Yes | At least one of `markRead`, `markFlagged`, `delete`, `moveTo` |
716
+ | `matchAll` | boolean | No | `true` (default) = all conditions must match; `false` = any |
717
+ | `enabled` | boolean | No | Whether the rule is enabled on creation (default `true`) |
718
+
719
+ Each condition is `{ field, operator, value }` where `field` is one of `from`, `to`, `cc`, `subject`, `content` and `operator` is one of `contains`, `notContains`, `equals`, `beginsWith`, `endsWith`. Actions: `markRead` / `markFlagged` / `delete` (booleans), `moveTo` (mailbox name) with optional `moveToAccount`.
720
+
721
+ **Example:**
722
+ ```json
723
+ {
724
+ "name": "Newsletters",
725
+ "conditions": [{ "field": "from", "operator": "contains", "value": "newsletter" }],
726
+ "actions": { "markRead": true, "moveTo": "Reading" }
727
+ }
728
+ ```
729
+
730
+ ---
731
+
732
+ #### `delete-rule`
733
+
734
+ Delete a mail rule by name.
735
+
736
+ | Parameter | Type | Required | Description |
737
+ |-----------|------|----------|-------------|
738
+ | `name` | string | Yes | Rule name |
739
+
740
+ ---
741
+
617
742
  ### Contacts
618
743
 
619
744
  #### `search-contacts`
@@ -631,7 +756,7 @@ Search contacts in Contacts.app.
631
756
 
632
757
  ### Templates
633
758
 
634
- Email templates are stored in memory for the duration of the server session.
759
+ Email templates are **persisted to disk** so they survive server restarts, stored as JSON at `APPLE_MAIL_MCP_TEMPLATES_FILE` (default `~/Library/Application Support/apple-mail-mcp/templates.json`).
635
760
 
636
761
  #### `save-template`
637
762
 
@@ -702,6 +827,16 @@ Verify Mail.app connectivity and permissions.
702
827
 
703
828
  ---
704
829
 
830
+ #### `doctor`
831
+
832
+ Run a full setup diagnostic: Mail.app automation permission, account state (flagging disabled accounts), and each configured IMAP/SMTP backend — each reported as ok / warn / fail with an actionable message.
833
+
834
+ **Parameters:** None
835
+
836
+ **Returns:** A per-check report (`structuredContent` carries the raw `{healthy, checks[]}`).
837
+
838
+ ---
839
+
705
840
  #### `get-mail-stats`
706
841
 
707
842
  Get mail statistics.