@bobfrankston/iflow-direct 0.1.10 → 0.1.12

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
@@ -49,10 +49,20 @@ export declare class CompatImapClient {
49
49
  getMessagesCount(mailbox: string): Promise<number>;
50
50
  /** Get all UIDs in a mailbox */
51
51
  getUids(mailbox: string): Promise<number[]>;
52
- /** Fetch messages by UID range string (e.g. "100,200,300" or "100:200") */
52
+ /** Fetch messages supports two calling conventions for compatibility:
53
+ *
54
+ * New: fetchMessages(mailbox, "100:200") — UID range string
55
+ * Old: fetchMessages(mailbox, endSeq, count) — sequence-number range (iflow compat)
56
+ *
57
+ * The old form computes UID range "start:end" from (end - count + 1) : end,
58
+ * matching the legacy iflow/imapflow fetchMessages(mailbox, end, count) API
59
+ * used by the puller. */
53
60
  fetchMessages(mailbox: string, uidRange: string, options?: {
54
61
  source?: boolean;
55
62
  }): Promise<any[]>;
63
+ fetchMessages(mailbox: string, end: number, count: number, options?: {
64
+ source?: boolean;
65
+ }): Promise<any[]>;
56
66
  /** Search messages in a mailbox */
57
67
  searchMessages(mailbox: string, criteria: any): Promise<number[]>;
58
68
  /** Search by header value — returns matching UIDs */
package/imap-compat.js CHANGED
@@ -160,11 +160,25 @@ export class CompatImapClient {
160
160
  await this.native.closeMailbox();
161
161
  return uids;
162
162
  }
163
- /** Fetch messages by UID range string (e.g. "100,200,300" or "100:200") */
164
- async fetchMessages(mailbox, uidRange, options) {
163
+ async fetchMessages(mailbox, rangeOrEnd, countOrOptions, maybeOptions) {
164
+ let range;
165
+ let options;
166
+ if (typeof rangeOrEnd === "number") {
167
+ // Legacy (iflow) calling convention: (mailbox, endSeq, count, options?)
168
+ const end = rangeOrEnd;
169
+ const count = typeof countOrOptions === "number" ? countOrOptions : 1;
170
+ const start = Math.max(1, end - count + 1);
171
+ range = `${start}:${end}`;
172
+ options = maybeOptions;
173
+ }
174
+ else {
175
+ // New calling convention: (mailbox, uidRange, options?)
176
+ range = rangeOrEnd;
177
+ options = typeof countOrOptions === "object" ? countOrOptions : undefined;
178
+ }
165
179
  await this.ensureConnected();
166
180
  await this.native.select(mailbox);
167
- const msgs = await this.native.fetchMessages(uidRange, options);
181
+ const msgs = await this.native.fetchMessages(range, options);
168
182
  await this.native.closeMailbox();
169
183
  return msgs.map(toCompatMessage);
170
184
  }
package/imap-native.d.ts CHANGED
@@ -54,6 +54,7 @@ export declare class NativeImapClient {
54
54
  private _connected;
55
55
  private idleTag;
56
56
  private idleCallback;
57
+ private idleRefreshTimer;
57
58
  private verbose;
58
59
  private selectedMailbox;
59
60
  private mailboxInfo;
@@ -114,6 +115,8 @@ export declare class NativeImapClient {
114
115
  /** Append a message to a mailbox */
115
116
  appendMessage(mailbox: string, message: string | Uint8Array, flags?: string[]): Promise<number | null>;
116
117
  startIdle(onNewMail: (count: number) => void): Promise<() => Promise<void>>;
118
+ /** Send DONE + re-IDLE. Called by the 28-minute refresh timer. */
119
+ private refreshIdle;
117
120
  getMessageCount(mailbox: string): Promise<number>;
118
121
  /** Inactivity timeout — how long to wait with NO data before declaring the connection dead.
119
122
  * This is NOT a wall-clock timeout. Timer resets every time data arrives from the server.
package/imap-native.js CHANGED
@@ -17,6 +17,7 @@ export class NativeImapClient {
17
17
  _connected = false;
18
18
  idleTag = null;
19
19
  idleCallback = null;
20
+ idleRefreshTimer = null;
20
21
  verbose;
21
22
  selectedMailbox = null;
22
23
  mailboxInfo = { exists: 0, recent: 0, uidNext: 0, uidValidity: 0, flags: [], permanentFlags: [] };
@@ -439,18 +440,74 @@ export class NativeImapClient {
439
440
  // ── IDLE ──
440
441
  async startIdle(onNewMail) {
441
442
  this.idleCallback = onNewMail;
442
- const tag = proto.nextTag();
443
- this.idleTag = tag;
444
- await this.transport.write(proto.idleCommand(tag));
445
- // Wait for "+" continuation
446
- await this.waitForContinuation(tag);
443
+ let stopped = false;
444
+ const beginIdleCycle = async () => {
445
+ const tag = proto.nextTag();
446
+ this.idleTag = tag;
447
+ await this.transport.write(proto.idleCommand(tag));
448
+ await this.waitForContinuation(tag);
449
+ // RFC 2177: re-IDLE every ~28 minutes to avoid servers that terminate
450
+ // idle sessions at the 29-minute mark, and to give us a steady
451
+ // application-level keepalive that detects silently-dropped sockets.
452
+ this.idleRefreshTimer = setTimeout(() => {
453
+ if (stopped)
454
+ return;
455
+ this.refreshIdle().catch(err => {
456
+ if (this.verbose)
457
+ console.error(` [imap] IDLE refresh failed: ${err.message}`);
458
+ });
459
+ }, 28 * 60 * 1000);
460
+ };
461
+ await beginIdleCycle();
447
462
  return async () => {
463
+ stopped = true;
464
+ if (this.idleRefreshTimer) {
465
+ clearTimeout(this.idleRefreshTimer);
466
+ this.idleRefreshTimer = null;
467
+ }
468
+ const tag = this.idleTag;
448
469
  this.idleTag = null;
449
470
  this.idleCallback = null;
450
- await this.transport.write(proto.doneCommand());
451
- await this.waitForTagged(tag);
471
+ try {
472
+ await this.transport.write(proto.doneCommand());
473
+ }
474
+ catch { /* ignore */ }
475
+ if (tag) {
476
+ try {
477
+ await this.waitForTagged(tag);
478
+ }
479
+ catch { /* ignore */ }
480
+ }
452
481
  };
453
482
  }
483
+ /** Send DONE + re-IDLE. Called by the 28-minute refresh timer. */
484
+ async refreshIdle() {
485
+ const oldTag = this.idleTag;
486
+ if (!oldTag || !this.idleCallback)
487
+ return;
488
+ const cb = this.idleCallback;
489
+ // DONE the current IDLE
490
+ await this.transport.write(proto.doneCommand());
491
+ try {
492
+ await this.waitForTagged(oldTag);
493
+ }
494
+ catch { /* best-effort */ }
495
+ // Re-IDLE with a new tag
496
+ this.idleCallback = cb; // waitForTagged resolved pendingCommand, reinstall callback
497
+ const newTag = proto.nextTag();
498
+ this.idleTag = newTag;
499
+ await this.transport.write(proto.idleCommand(newTag));
500
+ await this.waitForContinuation(newTag);
501
+ if (this.verbose)
502
+ console.log(` [imap] IDLE refreshed (${newTag})`);
503
+ // Schedule the next refresh
504
+ this.idleRefreshTimer = setTimeout(() => {
505
+ this.refreshIdle().catch(err => {
506
+ if (this.verbose)
507
+ console.error(` [imap] IDLE refresh failed: ${err.message}`);
508
+ });
509
+ }, 28 * 60 * 1000);
510
+ }
454
511
  // ── Message count (lightweight STATUS) ──
455
512
  async getMessageCount(mailbox) {
456
513
  const status = await this.getStatus(mailbox);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/iflow-direct",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Direct IMAP client — transport-agnostic, no Node.js dependencies, browser-ready",
5
5
  "main": "index.js",
6
6
  "types": "index.ts",