@bobfrankston/iflow-direct 0.1.54 → 0.1.56

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.
@@ -19,6 +19,7 @@ export declare class FetchedMessage implements NativeFetchedMessage {
19
19
  uid: number;
20
20
  flags: Set<string>;
21
21
  date: Date | null;
22
+ sentDate?: Date;
22
23
  subject: string;
23
24
  messageId: string;
24
25
  from: AddressData[];
@@ -24,6 +24,7 @@ export class FetchedMessage {
24
24
  uid;
25
25
  flags;
26
26
  date;
27
+ sentDate;
27
28
  subject;
28
29
  messageId;
29
30
  from;
package/imap-native.d.ts CHANGED
@@ -14,6 +14,7 @@ export interface NativeFetchedMessage {
14
14
  uid: number;
15
15
  flags: Set<string>;
16
16
  date: Date | null;
17
+ sentDate?: Date;
17
18
  subject: string;
18
19
  messageId: string;
19
20
  from: proto.AddressData[];
package/imap-native.js CHANGED
@@ -553,10 +553,25 @@ export class NativeImapClient {
553
553
  return;
554
554
  if (this.selectedMailbox !== folderPath)
555
555
  await this.select(folderPath);
556
- await this.fetchMessagesStream(uids.join(","), { source: true, headers: false }, (msg) => {
557
- if (msg.uid && msg.source)
556
+ // headers default (was `headers: false`): the single-message body fetch
557
+ // path (fetchMessage fetchMessagesStream, headers DEFAULT) extracts
558
+ // BODY[] correctly, but this batch path passed `headers: false` and
559
+ // returned 0 bodies 100% of the time — so prefetch never cached anything
560
+ // and bodies only landed when the user opened a message (Bob 2026-06-17
561
+ // "predownloading still broken"). The only difference between the two
562
+ // paths was this flag; matching the working path. Diagnostic counters
563
+ // confirm extraction so a regression is visible in the log.
564
+ let got = 0, sawResp = 0;
565
+ await this.fetchMessagesStream(uids.join(","), { source: true }, (msg) => {
566
+ sawResp++;
567
+ if (msg.uid && msg.source) {
568
+ got++;
558
569
  onBody(msg.uid, msg.source);
570
+ }
559
571
  });
572
+ if (got === 0 && uids.length > 0) {
573
+ console.error(` [imap] fetchBodiesBatch ${folderPath}: 0/${uids.length} bodies (saw ${sawResp} FETCH responses) — body literal not extracted`);
574
+ }
560
575
  }
561
576
  /** Fetch messages since a UID */
562
577
  async fetchSinceUid(sinceUid, options = {}, onChunk) {
@@ -691,6 +706,17 @@ export class NativeImapClient {
691
706
  return proto.parseSearchResponse(r.text);
692
707
  }
693
708
  }
709
+ // No untagged * SEARCH seen. If the tagged status is anything but OK,
710
+ // the search FAILED — throw so callers can tell "failed" from "no
711
+ // matches". Silently returning [] here let a failed HEADER search read
712
+ // as "message missing from server", and mailx's sent-sweep then
713
+ // re-APPENDed a copy every 5-minute tick — 3,704 duplicates of one
714
+ // message over ~13 days (2026-07-02). Empty-but-OK still returns []
715
+ // (some servers omit the * SEARCH line when there are zero hits).
716
+ const tagged = responses.find(r => r.tag === tag);
717
+ if (!tagged || tagged.type !== "OK") {
718
+ throw new Error(`SEARCH failed: ${tagged ? `${tagged.type} ${tagged.text || ""}` : "no tagged response"}`);
719
+ }
694
720
  return [];
695
721
  }
696
722
  /** Set flags on a message */
@@ -1498,6 +1524,11 @@ export class NativeImapClient {
1498
1524
  if (envMatch) {
1499
1525
  const env = proto.parseEnvelope(envMatch[1]);
1500
1526
  msg.date = msg.date || env.date;
1527
+ // Expose the ENVELOPE Date: header (the SENT time) separately
1528
+ // from msg.date (INTERNALDATE = server-received time). Callers
1529
+ // that want messages to cluster by when they were sent — not by
1530
+ // per-account delivery time — use sentDate. Additive/optional.
1531
+ msg.sentDate = env.date || undefined;
1501
1532
  msg.subject = env.subject;
1502
1533
  msg.messageId = env.messageId;
1503
1534
  msg.from = env.from;
package/imap-protocol.js CHANGED
@@ -302,8 +302,16 @@ export function parseEnvelope(envStr) {
302
302
  result.to = parseAddressList(tokens[5]);
303
303
  result.cc = parseAddressList(tokens[6]);
304
304
  result.bcc = parseAddressList(tokens[7]);
305
- result.inReplyTo = unquote(tokens[8]);
306
- result.messageId = unquote(tokens[9]);
305
+ // Trim fold whitespace. Servers hand back the ENVELOPE msg-id /
306
+ // in-reply-to with the original header's folding intact when the
307
+ // Message-ID sat on a continuation line ("Message-ID:\r\n\t<...>")
308
+ // — the value arrives as "\t<...>". Stored untrimmed, that tab
309
+ // poisoned every later HEADER search ("missing on server") and
310
+ // fueled mailx's sent-sweep re-APPEND loop: 3,707 duplicates of
311
+ // one Outlook-composed reply (2026-07-02). Trim here so no
312
+ // consumer ever sees the fold whitespace.
313
+ result.inReplyTo = unquote(tokens[8]).trim();
314
+ result.messageId = unquote(tokens[9]).trim();
307
315
  }
308
316
  }
309
317
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/iflow-direct",
3
- "version": "0.1.54",
3
+ "version": "0.1.56",
4
4
  "description": "Direct IMAP client — transport-agnostic, no Node.js dependencies, browser-ready",
5
5
  "main": "index.js",
6
6
  "types": "index.ts",