@bobfrankston/mailx-imap 0.1.99 → 0.1.100
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 +13 -0
- package/index.js +41 -17
- package/package.json +3 -3
package/index.d.ts
CHANGED
|
@@ -354,6 +354,19 @@ export declare class ImapManager extends EventEmitter {
|
|
|
354
354
|
* before _Spam / Sent / archive folders had a chance to rebind. */
|
|
355
355
|
private static readonly RECONCILE_DELETE_GRACE_MS;
|
|
356
356
|
private scheduleDeferredReconcileDelete;
|
|
357
|
+
/** Delete local rows whose uid >= the server's UIDNEXT for this folder.
|
|
358
|
+
* Such a uid was NEVER assigned in this folder (UIDNEXT is the next one the
|
|
359
|
+
* server will ever hand out), so the row is a cross-wired phantom ("UID is
|
|
360
|
+
* not identity" corruption) — a real message stamped with the wrong
|
|
361
|
+
* folder_id. They break prefetch (the server returns 0 FETCH responses for
|
|
362
|
+
* uids it doesn't have → no body caches) and the normal set-diff reconcile
|
|
363
|
+
* refuses to remove them when they're >50% of the folder. Deleting by the
|
|
364
|
+
* UIDNEXT invariant is unconditional-safe: the uid is impossible, can't be a
|
|
365
|
+
* move target, and the message's real copy re-syncs from its true folder
|
|
366
|
+
* (the local store is a cache). Caller must have a TRUSTWORTHY uidNext
|
|
367
|
+
* (STATUS result) AND confirmed UIDVALIDITY is unchanged for this folder
|
|
368
|
+
* (full-sync and QRESYNC-success paths both satisfy that). Returns the count. */
|
|
369
|
+
private sweepPhantomRows;
|
|
357
370
|
private unlinkBodyFile;
|
|
358
371
|
/** Fetch a single message body on demand, caching in the store.
|
|
359
372
|
*
|
package/index.js
CHANGED
|
@@ -1505,6 +1505,14 @@ export class ImapManager extends EventEmitter {
|
|
|
1505
1505
|
if (qr.newHighestModSeq !== undefined) {
|
|
1506
1506
|
this.db.updateFolderSync(folderId, qr.exists ? prevUidValidity : prevUidValidity, String(qr.newHighestModSeq));
|
|
1507
1507
|
}
|
|
1508
|
+
// QRESYNC confirmed UIDVALIDITY is unchanged (it falls back to
|
|
1509
|
+
// full sync otherwise), so serverUidNext is trustworthy — sweep
|
|
1510
|
+
// phantom (uid >= UIDNEXT) cross-wired rows here too. Without
|
|
1511
|
+
// this, folders with a stable modseq (most of them) take the
|
|
1512
|
+
// QRESYNC fast path forever and their phantoms never get swept
|
|
1513
|
+
// (Bob 2026-06-17: CEBog phantoms persisted through the
|
|
1514
|
+
// reconcile-only sweep because CEBog syncs via QRESYNC).
|
|
1515
|
+
this.sweepPhantomRows(accountId, folderId, folder.path, serverUidNext);
|
|
1508
1516
|
this.emit("syncProgress", accountId, `sync:${folder.path}`, 100);
|
|
1509
1517
|
console.log(` [qresync] ${accountId}/${folder.path}: done in ${Date.now() - __sfStart}ms (path: QRESYNC)`);
|
|
1510
1518
|
return newOnes.length;
|
|
@@ -1828,23 +1836,7 @@ export class ImapManager extends EventEmitter {
|
|
|
1828
1836
|
// server hiccup — so delete them unconditionally (the real copy lives
|
|
1829
1837
|
// in its true folder and re-syncs from there; the local store is a
|
|
1830
1838
|
// cache). Only runs when STATUS gave a trustworthy UIDNEXT.
|
|
1831
|
-
|
|
1832
|
-
try {
|
|
1833
|
-
const phantoms = this.db.getUidsForFolder(accountId, folderId).filter(u => u >= serverUidNext);
|
|
1834
|
-
if (phantoms.length > 0) {
|
|
1835
|
-
for (const uid of phantoms) {
|
|
1836
|
-
this.unlinkBodyFile(accountId, uid, folderId).catch(() => { });
|
|
1837
|
-
this.db.deleteMessage(accountId, folderId, uid, `phantom: uid>=server UIDNEXT ${serverUidNext}`, `mailx-imap phantom sweep (${folder.path})`);
|
|
1838
|
-
}
|
|
1839
|
-
deletedCount += phantoms.length;
|
|
1840
|
-
this.db.recalcFolderCounts(folderId);
|
|
1841
|
-
console.log(` [phantom-sweep] ${accountId}/${folder.path}: removed ${phantoms.length} cross-wired row(s) with uid >= UIDNEXT ${serverUidNext}`);
|
|
1842
|
-
}
|
|
1843
|
-
}
|
|
1844
|
-
catch (e) {
|
|
1845
|
-
console.error(` [phantom-sweep] ${accountId}/${folder.path}: ${e?.message || e}`);
|
|
1846
|
-
}
|
|
1847
|
-
}
|
|
1839
|
+
deletedCount += this.sweepPhantomRows(accountId, folderId, folder.path, serverUidNext);
|
|
1848
1840
|
try {
|
|
1849
1841
|
// Reuse the server UID list set-diff already fetched.
|
|
1850
1842
|
// Without this we made TWO `UID SEARCH` calls per folder
|
|
@@ -2973,6 +2965,38 @@ export class ImapManager extends EventEmitter {
|
|
|
2973
2965
|
}, ImapManager.RECONCILE_DELETE_GRACE_MS);
|
|
2974
2966
|
this.deferredDeletes.set(key, t);
|
|
2975
2967
|
}
|
|
2968
|
+
/** Delete local rows whose uid >= the server's UIDNEXT for this folder.
|
|
2969
|
+
* Such a uid was NEVER assigned in this folder (UIDNEXT is the next one the
|
|
2970
|
+
* server will ever hand out), so the row is a cross-wired phantom ("UID is
|
|
2971
|
+
* not identity" corruption) — a real message stamped with the wrong
|
|
2972
|
+
* folder_id. They break prefetch (the server returns 0 FETCH responses for
|
|
2973
|
+
* uids it doesn't have → no body caches) and the normal set-diff reconcile
|
|
2974
|
+
* refuses to remove them when they're >50% of the folder. Deleting by the
|
|
2975
|
+
* UIDNEXT invariant is unconditional-safe: the uid is impossible, can't be a
|
|
2976
|
+
* move target, and the message's real copy re-syncs from its true folder
|
|
2977
|
+
* (the local store is a cache). Caller must have a TRUSTWORTHY uidNext
|
|
2978
|
+
* (STATUS result) AND confirmed UIDVALIDITY is unchanged for this folder
|
|
2979
|
+
* (full-sync and QRESYNC-success paths both satisfy that). Returns the count. */
|
|
2980
|
+
sweepPhantomRows(accountId, folderId, folderPath, serverUidNext) {
|
|
2981
|
+
if (serverUidNext == null || serverUidNext <= 0)
|
|
2982
|
+
return 0;
|
|
2983
|
+
try {
|
|
2984
|
+
const phantoms = this.db.getUidsForFolder(accountId, folderId).filter(u => u >= serverUidNext);
|
|
2985
|
+
if (phantoms.length === 0)
|
|
2986
|
+
return 0;
|
|
2987
|
+
for (const uid of phantoms) {
|
|
2988
|
+
this.unlinkBodyFile(accountId, uid, folderId).catch(() => { });
|
|
2989
|
+
this.db.deleteMessage(accountId, folderId, uid, `phantom: uid>=server UIDNEXT ${serverUidNext}`, `mailx-imap phantom sweep (${folderPath})`);
|
|
2990
|
+
}
|
|
2991
|
+
this.db.recalcFolderCounts(folderId);
|
|
2992
|
+
console.log(` [phantom-sweep] ${accountId}/${folderPath}: removed ${phantoms.length} cross-wired row(s) with uid >= UIDNEXT ${serverUidNext}`);
|
|
2993
|
+
return phantoms.length;
|
|
2994
|
+
}
|
|
2995
|
+
catch (e) {
|
|
2996
|
+
console.error(` [phantom-sweep] ${accountId}/${folderPath}: ${e?.message || e}`);
|
|
2997
|
+
return 0;
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
2976
3000
|
async unlinkBodyFile(accountId, uid, folderId) {
|
|
2977
3001
|
try {
|
|
2978
3002
|
const row = this.db.getMessageByUid(accountId, uid, folderId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.100",
|
|
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.19",
|
|
13
13
|
"@bobfrankston/mailx-settings": "^0.1.28",
|
|
14
|
-
"@bobfrankston/mailx-store": "^0.1.
|
|
14
|
+
"@bobfrankston/mailx-store": "^0.1.54",
|
|
15
15
|
"@bobfrankston/iflow-direct": "^0.1.55",
|
|
16
16
|
"@bobfrankston/tcp-transport": "^0.1.7",
|
|
17
17
|
"@bobfrankston/smtp-direct": "^0.1.9",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bobfrankston/mailx-types": "^0.1.19",
|
|
41
41
|
"@bobfrankston/mailx-settings": "^0.1.28",
|
|
42
|
-
"@bobfrankston/mailx-store": "^0.1.
|
|
42
|
+
"@bobfrankston/mailx-store": "^0.1.54",
|
|
43
43
|
"@bobfrankston/iflow-direct": "^0.1.55",
|
|
44
44
|
"@bobfrankston/tcp-transport": "^0.1.7",
|
|
45
45
|
"@bobfrankston/smtp-direct": "^0.1.9",
|