@bobfrankston/rmfmail 1.1.163 → 1.1.166

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.
@@ -2446,6 +2446,26 @@ export class ImapManager extends EventEmitter {
2446
2446
  console.log(` [periodic] ${accountId}: STATUS check every ${interval / 1000}s (${isGmail ? "API" : "IMAP+IDLE"})`);
2447
2447
  }
2448
2448
 
2449
+ // DEADMAN: IDLE silently dropped on 2026-05-27 at 10:07 (empty
2450
+ // error message swallowed the cause) and 90 minutes of new mail
2451
+ // went undelivered until manual reload. Every 60s, check that an
2452
+ // IDLE watcher exists for every IMAP-path account; if not, kick
2453
+ // startWatching() to re-establish. startWatching itself is
2454
+ // idempotent (the `watchers.has(accountId) continue` guard means
2455
+ // accounts with healthy IDLE are skipped). One log line per
2456
+ // detected gap so a chronically failing account is obvious in
2457
+ // the log.
2458
+ const deadmanInterval = setInterval(() => {
2459
+ for (const [accountId] of this.configs) {
2460
+ if (this.isGmailAccount(accountId)) continue; // Gmail = API, no IDLE
2461
+ if (this.watchers.has(accountId)) continue;
2462
+ console.log(` [idle-deadman] ${accountId}: no IDLE watcher — attempting restart`);
2463
+ this.startWatching().catch(e =>
2464
+ console.error(` [idle-deadman] ${accountId} restart failed: ${e?.message || e}`));
2465
+ }
2466
+ }, 60_000);
2467
+ this.syncIntervals.set("idle-deadman", deadmanInterval);
2468
+
2449
2469
  // Sync actions (sends + flags/deletes/moves) every 30 seconds — skip during active sync
2450
2470
  const actionsInterval = setInterval(async () => {
2451
2471
  if (this.syncing) return;
@@ -2572,7 +2592,20 @@ export class ImapManager extends EventEmitter {
2572
2592
  });
2573
2593
  console.log(` [idle] Watching INBOX for ${accountId}${useNotify ? " (+NOTIFY personal mailboxes)" : ""}`);
2574
2594
  } catch (e: any) {
2575
- console.error(` [idle] Failed to watch ${accountId}: ${e.message}`);
2595
+ // Defensive: same empty-message bug class as the transport
2596
+ // path. `Failed to watch bobma: ` with nothing after the
2597
+ // colon hid 86 minutes of IDLE outage on 2026-05-27 — new
2598
+ // messages arrived in Thunderbird but mailx never noticed
2599
+ // because IDLE silently died and the log gave no clue why.
2600
+ const parts: string[] = [];
2601
+ if (e?.message) parts.push(String(e.message));
2602
+ if (e?.code) parts.push(`code=${e.code}`);
2603
+ if (e?.errno !== undefined) parts.push(`errno=${e.errno}`);
2604
+ if (e?.syscall) parts.push(`syscall=${e.syscall}`);
2605
+ const desc = parts.length > 0
2606
+ ? parts.join(" ")
2607
+ : `<no message> ${typeof e} ${e?.constructor?.name || ""}`.trim();
2608
+ console.error(` [idle] Failed to watch ${accountId}: ${desc}`);
2576
2609
  }
2577
2610
  }
2578
2611
  }
@@ -2676,7 +2709,7 @@ export class ImapManager extends EventEmitter {
2676
2709
  }
2677
2710
  const envelope: any = this.db.getMessageByUid(accountId, uid, folderId);
2678
2711
  let storedPath = envelope?.bodyPath || "";
2679
- if (!storedPath) storedPath = this.db.getMessageBodyPath(accountId, uid) || "";
2712
+ if (!storedPath) storedPath = this.db.getMessageBodyPath(accountId, uid, folderId) || "";
2680
2713
  if (storedPath && await this.bodyStore.hasByPath(storedPath)) {
2681
2714
  return this.bodyStore.readByPath(storedPath);
2682
2715
  }
@@ -3046,7 +3079,18 @@ export class ImapManager extends EventEmitter {
3046
3079
  batchSucceeded = true;
3047
3080
  this.clearFolderErrors(accountId, folder.path);
3048
3081
  } catch (e: any) {
3049
- const msg = String(e?.message || "");
3082
+ // Build a description that never collapses to "".
3083
+ // Even with the iflow transport fix below, defense
3084
+ // in depth: some error paths produce objects whose
3085
+ // .message is empty, or pass non-Error values.
3086
+ const parts: string[] = [];
3087
+ if (e?.message) parts.push(String(e.message));
3088
+ if (e?.code) parts.push(`code=${e.code}`);
3089
+ if (e?.errno !== undefined) parts.push(`errno=${e.errno}`);
3090
+ if (e?.syscall) parts.push(`syscall=${e.syscall}`);
3091
+ const msg = parts.length > 0
3092
+ ? parts.join(" ")
3093
+ : `<no message> ${typeof e} ${e?.constructor?.name || ""}`.trim();
3050
3094
  console.error(` [prefetch] ${accountId} folder ${folder.path} chunk ${chunkStart / PREFETCH_CHUNK_SIZE}: batch fetch failed: ${msg}`);
3051
3095
  counters.errors++;
3052
3096
  this.recordFolderError(accountId, folder.path);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-imap",
3
- "version": "0.1.65",
3
+ "version": "0.1.66",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bobfrankston/mailx-imap",
9
- "version": "0.1.65",
9
+ "version": "0.1.66",
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
12
  "@bobfrankston/iflow-direct": "^0.1.27",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-imap",
3
- "version": "0.1.65",
3
+ "version": "0.1.66",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -2549,7 +2549,7 @@ export class MailxService {
2549
2549
  // re-fetch here would be wasted IMAP work and risks racing.
2550
2550
  const bodyStore = this.imapManager.getBodyStore();
2551
2551
  let raw = null;
2552
- const storedPath = envelope.bodyPath || this.db.getMessageBodyPath(accountId, uid) || "";
2552
+ const storedPath = envelope.bodyPath || this.db.getMessageBodyPath(accountId, uid, folderId ?? envelope.folderId) || "";
2553
2553
  if (storedPath && await bodyStore.hasByPath(storedPath)) {
2554
2554
  try {
2555
2555
  raw = await bodyStore.readByPath(storedPath);