@bobfrankston/mailx-imap 0.1.70 → 0.1.72

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.
Files changed (3) hide show
  1. package/index.d.ts +1 -0
  2. package/index.js +50 -3
  3. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -59,6 +59,7 @@ export interface OutboxStatus {
59
59
  export declare class ImapManager extends EventEmitter {
60
60
  private configs;
61
61
  private watchers;
62
+ private watcherClients;
62
63
  private fetchClients;
63
64
  /** The Store is the architectural nexus — owner of MailxDB +
64
65
  * FileMessageStore + the event bus. This package (mailx-imap) is a
package/index.js CHANGED
@@ -158,6 +158,14 @@ async function withTimeout(promise, ms, client, label) {
158
158
  export class ImapManager extends EventEmitter {
159
159
  configs = new Map();
160
160
  watchers = new Map();
161
+ // Parallel map of the IDLE watch client per account, so runFullSync /
162
+ // startWatching can health-check the live socket and refresh ONLY dead
163
+ // watchers instead of tearing every watcher down each cycle. The blanket
164
+ // teardown was the IDLE-churn driver: it dropped healthy IDLE every 5 min,
165
+ // and re-establishing competed for the saturated 4-permit host semaphore
166
+ // (folder syncs holding all permits, timing out at 360s), leaving new mail
167
+ // on 5-min polling for minutes at a time (Bob 2026-05-28).
168
+ watcherClients = new Map();
161
169
  fetchClients = new Map();
162
170
  /** The Store is the architectural nexus — owner of MailxDB +
163
171
  * FileMessageStore + the event bus. This package (mailx-imap) is a
@@ -294,6 +302,7 @@ export class ImapManager extends EventEmitter {
294
302
  }
295
303
  catch { /* */ }
296
304
  this.watchers.delete(accountId);
305
+ this.watcherClients.delete(accountId);
297
306
  }
298
307
  // Delete only the IMAP token (not contacts — separate scope, separate consent)
299
308
  const accountDir = tokenDirName(account.imap.user);
@@ -2473,7 +2482,12 @@ export class ImapManager extends EventEmitter {
2473
2482
  async runFullSync() {
2474
2483
  console.log(` [periodic] Full sync at ${new Date().toLocaleTimeString()}`);
2475
2484
  await this.syncAll();
2476
- await this.stopWatching();
2485
+ // Do NOT tear down healthy IDLE here. startWatching is idempotent and
2486
+ // now health-checks each existing watcher: alive ones are left intact,
2487
+ // dead/missing ones are re-established. The old unconditional
2488
+ // stopWatching()+startWatching() dropped healthy IDLE every cycle and
2489
+ // the re-establish stalled behind the saturated host semaphore — the
2490
+ // "still slow polling / not subscribing" gaps (Bob 2026-05-28).
2477
2491
  await this.startWatching();
2478
2492
  }
2479
2493
  /** Stop periodic sync */
@@ -2494,8 +2508,28 @@ export class ImapManager extends EventEmitter {
2494
2508
  * right way to make that cheap, not piling per-folder IDLE sockets). */
2495
2509
  async startWatching() {
2496
2510
  for (const [accountId] of this.configs) {
2497
- if (this.watchers.has(accountId))
2498
- continue;
2511
+ if (this.watchers.has(accountId)) {
2512
+ // Already watching — but is the socket still alive? A silently
2513
+ // dropped IDLE leaves the watcher entry in place, so without
2514
+ // this health-check the deadman (which only checks for MISSING
2515
+ // entries) would never refresh it. If dead, tear down just THIS
2516
+ // one and fall through to re-establish; if alive, leave it.
2517
+ const wc = this.watcherClients.get(accountId);
2518
+ const sock = wc?.native?.transport?.socket;
2519
+ const dead = !wc || sock?.destroyed || sock?.readyState === "closed" || wc?._dead;
2520
+ if (!dead)
2521
+ continue;
2522
+ console.log(` [idle] ${accountId}: watcher socket dead — refreshing`);
2523
+ const stop = this.watchers.get(accountId);
2524
+ if (stop) {
2525
+ try {
2526
+ await stop();
2527
+ }
2528
+ catch { /* */ }
2529
+ }
2530
+ this.watchers.delete(accountId);
2531
+ this.watcherClients.delete(accountId);
2532
+ }
2499
2533
  try {
2500
2534
  // IDLE keeps its own dedicated socket — once the connection
2501
2535
  // is parked in IDLE, it's unusable for any other command, so
@@ -2557,6 +2591,7 @@ export class ImapManager extends EventEmitter {
2557
2591
  await stop();
2558
2592
  await watchClient.logout();
2559
2593
  });
2594
+ this.watcherClients.set(accountId, watchClient);
2560
2595
  console.log(` [idle] Watching INBOX for ${accountId}${useNotify ? " (+NOTIFY personal mailboxes)" : ""}`);
2561
2596
  }
2562
2597
  catch (e) {
@@ -2590,6 +2625,7 @@ export class ImapManager extends EventEmitter {
2590
2625
  catch { /* ignore */ }
2591
2626
  }
2592
2627
  this.watchers.clear();
2628
+ this.watcherClients.clear();
2593
2629
  }
2594
2630
  /** Unlink the on-disk body file for a message by reading its `body_path`
2595
2631
  * from the DB. Safe to call either before or after `db.deleteMessage`
@@ -3064,6 +3100,17 @@ export class ImapManager extends EventEmitter {
3064
3100
  }
3065
3101
  catch (e) {
3066
3102
  console.error(` [prefetch] ${accountId}/${uid}: store write failed: ${e.message}`);
3103
+ // Back off this UID. A corrupt body (the
3104
+ // putMessage guard rejecting an IMAP command
3105
+ // leaked into the FETCH response — iflow
3106
+ // protocol desync) was NOT marked, so the
3107
+ // same poisoned UID got re-fetched every
3108
+ // prefetch cycle forever, spamming the log
3109
+ // and wasting fetch turns (Bob 2026-05-28
3110
+ // "why is prefetching still broken" — UID
3111
+ // 59686 failing on a loop). markPrefetchEmpty
3112
+ // applies the 5m→30m→2h→12h backoff.
3113
+ this.markPrefetchEmpty(accountId, folderId, uid);
3067
3114
  }
3068
3115
  })());
3069
3116
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-imap",
3
- "version": "0.1.70",
3
+ "version": "0.1.72",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",