@bobfrankston/iflow 1.0.53 → 1.0.55

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.
@@ -37,10 +37,10 @@ export declare class CompatImapClient {
37
37
  fetchMessagesSinceUid(mailbox: string, sinceUid: number, options?: {
38
38
  source?: boolean;
39
39
  }): Promise<any[]>;
40
- /** Fetch messages by date range */
40
+ /** Fetch messages by date range. Optional onChunk callback for incremental processing. */
41
41
  fetchMessageByDate(mailbox: string, start: Date, end?: Date, options?: {
42
42
  source?: boolean;
43
- }): Promise<any[]>;
43
+ }, onChunk?: (msgs: any[]) => void): Promise<any[]>;
44
44
  /** Fetch a single message by UID */
45
45
  fetchMessageByUid(mailbox: string, uid: number, options?: {
46
46
  source?: boolean;
@@ -102,11 +102,12 @@ export class CompatImapClient {
102
102
  await this.native.closeMailbox();
103
103
  return msgs.map(toCompatMessage);
104
104
  }
105
- /** Fetch messages by date range */
106
- async fetchMessageByDate(mailbox, start, end, options) {
105
+ /** Fetch messages by date range. Optional onChunk callback for incremental processing. */
106
+ async fetchMessageByDate(mailbox, start, end, options, onChunk) {
107
107
  await this.ensureConnected();
108
108
  await this.native.select(mailbox);
109
- const msgs = await this.native.fetchByDate(start, end, options);
109
+ const chunkCb = onChunk ? (raw) => onChunk(raw.map(toCompatMessage)) : undefined;
110
+ const msgs = await this.native.fetchByDate(start, end, options, chunkCb);
110
111
  await this.native.closeMailbox();
111
112
  return msgs.map(toCompatMessage);
112
113
  }
@@ -86,11 +86,11 @@ export declare class NativeImapClient {
86
86
  /** Fetch messages since a UID */
87
87
  fetchSinceUid(sinceUid: number, options?: {
88
88
  source?: boolean;
89
- }): Promise<NativeFetchedMessage[]>;
90
- /** Fetch messages by date range */
89
+ }, onChunk?: (msgs: NativeFetchedMessage[]) => void): Promise<NativeFetchedMessage[]>;
90
+ /** Fetch messages by date range. Optional onChunk callback receives each batch as it arrives. */
91
91
  fetchByDate(since: Date, before?: Date, options?: {
92
92
  source?: boolean;
93
- }): Promise<NativeFetchedMessage[]>;
93
+ }, onChunk?: (msgs: NativeFetchedMessage[]) => void): Promise<NativeFetchedMessage[]>;
94
94
  /** Fetch a single message by UID */
