apple-mail-mcp 2.19.5 → 2.19.7
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 +30 -12
- package/build/cli.js +314 -22
- package/build/index.js +414 -217
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -236,6 +236,19 @@ Search for messages matching criteria. Searches all accounts by default.
|
|
|
236
236
|
| `dateTo` | string | No | End date filter (e.g., "March 1, 2026") |
|
|
237
237
|
| `limit` | number | No | Max results, 1–500 (default: 50) |
|
|
238
238
|
|
|
239
|
+
**Returns:** List of matching messages with ID, date, subject, sender, and read state.
|
|
240
|
+
|
|
241
|
+
⚠️ **Row dates.** Since 2.19.7 every row — on both the IMAP and AppleScript
|
|
242
|
+
backends — carries both `dateSent` (the message's `Date:` header) and
|
|
243
|
+
`dateReceived` (mailbox arrival — IMAP `INTERNALDATE` or Mail's `date received`).
|
|
244
|
+
They differ legitimately by transit time; when they differ by **years**, the
|
|
245
|
+
mailbox was migrated or re-imported and the arrival timestamp was reset — trust
|
|
246
|
+
`dateSent` for chronology ([#224](https://github.com/sweetrb/apple-mail-mcp/issues/224)).
|
|
247
|
+
`dateSent` is omitted (not invented) when the message carries no parseable
|
|
248
|
+
`Date:` header, or when it is more than 7 days later than `dateReceived` — Mail
|
|
249
|
+
substitutes a timestamp of its own for a `Date:` header it cannot parse
|
|
250
|
+
([#234](https://github.com/sweetrb/apple-mail-mcp/issues/234)).
|
|
251
|
+
|
|
239
252
|
**Large mailboxes & partial results.** Apple Mail's AppleScript bridge cannot
|
|
240
253
|
search very large IMAP/Gmail mailboxes (tens of thousands of messages) before
|
|
241
254
|
the Apple Event times out — empirically even reading the newest 20 messages of
|
|
@@ -289,7 +302,7 @@ Get the full content of a message.
|
|
|
289
302
|
| `mailbox` | string | No | Mailbox holding the message (e.g. `"Sent Items"`). With `account`, opens that mailbox directly instead of scanning every mailbox — this is the fix for timeouts on large folders |
|
|
290
303
|
| `account` | string | No | Account holding the message. Pair with `mailbox` to skip the cross-mailbox scan |
|
|
291
304
|
|
|
292
|
-
**Returns:** Subject line and message body (plain text by default, HTML if `preferHtml` is true and HTML content is available). `structuredContent` also carries `rfcMessageId` and, since 2.19.0, two dates: `dateSent` (the message's `Date:` header — Mail's `date sent`) and `dateReceived` (arrival in the mailbox — Mail's `date received` / IMAP `INTERNALDATE`). They differ legitimately by transit time; when they differ by **years**, the mailbox was migrated or re-imported and the arrival timestamp was reset — trust `dateSent` for chronology ([#224](https://github.com/sweetrb/apple-mail-mcp/issues/224)).
|
|
305
|
+
**Returns:** Subject line and message body (plain text by default, HTML if `preferHtml` is true and HTML content is available). `structuredContent` also carries `rfcMessageId` and, since 2.19.0, two dates: `dateSent` (the message's `Date:` header — Mail's `date sent`) and `dateReceived` (arrival in the mailbox — Mail's `date received` / IMAP `INTERNALDATE`). They differ legitimately by transit time; when they differ by **years**, the mailbox was migrated or re-imported and the arrival timestamp was reset — trust `dateSent` for chronology ([#224](https://github.com/sweetrb/apple-mail-mcp/issues/224)). Since 2.19.6 `dateSent` is **omitted** when it is more than 7 days later than `dateReceived`: a message cannot be sent after it arrived, and Mail.app substitutes a timestamp of its own for a `Date:` header it cannot parse ([#234](https://github.com/sweetrb/apple-mail-mcp/issues/234)). `isHtml` reports what was actually returned — a message with no `text/plain` part returns its HTML part with `isHtml: true`. Bodies are decoded by each part's declared `charset`, falling back to windows-1252 for bytes that are not valid UTF-8.
|
|
293
306
|
|
|
294
307
|
> **Large messages / attachments:** reading a full message routes through
|
|
295
308
|
> `osascript`, whose captured output buffer defaults to **64 MB**. Override it
|
|
@@ -310,9 +323,9 @@ Return a message's raw RFC 5322 header block — without fetching the body or an
|
|
|
310
323
|
| `mailbox` | string | No | Mailbox holding the message. With `account`, opens that mailbox directly instead of scanning every mailbox |
|
|
311
324
|
| `account` | string | No | Account holding the message. Pair with `mailbox` to skip the cross-mailbox scan |
|
|
312
325
|
|
|
313
|
-
**Returns:** The raw header block as text. `structuredContent` carries `raw`, every header as ordered `headers[]` (`{name, value}`, folded lines joined, duplicates such as `Received:` kept in wire order, values left RFC 2047-encoded), `headerCount`, and the decoded key fields: `date` (ISO 8601, from the `Date:` header), `dateHeader` (verbatim), `dateReceived` (mailbox arrival time — IMAP `INTERNALDATE` or Mail's `date received`), `messageId`, `subject`, `from`, `to`, `cc`, `replyTo`, `inReplyTo`, `references[]` and `received[]` (first entry = last hop). Fields the message does not carry are omitted.
|
|
326
|
+
**Returns:** The raw header block as text. `structuredContent` carries `raw`, every header as ordered `headers[]` (`{name, value}`, folded lines joined, duplicates such as `Received:` kept in wire order, values left RFC 2047-encoded), `headerCount`, and the decoded key fields: `date` (ISO 8601, from the `Date:` header), `dateHeader` (verbatim), `dateReceived` (mailbox arrival time — IMAP `INTERNALDATE` or Mail's `date received`), `messageId`, `subject`, `from`, `to`, `cc`, `replyTo`, `inReplyTo`, `references[]` and `received[]` (first entry = last hop). Fields the message does not carry are omitted. `backend` says which backend read the block (`"imap"` or `"applescript"`), and `warnings[]` appears when a malformed block was repaired ([#234](https://github.com/sweetrb/apple-mail-mcp/issues/234)).
|
|
314
327
|
|
|
315
|
-
**Backends:** an `imap:` id
|
|
328
|
+
**Backends:** an `imap:` id reads the first 64 KiB of `BODY.PEEK[]` over IMAP and cuts the header block out of it (cheap even for a 20 MB message). That is deliberate: iCloud rewrites 8-bit header bytes to `*` in ENVELOPE and `BODY[HEADER]`, and only `BODY[]` returns them as stored; each line is decoded as UTF-8, falling back to windows-1252, so a raw latin-1 display name survives. A header block larger than the window falls back to `BODY.PEEK[HEADER]`. A numeric id reads Mail's `all headers` property over AppleScript, with the same mailbox-scoped fast path as `get-message`. ⚠️ That property is Mail's own rendering, not the stored bytes: for a `Date:` value Mail cannot parse it drops the value and joins the next header onto the name (`Date: Subject: …`). The tool splits that back apart, reports the date as absent rather than as a Subject string, and says so in `warnings[]`; the `imap:` id for the same message has the real `Date:`.
|
|
316
329
|
|
|
317
330
|
---
|
|
318
331
|
|
|
@@ -331,15 +344,20 @@ List messages in a mailbox.
|
|
|
331
344
|
|
|
332
345
|
**Returns:** List of messages with ID, date, subject, and sender.
|
|
333
346
|
|
|
334
|
-
⚠️ **Row dates
|
|
335
|
-
`dateSent` (the message's `Date:`
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
347
|
+
⚠️ **Row dates.** Since 2.19.7 every row — on both the **IMAP** and
|
|
348
|
+
**AppleScript** backends — carries both `dateSent` (the message's `Date:`
|
|
349
|
+
header) and `dateReceived` (mailbox arrival — IMAP `INTERNALDATE`, falling back
|
|
350
|
+
to the header date if the server withholds it; Mail's `date received` on the
|
|
351
|
+
AppleScript path). They differ legitimately by transit time; when they differ
|
|
352
|
+
by **years**, the mailbox was migrated or re-imported and the arrival timestamp
|
|
353
|
+
was reset — trust `dateSent` for chronology
|
|
354
|
+
([#224](https://github.com/sweetrb/apple-mail-mcp/issues/224)). Since 2.19.6 the
|
|
355
|
+
IMAP `dateSent` is recovered even when the server's own parse of the `Date:`
|
|
356
|
+
header failed (legacy locale dates such as `jue ago 30 13:55:12 2007`); both
|
|
357
|
+
backends omit `dateSent` (rather than reporting it) when it is more than 7 days
|
|
358
|
+
later than `dateReceived` ([#234](https://github.com/sweetrb/apple-mail-mcp/issues/234)).
|
|
359
|
+
Before 2.19.7 the AppleScript path carried only `dateReceived`. Sort order keys
|
|
360
|
+
on the header date on both backends, which is the stable one.
|
|
343
361
|
|
|
344
362
|
---
|
|
345
363
|
|
package/build/cli.js
CHANGED
|
@@ -57664,6 +57664,24 @@ function extractMimeType(headers) {
|
|
|
57664
57664
|
const typeMatch = ctHeader.match(/^([^;\s]+)/);
|
|
57665
57665
|
return typeMatch ? typeMatch[1].toLowerCase() : "application/octet-stream";
|
|
57666
57666
|
}
|
|
57667
|
+
function extractCharset(headers) {
|
|
57668
|
+
const ct = getHeader(headers, "Content-Type");
|
|
57669
|
+
const m = ct?.match(/charset\s*=\s*"?([^";\s]+)"?/i);
|
|
57670
|
+
return m ? m[1].toLowerCase() : null;
|
|
57671
|
+
}
|
|
57672
|
+
function decodePartText(bytes, charset) {
|
|
57673
|
+
if (charset && !["utf-8", "utf8", "us-ascii", "ascii"].includes(charset)) {
|
|
57674
|
+
try {
|
|
57675
|
+
return new TextDecoder(charset).decode(bytes);
|
|
57676
|
+
} catch {
|
|
57677
|
+
}
|
|
57678
|
+
}
|
|
57679
|
+
try {
|
|
57680
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
57681
|
+
} catch {
|
|
57682
|
+
return new TextDecoder("windows-1252").decode(bytes);
|
|
57683
|
+
}
|
|
57684
|
+
}
|
|
57667
57685
|
function splitMimeParts(source, boundary) {
|
|
57668
57686
|
const parts = [];
|
|
57669
57687
|
const boundaryDelim = `--${boundary}`;
|
|
@@ -57729,7 +57747,7 @@ function extractHtmlBody(source) {
|
|
|
57729
57747
|
for (const part of walkLeafParts(source, boundary)) {
|
|
57730
57748
|
if (extractMimeType(part.headers) === "text/html") {
|
|
57731
57749
|
const encoding2 = getHeader(part.headers, "Content-Transfer-Encoding");
|
|
57732
|
-
return decodeBody(part.body, encoding2).
|
|
57750
|
+
return decodePartText(decodeBody(part.body, encoding2), extractCharset(part.headers));
|
|
57733
57751
|
}
|
|
57734
57752
|
}
|
|
57735
57753
|
return null;
|
|
@@ -57740,7 +57758,7 @@ function extractHtmlBody(source) {
|
|
|
57740
57758
|
if (extractMimeType(headers) !== "text/html") return null;
|
|
57741
57759
|
const body = source.substring(blankLineIdx).replace(/^\r?\n\r?\n/, "");
|
|
57742
57760
|
const encoding = getHeader(headers, "Content-Transfer-Encoding");
|
|
57743
|
-
return decodeBody(body, encoding)
|
|
57761
|
+
return decodePartText(decodeBody(body, encoding), extractCharset(headers));
|
|
57744
57762
|
}
|
|
57745
57763
|
function extractTextBody(source) {
|
|
57746
57764
|
if (!source || !source.trim()) return null;
|
|
@@ -57749,7 +57767,7 @@ function extractTextBody(source) {
|
|
|
57749
57767
|
for (const part of walkLeafParts(source, boundary)) {
|
|
57750
57768
|
if (extractMimeType(part.headers) === "text/plain") {
|
|
57751
57769
|
const encoding2 = getHeader(part.headers, "Content-Transfer-Encoding");
|
|
57752
|
-
return decodeBody(part.body, encoding2).
|
|
57770
|
+
return decodePartText(decodeBody(part.body, encoding2), extractCharset(part.headers));
|
|
57753
57771
|
}
|
|
57754
57772
|
}
|
|
57755
57773
|
return null;
|
|
@@ -57761,7 +57779,7 @@ function extractTextBody(source) {
|
|
|
57761
57779
|
if (ct !== "text/plain" && getHeader(headers, "Content-Type") !== null) return null;
|
|
57762
57780
|
const body = source.substring(blankLineIdx).replace(/^\r?\n\r?\n/, "");
|
|
57763
57781
|
const encoding = getHeader(headers, "Content-Transfer-Encoding");
|
|
57764
|
-
return decodeBody(body, encoding)
|
|
57782
|
+
return decodePartText(decodeBody(body, encoding), extractCharset(headers));
|
|
57765
57783
|
}
|
|
57766
57784
|
var MAX_MIME_DEPTH;
|
|
57767
57785
|
var init_mimeParse = __esm({
|
|
@@ -57772,14 +57790,225 @@ var init_mimeParse = __esm({
|
|
|
57772
57790
|
});
|
|
57773
57791
|
|
|
57774
57792
|
// src/utils/headers.ts
|
|
57793
|
+
function decodeEncodedWords(value) {
|
|
57794
|
+
if (!value.includes("=?")) return value;
|
|
57795
|
+
const joined = value.replace(/(\?=)\s+(=\?)/g, "$1$2");
|
|
57796
|
+
return joined.replace(
|
|
57797
|
+
/=\?([^?]+)\?([BbQq])\?([^?]*)\?=/g,
|
|
57798
|
+
(whole, charset, enc, text) => {
|
|
57799
|
+
try {
|
|
57800
|
+
const bytes = enc.toUpperCase() === "B" ? Buffer.from(text, "base64") : Buffer.from(
|
|
57801
|
+
text.replace(/_/g, " ").replace(
|
|
57802
|
+
/=([0-9A-Fa-f]{2})/g,
|
|
57803
|
+
(_m, h) => String.fromCharCode(parseInt(h, 16))
|
|
57804
|
+
),
|
|
57805
|
+
"latin1"
|
|
57806
|
+
);
|
|
57807
|
+
return new TextDecoder(normalizeCharset(charset)).decode(bytes);
|
|
57808
|
+
} catch {
|
|
57809
|
+
return whole;
|
|
57810
|
+
}
|
|
57811
|
+
}
|
|
57812
|
+
);
|
|
57813
|
+
}
|
|
57814
|
+
function normalizeCharset(charset) {
|
|
57815
|
+
const bare = charset.split("*")[0].trim().toLowerCase();
|
|
57816
|
+
try {
|
|
57817
|
+
new TextDecoder(bare);
|
|
57818
|
+
return bare;
|
|
57819
|
+
} catch {
|
|
57820
|
+
return "utf-8";
|
|
57821
|
+
}
|
|
57822
|
+
}
|
|
57823
|
+
function bareId(raw) {
|
|
57824
|
+
return raw.trim().replace(/^<+/, "").replace(/>+$/, "").trim();
|
|
57825
|
+
}
|
|
57826
|
+
function splitIds(raw) {
|
|
57827
|
+
const bracketed = raw.match(/<[^>]+>/g);
|
|
57828
|
+
if (bracketed) return bracketed.map(bareId).filter(Boolean);
|
|
57829
|
+
return raw.split(/[\s,]+/).map(bareId).filter(Boolean);
|
|
57830
|
+
}
|
|
57831
|
+
function parseDateHeader(value) {
|
|
57832
|
+
const direct = new Date(value);
|
|
57833
|
+
if (!Number.isNaN(direct.getTime())) return direct;
|
|
57834
|
+
let replaced = value;
|
|
57835
|
+
for (const [abbr, en] of Object.entries(LOCALE_MONTHS)) {
|
|
57836
|
+
const re = new RegExp(`\\b${abbr}\\.?\\b`, "i");
|
|
57837
|
+
if (re.test(replaced)) {
|
|
57838
|
+
replaced = replaced.replace(re, en);
|
|
57839
|
+
break;
|
|
57840
|
+
}
|
|
57841
|
+
}
|
|
57842
|
+
if (replaced !== value) {
|
|
57843
|
+
const viaMonth = new Date(replaced);
|
|
57844
|
+
if (!Number.isNaN(viaMonth.getTime())) return viaMonth;
|
|
57845
|
+
}
|
|
57846
|
+
return void 0;
|
|
57847
|
+
}
|
|
57848
|
+
function splitFusedDate(headers) {
|
|
57849
|
+
const i = headers.findIndex((h) => h.name.toLowerCase() === "date");
|
|
57850
|
+
if (i === -1) return void 0;
|
|
57851
|
+
const m = /^([A-Za-z][A-Za-z0-9-]*):[ \t]?(.*)$/.exec(headers[i].value);
|
|
57852
|
+
if (!m) return void 0;
|
|
57853
|
+
const name = m[1];
|
|
57854
|
+
if (!FUSABLE_HEADER_NAMES.has(name.toLowerCase()) && !/^x-/i.test(name)) return void 0;
|
|
57855
|
+
headers.splice(i, 1, { name: headers[i].name, value: "" }, { name, value: m[2].trim() });
|
|
57856
|
+
return name;
|
|
57857
|
+
}
|
|
57858
|
+
function parseHeaderBlock(input) {
|
|
57859
|
+
const text = (input ?? "").replace(/\r\n|\r/g, "\n");
|
|
57860
|
+
const blank = text.search(/\n\n/);
|
|
57861
|
+
const raw = (blank === -1 ? text : text.slice(0, blank)).replace(/\n+$/, "");
|
|
57862
|
+
const headers = [];
|
|
57863
|
+
for (const line of raw.split("\n")) {
|
|
57864
|
+
if (/^[ \t]/.test(line) && headers.length) {
|
|
57865
|
+
headers[headers.length - 1].value += " " + line.trim();
|
|
57866
|
+
continue;
|
|
57867
|
+
}
|
|
57868
|
+
const m = /^([!-9;-~]+):[ \t]?(.*)$/.exec(line);
|
|
57869
|
+
if (!m) continue;
|
|
57870
|
+
headers.push({ name: m[1], value: m[2].trim() });
|
|
57871
|
+
}
|
|
57872
|
+
const warnings = [];
|
|
57873
|
+
const fused = splitFusedDate(headers);
|
|
57874
|
+
if (fused) {
|
|
57875
|
+
warnings.push(
|
|
57876
|
+
`The Date: header arrived with no value and the ${fused}: header joined onto it (Mail.app's all-headers property does this for a Date: it cannot parse). Split back into Date: and ${fused}:; the send date is unknown from this source.`
|
|
57877
|
+
);
|
|
57878
|
+
}
|
|
57879
|
+
const first = (name) => headers.find((h) => h.name.toLowerCase() === name.toLowerCase())?.value;
|
|
57880
|
+
const all = (name) => headers.filter((h) => h.name.toLowerCase() === name.toLowerCase()).map((h) => h.value);
|
|
57881
|
+
const decoded = (name) => {
|
|
57882
|
+
const v = first(name);
|
|
57883
|
+
return v === void 0 ? void 0 : decodeEncodedWords(v);
|
|
57884
|
+
};
|
|
57885
|
+
const dateHeader = first("Date") || void 0;
|
|
57886
|
+
let date;
|
|
57887
|
+
if (dateHeader) {
|
|
57888
|
+
const parsed = parseDateHeader(dateHeader.replace(/\s*\([^)]*\)\s*$/, ""));
|
|
57889
|
+
if (parsed) date = parsed.toISOString();
|
|
57890
|
+
}
|
|
57891
|
+
const messageIdRaw = first("Message-ID") ?? first("Message-Id");
|
|
57892
|
+
const inReplyToRaw = first("In-Reply-To");
|
|
57893
|
+
const referencesRaw = first("References");
|
|
57894
|
+
return {
|
|
57895
|
+
raw,
|
|
57896
|
+
headers,
|
|
57897
|
+
date,
|
|
57898
|
+
dateHeader,
|
|
57899
|
+
messageId: messageIdRaw ? bareId(messageIdRaw) || void 0 : void 0,
|
|
57900
|
+
subject: decoded("Subject"),
|
|
57901
|
+
from: decoded("From"),
|
|
57902
|
+
to: decoded("To"),
|
|
57903
|
+
cc: decoded("Cc"),
|
|
57904
|
+
replyTo: decoded("Reply-To"),
|
|
57905
|
+
inReplyTo: inReplyToRaw ? bareId(inReplyToRaw) || void 0 : void 0,
|
|
57906
|
+
references: referencesRaw ? splitIds(referencesRaw) : [],
|
|
57907
|
+
received: all("Received"),
|
|
57908
|
+
...warnings.length ? { warnings } : {}
|
|
57909
|
+
};
|
|
57910
|
+
}
|
|
57911
|
+
function plausibleDateSent(sent, received) {
|
|
57912
|
+
if (!sent || Number.isNaN(sent.getTime())) return void 0;
|
|
57913
|
+
if (!received || Number.isNaN(received.getTime())) return sent;
|
|
57914
|
+
return sent.getTime() - received.getTime() > MAX_SENT_AFTER_RECEIVED_MS ? void 0 : sent;
|
|
57915
|
+
}
|
|
57916
|
+
function headerBlockBytes(source, whole) {
|
|
57917
|
+
const cuts = [source.indexOf("\r\n\r\n"), source.indexOf("\n\n")].filter((i) => i !== -1);
|
|
57918
|
+
if (cuts.length) return source.subarray(0, Math.min(...cuts));
|
|
57919
|
+
return whole ? source : void 0;
|
|
57920
|
+
}
|
|
57921
|
+
function decodeHeaderBytes(bytes) {
|
|
57922
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
57923
|
+
const cp1252 = new TextDecoder("windows-1252");
|
|
57924
|
+
const lines = [];
|
|
57925
|
+
let start = 0;
|
|
57926
|
+
for (let i = 0; i <= bytes.length; i++) {
|
|
57927
|
+
if (i < bytes.length && bytes[i] !== 10) continue;
|
|
57928
|
+
const line = bytes.subarray(start, i);
|
|
57929
|
+
try {
|
|
57930
|
+
lines.push(utf8.decode(line));
|
|
57931
|
+
} catch {
|
|
57932
|
+
lines.push(cp1252.decode(line));
|
|
57933
|
+
}
|
|
57934
|
+
start = i + 1;
|
|
57935
|
+
}
|
|
57936
|
+
return lines.join("\n");
|
|
57937
|
+
}
|
|
57775
57938
|
function isoOrUndefined(d) {
|
|
57776
57939
|
if (d === void 0 || d === "") return void 0;
|
|
57777
57940
|
const date = d instanceof Date ? d : new Date(d);
|
|
57778
57941
|
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
57779
57942
|
}
|
|
57943
|
+
var LOCALE_MONTHS, FUSABLE_HEADER_NAMES, MAX_SENT_AFTER_RECEIVED_MS;
|
|
57780
57944
|
var init_headers = __esm({
|
|
57781
57945
|
"src/utils/headers.ts"() {
|
|
57782
57946
|
"use strict";
|
|
57947
|
+
LOCALE_MONTHS = {
|
|
57948
|
+
// Spanish
|
|
57949
|
+
ene: "Jan",
|
|
57950
|
+
feb: "Feb",
|
|
57951
|
+
mar: "Mar",
|
|
57952
|
+
abr: "Apr",
|
|
57953
|
+
may: "May",
|
|
57954
|
+
jun: "Jun",
|
|
57955
|
+
jul: "Jul",
|
|
57956
|
+
ago: "Aug",
|
|
57957
|
+
sep: "Sep",
|
|
57958
|
+
set: "Sep",
|
|
57959
|
+
oct: "Oct",
|
|
57960
|
+
nov: "Nov",
|
|
57961
|
+
dic: "Dec",
|
|
57962
|
+
// French
|
|
57963
|
+
janv: "Jan",
|
|
57964
|
+
f\u00E9vr: "Feb",
|
|
57965
|
+
fevr: "Feb",
|
|
57966
|
+
avr: "Apr",
|
|
57967
|
+
mai: "May",
|
|
57968
|
+
juin: "Jun",
|
|
57969
|
+
juil: "Jul",
|
|
57970
|
+
ao\u00FBt: "Aug",
|
|
57971
|
+
aout: "Aug",
|
|
57972
|
+
d\u00E9c: "Dec",
|
|
57973
|
+
dec: "Dec",
|
|
57974
|
+
// German
|
|
57975
|
+
jan: "Jan",
|
|
57976
|
+
m\u00E4r: "Mar",
|
|
57977
|
+
maer: "Mar",
|
|
57978
|
+
mrz: "Mar",
|
|
57979
|
+
okt: "Oct",
|
|
57980
|
+
dez: "Dec",
|
|
57981
|
+
// Italian / Portuguese
|
|
57982
|
+
gen: "Jan",
|
|
57983
|
+
giu: "Jun",
|
|
57984
|
+
lug: "Jul",
|
|
57985
|
+
ott: "Oct",
|
|
57986
|
+
out: "Oct",
|
|
57987
|
+
fev: "Feb"
|
|
57988
|
+
};
|
|
57989
|
+
FUSABLE_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
57990
|
+
"subject",
|
|
57991
|
+
"from",
|
|
57992
|
+
"to",
|
|
57993
|
+
"cc",
|
|
57994
|
+
"bcc",
|
|
57995
|
+
"sender",
|
|
57996
|
+
"reply-to",
|
|
57997
|
+
"message-id",
|
|
57998
|
+
"in-reply-to",
|
|
57999
|
+
"references",
|
|
58000
|
+
"mime-version",
|
|
58001
|
+
"content-type",
|
|
58002
|
+
"content-transfer-encoding",
|
|
58003
|
+
"content-disposition",
|
|
58004
|
+
"return-path",
|
|
58005
|
+
"received",
|
|
58006
|
+
"importance",
|
|
58007
|
+
"priority",
|
|
58008
|
+
"thread-topic",
|
|
58009
|
+
"thread-index"
|
|
58010
|
+
]);
|
|
58011
|
+
MAX_SENT_AFTER_RECEIVED_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
57783
58012
|
}
|
|
57784
58013
|
});
|
|
57785
58014
|
|
|
@@ -57801,6 +58030,7 @@ var init_auditLog = __esm({
|
|
|
57801
58030
|
// src/services/imapClient.ts
|
|
57802
58031
|
var imapClient_exports = {};
|
|
57803
58032
|
__export(imapClient_exports, {
|
|
58033
|
+
HEADER_WINDOW_BYTES: () => HEADER_WINDOW_BYTES,
|
|
57804
58034
|
IMAP_ENV: () => IMAP_ENV,
|
|
57805
58035
|
MAX_COMPOSE_SOURCE_BYTES: () => MAX_COMPOSE_SOURCE_BYTES,
|
|
57806
58036
|
__resetPool: () => __resetPool,
|
|
@@ -58084,12 +58314,36 @@ function buildCriteria(a, listMode) {
|
|
|
58084
58314
|
if (Object.keys(c).length === 0) c.all = true;
|
|
58085
58315
|
return c;
|
|
58086
58316
|
}
|
|
58317
|
+
function validDate(d) {
|
|
58318
|
+
if (!d) return void 0;
|
|
58319
|
+
const parsed = d instanceof Date ? d : new Date(d);
|
|
58320
|
+
return Number.isNaN(parsed.getTime()) ? void 0 : parsed;
|
|
58321
|
+
}
|
|
58322
|
+
function asBuffer(v) {
|
|
58323
|
+
return Buffer.isBuffer(v) ? v : Buffer.from(v);
|
|
58324
|
+
}
|
|
58325
|
+
function headerDate(m, headerText) {
|
|
58326
|
+
const env = m.envelope?.date;
|
|
58327
|
+
if (env instanceof Date) {
|
|
58328
|
+
if (!Number.isNaN(env.getTime())) return env;
|
|
58329
|
+
} else if (typeof env === "string" && env.trim()) {
|
|
58330
|
+
const parsed = parseDateHeader(env.replace(/\s*\([^)]*\)\s*$/, "").trim());
|
|
58331
|
+
if (parsed) return parsed;
|
|
58332
|
+
}
|
|
58333
|
+
const block = headerText ?? (m.headers ? decodeHeaderBytes(asBuffer(m.headers)) : "");
|
|
58334
|
+
const iso = block ? parseHeaderBlock(block).date : void 0;
|
|
58335
|
+
return iso ? new Date(iso) : void 0;
|
|
58336
|
+
}
|
|
58337
|
+
function sentDate(m, headerText) {
|
|
58338
|
+
return plausibleDateSent(headerDate(m, headerText), validDate(m.internalDate));
|
|
58339
|
+
}
|
|
58087
58340
|
function formatRow(m, account, path) {
|
|
58088
58341
|
const env = m.envelope ?? {};
|
|
58089
58342
|
const subject = env.subject || "(no subject)";
|
|
58090
58343
|
const a = env.from?.[0];
|
|
58091
58344
|
const from = a ? a.name ? `${a.name} <${a.address ?? ""}>` : a.address ?? "(unknown)" : "(unknown)";
|
|
58092
|
-
const
|
|
58345
|
+
const shown = sentDate(m) ?? validDate(m.internalDate);
|
|
58346
|
+
const date = shown ? shown.toLocaleDateString() : "";
|
|
58093
58347
|
const read = m.flags?.has("\\Seen") ? "read" : "unread";
|
|
58094
58348
|
return ` - ID: ${encodeImapId(account, path, m.uid)} | ${date} | ${subject} (from: ${from}) [${read}]`;
|
|
58095
58349
|
}
|
|
@@ -58110,8 +58364,10 @@ function structuredRow(m, account, path) {
|
|
|
58110
58364
|
// emitted under the name that is true of them, matching `messageSummary`.
|
|
58111
58365
|
// `dateReceived` falls back to the header date when the server withheld
|
|
58112
58366
|
// INTERNALDATE, which is the old behaviour and never invents a value.
|
|
58113
|
-
|
|
58114
|
-
|
|
58367
|
+
// #234: both now see the RECOVERED header date (see headerDate), and
|
|
58368
|
+
// `dateSent` omits a value implausibly later than arrival (see sentDate).
|
|
58369
|
+
dateSent: isoOrEmpty(sentDate(m)),
|
|
58370
|
+
dateReceived: isoOrEmpty(validDate(m.internalDate) ?? headerDate(m)),
|
|
58115
58371
|
isRead: m.flags?.has("\\Seen") ?? false,
|
|
58116
58372
|
isFlagged: m.flags?.has("\\Flagged") ?? false,
|
|
58117
58373
|
flagColorIndex: mailFlagColorIndex(m.flags),
|
|
@@ -58135,9 +58391,7 @@ function hasMailboxFlag(mailbox, wanted) {
|
|
|
58135
58391
|
return [...mailbox.flags ?? []].some((flag) => flag.toLowerCase() === normalized);
|
|
58136
58392
|
}
|
|
58137
58393
|
function messageDateEpoch(message) {
|
|
58138
|
-
|
|
58139
|
-
const epoch = new Date(message.envelope.date).getTime();
|
|
58140
|
-
return Number.isNaN(epoch) ? 0 : epoch;
|
|
58394
|
+
return (sentDate(message) ?? validDate(message.internalDate))?.getTime() ?? 0;
|
|
58141
58395
|
}
|
|
58142
58396
|
function messageIdentity(entry) {
|
|
58143
58397
|
const raw = entry.message.envelope?.messageId?.trim() ?? "";
|
|
@@ -58161,7 +58415,13 @@ async function fetchMailboxMatches(client, path, criteria, newestCount) {
|
|
|
58161
58415
|
// INTERNALDATE rides along for the same reason, and is why `dateReceived`
|
|
58162
58416
|
// can finally mean what it says: imapflow's `envelope.date` is built from
|
|
58163
58417
|
// the header block, so it IS the `Date:` header, not arrival time.
|
|
58164
|
-
|
|
58418
|
+
//
|
|
58419
|
+
// The `Date:` header itself (BODY.PEEK[HEADER.FIELDS (DATE)]) rides in the
|
|
58420
|
+
// SAME FETCH command, so a date the server's ENVELOPE parser rejected can
|
|
58421
|
+
// still be recovered (#234). Measured on 50 real messages, 6 alternating
|
|
58422
|
+
// runs: median ~334ms without vs ~315ms with — inside the noise — for ~44
|
|
58423
|
+
// bytes per message. Too cheap to hide behind an opt-in.
|
|
58424
|
+
{ envelope: true, flags: true, bodyStructure: true, internalDate: true, headers: ["date"] },
|
|
58165
58425
|
{ uid: true }
|
|
58166
58426
|
)) {
|
|
58167
58427
|
byUid.set(msg.uid, msg);
|
|
@@ -58641,16 +58901,32 @@ async function imapGetMessage(id, preferHtml, deps = {}) {
|
|
|
58641
58901
|
);
|
|
58642
58902
|
if (!msg)
|
|
58643
58903
|
return { success: false, error: `IMAP message UID ${ref.uid} not found in "${ref.path}".` };
|
|
58644
|
-
const
|
|
58645
|
-
const src =
|
|
58646
|
-
const
|
|
58904
|
+
const sourceBytes = msg.source ? asBuffer(msg.source) : Buffer.alloc(0);
|
|
58905
|
+
const src = sourceBytes.toString("latin1");
|
|
58906
|
+
const headerBytes = sourceBytes.length ? headerBlockBytes(sourceBytes, true) : void 0;
|
|
58907
|
+
const headerText = headerBytes ? decodeHeaderBytes(headerBytes) : void 0;
|
|
58908
|
+
const subject = (headerText ? parseHeaderBlock(headerText).subject : void 0) || msg.envelope?.subject || "(no subject)";
|
|
58909
|
+
let body;
|
|
58910
|
+
let isHtml = false;
|
|
58911
|
+
if (preferHtml) {
|
|
58912
|
+
body = extractHtmlBody(src);
|
|
58913
|
+
isHtml = body !== null;
|
|
58914
|
+
body ??= extractTextBody(src);
|
|
58915
|
+
} else {
|
|
58916
|
+
body = extractTextBody(src);
|
|
58917
|
+
if (body === null) {
|
|
58918
|
+
body = extractHtmlBody(src);
|
|
58919
|
+
isHtml = body !== null;
|
|
58920
|
+
}
|
|
58921
|
+
}
|
|
58647
58922
|
return {
|
|
58648
58923
|
success: true,
|
|
58649
58924
|
info: `Subject: ${subject}
|
|
58650
58925
|
|
|
58651
|
-
${body}`,
|
|
58926
|
+
${body ?? "(no readable body)"}`,
|
|
58652
58927
|
meta: {
|
|
58653
|
-
|
|
58928
|
+
isHtml,
|
|
58929
|
+
dateSent: isoOrUndefined(sentDate(msg, headerText)),
|
|
58654
58930
|
dateReceived: isoOrUndefined(msg.internalDate),
|
|
58655
58931
|
// The envelope carries the Message-ID; `info` deliberately does not (it
|
|
58656
58932
|
// is subject + body), so the caller could never recover it from there.
|
|
@@ -58665,12 +58941,18 @@ async function imapGetMessageHeaders(id, deps = {}) {
|
|
|
58665
58941
|
return withMailbox(ref.path, depsForMessageRef(ref, deps), async (client) => {
|
|
58666
58942
|
const msg = await client.fetchOne(
|
|
58667
58943
|
String(ref.uid),
|
|
58668
|
-
{ envelope: true, internalDate: true,
|
|
58944
|
+
{ envelope: true, internalDate: true, source: { start: 0, maxLength: HEADER_WINDOW_BYTES } },
|
|
58669
58945
|
{ uid: true }
|
|
58670
58946
|
);
|
|
58671
58947
|
if (!msg)
|
|
58672
58948
|
return { success: false, error: `IMAP message UID ${ref.uid} not found in "${ref.path}".` };
|
|
58673
|
-
const
|
|
58949
|
+
const window = msg.source ? asBuffer(msg.source) : Buffer.alloc(0);
|
|
58950
|
+
const block = window.length ? headerBlockBytes(window, window.length < HEADER_WINDOW_BYTES) : void 0;
|
|
58951
|
+
let raw = block ? decodeHeaderBytes(block) : "";
|
|
58952
|
+
if (!raw.trim()) {
|
|
58953
|
+
const h = await client.fetchOne(String(ref.uid), { headers: true }, { uid: true });
|
|
58954
|
+
raw = h && h.headers ? decodeHeaderBytes(asBuffer(h.headers)) : "";
|
|
58955
|
+
}
|
|
58674
58956
|
if (!raw.trim()) return { success: false, error: "IMAP returned no header block." };
|
|
58675
58957
|
return {
|
|
58676
58958
|
success: true,
|
|
@@ -59040,7 +59322,7 @@ function senderName(from) {
|
|
|
59040
59322
|
return a.name ? `${a.name} <${a.address ?? ""}>` : a.address ?? "(unknown)";
|
|
59041
59323
|
}
|
|
59042
59324
|
function dateMs(m) {
|
|
59043
|
-
return
|
|
59325
|
+
return messageDateEpoch(m);
|
|
59044
59326
|
}
|
|
59045
59327
|
async function imapThread(id, deps = {}, limit = 50) {
|
|
59046
59328
|
const ref = decodeImapId(id);
|
|
@@ -59080,7 +59362,16 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
59080
59362
|
// Same reason as the list/search fetch: get-thread emits structured
|
|
59081
59363
|
// rows too, so it needs BODYSTRUCTURE or its hasAttachments would
|
|
59082
59364
|
// silently disagree with the same message seen via search.
|
|
59083
|
-
|
|
59365
|
+
// INTERNALDATE and the Date: header ride along for the same reason as
|
|
59366
|
+
// the list/search fetch: the per-message date is recovered and
|
|
59367
|
+
// sanity-checked exactly as a row's dateSent is (#234).
|
|
59368
|
+
{
|
|
59369
|
+
envelope: true,
|
|
59370
|
+
flags: true,
|
|
59371
|
+
bodyStructure: true,
|
|
59372
|
+
internalDate: true,
|
|
59373
|
+
headers: ["date"]
|
|
59374
|
+
},
|
|
59084
59375
|
{ uid: true }
|
|
59085
59376
|
)) {
|
|
59086
59377
|
msgs.push(msg);
|
|
@@ -59099,7 +59390,7 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
59099
59390
|
// IMAP server's own ENVELOPE parser couldn't normalize) threw
|
|
59100
59391
|
// `RangeError: Invalid time value` out of get-thread (#226 follow-up).
|
|
59101
59392
|
// Same omitted-not-invented contract as `structuredRow` (2.19.2).
|
|
59102
|
-
date: isoOrEmpty(m
|
|
59393
|
+
date: isoOrEmpty(sentDate(m)),
|
|
59103
59394
|
isRead: m.flags?.has("\\Seen") ?? false
|
|
59104
59395
|
}))
|
|
59105
59396
|
};
|
|
@@ -59113,7 +59404,7 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
59113
59404
|
true
|
|
59114
59405
|
);
|
|
59115
59406
|
}
|
|
59116
|
-
var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, poolConnect, pools, connecting, MAX_COMPOSE_SOURCE_BYTES, MAIL_FLAG_BITS, imapMarkRead, imapMarkUnread, FALLBACK_TRASH_PATH, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete;
|
|
59407
|
+
var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, poolConnect, pools, connecting, MAX_COMPOSE_SOURCE_BYTES, HEADER_WINDOW_BYTES, MAIL_FLAG_BITS, imapMarkRead, imapMarkUnread, FALLBACK_TRASH_PATH, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete;
|
|
59117
59408
|
var init_imapClient = __esm({
|
|
59118
59409
|
"src/services/imapClient.ts"() {
|
|
59119
59410
|
"use strict";
|
|
@@ -59169,6 +59460,7 @@ var init_imapClient = __esm({
|
|
|
59169
59460
|
pools = /* @__PURE__ */ new Map();
|
|
59170
59461
|
connecting = /* @__PURE__ */ new Map();
|
|
59171
59462
|
MAX_COMPOSE_SOURCE_BYTES = 25 * 1024 * 1024;
|
|
59463
|
+
HEADER_WINDOW_BYTES = 64 * 1024;
|
|
59172
59464
|
MAIL_FLAG_BITS = ["$MailFlagBit0", "$MailFlagBit1", "$MailFlagBit2"];
|
|
59173
59465
|
imapMarkRead = (id, deps = {}) => flagOp(id, "\\Seen", true, deps);
|
|
59174
59466
|
imapMarkUnread = (id, deps = {}) => flagOp(id, "\\Seen", false, deps);
|