@bobfrankston/rmfmail 1.1.187 → 1.1.189
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/client/app.bundle.js +4 -4
- package/client/app.bundle.js.map +2 -2
- package/client/app.js +10 -5
- package/client/app.js.map +1 -1
- package/client/app.ts +10 -5
- package/client/lib/mailxapi.js +18 -0
- package/package.json +3 -3
- package/packages/mailx-imap/index.d.ts +1 -0
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +50 -3
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +44 -3
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-38560 → node_modules.npmglobalize-stash-4032}/.package-lock.json +0 -0
|
@@ -223,6 +223,14 @@ async function withTimeout<T>(promise: Promise<T>, ms: number, client: any, labe
|
|
|
223
223
|
export class ImapManager extends EventEmitter {
|
|
224
224
|
private configs: Map<string, ReturnType<typeof createAutoImapConfig>> = new Map();
|
|
225
225
|
private watchers: Map<string, () => void> = new Map();
|
|
226
|
+
// Parallel map of the IDLE watch client per account, so runFullSync /
|
|
227
|
+
// startWatching can health-check the live socket and refresh ONLY dead
|
|
228
|
+
// watchers instead of tearing every watcher down each cycle. The blanket
|
|
229
|
+
// teardown was the IDLE-churn driver: it dropped healthy IDLE every 5 min,
|
|
230
|
+
// and re-establishing competed for the saturated 4-permit host semaphore
|
|
231
|
+
// (folder syncs holding all permits, timing out at 360s), leaving new mail
|
|
232
|
+
// on 5-min polling for minutes at a time (Bob 2026-05-28).
|
|
233
|
+
private watcherClients: Map<string, any> = new Map();
|
|
226
234
|
private fetchClients: Map<string, any> = new Map();
|
|
227
235
|
/** The Store is the architectural nexus — owner of MailxDB +
|
|
228
236
|
* FileMessageStore + the event bus. This package (mailx-imap) is a
|
|
@@ -357,7 +365,7 @@ export class ImapManager extends EventEmitter {
|
|
|
357
365
|
|
|
358
366
|
// Stop IDLE watcher for this account
|
|
359
367
|
const stopWatcher = this.watchers.get(accountId);
|
|
360
|
-
if (stopWatcher) { try { await stopWatcher(); } catch { /* */ } this.watchers.delete(accountId); }
|
|
368
|
+
if (stopWatcher) { try { await stopWatcher(); } catch { /* */ } this.watchers.delete(accountId); this.watcherClients.delete(accountId); }
|
|
361
369
|
|
|
362
370
|
// Delete only the IMAP token (not contacts — separate scope, separate consent)
|
|
363
371
|
const accountDir = tokenDirName(account.imap.user);
|
|
@@ -2513,7 +2521,12 @@ export class ImapManager extends EventEmitter {
|
|
|
2513
2521
|
async runFullSync(): Promise<void> {
|
|
2514
2522
|
console.log(` [periodic] Full sync at ${new Date().toLocaleTimeString()}`);
|
|
2515
2523
|
await this.syncAll();
|
|
2516
|
-
|
|
2524
|
+
// Do NOT tear down healthy IDLE here. startWatching is idempotent and
|
|
2525
|
+
// now health-checks each existing watcher: alive ones are left intact,
|
|
2526
|
+
// dead/missing ones are re-established. The old unconditional
|
|
2527
|
+
// stopWatching()+startWatching() dropped healthy IDLE every cycle and
|
|
2528
|
+
// the re-establish stalled behind the saturated host semaphore — the
|
|
2529
|
+
// "still slow polling / not subscribing" gaps (Bob 2026-05-28).
|
|
2517
2530
|
await this.startWatching();
|
|
2518
2531
|
}
|
|
2519
2532
|
|
|
@@ -2537,7 +2550,22 @@ export class ImapManager extends EventEmitter {
|
|
|
2537
2550
|
* right way to make that cheap, not piling per-folder IDLE sockets). */
|
|
2538
2551
|
async startWatching(): Promise<void> {
|
|
2539
2552
|
for (const [accountId] of this.configs) {
|
|
2540
|
-
if (this.watchers.has(accountId))
|
|
2553
|
+
if (this.watchers.has(accountId)) {
|
|
2554
|
+
// Already watching — but is the socket still alive? A silently
|
|
2555
|
+
// dropped IDLE leaves the watcher entry in place, so without
|
|
2556
|
+
// this health-check the deadman (which only checks for MISSING
|
|
2557
|
+
// entries) would never refresh it. If dead, tear down just THIS
|
|
2558
|
+
// one and fall through to re-establish; if alive, leave it.
|
|
2559
|
+
const wc = this.watcherClients.get(accountId);
|
|
2560
|
+
const sock = wc?.native?.transport?.socket;
|
|
2561
|
+
const dead = !wc || sock?.destroyed || sock?.readyState === "closed" || wc?._dead;
|
|
2562
|
+
if (!dead) continue;
|
|
2563
|
+
console.log(` [idle] ${accountId}: watcher socket dead — refreshing`);
|
|
2564
|
+
const stop = this.watchers.get(accountId);
|
|
2565
|
+
if (stop) { try { await stop(); } catch { /* */ } }
|
|
2566
|
+
this.watchers.delete(accountId);
|
|
2567
|
+
this.watcherClients.delete(accountId);
|
|
2568
|
+
}
|
|
2541
2569
|
try {
|
|
2542
2570
|
// IDLE keeps its own dedicated socket — once the connection
|
|
2543
2571
|
// is parked in IDLE, it's unusable for any other command, so
|
|
@@ -2602,6 +2630,7 @@ export class ImapManager extends EventEmitter {
|
|
|
2602
2630
|
await stop();
|
|
2603
2631
|
await watchClient.logout();
|
|
2604
2632
|
});
|
|
2633
|
+
this.watcherClients.set(accountId, watchClient);
|
|
2605
2634
|
console.log(` [idle] Watching INBOX for ${accountId}${useNotify ? " (+NOTIFY personal mailboxes)" : ""}`);
|
|
2606
2635
|
} catch (e: any) {
|
|
2607
2636
|
// Defensive: same empty-message bug class as the transport
|
|
@@ -2628,6 +2657,7 @@ export class ImapManager extends EventEmitter {
|
|
|
2628
2657
|
try { await stop(); } catch { /* ignore */ }
|
|
2629
2658
|
}
|
|
2630
2659
|
this.watchers.clear();
|
|
2660
|
+
this.watcherClients.clear();
|
|
2631
2661
|
}
|
|
2632
2662
|
|
|
2633
2663
|
/** Unlink the on-disk body file for a message by reading its `body_path`
|
|
@@ -3077,6 +3107,17 @@ export class ImapManager extends EventEmitter {
|
|
|
3077
3107
|
madeProgress = true;
|
|
3078
3108
|
} catch (e: any) {
|
|
3079
3109
|
console.error(` [prefetch] ${accountId}/${uid}: store write failed: ${e.message}`);
|
|
3110
|
+
// Back off this UID. A corrupt body (the
|
|
3111
|
+
// putMessage guard rejecting an IMAP command
|
|
3112
|
+
// leaked into the FETCH response — iflow
|
|
3113
|
+
// protocol desync) was NOT marked, so the
|
|
3114
|
+
// same poisoned UID got re-fetched every
|
|
3115
|
+
// prefetch cycle forever, spamming the log
|
|
3116
|
+
// and wasting fetch turns (Bob 2026-05-28
|
|
3117
|
+
// "why is prefetching still broken" — UID
|
|
3118
|
+
// 59686 failing on a loop). markPrefetchEmpty
|
|
3119
|
+
// applies the 5m→30m→2h→12h backoff.
|
|
3120
|
+
this.markPrefetchEmpty(accountId, folderId, uid);
|
|
3080
3121
|
}
|
|
3081
3122
|
})());
|
|
3082
3123
|
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.72",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bobfrankston/mailx-imap",
|
|
9
|
-
"version": "0.1.
|
|
9
|
+
"version": "0.1.72",
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@bobfrankston/iflow-direct": "^0.1.27",
|