apple-mail-mcp 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -21
- package/build/index.js +64 -13
- package/build/services/appleMailManager.d.ts +24 -3
- package/build/services/appleMailManager.d.ts.map +1 -1
- package/build/services/appleMailManager.js +55 -18
- package/build/services/imapClient.d.ts +34 -0
- package/build/services/imapClient.d.ts.map +1 -1
- package/build/services/imapClient.js +138 -9
- package/build/utils/mimeParse.d.ts +6 -0
- package/build/utils/mimeParse.d.ts.map +1 -1
- package/build/utils/mimeParse.js +30 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -270,24 +270,31 @@ Then send:
|
|
|
270
270
|
|
|
271
271
|
The default `applescript` transport is unchanged; SMTP is opt-in per call.
|
|
272
272
|
|
|
273
|
-
##### IMAP backend
|
|
273
|
+
##### IMAP backend — opt-in
|
|
274
274
|
|
|
275
275
|
AppleScript runs `search`/`list` predicates client-side over the Apple Event
|
|
276
276
|
bridge, which is slow and can time out (false-empty) on large Gmail/IMAP
|
|
277
|
-
mailboxes (see [#24](https://github.com/sweetrb/apple-mail-mcp/issues/24))
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
277
|
+
mailboxes (see [#24](https://github.com/sweetrb/apple-mail-mcp/issues/24)), and
|
|
278
|
+
its `delete`/`rename mailbox` and draft handlers don't work on server-side
|
|
279
|
+
accounts at all (#42). When an account is configured for IMAP, the MCP routes to
|
|
280
|
+
a server-side IMAP backend ([#43](https://github.com/sweetrb/apple-mail-mcp/issues/43))
|
|
281
|
+
that is fast and correct on exactly those mailboxes. This is **opt-in and
|
|
282
|
+
additive**: any account without IMAP configured behaves exactly as before
|
|
283
|
+
(AppleScript).
|
|
284
|
+
|
|
285
|
+
What routes to IMAP when an account is IMAP-configured:
|
|
286
|
+
|
|
287
|
+
- **Read:** `search-messages`, `list-messages` (server-side `SEARCH`, typically sub-second), and `get-message`.
|
|
288
|
+
- **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
|
+
- **Message mutations:** `mark-as-read`/`unread`, `flag-message`/`unflag-message`, `move-message`, `delete-message`.
|
|
290
|
+
|
|
291
|
+
**Message ids are backend-tagged.** The IMAP read path emits self-describing ids
|
|
292
|
+
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.)
|
|
291
298
|
|
|
292
299
|
Routing is conservative: only a call whose explicit `account` matches the
|
|
293
300
|
configured IMAP account goes to IMAP; everything else falls through to
|
|
@@ -309,10 +316,8 @@ config. Gmail label semantics: common names (`All Mail`, `Sent`, `Trash`,
|
|
|
309
316
|
`Spam`, `Important`, …) map to their `[Gmail]/…` IMAP paths automatically.
|
|
310
317
|
|
|
311
318
|
> Note: each call currently opens its own IMAP connection (no pooling yet), so
|
|
312
|
-
> expect a few seconds of connection overhead per call
|
|
313
|
-
>
|
|
314
|
-
> [#42](https://github.com/sweetrb/apple-mail-mcp/issues/42). IMAP-backed
|
|
315
|
-
> message-level mutations are still future work (see #43).
|
|
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).
|
|
316
321
|
>
|
|
317
322
|
> **iCloud:** set `APPLE_MAIL_MCP_IMAP_HOST=imap.mail.me.com`, `APPLE_MAIL_MCP_IMAP_USER`
|
|
318
323
|
> to your iCloud address, `APPLE_MAIL_MCP_IMAP_ACCOUNT` to the Mail account name
|
|
@@ -848,8 +853,8 @@ The entrypoint is written as:
|
|
|
848
853
|
| No sending HTML email | Emails are sent as plain text; reading HTML content is supported |
|
|
849
854
|
| Attachments require absolute paths | File attachments must use full absolute paths (e.g., `/Users/me/file.pdf`) |
|
|
850
855
|
| No smart mailboxes | Cannot access Smart Mailboxes via AppleScript |
|
|
851
|
-
| 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
|
|
852
|
-
| Can't delete/rename server-side mailboxes or mutate drafts | Mail.app's AppleScript bridge can only `delete`/`rename` **local "On My Mac"** mailboxes and cannot delete/move drafts — it throws `AppleEvent handler failed` for IMAP/Gmail/Workspace/iCloud/Exchange mailboxes
|
|
856
|
+
| Very large mailboxes not searchable *via AppleScript* | 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 — or configure the [IMAP backend](#imap-backend--opt-in), which searches these server-side in well under a second. ([#24](https://github.com/sweetrb/apple-mail-mcp/issues/24)) |
|
|
857
|
+
| Can't delete/rename server-side mailboxes or mutate drafts *via AppleScript* | Mail.app's AppleScript bridge can only `delete`/`rename` **local "On My Mac"** mailboxes and cannot delete/move drafts — it throws `AppleEvent handler failed` for IMAP/Gmail/Workspace/iCloud/Exchange mailboxes (the GUI can do it). Without IMAP configured, `delete-mailbox`/`rename-mailbox`/`delete-message`/`move-message` return a clear "do it in Mail.app directly" error instead of a generic failure. With the [IMAP backend](#imap-backend--opt-in) configured for the account, these operations run via IMAP and succeed. ([#42](https://github.com/sweetrb/apple-mail-mcp/issues/42)) |
|
|
853
858
|
| In-memory templates | Email templates are not persisted across server restarts |
|
|
854
859
|
| Numeric-only message IDs | Message IDs must contain only digits (validated by schema) |
|
|
855
860
|
| Batch size cap | Batch operations are limited to 100 messages per request |
|
package/build/index.js
CHANGED
|
@@ -25,17 +25,25 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
25
25
|
import { z } from "zod";
|
|
26
26
|
import { AppleMailManager } from "./services/appleMailManager.js";
|
|
27
27
|
import { sendViaSmtp } from "./services/smtpMailer.js";
|
|
28
|
-
import { isImapAccount, imapSearchMessages, imapListMessages, imapCreateMailbox, imapDeleteMailbox, imapRenameMailbox, } from "./services/imapClient.js";
|
|
28
|
+
import { isImapAccount, imapSearchMessages, imapListMessages, imapCreateMailbox, imapDeleteMailbox, imapRenameMailbox, decodeImapId, imapGetMessage, imapMarkRead, imapMarkUnread, imapFlagMessage, imapUnflagMessage, imapMoveMessageById, imapDeleteMessageById, } from "./services/imapClient.js";
|
|
29
29
|
import { createSerialGate } from "./utils/serialize.js";
|
|
30
30
|
// =============================================================================
|
|
31
31
|
// Shared Validation Schemas
|
|
32
32
|
// =============================================================================
|
|
33
|
-
/**
|
|
34
|
-
*
|
|
35
|
-
const
|
|
36
|
-
/**
|
|
33
|
+
/** AppleScript message IDs are always numeric. Enforced to prevent AppleScript
|
|
34
|
+
* injection via the `whose id is ${id}` interpolation. */
|
|
35
|
+
const NUMERIC_MESSAGE_ID_SCHEMA = z.string().regex(/^\d+$/, "Message ID must be numeric");
|
|
36
|
+
/** A single-message id is EITHER an AppleScript numeric id OR an IMAP composite
|
|
37
|
+
* token (`imap:<base64url>`, emitted by the IMAP read path). The IMAP form is
|
|
38
|
+
* base64url so it stays injection-safe; it never reaches AppleScript (it's
|
|
39
|
+
* decoded and routed to IMAP instead). */
|
|
40
|
+
const MESSAGE_ID_SCHEMA = z
|
|
41
|
+
.string()
|
|
42
|
+
.regex(/^(\d+|imap:[A-Za-z0-9_-]+)$/, "Message ID must be numeric or an IMAP id (imap:…)");
|
|
43
|
+
/** Batch operations are AppleScript-only and capped to prevent unbounded loops /
|
|
44
|
+
* DoS. IMAP ids are rejected here — use the single-message tools for those. */
|
|
37
45
|
const BATCH_IDS_SCHEMA = z
|
|
38
|
-
.array(
|
|
46
|
+
.array(NUMERIC_MESSAGE_ID_SCHEMA)
|
|
39
47
|
.min(1, "At least one message ID is required")
|
|
40
48
|
.max(100, "Cannot process more than 100 messages in a single batch");
|
|
41
49
|
/** Date filter strings must look like natural-language dates (e.g. "March 1, 2026").
|
|
@@ -195,7 +203,14 @@ server.tool("get-message", {
|
|
|
195
203
|
.boolean()
|
|
196
204
|
.optional()
|
|
197
205
|
.describe("Return the HTML body (extracted from the message source) instead of plain text"),
|
|
198
|
-
}, withErrorHandling(({ id, preferHtml }) => {
|
|
206
|
+
}, withErrorHandling(async ({ id, preferHtml }) => {
|
|
207
|
+
// IMAP id (imap:…) → fetch via IMAP (#43 Phase 3); else AppleScript.
|
|
208
|
+
if (decodeImapId(id)) {
|
|
209
|
+
const r = await imapGetMessage(id, preferHtml === true);
|
|
210
|
+
return r.success
|
|
211
|
+
? successResponse(r.info ?? "")
|
|
212
|
+
: errorResponse(r.error ?? `Message with ID "${id}" not found`);
|
|
213
|
+
}
|
|
199
214
|
// Only fetch/parse the raw source when HTML is actually requested (#32).
|
|
200
215
|
const content = mailManager.getMessageContent(id, preferHtml === true);
|
|
201
216
|
if (!content) {
|
|
@@ -364,7 +379,13 @@ server.tool("forward-message", {
|
|
|
364
379
|
// --- mark-as-read ---
|
|
365
380
|
server.tool("mark-as-read", {
|
|
366
381
|
id: MESSAGE_ID_SCHEMA,
|
|
367
|
-
}, withErrorHandling(({ id }) => {
|
|
382
|
+
}, withErrorHandling(async ({ id }) => {
|
|
383
|
+
if (decodeImapId(id)) {
|
|
384
|
+
const r = await imapMarkRead(id);
|
|
385
|
+
return r.success
|
|
386
|
+
? successResponse("Message marked as read")
|
|
387
|
+
: errorResponse(r.error ?? `Failed to mark message "${id}" as read`);
|
|
388
|
+
}
|
|
368
389
|
const success = mailManager.markAsRead(id);
|
|
369
390
|
if (!success) {
|
|
370
391
|
return errorResponse(`Failed to mark message "${id}" as read`);
|
|
@@ -374,7 +395,13 @@ server.tool("mark-as-read", {
|
|
|
374
395
|
// --- mark-as-unread ---
|
|
375
396
|
server.tool("mark-as-unread", {
|
|
376
397
|
id: MESSAGE_ID_SCHEMA,
|
|
377
|
-
}, withErrorHandling(({ id }) => {
|
|
398
|
+
}, withErrorHandling(async ({ id }) => {
|
|
399
|
+
if (decodeImapId(id)) {
|
|
400
|
+
const r = await imapMarkUnread(id);
|
|
401
|
+
return r.success
|
|
402
|
+
? successResponse("Message marked as unread")
|
|
403
|
+
: errorResponse(r.error ?? `Failed to mark message "${id}" as unread`);
|
|
404
|
+
}
|
|
378
405
|
const success = mailManager.markAsUnread(id);
|
|
379
406
|
if (!success) {
|
|
380
407
|
return errorResponse(`Failed to mark message "${id}" as unread`);
|
|
@@ -384,7 +411,13 @@ server.tool("mark-as-unread", {
|
|
|
384
411
|
// --- flag-message ---
|
|
385
412
|
server.tool("flag-message", {
|
|
386
413
|
id: MESSAGE_ID_SCHEMA,
|
|
387
|
-
}, withErrorHandling(({ id }) => {
|
|
414
|
+
}, withErrorHandling(async ({ id }) => {
|
|
415
|
+
if (decodeImapId(id)) {
|
|
416
|
+
const r = await imapFlagMessage(id);
|
|
417
|
+
return r.success
|
|
418
|
+
? successResponse("Message flagged")
|
|
419
|
+
: errorResponse(r.error ?? `Failed to flag message "${id}"`);
|
|
420
|
+
}
|
|
388
421
|
const success = mailManager.flagMessage(id);
|
|
389
422
|
if (!success) {
|
|
390
423
|
return errorResponse(`Failed to flag message "${id}"`);
|
|
@@ -394,7 +427,13 @@ server.tool("flag-message", {
|
|
|
394
427
|
// --- unflag-message ---
|
|
395
428
|
server.tool("unflag-message", {
|
|
396
429
|
id: MESSAGE_ID_SCHEMA,
|
|
397
|
-
}, withErrorHandling(({ id }) => {
|
|
430
|
+
}, withErrorHandling(async ({ id }) => {
|
|
431
|
+
if (decodeImapId(id)) {
|
|
432
|
+
const r = await imapUnflagMessage(id);
|
|
433
|
+
return r.success
|
|
434
|
+
? successResponse("Message unflagged")
|
|
435
|
+
: errorResponse(r.error ?? `Failed to unflag message "${id}"`);
|
|
436
|
+
}
|
|
398
437
|
const success = mailManager.unflagMessage(id);
|
|
399
438
|
if (!success) {
|
|
400
439
|
return errorResponse(`Failed to unflag message "${id}"`);
|
|
@@ -404,7 +443,13 @@ server.tool("unflag-message", {
|
|
|
404
443
|
// --- delete-message ---
|
|
405
444
|
server.tool("delete-message", {
|
|
406
445
|
id: MESSAGE_ID_SCHEMA,
|
|
407
|
-
}, withErrorHandling(({ id }) => {
|
|
446
|
+
}, withErrorHandling(async ({ id }) => {
|
|
447
|
+
if (decodeImapId(id)) {
|
|
448
|
+
const r = await imapDeleteMessageById(id);
|
|
449
|
+
return r.success
|
|
450
|
+
? successResponse("Message deleted")
|
|
451
|
+
: errorResponse(r.error ?? `Failed to delete message "${id}"`);
|
|
452
|
+
}
|
|
408
453
|
const { success, error } = mailManager.deleteMessage(id);
|
|
409
454
|
if (!success) {
|
|
410
455
|
return errorResponse(error || `Failed to delete message "${id}"`);
|
|
@@ -416,7 +461,13 @@ server.tool("move-message", {
|
|
|
416
461
|
id: MESSAGE_ID_SCHEMA,
|
|
417
462
|
mailbox: z.string().min(1, "Destination mailbox is required"),
|
|
418
463
|
account: z.string().optional().describe("Account containing the destination mailbox"),
|
|
419
|
-
}, withErrorHandling(({ id, mailbox, account }) => {
|
|
464
|
+
}, withErrorHandling(async ({ id, mailbox, account }) => {
|
|
465
|
+
if (decodeImapId(id)) {
|
|
466
|
+
const r = await imapMoveMessageById(id, mailbox);
|
|
467
|
+
return r.success
|
|
468
|
+
? successResponse(`Message moved to "${mailbox}"`)
|
|
469
|
+
: errorResponse(r.error ?? `Failed to move message to "${mailbox}"`);
|
|
470
|
+
}
|
|
420
471
|
const { success, error } = mailManager.moveMessage(id, mailbox, account);
|
|
421
472
|
if (!success) {
|
|
422
473
|
return errorResponse(error || `Failed to move message to "${mailbox}"`);
|
|
@@ -54,6 +54,25 @@ export declare function isPathWithinAllowedRoots(resolvedPath: string): boolean;
|
|
|
54
54
|
* Exported for unit testing.
|
|
55
55
|
*/
|
|
56
56
|
export declare function describeMailboxOpError(op: "delete" | "rename", raw: string): string;
|
|
57
|
+
/** Env var to pin the default account (matched by account name or email). */
|
|
58
|
+
export declare const DEFAULT_ACCOUNT_ENV = "APPLE_MAIL_MCP_DEFAULT_ACCOUNT";
|
|
59
|
+
/**
|
|
60
|
+
* Choose the account to use when a tool call omits `account`.
|
|
61
|
+
*
|
|
62
|
+
* Priority: explicit `override` (by name or email) → Mail's default-send
|
|
63
|
+
* account *if enabled* → first enabled account → first account → null. The key
|
|
64
|
+
* guarantee (issue #47): a **disabled** account is never chosen implicitly — it
|
|
65
|
+
* can only be selected via an explicit override (deliberate user intent) or as
|
|
66
|
+
* a last resort when no account is enabled. This prevents operations silently
|
|
67
|
+
* landing in a configured-but-disabled account (e.g. an unused iCloud account
|
|
68
|
+
* that's still addressable via AppleScript).
|
|
69
|
+
*
|
|
70
|
+
* Pure/exported for unit testing.
|
|
71
|
+
*/
|
|
72
|
+
export declare function chooseDefaultAccount(accounts: Account[], opts?: {
|
|
73
|
+
override?: string;
|
|
74
|
+
defaultSendEmail?: string;
|
|
75
|
+
}): string | null;
|
|
57
76
|
export declare function escapeForAppleScript(text: string): string;
|
|
58
77
|
/**
|
|
59
78
|
* Emits AppleScript that builds a date into the variable `varName` from numeric
|
|
@@ -135,9 +154,11 @@ export declare class AppleMailManager {
|
|
|
135
154
|
*/
|
|
136
155
|
private invalidateCache;
|
|
137
156
|
/**
|
|
138
|
-
* Resolves the account to use for an operation.
|
|
139
|
-
*
|
|
140
|
-
*
|
|
157
|
+
* Resolves the account to use for an operation when the caller omits one.
|
|
158
|
+
*
|
|
159
|
+
* Order (see chooseDefaultAccount): the APPLE_MAIL_MCP_DEFAULT_ACCOUNT env
|
|
160
|
+
* override → Mail.app's configured default-send account (if enabled) → the
|
|
161
|
+
* first enabled account. A disabled account is never chosen implicitly (#47).
|
|
141
162
|
*/
|
|
142
163
|
private resolveAccount;
|
|
143
164
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"appleMailManager.d.ts","sourceRoot":"","sources":["../../src/services/appleMailManager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAQH,OAAO,KAAK,EACV,OAAO,EACP,cAAc,EACd,OAAO,EACP,OAAO,EACP,UAAU,EACV,iBAAiB,EACjB,SAAS,EAET,oBAAoB,EACpB,UAAU,EACV,qBAAqB,EACrB,QAAQ,EACR,OAAO,EACP,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACb,MAAM,YAAY,CAAC;AA2DpB;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAK7F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GACd;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,iBAAiB,CAAA;CAAE,CAsCrD;AAqBD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAKtE;AAWD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,QAAQ,GAAG,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAOnF;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAczD;AAiED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,GAAG,MAAM,CAWrE;AA2CD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,sBAAsB;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CAoB5E;AAED,qBAAa,gBAAgB;IAC3B;;OAEG;IACH,OAAO,CAAC,cAAc,CAAuB;IAE7C;;;;OAIG;IACH,OAAO,CAAC,KAAK,CAGX;IAEF,8CAA8C;IAC9C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;IAEvC;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAUzB;;;;OAIG;IACH,OAAO,CAAC,qBAAqB;IAW7B;;;OAGG;IACH,OAAO,CAAC,eAAe;IAKvB
|
|
1
|
+
{"version":3,"file":"appleMailManager.d.ts","sourceRoot":"","sources":["../../src/services/appleMailManager.ts"],"names":[],"mappings":"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"}
|
|
@@ -181,6 +181,41 @@ export function describeMailboxOpError(op, raw) {
|
|
|
181
181
|
}
|
|
182
182
|
return trimmed || `Failed to ${op} mailbox`;
|
|
183
183
|
}
|
|
184
|
+
/** Env var to pin the default account (matched by account name or email). */
|
|
185
|
+
export const DEFAULT_ACCOUNT_ENV = "APPLE_MAIL_MCP_DEFAULT_ACCOUNT";
|
|
186
|
+
/**
|
|
187
|
+
* Choose the account to use when a tool call omits `account`.
|
|
188
|
+
*
|
|
189
|
+
* Priority: explicit `override` (by name or email) → Mail's default-send
|
|
190
|
+
* account *if enabled* → first enabled account → first account → null. The key
|
|
191
|
+
* guarantee (issue #47): a **disabled** account is never chosen implicitly — it
|
|
192
|
+
* can only be selected via an explicit override (deliberate user intent) or as
|
|
193
|
+
* a last resort when no account is enabled. This prevents operations silently
|
|
194
|
+
* landing in a configured-but-disabled account (e.g. an unused iCloud account
|
|
195
|
+
* that's still addressable via AppleScript).
|
|
196
|
+
*
|
|
197
|
+
* Pure/exported for unit testing.
|
|
198
|
+
*/
|
|
199
|
+
export function chooseDefaultAccount(accounts, opts = {}) {
|
|
200
|
+
const norm = (s) => s.trim().toLowerCase();
|
|
201
|
+
const override = opts.override?.trim();
|
|
202
|
+
if (override) {
|
|
203
|
+
const o = norm(override);
|
|
204
|
+
const m = accounts.find((a) => norm(a.name) === o || norm(a.email) === o);
|
|
205
|
+
if (m)
|
|
206
|
+
return m.name; // honor an explicit pin even if that account is disabled
|
|
207
|
+
}
|
|
208
|
+
if (opts.defaultSendEmail) {
|
|
209
|
+
const e = norm(opts.defaultSendEmail);
|
|
210
|
+
const m = accounts.find((a) => norm(a.email) === e);
|
|
211
|
+
if (m && m.enabled)
|
|
212
|
+
return m.name;
|
|
213
|
+
}
|
|
214
|
+
const firstEnabled = accounts.find((a) => a.enabled);
|
|
215
|
+
if (firstEnabled)
|
|
216
|
+
return firstEnabled.name;
|
|
217
|
+
return accounts[0]?.name ?? null;
|
|
218
|
+
}
|
|
184
219
|
export function escapeForAppleScript(text) {
|
|
185
220
|
if (!text)
|
|
186
221
|
return "";
|
|
@@ -396,16 +431,20 @@ export class AppleMailManager {
|
|
|
396
431
|
this.cache.mailboxNames.clear();
|
|
397
432
|
}
|
|
398
433
|
/**
|
|
399
|
-
* Resolves the account to use for an operation.
|
|
400
|
-
*
|
|
401
|
-
*
|
|
434
|
+
* Resolves the account to use for an operation when the caller omits one.
|
|
435
|
+
*
|
|
436
|
+
* Order (see chooseDefaultAccount): the APPLE_MAIL_MCP_DEFAULT_ACCOUNT env
|
|
437
|
+
* override → Mail.app's configured default-send account (if enabled) → the
|
|
438
|
+
* first enabled account. A disabled account is never chosen implicitly (#47).
|
|
402
439
|
*/
|
|
403
440
|
resolveAccount(account) {
|
|
404
441
|
if (account)
|
|
405
442
|
return account;
|
|
406
443
|
if (this.defaultAccount)
|
|
407
444
|
return this.defaultAccount;
|
|
408
|
-
|
|
445
|
+
const accounts = this.getCachedAccounts();
|
|
446
|
+
// Mail.app's default send account (inspect a throwaway outgoing message).
|
|
447
|
+
let defaultSendEmail;
|
|
409
448
|
const defaultResult = executeAppleScript(buildAppLevelScript(`
|
|
410
449
|
set newMsg to make new outgoing message
|
|
411
450
|
set fromAddr to sender of newMsg
|
|
@@ -413,24 +452,22 @@ export class AppleMailManager {
|
|
|
413
452
|
return fromAddr
|
|
414
453
|
`));
|
|
415
454
|
if (defaultResult.success && defaultResult.output.trim()) {
|
|
416
|
-
// sender returns "Name <email>" —
|
|
455
|
+
// sender returns "Name <email>" — pull out the address
|
|
417
456
|
const senderOutput = defaultResult.output.trim();
|
|
418
457
|
const emailMatch = senderOutput.match(/<([^>]+)>/);
|
|
419
|
-
|
|
420
|
-
const accounts = this.getCachedAccounts();
|
|
421
|
-
const matchedAccount = accounts.find((a) => a.email.toLowerCase() === defaultEmail.toLowerCase());
|
|
422
|
-
if (matchedAccount) {
|
|
423
|
-
this.defaultAccount = matchedAccount.name;
|
|
424
|
-
return this.defaultAccount;
|
|
425
|
-
}
|
|
458
|
+
defaultSendEmail = emailMatch ? emailMatch[1] : senderOutput;
|
|
426
459
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
460
|
+
const chosen = chooseDefaultAccount(accounts, {
|
|
461
|
+
override: process.env[DEFAULT_ACCOUNT_ENV],
|
|
462
|
+
defaultSendEmail,
|
|
463
|
+
});
|
|
464
|
+
if (chosen) {
|
|
465
|
+
this.defaultAccount = chosen;
|
|
466
|
+
return chosen;
|
|
432
467
|
}
|
|
433
|
-
return
|
|
468
|
+
// No accounts at all — return something rather than throw; downstream
|
|
469
|
+
// AppleScript will surface a clear "account not found".
|
|
470
|
+
return accounts[0]?.name ?? "iCloud";
|
|
434
471
|
}
|
|
435
472
|
/**
|
|
436
473
|
* Resolves a mailbox name to its actual name in the account.
|
|
@@ -42,6 +42,7 @@ interface ImapMessage {
|
|
|
42
42
|
uid: number;
|
|
43
43
|
envelope?: ImapEnvelope;
|
|
44
44
|
flags?: Set<string>;
|
|
45
|
+
source?: Buffer | string;
|
|
45
46
|
}
|
|
46
47
|
interface MailboxLock {
|
|
47
48
|
release: () => void;
|
|
@@ -50,6 +51,9 @@ interface ImapMailboxListing {
|
|
|
50
51
|
path: string;
|
|
51
52
|
name: string;
|
|
52
53
|
}
|
|
54
|
+
type FlagOpts = {
|
|
55
|
+
uid: boolean;
|
|
56
|
+
};
|
|
53
57
|
export interface ImapClientLike {
|
|
54
58
|
connect(): Promise<void>;
|
|
55
59
|
getMailboxLock(path: string): Promise<MailboxLock>;
|
|
@@ -59,6 +63,9 @@ export interface ImapClientLike {
|
|
|
59
63
|
fetch(range: string, query: Record<string, unknown>, opts: {
|
|
60
64
|
uid: true;
|
|
61
65
|
}): AsyncIterable<ImapMessage>;
|
|
66
|
+
fetchOne(range: string, query: Record<string, unknown>, opts: {
|
|
67
|
+
uid: true;
|
|
68
|
+
}): Promise<ImapMessage | false>;
|
|
62
69
|
list(): Promise<ImapMailboxListing[]>;
|
|
63
70
|
mailboxCreate(path: string): Promise<{
|
|
64
71
|
path: string;
|
|
@@ -71,8 +78,18 @@ export interface ImapClientLike {
|
|
|
71
78
|
mailboxDelete(path: string): Promise<{
|
|
72
79
|
path: string;
|
|
73
80
|
}>;
|
|
81
|
+
messageFlagsAdd(range: number[], flags: string[], opts: FlagOpts): Promise<boolean>;
|
|
82
|
+
messageFlagsRemove(range: number[], flags: string[], opts: FlagOpts): Promise<boolean>;
|
|
83
|
+
messageMove(range: number[], destination: string, opts: FlagOpts): Promise<unknown>;
|
|
84
|
+
messageDelete(range: number[], opts: FlagOpts): Promise<boolean>;
|
|
74
85
|
logout(): Promise<void>;
|
|
75
86
|
}
|
|
87
|
+
export declare function encodeImapId(account: string, path: string, uid: number): string;
|
|
88
|
+
export declare function decodeImapId(id: string): {
|
|
89
|
+
account: string;
|
|
90
|
+
path: string;
|
|
91
|
+
uid: number;
|
|
92
|
+
} | null;
|
|
76
93
|
export type ImapConnect = (cfg: ImapConfig) => Promise<ImapClientLike>;
|
|
77
94
|
/** True only when IMAP is configured AND the explicit `account` matches it. */
|
|
78
95
|
export declare function isImapAccount(account: string | undefined, env?: NodeJS.ProcessEnv): boolean;
|
|
@@ -104,5 +121,22 @@ export declare function imapRenameMailbox(oldName: string, newName: string, deps
|
|
|
104
121
|
connect?: ImapConnect;
|
|
105
122
|
config?: ImapConfig;
|
|
106
123
|
}): Promise<ImapOpResult>;
|
|
124
|
+
/** Read a message by composite IMAP id; returns "Subject: …\n\n<body>". */
|
|
125
|
+
export declare function imapGetMessage(id: string, preferHtml: boolean, deps?: {
|
|
126
|
+
connect?: ImapConnect;
|
|
127
|
+
config?: ImapConfig;
|
|
128
|
+
}): Promise<ImapOpResult>;
|
|
129
|
+
export declare const imapMarkRead: (id: string, deps?: {}) => Promise<ImapOpResult>;
|
|
130
|
+
export declare const imapMarkUnread: (id: string, deps?: {}) => Promise<ImapOpResult>;
|
|
131
|
+
export declare const imapFlagMessage: (id: string, deps?: {}) => Promise<ImapOpResult>;
|
|
132
|
+
export declare const imapUnflagMessage: (id: string, deps?: {}) => Promise<ImapOpResult>;
|
|
133
|
+
export declare function imapMoveMessageById(id: string, destMailbox: string, deps?: {
|
|
134
|
+
connect?: ImapConnect;
|
|
135
|
+
config?: ImapConfig;
|
|
136
|
+
}): Promise<ImapOpResult>;
|
|
137
|
+
export declare function imapDeleteMessageById(id: string, deps?: {
|
|
138
|
+
connect?: ImapConnect;
|
|
139
|
+
config?: ImapConfig;
|
|
140
|
+
}): Promise<ImapOpResult>;
|
|
107
141
|
export {};
|
|
108
142
|
//# sourceMappingURL=imapClient.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"imapClient.d.ts","sourceRoot":"","sources":["../../src/services/imapClient.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"imapClient.d.ts","sourceRoot":"","sources":["../../src/services/imapClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* IMAP backend
|
|
2
|
+
* IMAP backend (issue #43).
|
|
3
3
|
*
|
|
4
4
|
* AppleScript-over-Mail.app is the default. When an account is explicitly
|
|
5
|
-
* configured for IMAP (env below),
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* configured for IMAP (env below), operations route here instead:
|
|
6
|
+
* - read: search-messages / list-messages (server-side SEARCH, orders of
|
|
7
|
+
* magnitude faster and correct on large Gmail mailboxes where
|
|
8
|
+
* AppleScript times out with a false-empty) and get-message;
|
|
9
|
+
* - folders: create / rename / delete-mailbox (work on the server hierarchy
|
|
10
|
+
* that AppleScript can't touch — #42);
|
|
11
|
+
* - message: mark/flag/move/delete, keyed by the composite `imap:` id the
|
|
12
|
+
* read path emits (see encodeImapId/decodeImapId below).
|
|
13
|
+
* Everything is opt-in and additive; un-configured accounts use AppleScript.
|
|
9
14
|
*
|
|
10
15
|
* Opt-in via env (mirrors the SMTP transport pattern):
|
|
11
16
|
* APPLE_MAIL_MCP_IMAP_USER (required — enables IMAP; the login address)
|
|
@@ -19,6 +24,7 @@
|
|
|
19
24
|
*/
|
|
20
25
|
import { ImapFlow } from "imapflow";
|
|
21
26
|
import { readKeychainPassword } from "../services/smtpMailer.js";
|
|
27
|
+
import { extractHtmlBody, extractTextBody } from "../utils/mimeParse.js";
|
|
22
28
|
export const IMAP_ENV = {
|
|
23
29
|
user: "APPLE_MAIL_MCP_IMAP_USER",
|
|
24
30
|
account: "APPLE_MAIL_MCP_IMAP_ACCOUNT",
|
|
@@ -28,6 +34,30 @@ export const IMAP_ENV = {
|
|
|
28
34
|
keychainService: "APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE",
|
|
29
35
|
keychainAccount: "APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT",
|
|
30
36
|
};
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Composite IMAP message id (Phase 3): a self-describing token the IMAP read
|
|
39
|
+
// path emits so the same id round-trips back to get-message and the message
|
|
40
|
+
// mutations. AppleScript message ids are bare numbers; an IMAP id is
|
|
41
|
+
// `imap:<base64url({a:account,p:mailboxPath,u:uid})>`. UIDs are per-mailbox, so
|
|
42
|
+
// the mailbox path must travel with the uid. base64url keeps it schema-safe.
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
export function encodeImapId(account, path, uid) {
|
|
45
|
+
const payload = Buffer.from(JSON.stringify({ a: account, p: path, u: uid }), "utf8").toString("base64url");
|
|
46
|
+
return `imap:${payload}`;
|
|
47
|
+
}
|
|
48
|
+
export function decodeImapId(id) {
|
|
49
|
+
if (!id || !id.startsWith("imap:"))
|
|
50
|
+
return null;
|
|
51
|
+
try {
|
|
52
|
+
const obj = JSON.parse(Buffer.from(id.slice("imap:".length), "base64url").toString("utf8"));
|
|
53
|
+
if (typeof obj.u !== "number" || typeof obj.p !== "string")
|
|
54
|
+
return null;
|
|
55
|
+
return { account: String(obj.a ?? ""), path: obj.p, uid: obj.u };
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
31
61
|
/** True only when IMAP is configured AND the explicit `account` matches it. */
|
|
32
62
|
export function isImapAccount(account, env = process.env) {
|
|
33
63
|
const user = env[IMAP_ENV.user]?.trim();
|
|
@@ -119,7 +149,7 @@ function buildCriteria(a, listMode) {
|
|
|
119
149
|
c.all = true;
|
|
120
150
|
return c;
|
|
121
151
|
}
|
|
122
|
-
function formatRow(m) {
|
|
152
|
+
function formatRow(m, account, path) {
|
|
123
153
|
const env = m.envelope ?? {};
|
|
124
154
|
const subject = env.subject || "(no subject)";
|
|
125
155
|
const a = env.from?.[0];
|
|
@@ -130,7 +160,9 @@ function formatRow(m) {
|
|
|
130
160
|
: "(unknown)";
|
|
131
161
|
const date = env.date ? new Date(env.date).toLocaleDateString() : "";
|
|
132
162
|
const read = m.flags?.has("\\Seen") ? "read" : "unread";
|
|
133
|
-
|
|
163
|
+
// Emit the self-describing IMAP id so get-message and the message mutations
|
|
164
|
+
// can route this row back to IMAP (Phase 3).
|
|
165
|
+
return ` - ID: ${encodeImapId(account, path, m.uid)} | ${date} | ${subject} (from: ${from}) [${read}]`;
|
|
134
166
|
}
|
|
135
167
|
async function run(args, listMode, deps) {
|
|
136
168
|
const cfg = deps.config ?? resolveImapConfig();
|
|
@@ -153,13 +185,13 @@ async function run(args, listMode, deps) {
|
|
|
153
185
|
.slice(offset, offset + limit);
|
|
154
186
|
const byUid = new Map();
|
|
155
187
|
for await (const msg of client.fetch(newest.join(","), { envelope: true, flags: true }, { uid: true })) {
|
|
156
|
-
byUid.set(msg.uid, formatRow(msg));
|
|
188
|
+
byUid.set(msg.uid, formatRow(msg, cfg.accountLabel, path));
|
|
157
189
|
}
|
|
158
190
|
const rows = newest.map((u) => byUid.get(u)).filter((r) => Boolean(r));
|
|
159
191
|
const verb = listMode ? "listed" : "matched";
|
|
160
192
|
return (`Found ${rows.length} message(s) via IMAP (server-side, account ${cfg.accountLabel}, mailbox "${path}"; ${uids.length} total ${verb}):\n` +
|
|
161
193
|
rows.join("\n") +
|
|
162
|
-
`\n\nNote: IDs
|
|
194
|
+
`\n\nNote: these IMAP IDs (imap:…) work with get-message and the message mutations (mark/flag/move/delete-message), which route back to IMAP.`);
|
|
163
195
|
}
|
|
164
196
|
finally {
|
|
165
197
|
lock.release();
|
|
@@ -258,3 +290,100 @@ export function imapRenameMailbox(oldName, newName, deps = {}) {
|
|
|
258
290
|
}
|
|
259
291
|
});
|
|
260
292
|
}
|
|
293
|
+
// ===========================================================================
|
|
294
|
+
// Phase 3 — message-level operations by composite IMAP id (issue #43)
|
|
295
|
+
//
|
|
296
|
+
// get-message / mark / flag / move / delete-message route here when the message
|
|
297
|
+
// id is an `imap:` token (emitted by the IMAP read path). The token carries the
|
|
298
|
+
// mailbox path + UID, so the op opens that mailbox and acts on the UID.
|
|
299
|
+
// ===========================================================================
|
|
300
|
+
/** Connect, open the message's mailbox, run `fn`, release + log out. */
|
|
301
|
+
async function withMailbox(path, deps, fn) {
|
|
302
|
+
return withClient(deps, async (client) => {
|
|
303
|
+
const lock = await client.getMailboxLock(path);
|
|
304
|
+
try {
|
|
305
|
+
return await fn(client);
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
lock.release();
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
/** Read a message by composite IMAP id; returns "Subject: …\n\n<body>". */
|
|
313
|
+
export async function imapGetMessage(id, preferHtml, deps = {}) {
|
|
314
|
+
const ref = decodeImapId(id);
|
|
315
|
+
if (!ref)
|
|
316
|
+
return { success: false, error: `Not an IMAP message id: "${id}".` };
|
|
317
|
+
return withMailbox(ref.path, deps, async (client) => {
|
|
318
|
+
const msg = await client.fetchOne(String(ref.uid), { envelope: true, source: true }, { uid: true });
|
|
319
|
+
if (!msg)
|
|
320
|
+
return { success: false, error: `IMAP message UID ${ref.uid} not found in "${ref.path}".` };
|
|
321
|
+
const subject = msg.envelope?.subject || "(no subject)";
|
|
322
|
+
const src = msg.source ? msg.source.toString() : "";
|
|
323
|
+
const body = (preferHtml ? extractHtmlBody(src) : extractTextBody(src)) ??
|
|
324
|
+
extractTextBody(src) ??
|
|
325
|
+
extractHtmlBody(src) ??
|
|
326
|
+
"(no readable body)";
|
|
327
|
+
return { success: true, info: `Subject: ${subject}\n\n${body}` };
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
function flagOp(id, flag, add, deps) {
|
|
331
|
+
const ref = decodeImapId(id);
|
|
332
|
+
if (!ref)
|
|
333
|
+
return Promise.resolve({ success: false, error: `Not an IMAP message id: "${id}".` });
|
|
334
|
+
return withMailbox(ref.path, deps, async (client) => {
|
|
335
|
+
try {
|
|
336
|
+
const ok = add
|
|
337
|
+
? await client.messageFlagsAdd([ref.uid], [flag], { uid: true })
|
|
338
|
+
: await client.messageFlagsRemove([ref.uid], [flag], { uid: true });
|
|
339
|
+
if (!ok)
|
|
340
|
+
return { success: false, error: `IMAP flag update returned false for UID ${ref.uid}.` };
|
|
341
|
+
return { success: true };
|
|
342
|
+
}
|
|
343
|
+
catch (e) {
|
|
344
|
+
return { success: false, error: `IMAP flag update failed for UID ${ref.uid}: ${errText(e)}` };
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
export const imapMarkRead = (id, deps = {}) => flagOp(id, "\\Seen", true, deps);
|
|
349
|
+
export const imapMarkUnread = (id, deps = {}) => flagOp(id, "\\Seen", false, deps);
|
|
350
|
+
export const imapFlagMessage = (id, deps = {}) => flagOp(id, "\\Flagged", true, deps);
|
|
351
|
+
export const imapUnflagMessage = (id, deps = {}) => flagOp(id, "\\Flagged", false, deps);
|
|
352
|
+
export async function imapMoveMessageById(id, destMailbox, deps = {}) {
|
|
353
|
+
const ref = decodeImapId(id);
|
|
354
|
+
if (!ref)
|
|
355
|
+
return { success: false, error: `Not an IMAP message id: "${id}".` };
|
|
356
|
+
return withClient(deps, async (client) => {
|
|
357
|
+
const destPath = (await findMailboxPath(client, destMailbox)) ?? resolveMailboxPath(destMailbox, "list");
|
|
358
|
+
const lock = await client.getMailboxLock(ref.path);
|
|
359
|
+
try {
|
|
360
|
+
await client.messageMove([ref.uid], destPath, { uid: true });
|
|
361
|
+
return { success: true, info: `Moved UID ${ref.uid} to "${destPath}" via IMAP.` };
|
|
362
|
+
}
|
|
363
|
+
catch (e) {
|
|
364
|
+
return {
|
|
365
|
+
success: false,
|
|
366
|
+
error: `IMAP move failed for UID ${ref.uid} -> "${destPath}": ${errText(e)}`,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
finally {
|
|
370
|
+
lock.release();
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
export async function imapDeleteMessageById(id, deps = {}) {
|
|
375
|
+
const ref = decodeImapId(id);
|
|
376
|
+
if (!ref)
|
|
377
|
+
return { success: false, error: `Not an IMAP message id: "${id}".` };
|
|
378
|
+
return withMailbox(ref.path, deps, async (client) => {
|
|
379
|
+
try {
|
|
380
|
+
const ok = await client.messageDelete([ref.uid], { uid: true });
|
|
381
|
+
if (!ok)
|
|
382
|
+
return { success: false, error: `IMAP delete returned false for UID ${ref.uid}.` };
|
|
383
|
+
return { success: true, info: `Deleted UID ${ref.uid} from "${ref.path}" via IMAP.` };
|
|
384
|
+
}
|
|
385
|
+
catch (e) {
|
|
386
|
+
return { success: false, error: `IMAP delete failed for UID ${ref.uid}: ${errText(e)}` };
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
}
|
|
@@ -43,6 +43,12 @@ export declare function parseMimeAttachments(source: string): MimeAttachmentInfo
|
|
|
43
43
|
* @returns The decoded HTML body, or null if the message has no text/html part
|
|
44
44
|
*/
|
|
45
45
|
export declare function extractHtmlBody(source: string): string | null;
|
|
46
|
+
/**
|
|
47
|
+
* Extract the decoded `text/plain` body from raw MIME source. Mirror of
|
|
48
|
+
* extractHtmlBody; used by the IMAP get-message path (#43 Phase 3) to render a
|
|
49
|
+
* message fetched by UID. Returns null when there's no text/plain part.
|
|
50
|
+
*/
|
|
51
|
+
export declare function extractTextBody(source: string): string | null;
|
|
46
52
|
/**
|
|
47
53
|
* Extract and decode a specific attachment from MIME source by filename.
|
|
48
54
|
* Supports base64, quoted-printable, and 7bit/8bit/binary transfer encodings.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mimeParse.d.ts","sourceRoot":"","sources":["../../src/utils/mimeParse.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,kBAAkB;IACjC,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAmB,SAAQ,kBAAkB;IAC5D,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;CACd;AAyLD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,kBAAkB,EAAE,CAyBzE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAuB7D;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,GACrB,kBAAkB,GAAG,IAAI,CAwB3B"}
|
|
1
|
+
{"version":3,"file":"mimeParse.d.ts","sourceRoot":"","sources":["../../src/utils/mimeParse.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,kBAAkB;IACjC,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAmB,SAAQ,kBAAkB;IAC5D,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;CACd;AAyLD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,kBAAkB,EAAE,CAyBzE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAuB7D;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAuB7D;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,GACrB,kBAAkB,GAAG,IAAI,CAwB3B"}
|
package/build/utils/mimeParse.js
CHANGED
|
@@ -243,6 +243,36 @@ export function extractHtmlBody(source) {
|
|
|
243
243
|
const encoding = getHeader(headers, "Content-Transfer-Encoding");
|
|
244
244
|
return decodeBody(body, encoding).toString("utf8");
|
|
245
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Extract the decoded `text/plain` body from raw MIME source. Mirror of
|
|
248
|
+
* extractHtmlBody; used by the IMAP get-message path (#43 Phase 3) to render a
|
|
249
|
+
* message fetched by UID. Returns null when there's no text/plain part.
|
|
250
|
+
*/
|
|
251
|
+
export function extractTextBody(source) {
|
|
252
|
+
if (!source || !source.trim())
|
|
253
|
+
return null;
|
|
254
|
+
const boundary = extractBoundary(source);
|
|
255
|
+
if (boundary) {
|
|
256
|
+
for (const part of walkLeafParts(source, boundary)) {
|
|
257
|
+
if (extractMimeType(part.headers) === "text/plain") {
|
|
258
|
+
const encoding = getHeader(part.headers, "Content-Transfer-Encoding");
|
|
259
|
+
return decodeBody(part.body, encoding).toString("utf8");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
// Non-multipart: treat as text/plain unless the Content-Type says otherwise.
|
|
265
|
+
const blankLineIdx = source.search(/\r?\n\r?\n/);
|
|
266
|
+
if (blankLineIdx === -1)
|
|
267
|
+
return null;
|
|
268
|
+
const headers = source.substring(0, blankLineIdx);
|
|
269
|
+
const ct = extractMimeType(headers);
|
|
270
|
+
if (ct !== "text/plain" && getHeader(headers, "Content-Type") !== null)
|
|
271
|
+
return null;
|
|
272
|
+
const body = source.substring(blankLineIdx).replace(/^\r?\n\r?\n/, "");
|
|
273
|
+
const encoding = getHeader(headers, "Content-Transfer-Encoding");
|
|
274
|
+
return decodeBody(body, encoding).toString("utf8");
|
|
275
|
+
}
|
|
246
276
|
/**
|
|
247
277
|
* Extract and decode a specific attachment from MIME source by filename.
|
|
248
278
|
* Supports base64, quoted-printable, and 7bit/8bit/binary transfer encodings.
|