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 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 fetches `BODY.PEEK[HEADER]` over IMAP (cheap even for a 20 MB message); a numeric id reads Mail's `all headers` property over AppleScript, with the same mailbox-scoped fast path as `get-message`.
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).toString("utf8");
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).toString("utf8");
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).toString("utf8");
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).toString("utf8");
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 date = env.date ? new Date(env.date).toLocaleDateString() : "";
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
- dateSent: isoOrEmpty(env.date),
58114
- dateReceived: isoOrEmpty(m.internalDate ?? env.date),
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
- if (!message.envelope?.date) return 0;
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
- { envelope: true, flags: true, bodyStructure: true, internalDate: true },
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 subject = msg.envelope?.subject || "(no subject)";
58645
- const src = msg.source ? msg.source.toString() : "";
58646
- const body = (preferHtml ? extractHtmlBody(src) : extractTextBody(src)) ?? extractTextBody(src) ?? extractHtmlBody(src) ?? "(no readable body)";
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
- dateSent: isoOrUndefined(msg.envelope?.date),
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, headers: 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 raw = msg.headers ? msg.headers.toString() : "";
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 m.envelope?.date ? new Date(m.envelope.date).getTime() : 0;
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
- { envelope: true, flags: true, bodyStructure: true },
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
- date: m.envelope?.date ? new Date(m.envelope.date).toISOString() : "",
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).toString("utf8");
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).toString("utf8");
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).toString("utf8");
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).toString("utf8");
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 date3 = env.date ? new Date(env.date).toLocaleDateString() : "";
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
- dateSent: isoOrEmpty(env.date),
65584
- dateReceived: isoOrEmpty(m.internalDate ?? env.date),
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
- if (!message.envelope?.date) return 0;
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
- { envelope: true, flags: true, bodyStructure: true, internalDate: true },
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 subject = msg.envelope?.subject || "(no subject)";
66115
- const src = msg.source ? msg.source.toString() : "";
66116
- const body = (preferHtml ? extractHtmlBody(src) : extractTextBody(src)) ?? extractTextBody(src) ?? extractHtmlBody(src) ?? "(no readable body)";
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
- dateSent: isoOrUndefined(msg.envelope?.date),
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, headers: 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 raw = msg.headers ? msg.headers.toString() : "";
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 m.envelope?.date ? new Date(m.envelope.date).getTime() : 0;
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
- { envelope: true, flags: true, bodyStructure: true },
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
- date: m.envelope?.date ? new Date(m.envelope.date).toISOString() : "",
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
- dateReceived: m.dateReceived instanceof Date ? m.dateReceived.toISOString() : m.dateReceived,
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
- isHtml: preferHtml === true,
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(id, parseHeaderBlock(r.info), r.meta?.dateReceived) : void 0,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.19.4",
3
+ "version": "2.19.6",
4
4
  "description": "MCP server for Apple Mail - read, search, send, and manage emails via Claude and other AI assistants",
5
5
  "type": "module",
6
6
  "main": "build/index.js",