@bobfrankston/iflow 1.0.53 → 1.0.54

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.
@@ -115,8 +115,12 @@ 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
+ /** Active command timer — reset by handleData on every data arrival */
123
+ private commandTimer;
120
124
  private sendCommand;
121
125
  private waitForContinuation;
122
126
  private waitForTagged;
@@ -290,7 +290,21 @@ export class NativeImapClient {
290
290
  }
291
291
  /** Fetch messages since a UID */
292
292
  async fetchSinceUid(sinceUid, options = {}) {
293
- return this.fetchMessages(`${sinceUid + 1}:*`, options);
293
+ // Search for UIDs first, then fetch in chunks (avoids unbounded single command)
294
+ const uids = await this.search(`UID ${sinceUid + 1}:*`);
295
+ if (uids.length === 0)
296
+ return [];
297
+ if (uids.length <= 500) {
298
+ return this.fetchMessages(uids.join(","), options);
299
+ }
300
+ // Chunk large fetches
301
+ const allMessages = [];
302
+ for (let i = 0; i < uids.length; i += 500) {
303
+ const chunk = uids.slice(i, i + 500);
304
+ const msgs = await this.fetchMessages(chunk.join(","), options);
305
+ allMessages.push(...msgs);
306
+ }
307
+ return allMessages;
294
308
  }
295
309
  /** Fetch messages by date range */
296
310
  async fetchByDate(since, before, options = {}) {
@@ -426,23 +440,34 @@ export class NativeImapClient {
426
440
  return status.messages || 0;
427
441
  }
428
442
  // ── Low-level command handling ──
429
- /** Default timeout for IMAP commands (30s). Prevents hanging connections. */
430
- commandTimeout = 30000;
443
+ /** Inactivity timeout how long to wait with NO data before declaring the connection dead.
444
+ * This is NOT a wall-clock timeout. Timer resets every time data arrives from the server.
445
+ * A large FETCH returning data continuously will never timeout. */
446
+ inactivityTimeout = 30000;
447
+ /** Active command timer — reset by handleData on every data arrival */
448
+ commandTimer = null;
431
449
  sendCommand(tag, command) {
432
450
  return new Promise((resolve, reject) => {
433
451
  if (this.verbose && !command.includes("LOGIN") && !command.includes("AUTHENTICATE")) {
434
452
  console.log(` [imap] > ${command.trimEnd()}`);
435
453
  }
436
- const timer = setTimeout(() => {
454
+ const onTimeout = () => {
455
+ this.commandTimer = null;
437
456
  this.pendingCommand = null;
438
- reject(new Error(`IMAP command timeout (${this.commandTimeout / 1000}s): ${command.split("\r")[0].substring(0, 50)}`));
439
- }, this.commandTimeout);
457
+ // Kill the connection a timed-out connection has stale data in the pipe
458
+ this.transport.close?.();
459
+ reject(new Error(`IMAP inactivity timeout (${this.inactivityTimeout / 1000}s): ${command.split("\r")[0].substring(0, 80)}`));
460
+ };
461
+ this.commandTimer = setTimeout(onTimeout, this.inactivityTimeout);
440
462
  this.pendingCommand = {
441
463
  tag, responses: [],
442
- resolve: (responses) => { clearTimeout(timer); resolve(responses); },
443
- reject: (err) => { clearTimeout(timer); reject(err); },
464
+ resolve: (responses) => { if (this.commandTimer)
465
+ clearTimeout(this.commandTimer); this.commandTimer = null; resolve(responses); },
466
+ reject: (err) => { if (this.commandTimer)
467
+ clearTimeout(this.commandTimer); this.commandTimer = null; reject(err); },
444
468
  };
445
- this.transport.write(command).catch((err) => { clearTimeout(timer); reject(err); });
469
+ this.transport.write(command).catch((err) => { if (this.commandTimer)
470
+ clearTimeout(this.commandTimer); this.commandTimer = null; reject(err); });
446
471
  });
447
472
  }
448
473
  waitForContinuation(tag) {
@@ -464,6 +489,19 @@ export class NativeImapClient {
464
489
  });
465
490
  }
466
491
  handleData(data) {
492
+ // Reset inactivity timer — data is flowing, connection is alive
493
+ if (this.commandTimer) {
494
+ clearTimeout(this.commandTimer);
495
+ this.commandTimer = setTimeout(() => {
496
+ this.commandTimer = null;
497
+ if (this.pendingCommand) {
498
+ const cmd = this.pendingCommand;
499
+ this.pendingCommand = null;
500
+ this.transport.close?.();
501
+ cmd.reject(new Error(`IMAP inactivity timeout (${this.inactivityTimeout / 1000}s)`));
502
+ }
503
+ }, this.inactivityTimeout);
504
+ }
467
505
  this.buffer += data;
468
506
  this.processBuffer();
469
507
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/iflow",
3
- "version": "1.0.53",
3
+ "version": "1.0.54",
4
4
  "description": "IMAP client wrapper library",
5
5
  "main": "index.js",
6
6
  "types": "index.ts",