@bobfrankston/mailx-imap 0.1.64 → 0.1.66
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/index.js +66 -4
- package/package.json +9 -9
package/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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);
|
|
@@ -4236,7 +4288,17 @@ export class ImapManager extends EventEmitter {
|
|
|
4236
4288
|
}, { slow: true });
|
|
4237
4289
|
}
|
|
4238
4290
|
catch { /* best-effort */ }
|
|
4239
|
-
|
|
4291
|
+
// Suppress the banner for transient network errors. ETIMEDOUT
|
|
4292
|
+
// on a Dovecot send is the server being slow / a TCP idle
|
|
4293
|
+
// drop, NOT a user-actionable failure — the next outbox tick
|
|
4294
|
+
// retries with exponential backoff. Surfacing it as a sticky
|
|
4295
|
+
// "Send failed: ETIMEDOUT" banner trains the user to ignore
|
|
4296
|
+
// banners (Bob 2026-05-26 "timeout??"). handleSyncError uses
|
|
4297
|
+
// the same classifier; keep the two paths in sync.
|
|
4298
|
+
const isTransient = /timeout|ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENETUNREACH|Too many|socket hang up|EPIPE|write after end/i.test(errMsg);
|
|
4299
|
+
if (!isTransient) {
|
|
4300
|
+
this.emit("accountError", accountId, `Send failed: ${errMsg}`, "Message kept in Outbox", false);
|
|
4301
|
+
}
|
|
4240
4302
|
if (/auth|login|credential|invalid/i.test(errMsg)) {
|
|
4241
4303
|
this.outboxBackoff.set(accountId, Date.now() + 3600000); // 1 hour
|
|
4242
4304
|
console.error(` [outbox] Auth failure for ${accountId} — outbox paused for 1 hour`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.66",
|
|
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.
|
|
14
|
-
"@bobfrankston/mailx-store": "^0.1.
|
|
15
|
-
"@bobfrankston/iflow-direct": "^0.1.
|
|
13
|
+
"@bobfrankston/mailx-settings": "^0.1.25",
|
|
14
|
+
"@bobfrankston/mailx-store": "^0.1.38",
|
|
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.
|
|
19
|
+
"@bobfrankston/oauthsupport": "^1.0.28"
|
|
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.
|
|
42
|
-
"@bobfrankston/mailx-store": "^0.1.
|
|
43
|
-
"@bobfrankston/iflow-direct": "^0.1.
|
|
41
|
+
"@bobfrankston/mailx-settings": "^0.1.25",
|
|
42
|
+
"@bobfrankston/mailx-store": "^0.1.38",
|
|
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.
|
|
47
|
+
"@bobfrankston/oauthsupport": "^1.0.28"
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
}
|