@bobfrankston/mailx-imap 0.1.25 → 0.1.26
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 +4 -1
- package/index.js +49 -70
- package/package.json +5 -5
package/index.d.ts
CHANGED
|
@@ -247,7 +247,10 @@ export declare class ImapManager extends EventEmitter {
|
|
|
247
247
|
stopPeriodicSync(): void;
|
|
248
248
|
/** Check if an account is OAuth (Gmail/Outlook — generous connection limits) */
|
|
249
249
|
isOAuthAccount(accountId: string): boolean;
|
|
250
|
-
/** Start IMAP IDLE
|
|
250
|
+
/** Start an IMAP IDLE watcher per account on INBOX. Other folders are
|
|
251
|
+
* not special-cased here — sync runs uniformly on every folder via the
|
|
252
|
+
* periodic full-sync timer (STATUS-before-SELECT optimization is the
|
|
253
|
+
* right way to make that cheap, not piling per-folder IDLE sockets). */
|
|
251
254
|
startWatching(): Promise<void>;
|
|
252
255
|
/** Stop all IDLE watchers */
|
|
253
256
|
stopWatching(): Promise<void>;
|
package/index.js
CHANGED
|
@@ -878,6 +878,36 @@ export class ImapManager extends EventEmitter {
|
|
|
878
878
|
// monotonically increasing within a UIDVALIDITY (RFC 3501); a
|
|
879
879
|
// high-water mark is the right anchor for incremental fetch.
|
|
880
880
|
const highestUid = this.db.getHighestUid(accountId, folderId);
|
|
881
|
+
// STATUS-before-SELECT: ask the server cheaply whether anything has
|
|
882
|
+
// changed (UIDNEXT moved or message count differs). STATUS doesn't
|
|
883
|
+
// load the mailbox index, doesn't lock the mailbox, doesn't risk the
|
|
884
|
+
// SELECT-wedge that's been hammering bobma. Only when STATUS says
|
|
885
|
+
// there's actual work to do do we fall through to SELECT+FETCH.
|
|
886
|
+
// Apply uniformly to every folder — no special-casing.
|
|
887
|
+
try {
|
|
888
|
+
if (typeof client.getStatus === "function") {
|
|
889
|
+
const status = await client.getStatus(folder.path);
|
|
890
|
+
const serverHighest = (status.uidNext || 1) - 1;
|
|
891
|
+
const serverCount = status.messages ?? -1;
|
|
892
|
+
const localCount = this.db.getMessageCount(accountId, folderId);
|
|
893
|
+
const isUpToDate = highestUid > 0
|
|
894
|
+
&& serverHighest <= highestUid
|
|
895
|
+
&& serverCount >= 0
|
|
896
|
+
&& serverCount === localCount;
|
|
897
|
+
if (isUpToDate) {
|
|
898
|
+
console.log(` [sync] ${accountId}/${folder.path}: STATUS up-to-date (uidNext=${status.uidNext}, server=${serverCount}, local=${localCount}) — skipping SELECT`);
|
|
899
|
+
this.emit("syncProgress", accountId, `sync:${folder.path}`, 100);
|
|
900
|
+
return 0;
|
|
901
|
+
}
|
|
902
|
+
console.log(` [sync] ${accountId}/${folder.path}: STATUS uidNext=${status.uidNext} server=${serverCount} local=${localCount} highestUid=${highestUid} — needs SELECT`);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
catch (e) {
|
|
906
|
+
// STATUS shouldn't fail, but don't make it load-bearing — fall
|
|
907
|
+
// through to the SELECT path on any error and let that path's
|
|
908
|
+
// existing handling deal with it.
|
|
909
|
+
console.log(` [sync] ${accountId}/${folder.path}: STATUS failed (${e?.message || e}) — falling through to SELECT`);
|
|
910
|
+
}
|
|
881
911
|
console.log(` [sync] ${accountId}/${folder.path}: highestUid=${highestUid}, fetching...`);
|
|
882
912
|
let messages;
|
|
883
913
|
const firstSync = highestUid === 0;
|
|
@@ -1108,7 +1138,7 @@ export class ImapManager extends EventEmitter {
|
|
|
1108
1138
|
const tag = env ? `msgid=${env.messageId || "?"} subj="${(env.subject || "").slice(0, 60)}"` : "unknown";
|
|
1109
1139
|
console.log(` [reconcile-delete] ${accountId}/${folder.path} uid=${uid} ${tag}`);
|
|
1110
1140
|
this.unlinkBodyFile(accountId, uid, folderId).catch(() => { });
|
|
1111
|
-
this.db.deleteMessage(accountId, uid);
|
|
1141
|
+
this.db.deleteMessage(accountId, uid, "reconcile: server returned UID list without this row", `mailx-imap syncFolder reconcile (${folder.path})`);
|
|
1112
1142
|
deletedCount++;
|
|
1113
1143
|
}
|
|
1114
1144
|
if (deletedCount > 0)
|
|
@@ -1484,7 +1514,7 @@ export class ImapManager extends EventEmitter {
|
|
|
1484
1514
|
const tag = env ? `msgid=${env.messageId || "?"} subj="${(env.subject || "").slice(0, 60)}"` : "unknown";
|
|
1485
1515
|
console.log(` [reconcile-delete] ${accountId}/${folder.path} uid=${uid} ${tag}`);
|
|
1486
1516
|
this.unlinkBodyFile(accountId, uid, folder.id).catch(() => { });
|
|
1487
|
-
this.db.deleteMessage(accountId, uid);
|
|
1517
|
+
this.db.deleteMessage(accountId, uid, "Gmail-API reconcile: server list missing this UID", `mailx-imap Gmail reconcile (${folder.path})`);
|
|
1488
1518
|
}
|
|
1489
1519
|
if (toDelete.length > 0)
|
|
1490
1520
|
console.log(` [api] ${accountId}/${folder.path}: ${toDelete.length} deleted`);
|
|
@@ -1863,7 +1893,10 @@ export class ImapManager extends EventEmitter {
|
|
|
1863
1893
|
const config = this.configs.get(accountId);
|
|
1864
1894
|
return !!config?.tokenProvider;
|
|
1865
1895
|
}
|
|
1866
|
-
/** Start IMAP IDLE
|
|
1896
|
+
/** Start an IMAP IDLE watcher per account on INBOX. Other folders are
|
|
1897
|
+
* not special-cased here — sync runs uniformly on every folder via the
|
|
1898
|
+
* periodic full-sync timer (STATUS-before-SELECT optimization is the
|
|
1899
|
+
* right way to make that cheap, not piling per-folder IDLE sockets). */
|
|
1867
1900
|
async startWatching() {
|
|
1868
1901
|
for (const [accountId] of this.configs) {
|
|
1869
1902
|
if (this.watchers.has(accountId))
|
|
@@ -1873,70 +1906,16 @@ export class ImapManager extends EventEmitter {
|
|
|
1873
1906
|
// is parked in IDLE, it's unusable for any other command, so
|
|
1874
1907
|
// it can't share the ops queue. Counts against the per-host
|
|
1875
1908
|
// semaphore (one slot for the IDLE socket).
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
// full sync.
|
|
1882
|
-
const stops = [];
|
|
1883
|
-
const clients = [];
|
|
1884
|
-
const watchOne = async (mailboxLabel, path) => {
|
|
1885
|
-
const client = await this.createClient(accountId, "idle");
|
|
1886
|
-
clients.push(client);
|
|
1887
|
-
const stop = await client.watchMailbox(path, (newCount) => {
|
|
1888
|
-
console.log(` [idle] ${accountId} ${path}: ${newCount} new message(s)`);
|
|
1889
|
-
if (mailboxLabel === "inbox") {
|
|
1890
|
-
// Fast path: incremental fetch of NEW UIDs only.
|
|
1891
|
-
// Heavy reconcile runs on the 5-minute STATUS poll.
|
|
1892
|
-
this.syncInboxNewOnly(accountId).catch(e => console.error(` [idle] inbox sync error: ${e.message}`));
|
|
1893
|
-
}
|
|
1894
|
-
else {
|
|
1895
|
-
// Sent / Drafts changed elsewhere. Use the
|
|
1896
|
-
// standard folder sync — picks up the new UID,
|
|
1897
|
-
// rebinds any optimistic local row by Message-ID.
|
|
1898
|
-
const folder = this.findFolder(accountId, mailboxLabel);
|
|
1899
|
-
if (folder) {
|
|
1900
|
-
this.syncFolder(accountId, folder.id).catch(e => console.error(` [idle] ${path} sync error: ${e.message}`));
|
|
1901
|
-
}
|
|
1902
|
-
}
|
|
1903
|
-
});
|
|
1904
|
-
stops.push(stop);
|
|
1905
|
-
};
|
|
1906
|
-
await watchOne("inbox", "INBOX");
|
|
1907
|
-
const sent = this.findFolder(accountId, "sent");
|
|
1908
|
-
if (sent) {
|
|
1909
|
-
try {
|
|
1910
|
-
await watchOne("sent", sent.path);
|
|
1911
|
-
}
|
|
1912
|
-
catch (e) {
|
|
1913
|
-
console.error(` [idle] Failed to watch ${sent.path}: ${e.message}`);
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
1916
|
-
const drafts = this.findFolder(accountId, "drafts");
|
|
1917
|
-
if (drafts) {
|
|
1918
|
-
try {
|
|
1919
|
-
await watchOne("drafts", drafts.path);
|
|
1920
|
-
}
|
|
1921
|
-
catch (e) {
|
|
1922
|
-
console.error(` [idle] Failed to watch ${drafts.path}: ${e.message}`);
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1909
|
+
const watchClient = await this.createClient(accountId, "idle");
|
|
1910
|
+
const stop = await watchClient.watchMailbox("INBOX", (newCount) => {
|
|
1911
|
+
console.log(` [idle] ${accountId}: ${newCount} new message(s)`);
|
|
1912
|
+
this.syncInboxNewOnly(accountId).catch(e => console.error(` [idle] sync error: ${e.message}`));
|
|
1913
|
+
});
|
|
1925
1914
|
this.watchers.set(accountId, async () => {
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
await stop();
|
|
1929
|
-
}
|
|
1930
|
-
catch { /* ignore */ }
|
|
1931
|
-
}
|
|
1932
|
-
for (const c of clients) {
|
|
1933
|
-
try {
|
|
1934
|
-
await c.logout();
|
|
1935
|
-
}
|
|
1936
|
-
catch { /* ignore */ }
|
|
1937
|
-
}
|
|
1915
|
+
await stop();
|
|
1916
|
+
await watchClient.logout();
|
|
1938
1917
|
});
|
|
1939
|
-
console.log(` [idle] Watching INBOX
|
|
1918
|
+
console.log(` [idle] Watching INBOX for ${accountId}`);
|
|
1940
1919
|
}
|
|
1941
1920
|
catch (e) {
|
|
1942
1921
|
console.error(` [idle] Failed to watch ${accountId}: ${e.message}`);
|
|
@@ -2204,7 +2183,7 @@ export class ImapManager extends EventEmitter {
|
|
|
2204
2183
|
continue;
|
|
2205
2184
|
try {
|
|
2206
2185
|
this.unlinkBodyFile(accountId, uid, folderId).catch(() => { });
|
|
2207
|
-
this.db.deleteMessage(accountId, uid);
|
|
2186
|
+
this.db.deleteMessage(accountId, uid, "prefetch batch: server didn't return body for queued UID — assumed deleted", "mailx-imap prefetchBodies (Gmail batch)");
|
|
2208
2187
|
counters.deleted++;
|
|
2209
2188
|
madeProgress = true;
|
|
2210
2189
|
}
|
|
@@ -2304,7 +2283,7 @@ export class ImapManager extends EventEmitter {
|
|
|
2304
2283
|
continue;
|
|
2305
2284
|
try {
|
|
2306
2285
|
this.unlinkBodyFile(accountId, uid, folderId).catch(() => { });
|
|
2307
|
-
this.db.deleteMessage(accountId, uid);
|
|
2286
|
+
this.db.deleteMessage(accountId, uid, "prefetch batch: server didn't return body for queued UID — assumed deleted", "mailx-imap prefetchBodies (IMAP batch)");
|
|
2308
2287
|
counters.deleted++;
|
|
2309
2288
|
madeProgress = true;
|
|
2310
2289
|
}
|
|
@@ -2340,7 +2319,7 @@ export class ImapManager extends EventEmitter {
|
|
|
2340
2319
|
// Local first — remove all from DB immediately
|
|
2341
2320
|
for (const msg of messages) {
|
|
2342
2321
|
this.unlinkBodyFile(accountId, msg.uid, msg.folderId).catch(() => { });
|
|
2343
|
-
this.db.deleteMessage(accountId, msg.uid);
|
|
2322
|
+
this.db.deleteMessage(accountId, msg.uid, "user-initiated delete (bulk)", "mailx-imap deleteMessages");
|
|
2344
2323
|
}
|
|
2345
2324
|
console.log(` Deleted ${messages.length} messages locally`);
|
|
2346
2325
|
// Queue IMAP actions
|
|
@@ -2393,7 +2372,7 @@ export class ImapManager extends EventEmitter {
|
|
|
2393
2372
|
const trash = this.findFolder(accountId, "trash");
|
|
2394
2373
|
// Local first — remove from DB immediately
|
|
2395
2374
|
this.unlinkBodyFile(accountId, uid, folderId).catch(() => { });
|
|
2396
|
-
this.db.deleteMessage(accountId, uid);
|
|
2375
|
+
this.db.deleteMessage(accountId, uid, "user-initiated trash", "mailx-imap trashMessage");
|
|
2397
2376
|
// Queue IMAP action + log the resolution so "I deleted a message and
|
|
2398
2377
|
// now it's in neither trash nor deleted" is diagnosable from the log.
|
|
2399
2378
|
if (trash && trash.id !== folderId) {
|
|
@@ -2435,7 +2414,7 @@ export class ImapManager extends EventEmitter {
|
|
|
2435
2414
|
if (!msg)
|
|
2436
2415
|
throw new Error(`Message UID ${uid} not found in ${fromFolder.path}`);
|
|
2437
2416
|
await sourceClient.moveMessageToServer(msg, fromFolder.path, targetClient, toFolder.path);
|
|
2438
|
-
this.db.deleteMessage(fromAccountId, uid);
|
|
2417
|
+
this.db.deleteMessage(fromAccountId, uid, `cross-account move to ${toAccountId}/${toFolder.path}`, "mailx-imap moveBetweenAccounts");
|
|
2439
2418
|
console.log(` Cross-account move: ${fromAccountId}/${fromFolder.path} UID ${uid} → ${toAccountId}/${toFolder.path}`);
|
|
2440
2419
|
});
|
|
2441
2420
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
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.10",
|
|
13
13
|
"@bobfrankston/mailx-settings": "^0.1.13",
|
|
14
|
-
"@bobfrankston/mailx-store": "^0.1.
|
|
15
|
-
"@bobfrankston/iflow-direct": "^0.1.
|
|
14
|
+
"@bobfrankston/mailx-store": "^0.1.11",
|
|
15
|
+
"@bobfrankston/iflow-direct": "^0.1.32",
|
|
16
16
|
"@bobfrankston/tcp-transport": "^0.1.5",
|
|
17
17
|
"@bobfrankston/smtp-direct": "^0.1.5",
|
|
18
18
|
"@bobfrankston/mailx-sync": "^0.1.15",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bobfrankston/mailx-types": "^0.1.10",
|
|
41
41
|
"@bobfrankston/mailx-settings": "^0.1.13",
|
|
42
|
-
"@bobfrankston/mailx-store": "^0.1.
|
|
43
|
-
"@bobfrankston/iflow-direct": "^0.1.
|
|
42
|
+
"@bobfrankston/mailx-store": "^0.1.11",
|
|
43
|
+
"@bobfrankston/iflow-direct": "^0.1.32",
|
|
44
44
|
"@bobfrankston/tcp-transport": "^0.1.5",
|
|
45
45
|
"@bobfrankston/smtp-direct": "^0.1.5",
|
|
46
46
|
"@bobfrankston/mailx-sync": "^0.1.15",
|