apple-mail-mcp 2.19.4 → 2.19.6
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 +7 -3
- package/build/cli.js +319 -22
- package/build/index.js +385 -205
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -289,7 +289,7 @@ Get the full content of a message.
|
|
|
289
289
|
| `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
290
|
| `account` | string | No | Account holding the message. Pair with `mailbox` to skip the cross-mailbox scan |
|
|
291
291
|
|
|
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)).
|
|
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)). 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
293
|
|
|
294
294
|
> **Large messages / attachments:** reading a full message routes through
|
|
295
295
|
> `osascript`, whose captured output buffer defaults to **64 MB**. Override it
|
|
@@ -310,9 +310,9 @@ Return a message's raw RFC 5322 header block — without fetching the body or an
|
|
|
310
310
|
| `mailbox` | string | No | Mailbox holding the message. With `account`, opens that mailbox directly instead of scanning every mailbox |
|
|
311
311
|
| `account` | string | No | Account holding the message. Pair with `mailbox` to skip the cross-mailbox scan |
|
|
312
312
|
|
|
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.
|
|
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. `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
314
|
|
|
315
|
-
**Backends:** an `imap:` id
|
|
315
|
+
**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
316
|
|
|
317
317
|
---
|
|
318
318
|
|
|
@@ -337,6 +337,10 @@ List messages in a mailbox.
|
|
|
337
337
|
They differ legitimately by transit time; when they differ by **years**, the
|
|
338
338
|
mailbox was migrated or re-imported and the arrival timestamp was reset — trust
|
|
339
339
|
`dateSent` for chronology ([#224](https://github.com/sweetrb/apple-mail-mcp/issues/224)).
|
|
340
|
+
Since 2.19.6 the IMAP `dateSent` is recovered even when the server's own parse of
|
|
341
|
+
the `Date:` header failed (legacy locale dates such as `jue ago 30 13:55:12 2007`),
|
|
342
|
+
and is omitted rather than reported when it is more than 7 days later than
|
|
343
|
+
`dateReceived` ([#234](https://github.com/sweetrb/apple-mail-mcp/issues/234)).
|
|
340
344
|
On the **AppleScript** path rows carry only `dateReceived` (Mail's `date
|
|
341
345
|
received`); `dateSent` is not yet emitted there. Sort order keys on the header
|
|
342
346
|
date on both backends, which is the stable one.
|
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);
|
|
@@ -59094,7 +59385,12 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
59094
59385
|
id: encodeImapId(ref.account, ref.path, m.uid),
|
|
59095
59386
|
subject: m.envelope?.subject || "(no subject)",
|
|
59096
59387
|
sender: senderName(m.envelope?.from),
|
|
59097
|
-
|
|
59388
|
+
// Was `new Date(m.envelope.date).toISOString()`, unguarded — a
|
|
59389
|
+
// truthy but unparseable `env.date` (e.g. a non-RFC-5322 header the
|
|
59390
|
+
// IMAP server's own ENVELOPE parser couldn't normalize) threw
|
|
59391
|
+
// `RangeError: Invalid time value` out of get-thread (#226 follow-up).
|
|
59392
|
+
// Same omitted-not-invented contract as `structuredRow` (2.19.2).
|
|
59393
|
+
date: isoOrEmpty(sentDate(m)),
|
|
59098
59394
|
isRead: m.flags?.has("\\Seen") ?? false
|
|
59099
59395
|
}))
|
|
59100
59396
|
};
|
|
@@ -59108,7 +59404,7 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
59108
59404
|
true
|
|
59109
59405
|
);
|
|
59110
59406
|
}
|
|
59111
|
-
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;
|
|
59112
59408
|
var init_imapClient = __esm({
|
|
59113
59409
|
"src/services/imapClient.ts"() {
|
|
59114
59410
|
"use strict";
|
|
@@ -59164,6 +59460,7 @@ var init_imapClient = __esm({
|
|
|
59164
59460
|
pools = /* @__PURE__ */ new Map();
|
|
59165
59461
|
connecting = /* @__PURE__ */ new Map();
|
|
59166
59462
|
MAX_COMPOSE_SOURCE_BYTES = 25 * 1024 * 1024;
|
|
59463
|
+
HEADER_WINDOW_BYTES = 64 * 1024;
|
|
59167
59464
|
MAIL_FLAG_BITS = ["$MailFlagBit0", "$MailFlagBit1", "$MailFlagBit2"];
|
|
59168
59465
|
imapMarkRead = (id, deps = {}) => flagOp(id, "\\Seen", true, deps);
|
|
59169
59466
|
imapMarkUnread = (id, deps = {}) => flagOp(id, "\\Seen", false, deps);
|
package/build/index.js
CHANGED
|
@@ -7255,6 +7255,24 @@ function extractMimeType(headers) {
|
|
|
7255
7255
|
const typeMatch = ctHeader.match(/^([^;\s]+)/);
|
|
7256
7256
|
return typeMatch ? typeMatch[1].toLowerCase() : "application/octet-stream";
|
|
7257
7257
|
}
|
|
7258
|
+
function extractCharset(headers) {
|
|
7259
|
+
const ct = getHeader(headers, "Content-Type");
|
|
7260
|
+
const m = ct?.match(/charset\s*=\s*"?([^";\s]+)"?/i);
|
|
7261
|
+
return m ? m[1].toLowerCase() : null;
|
|
7262
|
+
}
|
|
7263
|
+
function decodePartText(bytes, charset) {
|
|
7264
|
+
if (charset && !["utf-8", "utf8", "us-ascii", "ascii"].includes(charset)) {
|
|
7265
|
+
try {
|
|
7266
|
+
return new TextDecoder(charset).decode(bytes);
|
|
7267
|
+
} catch {
|
|
7268
|
+
}
|
|
7269
|
+
}
|
|
7270
|
+
try {
|
|
7271
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
7272
|
+
} catch {
|
|
7273
|
+
return new TextDecoder("windows-1252").decode(bytes);
|
|
7274
|
+
}
|
|
7275
|
+
}
|
|
7258
7276
|
function estimateBase64Size(base64Body) {
|
|
7259
7277
|
const cleaned = base64Body.replace(/[\s\r\n]/g, "");
|
|
7260
7278
|
return Math.floor(cleaned.length * 3 / 4);
|
|
@@ -7348,7 +7366,7 @@ function extractHtmlBody(source) {
|
|
|
7348
7366
|
for (const part of walkLeafParts(source, boundary)) {
|
|
7349
7367
|
if (extractMimeType(part.headers) === "text/html") {
|
|
7350
7368
|
const encoding2 = getHeader(part.headers, "Content-Transfer-Encoding");
|
|
7351
|
-
return decodeBody(part.body, encoding2).
|
|
7369
|
+
return decodePartText(decodeBody(part.body, encoding2), extractCharset(part.headers));
|
|
7352
7370
|
}
|
|
7353
7371
|
}
|
|
7354
7372
|
return null;
|
|
@@ -7359,7 +7377,7 @@ function extractHtmlBody(source) {
|
|
|
7359
7377
|
if (extractMimeType(headers) !== "text/html") return null;
|
|
7360
7378
|
const body = source.substring(blankLineIdx).replace(/^\r?\n\r?\n/, "");
|
|
7361
7379
|
const encoding = getHeader(headers, "Content-Transfer-Encoding");
|
|
7362
|
-
return decodeBody(body, encoding)
|
|
7380
|
+
return decodePartText(decodeBody(body, encoding), extractCharset(headers));
|
|
7363
7381
|
}
|
|
7364
7382
|
function extractTextBody(source) {
|
|
7365
7383
|
if (!source || !source.trim()) return null;
|
|
@@ -7368,7 +7386,7 @@ function extractTextBody(source) {
|
|
|
7368
7386
|
for (const part of walkLeafParts(source, boundary)) {
|
|
7369
7387
|
if (extractMimeType(part.headers) === "text/plain") {
|
|
7370
7388
|
const encoding2 = getHeader(part.headers, "Content-Transfer-Encoding");
|
|
7371
|
-
return decodeBody(part.body, encoding2).
|
|
7389
|
+
return decodePartText(decodeBody(part.body, encoding2), extractCharset(part.headers));
|
|
7372
7390
|
}
|
|
7373
7391
|
}
|
|
7374
7392
|
return null;
|
|
@@ -7380,7 +7398,7 @@ function extractTextBody(source) {
|
|
|
7380
7398
|
if (ct !== "text/plain" && getHeader(headers, "Content-Type") !== null) return null;
|
|
7381
7399
|
const body = source.substring(blankLineIdx).replace(/^\r?\n\r?\n/, "");
|
|
7382
7400
|
const encoding = getHeader(headers, "Content-Transfer-Encoding");
|
|
7383
|
-
return decodeBody(body, encoding)
|
|
7401
|
+
return decodePartText(decodeBody(body, encoding), extractCharset(headers));
|
|
7384
7402
|
}
|
|
7385
7403
|
function extractRfcMessageIdFromSource(source) {
|
|
7386
7404
|
if (!source || !source.trim()) return "";
|
|
@@ -7417,6 +7435,252 @@ var init_mimeParse = __esm({
|
|
|
7417
7435
|
}
|
|
7418
7436
|
});
|
|
7419
7437
|
|
|
7438
|
+
// src/utils/headers.ts
|
|
7439
|
+
function decodeEncodedWords(value) {
|
|
7440
|
+
if (!value.includes("=?")) return value;
|
|
7441
|
+
const joined = value.replace(/(\?=)\s+(=\?)/g, "$1$2");
|
|
7442
|
+
return joined.replace(
|
|
7443
|
+
/=\?([^?]+)\?([BbQq])\?([^?]*)\?=/g,
|
|
7444
|
+
(whole, charset, enc, text) => {
|
|
7445
|
+
try {
|
|
7446
|
+
const bytes = enc.toUpperCase() === "B" ? Buffer.from(text, "base64") : Buffer.from(
|
|
7447
|
+
text.replace(/_/g, " ").replace(
|
|
7448
|
+
/=([0-9A-Fa-f]{2})/g,
|
|
7449
|
+
(_m, h) => String.fromCharCode(parseInt(h, 16))
|
|
7450
|
+
),
|
|
7451
|
+
"latin1"
|
|
7452
|
+
);
|
|
7453
|
+
return new TextDecoder(normalizeCharset(charset)).decode(bytes);
|
|
7454
|
+
} catch {
|
|
7455
|
+
return whole;
|
|
7456
|
+
}
|
|
7457
|
+
}
|
|
7458
|
+
);
|
|
7459
|
+
}
|
|
7460
|
+
function normalizeCharset(charset) {
|
|
7461
|
+
const bare = charset.split("*")[0].trim().toLowerCase();
|
|
7462
|
+
try {
|
|
7463
|
+
new TextDecoder(bare);
|
|
7464
|
+
return bare;
|
|
7465
|
+
} catch {
|
|
7466
|
+
return "utf-8";
|
|
7467
|
+
}
|
|
7468
|
+
}
|
|
7469
|
+
function bareId(raw) {
|
|
7470
|
+
return raw.trim().replace(/^<+/, "").replace(/>+$/, "").trim();
|
|
7471
|
+
}
|
|
7472
|
+
function splitIds(raw) {
|
|
7473
|
+
const bracketed = raw.match(/<[^>]+>/g);
|
|
7474
|
+
if (bracketed) return bracketed.map(bareId).filter(Boolean);
|
|
7475
|
+
return raw.split(/[\s,]+/).map(bareId).filter(Boolean);
|
|
7476
|
+
}
|
|
7477
|
+
function parseDateHeader(value) {
|
|
7478
|
+
const direct = new Date(value);
|
|
7479
|
+
if (!Number.isNaN(direct.getTime())) return direct;
|
|
7480
|
+
let replaced = value;
|
|
7481
|
+
for (const [abbr, en] of Object.entries(LOCALE_MONTHS)) {
|
|
7482
|
+
const re = new RegExp(`\\b${abbr}\\.?\\b`, "i");
|
|
7483
|
+
if (re.test(replaced)) {
|
|
7484
|
+
replaced = replaced.replace(re, en);
|
|
7485
|
+
break;
|
|
7486
|
+
}
|
|
7487
|
+
}
|
|
7488
|
+
if (replaced !== value) {
|
|
7489
|
+
const viaMonth = new Date(replaced);
|
|
7490
|
+
if (!Number.isNaN(viaMonth.getTime())) return viaMonth;
|
|
7491
|
+
}
|
|
7492
|
+
return void 0;
|
|
7493
|
+
}
|
|
7494
|
+
function splitFusedDate(headers) {
|
|
7495
|
+
const i = headers.findIndex((h) => h.name.toLowerCase() === "date");
|
|
7496
|
+
if (i === -1) return void 0;
|
|
7497
|
+
const m = /^([A-Za-z][A-Za-z0-9-]*):[ \t]?(.*)$/.exec(headers[i].value);
|
|
7498
|
+
if (!m) return void 0;
|
|
7499
|
+
const name = m[1];
|
|
7500
|
+
if (!FUSABLE_HEADER_NAMES.has(name.toLowerCase()) && !/^x-/i.test(name)) return void 0;
|
|
7501
|
+
headers.splice(i, 1, { name: headers[i].name, value: "" }, { name, value: m[2].trim() });
|
|
7502
|
+
return name;
|
|
7503
|
+
}
|
|
7504
|
+
function parseHeaderBlock(input) {
|
|
7505
|
+
const text = (input ?? "").replace(/\r\n|\r/g, "\n");
|
|
7506
|
+
const blank = text.search(/\n\n/);
|
|
7507
|
+
const raw = (blank === -1 ? text : text.slice(0, blank)).replace(/\n+$/, "");
|
|
7508
|
+
const headers = [];
|
|
7509
|
+
for (const line of raw.split("\n")) {
|
|
7510
|
+
if (/^[ \t]/.test(line) && headers.length) {
|
|
7511
|
+
headers[headers.length - 1].value += " " + line.trim();
|
|
7512
|
+
continue;
|
|
7513
|
+
}
|
|
7514
|
+
const m = /^([!-9;-~]+):[ \t]?(.*)$/.exec(line);
|
|
7515
|
+
if (!m) continue;
|
|
7516
|
+
headers.push({ name: m[1], value: m[2].trim() });
|
|
7517
|
+
}
|
|
7518
|
+
const warnings = [];
|
|
7519
|
+
const fused = splitFusedDate(headers);
|
|
7520
|
+
if (fused) {
|
|
7521
|
+
warnings.push(
|
|
7522
|
+
`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.`
|
|
7523
|
+
);
|
|
7524
|
+
}
|
|
7525
|
+
const first = (name) => headers.find((h) => h.name.toLowerCase() === name.toLowerCase())?.value;
|
|
7526
|
+
const all = (name) => headers.filter((h) => h.name.toLowerCase() === name.toLowerCase()).map((h) => h.value);
|
|
7527
|
+
const decoded = (name) => {
|
|
7528
|
+
const v = first(name);
|
|
7529
|
+
return v === void 0 ? void 0 : decodeEncodedWords(v);
|
|
7530
|
+
};
|
|
7531
|
+
const dateHeader = first("Date") || void 0;
|
|
7532
|
+
let date3;
|
|
7533
|
+
if (dateHeader) {
|
|
7534
|
+
const parsed = parseDateHeader(dateHeader.replace(/\s*\([^)]*\)\s*$/, ""));
|
|
7535
|
+
if (parsed) date3 = parsed.toISOString();
|
|
7536
|
+
}
|
|
7537
|
+
const messageIdRaw = first("Message-ID") ?? first("Message-Id");
|
|
7538
|
+
const inReplyToRaw = first("In-Reply-To");
|
|
7539
|
+
const referencesRaw = first("References");
|
|
7540
|
+
return {
|
|
7541
|
+
raw,
|
|
7542
|
+
headers,
|
|
7543
|
+
date: date3,
|
|
7544
|
+
dateHeader,
|
|
7545
|
+
messageId: messageIdRaw ? bareId(messageIdRaw) || void 0 : void 0,
|
|
7546
|
+
subject: decoded("Subject"),
|
|
7547
|
+
from: decoded("From"),
|
|
7548
|
+
to: decoded("To"),
|
|
7549
|
+
cc: decoded("Cc"),
|
|
7550
|
+
replyTo: decoded("Reply-To"),
|
|
7551
|
+
inReplyTo: inReplyToRaw ? bareId(inReplyToRaw) || void 0 : void 0,
|
|
7552
|
+
references: referencesRaw ? splitIds(referencesRaw) : [],
|
|
7553
|
+
received: all("Received"),
|
|
7554
|
+
...warnings.length ? { warnings } : {}
|
|
7555
|
+
};
|
|
7556
|
+
}
|
|
7557
|
+
function headersStructured(id, parsed, dateReceived, backend) {
|
|
7558
|
+
const received = dateReceived instanceof Date ? Number.isNaN(dateReceived.getTime()) ? void 0 : dateReceived.toISOString() : dateReceived || void 0;
|
|
7559
|
+
return {
|
|
7560
|
+
id,
|
|
7561
|
+
...backend ? { backend } : {},
|
|
7562
|
+
raw: parsed.raw,
|
|
7563
|
+
headers: parsed.headers,
|
|
7564
|
+
headerCount: parsed.headers.length,
|
|
7565
|
+
...parsed.date !== void 0 ? { date: parsed.date } : {},
|
|
7566
|
+
...parsed.dateHeader !== void 0 ? { dateHeader: parsed.dateHeader } : {},
|
|
7567
|
+
...received !== void 0 ? { dateReceived: received } : {},
|
|
7568
|
+
...parsed.messageId !== void 0 ? { messageId: parsed.messageId } : {},
|
|
7569
|
+
...parsed.subject !== void 0 ? { subject: parsed.subject } : {},
|
|
7570
|
+
...parsed.from !== void 0 ? { from: parsed.from } : {},
|
|
7571
|
+
...parsed.to !== void 0 ? { to: parsed.to } : {},
|
|
7572
|
+
...parsed.cc !== void 0 ? { cc: parsed.cc } : {},
|
|
7573
|
+
...parsed.replyTo !== void 0 ? { replyTo: parsed.replyTo } : {},
|
|
7574
|
+
...parsed.inReplyTo !== void 0 ? { inReplyTo: parsed.inReplyTo } : {},
|
|
7575
|
+
references: parsed.references,
|
|
7576
|
+
received: parsed.received,
|
|
7577
|
+
...parsed.warnings?.length ? { warnings: parsed.warnings } : {}
|
|
7578
|
+
};
|
|
7579
|
+
}
|
|
7580
|
+
function plausibleDateSent(sent, received) {
|
|
7581
|
+
if (!sent || Number.isNaN(sent.getTime())) return void 0;
|
|
7582
|
+
if (!received || Number.isNaN(received.getTime())) return sent;
|
|
7583
|
+
return sent.getTime() - received.getTime() > MAX_SENT_AFTER_RECEIVED_MS ? void 0 : sent;
|
|
7584
|
+
}
|
|
7585
|
+
function headerBlockBytes(source, whole) {
|
|
7586
|
+
const cuts = [source.indexOf("\r\n\r\n"), source.indexOf("\n\n")].filter((i) => i !== -1);
|
|
7587
|
+
if (cuts.length) return source.subarray(0, Math.min(...cuts));
|
|
7588
|
+
return whole ? source : void 0;
|
|
7589
|
+
}
|
|
7590
|
+
function decodeHeaderBytes(bytes) {
|
|
7591
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
7592
|
+
const cp1252 = new TextDecoder("windows-1252");
|
|
7593
|
+
const lines = [];
|
|
7594
|
+
let start = 0;
|
|
7595
|
+
for (let i = 0; i <= bytes.length; i++) {
|
|
7596
|
+
if (i < bytes.length && bytes[i] !== 10) continue;
|
|
7597
|
+
const line = bytes.subarray(start, i);
|
|
7598
|
+
try {
|
|
7599
|
+
lines.push(utf8.decode(line));
|
|
7600
|
+
} catch {
|
|
7601
|
+
lines.push(cp1252.decode(line));
|
|
7602
|
+
}
|
|
7603
|
+
start = i + 1;
|
|
7604
|
+
}
|
|
7605
|
+
return lines.join("\n");
|
|
7606
|
+
}
|
|
7607
|
+
function isoOrUndefined(d) {
|
|
7608
|
+
if (d === void 0 || d === "") return void 0;
|
|
7609
|
+
const date3 = d instanceof Date ? d : new Date(d);
|
|
7610
|
+
return Number.isNaN(date3.getTime()) ? void 0 : date3.toISOString();
|
|
7611
|
+
}
|
|
7612
|
+
var LOCALE_MONTHS, FUSABLE_HEADER_NAMES, MAX_SENT_AFTER_RECEIVED_MS;
|
|
7613
|
+
var init_headers = __esm({
|
|
7614
|
+
"src/utils/headers.ts"() {
|
|
7615
|
+
"use strict";
|
|
7616
|
+
LOCALE_MONTHS = {
|
|
7617
|
+
// Spanish
|
|
7618
|
+
ene: "Jan",
|
|
7619
|
+
feb: "Feb",
|
|
7620
|
+
mar: "Mar",
|
|
7621
|
+
abr: "Apr",
|
|
7622
|
+
may: "May",
|
|
7623
|
+
jun: "Jun",
|
|
7624
|
+
jul: "Jul",
|
|
7625
|
+
ago: "Aug",
|
|
7626
|
+
sep: "Sep",
|
|
7627
|
+
set: "Sep",
|
|
7628
|
+
oct: "Oct",
|
|
7629
|
+
nov: "Nov",
|
|
7630
|
+
dic: "Dec",
|
|
7631
|
+
// French
|
|
7632
|
+
janv: "Jan",
|
|
7633
|
+
f\u00E9vr: "Feb",
|
|
7634
|
+
fevr: "Feb",
|
|
7635
|
+
avr: "Apr",
|
|
7636
|
+
mai: "May",
|
|
7637
|
+
juin: "Jun",
|
|
7638
|
+
juil: "Jul",
|
|
7639
|
+
ao\u00FBt: "Aug",
|
|
7640
|
+
aout: "Aug",
|
|
7641
|
+
d\u00E9c: "Dec",
|
|
7642
|
+
dec: "Dec",
|
|
7643
|
+
// German
|
|
7644
|
+
jan: "Jan",
|
|
7645
|
+
m\u00E4r: "Mar",
|
|
7646
|
+
maer: "Mar",
|
|
7647
|
+
mrz: "Mar",
|
|
7648
|
+
okt: "Oct",
|
|
7649
|
+
dez: "Dec",
|
|
7650
|
+
// Italian / Portuguese
|
|
7651
|
+
gen: "Jan",
|
|
7652
|
+
giu: "Jun",
|
|
7653
|
+
lug: "Jul",
|
|
7654
|
+
ott: "Oct",
|
|
7655
|
+
out: "Oct",
|
|
7656
|
+
fev: "Feb"
|
|
7657
|
+
};
|
|
7658
|
+
FUSABLE_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
7659
|
+
"subject",
|
|
7660
|
+
"from",
|
|
7661
|
+
"to",
|
|
7662
|
+
"cc",
|
|
7663
|
+
"bcc",
|
|
7664
|
+
"sender",
|
|
7665
|
+
"reply-to",
|
|
7666
|
+
"message-id",
|
|
7667
|
+
"in-reply-to",
|
|
7668
|
+
"references",
|
|
7669
|
+
"mime-version",
|
|
7670
|
+
"content-type",
|
|
7671
|
+
"content-transfer-encoding",
|
|
7672
|
+
"content-disposition",
|
|
7673
|
+
"return-path",
|
|
7674
|
+
"received",
|
|
7675
|
+
"importance",
|
|
7676
|
+
"priority",
|
|
7677
|
+
"thread-topic",
|
|
7678
|
+
"thread-index"
|
|
7679
|
+
]);
|
|
7680
|
+
MAX_SENT_AFTER_RECEIVED_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
7681
|
+
}
|
|
7682
|
+
});
|
|
7683
|
+
|
|
7420
7684
|
// src/utils/attachmentLimits.ts
|
|
7421
7685
|
function isInlineAttachmentBase64WithinLimit(contentBase64) {
|
|
7422
7686
|
if (contentBase64.length > MAX_INLINE_ATTACHMENT_BASE64_INPUT_CHARS) return false;
|
|
@@ -65092,185 +65356,10 @@ var require_imap_flow = __commonJS({
|
|
|
65092
65356
|
}
|
|
65093
65357
|
});
|
|
65094
65358
|
|
|
65095
|
-
// src/utils/headers.ts
|
|
65096
|
-
function decodeEncodedWords(value) {
|
|
65097
|
-
if (!value.includes("=?")) return value;
|
|
65098
|
-
const joined = value.replace(/(\?=)\s+(=\?)/g, "$1$2");
|
|
65099
|
-
return joined.replace(
|
|
65100
|
-
/=\?([^?]+)\?([BbQq])\?([^?]*)\?=/g,
|
|
65101
|
-
(whole, charset, enc, text) => {
|
|
65102
|
-
try {
|
|
65103
|
-
const bytes = enc.toUpperCase() === "B" ? Buffer.from(text, "base64") : Buffer.from(
|
|
65104
|
-
text.replace(/_/g, " ").replace(
|
|
65105
|
-
/=([0-9A-Fa-f]{2})/g,
|
|
65106
|
-
(_m, h) => String.fromCharCode(parseInt(h, 16))
|
|
65107
|
-
),
|
|
65108
|
-
"latin1"
|
|
65109
|
-
);
|
|
65110
|
-
return new TextDecoder(normalizeCharset(charset)).decode(bytes);
|
|
65111
|
-
} catch {
|
|
65112
|
-
return whole;
|
|
65113
|
-
}
|
|
65114
|
-
}
|
|
65115
|
-
);
|
|
65116
|
-
}
|
|
65117
|
-
function normalizeCharset(charset) {
|
|
65118
|
-
const bare = charset.split("*")[0].trim().toLowerCase();
|
|
65119
|
-
try {
|
|
65120
|
-
new TextDecoder(bare);
|
|
65121
|
-
return bare;
|
|
65122
|
-
} catch {
|
|
65123
|
-
return "utf-8";
|
|
65124
|
-
}
|
|
65125
|
-
}
|
|
65126
|
-
function bareId(raw) {
|
|
65127
|
-
return raw.trim().replace(/^<+/, "").replace(/>+$/, "").trim();
|
|
65128
|
-
}
|
|
65129
|
-
function splitIds(raw) {
|
|
65130
|
-
const bracketed = raw.match(/<[^>]+>/g);
|
|
65131
|
-
if (bracketed) return bracketed.map(bareId).filter(Boolean);
|
|
65132
|
-
return raw.split(/[\s,]+/).map(bareId).filter(Boolean);
|
|
65133
|
-
}
|
|
65134
|
-
function parseDateHeader(value) {
|
|
65135
|
-
const direct = new Date(value);
|
|
65136
|
-
if (!Number.isNaN(direct.getTime())) return direct;
|
|
65137
|
-
let replaced = value;
|
|
65138
|
-
for (const [abbr, en] of Object.entries(LOCALE_MONTHS)) {
|
|
65139
|
-
const re = new RegExp(`\\b${abbr}\\.?\\b`, "i");
|
|
65140
|
-
if (re.test(replaced)) {
|
|
65141
|
-
replaced = replaced.replace(re, en);
|
|
65142
|
-
break;
|
|
65143
|
-
}
|
|
65144
|
-
}
|
|
65145
|
-
if (replaced !== value) {
|
|
65146
|
-
const viaMonth = new Date(replaced);
|
|
65147
|
-
if (!Number.isNaN(viaMonth.getTime())) return viaMonth;
|
|
65148
|
-
}
|
|
65149
|
-
return void 0;
|
|
65150
|
-
}
|
|
65151
|
-
function parseHeaderBlock(input) {
|
|
65152
|
-
const text = (input ?? "").replace(/\r\n|\r/g, "\n");
|
|
65153
|
-
const blank = text.search(/\n\n/);
|
|
65154
|
-
const raw = (blank === -1 ? text : text.slice(0, blank)).replace(/\n+$/, "");
|
|
65155
|
-
const headers = [];
|
|
65156
|
-
for (const line of raw.split("\n")) {
|
|
65157
|
-
if (/^[ \t]/.test(line) && headers.length) {
|
|
65158
|
-
headers[headers.length - 1].value += " " + line.trim();
|
|
65159
|
-
continue;
|
|
65160
|
-
}
|
|
65161
|
-
const m = /^([!-9;-~]+):[ \t]?(.*)$/.exec(line);
|
|
65162
|
-
if (!m) continue;
|
|
65163
|
-
headers.push({ name: m[1], value: m[2].trim() });
|
|
65164
|
-
}
|
|
65165
|
-
const first = (name) => headers.find((h) => h.name.toLowerCase() === name.toLowerCase())?.value;
|
|
65166
|
-
const all = (name) => headers.filter((h) => h.name.toLowerCase() === name.toLowerCase()).map((h) => h.value);
|
|
65167
|
-
const decoded = (name) => {
|
|
65168
|
-
const v = first(name);
|
|
65169
|
-
return v === void 0 ? void 0 : decodeEncodedWords(v);
|
|
65170
|
-
};
|
|
65171
|
-
const dateHeader = first("Date");
|
|
65172
|
-
let date3;
|
|
65173
|
-
if (dateHeader) {
|
|
65174
|
-
const parsed = parseDateHeader(dateHeader.replace(/\s*\([^)]*\)\s*$/, ""));
|
|
65175
|
-
if (parsed) date3 = parsed.toISOString();
|
|
65176
|
-
}
|
|
65177
|
-
const messageIdRaw = first("Message-ID") ?? first("Message-Id");
|
|
65178
|
-
const inReplyToRaw = first("In-Reply-To");
|
|
65179
|
-
const referencesRaw = first("References");
|
|
65180
|
-
return {
|
|
65181
|
-
raw,
|
|
65182
|
-
headers,
|
|
65183
|
-
date: date3,
|
|
65184
|
-
dateHeader,
|
|
65185
|
-
messageId: messageIdRaw ? bareId(messageIdRaw) || void 0 : void 0,
|
|
65186
|
-
subject: decoded("Subject"),
|
|
65187
|
-
from: decoded("From"),
|
|
65188
|
-
to: decoded("To"),
|
|
65189
|
-
cc: decoded("Cc"),
|
|
65190
|
-
replyTo: decoded("Reply-To"),
|
|
65191
|
-
inReplyTo: inReplyToRaw ? bareId(inReplyToRaw) || void 0 : void 0,
|
|
65192
|
-
references: referencesRaw ? splitIds(referencesRaw) : [],
|
|
65193
|
-
received: all("Received")
|
|
65194
|
-
};
|
|
65195
|
-
}
|
|
65196
|
-
function headersStructured(id, parsed, dateReceived) {
|
|
65197
|
-
const received = dateReceived instanceof Date ? Number.isNaN(dateReceived.getTime()) ? void 0 : dateReceived.toISOString() : dateReceived || void 0;
|
|
65198
|
-
return {
|
|
65199
|
-
id,
|
|
65200
|
-
raw: parsed.raw,
|
|
65201
|
-
headers: parsed.headers,
|
|
65202
|
-
headerCount: parsed.headers.length,
|
|
65203
|
-
...parsed.date !== void 0 ? { date: parsed.date } : {},
|
|
65204
|
-
...parsed.dateHeader !== void 0 ? { dateHeader: parsed.dateHeader } : {},
|
|
65205
|
-
...received !== void 0 ? { dateReceived: received } : {},
|
|
65206
|
-
...parsed.messageId !== void 0 ? { messageId: parsed.messageId } : {},
|
|
65207
|
-
...parsed.subject !== void 0 ? { subject: parsed.subject } : {},
|
|
65208
|
-
...parsed.from !== void 0 ? { from: parsed.from } : {},
|
|
65209
|
-
...parsed.to !== void 0 ? { to: parsed.to } : {},
|
|
65210
|
-
...parsed.cc !== void 0 ? { cc: parsed.cc } : {},
|
|
65211
|
-
...parsed.replyTo !== void 0 ? { replyTo: parsed.replyTo } : {},
|
|
65212
|
-
...parsed.inReplyTo !== void 0 ? { inReplyTo: parsed.inReplyTo } : {},
|
|
65213
|
-
references: parsed.references,
|
|
65214
|
-
received: parsed.received
|
|
65215
|
-
};
|
|
65216
|
-
}
|
|
65217
|
-
function isoOrUndefined(d) {
|
|
65218
|
-
if (d === void 0 || d === "") return void 0;
|
|
65219
|
-
const date3 = d instanceof Date ? d : new Date(d);
|
|
65220
|
-
return Number.isNaN(date3.getTime()) ? void 0 : date3.toISOString();
|
|
65221
|
-
}
|
|
65222
|
-
var LOCALE_MONTHS;
|
|
65223
|
-
var init_headers = __esm({
|
|
65224
|
-
"src/utils/headers.ts"() {
|
|
65225
|
-
"use strict";
|
|
65226
|
-
LOCALE_MONTHS = {
|
|
65227
|
-
// Spanish
|
|
65228
|
-
ene: "Jan",
|
|
65229
|
-
feb: "Feb",
|
|
65230
|
-
mar: "Mar",
|
|
65231
|
-
abr: "Apr",
|
|
65232
|
-
may: "May",
|
|
65233
|
-
jun: "Jun",
|
|
65234
|
-
jul: "Jul",
|
|
65235
|
-
ago: "Aug",
|
|
65236
|
-
sep: "Sep",
|
|
65237
|
-
set: "Sep",
|
|
65238
|
-
oct: "Oct",
|
|
65239
|
-
nov: "Nov",
|
|
65240
|
-
dic: "Dec",
|
|
65241
|
-
// French
|
|
65242
|
-
janv: "Jan",
|
|
65243
|
-
f\u00E9vr: "Feb",
|
|
65244
|
-
fevr: "Feb",
|
|
65245
|
-
avr: "Apr",
|
|
65246
|
-
mai: "May",
|
|
65247
|
-
juin: "Jun",
|
|
65248
|
-
juil: "Jul",
|
|
65249
|
-
ao\u00FBt: "Aug",
|
|
65250
|
-
aout: "Aug",
|
|
65251
|
-
d\u00E9c: "Dec",
|
|
65252
|
-
dec: "Dec",
|
|
65253
|
-
// German
|
|
65254
|
-
jan: "Jan",
|
|
65255
|
-
m\u00E4r: "Mar",
|
|
65256
|
-
maer: "Mar",
|
|
65257
|
-
mrz: "Mar",
|
|
65258
|
-
okt: "Oct",
|
|
65259
|
-
dez: "Dec",
|
|
65260
|
-
// Italian / Portuguese
|
|
65261
|
-
gen: "Jan",
|
|
65262
|
-
giu: "Jun",
|
|
65263
|
-
lug: "Jul",
|
|
65264
|
-
ott: "Oct",
|
|
65265
|
-
out: "Oct",
|
|
65266
|
-
fev: "Feb"
|
|
65267
|
-
};
|
|
65268
|
-
}
|
|
65269
|
-
});
|
|
65270
|
-
|
|
65271
65359
|
// src/services/imapClient.ts
|
|
65272
65360
|
var imapClient_exports = {};
|
|
65273
65361
|
__export(imapClient_exports, {
|
|
65362
|
+
HEADER_WINDOW_BYTES: () => HEADER_WINDOW_BYTES,
|
|
65274
65363
|
IMAP_ENV: () => IMAP_ENV,
|
|
65275
65364
|
MAX_COMPOSE_SOURCE_BYTES: () => MAX_COMPOSE_SOURCE_BYTES,
|
|
65276
65365
|
__resetPool: () => __resetPool,
|
|
@@ -65554,12 +65643,36 @@ function buildCriteria(a, listMode) {
|
|
|
65554
65643
|
if (Object.keys(c).length === 0) c.all = true;
|
|
65555
65644
|
return c;
|
|
65556
65645
|
}
|
|
65646
|
+
function validDate(d) {
|
|
65647
|
+
if (!d) return void 0;
|
|
65648
|
+
const parsed = d instanceof Date ? d : new Date(d);
|
|
65649
|
+
return Number.isNaN(parsed.getTime()) ? void 0 : parsed;
|
|
65650
|
+
}
|
|
65651
|
+
function asBuffer(v) {
|
|
65652
|
+
return Buffer.isBuffer(v) ? v : Buffer.from(v);
|
|
65653
|
+
}
|
|
65654
|
+
function headerDate(m, headerText) {
|
|
65655
|
+
const env = m.envelope?.date;
|
|
65656
|
+
if (env instanceof Date) {
|
|
65657
|
+
if (!Number.isNaN(env.getTime())) return env;
|
|
65658
|
+
} else if (typeof env === "string" && env.trim()) {
|
|
65659
|
+
const parsed = parseDateHeader(env.replace(/\s*\([^)]*\)\s*$/, "").trim());
|
|
65660
|
+
if (parsed) return parsed;
|
|
65661
|
+
}
|
|
65662
|
+
const block = headerText ?? (m.headers ? decodeHeaderBytes(asBuffer(m.headers)) : "");
|
|
65663
|
+
const iso = block ? parseHeaderBlock(block).date : void 0;
|
|
65664
|
+
return iso ? new Date(iso) : void 0;
|
|
65665
|
+
}
|
|
65666
|
+
function sentDate(m, headerText) {
|
|
65667
|
+
return plausibleDateSent(headerDate(m, headerText), validDate(m.internalDate));
|
|
65668
|
+
}
|
|
65557
65669
|
function formatRow(m, account, path) {
|
|
65558
65670
|
const env = m.envelope ?? {};
|
|
65559
65671
|
const subject = env.subject || "(no subject)";
|
|
65560
65672
|
const a = env.from?.[0];
|
|
65561
65673
|
const from = a ? a.name ? `${a.name} <${a.address ?? ""}>` : a.address ?? "(unknown)" : "(unknown)";
|
|
65562
|
-
const
|
|
65674
|
+
const shown = sentDate(m) ?? validDate(m.internalDate);
|
|
65675
|
+
const date3 = shown ? shown.toLocaleDateString() : "";
|
|
65563
65676
|
const read = m.flags?.has("\\Seen") ? "read" : "unread";
|
|
65564
65677
|
return ` - ID: ${encodeImapId(account, path, m.uid)} | ${date3} | ${subject} (from: ${from}) [${read}]`;
|
|
65565
65678
|
}
|
|
@@ -65580,8 +65693,10 @@ function structuredRow(m, account, path) {
|
|
|
65580
65693
|
// emitted under the name that is true of them, matching `messageSummary`.
|
|
65581
65694
|
// `dateReceived` falls back to the header date when the server withheld
|
|
65582
65695
|
// INTERNALDATE, which is the old behaviour and never invents a value.
|
|
65583
|
-
|
|
65584
|
-
|
|
65696
|
+
// #234: both now see the RECOVERED header date (see headerDate), and
|
|
65697
|
+
// `dateSent` omits a value implausibly later than arrival (see sentDate).
|
|
65698
|
+
dateSent: isoOrEmpty(sentDate(m)),
|
|
65699
|
+
dateReceived: isoOrEmpty(validDate(m.internalDate) ?? headerDate(m)),
|
|
65585
65700
|
isRead: m.flags?.has("\\Seen") ?? false,
|
|
65586
65701
|
isFlagged: m.flags?.has("\\Flagged") ?? false,
|
|
65587
65702
|
flagColorIndex: mailFlagColorIndex(m.flags),
|
|
@@ -65605,9 +65720,7 @@ function hasMailboxFlag(mailbox, wanted) {
|
|
|
65605
65720
|
return [...mailbox.flags ?? []].some((flag) => flag.toLowerCase() === normalized);
|
|
65606
65721
|
}
|
|
65607
65722
|
function messageDateEpoch(message) {
|
|
65608
|
-
|
|
65609
|
-
const epoch = new Date(message.envelope.date).getTime();
|
|
65610
|
-
return Number.isNaN(epoch) ? 0 : epoch;
|
|
65723
|
+
return (sentDate(message) ?? validDate(message.internalDate))?.getTime() ?? 0;
|
|
65611
65724
|
}
|
|
65612
65725
|
function messageIdentity(entry) {
|
|
65613
65726
|
const raw = entry.message.envelope?.messageId?.trim() ?? "";
|
|
@@ -65631,7 +65744,13 @@ async function fetchMailboxMatches(client, path, criteria, newestCount) {
|
|
|
65631
65744
|
// INTERNALDATE rides along for the same reason, and is why `dateReceived`
|
|
65632
65745
|
// can finally mean what it says: imapflow's `envelope.date` is built from
|
|
65633
65746
|
// the header block, so it IS the `Date:` header, not arrival time.
|
|
65634
|
-
|
|
65747
|
+
//
|
|
65748
|
+
// The `Date:` header itself (BODY.PEEK[HEADER.FIELDS (DATE)]) rides in the
|
|
65749
|
+
// SAME FETCH command, so a date the server's ENVELOPE parser rejected can
|
|
65750
|
+
// still be recovered (#234). Measured on 50 real messages, 6 alternating
|
|
65751
|
+
// runs: median ~334ms without vs ~315ms with — inside the noise — for ~44
|
|
65752
|
+
// bytes per message. Too cheap to hide behind an opt-in.
|
|
65753
|
+
{ envelope: true, flags: true, bodyStructure: true, internalDate: true, headers: ["date"] },
|
|
65635
65754
|
{ uid: true }
|
|
65636
65755
|
)) {
|
|
65637
65756
|
byUid.set(msg.uid, msg);
|
|
@@ -66111,16 +66230,32 @@ async function imapGetMessage(id, preferHtml, deps = {}) {
|
|
|
66111
66230
|
);
|
|
66112
66231
|
if (!msg)
|
|
66113
66232
|
return { success: false, error: `IMAP message UID ${ref.uid} not found in "${ref.path}".` };
|
|
66114
|
-
const
|
|
66115
|
-
const src =
|
|
66116
|
-
const
|
|
66233
|
+
const sourceBytes = msg.source ? asBuffer(msg.source) : Buffer.alloc(0);
|
|
66234
|
+
const src = sourceBytes.toString("latin1");
|
|
66235
|
+
const headerBytes = sourceBytes.length ? headerBlockBytes(sourceBytes, true) : void 0;
|
|
66236
|
+
const headerText = headerBytes ? decodeHeaderBytes(headerBytes) : void 0;
|
|
66237
|
+
const subject = (headerText ? parseHeaderBlock(headerText).subject : void 0) || msg.envelope?.subject || "(no subject)";
|
|
66238
|
+
let body;
|
|
66239
|
+
let isHtml = false;
|
|
66240
|
+
if (preferHtml) {
|
|
66241
|
+
body = extractHtmlBody(src);
|
|
66242
|
+
isHtml = body !== null;
|
|
66243
|
+
body ??= extractTextBody(src);
|
|
66244
|
+
} else {
|
|
66245
|
+
body = extractTextBody(src);
|
|
66246
|
+
if (body === null) {
|
|
66247
|
+
body = extractHtmlBody(src);
|
|
66248
|
+
isHtml = body !== null;
|
|
66249
|
+
}
|
|
66250
|
+
}
|
|
66117
66251
|
return {
|
|
66118
66252
|
success: true,
|
|
66119
66253
|
info: `Subject: ${subject}
|
|
66120
66254
|
|
|
66121
|
-
${body}`,
|
|
66255
|
+
${body ?? "(no readable body)"}`,
|
|
66122
66256
|
meta: {
|
|
66123
|
-
|
|
66257
|
+
isHtml,
|
|
66258
|
+
dateSent: isoOrUndefined(sentDate(msg, headerText)),
|
|
66124
66259
|
dateReceived: isoOrUndefined(msg.internalDate),
|
|
66125
66260
|
// The envelope carries the Message-ID; `info` deliberately does not (it
|
|
66126
66261
|
// is subject + body), so the caller could never recover it from there.
|
|
@@ -66135,12 +66270,18 @@ async function imapGetMessageHeaders(id, deps = {}) {
|
|
|
66135
66270
|
return withMailbox(ref.path, depsForMessageRef(ref, deps), async (client) => {
|
|
66136
66271
|
const msg = await client.fetchOne(
|
|
66137
66272
|
String(ref.uid),
|
|
66138
|
-
{ envelope: true, internalDate: true,
|
|
66273
|
+
{ envelope: true, internalDate: true, source: { start: 0, maxLength: HEADER_WINDOW_BYTES } },
|
|
66139
66274
|
{ uid: true }
|
|
66140
66275
|
);
|
|
66141
66276
|
if (!msg)
|
|
66142
66277
|
return { success: false, error: `IMAP message UID ${ref.uid} not found in "${ref.path}".` };
|
|
66143
|
-
const
|
|
66278
|
+
const window = msg.source ? asBuffer(msg.source) : Buffer.alloc(0);
|
|
66279
|
+
const block = window.length ? headerBlockBytes(window, window.length < HEADER_WINDOW_BYTES) : void 0;
|
|
66280
|
+
let raw = block ? decodeHeaderBytes(block) : "";
|
|
66281
|
+
if (!raw.trim()) {
|
|
66282
|
+
const h = await client.fetchOne(String(ref.uid), { headers: true }, { uid: true });
|
|
66283
|
+
raw = h && h.headers ? decodeHeaderBytes(asBuffer(h.headers)) : "";
|
|
66284
|
+
}
|
|
66144
66285
|
if (!raw.trim()) return { success: false, error: "IMAP returned no header block." };
|
|
66145
66286
|
return {
|
|
66146
66287
|
success: true,
|
|
@@ -66510,7 +66651,7 @@ function senderName(from) {
|
|
|
66510
66651
|
return a.name ? `${a.name} <${a.address ?? ""}>` : a.address ?? "(unknown)";
|
|
66511
66652
|
}
|
|
66512
66653
|
function dateMs(m) {
|
|
66513
|
-
return
|
|
66654
|
+
return messageDateEpoch(m);
|
|
66514
66655
|
}
|
|
66515
66656
|
async function imapThread(id, deps = {}, limit = 50) {
|
|
66516
66657
|
const ref = decodeImapId(id);
|
|
@@ -66550,7 +66691,16 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
66550
66691
|
// Same reason as the list/search fetch: get-thread emits structured
|
|
66551
66692
|
// rows too, so it needs BODYSTRUCTURE or its hasAttachments would
|
|
66552
66693
|
// silently disagree with the same message seen via search.
|
|
66553
|
-
|
|
66694
|
+
// INTERNALDATE and the Date: header ride along for the same reason as
|
|
66695
|
+
// the list/search fetch: the per-message date is recovered and
|
|
66696
|
+
// sanity-checked exactly as a row's dateSent is (#234).
|
|
66697
|
+
{
|
|
66698
|
+
envelope: true,
|
|
66699
|
+
flags: true,
|
|
66700
|
+
bodyStructure: true,
|
|
66701
|
+
internalDate: true,
|
|
66702
|
+
headers: ["date"]
|
|
66703
|
+
},
|
|
66554
66704
|
{ uid: true }
|
|
66555
66705
|
)) {
|
|
66556
66706
|
msgs.push(msg);
|
|
@@ -66564,7 +66714,12 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
66564
66714
|
id: encodeImapId(ref.account, ref.path, m.uid),
|
|
66565
66715
|
subject: m.envelope?.subject || "(no subject)",
|
|
66566
66716
|
sender: senderName(m.envelope?.from),
|
|
66567
|
-
|
|
66717
|
+
// Was `new Date(m.envelope.date).toISOString()`, unguarded — a
|
|
66718
|
+
// truthy but unparseable `env.date` (e.g. a non-RFC-5322 header the
|
|
66719
|
+
// IMAP server's own ENVELOPE parser couldn't normalize) threw
|
|
66720
|
+
// `RangeError: Invalid time value` out of get-thread (#226 follow-up).
|
|
66721
|
+
// Same omitted-not-invented contract as `structuredRow` (2.19.2).
|
|
66722
|
+
date: isoOrEmpty(sentDate(m)),
|
|
66568
66723
|
isRead: m.flags?.has("\\Seen") ?? false
|
|
66569
66724
|
}))
|
|
66570
66725
|
};
|
|
@@ -66578,7 +66733,7 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
66578
66733
|
true
|
|
66579
66734
|
);
|
|
66580
66735
|
}
|
|
66581
|
-
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;
|
|
66736
|
+
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;
|
|
66582
66737
|
var init_imapClient = __esm({
|
|
66583
66738
|
"src/services/imapClient.ts"() {
|
|
66584
66739
|
"use strict";
|
|
@@ -66634,6 +66789,7 @@ var init_imapClient = __esm({
|
|
|
66634
66789
|
pools = /* @__PURE__ */ new Map();
|
|
66635
66790
|
connecting = /* @__PURE__ */ new Map();
|
|
66636
66791
|
MAX_COMPOSE_SOURCE_BYTES = 25 * 1024 * 1024;
|
|
66792
|
+
HEADER_WINDOW_BYTES = 64 * 1024;
|
|
66637
66793
|
MAIL_FLAG_BITS = ["$MailFlagBit0", "$MailFlagBit1", "$MailFlagBit2"];
|
|
66638
66794
|
imapMarkRead = (id, deps = {}) => flagOp(id, "\\Seen", true, deps);
|
|
66639
66795
|
imapMarkUnread = (id, deps = {}) => flagOp(id, "\\Seen", false, deps);
|
|
@@ -81674,6 +81830,7 @@ function executeAppleScript(script, options = {}) {
|
|
|
81674
81830
|
// src/services/appleMailManager.ts
|
|
81675
81831
|
init_docsUrls();
|
|
81676
81832
|
init_mimeParse();
|
|
81833
|
+
init_headers();
|
|
81677
81834
|
|
|
81678
81835
|
// src/services/templateStore.ts
|
|
81679
81836
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -83723,7 +83880,8 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
83723
83880
|
const subject = subjParts[0];
|
|
83724
83881
|
const idAndDates = (subjParts.length > 1 ? subjParts[1] : "").split(DATES_MARKER);
|
|
83725
83882
|
const rfcMessageId = normalizeRfcMessageId(idAndDates[0]);
|
|
83726
|
-
const { dateSent, dateReceived } = parseMessageDates(idAndDates[1] ?? "");
|
|
83883
|
+
const { dateSent: mailDateSent, dateReceived } = parseMessageDates(idAndDates[1] ?? "");
|
|
83884
|
+
const dateSent = plausibleDateSent(mailDateSent, dateReceived);
|
|
83727
83885
|
const htmlContent = includeHtml && rawSource ? extractHtmlBody(rawSource) || void 0 : void 0;
|
|
83728
83886
|
return {
|
|
83729
83887
|
id: id.toString(),
|
|
@@ -86658,7 +86816,16 @@ function messageSummary(m) {
|
|
|
86658
86816
|
id: m.id,
|
|
86659
86817
|
subject: m.subject,
|
|
86660
86818
|
sender: m.sender,
|
|
86661
|
-
|
|
86819
|
+
// `m.dateReceived` can be a genuine `Date` instance that is nonetheless
|
|
86820
|
+
// INVALID: `parseAppleScriptDate` (appleMailManager.ts) returns `new
|
|
86821
|
+
// Date(NaN)` rather than `undefined` when Mail's own "date received" /
|
|
86822
|
+
// "date sent" property doesn't parse, and `instanceof Date` is true for an
|
|
86823
|
+
// invalid Date too. Calling `.toISOString()` unconditionally threw
|
|
86824
|
+
// `RangeError: Invalid time value` out of search-messages/list-messages for
|
|
86825
|
+
// any AppleScript-backed row with an unparseable arrival date (#226
|
|
86826
|
+
// follow-up) — omitted-as-empty, never invented, same contract as the IMAP
|
|
86827
|
+
// rows' `isoOrEmpty`.
|
|
86828
|
+
dateReceived: m.dateReceived instanceof Date ? Number.isNaN(m.dateReceived.getTime()) ? "" : m.dateReceived.toISOString() : m.dateReceived,
|
|
86662
86829
|
isRead: m.isRead,
|
|
86663
86830
|
isFlagged: m.isFlagged,
|
|
86664
86831
|
mailbox: m.mailbox,
|
|
@@ -87927,7 +88094,7 @@ Do not use when: you don't yet have an id (use search-messages or list-messages
|
|
|
87927
88094
|
"Stable RFC 5322 Message-ID (angle brackets stripped); empty when the message has none"
|
|
87928
88095
|
),
|
|
87929
88096
|
dateSent: external_exports.string().optional().describe(
|
|
87930
|
-
"ISO 8601 send time from the message's Date: header (Mail's `date sent`). Absent when the message carries no parseable Date: header."
|
|
88097
|
+
"ISO 8601 send time from the message's Date: header (Mail's `date sent`). Absent when the message carries no parseable Date: header, or when the value is more than 7 days later than dateReceived \u2014 a send time after arrival is not a real one."
|
|
87931
88098
|
),
|
|
87932
88099
|
dateReceived: external_exports.string().optional().describe(
|
|
87933
88100
|
"ISO 8601 arrival time in the mailbox (IMAP INTERNALDATE / Mail's `date received`). A migration or re-import resets this; compare with dateSent."
|
|
@@ -87947,7 +88114,9 @@ Do not use when: you don't yet have an id (use search-messages or list-messages
|
|
|
87947
88114
|
id,
|
|
87948
88115
|
subject: subjectFromGetMessage(r.info),
|
|
87949
88116
|
body: sep3 >= 0 ? r.info.slice(sep3 + 2) : r.info,
|
|
87950
|
-
|
|
88117
|
+
// What the IMAP read actually extracted (#234): with no text/plain
|
|
88118
|
+
// part it returns the HTML part, which must not claim to be text.
|
|
88119
|
+
isHtml: typeof r.meta?.isHtml === "boolean" ? r.meta.isHtml : preferHtml === true,
|
|
87951
88120
|
// Prefer the envelope's Message-ID (#224 fix): `info` is subject +
|
|
87952
88121
|
// body with no header block, so parsing it yielded "" for every
|
|
87953
88122
|
// IMAP-sourced message from 2.2.0 through 2.18.1.
|
|
@@ -87995,7 +88164,7 @@ var HEADER_FIELD_SCHEMA = external_exports.object({
|
|
|
87995
88164
|
registerTool(
|
|
87996
88165
|
"get-message-headers",
|
|
87997
88166
|
{
|
|
87998
|
-
description: "Use when: you need a message's raw RFC 5322 headers \u2014 the author's Date: header (not the mailbox arrival time), Message-ID, In-Reply-To/References, the Received: hop trace, or any custom X- header \u2014 for a message whose id you already have (numeric or imap:\u2026). Cheap: never downloads the body or attachments.\nReturns: the raw header block (text), every header as ordered {name, value} pairs with folding undone, and the decoded key fields: date (ISO 8601, from the Date: header), dateHeader (verbatim), dateReceived (mailbox arrival time \u2014 the value a migration or re-import resets, so compare it with date), messageId, subject, from, to, cc, replyTo, inReplyTo, references[], received[].\nTip: pass the mailbox+account you got the id from so a numeric id is fetched directly instead of scanning every mailbox.\nDo not use when: you want the body (use get-message), the conversation (use get-thread), or only the Message-ID (get-message already returns rfcMessageId).",
|
|
88167
|
+
description: "Use when: you need a message's raw RFC 5322 headers \u2014 the author's Date: header (not the mailbox arrival time), Message-ID, In-Reply-To/References, the Received: hop trace, or any custom X- header \u2014 for a message whose id you already have (numeric or imap:\u2026). Cheap: never downloads the body or attachments.\nReturns: the raw header block (text), every header as ordered {name, value} pairs with folding undone, and the decoded key fields: date (ISO 8601, from the Date: header), dateHeader (verbatim), dateReceived (mailbox arrival time \u2014 the value a migration or re-import resets, so compare it with date), messageId, subject, from, to, cc, replyTo, inReplyTo, references[], received[], backend (imap or applescript), and warnings[] when a malformed block was repaired.\nTip: pass the mailbox+account you got the id from so a numeric id is fetched directly instead of scanning every mailbox.\nDo not use when: you want the body (use get-message), the conversation (use get-thread), or only the Message-ID (get-message already returns rfcMessageId).",
|
|
87999
88168
|
inputSchema: {
|
|
88000
88169
|
id: MESSAGE_ID_SCHEMA,
|
|
88001
88170
|
mailbox: external_exports.string().optional().describe(
|
|
@@ -88005,6 +88174,12 @@ registerTool(
|
|
|
88005
88174
|
},
|
|
88006
88175
|
outputSchema: {
|
|
88007
88176
|
id: external_exports.string().optional(),
|
|
88177
|
+
backend: external_exports.string().optional().describe(
|
|
88178
|
+
'Which backend read the headers: "imap" (the stored bytes over IMAP) or "applescript" (Mail\'s `all headers` property, which Mail renders itself and can alter)'
|
|
88179
|
+
),
|
|
88180
|
+
warnings: external_exports.array(external_exports.string()).optional().describe(
|
|
88181
|
+
"Repairs applied to a malformed header block, in plain words \u2014 e.g. a Date: value Mail.app dropped with the next header joined onto it. Absent when none fired; `raw` is never rewritten."
|
|
88182
|
+
),
|
|
88008
88183
|
raw: external_exports.string().optional().describe("The raw header block, exactly as stored"),
|
|
88009
88184
|
headers: external_exports.array(HEADER_FIELD_SCHEMA).optional(),
|
|
88010
88185
|
headerCount: external_exports.number().optional(),
|
|
@@ -88027,7 +88202,12 @@ registerTool(
|
|
|
88027
88202
|
({ id, mailbox, account }) => routeMessage(id, {
|
|
88028
88203
|
// imap: id → BODY.PEEK[HEADER] + INTERNALDATE, no body download.
|
|
88029
88204
|
imap: () => imapGetMessageHeaders(id, { account }),
|
|
88030
|
-
structuredFromResult: (r) => r.info ? headersStructured(
|
|
88205
|
+
structuredFromResult: (r) => r.info ? headersStructured(
|
|
88206
|
+
id,
|
|
88207
|
+
parseHeaderBlock(r.info),
|
|
88208
|
+
r.meta?.dateReceived,
|
|
88209
|
+
"imap"
|
|
88210
|
+
) : void 0,
|
|
88031
88211
|
apple: () => {
|
|
88032
88212
|
const h = mailManager.getMessageHeaders(id, { account, mailbox });
|
|
88033
88213
|
if (!h) {
|
|
@@ -88036,7 +88216,7 @@ registerTool(
|
|
|
88036
88216
|
}
|
|
88037
88217
|
return successResponse(
|
|
88038
88218
|
h.raw,
|
|
88039
|
-
headersStructured(id, parseHeaderBlock(h.raw), h.dateReceived)
|
|
88219
|
+
headersStructured(id, parseHeaderBlock(h.raw), h.dateReceived, "applescript")
|
|
88040
88220
|
);
|
|
88041
88221
|
},
|
|
88042
88222
|
ok: "",
|
package/package.json
CHANGED