@bobfrankston/mailx-imap 0.1.97 → 0.1.99

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.
Files changed (2) hide show
  1. package/index.js +34 -0
  2. package/package.json +5 -5
package/index.js CHANGED
@@ -1197,6 +1197,7 @@ export class ImapManager extends EventEmitter {
1197
1197
  to: toEmailAddresses(msg.to || []),
1198
1198
  cc: toEmailAddresses(msg.cc || []),
1199
1199
  flags, size: msg.size || 0, hasAttachments, preview, bodyPath,
1200
+ exclusive: true, // IMAP path → one folder per message
1200
1201
  },
1201
1202
  ftsBody,
1202
1203
  });
@@ -1360,6 +1361,7 @@ export class ImapManager extends EventEmitter {
1360
1361
  // where a full UID SEARCH is cheap and reveals server-side deletes
1361
1362
  // of older messages).
1362
1363
  let statusMessageCount = null;
1364
+ let serverUidNext = null;
1363
1365
  if (highestUid > 0) {
1364
1366
  try {
1365
1367
  console.log(` [sync-status] ${accountId}/${folder.path}: calling STATUS...`);
@@ -1370,6 +1372,7 @@ export class ImapManager extends EventEmitter {
1370
1372
  if (status && typeof status.messages === "number")
1371
1373
  statusMessageCount = status.messages;
1372
1374
  if (status && typeof status.uidNext === "number") {
1375
+ serverUidNext = status.uidNext;
1373
1376
  const serverHighest = status.uidNext - 1;
1374
1377
  const noNewUids = serverHighest <= highestUid;
1375
1378
  const countsMatch = typeof status.messages === "number" && status.messages === localCount;
@@ -1755,6 +1758,7 @@ export class ImapManager extends EventEmitter {
1755
1758
  hasAttachments: parsed.hasAttachments,
1756
1759
  preview: parsed.preview,
1757
1760
  bodyPath,
1761
+ exclusive: true, // IMAP path → one folder per message
1758
1762
  },
1759
1763
  ftsBody: parsed.bodyText || "",
1760
1764
  });
@@ -1811,6 +1815,36 @@ export class ImapManager extends EventEmitter {
1811
1815
  // data (the "ubiquiti letter disappeared after reply" case had no trace).
1812
1816
  let deletedCount = 0;
1813
1817
  if (!firstSync) {
1818
+ // PHANTOM SWEEP (Bob 2026-06-17). A local row whose uid >= the server's
1819
+ // UIDNEXT is PROVABLY impossible: UIDNEXT is the next uid the server
1820
+ // will ever assign in this folder, so any uid >= it was never assigned
1821
+ // here. These are cross-wired rows ("UID is not identity" corruption —
1822
+ // CEBog had 106/120 rows with uid >= UIDNEXT=94, real messages stamped
1823
+ // with the wrong folder_id). They break prefetch: it asks the server
1824
+ // for uids the folder doesn't have → 0 FETCH responses → no body ever
1825
+ // caches. The normal set-diff reconcile WON'T remove them because the
1826
+ // 50%-deletion safety guard refuses (88% of CEBog would delete). But
1827
+ // these uids can't be a real message, a move target, or a transient
1828
+ // server hiccup — so delete them unconditionally (the real copy lives
1829
+ // in its true folder and re-syncs from there; the local store is a
1830
+ // cache). Only runs when STATUS gave a trustworthy UIDNEXT.
1831
+ if (serverUidNext != null && serverUidNext > 0) {
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
+ }
1814
1848
  try {
1815
1849
  // Reuse the server UID list set-diff already fetched.
1816
1850
  // Without this we made TWO `UID SEARCH` calls per folder
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-imap",
3
- "version": "0.1.97",
3
+ "version": "0.1.99",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -11,8 +11,8 @@
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.51",
15
- "@bobfrankston/iflow-direct": "^0.1.54",
14
+ "@bobfrankston/mailx-store": "^0.1.53",
15
+ "@bobfrankston/iflow-direct": "^0.1.55",
16
16
  "@bobfrankston/tcp-transport": "^0.1.7",
17
17
  "@bobfrankston/smtp-direct": "^0.1.9",
18
18
  "@bobfrankston/mailx-sync": "^0.1.22",
@@ -39,8 +39,8 @@
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.51",
43
- "@bobfrankston/iflow-direct": "^0.1.54",
42
+ "@bobfrankston/mailx-store": "^0.1.53",
43
+ "@bobfrankston/iflow-direct": "^0.1.55",
44
44
  "@bobfrankston/tcp-transport": "^0.1.7",
45
45
  "@bobfrankston/smtp-direct": "^0.1.9",
46
46
  "@bobfrankston/mailx-sync": "^0.1.22",