@bobfrankston/mailx-imap 0.1.69 → 0.1.71
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.d.ts +1 -0
- package/index.js +51 -3
- package/package.json +3 -3
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);
|
|
@@ -1049,6 +1058,18 @@ export class ImapManager extends EventEmitter {
|
|
|
1049
1058
|
*
|
|
1050
1059
|
* Fires the same emits as a normal sync so the UI updates. */
|
|
1051
1060
|
async insertLocalRowFromSource(accountId, folder, uid, source, flags) {
|
|
1061
|
+
// Guard against uid <= 0. Valid IMAP UIDs are always ≥ 1; a 0 here
|
|
1062
|
+
// means the APPENDUID capture failed (server returned no UIDPLUS, or
|
|
1063
|
+
// appendMessage resolved to 0 instead of null) and the caller didn't
|
|
1064
|
+
// catch it. Inserting uid=0 created a phantom INBOX row with no
|
|
1065
|
+
// message_id, no body, no subject — the "blank line in the summary"
|
|
1066
|
+
// (Bob 2026-05-28). Refuse it; the caller's syncFolder fallback will
|
|
1067
|
+
// pick the message up with its real UID on the next sync.
|
|
1068
|
+
if (!uid || uid <= 0) {
|
|
1069
|
+
console.error(` [insert] refusing uid=${uid} for ${accountId}/${folder.path} — invalid IMAP UID (APPENDUID likely failed); deferring to sync`);
|
|
1070
|
+
this.syncFolder(accountId, folder.id).catch(() => { });
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1052
1073
|
// insertLocalRowFromSource runs right after sendMessage — that's a
|
|
1053
1074
|
// user-initiated path but the parse cost is on the post-send
|
|
1054
1075
|
// background work, not the click-through. Tag as background so a
|
|
@@ -2461,7 +2482,12 @@ export class ImapManager extends EventEmitter {
|
|
|
2461
2482
|
async runFullSync() {
|
|
2462
2483
|
console.log(` [periodic] Full sync at ${new Date().toLocaleTimeString()}`);
|
|
2463
2484
|
await this.syncAll();
|
|
2464
|
-
|
|
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).
|
|
2465
2491
|
await this.startWatching();
|
|
2466
2492
|
}
|
|
2467
2493
|
/** Stop periodic sync */
|
|
@@ -2482,8 +2508,28 @@ export class ImapManager extends EventEmitter {
|
|
|
2482
2508
|
* right way to make that cheap, not piling per-folder IDLE sockets). */
|
|
2483
2509
|
async startWatching() {
|
|
2484
2510
|
for (const [accountId] of this.configs) {
|
|
2485
|
-
if (this.watchers.has(accountId))
|
|
2486
|
-
|
|
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
|
+
}
|
|
2487
2533
|
try {
|
|
2488
2534
|
// IDLE keeps its own dedicated socket — once the connection
|
|
2489
2535
|
// is parked in IDLE, it's unusable for any other command, so
|
|
@@ -2545,6 +2591,7 @@ export class ImapManager extends EventEmitter {
|
|
|
2545
2591
|
await stop();
|
|
2546
2592
|
await watchClient.logout();
|
|
2547
2593
|
});
|
|
2594
|
+
this.watcherClients.set(accountId, watchClient);
|
|
2548
2595
|
console.log(` [idle] Watching INBOX for ${accountId}${useNotify ? " (+NOTIFY personal mailboxes)" : ""}`);
|
|
2549
2596
|
}
|
|
2550
2597
|
catch (e) {
|
|
@@ -2578,6 +2625,7 @@ export class ImapManager extends EventEmitter {
|
|
|
2578
2625
|
catch { /* ignore */ }
|
|
2579
2626
|
}
|
|
2580
2627
|
this.watchers.clear();
|
|
2628
|
+
this.watcherClients.clear();
|
|
2581
2629
|
}
|
|
2582
2630
|
/** Unlink the on-disk body file for a message by reading its `body_path`
|
|
2583
2631
|
* from the DB. Safe to call either before or after `db.deleteMessage`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.71",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@bobfrankston/mailx-types": "^0.1.18",
|
|
13
13
|
"@bobfrankston/mailx-settings": "^0.1.25",
|
|
14
|
-
"@bobfrankston/mailx-store": "^0.1.
|
|
14
|
+
"@bobfrankston/mailx-store": "^0.1.41",
|
|
15
15
|
"@bobfrankston/iflow-direct": "^0.1.51",
|
|
16
16
|
"@bobfrankston/tcp-transport": "^0.1.6",
|
|
17
17
|
"@bobfrankston/smtp-direct": "^0.1.8",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bobfrankston/mailx-types": "^0.1.18",
|
|
41
41
|
"@bobfrankston/mailx-settings": "^0.1.25",
|
|
42
|
-
"@bobfrankston/mailx-store": "^0.1.
|
|
42
|
+
"@bobfrankston/mailx-store": "^0.1.41",
|
|
43
43
|
"@bobfrankston/iflow-direct": "^0.1.51",
|
|
44
44
|
"@bobfrankston/tcp-transport": "^0.1.6",
|
|
45
45
|
"@bobfrankston/smtp-direct": "^0.1.8",
|