@bobfrankston/rmfmail 1.2.46 → 1.2.48
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/package.json +3 -3
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +61 -24
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +61 -25
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-settings/package.json +1 -1
|
@@ -1346,10 +1346,11 @@ export class ImapManager extends EventEmitter {
|
|
|
1346
1346
|
client: any,
|
|
1347
1347
|
accountId: string,
|
|
1348
1348
|
folderId: number,
|
|
1349
|
-
|
|
1349
|
+
folder: { id: number; path: string },
|
|
1350
1350
|
statusMessageCount: number | null,
|
|
1351
1351
|
excludeUids: Set<number>,
|
|
1352
|
-
): Promise<{
|
|
1352
|
+
): Promise<{ recoveredCount: number; serverUids: number[] | null; dateBounded: boolean }> {
|
|
1353
|
+
const folderPath = folder.path;
|
|
1353
1354
|
const existingUids = this.db.getUidsForFolder(accountId, folderId);
|
|
1354
1355
|
// Small folders (Drafts, Sent, …) get a FULL UID fetch so server-side
|
|
1355
1356
|
// deletions of OLDER messages are reflected; large folders (134k INBOX)
|
|
@@ -1376,27 +1377,50 @@ export class ImapManager extends EventEmitter {
|
|
|
1376
1377
|
console.log(` [sync-uids] ${accountId}/${folderPath}: ${serverUids.length} UIDs in ${Date.now() - __t0}ms`);
|
|
1377
1378
|
const existingSet = new Set(existingUids);
|
|
1378
1379
|
const missingUids = serverUids.filter((uid: number) => !existingSet.has(uid) && !excludeUids.has(uid));
|
|
1379
|
-
|
|
1380
|
-
if (missingUids.length === 0) return { recovered, serverUids, dateBounded };
|
|
1380
|
+
if (missingUids.length === 0) return { recoveredCount: 0, serverUids, dateBounded };
|
|
1381
1381
|
// Cap so a misbehaving response / brand-new account doesn't pull tens
|
|
1382
1382
|
// of thousands at once; the rest resumes next cycle. Higher UID ≈ more
|
|
1383
|
-
// recent under stable UIDVALIDITY, so prefer newest when capping
|
|
1383
|
+
// recent under stable UIDVALIDITY, so prefer newest when capping — the
|
|
1384
|
+
// user cares most about recent mail and the deficit gate keeps calling
|
|
1385
|
+
// us until the backlog is drained.
|
|
1386
|
+
//
|
|
1387
|
+
// Kept deliberately small (Bob 2026-06-20): each backfilled batch feeds
|
|
1388
|
+
// the body-prefetcher, which issues large UID FETCHes on the single
|
|
1389
|
+
// shared slow-lane connection. A 5000-message batch flooded that
|
|
1390
|
+
// connection (cmd-chain waits of 70s+, 90s FETCH timeouts) and wedged
|
|
1391
|
+
// the whole sync worker. A small batch per cycle keeps prefetch's queue
|
|
1392
|
+
// shallow so the connection stays responsive; the deficit gate drains
|
|
1393
|
+
// the backlog over more cycles instead of one connection-killing burst.
|
|
1384
1394
|
const BACKFILL_CHUNK_SIZE = 100;
|
|
1385
|
-
const RECONCILE_FETCH_CAP =
|
|
1395
|
+
const RECONCILE_FETCH_CAP = 1000;
|
|
1386
1396
|
let toFetch = missingUids;
|
|
1387
1397
|
if (missingUids.length > RECONCILE_FETCH_CAP) {
|
|
1388
|
-
console.log(` ${folderPath}: ${missingUids.length} server-only UIDs — capped at ${RECONCILE_FETCH_CAP};
|
|
1398
|
+
console.log(` ${folderPath}: ${missingUids.length} server-only UIDs — capped at ${RECONCILE_FETCH_CAP}; resumes next cycle`);
|
|
1389
1399
|
toFetch = missingUids.slice().sort((a: number, b: number) => b - a).slice(0, RECONCILE_FETCH_CAP);
|
|
1390
1400
|
} else {
|
|
1391
1401
|
console.log(` ${folderPath}: ${missingUids.length} server-only UIDs — fetching`);
|
|
1392
1402
|
}
|
|
1403
|
+
// Store each chunk as it arrives so a mid-stream connection drop (the
|
|
1404
|
+
// "Not connected" / desync class on this server) keeps the progress it
|
|
1405
|
+
// already made instead of losing the whole batch. Stop on the first
|
|
1406
|
+
// chunk error; the deficit gate calls us again next cycle to resume.
|
|
1407
|
+
let recoveredCount = 0;
|
|
1393
1408
|
for (let i = 0; i < toFetch.length; i += BACKFILL_CHUNK_SIZE) {
|
|
1394
1409
|
const chunk = toFetch.slice(i, i + BACKFILL_CHUNK_SIZE);
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1410
|
+
try {
|
|
1411
|
+
const got = await client.fetchMessages(folderPath, chunk.join(","), { source: false });
|
|
1412
|
+
recoveredCount += await this.storeMessages(accountId, folderId, folder, got, 0);
|
|
1413
|
+
} catch (e: any) {
|
|
1414
|
+
console.error(` ${folderPath}: backfill chunk ${i}-${i + chunk.length} failed (${e?.message || e}) — keeping ${recoveredCount} so far, resuming next cycle`);
|
|
1415
|
+
break;
|
|
1416
|
+
}
|
|
1417
|
+
console.log(` ${folderPath}: backfill ${Math.min(i + BACKFILL_CHUNK_SIZE, toFetch.length)}/${toFetch.length} (stored ${recoveredCount})`);
|
|
1398
1418
|
}
|
|
1399
|
-
|
|
1419
|
+
if (recoveredCount > 0) {
|
|
1420
|
+
this.db.recalcFolderCounts(folderId);
|
|
1421
|
+
this.emit("folderCountsChanged", accountId, {});
|
|
1422
|
+
}
|
|
1423
|
+
return { recoveredCount, serverUids, dateBounded };
|
|
1400
1424
|
}
|
|
1401
1425
|
|
|
1402
1426
|
async syncFolder(accountId: string, folderId: number, client?: any): Promise<number> {
|
|
@@ -1611,20 +1635,31 @@ export class ImapManager extends EventEmitter {
|
|
|
1611
1635
|
// set-diff on the first sync after boot, then at most once
|
|
1612
1636
|
// per RECONCILE_THROTTLE_MS so the fast path stays fast.
|
|
1613
1637
|
let backfilledCount = 0;
|
|
1638
|
+
// Run the set-diff when EITHER the folder has a known
|
|
1639
|
+
// deficit (local count < server count → messages are
|
|
1640
|
+
// missing, keep reconciling every cycle until caught up) OR
|
|
1641
|
+
// the throttle window elapsed (periodic safety sweep for
|
|
1642
|
+
// gaps that don't move the count, e.g. a desync-dropped
|
|
1643
|
+
// message replaced by a later arrival). On success advance
|
|
1644
|
+
// the throttle; on FAILURE leave it so the next cycle
|
|
1645
|
+
// retries instead of locking out for RECONCILE_THROTTLE_MS
|
|
1646
|
+
// (Bob 2026-06-20: boot-time "Not connected" failure locked
|
|
1647
|
+
// INBOX recovery out for 15 min).
|
|
1648
|
+
const localCount = this.db.getMessageCount(accountId, folderId);
|
|
1649
|
+
const hasDeficit = statusMessageCount !== null && localCount < statusMessageCount;
|
|
1614
1650
|
const lastRecon = this.lastReconcileMs.get(folderId) ?? 0;
|
|
1615
|
-
|
|
1616
|
-
|
|
1651
|
+
const throttleElapsed = Date.now() - lastRecon > RECONCILE_THROTTLE_MS;
|
|
1652
|
+
if (hasDeficit || throttleElapsed) {
|
|
1617
1653
|
try {
|
|
1618
1654
|
const sd = await this.fetchServerOnlyUids(
|
|
1619
|
-
client, accountId, folderId, folder
|
|
1655
|
+
client, accountId, folderId, folder, statusMessageCount,
|
|
1620
1656
|
new Set(newOnes.map((m: any) => m.uid)),
|
|
1621
1657
|
);
|
|
1622
|
-
|
|
1623
|
-
|
|
1658
|
+
backfilledCount = sd.recoveredCount;
|
|
1659
|
+
if (backfilledCount > 0) {
|
|
1624
1660
|
console.log(` [qresync] ${accountId}/${folder.path}: set-diff backfilled ${backfilledCount} server-only UID(s)`);
|
|
1625
|
-
this.db.recalcFolderCounts(folderId);
|
|
1626
|
-
this.emit("folderCountsChanged", accountId, {});
|
|
1627
1661
|
}
|
|
1662
|
+
this.lastReconcileMs.set(folderId, Date.now());
|
|
1628
1663
|
} catch (e: any) {
|
|
1629
1664
|
console.error(` [qresync] ${accountId}/${folder.path}: set-diff backfill failed: ${e?.message || e}`);
|
|
1630
1665
|
}
|
|
@@ -1704,23 +1739,24 @@ export class ImapManager extends EventEmitter {
|
|
|
1704
1739
|
// historical messages in one cycle. Hit the cap and the next
|
|
1705
1740
|
// sync picks up the rest.
|
|
1706
1741
|
try {
|
|
1707
|
-
// Shared with the QRESYNC fast path (fetchServerOnlyUids).
|
|
1708
|
-
//
|
|
1709
|
-
//
|
|
1710
|
-
//
|
|
1742
|
+
// Shared with the QRESYNC fast path (fetchServerOnlyUids). It
|
|
1743
|
+
// stores recovered messages itself (chunk-at-a-time, so a
|
|
1744
|
+
// dropped connection keeps partial progress). Exclude the UIDs
|
|
1745
|
+
// we just fetched via fetchMessagesSinceUid — they're in
|
|
1746
|
+
// `messages`, about to be stored by the batch loop below, so
|
|
1747
|
+
// they'd otherwise look "missing" and get re-fetched.
|
|
1711
1748
|
const sd = await this.fetchServerOnlyUids(
|
|
1712
|
-
client, accountId, folderId, folder
|
|
1749
|
+
client, accountId, folderId, folder, statusMessageCount,
|
|
1713
1750
|
new Set(messages.map(m => m.uid)),
|
|
1714
1751
|
);
|
|
1715
|
-
messages.push(...sd.recovered);
|
|
1716
1752
|
// Stash the server UID list for the deletion-reconciliation
|
|
1717
1753
|
// block below — no point hitting the server a second time.
|
|
1718
1754
|
serverUidsCached = sd.serverUids;
|
|
1719
1755
|
serverUidsAreDateBounded = sd.dateBounded;
|
|
1756
|
+
this.lastReconcileMs.set(folderId, Date.now());
|
|
1720
1757
|
} catch (e: any) {
|
|
1721
1758
|
console.error(` ${folder.path}: reconciliation failed: ${e.message}`);
|
|
1722
1759
|
}
|
|
1723
|
-
this.lastReconcileMs.set(folderId, Date.now());
|
|
1724
1760
|
|
|
1725
1761
|
// Date-based backfill via SEARCH SINCE was here. Removed —
|
|
1726
1762
|
// set-diff above already covers the same case (any server UID
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.104",
|
|
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.104",
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@bobfrankston/iflow-direct": "^0.1.27",
|