@bobfrankston/mailx-imap 0.1.94 → 0.1.96
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 +65 -18
- package/package.json +5 -5
package/index.js
CHANGED
|
@@ -3386,24 +3386,71 @@ export class ImapManager extends EventEmitter {
|
|
|
3386
3386
|
const raw = Buffer.from(source, "utf-8");
|
|
3387
3387
|
const bodyPath = await this.bodyStore.putMessage(accountId, folderId, uid, raw);
|
|
3388
3388
|
const parsed = await extractPreview(source);
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3389
|
+
// The body_path DB write races the main thread's
|
|
3390
|
+
// writer (two-writer WAL contention since sync
|
|
3391
|
+
// moved to its own thread). That's TRANSIENT —
|
|
3392
|
+
// the .eml is already on disk; only this UPDATE
|
|
3393
|
+
// is losing the lock. Retry with backoff instead
|
|
3394
|
+
// of treating it as a poison body. Backing a
|
|
3395
|
+
// momentary lock off for 5m→12h was exactly why
|
|
3396
|
+
// prefetched bodies "weren't happening" (Bob
|
|
3397
|
+
// 2026-06-14): the .eml sat on disk but body_path
|
|
3398
|
+
// never landed, so the row looked un-downloaded
|
|
3399
|
+
// AND was suppressed from re-fetch.
|
|
3400
|
+
let wrote = false;
|
|
3401
|
+
for (let attempt = 0; attempt < 4 && !wrote; attempt++) {
|
|
3402
|
+
try {
|
|
3403
|
+
this.db.updateBodyMeta(accountId, folderId, uid, bodyPath, parsed.hasAttachments, parsed.preview);
|
|
3404
|
+
wrote = true;
|
|
3405
|
+
}
|
|
3406
|
+
catch (we) {
|
|
3407
|
+
const locked = /database is locked|sqlite_busy|busy/i.test(String(we?.message || we));
|
|
3408
|
+
if (locked && attempt < 3) {
|
|
3409
|
+
await new Promise(r => setTimeout(r, 300 * (attempt + 1)));
|
|
3410
|
+
continue;
|
|
3411
|
+
}
|
|
3412
|
+
if (locked) {
|
|
3413
|
+
// Still locked — leave the UID
|
|
3414
|
+
// un-backed-off so the next tick heals it.
|
|
3415
|
+
console.error(` [prefetch] ${accountId}/${uid}: body_path write still locked after retries — retry next tick`);
|
|
3416
|
+
}
|
|
3417
|
+
else {
|
|
3418
|
+
// Genuine write failure (corrupt body etc.) — back off.
|
|
3419
|
+
console.error(` [prefetch] ${accountId}/${uid}: store write failed: ${we?.message || we}`);
|
|
3420
|
+
this.markPrefetchEmpty(accountId, folderId, uid);
|
|
3421
|
+
}
|
|
3422
|
+
break;
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
if (wrote) {
|
|
3426
|
+
this.emit("bodyCached", accountId, uid);
|
|
3427
|
+
this.clearPrefetchEmpty(accountId, folderId, uid); // healed — drop stale backoff
|
|
3428
|
+
counters.totalFetched++;
|
|
3429
|
+
madeProgress = true;
|
|
3430
|
+
}
|
|
3394
3431
|
}
|
|
3395
3432
|
catch (e) {
|
|
3396
|
-
|
|
3397
|
-
//
|
|
3398
|
-
//
|
|
3399
|
-
//
|
|
3400
|
-
//
|
|
3401
|
-
//
|
|
3402
|
-
//
|
|
3403
|
-
//
|
|
3404
|
-
//
|
|
3405
|
-
//
|
|
3406
|
-
//
|
|
3433
|
+
const emsg = String(e?.message || e);
|
|
3434
|
+
// "IMAP command leaked into response stream" /
|
|
3435
|
+
// "corrupt body" means the byte-buffer parser lost
|
|
3436
|
+
// sync — a partial/flaky read shifted a literal
|
|
3437
|
+
// boundary so this FETCH body captured a later
|
|
3438
|
+
// command's bytes. The MESSAGE is probably fine; the
|
|
3439
|
+
// CONNECTION is poisoned (every subsequent read on
|
|
3440
|
+
// it is now garbage). Re-throw so withConnection
|
|
3441
|
+
// discards this client and the next chunk reconnects
|
|
3442
|
+
// clean — instead of marking a good message as poison
|
|
3443
|
+
// and limping on a dead socket until the 300s timeout,
|
|
3444
|
+
// which left bobma INBOX stuck and "no new mail"
|
|
3445
|
+
// (Bob 2026-06-15). Do NOT back the UID off; it
|
|
3446
|
+
// retries next tick on a clean connection.
|
|
3447
|
+
if (/command leaked|corrupt body|desync/i.test(emsg)) {
|
|
3448
|
+
console.error(` [prefetch] ${accountId}/${uid}: connection desync — discarding socket to resync: ${emsg.slice(0, 90)}`);
|
|
3449
|
+
throw new Error(`imap-desync: ${emsg.slice(0, 80)}`);
|
|
3450
|
+
}
|
|
3451
|
+
console.error(` [prefetch] ${accountId}/${uid}: store write failed: ${emsg}`);
|
|
3452
|
+
// Genuine poison body (not a desync): back off
|
|
3453
|
+
// 5m→30m→2h→12h so it isn't re-fetched on a loop.
|
|
3407
3454
|
this.markPrefetchEmpty(accountId, folderId, uid);
|
|
3408
3455
|
}
|
|
3409
3456
|
})());
|
|
@@ -3449,9 +3496,9 @@ export class ImapManager extends EventEmitter {
|
|
|
3449
3496
|
// (markPrefetchEmpty is per-message poison only) —
|
|
3450
3497
|
// they retry next tick. Genuine per-body errors are
|
|
3451
3498
|
// handled per-UID above and never reach here.
|
|
3452
|
-
const dead = /not connected|ETIMEDOUT|ECONNRESET|ECONNREFUSED|EPIPE|socket|connection (closed|reset|lost)|disconnect/i.test(msg);
|
|
3499
|
+
const dead = /not connected|ETIMEDOUT|ECONNRESET|ECONNREFUSED|EPIPE|socket|connection (closed|reset|lost)|disconnect|imap-desync/i.test(msg);
|
|
3453
3500
|
if (dead) {
|
|
3454
|
-
console.log(` [prefetch] ${accountId}/${folder.path}: connection down — deferring rest to next tick`);
|
|
3501
|
+
console.log(` [prefetch] ${accountId}/${folder.path}: connection down/desync — deferring rest to next tick (fresh socket)`);
|
|
3455
3502
|
break;
|
|
3456
3503
|
}
|
|
3457
3504
|
if (counters.errors >= ERROR_BUDGET)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.96",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@bobfrankston/mailx-types": "^0.1.19",
|
|
13
|
-
"@bobfrankston/mailx-settings": "^0.1.
|
|
14
|
-
"@bobfrankston/mailx-store": "^0.1.
|
|
13
|
+
"@bobfrankston/mailx-settings": "^0.1.28",
|
|
14
|
+
"@bobfrankston/mailx-store": "^0.1.50",
|
|
15
15
|
"@bobfrankston/iflow-direct": "^0.1.53",
|
|
16
16
|
"@bobfrankston/tcp-transport": "^0.1.7",
|
|
17
17
|
"@bobfrankston/smtp-direct": "^0.1.9",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
".transformedSnapshot": {
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bobfrankston/mailx-types": "^0.1.19",
|
|
41
|
-
"@bobfrankston/mailx-settings": "^0.1.
|
|
42
|
-
"@bobfrankston/mailx-store": "^0.1.
|
|
41
|
+
"@bobfrankston/mailx-settings": "^0.1.28",
|
|
42
|
+
"@bobfrankston/mailx-store": "^0.1.50",
|
|
43
43
|
"@bobfrankston/iflow-direct": "^0.1.53",
|
|
44
44
|
"@bobfrankston/tcp-transport": "^0.1.7",
|
|
45
45
|
"@bobfrankston/smtp-direct": "^0.1.9",
|