@bobfrankston/mailx-imap 0.1.65 → 0.1.67

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 (2) hide show
  1. package/index.js +60 -8
  2. package/package.json +9 -9
package/index.js CHANGED
@@ -1238,7 +1238,7 @@ export class ImapManager extends EventEmitter {
1238
1238
  for (const uid of qr.vanishedUids) {
1239
1239
  const env = this.db.getMessageByUid(accountId, uid, folderId);
1240
1240
  if (env) {
1241
- this.db.deleteMessage(accountId, uid, "server VANISHED via QRESYNC", "mailx-imap syncFolder/qresync");
1241
+ this.db.deleteMessage(accountId, folderId, uid, "server VANISHED via QRESYNC", "mailx-imap syncFolder/qresync");
1242
1242
  vanishedApplied++;
1243
1243
  }
1244
1244
  }
@@ -2078,7 +2078,7 @@ export class ImapManager extends EventEmitter {
2078
2078
  const tag = env ? `msgid=${env.messageId || "?"} subj="${(env.subject || "").slice(0, 60)}"` : "unknown";
2079
2079
  console.log(` [reconcile-delete] ${accountId}/${folder.path} uid=${uid} ${tag}`);
2080
2080
  this.unlinkBodyFile(accountId, uid, folder.id).catch(() => { });
2081
- this.db.deleteMessage(accountId, uid, "Gmail-API reconcile: server list missing this UID", `mailx-imap Gmail reconcile (${folder.path})`);
2081
+ this.db.deleteMessage(accountId, folder.id, uid, "Gmail-API reconcile: server list missing this UID", `mailx-imap Gmail reconcile (${folder.path})`);
2082
2082
  }
2083
2083
  if (toDelete.length > 0)
2084
2084
  console.log(` [api] ${accountId}/${folder.path}: ${toDelete.length} deleted`);
@@ -2410,6 +2410,26 @@ export class ImapManager extends EventEmitter {
2410
2410
  this.syncIntervals.set(`quick:${accountId}`, timer);
2411
2411
  console.log(` [periodic] ${accountId}: STATUS check every ${interval / 1000}s (${isGmail ? "API" : "IMAP+IDLE"})`);
2412
2412
  }
2413
+ // DEADMAN: IDLE silently dropped on 2026-05-27 at 10:07 (empty
2414
+ // error message swallowed the cause) and 90 minutes of new mail
2415
+ // went undelivered until manual reload. Every 60s, check that an
2416
+ // IDLE watcher exists for every IMAP-path account; if not, kick
2417
+ // startWatching() to re-establish. startWatching itself is
2418
+ // idempotent (the `watchers.has(accountId) continue` guard means
2419
+ // accounts with healthy IDLE are skipped). One log line per
2420
+ // detected gap so a chronically failing account is obvious in
2421
+ // the log.
2422
+ const deadmanInterval = setInterval(() => {
2423
+ for (const [accountId] of this.configs) {
2424
+ if (this.isGmailAccount(accountId))
2425
+ continue; // Gmail = API, no IDLE
2426
+ if (this.watchers.has(accountId))
2427
+ continue;
2428
+ console.log(` [idle-deadman] ${accountId}: no IDLE watcher — attempting restart`);
2429
+ this.startWatching().catch(e => console.error(` [idle-deadman] ${accountId} restart failed: ${e?.message || e}`));
2430
+ }
2431
+ }, 60_000);
2432
+ this.syncIntervals.set("idle-deadman", deadmanInterval);
2413
2433
  // Sync actions (sends + flags/deletes/moves) every 30 seconds — skip during active sync
2414
2434
  const actionsInterval = setInterval(async () => {
2415
2435
  if (this.syncing)
@@ -2528,7 +2548,24 @@ export class ImapManager extends EventEmitter {
2528
2548
  console.log(` [idle] Watching INBOX for ${accountId}${useNotify ? " (+NOTIFY personal mailboxes)" : ""}`);
2529
2549
  }
2530
2550
  catch (e) {
2531
- console.error(` [idle] Failed to watch ${accountId}: ${e.message}`);
2551
+ // Defensive: same empty-message bug class as the transport
2552
+ // path. `Failed to watch bobma: ` with nothing after the
2553
+ // colon hid 86 minutes of IDLE outage on 2026-05-27 — new
2554
+ // messages arrived in Thunderbird but mailx never noticed
2555
+ // because IDLE silently died and the log gave no clue why.
2556
+ const parts = [];
2557
+ if (e?.message)
2558
+ parts.push(String(e.message));
2559
+ if (e?.code)
2560
+ parts.push(`code=${e.code}`);
2561
+ if (e?.errno !== undefined)
2562
+ parts.push(`errno=${e.errno}`);
2563
+ if (e?.syscall)
2564
+ parts.push(`syscall=${e.syscall}`);
2565
+ const desc = parts.length > 0
2566
+ ? parts.join(" ")
2567
+ : `<no message> ${typeof e} ${e?.constructor?.name || ""}`.trim();
2568
+ console.error(` [idle] Failed to watch ${accountId}: ${desc}`);
2532
2569
  }
2533
2570
  }
2534
2571
  }
@@ -2595,7 +2632,7 @@ export class ImapManager extends EventEmitter {
2595
2632
  const tag = env.messageId ? `msgid=${env.messageId} subj="${(env.subject || "").slice(0, 60)}"` : "no-msgid";
2596
2633
  console.log(` [reconcile-delete] ${accountId}/${folderPath} uid=${uid} ${tag} (legacy path)`);
2597
2634
  this.unlinkBodyFile(accountId, uid, folderId).catch(() => { });
2598
- this.db.deleteMessage(accountId, uid, "reconcile: server missing this UID after grace (legacy)", `mailx-imap deferred reconcile (${folderPath})`);
2635
+ this.db.deleteMessage(accountId, folderId, uid, "reconcile: server missing this UID after grace (legacy)", `mailx-imap deferred reconcile (${folderPath})`);
2599
2636
  }
2600
2637
  this.db.recalcFolderCounts(folderId);
2601
2638
  this.emit("folderCountsChanged", accountId, {});
@@ -2634,7 +2671,7 @@ export class ImapManager extends EventEmitter {
2634
2671
  const envelope = this.db.getMessageByUid(accountId, uid, folderId);
2635
2672
  let storedPath = envelope?.bodyPath || "";
2636
2673
  if (!storedPath)
2637
- storedPath = this.db.getMessageBodyPath(accountId, uid) || "";
2674
+ storedPath = this.db.getMessageBodyPath(accountId, uid, folderId) || "";
2638
2675
  if (storedPath && await this.bodyStore.hasByPath(storedPath)) {
2639
2676
  return this.bodyStore.readByPath(storedPath);
2640
2677
  }
@@ -3030,7 +3067,22 @@ export class ImapManager extends EventEmitter {
3030
3067
  this.clearFolderErrors(accountId, folder.path);
3031
3068
  }
3032
3069
  catch (e) {
3033
- const msg = String(e?.message || "");
3070
+ // Build a description that never collapses to "".
3071
+ // Even with the iflow transport fix below, defense
3072
+ // in depth: some error paths produce objects whose
3073
+ // .message is empty, or pass non-Error values.
3074
+ const parts = [];
3075
+ if (e?.message)
3076
+ parts.push(String(e.message));
3077
+ if (e?.code)
3078
+ parts.push(`code=${e.code}`);
3079
+ if (e?.errno !== undefined)
3080
+ parts.push(`errno=${e.errno}`);
3081
+ if (e?.syscall)
3082
+ parts.push(`syscall=${e.syscall}`);
3083
+ const msg = parts.length > 0
3084
+ ? parts.join(" ")
3085
+ : `<no message> ${typeof e} ${e?.constructor?.name || ""}`.trim();
3034
3086
  console.error(` [prefetch] ${accountId} folder ${folder.path} chunk ${chunkStart / PREFETCH_CHUNK_SIZE}: batch fetch failed: ${msg}`);
3035
3087
  counters.errors++;
3036
3088
  this.recordFolderError(accountId, folder.path);
@@ -3162,7 +3214,7 @@ export class ImapManager extends EventEmitter {
3162
3214
  if (!msg)
3163
3215
  throw new Error(`Message UID ${uid} not found in ${fromFolder.path}`);
3164
3216
  await sourceClient.moveMessageToServer(msg, fromFolder.path, targetClient, toFolder.path);
3165
- this.db.deleteMessage(fromAccountId, uid, `cross-account move to ${toAccountId}/${toFolder.path}`, "mailx-imap moveBetweenAccounts");
3217
+ this.db.deleteMessage(fromAccountId, fromFolder.id, uid, `cross-account move to ${toAccountId}/${toFolder.path}`, "mailx-imap moveBetweenAccounts");
3166
3218
  console.log(` Cross-account move: ${fromAccountId}/${fromFolder.path} UID ${uid} → ${toAccountId}/${toFolder.path}`);
3167
3219
  });
3168
3220
  });
