apple-mail-mcp 2.19.22 → 2.20.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 +109 -34
- package/build/cli.js +179 -27
- package/build/index.js +430 -85
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -164,11 +164,11 @@ tool, and troubleshooting. Verify any time by running the **`doctor`** tool.
|
|
|
164
164
|
| **Search Messages** | Search by sender, subject, content, date range, read/flagged status — across all accounts |
|
|
165
165
|
| **Read Messages** | Get full email content (plain text or HTML) |
|
|
166
166
|
| **Read Headers** | Get a message's raw RFC 5322 headers — the author's `Date:`, Message-ID, threading ids, `Received:` trace — without downloading the body |
|
|
167
|
-
| **Send Email** | Compose and send new emails (attach by file path or inline base64 content)
|
|
167
|
+
| **Send Email** | Compose and send new emails (attach by file path or inline base64 content; optional `inReplyTo`/`references` threading over SMTP) |
|
|
168
168
|
| **Send Serial Email** | Mail merge — send personalized emails to a list of recipients with {{placeholder}} support |
|
|
169
|
-
| **Create Draft** | Save emails to Drafts folder (attach by file path or inline base64 content)
|
|
170
|
-
| **Reply** | Reply to messages (with reply-all support)
|
|
171
|
-
| **Forward** | Forward messages to new recipients
|
|
169
|
+
| **Create Draft** | Save emails to Drafts folder (attach by file path or inline base64 content; threaded drafts filed over IMAP) |
|
|
170
|
+
| **Reply** | Reply to messages (with reply-all support and attachments) |
|
|
171
|
+
| **Forward** | Forward messages to new recipients (with extra attachments) |
|
|
172
172
|
| **Get Thread** | Group a conversation by normalized subject (across AppleScript or IMAP) |
|
|
173
173
|
| **Mark Read/Unread** | Change read status (single or batch) |
|
|
174
174
|
| **Flag/Unflag** | Flag or unflag messages (single or batch) |
|
|
@@ -402,16 +402,19 @@ Send a new email immediately.
|
|
|
402
402
|
|
|
403
403
|
**⚠️ Safety:** Sends real mail immediately and cannot be unsent. Confirm the recipients, subject, and body with the user before calling.
|
|
404
404
|
|
|
405
|
-
| Parameter | Type | Required | Description
|
|
406
|
-
| ------------- | --------------------------------------- | -------- |
|
|
407
|
-
| `to` | string[] | Yes | Recipient addresses
|
|
408
|
-
| `subject` | string | Yes | Email subject
|
|
409
|
-
| `body` | string | Yes | Email body (plain text)
|
|
410
|
-
| `cc` | string[] | No | CC recipients
|
|
411
|
-
| `bcc` | string[] | No | BCC recipients
|
|
412
|
-
| `account` | string | No | Mail.app account label, or an email-form SMTP From override. An SMTP override must match `APPLE_MAIL_MCP_SMTP_USER`, `APPLE_MAIL_MCP_SMTP_FROM`, or an address in `APPLE_MAIL_MCP_SMTP_ALLOWED_FROM`
|
|
413
|
-
| `attachments` | (string \| {filename, contentBase64})[] | No | Up to 20 attachments: absolute file paths inside the configured read roots (e.g., `"/Users/me/Documents/report.pdf"`) and/or inline `{filename, contentBase64}` objects up to 25 MiB decoded each
|
|
414
|
-
| `transport` | `"applescript"` \| `"smtp"` | No | Send transport. If omitted, **SMTP is used automatically when configured** (otherwise AppleScript). Pass `"smtp"` to require clean MIME, or `"applescript"` to force the Mail.app path — see [SMTP transport](#smtp-transport)
|
|
405
|
+
| Parameter | Type | Required | Description |
|
|
406
|
+
| ------------- | --------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
407
|
+
| `to` | string[] | Yes | Recipient addresses |
|
|
408
|
+
| `subject` | string | Yes | Email subject |
|
|
409
|
+
| `body` | string | Yes | Email body (plain text) |
|
|
410
|
+
| `cc` | string[] | No | CC recipients |
|
|
411
|
+
| `bcc` | string[] | No | BCC recipients |
|
|
412
|
+
| `account` | string | No | Mail.app account label, or an email-form SMTP From override. An SMTP override must match `APPLE_MAIL_MCP_SMTP_USER`, `APPLE_MAIL_MCP_SMTP_FROM`, or an address in `APPLE_MAIL_MCP_SMTP_ALLOWED_FROM` |
|
|
413
|
+
| `attachments` | (string \| {filename, contentBase64})[] | No | Up to 20 attachments: absolute file paths inside the configured read roots (e.g., `"/Users/me/Documents/report.pdf"`) and/or inline `{filename, contentBase64}` objects up to 25 MiB decoded each |
|
|
414
|
+
| `transport` | `"applescript"` \| `"smtp"` | No | Send transport. If omitted, **SMTP is used automatically when configured** (otherwise AppleScript). Pass `"smtp"` to require clean MIME, or `"applescript"` to force the Mail.app path — see [SMTP transport](#smtp-transport) |
|
|
415
|
+
| `replyTo` | string | No | SMTP only: `Reply-To` header, when replies should go somewhere other than the From address. Ignored on the AppleScript transport |
|
|
416
|
+
| `inReplyTo` | string | No | SMTP only (2.20.0): thread the message as a reply — the parent's `Message-ID` token with angle brackets, e.g. `"<abc@mail.example.com>"`. Emitted verbatim, never RFC 2047-encoded. See [Threading headers](#threading-headers-inreplyto--references) |
|
|
417
|
+
| `references` | string[] | No | SMTP only (2.20.0): the `References` chain, oldest first (the parent's own `References` + its `Message-ID`). Defaults to `[inReplyTo]` when omitted |
|
|
415
418
|
|
|
416
419
|
**Example:**
|
|
417
420
|
|
|
@@ -425,6 +428,22 @@ Send a new email immediately.
|
|
|
425
428
|
}
|
|
426
429
|
```
|
|
427
430
|
|
|
431
|
+
##### Threading headers (`inReplyTo` / `references`)
|
|
432
|
+
|
|
433
|
+
`send-email` and `create-draft` accept caller-built threading headers (2.20.0, #267) for
|
|
434
|
+
callers that compose the recipients and quoted text themselves. **Prefer
|
|
435
|
+
`reply-to-message`** — it derives recipients, `In-Reply-To`/`References` and the quote
|
|
436
|
+
from the original, and (since 2.20.0) takes attachments too.
|
|
437
|
+
|
|
438
|
+
- Each value must be a single RFC 5322 msg-id token **with** angle brackets
|
|
439
|
+
(`<left@right>`, no whitespace); anything else is rejected by validation.
|
|
440
|
+
- The headers are always emitted as plain `<id>` tokens — never RFC 2047-encoded, even
|
|
441
|
+
for Outlook/Exchange IDs longer than 76 characters (a long header is folded at
|
|
442
|
+
whitespace between tokens, which is legal and does not alter the id).
|
|
443
|
+
- Mail.app's AppleScript cannot set these headers, so they are **never silently
|
|
444
|
+
dropped**: `send-email` refuses them unless the SMTP transport is in use, and
|
|
445
|
+
`create-draft` files a threaded draft over IMAP instead of through Mail.app (below).
|
|
446
|
+
|
|
428
447
|
##### SMTP transport
|
|
429
448
|
|
|
430
449
|
On macOS 15+ (Sequoia/Tahoe), Mail.app wraps any AppleScript-injected body in
|
|
@@ -783,10 +802,27 @@ Save an email to Drafts without sending.
|
|
|
783
802
|
| `body` | string | Yes | Email body (plain text) |
|
|
784
803
|
| `cc` | string[] | No | CC recipients |
|
|
785
804
|
| `bcc` | string[] | No | BCC recipients |
|
|
786
|
-
| `account` | string | No | Account for draft
|
|
805
|
+
| `account` | string | No | Account for draft. With `inReplyTo`/`references` it must name a configured IMAP account (label or login) |
|
|
787
806
|
| `attachments` | (string \| {filename, contentBase64})[] | No | Up to 20 attachments: absolute file paths inside the configured read roots and/or inline `{filename, contentBase64}` objects up to 25 MiB decoded each |
|
|
788
|
-
|
|
789
|
-
|
|
807
|
+
| `inReplyTo` | string | No | (2.20.0) Parent `Message-ID` token, e.g. `"<abc@mail.example.com>"` — makes this a **threaded draft** (see below) |
|
|
808
|
+
| `references` | string[] | No | (2.20.0) `References` chain, oldest first; defaults to `[inReplyTo]` |
|
|
809
|
+
|
|
810
|
+
**Returns:** Confirmation that draft was created, with `transport` (`applescript`, or `imap` for a threaded draft).
|
|
811
|
+
|
|
812
|
+
**Threaded drafts (2.20.0, #267):** Mail.app's `outgoing message` has no header property,
|
|
813
|
+
so a draft with `inReplyTo`/`references` cannot go through AppleScript. Instead the
|
|
814
|
+
server composes it with the same MIME builder as the SMTP send (attachments included) and
|
|
815
|
+
files it over IMAP into the account's Drafts mailbox (SPECIAL-USE `\Drafts`), flagged
|
|
816
|
+
`\Draft`. The account is `account` when given (it must be a configured IMAP account —
|
|
817
|
+
pass the exact `accountLabel` or login from `list-accounts`/your `APPLE_MAIL_MCP_IMAP_*`
|
|
818
|
+
config, e.g. `"user@icloud.com"`; a generic provider name like `"iCloud"` is not a
|
|
819
|
+
recognized alias and is refused);
|
|
820
|
+
otherwise the IMAP account whose login is the SMTP identity, else the only configured
|
|
821
|
+
IMAP account. When none qualifies — IMAP unconfigured, an unknown `account`, or several
|
|
822
|
+
accounts and no way to choose — the call **fails and files nothing**; it never falls back
|
|
823
|
+
to an unthreaded Mail.app draft. The result names the Drafts `mailbox`, the `account` and
|
|
824
|
+
the draft's `messageId`. Without threading headers, `create-draft` is unchanged
|
|
825
|
+
(AppleScript).
|
|
790
826
|
|
|
791
827
|
#### `get-thread`
|
|
792
828
|
|
|
@@ -832,13 +868,14 @@ Map `imap:` message IDs to their numeric Mail.app IDs, via each message's RFC 53
|
|
|
832
868
|
|
|
833
869
|
Reply to an existing message.
|
|
834
870
|
|
|
835
|
-
| Parameter
|
|
836
|
-
|
|
|
837
|
-
| `id`
|
|
838
|
-
| `body`
|
|
839
|
-
| `replyAll`
|
|
840
|
-
| `send`
|
|
841
|
-
| `transport`
|
|
871
|
+
| Parameter | Type | Required | Description |
|
|
872
|
+
| ------------- | --------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
873
|
+
| `id` | string | Yes | Message ID to reply to |
|
|
874
|
+
| `body` | string | Yes | Reply body (plain text; HTML tags such as `<br>` are not rendered) |
|
|
875
|
+
| `replyAll` | boolean | No | Reply to all recipients (default: false) |
|
|
876
|
+
| `send` | boolean | No | Send immediately (default: true, false = save as draft) |
|
|
877
|
+
| `transport` | string | No | `smtp` or `applescript`; omitted prefers configured SMTP when sending. Drafts use AppleScript. |
|
|
878
|
+
| `attachments` | (string \| {filename, contentBase64})[] | No | Up to 20 attachments, same rules as `send-email`: absolute file paths inside the configured read roots and/or inline `{filename, contentBase64}` objects up to 25 MiB decoded each |
|
|
842
879
|
|
|
843
880
|
**Example - Reply to sender only:**
|
|
844
881
|
|
|
@@ -860,6 +897,30 @@ Reply to an existing message.
|
|
|
860
897
|
}
|
|
861
898
|
```
|
|
862
899
|
|
|
900
|
+
**Example - Reply all with an attachment (2.20.0):**
|
|
901
|
+
|
|
902
|
+
```json
|
|
903
|
+
{
|
|
904
|
+
"id": "imap:eyJhIjoiV29yayIsInAiOiJJTkJPWCIsInUiOjQyfQ",
|
|
905
|
+
"body": "Here is the PDF you asked for.",
|
|
906
|
+
"replyAll": true,
|
|
907
|
+
"attachments": [
|
|
908
|
+
"/Users/me/Documents/quote.pdf",
|
|
909
|
+
{ "filename": "notes.txt", "contentBase64": "SGVsbG8K" }
|
|
910
|
+
]
|
|
911
|
+
}
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
**Attachments (2.20.0, #267):** same schema, limits (20 items, 25 MiB each) and read
|
|
915
|
+
roots as `send-email`, validated before the original is fetched — an out-of-roots path
|
|
916
|
+
fails with exactly `send-email`'s error. Recipients, threading headers and the quote are
|
|
917
|
+
identical to the same call without attachments. Over SMTP the files ride the same
|
|
918
|
+
nodemailer path `send-email` uses, so the Sent-folder copy (`sentCopy`) carries them too.
|
|
919
|
+
On the AppleScript transport they are added with `make new attachment` **after** the
|
|
920
|
+
reply content is set and before `save`/`send` (inline items are written to `0600` temp
|
|
921
|
+
files and removed afterwards), so a `send: false` draft contains both the attachments and
|
|
922
|
+
the quoted original. Results report `attachmentCount`.
|
|
923
|
+
|
|
863
924
|
**Delivery:** `imap:` IDs are read directly from their encoded account, mailbox, and UID. Numeric IDs are read through Mail.app. With SMTP configured, replies use clean MIME with the original `Message-ID` in `In-Reply-To` and the full `References` chain. Only the original plain-text body is quoted; the new text is not. Pass `transport: "smtp"` to require this path, or `transport: "applescript"` to use Mail.app explicitly.
|
|
864
925
|
|
|
865
926
|
**AppleScript transport quoting (v2.19.12):** every draft (`send: false`) and any send that falls back to AppleScript (SMTP not configured) also gets the original quoted — an "On \<date\>, \<sender\> wrote:" attribution line followed by the `> `-prefixed original body, built the same way as the SMTP path. Mail's own `reply`/`forward` AppleScript commands generate this quote internally but expose no way to read it back, so it's built here instead of relying on Mail's (unreadable) copy. Unlike SMTP, this path has no sending-identity restriction — it's read-only source lookup, not sending.
|
|
@@ -868,7 +929,7 @@ Reply to an existing message.
|
|
|
868
929
|
|
|
869
930
|
The direct IMAP source read is bounded to 25 MiB, including MIME attachments. Its account login must match the SMTP login, configured From, or an explicitly configured `APPLE_MAIL_MCP_SMTP_ALLOWED_FROM` identity; otherwise the call fails rather than sending from an unrelated account. The SMTP configuration still represents a single sending identity, not a per-account SMTP registry.
|
|
870
931
|
|
|
871
|
-
Success includes `transport` and, for SMTP when returned by the server, the new `messageId`. SMTP
|
|
932
|
+
Success includes `transport`, `attachmentCount` and, for SMTP when returned by the server, the new `messageId`. Over SMTP a best-effort Sent-folder copy is filed over IMAP exactly as for `send-email` (`sentCopy` / `sentCopyError`, see [SMTP transport](#smtp-transport)). `send-email` is for **new conversations**: adding `Re:` to its subject does not add threading headers. Use this reply tool with the original id instead.
|
|
872
933
|
|
|
873
934
|
**⚠️ Safety:** With the default `send: true`, sends real mail immediately and cannot be unsent. Confirm the recipients, subject, and body with the user before calling (or pass `send: false` to save a draft for review).
|
|
874
935
|
|
|
@@ -878,15 +939,18 @@ Success includes `transport` and, for SMTP when returned by the server, the new
|
|
|
878
939
|
|
|
879
940
|
Forward a message to new recipients.
|
|
880
941
|
|
|
881
|
-
| Parameter
|
|
882
|
-
|
|
|
883
|
-
| `id`
|
|
884
|
-
| `to`
|
|
885
|
-
| `body`
|
|
886
|
-
| `send`
|
|
887
|
-
| `transport`
|
|
942
|
+
| Parameter | Type | Required | Description |
|
|
943
|
+
| ------------- | --------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
944
|
+
| `id` | string | Yes | Message ID to forward |
|
|
945
|
+
| `to` | string[] | Yes | Recipients to forward to |
|
|
946
|
+
| `body` | string | No | Message to prepend (plain text) |
|
|
947
|
+
| `send` | boolean | No | Send immediately (default: true, false = save as draft) |
|
|
948
|
+
| `transport` | string | No | `smtp` or `applescript`; omitted prefers configured SMTP when sending. Drafts use AppleScript. |
|
|
949
|
+
| `attachments` | (string \| {filename, contentBase64})[] | No | Up to 20 additional attachments, same rules as `send-email`: absolute file paths inside the configured read roots and/or inline `{filename, contentBase64}` objects up to 25 MiB decoded each |
|
|
950
|
+
|
|
951
|
+
**Delivery:** uses the same source lookup, transport selection, 25 MiB source limit, account-identity check, and failure behavior as `reply-to-message`. A forward deliberately starts a new conversation, so it has no `In-Reply-To` or `References` headers. The existing plain-text forwarding behavior is unchanged: original attachments are not reattached. See [SMTP transport](#smtp-transport). On the AppleScript transport, a `body` is merged with the forwarded-message header block and original text the same way the SMTP path builds it (see `reply-to-message`'s AppleScript transport quoting note); with no `body`, Mail's own forward content is left untouched — unless `attachments` are given, in which case the forwarded-message block is built the same way so Mail has a paragraph to anchor the new files after.
|
|
888
952
|
|
|
889
|
-
**
|
|
953
|
+
**Attachments (2.20.0, #267):** `attachments` adds new files to the forward, with or without a prepended `body`, under the same rules, validation and per-transport mechanics as `reply-to-message`'s attachments. They are independent of the original's attachments (which the SMTP path still does not reattach).
|
|
890
954
|
|
|
891
955
|
SMTP forwarding requires a readable plain-text original. HTML-only IMAP messages and failed Mail.app body reads return an error before sending instead of silently omitting the original content. Explicitly select `transport: "applescript"` to forward these with Mail.app; no automatic fallback occurs. An intentionally empty plain-text message is still valid.
|
|
892
956
|
|
|
@@ -963,7 +1027,18 @@ List attachments on a message.
|
|
|
963
1027
|
| --------- | ------ | -------- | ----------- |
|
|
964
1028
|
| `id` | string | Yes | Message ID |
|
|
965
1029
|
|
|
966
|
-
**Returns:** List of attachments with name, MIME type, and size
|
|
1030
|
+
**Returns:** List of attachments with name, MIME type, and `size` — the **decoded** byte count,
|
|
1031
|
+
i.e. what `fetch-attachment` returns.
|
|
1032
|
+
|
|
1033
|
+
**Sizes over IMAP.** `BODYSTRUCTURE` reports a part's size in its transfer encoding, so a
|
|
1034
|
+
base64 attachment's raw figure is ~4/3 of the file. `list-attachments` converts it: a
|
|
1035
|
+
`Content-Disposition` `size=` parameter wins when present; 7bit/8bit/binary parts are exact
|
|
1036
|
+
as reported; base64 parts are measured exactly from a short tail of each part (padding and
|
|
1037
|
+
line wrap), fetched in one batched `FETCH` for the whole message; quoted-printable parts up
|
|
1038
|
+
to 64 KiB are fetched and counted exactly. If a part can't be measured (the probe fails, the
|
|
1039
|
+
wrap is irregular, or a quoted-printable part is larger), the size is an estimate within a
|
|
1040
|
+
few bytes for base64 — an upper bound for quoted-printable — and the entry carries
|
|
1041
|
+
`sizeApproximate: true`. `fetch-attachment` always reports the exact `bytes`.
|
|
967
1042
|
|
|
968
1043
|
---
|
|
969
1044
|
|
package/build/cli.js
CHANGED
|
@@ -57841,11 +57841,15 @@ __export(imapClient_exports, {
|
|
|
57841
57841
|
MAX_UNSCOPED_SEARCH_DEPTH: () => MAX_UNSCOPED_SEARCH_DEPTH,
|
|
57842
57842
|
__resetPool: () => __resetPool,
|
|
57843
57843
|
__setPoolConnect: () => __setPoolConnect,
|
|
57844
|
+
base64DecodedLength: () => base64DecodedLength,
|
|
57845
|
+
base64DecodedSizeFromTail: () => base64DecodedSizeFromTail,
|
|
57844
57846
|
bodyStructureHasAttachments: () => bodyStructureHasAttachments,
|
|
57845
57847
|
buildImapConnectionOptions: () => buildImapConnectionOptions,
|
|
57846
57848
|
decodeImapId: () => decodeImapId,
|
|
57849
|
+
decodedSizeWithoutBody: () => decodedSizeWithoutBody,
|
|
57847
57850
|
dropAllPools: () => dropAllPools,
|
|
57848
57851
|
encodeImapId: () => encodeImapId,
|
|
57852
|
+
imapAppendDraft: () => imapAppendDraft,
|
|
57849
57853
|
imapAppendSentCopy: () => imapAppendSentCopy,
|
|
57850
57854
|
imapBatchDelete: () => imapBatchDelete,
|
|
57851
57855
|
imapBatchFlag: () => imapBatchFlag,
|
|
@@ -57883,6 +57887,8 @@ __export(imapClient_exports, {
|
|
|
57883
57887
|
matchMailbox: () => matchMailbox,
|
|
57884
57888
|
normalizeMessageId: () => normalizeMessageId,
|
|
57885
57889
|
omittedNote: () => omittedNote,
|
|
57890
|
+
quotedPrintableDecodedLength: () => quotedPrintableDecodedLength,
|
|
57891
|
+
resolveDraftImapAccount: () => resolveDraftImapAccount,
|
|
57886
57892
|
resolveImapConfig: () => resolveImapConfig,
|
|
57887
57893
|
resolveImapConfigs: () => resolveImapConfigs,
|
|
57888
57894
|
resolveMailboxPath: () => resolveMailboxPath,
|
|
@@ -58940,6 +58946,40 @@ async function imapAppendSentCopy(smtpUser, raw, deps = {}) {
|
|
|
58940
58946
|
return { attempted: true, success: false, error: errText(e) };
|
|
58941
58947
|
}
|
|
58942
58948
|
}
|
|
58949
|
+
function resolveDraftImapAccount(account, smtpUser, env = process.env) {
|
|
58950
|
+
const specs = listImapAccountSpecs(env);
|
|
58951
|
+
const why = "Mail.app's AppleScript cannot set In-Reply-To/References on a draft, so a threaded draft is filed over IMAP into the account's Drafts mailbox";
|
|
58952
|
+
if (specs.length === 0) {
|
|
58953
|
+
throw new Error(
|
|
58954
|
+
`${why}, and IMAP is not configured. Configure IMAP, omit inReplyTo/references, or use reply-to-message with send=false. ${SETUP_HINT}`
|
|
58955
|
+
);
|
|
58956
|
+
}
|
|
58957
|
+
let spec;
|
|
58958
|
+
if (account) {
|
|
58959
|
+
spec = specs.find((s) => specMatchesSelector(s, account));
|
|
58960
|
+
if (!spec) {
|
|
58961
|
+
throw new Error(
|
|
58962
|
+
`${why}, and "${account}" is not a configured IMAP account. Configured: ${specs.map((s) => s.accountLabel).join(", ")}.`
|
|
58963
|
+
);
|
|
58964
|
+
}
|
|
58965
|
+
} else {
|
|
58966
|
+
spec = (smtpUser ? specs.find((s) => specMatchesSelector(s, smtpUser)) : void 0) ?? (specs.length === 1 ? specs[0] : void 0);
|
|
58967
|
+
if (!spec) {
|
|
58968
|
+
throw new Error(
|
|
58969
|
+
`${why}; several IMAP accounts are configured, so pass \`account\` to choose one: ${specs.map((s) => s.accountLabel).join(", ")}.`
|
|
58970
|
+
);
|
|
58971
|
+
}
|
|
58972
|
+
}
|
|
58973
|
+
return { label: spec.accountLabel, user: spec.user };
|
|
58974
|
+
}
|
|
58975
|
+
async function imapAppendDraft(account, raw, deps = {}) {
|
|
58976
|
+
return withClient({ ...deps, account: deps.account ?? account }, async (client, cfg) => {
|
|
58977
|
+
const path3 = await resolveMailboxPath(client, "drafts", "list");
|
|
58978
|
+
const res = await client.append(path3, raw, ["\\Draft", "\\Seen"]);
|
|
58979
|
+
if (!res) throw new Error(`server rejected the APPEND to "${path3}" (IMAP NO/BAD)`);
|
|
58980
|
+
return { mailbox: path3, account: cfg.accountLabel };
|
|
58981
|
+
});
|
|
58982
|
+
}
|
|
58943
58983
|
async function withMailbox(path3, deps, fn) {
|
|
58944
58984
|
return withClient(deps, async (client) => {
|
|
58945
58985
|
const lock = await client.getMailboxLock(path3);
|
|
@@ -59308,6 +59348,56 @@ async function imapDeleteMessageById(id, deps = {}) {
|
|
|
59308
59348
|
}
|
|
59309
59349
|
});
|
|
59310
59350
|
}
|
|
59351
|
+
function dispositionSize(params) {
|
|
59352
|
+
const raw = params?.size?.trim();
|
|
59353
|
+
return raw && /^\d+$/.test(raw) ? Number(raw) : void 0;
|
|
59354
|
+
}
|
|
59355
|
+
function base64DecodedLength(body) {
|
|
59356
|
+
const s = typeof body === "string" ? body : body.toString("latin1");
|
|
59357
|
+
const n = s.replace(/[^A-Za-z0-9+/]/g, "").length;
|
|
59358
|
+
return Math.floor(n * 3 / 4);
|
|
59359
|
+
}
|
|
59360
|
+
function base64DecodedSizeFromTail(total, tail) {
|
|
59361
|
+
if (tail.length >= total) return base64DecodedLength(tail);
|
|
59362
|
+
const s = tail.toString("latin1");
|
|
59363
|
+
const term = s.includes("\r\n") ? "\r\n" : "\n";
|
|
59364
|
+
const segs = s.split(term);
|
|
59365
|
+
if (segs.length < 3) return void 0;
|
|
59366
|
+
let trailing = 0;
|
|
59367
|
+
while (segs.length > 1 && segs[segs.length - 1] === "") {
|
|
59368
|
+
segs.pop();
|
|
59369
|
+
trailing++;
|
|
59370
|
+
}
|
|
59371
|
+
const last = segs[segs.length - 1];
|
|
59372
|
+
const full = segs.slice(1, -1);
|
|
59373
|
+
if (full.length === 0 || !last) return void 0;
|
|
59374
|
+
const L = full[0].length;
|
|
59375
|
+
const alphabet = /^[A-Za-z0-9+/]+$/;
|
|
59376
|
+
if (L === 0 || L % 4 !== 0 || !full.every((l) => l.length === L && alphabet.test(l))) {
|
|
59377
|
+
return void 0;
|
|
59378
|
+
}
|
|
59379
|
+
const m = /^([A-Za-z0-9+/]*)(=*)$/.exec(last);
|
|
59380
|
+
if (!m || m[2].length > 2 || last.length > L) return void 0;
|
|
59381
|
+
const before = total - trailing * term.length - last.length;
|
|
59382
|
+
if (before < 0 || before % (L + term.length) !== 0) return void 0;
|
|
59383
|
+
const chars = before / (L + term.length) * L + last.length;
|
|
59384
|
+
if (chars % 4 !== 0) return void 0;
|
|
59385
|
+
return chars / 4 * 3 - m[2].length;
|
|
59386
|
+
}
|
|
59387
|
+
function quotedPrintableDecodedLength(body) {
|
|
59388
|
+
const s = (typeof body === "string" ? body : body.toString("latin1")).replace(/=\r?\n/g, "");
|
|
59389
|
+
const escapes = s.match(/=[0-9A-Fa-f]{2}/g)?.length ?? 0;
|
|
59390
|
+
return s.length - escapes * 2;
|
|
59391
|
+
}
|
|
59392
|
+
function decodedSizeWithoutBody(a) {
|
|
59393
|
+
if (a.declaredSize !== void 0) return { size: a.declaredSize, exact: true };
|
|
59394
|
+
if (a.encoding === "base64") {
|
|
59395
|
+
const chars = a.encodedSize - 2 * Math.floor(a.encodedSize / 78);
|
|
59396
|
+
return { size: Math.floor(chars * 3 / 4), exact: false };
|
|
59397
|
+
}
|
|
59398
|
+
if (a.encoding === "quoted-printable") return { size: a.encodedSize, exact: false };
|
|
59399
|
+
return { size: a.encodedSize, exact: true };
|
|
59400
|
+
}
|
|
59311
59401
|
function collectAttachments(node, out = []) {
|
|
59312
59402
|
if (!node) return out;
|
|
59313
59403
|
const filename = node.dispositionParameters?.filename || node.parameters?.name;
|
|
@@ -59319,7 +59409,9 @@ function collectAttachments(node, out = []) {
|
|
|
59319
59409
|
part: node.part,
|
|
59320
59410
|
filename: filename || `part-${node.part}`,
|
|
59321
59411
|
mimeType: node.type || "application/octet-stream",
|
|
59322
|
-
|
|
59412
|
+
encodedSize: node.size ?? 0,
|
|
59413
|
+
encoding: (node.encoding || "").toLowerCase().trim(),
|
|
59414
|
+
declaredSize: dispositionSize(node.dispositionParameters)
|
|
59323
59415
|
});
|
|
59324
59416
|
}
|
|
59325
59417
|
for (const child of node.childNodes ?? []) collectAttachments(child, out);
|
|
@@ -59340,6 +59432,52 @@ async function streamToBuffer(content, maxBytes) {
|
|
|
59340
59432
|
}
|
|
59341
59433
|
return Buffer.concat(chunks);
|
|
59342
59434
|
}
|
|
59435
|
+
async function resolveDecodedSizes(client, uid, atts) {
|
|
59436
|
+
const out = atts.map(decodedSizeWithoutBody);
|
|
59437
|
+
const probes = [];
|
|
59438
|
+
atts.forEach((a, i) => {
|
|
59439
|
+
if (a.declaredSize !== void 0 || a.encodedSize <= 0) return;
|
|
59440
|
+
if (a.encoding === "base64") {
|
|
59441
|
+
probes.push({ i, start: Math.max(0, a.encodedSize - BASE64_TAIL_WINDOW), kind: "b64" });
|
|
59442
|
+
} else if (a.encoding === "quoted-printable" && a.encodedSize <= QP_EXACT_MAX_BYTES) {
|
|
59443
|
+
probes.push({ i, start: 0, kind: "qp" });
|
|
59444
|
+
}
|
|
59445
|
+
});
|
|
59446
|
+
if (probes.length === 0) return out;
|
|
59447
|
+
let parts;
|
|
59448
|
+
try {
|
|
59449
|
+
const msg = await client.fetchOne(
|
|
59450
|
+
String(uid),
|
|
59451
|
+
{
|
|
59452
|
+
uid: true,
|
|
59453
|
+
bodyParts: probes.map((p) => ({
|
|
59454
|
+
key: atts[p.i].part,
|
|
59455
|
+
start: p.start,
|
|
59456
|
+
maxLength: atts[p.i].encodedSize - p.start
|
|
59457
|
+
}))
|
|
59458
|
+
},
|
|
59459
|
+
{ uid: true }
|
|
59460
|
+
);
|
|
59461
|
+
parts = msg ? msg.bodyParts : void 0;
|
|
59462
|
+
} catch {
|
|
59463
|
+
return out;
|
|
59464
|
+
}
|
|
59465
|
+
for (const p of probes) {
|
|
59466
|
+
const a = atts[p.i];
|
|
59467
|
+
const buf = parts?.get(a.part.toLowerCase());
|
|
59468
|
+
if (!buf) continue;
|
|
59469
|
+
let exact;
|
|
59470
|
+
if (p.kind === "qp") {
|
|
59471
|
+
exact = quotedPrintableDecodedLength(buf);
|
|
59472
|
+
} else {
|
|
59473
|
+
const requested = a.encodedSize - p.start;
|
|
59474
|
+
const total = buf.length < requested ? p.start + buf.length : a.encodedSize;
|
|
59475
|
+
exact = base64DecodedSizeFromTail(total, buf);
|
|
59476
|
+
}
|
|
59477
|
+
if (exact !== void 0) out[p.i] = { size: exact, exact: true };
|
|
59478
|
+
}
|
|
59479
|
+
return out;
|
|
59480
|
+
}
|
|
59343
59481
|
async function imapListAttachments(id, deps = {}) {
|
|
59344
59482
|
const ref = decodeImapId(id);
|
|
59345
59483
|
if (!ref) return { success: false, error: `Not an IMAP message id: "${id}".` };
|
|
@@ -59348,12 +59486,18 @@ async function imapListAttachments(id, deps = {}) {
|
|
|
59348
59486
|
if (!msg || !msg.bodyStructure) {
|
|
59349
59487
|
return { success: false, error: `IMAP message UID ${ref.uid} not found in "${ref.path}".` };
|
|
59350
59488
|
}
|
|
59351
|
-
const
|
|
59352
|
-
|
|
59353
|
-
|
|
59354
|
-
|
|
59355
|
-
|
|
59356
|
-
|
|
59489
|
+
const atts = collectAttachments(msg.bodyStructure);
|
|
59490
|
+
const sizes = await resolveDecodedSizes(client, ref.uid, atts);
|
|
59491
|
+
const attachments = atts.map((a, i) => {
|
|
59492
|
+
const info = {
|
|
59493
|
+
id: `${id}#${a.part}`,
|
|
59494
|
+
name: a.filename,
|
|
59495
|
+
mimeType: a.mimeType,
|
|
59496
|
+
size: sizes[i].size
|
|
59497
|
+
};
|
|
59498
|
+
if (!sizes[i].exact) info.sizeApproximate = true;
|
|
59499
|
+
return info;
|
|
59500
|
+
});
|
|
59357
59501
|
return { success: true, attachments };
|
|
59358
59502
|
});
|
|
59359
59503
|
}
|
|
@@ -59374,10 +59518,11 @@ async function imapFetchAttachment(id, attachmentName, deps = {}) {
|
|
|
59374
59518
|
error: `Attachment "${attachmentName}" not found on UID ${ref.uid}. Available: ${names}.`
|
|
59375
59519
|
};
|
|
59376
59520
|
}
|
|
59377
|
-
|
|
59521
|
+
const expected = decodedSizeWithoutBody(match).size;
|
|
59522
|
+
if (expected > MAX_IMAP_ATTACHMENT_BYTES) {
|
|
59378
59523
|
return {
|
|
59379
59524
|
success: false,
|
|
59380
|
-
error: `IMAP attachment "${attachmentName}" is ${
|
|
59525
|
+
error: `IMAP attachment "${attachmentName}" is ${expected} bytes; the maximum is ${MAX_IMAP_ATTACHMENT_BYTES} bytes (25 MiB).`
|
|
59381
59526
|
};
|
|
59382
59527
|
}
|
|
59383
59528
|
try {
|
|
@@ -59565,7 +59710,7 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
59565
59710
|
true
|
|
59566
59711
|
);
|
|
59567
59712
|
}
|
|
59568
|
-
var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, NOT_DELETED, LARGE_MAILBOX_MESSAGES, FIRST_SEARCH_WINDOW, MAX_WINDOW, MAX_UNSCOPED_SEARCH_DEPTH, ROW_QUERY, ROW_QUERY_NO_STRUCTURE, ROW_QUERY_HEADERS_ONLY, DELETED_MARGIN, poolConnect, pools, connecting, MAX_COMPOSE_SOURCE_BYTES, MAX_RFC822_INLINE_BYTES, MAX_RFC822_FILE_BYTES, HEADER_WINDOW_BYTES, MAIL_FLAG_BITS, imapMarkRead, imapMarkUnread, FALLBACK_TRASH_PATH, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete;
|
|
59713
|
+
var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, NOT_DELETED, LARGE_MAILBOX_MESSAGES, FIRST_SEARCH_WINDOW, MAX_WINDOW, MAX_UNSCOPED_SEARCH_DEPTH, ROW_QUERY, ROW_QUERY_NO_STRUCTURE, ROW_QUERY_HEADERS_ONLY, DELETED_MARGIN, poolConnect, pools, connecting, MAX_COMPOSE_SOURCE_BYTES, MAX_RFC822_INLINE_BYTES, MAX_RFC822_FILE_BYTES, HEADER_WINDOW_BYTES, MAIL_FLAG_BITS, imapMarkRead, imapMarkUnread, FALLBACK_TRASH_PATH, BASE64_TAIL_WINDOW, QP_EXACT_MAX_BYTES, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete;
|
|
59569
59714
|
var init_imapClient = __esm({
|
|
59570
59715
|
"src/services/imapClient.ts"() {
|
|
59571
59716
|
"use strict";
|
|
@@ -59670,6 +59815,8 @@ var init_imapClient = __esm({
|
|
|
59670
59815
|
imapMarkRead = (id, deps = {}) => flagOp(id, "\\Seen", true, deps);
|
|
59671
59816
|
imapMarkUnread = (id, deps = {}) => flagOp(id, "\\Seen", false, deps);
|
|
59672
59817
|
FALLBACK_TRASH_PATH = "Trash";
|
|
59818
|
+
BASE64_TAIL_WINDOW = 1024;
|
|
59819
|
+
QP_EXACT_MAX_BYTES = 64 * 1024;
|
|
59673
59820
|
imapBatchMarkRead = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids) => {
|
|
59674
59821
|
assertMutated(
|
|
59675
59822
|
await c.messageFlagsAdd(uids, ["\\Seen"], { uid: true }),
|
|
@@ -59795,6 +59942,27 @@ function buildAttachments(attachments) {
|
|
|
59795
59942
|
return { filename: a.filename, content: decodeInlineAttachment(a.contentBase64) };
|
|
59796
59943
|
});
|
|
59797
59944
|
}
|
|
59945
|
+
function buildMailOptions(opts, from, attachments) {
|
|
59946
|
+
const html = opts.htmlBody?.trim() ? opts.htmlBody : void 0;
|
|
59947
|
+
return {
|
|
59948
|
+
from,
|
|
59949
|
+
to: opts.to,
|
|
59950
|
+
cc: opts.cc,
|
|
59951
|
+
bcc: opts.bcc,
|
|
59952
|
+
subject: opts.subject,
|
|
59953
|
+
text: opts.body,
|
|
59954
|
+
// When present, nodemailer emits multipart/alternative (text + html).
|
|
59955
|
+
html,
|
|
59956
|
+
attachments,
|
|
59957
|
+
// RFC 5322 threading for SMTP replies/forwards (2.5.0). nodemailer treats
|
|
59958
|
+
// In-Reply-To / References as structured msg-id headers and never
|
|
59959
|
+
// RFC 2047-encodes them, however long the id (pinned by a test, #267).
|
|
59960
|
+
inReplyTo: opts.inReplyTo?.trim() || void 0,
|
|
59961
|
+
references: opts.references?.length ? opts.references : void 0,
|
|
59962
|
+
// Reply-To header (2.18.0, issue #220): nodemailer already supports this.
|
|
59963
|
+
replyTo: opts.replyTo?.trim() || void 0
|
|
59964
|
+
};
|
|
59965
|
+
}
|
|
59798
59966
|
async function sendViaSmtp(opts, config, createTransport2 = nodemailer_default.createTransport, appendSentCopy = defaultAppendSentCopy) {
|
|
59799
59967
|
let cfg;
|
|
59800
59968
|
try {
|
|
@@ -59833,23 +60001,7 @@ async function sendViaSmtp(opts, config, createTransport2 = nodemailer_default.c
|
|
|
59833
60001
|
requireTLS,
|
|
59834
60002
|
auth: { user: cfg.user, pass: cfg.pass }
|
|
59835
60003
|
});
|
|
59836
|
-
const
|
|
59837
|
-
const mailOptions = {
|
|
59838
|
-
from: requestedFrom || cfg.from,
|
|
59839
|
-
to: opts.to,
|
|
59840
|
-
cc: opts.cc,
|
|
59841
|
-
bcc: opts.bcc,
|
|
59842
|
-
subject: opts.subject,
|
|
59843
|
-
text: opts.body,
|
|
59844
|
-
// When present, nodemailer emits multipart/alternative (text + html).
|
|
59845
|
-
html,
|
|
59846
|
-
attachments,
|
|
59847
|
-
// RFC 5322 threading for SMTP replies/forwards (2.5.0).
|
|
59848
|
-
inReplyTo: opts.inReplyTo?.trim() || void 0,
|
|
59849
|
-
references: opts.references?.length ? opts.references : void 0,
|
|
59850
|
-
// Reply-To header (2.18.0, issue #220): nodemailer already supports this.
|
|
59851
|
-
replyTo: opts.replyTo?.trim() || void 0
|
|
59852
|
-
};
|
|
60004
|
+
const mailOptions = buildMailOptions(opts, requestedFrom || cfg.from, attachments);
|
|
59853
60005
|
try {
|
|
59854
60006
|
const info = await transporter.sendMail(mailOptions);
|
|
59855
60007
|
let copyFields = {};
|