@bobfrankston/mailx-imap 0.1.38 → 0.1.39
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.js +46 -9
- package/package.json +3 -3
package/index.js
CHANGED
|
@@ -1041,6 +1041,11 @@ export class ImapManager extends EventEmitter {
|
|
|
1041
1041
|
// matches our local count, NOTHING changed — skip SELECT entirely.
|
|
1042
1042
|
// For first sync (highestUid = 0) skip the STATUS check and let the
|
|
1043
1043
|
// existing first-sync path (sequence-FETCH the latest N) run.
|
|
1044
|
+
// Captured at function scope so the set-diff block below can decide
|
|
1045
|
+
// when to skip the date-bound and fetch all UIDs (small folder
|
|
1046
|
+
// where a full UID SEARCH is cheap and reveals server-side deletes
|
|
1047
|
+
// of older messages).
|
|
1048
|
+
let statusMessageCount = null;
|
|
1044
1049
|
if (highestUid > 0) {
|
|
1045
1050
|
try {
|
|
1046
1051
|
console.log(` [sync-status] ${accountId}/${folder.path}: calling STATUS...`);
|
|
@@ -1048,6 +1053,8 @@ export class ImapManager extends EventEmitter {
|
|
|
1048
1053
|
const status = await client.native?.getStatus?.(folder.path)
|
|
1049
1054
|
?? await client.getStatus?.(folder.path);
|
|
1050
1055
|
console.log(` [sync-status] ${accountId}/${folder.path}: STATUS returned in ${Date.now() - __statusT0}ms`);
|
|
1056
|
+
if (status && typeof status.messages === "number")
|
|
1057
|
+
statusMessageCount = status.messages;
|
|
1051
1058
|
if (status && typeof status.uidNext === "number") {
|
|
1052
1059
|
const serverHighest = status.uidNext - 1;
|
|
1053
1060
|
const noNewUids = serverHighest <= highestUid;
|
|
@@ -1132,19 +1139,38 @@ export class ImapManager extends EventEmitter {
|
|
|
1132
1139
|
// archive every cycle. First sync gets all UIDs (no anchor
|
|
1133
1140
|
// yet so we have to compare against the empty local set
|
|
1134
1141
|
// anyway).
|
|
1142
|
+
// Small folders (Drafts, Sent, Outbox, Trash, …) get a FULL
|
|
1143
|
+
// UID fetch instead of date-bounded. Without this, server-side
|
|
1144
|
+
// deletions of OLDER messages (e.g., user emptied Trash from
|
|
1145
|
+
// Thunderbird) are never reflected — date-bound filters them
|
|
1146
|
+
// out of the comparison entirely. Bob 2026-05-12: "I deleted
|
|
1147
|
+
// my drafts and Thunderbird shows that but rmfmail is not
|
|
1148
|
+
// acknowledging the deletions." Threshold tuned conservatively
|
|
1149
|
+
// — UID SEARCH ALL on a 500-message folder is sub-second; the
|
|
1150
|
+
// pathology being avoided is the same call on 130k+ INBOX.
|
|
1151
|
+
const SMALL_FOLDER_THRESHOLD = 500;
|
|
1152
|
+
const isSmallFolder = statusMessageCount !== null && statusMessageCount <= SMALL_FOLDER_THRESHOLD;
|
|
1135
1153
|
const SINCE_DAYS = effectiveDays > 0 ? effectiveDays : 1825;
|
|
1136
1154
|
const sinceDate = new Date(Date.now() - SINCE_DAYS * 86400000);
|
|
1137
|
-
|
|
1155
|
+
let allServerUids;
|
|
1138
1156
|
const __uidsT0 = Date.now();
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1157
|
+
if (isSmallFolder) {
|
|
1158
|
+
console.log(` [sync-uids] ${accountId}/${folder.path}: small folder (server=${statusMessageCount}) — getUids ALL`);
|
|
1159
|
+
allServerUids = await client.getUids(folder.path);
|
|
1160
|
+
serverUidsAreDateBounded = false;
|
|
1161
|
+
}
|
|
1162
|
+
else {
|
|
1163
|
+
console.log(` [sync-uids] ${accountId}/${folder.path}: getUidsSince ${sinceDate.toISOString().slice(0, 10)}...`);
|
|
1164
|
+
allServerUids = typeof client.getUidsSince === "function"
|
|
1165
|
+
? await client.getUidsSince(folder.path, sinceDate)
|
|
1166
|
+
: await client.getUids(folder.path);
|
|
1167
|
+
serverUidsAreDateBounded = true;
|
|
1168
|
+
}
|
|
1142
1169
|
console.log(` [sync-uids] ${accountId}/${folder.path}: ${allServerUids.length} UIDs in ${Date.now() - __uidsT0}ms`);
|
|
1143
1170
|
// Stash for the deletion-reconciliation block below — we
|
|
1144
|
-
// already have the
|
|
1145
|
-
//
|
|
1171
|
+
// already have the server UID list, no point hitting the
|
|
1172
|
+
// server a second time.
|
|
1146
1173
|
serverUidsCached = allServerUids;
|
|
1147
|
-
serverUidsAreDateBounded = true;
|
|
1148
1174
|
const existingSet = new Set(existingUids);
|
|
1149
1175
|
const newSet = new Set(messages.map(m => m.uid));
|
|
1150
1176
|
const missingUids = allServerUids.filter((uid) => !existingSet.has(uid) && !newSet.has(uid));
|
|
@@ -1367,10 +1393,21 @@ export class ImapManager extends EventEmitter {
|
|
|
1367
1393
|
localUids = localUidsAll.filter(u => u >= minServerUid);
|
|
1368
1394
|
}
|
|
1369
1395
|
const toDelete = localUids.filter(uid => !serverUids.has(uid));
|
|
1370
|
-
|
|
1396
|
+
// Authoritative-list signal: when the server's UID list came
|
|
1397
|
+
// back as the FULL set (not date-bounded) AND the size matches
|
|
1398
|
+
// what STATUS reported, we know we have the server's truth.
|
|
1399
|
+
// The 50% safety guard exists to protect against partial /
|
|
1400
|
+
// transient server responses; it should NOT block reconcile
|
|
1401
|
+
// when the response is authoritative. Without this carve-out,
|
|
1402
|
+
// emptying a folder externally (Thunderbird, webmail, mobile)
|
|
1403
|
+
// leaves the old rows in mailx forever (Bob 2026-05-12).
|
|
1404
|
+
const authoritative = !serverUidsAreDateBounded
|
|
1405
|
+
&& statusMessageCount !== null
|
|
1406
|
+
&& serverUidsArr.length === statusMessageCount;
|
|
1407
|
+
if (serverUidsArr.length === 0 && localUidsAll.length > 0 && !authoritative) {
|
|
1371
1408
|
console.log(` [sync] ${accountId}/${folder.path}: reconcile skipped — server UID list empty but local has ${localUidsAll.length} (treating as transient)`);
|
|
1372
1409
|
}
|
|
1373
|
-
else if (localUids.length > 0 && toDelete.length / localUids.length > 0.5) {
|
|
1410
|
+
else if (!authoritative && localUids.length > 0 && toDelete.length / localUids.length > 0.5) {
|
|
1374
1411
|
console.log(` [sync] ${accountId}/${folder.path}: reconcile REFUSED — would delete ${toDelete.length}/${localUids.length} (${Math.round(toDelete.length / localUids.length * 100)}%) — probably a sync bug, skipping`);
|
|
1375
1412
|
}
|
|
1376
1413
|
else if (toDelete.length > 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.39",
|
|
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.11",
|
|
13
13
|
"@bobfrankston/mailx-settings": "^0.1.16",
|
|
14
|
-
"@bobfrankston/mailx-store": "^0.1.
|
|
14
|
+
"@bobfrankston/mailx-store": "^0.1.20",
|
|
15
15
|
"@bobfrankston/iflow-direct": "^0.1.41",
|
|
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.11",
|
|
41
41
|
"@bobfrankston/mailx-settings": "^0.1.16",
|
|
42
|
-
"@bobfrankston/mailx-store": "^0.1.
|
|
42
|
+
"@bobfrankston/mailx-store": "^0.1.20",
|
|
43
43
|
"@bobfrankston/iflow-direct": "^0.1.41",
|
|
44
44
|
"@bobfrankston/tcp-transport": "^0.1.6",
|
|
45
45
|
"@bobfrankston/smtp-direct": "^0.1.8",
|