@bobfrankston/iflow-direct 0.1.27 → 0.1.30

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/gmail.d.ts CHANGED
@@ -12,6 +12,7 @@ export interface GmailOAuthConfig {
12
12
  inactivityTimeout?: number;
13
13
  fetchChunkSize?: number;
14
14
  fetchChunkSizeMax?: number;
15
+ greetingTimeout?: number;
15
16
  }
16
17
  /**
17
18
  * Check if email address is Gmail
@@ -41,5 +42,6 @@ export declare function createAutoImapConfig(config: {
41
42
  inactivityTimeout?: number;
42
43
  fetchChunkSize?: number;
43
44
  fetchChunkSizeMax?: number;
45
+ greetingTimeout?: number;
44
46
  }): ImapClientConfig;
45
47
  //# sourceMappingURL=gmail.d.ts.map
package/gmail.js CHANGED
@@ -36,6 +36,7 @@ export function createGmailConfig(config) {
36
36
  inactivityTimeout: config.inactivityTimeout,
37
37
  fetchChunkSize: config.fetchChunkSize,
38
38
  fetchChunkSizeMax: config.fetchChunkSizeMax,
39
+ greetingTimeout: config.greetingTimeout,
39
40
  };
40
41
  }
41
42
  /**
@@ -56,6 +57,7 @@ export function createAutoImapConfig(config) {
56
57
  inactivityTimeout: config.inactivityTimeout,
57
58
  fetchChunkSize: config.fetchChunkSize,
58
59
  fetchChunkSizeMax: config.fetchChunkSizeMax,
60
+ greetingTimeout: config.greetingTimeout,
59
61
  });
60
62
  }
61
63
  else {
@@ -69,6 +71,7 @@ export function createAutoImapConfig(config) {
69
71
  inactivityTimeout: config.inactivityTimeout,
70
72
  fetchChunkSize: config.fetchChunkSize,
71
73
  fetchChunkSizeMax: config.fetchChunkSizeMax,
74
+ greetingTimeout: config.greetingTimeout,
72
75
  };
73
76
  }
74
77
  }
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;
@@ -164,6 +173,10 @@ export declare class NativeImapClient {
164
173
  * 60s accommodates Gmail which is slow on SEARCH for large folders.
165
174
  * Overridable via ImapClientConfig.inactivityTimeout — slow Dovecot servers need 180s+. */
166
175
  private inactivityTimeout;
176
+ /** Server-greeting timeout in ms — how long to wait for the server's
177
+ * initial banner after TCP/TLS connects. Default 10s; bumped to 30s
178
+ * for slow shared-hosting Dovecot in mailx-imap. */
179
+ private greetingTimeout;
167
180
  /** Fetch chunk sizes — start small for quick first paint, ramp up for throughput.
168
181
  * Default 25 initial → 500 max. Overridable via ImapClientConfig.fetchChunkSize /
169
182
  * fetchChunkSizeMax. Slow servers benefit from smaller chunks (fewer timeouts). */
package/imap-native.js CHANGED
@@ -34,6 +34,7 @@ export class NativeImapClient {
34
34
  this.inactivityTimeout = config.inactivityTimeout ?? 60000;
35
35
  this.fetchChunkSize = config.fetchChunkSize ?? 25;
36
36
  this.fetchChunkSizeMax = config.fetchChunkSizeMax ?? 500;
37
+ this.greetingTimeout = config.greetingTimeout ?? 10000;
37
38
  }
38
39
  get connected() { return this._connected; }
39
40
  /** Check the underlying transport's connected state — catches silently dead sockets. */
@@ -116,8 +117,8 @@ export class NativeImapClient {
116
117
  return new Promise((resolve, reject) => {
117
118
  const timeout = setTimeout(() => {
118
119
  this.greetingResolve = null;
119
- reject(new Error("Greeting timeout (10s)"));
120
- }, 10000);
120
+ reject(new Error(`Greeting timeout (${this.greetingTimeout / 1000}s)`));
121
+ }, this.greetingTimeout);
121
122
  this.greetingResolve = (resp) => {
122
123
  clearTimeout(timeout);
123
124
  resolve(resp);
@@ -400,26 +401,88 @@ export class NativeImapClient {
400
401
  since,
401
402
  before: before || undefined,
402
403
  });
404
+ // SEARCH SINCE on Dovecot can take minutes on a cold mailbox while it
405
+ // (re)builds its date index. Without a "search starting" log, mailx
406
+ // shows nothing for the entire SEARCH window — the user thinks
407
+ // nothing is happening. Bracket the SEARCH with logs so the silence
408
+ // is at least labeled.
409
+ const t0 = Date.now();
410
+ console.log(` [search] SINCE ${since.toISOString().slice(0, 10)}${before ? ` BEFORE ${before.toISOString().slice(0, 10)}` : ""} — running...`);
403
411
  const uids = await this.search(criteria);
412
+ console.log(` [search] returned ${uids.length} UIDs in ${Date.now() - t0}ms`);
404
413
  if (uids.length === 0)
405
414
  return [];
406
415
  // Reverse so newest messages (highest UIDs) come first
407
416
  uids.reverse();
408
- // console.log(` [fetch] ${uids.length} UIDs to fetch (newest first)`);
409
417
  const allMessages = [];
410
418
  let chunkSize = this.fetchChunkSize;
419
+ let chunkIndex = 0;
411
420
  for (let i = 0; i < uids.length; i += chunkSize) {
412
421
  const chunk = uids.slice(i, i + chunkSize);
413
422
  const msgs = await this.fetchMessages(chunk.join(","), options);
414
423
  allMessages.push(...msgs);
415
- // console.log(` [fetch] ${allMessages.length}/${uids.length} (chunk of ${chunk.length})`);
424
+ chunkIndex++;
425
+ // Log every 5th chunk + the first one — enough to see progress
426
+ // without spamming. The first-chunk log proves the fetch is alive
427
+ // even when subsequent chunks are slow.
428
+ if (chunkIndex === 1 || chunkIndex % 5 === 0) {
429
+ console.log(` [fetch] ${allMessages.length}/${uids.length} messages (chunk ${chunkIndex})`);
430
+ }
416
431
  if (onChunk)
417
432
  onChunk(msgs);
418
433
  if (chunkSize < this.fetchChunkSizeMax)
419
434
  chunkSize = Math.min(chunkSize * 4, this.fetchChunkSizeMax);
420
435
  }
436
+ console.log(` [fetch] done — ${allMessages.length} messages in ${Date.now() - t0}ms`);
421
437
  return allMessages;
422
438
  }
