@bobfrankston/iflow-direct 0.1.27 → 0.1.28

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/imap-compat.d.ts CHANGED
@@ -51,6 +51,13 @@ export declare class CompatImapClient {
51
51
  fetchMessageByDate(mailbox: string, start: Date, end?: Date, options?: {
52
52
  source?: boolean;
53
53
  }, onChunk?: (msgs: FetchedMessage[]) => void): Promise<FetchedMessage[]>;
54
+ /** Fetch the most recent N messages by sequence number — sidesteps
55
+ * SEARCH SINCE which can take minutes on cold Dovecot mailboxes. Used
56
+ * by mailx-imap's first-sync path so the user sees something fast even
57
+ * on a wide folder tree. The 30-day backfill is a follow-up step. */
58
+ fetchLatestN(mailbox: string, n: number, options?: {
59
+ source?: boolean;
60
+ }, onChunk?: (msgs: FetchedMessage[]) => void): Promise<FetchedMessage[]>;
54
61
  /** Fetch a single message by UID */
55
62
  fetchMessageByUid(mailbox: string, uid: number, options?: {
56
63
  source?: boolean;
package/imap-compat.js CHANGED
@@ -135,6 +135,18 @@ export class CompatImapClient {
135
135
  await this.native.closeMailbox();
136
136
  return msgs.map(m => new FetchedMessage(m));
137
137
  }
138
+ /** Fetch the most recent N messages by sequence number — sidesteps
139
+ * SEARCH SINCE which can take minutes on cold Dovecot mailboxes. Used
140
+ * by mailx-imap's first-sync path so the user sees something fast even
141
+ * on a wide folder tree. The 30-day backfill is a follow-up step. */
142
+ async fetchLatestN(mailbox, n, options, onChunk) {
143
+ await this.ensureConnected();
144
+ await this.native.select(mailbox);
145
+ const chunkCb = onChunk ? (raw) => onChunk(raw.map(m => new FetchedMessage(m))) : undefined;
146
+ const msgs = await this.native.fetchLatestN(n, options, chunkCb);
147
+ await this.native.closeMailbox();
148
+ return msgs.map(m => new FetchedMessage(m));
149
+ }
138
150
  /** Fetch a single message by UID */
139
151
  async fetchMessageByUid(mailbox, uid, options) {
140
152
  await this.ensureConnected();
package/imap-native.d.ts CHANGED
@@ -119,6 +119,15 @@ export declare class NativeImapClient {
119
119
  fetchByDate(since: Date, before?: Date, options?: {
120
120
  source?: boolean;
121
121
  }, onChunk?: (msgs: NativeFetchedMessage[]) => void): Promise<NativeFetchedMessage[]>;
122
+ /** Fetch the most recent N messages by sequence number — avoids
123
+ * SEARCH SINCE which can take minutes on cold Dovecot mailboxes. The
124
+ * selected mailbox's `EXISTS` count is used to compute the range
125
+ * `(EXISTS-N+1):EXISTS`; sequence FETCH is O(1) on the server because
126
+ * it just reads message slots, no INTERNALDATE walk. Caller must have
127
+ * SELECTed the mailbox first (sequence numbers are session-relative). */
128
+ fetchLatestN(n: number, options?: {
129
+ source?: boolean;
130
+ }, onChunk?: (msgs: NativeFetchedMessage[]) => void): Promise<NativeFetchedMessage[]>;
122
131
  /** Fetch a single message by UID */
123
132
  fetchMessage(uid: number, options?: {
124
133
  source?: boolean;
package/imap-native.js CHANGED
@@ -400,26 +400,88 @@ export class NativeImapClient {
400
400
  since,
401
401
  before: before || undefined,
402
402
  });
403
+ // SEARCH SINCE on Dovecot can take minutes on a cold mailbox while it
404
+ // (re)builds its date index. Without a "search starting" log, mailx
405
+ // shows nothing for the entire SEARCH window — the user thinks
406
+ // nothing is happening. Bracket the SEARCH with logs so the silence
407
+ // is at least labeled.
408
+ const t0 = Date.now();
409
+ console.log(` [search] SINCE ${since.toISOString().slice(0, 10)}${before ? ` BEFORE ${before.toISOString().slice(0, 10)}` : ""} — running...`);
403
410
  const uids = await this.search(criteria);
411
+ console.log(` [search] returned ${uids.length} UIDs in ${Date.now() - t0}ms`);
404
412
  if (uids.length === 0)
405
413
  return [];
406
414
  // Reverse so newest messages (highest UIDs) come first
407
415
  uids.reverse();
408
- // console.log(` [fetch] ${uids.length} UIDs to fetch (newest first)`);
409
416
  const allMessages = [];
410
417
  let chunkSize = this.fetchChunkSize;
418
+ let chunkIndex = 0;
411
419
  for (let i = 0; i < uids.length; i += chunkSize) {
412
420
  const chunk = uids.slice(i, i + chunkSize);
413
421
  const msgs = await this.fetchMessages(chunk.join(","), options);
414
422
  allMessages.push(...msgs);
415
- // console.log(` [fetch] ${allMessages.length}/${uids.length} (chunk of ${chunk.length})`);
423
+ chunkIndex++;
424
+ // Log every 5th chunk + the first one — enough to see progress
425
+ // without spamming. The first-chunk log proves the fetch is alive
426
+ // even when subsequent chunks are slow.
427
+ if (chunkIndex === 1 || chunkIndex % 5 === 0) {
428
+ console.log(` [fetch] ${allMessages.length}/${uids.length} messages (chunk ${chunkIndex})`);
429
+ }
416
430
  if (onChunk)
417
431
  onChunk(msgs);
418
432
  if (chunkSize < this.fetchChunkSizeMax)
419
433
  chunkSize = Math.min(chunkSize * 4, this.fetchChunkSizeMax);
420
434
  }
435
+ console.log(` [fetch] done — ${allMessages.length} messages in ${Date.now() - t0}ms`);
421
436
  return allMessages;
422
437
  }
438
+ /** Fetch the most recent N messages by sequence number — avoids
439
+ * SEARCH SINCE which can take minutes on cold Dovecot mailboxes. The
440
+ * selected mailbox's `EXISTS` count is used to compute the range
441
+ * `(EXISTS-N+1):EXISTS`; sequence FETCH is O(1) on the server because
442
+ * it just reads message slots, no INTERNALDATE walk. Caller must have
443
+ * SELECTed the mailbox first (sequence numbers are session-relative). */
444
+ async fetchLatestN(n, options = {}, onChunk) {
445
+ const exists = this.mailboxInfo.exists;
446
+ if (!exists || exists < 1)
447
+ return [];
448
+ const start = Math.max(1, exists - n + 1);
449
+ const range = `${start}:${exists}`;
450
+ const t0 = Date.now();
451
+ console.log(` [fetch-latest] ${this.selectedMailbox || "?"}: sequence ${range} (${Math.min(n, exists)} most recent of ${exists})`);
452
+ const items = ["UID", "FLAGS", "ENVELOPE", "RFC822.SIZE", "INTERNALDATE", "BODY.PEEK[HEADER]"];
453
+ if (options.source)
454
+ items.push("BODY.PEEK[]");
455
+ const tag = proto.nextTag();
456
+ const streamed = [];
457
+ const seenChunks = new Set();
458
+ const cb = (resp) => {
459
+ if (resp.tag !== "*" || resp.type !== "FETCH")
460
+ return;
461
+ const parsed = this.parseFetchResponses([resp]);
462
+ for (const msg of parsed)
463
+ streamed.push(msg);
464
+ // Flush onChunk every ~50 messages without buffering the whole
465
+ // response in memory — same shape as fetchByDate's chunk emit.
466
+ const bucket = Math.floor(streamed.length / 50);
467
+ if (onChunk && !seenChunks.has(bucket)) {
468
+ seenChunks.add(bucket);
469
+ onChunk(parsed);
470
+ }
471
+ };
472
+ await this.sendCommand(tag, proto.seqFetchCommand(tag, range, items), cb);
473
+ if (onChunk && streamed.length > 0) {
474
+ // Final flush in case last bucket didn't trip the threshold.
475
+ const lastBucket = Math.floor(streamed.length / 50);
476
+ if (!seenChunks.has(lastBucket))
477
+ onChunk([]);
478
+ }
479
+ // Reverse so newest-first matches fetchByDate ordering — caller code
480
+ // (mailx) expects that ordering when computing highestUid windows.
481
+ streamed.reverse();
482
+ console.log(` [fetch-latest] done — ${streamed.length} messages in ${Date.now() - t0}ms`);
483
+ return streamed;
484
+ }
423
485
  /** Fetch a single message by UID */
424
486
  async fetchMessage(uid, options = {}) {
425
487
  const msgs = await this.fetchMessages(String(uid), options);
@@ -79,6 +79,10 @@ export declare function examineCommand(tag: string, mailbox: string): string;
79
79
  export declare function statusCommand(tag: string, mailbox: string, items?: string[]): string;
80
80
  /** Build UID FETCH command */
81
81
  export declare function fetchCommand(tag: string, range: string, items: string[]): string;
82
+ /** Build sequence-number FETCH command (no UID prefix). Used by fetchLatestN
83
+ * to avoid SEARCH SINCE on a cold mailbox — sequence ranges are O(1) on the
84
+ * server because they're just message numbers in the current mailbox. */
85
+ export declare function seqFetchCommand(tag: string, range: string, items: string[]): string;
82
86
  /** Build UID SEARCH command */
83
87
  export declare function searchCommand(tag: string, criteria: string): string;
84
88
  /** Build UID STORE command (set/add/remove flags) */
package/imap-protocol.js CHANGED
@@ -48,6 +48,12 @@ export function statusCommand(tag, mailbox, items = ["MESSAGES", "UIDNEXT"]) {
48
48
  export function fetchCommand(tag, range, items) {
49
49
  return buildCommand(tag, `UID FETCH ${range} (${items.join(" ")})`);
50
50
  }
51
+ /** Build sequence-number FETCH command (no UID prefix). Used by fetchLatestN
52
+ * to avoid SEARCH SINCE on a cold mailbox — sequence ranges are O(1) on the
53
+ * server because they're just message numbers in the current mailbox. */
54
+ export function seqFetchCommand(tag, range, items) {
55
+ return buildCommand(tag, `FETCH ${range} (${items.join(" ")})`);
56
+ }
51
57
  /** Build UID SEARCH command */
52
58
  export function searchCommand(tag, criteria) {
53
59
  return buildCommand(tag, `UID SEARCH ${criteria}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/iflow-direct",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
4
4
  "description": "Direct IMAP client — transport-agnostic, no Node.js dependencies, browser-ready",
5
5
  "main": "index.js",
6
6
  "types": "index.ts",
@@ -19,7 +19,7 @@
19
19
  "author": "Bob Frankston",
20
20
  "license": "ISC",
21
21
  "dependencies": {
22
- "@bobfrankston/tcp-transport": "^0.1.4"
22
+ "@bobfrankston/tcp-transport": "^0.1.5"
23
23
  },
24
24
  "exports": {
25
25
  ".": {
@@ -50,7 +50,7 @@
50
50
  },
51
51
  ".transformedSnapshot": {
52
52
  "dependencies": {
53
- "@bobfrankston/tcp-transport": "^0.1.4"
53
+ "@bobfrankston/tcp-transport": "^0.1.5"
54
54
  }
55
55
  }
56
56
  }