apple-mail-mcp 2.0.0 → 2.1.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 +122 -24
- package/build/index.js +145 -43
- package/build/services/fileConfig.d.ts +7 -0
- package/build/services/fileConfig.d.ts.map +1 -0
- package/build/services/fileConfig.js +52 -0
- package/build/services/imapClient.d.ts +108 -0
- package/build/services/imapClient.d.ts.map +1 -1
- package/build/services/imapClient.js +291 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -299,14 +299,17 @@ What routes to IMAP when an account is IMAP-configured:
|
|
|
299
299
|
- **Read:** `search-messages`, `list-messages` (server-side `SEARCH`, typically sub-second), and `get-message`.
|
|
300
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).
|
|
301
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.
|
|
302
306
|
|
|
303
307
|
**Message ids are backend-tagged.** The IMAP read path emits self-describing ids
|
|
304
308
|
of the form `imap:<token>` (the token encodes the account, mailbox path, and
|
|
305
|
-
UID). Pass that id back to `get-message
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
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.
|
|
310
313
|
|
|
311
314
|
Routing is conservative: only a call whose explicit `account` matches the
|
|
312
315
|
configured IMAP account goes to IMAP; everything else falls through to
|
|
@@ -331,25 +334,85 @@ Each entry accepts `account`, `user`, `host`, `port`, `password`, `keychainServi
|
|
|
331
334
|
`keychainAccount`. Calls route to the account matching their `account` argument (or the
|
|
332
335
|
decoded `imap:` id), and each account keeps its own pooled connection.
|
|
333
336
|
|
|
334
|
-
**Push notifications (B5):** with `APPLE_MAIL_MCP_IMAP_IDLE=1`, the server watches each
|
|
335
|
-
account's INBOX and emits an MCP logging message + `notifications/resources/updated` for
|
|
336
|
-
`mail://mailboxes/{account}` when new mail arrives (real-time IDLE where the server
|
|
337
|
-
supports it, polling fallback otherwise).
|
|
338
|
-
|
|
339
337
|
As with SMTP, the password is read from the macOS **Keychain** by default (use
|
|
340
338
|
an app-specific password for Gmail/Workspace/iCloud), so no secret goes in
|
|
341
339
|
config. Gmail label semantics: common names (`All Mail`, `Sent`, `Trash`,
|
|
342
340
|
`Spam`, `Important`, …) map to their `[Gmail]/…` IMAP paths automatically.
|
|
343
341
|
|
|
344
|
-
> Note:
|
|
345
|
-
>
|
|
346
|
-
>
|
|
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)).
|
|
347
345
|
>
|
|
348
346
|
> **iCloud:** set `APPLE_MAIL_MCP_IMAP_HOST=imap.mail.me.com`, `APPLE_MAIL_MCP_IMAP_USER`
|
|
349
347
|
> to your iCloud address, `APPLE_MAIL_MCP_IMAP_ACCOUNT` to the Mail account name
|
|
350
348
|
> (e.g. `iCloud`), and use an **app-specific password** (from appleid.apple.com)
|
|
351
349
|
> stored in the Keychain.
|
|
352
350
|
|
|
351
|
+
##### Configuration file (when the host strips `env`)
|
|
352
|
+
|
|
353
|
+
Some host apps (e.g. Claude Desktop) launch the MCP server with a scrubbed
|
|
354
|
+
environment and ignore the `env` block in their server config, so there's no way
|
|
355
|
+
to pass `APPLE_MAIL_MCP_*` settings through it. In that case, put them in a JSON
|
|
356
|
+
file the host doesn't manage — `APPLE_MAIL_MCP_CONFIG_FILE`, or by default
|
|
357
|
+
`~/Library/Application Support/apple-mail-mcp/config.json`:
|
|
358
|
+
|
|
359
|
+
```json
|
|
360
|
+
{
|
|
361
|
+
"APPLE_MAIL_MCP_IMAP_USER": "you@gmail.com",
|
|
362
|
+
"APPLE_MAIL_MCP_IMAP_HOST": "imap.gmail.com",
|
|
363
|
+
"APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE": "imap.gmail.com",
|
|
364
|
+
"APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT": "you@gmail.com",
|
|
365
|
+
"APPLE_MAIL_MCP_IMAP_IDLE": "1"
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
The server reads it at startup and merges values into the environment **without
|
|
370
|
+
overriding** anything already set there (so an explicit `env` still wins). Store
|
|
371
|
+
only non-secret config here — **passwords belong in the Keychain**, never in this
|
|
372
|
+
file.
|
|
373
|
+
|
|
374
|
+
##### Push notifications (IMAP IDLE) — opt-in
|
|
375
|
+
|
|
376
|
+
When `APPLE_MAIL_MCP_IMAP_IDLE=1`, the server opens a dedicated, long-lived
|
|
377
|
+
connection to **each configured IMAP account** and watches its INBOX for new
|
|
378
|
+
mail. On arrival it pushes two MCP notifications to the client (no polling by the
|
|
379
|
+
client required):
|
|
380
|
+
|
|
381
|
+
1. **`notifications/message`** (logging) — a human-readable line, e.g.
|
|
382
|
+
`New mail in "Work": 2 new message(s) (INBOX now 1843).`
|
|
383
|
+
2. **`notifications/resources/updated`** — for the affected account's resource
|
|
384
|
+
`mail://mailboxes/{account}`, so a client subscribed to that resource knows to
|
|
385
|
+
re-read it.
|
|
386
|
+
|
|
387
|
+
This requires an IMAP account to be configured (single-account env or
|
|
388
|
+
`APPLE_MAIL_MCP_IMAP_ACCOUNTS`); accounts that only use AppleScript aren't
|
|
389
|
+
watched. Detection is **real-time** via the IMAP IDLE `EXISTS` event where the
|
|
390
|
+
server pushes it, with an automatic **polling fallback** for servers that don't.
|
|
391
|
+
Dropped connections reconnect with backoff, and the watchers shut down cleanly on
|
|
392
|
+
`SIGINT`/`SIGTERM`.
|
|
393
|
+
|
|
394
|
+
Enable it in your MCP client config alongside the IMAP settings:
|
|
395
|
+
|
|
396
|
+
```jsonc
|
|
397
|
+
{
|
|
398
|
+
"mcpServers": {
|
|
399
|
+
"apple-mail": {
|
|
400
|
+
"command": "node",
|
|
401
|
+
"args": ["/path/to/apple-mail-mcp/build/index.js"],
|
|
402
|
+
"env": {
|
|
403
|
+
"APPLE_MAIL_MCP_IMAP_USER": "you@gmail.com",
|
|
404
|
+
"APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE": "imap.gmail.com",
|
|
405
|
+
"APPLE_MAIL_MCP_IMAP_IDLE": "1"
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
> Note: this is most useful with clients that surface MCP logging messages or
|
|
413
|
+
> subscribe to resource-update notifications. Clients that ignore notifications
|
|
414
|
+
> are unaffected — the feature is opt-in and adds no behavior unless enabled.
|
|
415
|
+
|
|
353
416
|
---
|
|
354
417
|
|
|
355
418
|
#### `send-serial-email`
|
|
@@ -427,16 +490,6 @@ Return an attachment's bytes as base64 (the read counterpart to inline-base64 se
|
|
|
427
490
|
|
|
428
491
|
**Returns:** The attachment bytes, base64-encoded (also in `structuredContent.contentBase64`).
|
|
429
492
|
|
|
430
|
-
#### `create-rule` / `delete-rule`
|
|
431
|
-
|
|
432
|
-
Create a Mail rule with conditions and actions, or delete a rule by name.
|
|
433
|
-
|
|
434
|
-
`create-rule` parameters: `name` (string), `conditions` (array of `{field: from|to|cc|subject|content, operator: contains|notContains|equals|beginsWith|endsWith, value}`), `actions` (`{markRead?, markFlagged?, delete?, moveTo?, moveToAccount?}`), `matchAll` (default true), `enabled` (default true). `delete-rule` parameters: `name`.
|
|
435
|
-
|
|
436
|
-
#### `doctor`
|
|
437
|
-
|
|
438
|
-
Run a full setup diagnostic: Mail.app automation permission, account state (flags disabled accounts), and each configured IMAP/SMTP backend, each reported as ok / warn / fail with an actionable message. No parameters.
|
|
439
|
-
|
|
440
493
|
---
|
|
441
494
|
|
|
442
495
|
#### `reply-to-message`
|
|
@@ -674,6 +727,41 @@ Enable or disable a mail rule.
|
|
|
674
727
|
|
|
675
728
|
---
|
|
676
729
|
|
|
730
|
+
#### `create-rule`
|
|
731
|
+
|
|
732
|
+
Create a Mail rule with one or more conditions and actions.
|
|
733
|
+
|
|
734
|
+
| Parameter | Type | Required | Description |
|
|
735
|
+
|-----------|------|----------|-------------|
|
|
736
|
+
| `name` | string | Yes | Rule name (must be unique) |
|
|
737
|
+
| `conditions` | object[] | Yes | One or more `{field, operator, value}` (see below) |
|
|
738
|
+
| `actions` | object | Yes | At least one of `markRead`, `markFlagged`, `delete`, `moveTo` |
|
|
739
|
+
| `matchAll` | boolean | No | `true` (default) = all conditions must match; `false` = any |
|
|
740
|
+
| `enabled` | boolean | No | Whether the rule is enabled on creation (default `true`) |
|
|
741
|
+
|
|
742
|
+
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`.
|
|
743
|
+
|
|
744
|
+
**Example:**
|
|
745
|
+
```json
|
|
746
|
+
{
|
|
747
|
+
"name": "Newsletters",
|
|
748
|
+
"conditions": [{ "field": "from", "operator": "contains", "value": "newsletter" }],
|
|
749
|
+
"actions": { "markRead": true, "moveTo": "Reading" }
|
|
750
|
+
}
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
---
|
|
754
|
+
|
|
755
|
+
#### `delete-rule`
|
|
756
|
+
|
|
757
|
+
Delete a mail rule by name.
|
|
758
|
+
|
|
759
|
+
| Parameter | Type | Required | Description |
|
|
760
|
+
|-----------|------|----------|-------------|
|
|
761
|
+
| `name` | string | Yes | Rule name |
|
|
762
|
+
|
|
763
|
+
---
|
|
764
|
+
|
|
677
765
|
### Contacts
|
|
678
766
|
|
|
679
767
|
#### `search-contacts`
|
|
@@ -691,7 +779,7 @@ Search contacts in Contacts.app.
|
|
|
691
779
|
|
|
692
780
|
### Templates
|
|
693
781
|
|
|
694
|
-
Email templates are
|
|
782
|
+
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`).
|
|
695
783
|
|
|
696
784
|
#### `save-template`
|
|
697
785
|
|
|
@@ -762,6 +850,16 @@ Verify Mail.app connectivity and permissions.
|
|
|
762
850
|
|
|
763
851
|
---
|
|
764
852
|
|
|
853
|
+
#### `doctor`
|
|
854
|
+
|
|
855
|
+
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.
|
|
856
|
+
|
|
857
|
+
**Parameters:** None
|
|
858
|
+
|
|
859
|
+
**Returns:** A per-check report (`structuredContent` carries the raw `{healthy, checks[]}`).
|
|
860
|
+
|
|
861
|
+
---
|
|
862
|
+
|
|
765
863
|
#### `get-mail-stats`
|
|
766
864
|
|
|
767
865
|
Get mail statistics.
|
package/build/index.js
CHANGED
|
@@ -23,21 +23,24 @@ import { createRequire } from "module";
|
|
|
23
23
|
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
|
-
import { AppleMailManager } from "./services/appleMailManager.js";
|
|
26
|
+
import { AppleMailManager, isPathWithinAllowedRoots } from "./services/appleMailManager.js";
|
|
27
|
+
import { writeFileSync } from "fs";
|
|
28
|
+
import { resolve as resolvePath, join as joinPath } from "path";
|
|
27
29
|
import { sendViaSmtp } from "./services/smtpMailer.js";
|
|
28
|
-
import { isImapAccount, resolveImapConfigs, imapSearchMessages, imapListMessages, imapCreateMailbox, imapDeleteMailbox, imapRenameMailbox, imapGetMessage, imapMarkRead, imapMarkUnread, imapFlagMessage, imapUnflagMessage, imapMoveMessageById, imapDeleteMessageById, } from "./services/imapClient.js";
|
|
30
|
+
import { isImapAccount, resolveImapConfigs, imapSearchMessages, imapListMessages, imapUnreadCount, imapListMailboxes, imapMailStats, imapListAttachments, imapFetchAttachment, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete, imapBatchMove, imapThread, imapCreateMailbox, imapDeleteMailbox, imapRenameMailbox, imapGetMessage, imapMarkRead, imapMarkUnread, imapFlagMessage, imapUnflagMessage, imapMoveMessageById, imapDeleteMessageById, } from "./services/imapClient.js";
|
|
29
31
|
import { successResponse, errorResponse, partialCoverageBlock, withErrorHandling, messageSummary, } from "./tools/respond.js";
|
|
30
32
|
import { routeMessage } from "./services/messageRouter.js";
|
|
31
33
|
import { runDoctor, formatDoctorReport } from "./tools/doctor.js";
|
|
32
34
|
import { registerResourcesAndPrompts } from "./tools/resourcesAndPrompts.js";
|
|
33
35
|
import { normalizeSubject, subjectFromGetMessage } from "./tools/thread.js";
|
|
34
36
|
import { ImapIdleWatcher } from "./services/imapIdle.js";
|
|
37
|
+
import { loadFileConfig } from "./services/fileConfig.js";
|
|
38
|
+
// Load file-based config FIRST (2.1.1) — before anything reads APPLE_MAIL_MCP_*.
|
|
39
|
+
// Lets users configure the server when the host app strips the MCP env block.
|
|
40
|
+
loadFileConfig();
|
|
35
41
|
// =============================================================================
|
|
36
42
|
// Shared Validation Schemas
|
|
37
43
|
// =============================================================================
|
|
38
|
-
/** AppleScript message IDs are always numeric. Enforced to prevent AppleScript
|
|
39
|
-
* injection via the `whose id is ${id}` interpolation. */
|
|
40
|
-
const NUMERIC_MESSAGE_ID_SCHEMA = z.string().regex(/^\d+$/, "Message ID must be numeric");
|
|
41
44
|
/** A single-message id is EITHER an AppleScript numeric id OR an IMAP composite
|
|
42
45
|
* token (`imap:<base64url>`, emitted by the IMAP read path). The IMAP form is
|
|
43
46
|
* base64url so it stays injection-safe; it never reaches AppleScript (it's
|
|
@@ -45,10 +48,11 @@ const NUMERIC_MESSAGE_ID_SCHEMA = z.string().regex(/^\d+$/, "Message ID must be
|
|
|
45
48
|
const MESSAGE_ID_SCHEMA = z
|
|
46
49
|
.string()
|
|
47
50
|
.regex(/^(\d+|imap:[A-Za-z0-9_-]+)$/, "Message ID must be numeric or an IMAP id (imap:…)");
|
|
48
|
-
/** Batch operations
|
|
49
|
-
*
|
|
51
|
+
/** Batch operations accept numeric (AppleScript) and/or imap: ids (I2) and are
|
|
52
|
+
* capped to prevent unbounded loops / DoS. Numeric ids run via AppleScript;
|
|
53
|
+
* imap: ids are grouped by mailbox and applied in a single UID command. */
|
|
50
54
|
const BATCH_IDS_SCHEMA = z
|
|
51
|
-
.array(
|
|
55
|
+
.array(MESSAGE_ID_SCHEMA)
|
|
52
56
|
.min(1, "At least one message ID is required")
|
|
53
57
|
.max(100, "Cannot process more than 100 messages in a single batch");
|
|
54
58
|
/** Date filter strings must look like natural-language dates (e.g. "March 1, 2026").
|
|
@@ -99,6 +103,31 @@ const mailManager = new AppleMailManager();
|
|
|
99
103
|
registerResourcesAndPrompts(server, mailManager);
|
|
100
104
|
// Response helpers, the AppleScript serial gate, withErrorHandling, and the
|
|
101
105
|
// message backend router now live in @/tools/respond and @/services/messageRouter.
|
|
106
|
+
/**
|
|
107
|
+
* Split a batch of ids into numeric (AppleScript) and imap: (IMAP) groups, run
|
|
108
|
+
* each path, and merge into success/fail counts (I2). imap: ids apply in a
|
|
109
|
+
* single UID command per mailbox; numeric ids use the existing AppleScript batch.
|
|
110
|
+
*/
|
|
111
|
+
async function hybridBatchCounts(ids, appleFn, imapFn) {
|
|
112
|
+
const imapIds = ids.filter((i) => i.startsWith("imap:"));
|
|
113
|
+
const numericIds = ids.filter((i) => !i.startsWith("imap:"));
|
|
114
|
+
let success = 0;
|
|
115
|
+
let fail = 0;
|
|
116
|
+
const errors = [];
|
|
117
|
+
if (numericIds.length > 0) {
|
|
118
|
+
const res = appleFn(numericIds);
|
|
119
|
+
const s = res.filter((r) => r.success).length;
|
|
120
|
+
success += s;
|
|
121
|
+
fail += res.length - s;
|
|
122
|
+
}
|
|
123
|
+
if (imapIds.length > 0) {
|
|
124
|
+
const r = await imapFn(imapIds);
|
|
125
|
+
success += r.success;
|
|
126
|
+
fail += r.failed;
|
|
127
|
+
errors.push(...r.errors);
|
|
128
|
+
}
|
|
129
|
+
return { success, fail, errors };
|
|
130
|
+
}
|
|
102
131
|
// =============================================================================
|
|
103
132
|
// Message Tools
|
|
104
133
|
// =============================================================================
|
|
@@ -192,6 +221,14 @@ server.tool("get-thread", {
|
|
|
192
221
|
mailbox: z.string().optional().describe("Mailbox to search (omit to search all)"),
|
|
193
222
|
limit: z.number().optional().describe("Max messages in the thread (default 50)"),
|
|
194
223
|
}, withErrorHandling(async ({ id, account, mailbox, limit = 50 }) => {
|
|
224
|
+
// True threading via References/Message-ID when we have an imap: id (I5);
|
|
225
|
+
// falls through to subject grouping if the server lacks HEADER search or
|
|
226
|
+
// nothing References-linked is found.
|
|
227
|
+
if (id.startsWith("imap:")) {
|
|
228
|
+
const t = await imapThread(id, { account }, limit);
|
|
229
|
+
if (t && t.count > 1)
|
|
230
|
+
return successResponse(t.text, { ...t.structured });
|
|
231
|
+
}
|
|
195
232
|
// Resolve the seed message's subject, then gather the conversation by
|
|
196
233
|
// normalized subject (B1). Works across the AppleScript and IMAP backends.
|
|
197
234
|
let seedSubject = null;
|
|
@@ -467,10 +504,8 @@ server.tool("move-message", {
|
|
|
467
504
|
// --- batch-delete-messages ---
|
|
468
505
|
server.tool("batch-delete-messages", {
|
|
469
506
|
ids: BATCH_IDS_SCHEMA,
|
|
470
|
-
}, withErrorHandling(({ ids }) => {
|
|
471
|
-
const
|
|
472
|
-
const successCount = results.filter((r) => r.success).length;
|
|
473
|
-
const failCount = results.length - successCount;
|
|
507
|
+
}, withErrorHandling(async ({ ids }) => {
|
|
508
|
+
const { success: successCount, fail: failCount } = await hybridBatchCounts(ids, (n) => mailManager.batchDeleteMessages(n), (im) => imapBatchDelete(im));
|
|
474
509
|
if (failCount === 0) {
|
|
475
510
|
return successResponse(`Successfully deleted ${successCount} message(s)`);
|
|
476
511
|
}
|
|
@@ -486,10 +521,8 @@ server.tool("batch-move-messages", {
|
|
|
486
521
|
ids: BATCH_IDS_SCHEMA,
|
|
487
522
|
mailbox: z.string().min(1, "Destination mailbox is required"),
|
|
488
523
|
account: z.string().optional().describe("Account containing the destination mailbox"),
|
|
489
|
-
}, withErrorHandling(({ ids, mailbox, account }) => {
|
|
490
|
-
const
|
|
491
|
-
const successCount = results.filter((r) => r.success).length;
|
|
492
|
-
const failCount = results.length - successCount;
|
|
524
|
+
}, withErrorHandling(async ({ ids, mailbox, account }) => {
|
|
525
|
+
const { success: successCount, fail: failCount } = await hybridBatchCounts(ids, (n) => mailManager.batchMoveMessages(n, mailbox, account), (im) => imapBatchMove(im, mailbox, { account }));
|
|
493
526
|
if (failCount === 0) {
|
|
494
527
|
return successResponse(`Successfully moved ${successCount} message(s) to "${mailbox}"`);
|
|
495
528
|
}
|
|
@@ -503,10 +536,8 @@ server.tool("batch-move-messages", {
|
|
|
503
536
|
// --- batch-mark-as-read ---
|
|
504
537
|
server.tool("batch-mark-as-read", {
|
|
505
538
|
ids: BATCH_IDS_SCHEMA,
|
|
506
|
-
}, withErrorHandling(({ ids }) => {
|
|
507
|
-
const
|
|
508
|
-
const successCount = results.filter((r) => r.success).length;
|
|
509
|
-
const failCount = results.length - successCount;
|
|
539
|
+
}, withErrorHandling(async ({ ids }) => {
|
|
540
|
+
const { success: successCount, fail: failCount } = await hybridBatchCounts(ids, (n) => mailManager.batchMarkAsRead(n), (im) => imapBatchMarkRead(im));
|
|
510
541
|
if (failCount === 0) {
|
|
511
542
|
return successResponse(`Successfully marked ${successCount} message(s) as read`);
|
|
512
543
|
}
|
|
@@ -520,10 +551,8 @@ server.tool("batch-mark-as-read", {
|
|
|
520
551
|
// --- batch-mark-as-unread ---
|
|
521
552
|
server.tool("batch-mark-as-unread", {
|
|
522
553
|
ids: BATCH_IDS_SCHEMA,
|
|
523
|
-
}, withErrorHandling(({ ids }) => {
|
|
524
|
-
const
|
|
525
|
-
const successCount = results.filter((r) => r.success).length;
|
|
526
|
-
const failCount = results.length - successCount;
|
|
554
|
+
}, withErrorHandling(async ({ ids }) => {
|
|
555
|
+
const { success: successCount, fail: failCount } = await hybridBatchCounts(ids, (n) => mailManager.batchMarkAsUnread(n), (im) => imapBatchMarkUnread(im));
|
|
527
556
|
if (failCount === 0) {
|
|
528
557
|
return successResponse(`Successfully marked ${successCount} message(s) as unread`);
|
|
529
558
|
}
|
|
@@ -537,10 +566,8 @@ server.tool("batch-mark-as-unread", {
|
|
|
537
566
|
// --- batch-flag-messages ---
|
|
538
567
|
server.tool("batch-flag-messages", {
|
|
539
568
|
ids: BATCH_IDS_SCHEMA,
|
|
540
|
-
}, withErrorHandling(({ ids }) => {
|
|
541
|
-
const
|
|
542
|
-
const successCount = results.filter((r) => r.success).length;
|
|
543
|
-
const failCount = results.length - successCount;
|
|
569
|
+
}, withErrorHandling(async ({ ids }) => {
|
|
570
|
+
const { success: successCount, fail: failCount } = await hybridBatchCounts(ids, (n) => mailManager.batchFlagMessages(n), (im) => imapBatchFlag(im));
|
|
544
571
|
if (failCount === 0) {
|
|
545
572
|
return successResponse(`Successfully flagged ${successCount} message(s)`);
|
|
546
573
|
}
|
|
@@ -554,10 +581,8 @@ server.tool("batch-flag-messages", {
|
|
|
554
581
|
// --- batch-unflag-messages ---
|
|
555
582
|
server.tool("batch-unflag-messages", {
|
|
556
583
|
ids: BATCH_IDS_SCHEMA,
|
|
557
|
-
}, withErrorHandling(({ ids }) => {
|
|
558
|
-
const
|
|
559
|
-
const successCount = results.filter((r) => r.success).length;
|
|
560
|
-
const failCount = results.length - successCount;
|
|
584
|
+
}, withErrorHandling(async ({ ids }) => {
|
|
585
|
+
const { success: successCount, fail: failCount } = await hybridBatchCounts(ids, (n) => mailManager.batchUnflagMessages(n), (im) => imapBatchUnflag(im));
|
|
561
586
|
if (failCount === 0) {
|
|
562
587
|
return successResponse(`Successfully unflagged ${successCount} message(s)`);
|
|
563
588
|
}
|
|
@@ -571,8 +596,17 @@ server.tool("batch-unflag-messages", {
|
|
|
571
596
|
// --- list-attachments ---
|
|
572
597
|
server.tool("list-attachments", {
|
|
573
598
|
id: MESSAGE_ID_SCHEMA,
|
|
574
|
-
}, withErrorHandling(({ id }) => {
|
|
575
|
-
|
|
599
|
+
}, withErrorHandling(async ({ id }) => {
|
|
600
|
+
// IMAP (I1): BODYSTRUCTURE enumerates parts (incl. MIME attachments
|
|
601
|
+
// AppleScript can't see) without downloading the message.
|
|
602
|
+
const attachments = id.startsWith("imap:")
|
|
603
|
+
? await (async () => {
|
|
604
|
+
const r = await imapListAttachments(id);
|
|
605
|
+
if (!r.success)
|
|
606
|
+
throw new Error(r.error || "Failed to list attachments via IMAP");
|
|
607
|
+
return r.attachments ?? [];
|
|
608
|
+
})()
|
|
609
|
+
: mailManager.listAttachments(id);
|
|
576
610
|
const structured = { attachments, count: attachments.length };
|
|
577
611
|
if (attachments.length === 0) {
|
|
578
612
|
return successResponse("No attachments found", structured);
|
|
@@ -590,7 +624,25 @@ server.tool("save-attachment", {
|
|
|
590
624
|
id: MESSAGE_ID_SCHEMA,
|
|
591
625
|
attachmentName: z.string().min(1, "Attachment name is required"),
|
|
592
626
|
savePath: z.string().min(1, "Save directory path is required"),
|
|
593
|
-
}, withErrorHandling(({ id, attachmentName, savePath }) => {
|
|
627
|
+
}, withErrorHandling(async ({ id, attachmentName, savePath }) => {
|
|
628
|
+
// IMAP (I1): fetch the part's bytes via IMAP, then write into savePath (a
|
|
629
|
+
// directory) as savePath/attachmentName — mirroring the AppleScript path,
|
|
630
|
+
// with the same name + allowed-roots validation.
|
|
631
|
+
if (id.startsWith("imap:")) {
|
|
632
|
+
if (/[/\\\0]/.test(attachmentName) || attachmentName.includes("..")) {
|
|
633
|
+
return errorResponse(`Invalid attachment name: "${attachmentName}"`);
|
|
634
|
+
}
|
|
635
|
+
const resolvedDir = resolvePath(savePath);
|
|
636
|
+
if (!isPathWithinAllowedRoots(resolvedDir)) {
|
|
637
|
+
return errorResponse(`Save path "${savePath}" is outside allowed directories`);
|
|
638
|
+
}
|
|
639
|
+
const r = await imapFetchAttachment(id, attachmentName);
|
|
640
|
+
if (!r.success || !r.base64) {
|
|
641
|
+
return errorResponse(r.error || `Failed to fetch attachment "${attachmentName}"`);
|
|
642
|
+
}
|
|
643
|
+
writeFileSync(joinPath(resolvedDir, attachmentName), Buffer.from(r.base64, "base64"));
|
|
644
|
+
return successResponse(`Attachment "${attachmentName}" saved to ${savePath}`);
|
|
645
|
+
}
|
|
594
646
|
const success = mailManager.saveAttachment(id, attachmentName, savePath);
|
|
595
647
|
if (!success) {
|
|
596
648
|
return errorResponse(`Failed to save attachment "${attachmentName}"`);
|
|
@@ -599,12 +651,19 @@ server.tool("save-attachment", {
|
|
|
599
651
|
}, "Error saving attachment"));
|
|
600
652
|
// --- fetch-attachment ---
|
|
601
653
|
server.tool("fetch-attachment", {
|
|
602
|
-
id:
|
|
654
|
+
id: MESSAGE_ID_SCHEMA,
|
|
603
655
|
attachmentName: z.string().min(1, "Attachment name is required"),
|
|
604
|
-
}, withErrorHandling(({ id, attachmentName }) => {
|
|
656
|
+
}, withErrorHandling(async ({ id, attachmentName }) => {
|
|
605
657
|
// Returns the attachment bytes as base64 (B4) — the read counterpart to
|
|
606
|
-
// sending inline base64 content
|
|
607
|
-
//
|
|
658
|
+
// sending inline base64 content. IMAP (I1) fetches the part directly; numeric
|
|
659
|
+
// ids fall back to the AppleScript/MIME path.
|
|
660
|
+
if (id.startsWith("imap:")) {
|
|
661
|
+
const r = await imapFetchAttachment(id, attachmentName);
|
|
662
|
+
if (!r.success || !r.base64) {
|
|
663
|
+
return errorResponse(r.error || `Failed to fetch attachment "${attachmentName}"`);
|
|
664
|
+
}
|
|
665
|
+
return successResponse(`Fetched "${attachmentName}" (${r.bytes} bytes, base64-encoded below).\n\n${r.base64}`, { attachmentName, bytes: r.bytes, mimeType: r.mimeType, contentBase64: r.base64 });
|
|
666
|
+
}
|
|
608
667
|
const r = mailManager.getAttachmentBase64(id, attachmentName);
|
|
609
668
|
if (!r.success) {
|
|
610
669
|
return errorResponse(r.error || `Failed to fetch attachment "${attachmentName}"`);
|
|
@@ -617,7 +676,24 @@ server.tool("fetch-attachment", {
|
|
|
617
676
|
// --- list-mailboxes ---
|
|
618
677
|
server.tool("list-mailboxes", {
|
|
619
678
|
account: z.string().optional().describe("Account to list mailboxes from"),
|
|
620
|
-
}, withErrorHandling(({ account }) => {
|
|
679
|
+
}, withErrorHandling(async ({ account }) => {
|
|
680
|
+
// IMAP (I6): LIST + per-mailbox STATUS — sees the true server hierarchy and
|
|
681
|
+
// authoritative counts; falls back to AppleScript for non-IMAP accounts.
|
|
682
|
+
if (isImapAccount(account)) {
|
|
683
|
+
const boxes = await imapListMailboxes({ account });
|
|
684
|
+
const structured = {
|
|
685
|
+
mailboxes: boxes.map((b) => ({
|
|
686
|
+
name: b.path,
|
|
687
|
+
unreadCount: b.unseen,
|
|
688
|
+
messageCount: b.messages,
|
|
689
|
+
})),
|
|
690
|
+
count: boxes.length,
|
|
691
|
+
};
|
|
692
|
+
if (boxes.length === 0)
|
|
693
|
+
return successResponse("No mailboxes found", structured);
|
|
694
|
+
const list = boxes.map((b) => ` - ${b.path} (${b.unseen} unread)`).join("\n");
|
|
695
|
+
return successResponse(`Found ${boxes.length} mailbox(es):\n${list}`, structured);
|
|
696
|
+
}
|
|
621
697
|
const mailboxes = mailManager.listMailboxes(account);
|
|
622
698
|
const structured = { mailboxes, count: mailboxes.length };
|
|
623
699
|
if (mailboxes.length === 0) {
|
|
@@ -630,8 +706,12 @@ server.tool("list-mailboxes", {
|
|
|
630
706
|
server.tool("get-unread-count", {
|
|
631
707
|
mailbox: z.string().optional().describe("Mailbox to check (default: all)"),
|
|
632
708
|
account: z.string().optional().describe("Account to check"),
|
|
633
|
-
}, withErrorHandling(({ mailbox, account }) => {
|
|
634
|
-
|
|
709
|
+
}, withErrorHandling(async ({ mailbox, account }) => {
|
|
710
|
+
// IMAP (I4): STATUS (UNSEEN) is authoritative and fast even on huge
|
|
711
|
+
// mailboxes; falls back to AppleScript for non-IMAP accounts.
|
|
712
|
+
const count = isImapAccount(account)
|
|
713
|
+
? await imapUnreadCount(mailbox, { account })
|
|
714
|
+
: mailManager.getUnreadCount(mailbox, account);
|
|
635
715
|
const location = mailbox ? ` in "${mailbox}"` : "";
|
|
636
716
|
return successResponse(`${count} unread message(s)${location}`, {
|
|
637
717
|
unread: count,
|
|
@@ -893,7 +973,29 @@ server.tool("doctor", {}, withErrorHandling(async () => {
|
|
|
893
973
|
return successResponse(formatDoctorReport(report), { ...report });
|
|
894
974
|
}, "Error running doctor"));
|
|
895
975
|
// --- get-mail-stats ---
|
|
896
|
-
server.tool("get-mail-stats", {
|
|
976
|
+
server.tool("get-mail-stats", {
|
|
977
|
+
account: z
|
|
978
|
+
.string()
|
|
979
|
+
.optional()
|
|
980
|
+
.describe("Limit to one account; uses fast IMAP STATUS if that account is IMAP-configured"),
|
|
981
|
+
}, withErrorHandling(async ({ account }) => {
|
|
982
|
+
// IMAP (I3): for a named IMAP account, STATUS gives authoritative counts and
|
|
983
|
+
// SEARCH SINCE gives recent activity — fast even on huge mailboxes.
|
|
984
|
+
if (account && isImapAccount(account)) {
|
|
985
|
+
const s = await imapMailStats({ account });
|
|
986
|
+
const lines = [
|
|
987
|
+
`📊 Mail Statistics — ${account} (IMAP)`,
|
|
988
|
+
`══════════════════`,
|
|
989
|
+
`Total messages: ${s.totalMessages}`,
|
|
990
|
+
`Unread messages: ${s.totalUnread}`,
|
|
991
|
+
``,
|
|
992
|
+
`📥 Recently Received (INBOX):`,
|
|
993
|
+
` Last 24 hours: ${s.recent.last24h}`,
|
|
994
|
+
` Last 7 days: ${s.recent.last7d}`,
|
|
995
|
+
` Last 30 days: ${s.recent.last30d}`,
|
|
996
|
+
];
|
|
997
|
+
return successResponse(lines.join("\n"), { account, ...s });
|
|
998
|
+
}
|
|
897
999
|
const stats = mailManager.getMailStats();
|
|
898
1000
|
const lines = [];
|
|
899
1001
|
lines.push(`📊 Mail Statistics`);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function fileConfigPath(env?: NodeJS.ProcessEnv): string;
|
|
2
|
+
/**
|
|
3
|
+
* Merge a JSON config file's string values into `env` for keys not already set.
|
|
4
|
+
* Returns the list of keys actually applied. Tolerates a missing/corrupt file.
|
|
5
|
+
*/
|
|
6
|
+
export declare function loadFileConfig(env?: NodeJS.ProcessEnv, path?: string): string[];
|
|
7
|
+
//# sourceMappingURL=fileConfig.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileConfig.d.ts","sourceRoot":"","sources":["../../src/services/fileConfig.ts"],"names":[],"mappings":"AAqBA,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAI3E;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,IAAI,GAAE,MAA4B,GACjC,MAAM,EAAE,CAiBV"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-based configuration loader (2.1.1).
|
|
3
|
+
*
|
|
4
|
+
* Some host apps (e.g. Claude Desktop) spawn the MCP server with a scrubbed
|
|
5
|
+
* environment and ignore/strip the `env` block in their server config, so
|
|
6
|
+
* there's no way to pass `APPLE_MAIL_MCP_*` settings in. This loads them from a
|
|
7
|
+
* JSON file the host doesn't manage, merging into `process.env` WITHOUT
|
|
8
|
+
* overriding anything already set (so an explicit env still wins).
|
|
9
|
+
*
|
|
10
|
+
* The file holds only non-secret config — account/host/Keychain-service names
|
|
11
|
+
* and flags. Passwords are NEVER stored here; they stay in the macOS Keychain.
|
|
12
|
+
*
|
|
13
|
+
* Path: `APPLE_MAIL_MCP_CONFIG_FILE`, else
|
|
14
|
+
* `~/Library/Application Support/apple-mail-mcp/config.json`.
|
|
15
|
+
*
|
|
16
|
+
* @module services/fileConfig
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, readFileSync } from "fs";
|
|
19
|
+
import { join } from "path";
|
|
20
|
+
import { homedir } from "os";
|
|
21
|
+
export function fileConfigPath(env = process.env) {
|
|
22
|
+
const override = env.APPLE_MAIL_MCP_CONFIG_FILE;
|
|
23
|
+
if (override && override.trim())
|
|
24
|
+
return override.trim();
|
|
25
|
+
return join(homedir(), "Library", "Application Support", "apple-mail-mcp", "config.json");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Merge a JSON config file's string values into `env` for keys not already set.
|
|
29
|
+
* Returns the list of keys actually applied. Tolerates a missing/corrupt file.
|
|
30
|
+
*/
|
|
31
|
+
export function loadFileConfig(env = process.env, path = fileConfigPath(env)) {
|
|
32
|
+
const applied = [];
|
|
33
|
+
try {
|
|
34
|
+
if (!existsSync(path))
|
|
35
|
+
return applied;
|
|
36
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
37
|
+
if (!parsed || typeof parsed !== "object")
|
|
38
|
+
return applied;
|
|
39
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
40
|
+
if (typeof v !== "string")
|
|
41
|
+
continue;
|
|
42
|
+
if (env[k] === undefined || env[k] === "") {
|
|
43
|
+
env[k] = v;
|
|
44
|
+
applied.push(k);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
console.error(`Failed to load apple-mail-mcp config file ${path}: ${String(e)}`);
|
|
50
|
+
}
|
|
51
|
+
return applied;
|
|
52
|
+
}
|
|
@@ -38,12 +38,33 @@ interface ImapEnvelope {
|
|
|
38
38
|
subject?: string;
|
|
39
39
|
date?: Date | string;
|
|
40
40
|
from?: ImapAddress[];
|
|
41
|
+
messageId?: string;
|
|
42
|
+
inReplyTo?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface ImapBodyStructure {
|
|
45
|
+
part?: string;
|
|
46
|
+
type?: string;
|
|
47
|
+
disposition?: string;
|
|
48
|
+
dispositionParameters?: Record<string, string>;
|
|
49
|
+
parameters?: Record<string, string>;
|
|
50
|
+
size?: number;
|
|
51
|
+
encoding?: string;
|
|
52
|
+
childNodes?: ImapBodyStructure[];
|
|
41
53
|
}
|
|
42
54
|
interface ImapMessage {
|
|
43
55
|
uid: number;
|
|
44
56
|
envelope?: ImapEnvelope;
|
|
45
57
|
flags?: Set<string>;
|
|
46
58
|
source?: Buffer | string;
|
|
59
|
+
bodyStructure?: ImapBodyStructure;
|
|
60
|
+
headers?: Buffer | string;
|
|
61
|
+
}
|
|
62
|
+
interface ImapDownload {
|
|
63
|
+
meta?: {
|
|
64
|
+
filename?: string;
|
|
65
|
+
contentType?: string;
|
|
66
|
+
};
|
|
67
|
+
content: AsyncIterable<Uint8Array>;
|
|
47
68
|
}
|
|
48
69
|
interface MailboxLock {
|
|
49
70
|
release: () => void;
|
|
@@ -68,6 +89,19 @@ export interface ImapClientLike {
|
|
|
68
89
|
uid: true;
|
|
69
90
|
}): Promise<ImapMessage | false>;
|
|
70
91
|
list(): Promise<ImapMailboxListing[]>;
|
|
92
|
+
status(path: string, query: {
|
|
93
|
+
messages?: boolean;
|
|
94
|
+
unseen?: boolean;
|
|
95
|
+
recent?: boolean;
|
|
96
|
+
}): Promise<{
|
|
97
|
+
path: string;
|
|
98
|
+
messages?: number;
|
|
99
|
+
unseen?: number;
|
|
100
|
+
recent?: number;
|
|
101
|
+
}>;
|
|
102
|
+
download(range: string, part: string, opts: {
|
|
103
|
+
uid: true;
|
|
104
|
+
}): Promise<ImapDownload>;
|
|
71
105
|
mailboxCreate(path: string): Promise<{
|
|
72
106
|
path: string;
|
|
73
107
|
created: boolean;
|
|
@@ -123,6 +157,32 @@ export declare function resolveImapConfig(env?: NodeJS.ProcessEnv, account?: str
|
|
|
123
157
|
export declare function resolveMailboxPath(mailbox: string | undefined, mode: "search" | "list"): string;
|
|
124
158
|
export declare function imapSearchMessages(args: ImapSearchArgs, deps?: ImapDeps): Promise<string>;
|
|
125
159
|
export declare function imapListMessages(args: ImapSearchArgs, deps?: ImapDeps): Promise<string>;
|
|
160
|
+
/** Unread count via IMAP STATUS (UNSEEN). No mailbox → sum across all mailboxes. */
|
|
161
|
+
export declare function imapUnreadCount(mailbox: string | undefined, deps?: ImapDeps): Promise<number>;
|
|
162
|
+
export interface ImapMailboxInfo {
|
|
163
|
+
path: string;
|
|
164
|
+
name: string;
|
|
165
|
+
messages: number;
|
|
166
|
+
unseen: number;
|
|
167
|
+
}
|
|
168
|
+
/** List mailboxes with per-mailbox message/unseen counts via LIST + STATUS (I6). */
|
|
169
|
+
export declare function imapListMailboxes(deps?: ImapDeps): Promise<ImapMailboxInfo[]>;
|
|
170
|
+
export interface ImapStats {
|
|
171
|
+
totalMessages: number;
|
|
172
|
+
totalUnread: number;
|
|
173
|
+
perMailbox: {
|
|
174
|
+
mailbox: string;
|
|
175
|
+
messages: number;
|
|
176
|
+
unseen: number;
|
|
177
|
+
}[];
|
|
178
|
+
recent: {
|
|
179
|
+
last24h: number;
|
|
180
|
+
last7d: number;
|
|
181
|
+
last30d: number;
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/** Aggregate stats via STATUS (counts) + INBOX SEARCH SINCE (recent) (I3). */
|
|
185
|
+
export declare function imapMailStats(deps?: ImapDeps): Promise<ImapStats>;
|
|
126
186
|
export interface ImapOpResult {
|
|
127
187
|
success: boolean;
|
|
128
188
|
error?: string;
|
|
@@ -154,5 +214,53 @@ export declare const imapFlagMessage: (id: string, deps?: {}) => Promise<ImapOpR
|
|
|
154
214
|
export declare const imapUnflagMessage: (id: string, deps?: {}) => Promise<ImapOpResult>;
|
|
155
215
|
export declare function imapMoveMessageById(id: string, destMailbox: string, deps?: ImapDeps): Promise<ImapOpResult>;
|
|
156
216
|
export declare function imapDeleteMessageById(id: string, deps?: ImapDeps): Promise<ImapOpResult>;
|
|
217
|
+
export interface ImapAttachmentInfo {
|
|
218
|
+
id: string;
|
|
219
|
+
name: string;
|
|
220
|
+
mimeType: string;
|
|
221
|
+
size: number;
|
|
222
|
+
}
|
|
223
|
+
/** List a message's attachments via IMAP BODYSTRUCTURE (no full download). */
|
|
224
|
+
export declare function imapListAttachments(id: string, deps?: ImapDeps): Promise<{
|
|
225
|
+
success: boolean;
|
|
226
|
+
attachments?: ImapAttachmentInfo[];
|
|
227
|
+
error?: string;
|
|
228
|
+
}>;
|
|
229
|
+
/** Fetch one attachment's bytes (base64) via IMAP, matched by filename. */
|
|
230
|
+
export declare function imapFetchAttachment(id: string, attachmentName: string, deps?: ImapDeps): Promise<{
|
|
231
|
+
success: boolean;
|
|
232
|
+
base64?: string;
|
|
233
|
+
bytes?: number;
|
|
234
|
+
mimeType?: string;
|
|
235
|
+
error?: string;
|
|
236
|
+
}>;
|
|
237
|
+
export interface ImapBatchResult {
|
|
238
|
+
success: number;
|
|
239
|
+
failed: number;
|
|
240
|
+
errors: string[];
|
|
241
|
+
}
|
|
242
|
+
export declare const imapBatchMarkRead: (ids: string[], deps?: ImapDeps) => Promise<ImapBatchResult>;
|
|
243
|
+
export declare const imapBatchMarkUnread: (ids: string[], deps?: ImapDeps) => Promise<ImapBatchResult>;
|
|
244
|
+
export declare const imapBatchFlag: (ids: string[], deps?: ImapDeps) => Promise<ImapBatchResult>;
|
|
245
|
+
export declare const imapBatchUnflag: (ids: string[], deps?: ImapDeps) => Promise<ImapBatchResult>;
|
|
246
|
+
export declare const imapBatchDelete: (ids: string[], deps?: ImapDeps) => Promise<ImapBatchResult>;
|
|
247
|
+
export declare function imapBatchMove(ids: string[], destMailbox: string, deps?: ImapDeps): Promise<ImapBatchResult>;
|
|
248
|
+
export interface ImapThreadMessage {
|
|
249
|
+
id: string;
|
|
250
|
+
subject: string;
|
|
251
|
+
sender: string;
|
|
252
|
+
date: string;
|
|
253
|
+
isRead: boolean;
|
|
254
|
+
}
|
|
255
|
+
export interface ImapThreadResult {
|
|
256
|
+
count: number;
|
|
257
|
+
text: string;
|
|
258
|
+
structured: {
|
|
259
|
+
subject: string;
|
|
260
|
+
messages: ImapThreadMessage[];
|
|
261
|
+
count: number;
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
export declare function imapThread(id: string, deps?: ImapDeps, limit?: number): Promise<ImapThreadResult | null>;
|
|
157
265
|
export {};
|
|
158
266
|
//# sourceMappingURL=imapClient.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"imapClient.d.ts","sourceRoot":"","sources":["../../src/services/imapClient.ts"],"names":[],"mappings":"AA4BA,eAAO,MAAM,QAAQ;;;;;;;;;CAWX,CAAC;AAEX,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,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,WAAW;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AACD,UAAU,YAAY;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,WAAW,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"imapClient.d.ts","sourceRoot":"","sources":["../../src/services/imapClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -294,6 +294,97 @@ export function imapSearchMessages(args, deps = {}) {
|
|
|
294
294
|
export function imapListMessages(args, deps = {}) {
|
|
295
295
|
return run(args, true, deps);
|
|
296
296
|
}
|
|
297
|
+
// ===========================================================================
|
|
298
|
+
// Counts & stats via IMAP STATUS (2.1 optimizations I3/I4/I6)
|
|
299
|
+
//
|
|
300
|
+
// STATUS is a single server round-trip that returns authoritative message/unseen
|
|
301
|
+
// counts without enumerating messages — far faster and more reliable than
|
|
302
|
+
// AppleScript on large mailboxes (where the per-message walk times out, #8/#24).
|
|
303
|
+
// ===========================================================================
|
|
304
|
+
/** Unread count via IMAP STATUS (UNSEEN). No mailbox → sum across all mailboxes. */
|
|
305
|
+
export function imapUnreadCount(mailbox, deps = {}) {
|
|
306
|
+
return useClient(deps, async (client) => {
|
|
307
|
+
if (mailbox) {
|
|
308
|
+
const s = await client.status(resolveMailboxPath(mailbox, "list"), { unseen: true });
|
|
309
|
+
return s.unseen ?? 0;
|
|
310
|
+
}
|
|
311
|
+
let total = 0;
|
|
312
|
+
for (const b of await client.list()) {
|
|
313
|
+
try {
|
|
314
|
+
const s = await client.status(b.path, { unseen: true });
|
|
315
|
+
total += s.unseen ?? 0;
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// skip mailboxes that can't be STATUS'd (e.g. \Noselect parents)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return total;
|
|
322
|
+
}, true);
|
|
323
|
+
}
|
|
324
|
+
/** List mailboxes with per-mailbox message/unseen counts via LIST + STATUS (I6). */
|
|
325
|
+
export function imapListMailboxes(deps = {}) {
|
|
326
|
+
return useClient(deps, async (client) => {
|
|
327
|
+
const out = [];
|
|
328
|
+
for (const b of await client.list()) {
|
|
329
|
+
let messages = 0;
|
|
330
|
+
let unseen = 0;
|
|
331
|
+
try {
|
|
332
|
+
const s = await client.status(b.path, { messages: true, unseen: true });
|
|
333
|
+
messages = s.messages ?? 0;
|
|
334
|
+
unseen = s.unseen ?? 0;
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
// \Noselect or otherwise un-status-able mailbox → report zeros
|
|
338
|
+
}
|
|
339
|
+
out.push({ path: b.path, name: b.name, messages, unseen });
|
|
340
|
+
}
|
|
341
|
+
return out;
|
|
342
|
+
}, true);
|
|
343
|
+
}
|
|
344
|
+
/** Aggregate stats via STATUS (counts) + INBOX SEARCH SINCE (recent) (I3). */
|
|
345
|
+
export function imapMailStats(deps = {}) {
|
|
346
|
+
return useClient(deps, async (client) => {
|
|
347
|
+
const perMailbox = [];
|
|
348
|
+
let totalMessages = 0;
|
|
349
|
+
let totalUnread = 0;
|
|
350
|
+
for (const b of await client.list()) {
|
|
351
|
+
try {
|
|
352
|
+
const s = await client.status(b.path, { messages: true, unseen: true });
|
|
353
|
+
const messages = s.messages ?? 0;
|
|
354
|
+
const unseen = s.unseen ?? 0;
|
|
355
|
+
totalMessages += messages;
|
|
356
|
+
totalUnread += unseen;
|
|
357
|
+
perMailbox.push({ mailbox: b.path, messages, unseen });
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// skip un-status-able mailbox
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Recent counts against INBOX (the meaningful "received" surface).
|
|
364
|
+
const since = (days) => new Date(Date.now() - days * 86_400_000);
|
|
365
|
+
const countSince = async (days) => {
|
|
366
|
+
try {
|
|
367
|
+
const lock = await client.getMailboxLock("INBOX");
|
|
368
|
+
try {
|
|
369
|
+
const found = await client.search({ since: since(days) }, { uid: true });
|
|
370
|
+
return Array.isArray(found) ? found.length : 0;
|
|
371
|
+
}
|
|
372
|
+
finally {
|
|
373
|
+
lock.release();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return 0;
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
const [last24h, last7d, last30d] = await Promise.all([
|
|
381
|
+
countSince(1),
|
|
382
|
+
countSince(7),
|
|
383
|
+
countSince(30),
|
|
384
|
+
]);
|
|
385
|
+
return { totalMessages, totalUnread, perMailbox, recent: { last24h, last7d, last30d } };
|
|
386
|
+
}, true);
|
|
387
|
+
}
|
|
297
388
|
function errText(e) {
|
|
298
389
|
return e instanceof Error ? e.message : String(e);
|
|
299
390
|
}
|
|
@@ -618,3 +709,203 @@ export async function imapDeleteMessageById(id, deps = {}) {
|
|
|
618
709
|
}
|
|
619
710
|
});
|
|
620
711
|
}
|
|
712
|
+
/** Walk a BODYSTRUCTURE tree collecting attachment parts (disposition or filename). */
|
|
713
|
+
function collectAttachments(node, out = []) {
|
|
714
|
+
if (!node)
|
|
715
|
+
return out;
|
|
716
|
+
const filename = node.dispositionParameters?.filename || node.parameters?.name;
|
|
717
|
+
const disposition = node.disposition?.toLowerCase();
|
|
718
|
+
const isAttachment = !!node.part && (disposition === "attachment" || (!!filename && disposition !== "inline"));
|
|
719
|
+
if (isAttachment) {
|
|
720
|
+
out.push({
|
|
721
|
+
part: node.part,
|
|
722
|
+
filename: filename || `part-${node.part}`,
|
|
723
|
+
mimeType: node.type || "application/octet-stream",
|
|
724
|
+
size: node.size ?? 0,
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
for (const child of node.childNodes ?? [])
|
|
728
|
+
collectAttachments(child, out);
|
|
729
|
+
return out;
|
|
730
|
+
}
|
|
731
|
+
async function streamToBuffer(content) {
|
|
732
|
+
const chunks = [];
|
|
733
|
+
for await (const chunk of content)
|
|
734
|
+
chunks.push(Buffer.from(chunk));
|
|
735
|
+
return Buffer.concat(chunks);
|
|
736
|
+
}
|
|
737
|
+
/** List a message's attachments via IMAP BODYSTRUCTURE (no full download). */
|
|
738
|
+
export async function imapListAttachments(id, deps = {}) {
|
|
739
|
+
const ref = decodeImapId(id);
|
|
740
|
+
if (!ref)
|
|
741
|
+
return { success: false, error: `Not an IMAP message id: "${id}".` };
|
|
742
|
+
return withMailbox(ref.path, { ...deps, account: deps.account ?? ref.account }, async (client) => {
|
|
743
|
+
const msg = await client.fetchOne(String(ref.uid), { bodyStructure: true }, { uid: true });
|
|
744
|
+
if (!msg || !msg.bodyStructure) {
|
|
745
|
+
return { success: false, error: `IMAP message UID ${ref.uid} not found in "${ref.path}".` };
|
|
746
|
+
}
|
|
747
|
+
const attachments = collectAttachments(msg.bodyStructure).map((a) => ({
|
|
748
|
+
id: `${id}#${a.part}`,
|
|
749
|
+
name: a.filename,
|
|
750
|
+
mimeType: a.mimeType,
|
|
751
|
+
size: a.size,
|
|
752
|
+
}));
|
|
753
|
+
return { success: true, attachments };
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
/** Fetch one attachment's bytes (base64) via IMAP, matched by filename. */
|
|
757
|
+
export async function imapFetchAttachment(id, attachmentName, deps = {}) {
|
|
758
|
+
const ref = decodeImapId(id);
|
|
759
|
+
if (!ref)
|
|
760
|
+
return { success: false, error: `Not an IMAP message id: "${id}".` };
|
|
761
|
+
return withMailbox(ref.path, { ...deps, account: deps.account ?? ref.account }, async (client) => {
|
|
762
|
+
const msg = await client.fetchOne(String(ref.uid), { bodyStructure: true }, { uid: true });
|
|
763
|
+
if (!msg || !msg.bodyStructure) {
|
|
764
|
+
return { success: false, error: `IMAP message UID ${ref.uid} not found in "${ref.path}".` };
|
|
765
|
+
}
|
|
766
|
+
const atts = collectAttachments(msg.bodyStructure);
|
|
767
|
+
const match = atts.find((a) => a.filename === attachmentName);
|
|
768
|
+
if (!match) {
|
|
769
|
+
const names = atts.map((a) => a.filename).join(", ") || "none";
|
|
770
|
+
return {
|
|
771
|
+
success: false,
|
|
772
|
+
error: `Attachment "${attachmentName}" not found on UID ${ref.uid}. Available: ${names}.`,
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
const dl = await client.download(String(ref.uid), match.part, { uid: true });
|
|
776
|
+
const buf = await streamToBuffer(dl.content);
|
|
777
|
+
return {
|
|
778
|
+
success: true,
|
|
779
|
+
base64: buf.toString("base64"),
|
|
780
|
+
bytes: buf.length,
|
|
781
|
+
mimeType: match.mimeType,
|
|
782
|
+
};
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
async function imapBatch(ids, deps, op) {
|
|
786
|
+
const groups = new Map();
|
|
787
|
+
const errors = [];
|
|
788
|
+
let failed = 0;
|
|
789
|
+
for (const id of ids) {
|
|
790
|
+
const ref = decodeImapId(id);
|
|
791
|
+
if (!ref) {
|
|
792
|
+
failed++;
|
|
793
|
+
errors.push(`Not an IMAP id: "${id}"`);
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
const key = `${ref.account}${ref.path}`;
|
|
797
|
+
const g = groups.get(key) ?? { account: ref.account, path: ref.path, uids: [] };
|
|
798
|
+
g.uids.push(ref.uid);
|
|
799
|
+
groups.set(key, g);
|
|
800
|
+
}
|
|
801
|
+
let success = 0;
|
|
802
|
+
for (const g of groups.values()) {
|
|
803
|
+
try {
|
|
804
|
+
await useClient({ ...deps, account: deps.account ?? g.account }, async (client) => {
|
|
805
|
+
const lock = await client.getMailboxLock(g.path);
|
|
806
|
+
try {
|
|
807
|
+
await op(client, g.uids, g.path);
|
|
808
|
+
}
|
|
809
|
+
finally {
|
|
810
|
+
lock.release();
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
success += g.uids.length;
|
|
814
|
+
}
|
|
815
|
+
catch (e) {
|
|
816
|
+
failed += g.uids.length;
|
|
817
|
+
errors.push(`${g.path}: ${errText(e)}`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return { success, failed, errors };
|
|
821
|
+
}
|
|
822
|
+
export const imapBatchMarkRead = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids) => {
|
|
823
|
+
await c.messageFlagsAdd(uids, ["\\Seen"], { uid: true });
|
|
824
|
+
});
|
|
825
|
+
export const imapBatchMarkUnread = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids) => {
|
|
826
|
+
await c.messageFlagsRemove(uids, ["\\Seen"], { uid: true });
|
|
827
|
+
});
|
|
828
|
+
export const imapBatchFlag = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids) => {
|
|
829
|
+
await c.messageFlagsAdd(uids, ["\\Flagged"], { uid: true });
|
|
830
|
+
});
|
|
831
|
+
export const imapBatchUnflag = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids) => {
|
|
832
|
+
await c.messageFlagsRemove(uids, ["\\Flagged"], { uid: true });
|
|
833
|
+
});
|
|
834
|
+
export const imapBatchDelete = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids) => {
|
|
835
|
+
await c.messageDelete(uids, { uid: true });
|
|
836
|
+
});
|
|
837
|
+
export function imapBatchMove(ids, destMailbox, deps = {}) {
|
|
838
|
+
return imapBatch(ids, deps, async (c, uids) => {
|
|
839
|
+
const dest = (await findMailboxPath(c, destMailbox)) ?? resolveMailboxPath(destMailbox, "list");
|
|
840
|
+
await c.messageMove(uids, dest, { uid: true });
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
function senderName(from) {
|
|
844
|
+
const a = from?.[0];
|
|
845
|
+
if (!a)
|
|
846
|
+
return "(unknown)";
|
|
847
|
+
return a.name ? `${a.name} <${a.address ?? ""}>` : (a.address ?? "(unknown)");
|
|
848
|
+
}
|
|
849
|
+
function dateMs(m) {
|
|
850
|
+
return m.envelope?.date ? new Date(m.envelope.date).getTime() : 0;
|
|
851
|
+
}
|
|
852
|
+
export async function imapThread(id, deps = {}, limit = 50) {
|
|
853
|
+
const ref = decodeImapId(id);
|
|
854
|
+
if (!ref)
|
|
855
|
+
return null;
|
|
856
|
+
return useClient({ ...deps, account: deps.account ?? ref.account }, async (client) => {
|
|
857
|
+
const lock = await client.getMailboxLock(ref.path);
|
|
858
|
+
try {
|
|
859
|
+
const seed = await client.fetchOne(String(ref.uid), { envelope: true, headers: ["references", "in-reply-to", "message-id"] }, { uid: true });
|
|
860
|
+
if (!seed)
|
|
861
|
+
return null;
|
|
862
|
+
const seedMsgId = seed.envelope?.messageId;
|
|
863
|
+
const refIds = new Set();
|
|
864
|
+
const hdr = seed.headers ? seed.headers.toString() : "";
|
|
865
|
+
for (const m of hdr.matchAll(/<[^>]+>/g))
|
|
866
|
+
refIds.add(m[0]);
|
|
867
|
+
if (seed.envelope?.inReplyTo)
|
|
868
|
+
refIds.add(seed.envelope.inReplyTo);
|
|
869
|
+
const uidSet = new Set([ref.uid]);
|
|
870
|
+
const addFound = (found) => {
|
|
871
|
+
if (Array.isArray(found))
|
|
872
|
+
found.forEach((u) => uidSet.add(u));
|
|
873
|
+
};
|
|
874
|
+
// Descendants: anything referencing the seed.
|
|
875
|
+
if (seedMsgId) {
|
|
876
|
+
addFound(await client.search({ header: { references: seedMsgId } }, { uid: true }));
|
|
877
|
+
addFound(await client.search({ header: { "in-reply-to": seedMsgId } }, { uid: true }));
|
|
878
|
+
}
|
|
879
|
+
// Ancestors: messages whose Message-ID is in the seed's References (bounded).
|
|
880
|
+
for (const mid of [...refIds].slice(0, 20)) {
|
|
881
|
+
addFound(await client.search({ header: { "message-id": mid } }, { uid: true }));
|
|
882
|
+
}
|
|
883
|
+
if (uidSet.size <= 1)
|
|
884
|
+
return null; // only the seed → caller falls back to subject
|
|
885
|
+
const uids = [...uidSet].slice(0, limit);
|
|
886
|
+
const msgs = [];
|
|
887
|
+
for await (const msg of client.fetch(uids.join(","), { envelope: true, flags: true }, { uid: true })) {
|
|
888
|
+
msgs.push(msg);
|
|
889
|
+
}
|
|
890
|
+
msgs.sort((a, b) => dateMs(a) - dateMs(b)); // oldest first
|
|
891
|
+
const subject = seed.envelope?.subject || "(no subject)";
|
|
892
|
+
const structured = {
|
|
893
|
+
subject,
|
|
894
|
+
count: msgs.length,
|
|
895
|
+
messages: msgs.map((m) => ({
|
|
896
|
+
id: encodeImapId(ref.account, ref.path, m.uid),
|
|
897
|
+
subject: m.envelope?.subject || "(no subject)",
|
|
898
|
+
sender: senderName(m.envelope?.from),
|
|
899
|
+
date: m.envelope?.date ? new Date(m.envelope.date).toISOString() : "",
|
|
900
|
+
isRead: m.flags?.has("\\Seen") ?? false,
|
|
901
|
+
})),
|
|
902
|
+
};
|
|
903
|
+
const text = `Thread "${subject}" — ${msgs.length} message(s) via IMAP (References-linked, oldest first):\n` +
|
|
904
|
+
msgs.map((m) => formatRow(m, ref.account, ref.path)).join("\n");
|
|
905
|
+
return { count: msgs.length, text, structured };
|
|
906
|
+
}
|
|
907
|
+
finally {
|
|
908
|
+
lock.release();
|
|
909
|
+
}
|
|
910
|
+
}, true);
|
|
911
|
+
}
|