95
95
  fetchMessage(uid: number, options?: {
96
96
  source?: boolean;
@@ -115,8 +115,15 @@ export declare class NativeImapClient {
115
115
  appendMessage(mailbox: string, message: string | Uint8Array, flags?: string[]): Promise<number | null>;
116
116
  startIdle(onNewMail: (count: number) => void): Promise<() => Promise<void>>;
117
117
  getMessageCount(mailbox: string): Promise<number>;
118
- /** Default timeout for IMAP commands (30s). Prevents hanging connections. */
119
- private commandTimeout;
118
+ /** Inactivity timeout how long to wait with NO data before declaring the connection dead.
119
+ * This is NOT a wall-clock timeout. Timer resets every time data arrives from the server.
120
+ * A large FETCH returning data continuously will never timeout. */
121
+ private inactivityTimeout;
122
+ /** Fetch chunk sizes — start small for quick first paint, ramp up for throughput */
123
+ private static INITIAL_CHUNK_SIZE;
124
+ private static MAX_CHUNK_SIZE;
125
+ /** Active command timer — reset by handleData on every data arrival */
126
+ private commandTimer;
120
127
  private sendCommand;
121
128
  private waitForContinuation;
122
129
  private waitForTagged;
@@ -289,12 +289,33 @@ export class NativeImapClient {
289
289
  return this.parseFetchResponses(responses);
290
290
  }
291
291
  /** Fetch messages since a UID */
292
- async fetchSinceUid(sinceUid, options = {}) {
293
- return this.fetchMessages(`${sinceUid + 1}:*`, options);
292
+ async fetchSinceUid(sinceUid, options = {}, onChunk) {
293
+ const uids = await this.search(`UID ${sinceUid + 1}:*`);
294
+ if (uids.length === 0)
295
+ return [];
296
+ console.log(` [fetch] ${uids.length} UIDs since ${sinceUid}`);
297
+ if (uids.length <= NativeImapClient.INITIAL_CHUNK_SIZE) {
298
+ const msgs = await this.fetchMessages(uids.join(","), options);
299
+ if (onChunk)
300
+ onChunk(msgs);
301
+ return msgs;
302
+ }
303
+ const allMessages = [];
304
+ let chunkSize = NativeImapClient.INITIAL_CHUNK_SIZE;
305
+ for (let i = 0; i < uids.length; i += chunkSize) {
306
+ const chunk = uids.slice(i, i + chunkSize);
307
+ const msgs = await this.fetchMessages(chunk.join(","), options);
308
+ allMessages.push(...msgs);
309
+ console.log(` [fetch] ${allMessages.length}/${uids.length} (chunk of ${chunk.length})`);
310
+ if (onChunk)
311
+ onChunk(msgs);
312
+ if (chunkSize < NativeImapClient.MAX_CHUNK_SIZE)
313
+ chunkSize = Math.min(chunkSize * 4, NativeImapClient.MAX_CHUNK_SIZE);
314
+ }
315
+ return allMessages;
294
316
  }
295
- /** Fetch messages by date range */
296
- async fetchByDate(since, before, options = {}) {
297
- // First search for UIDs in date range, then fetch
317
+ /** Fetch messages by date range. Optional onChunk callback receives each batch as it arrives. */
318
+ async fetchByDate(since, before, options = {}, onChunk) {
298
319
  const criteria = proto.buildSearchCriteria({
299
320
  since,
300
321
  before: before || undefined,
@@ -302,14 +323,18 @@ export class NativeImapClient {
302
323
  const uids = await this.search(criteria);
303
324
  if (uids.length === 0)
304
325
  return [];
305
- // Fetch in chunks to avoid very long command lines
306
- const chunkSize = 500;
326
+ console.log(` [fetch] ${uids.length} UIDs to fetch`);
307
327
  const allMessages = [];
328
+ let chunkSize = NativeImapClient.INITIAL_CHUNK_SIZE;
308
329
  for (let i = 0; i < uids.length; i += chunkSize) {
309
330
  const chunk = uids.slice(i, i + chunkSize);
310
- const range = chunk.join(",");
311
- const msgs = await this.fetchMessages(range, options);
331
+ const msgs = await this.fetchMessages(chunk.join(","), options);
312
332
  allMessages.push(...msgs);
333
+ console.log(` [fetch] ${allMessages.length}/${uids.length} (chunk of ${chunk.length})`);
334
+ if (onChunk)
335
+ onChunk(msgs);
336
+ if (chunkSize < NativeImapClient.MAX_CHUNK_SIZE)
337
+ chunkSize = Math.min(chunkSize * 4, NativeImapClient.MAX_CHUNK_SIZE);
313
338
  }
314
339
  return allMessages;
315
340
  }
@@ -426,23 +451,37 @@ export class NativeImapClient {
426
451
  return status.messages || 0;
427
452
  }
428
453
  // ── Low-level command handling ──
429
- /** Default timeout for IMAP commands (30s). Prevents hanging connections. */
430
- commandTimeout = 30000;
454
+ /** Inactivity timeout how long to wait with NO data before declaring the connection dead.
455
+ * This is NOT a wall-clock timeout. Timer resets every time data arrives from the server.
456
+ * A large FETCH returning data continuously will never timeout. */
457
+ inactivityTimeout = 30000;
458
+ /** Fetch chunk sizes — start small for quick first paint, ramp up for throughput */
459
+ static INITIAL_CHUNK_SIZE = 25;
460
+ static MAX_CHUNK_SIZE = 500;
461
+ /** Active command timer — reset by handleData on every data arrival */
462
+ commandTimer = null;
431
463
  sendCommand(tag, command) {
432
464
  return new Promise((resolve, reject) => {
433
465
  if (this.verbose && !command.includes("LOGIN") && !command.includes("AUTHENTICATE")) {
434
466
  console.log(` [imap] > ${command.trimEnd()}`);
435
467
  }
436
- const timer = setTimeout(() => {
468
+ const onTimeout = () => {
469
+ this.commandTimer = null;
437
470
  this.pendingCommand = null;
438
- reject(new Error(`IMAP command timeout (${this.commandTimeout / 1000}s): ${command.split("\r")[0].substring(0, 50)}`));
439
- }, this.commandTimeout);
471
+ // Kill the connection a timed-out connection has stale data in the pipe
472
+ this.transport.close?.();
473
+ reject(new Error(`IMAP inactivity timeout (${this.inactivityTimeout / 1000}s): ${command.split("\r")[0].substring(0, 80)}`));
474
+ };
475
+ this.commandTimer = setTimeout(onTimeout, this.inactivityTimeout);
440
476
  this.pendingCommand = {
441
477
  tag, responses: [],
442
- resolve: (responses) => { clearTimeout(timer); resolve(responses); },
443
- reject: (err) => { clearTimeout(timer); reject(err); },
478
+ resolve: (responses) => { if (this.commandTimer)
479
+ clearTimeout(this.commandTimer); this.commandTimer = null; resolve(responses); },
480
+ reject: (err) => { if (this.commandTimer)
481
+ clearTimeout(this.commandTimer); this.commandTimer = null; reject(err); },
444
482
  };
445
- this.transport.write(command).catch((err) => { clearTimeout(timer); reject(err); });
483
+ this.transport.write(command).catch((err) => { if (this.commandTimer)
484
+ clearTimeout(this.commandTimer); this.commandTimer = null; reject(err); });
446
485
  });
447
486
  }
448
487
  waitForContinuation(tag) {
@@ -464,6 +503,19 @@ export class NativeImapClient {
464
503
  });
465
504
  }
466
505
  handleData(data) {
506
+ // Reset inactivity timer — data is flowing, connection is alive
507
+ if (this.commandTimer) {
508
+ clearTimeout(this.commandTimer);
509
+ this.commandTimer = setTimeout(() => {
510
+ this.commandTimer = null;
511
+ if (this.pendingCommand) {
512
+ const cmd = this.pendingCommand;
513
+ this.pendingCommand = null;
514
+ this.transport.close?.();
515
+ cmd.reject(new Error(`IMAP inactivity timeout (${this.inactivityTimeout / 1000}s)`));
516
+ }
517
+ }, this.inactivityTimeout);
518
+ }
467
519
  this.buffer += data;
468
520
  this.processBuffer();
469
521
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/iflow",
3
- "version": "1.0.53",
3
+ "version": "1.0.55",
4
4
  "description": "IMAP client wrapper library",
5
5
  "main": "index.js",
6
6
  "types": "index.ts",