@@ -3509,7 +3561,7 @@ export class ImapManager extends EventEmitter {
3509
3561
  const existing = this.db.getMessageByUid(accountId, draftUid, drafts.id);
3510
3562
  if (existing) {
3511
3563
  this.unlinkBodyFile(accountId, draftUid, drafts.id).catch(() => { });
3512
- this.db.deleteMessage(accountId, draftUid, "user sent the message (draft cleanup)", "mailx-imap deleteDraft (local)");
3564
+ this.db.deleteMessage(accountId, drafts.id, draftUid, "user sent the message (draft cleanup)", "mailx-imap deleteDraft (local)");
3513
3565
  localDeletedUid = draftUid;
3514
3566
  }
3515
3567
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-imap",
3
- "version": "0.1.65",
3
+ "version": "0.1.67",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -10,13 +10,13 @@
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
12
  "@bobfrankston/mailx-types": "^0.1.18",
13
- "@bobfrankston/mailx-settings": "^0.1.24",
14
- "@bobfrankston/mailx-store": "^0.1.38",
15
- "@bobfrankston/iflow-direct": "^0.1.50",
13
+ "@bobfrankston/mailx-settings": "^0.1.25",
14
+ "@bobfrankston/mailx-store": "^0.1.39",
15
+ "@bobfrankston/iflow-direct": "^0.1.51",
16
16
  "@bobfrankston/tcp-transport": "^0.1.6",
17
17
  "@bobfrankston/smtp-direct": "^0.1.8",
18
18
  "@bobfrankston/mailx-sync": "^0.1.19",
19
- "@bobfrankston/oauthsupport": "^1.0.27"
19
+ "@bobfrankston/oauthsupport": "^1.0.29"
20
20
  },
21
21
  "repository": {
22
22
  "type": "git",
@@ -38,13 +38,13 @@
38
38
  ".transformedSnapshot": {
39
39
  "dependencies": {
40
40
  "@bobfrankston/mailx-types": "^0.1.18",
41
- "@bobfrankston/mailx-settings": "^0.1.24",
42
- "@bobfrankston/mailx-store": "^0.1.38",
43
- "@bobfrankston/iflow-direct": "^0.1.50",
41
+ "@bobfrankston/mailx-settings": "^0.1.25",
42
+ "@bobfrankston/mailx-store": "^0.1.39",
43
+ "@bobfrankston/iflow-direct": "^0.1.51",
44
44
  "@bobfrankston/tcp-transport": "^0.1.6",
45
45
  "@bobfrankston/smtp-direct": "^0.1.8",
46
46
  "@bobfrankston/mailx-sync": "^0.1.19",
47
- "@bobfrankston/oauthsupport": "^1.0.27"
47
+ "@bobfrankston/oauthsupport": "^1.0.29"
48
48
  }
49
49
  }
50
50
  }