439
+ /** Fetch the most recent N messages by sequence number — avoids
440
+ * SEARCH SINCE which can take minutes on cold Dovecot mailboxes. The
441
+ * selected mailbox's `EXISTS` count is used to compute the range
442
+ * `(EXISTS-N+1):EXISTS`; sequence FETCH is O(1) on the server because
443
+ * it just reads message slots, no INTERNALDATE walk. Caller must have
444
+ * SELECTed the mailbox first (sequence numbers are session-relative). */
445
+ async fetchLatestN(n, options = {}, onChunk) {
446
+ const exists = this.mailboxInfo.exists;
447
+ if (!exists || exists < 1)
448
+ return [];
449
+ const start = Math.max(1, exists - n + 1);
450
+ const range = `${start}:${exists}`;
451
+ const t0 = Date.now();
452
+ console.log(` [fetch-latest] ${this.selectedMailbox || "?"}: sequence ${range} (${Math.min(n, exists)} most recent of ${exists})`);
453
+ const items = ["UID", "FLAGS", "ENVELOPE", "RFC822.SIZE", "INTERNALDATE", "BODY.PEEK[HEADER]"];
454
+ if (options.source)
455
+ items.push("BODY.PEEK[]");
456
+ const tag = proto.nextTag();
457
+ const streamed = [];
458
+ const seenChunks = new Set();
459
+ const cb = (resp) => {
460
+ if (resp.tag !== "*" || resp.type !== "FETCH")
461
+ return;
462
+ const parsed = this.parseFetchResponses([resp]);
463
+ for (const msg of parsed)
464
+ streamed.push(msg);
465
+ // Flush onChunk every ~50 messages without buffering the whole
466
+ // response in memory — same shape as fetchByDate's chunk emit.
467
+ const bucket = Math.floor(streamed.length / 50);
468
+ if (onChunk && !seenChunks.has(bucket)) {
469
+ seenChunks.add(bucket);
470
+ onChunk(parsed);
471
+ }
472
+ };
473
+ await this.sendCommand(tag, proto.seqFetchCommand(tag, range, items), cb);
474
+ if (onChunk && streamed.length > 0) {
475
+ // Final flush in case last bucket didn't trip the threshold.
476
+ const lastBucket = Math.floor(streamed.length / 50);
477
+ if (!seenChunks.has(lastBucket))
478
+ onChunk([]);
479
+ }
480
+ // Reverse so newest-first matches fetchByDate ordering — caller code
481
+ // (mailx) expects that ordering when computing highestUid windows.
482
+ streamed.reverse();
483
+ console.log(` [fetch-latest] done — ${streamed.length} messages in ${Date.now() - t0}ms`);
484
+ return streamed;
485
+ }
423
486
  /** Fetch a single message by UID */
424
487
  async fetchMessage(uid, options = {}) {
425
488
  const msgs = await this.fetchMessages(String(uid), options);
@@ -666,6 +729,10 @@ export class NativeImapClient {
666
729
  * 60s accommodates Gmail which is slow on SEARCH for large folders.
667
730
  * Overridable via ImapClientConfig.inactivityTimeout — slow Dovecot servers need 180s+. */
668
731
  inactivityTimeout;
732
+ /** Server-greeting timeout in ms — how long to wait for the server's
733
+ * initial banner after TCP/TLS connects. Default 10s; bumped to 30s
734
+ * for slow shared-hosting Dovecot in mailx-imap. */
735
+ greetingTimeout;
669
736
  /** Fetch chunk sizes — start small for quick first paint, ramp up for throughput.
670
737
  * Default 25 initial → 500 max. Overridable via ImapClientConfig.fetchChunkSize /
671
738
  * fetchChunkSizeMax. Slow servers benefit from smaller chunks (fewer timeouts). */
@@ -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.30",
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
  }
package/types.d.ts CHANGED
@@ -13,5 +13,6 @@ export interface ImapClientConfig {
13
13
  inactivityTimeout?: number;
14
14
  fetchChunkSize?: number;
15
15
  fetchChunkSizeMax?: number;
16
+ greetingTimeout?: number;
16
17
  }
17
18
  //# sourceMappingURL=types.d.ts.map