@bobfrankston/mailx-imap 0.1.148 → 0.1.149
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 +9 -0
- package/index.js +86 -0
- package/package.json +5 -5
package/index.d.ts
CHANGED
|
@@ -263,6 +263,15 @@ export declare class ImapManager extends EventEmitter {
|
|
|
263
263
|
private noteRateLimited;
|
|
264
264
|
/** Clear the cooldown after a connection succeeds. */
|
|
265
265
|
private noteRateLimitCleared;
|
|
266
|
+
/**
|
|
267
|
+
* Fetch bodies for rows that carry IMAP uids on an account whose normal
|
|
268
|
+
* body path is a REST API (Gmail today). Uses the account's IMAP
|
|
269
|
+
* connection — the same one the server search used to find them.
|
|
270
|
+
*
|
|
271
|
+
* Returns how many bodies landed. Failures are logged per row and do not
|
|
272
|
+
* abort the rest: one unreadable message must not stall the queue.
|
|
273
|
+
*/
|
|
274
|
+
private fetchBodiesOverImap;
|
|
266
275
|
private newClient;
|
|
267
276
|
/** Force-close every IMAP socket for an account — both lane clients
|
|
268
277
|
* (ops + fast) plus any lingering ones in openClients (e.g. an IDLE
|
package/index.js
CHANGED
|
@@ -27,6 +27,21 @@ const OUTBOX_RETRY_DELAY_MS = 60000;
|
|
|
27
27
|
* after boot always runs one (the map is empty); thereafter it's throttled to
|
|
28
28
|
* this interval so the QRESYNC speedup is preserved in steady state. */
|
|
29
29
|
const RECONCILE_THROTTLE_MS = 15 * 60 * 1000;
|
|
30
|
+
/**
|
|
31
|
+
* An IMAP UID is a 32-bit unsigned integer (RFC 3501 §2.3.1.1). The API
|
|
32
|
+
* providers mint their uid from the provider's own message id — Gmail uses
|
|
33
|
+
* `parseInt(id.slice(-12), 16)`, a 48-bit value — so the two uid spaces are
|
|
34
|
+
* disjoint and this bound tells them apart exactly, not heuristically.
|
|
35
|
+
*
|
|
36
|
+
* It matters because ONE ACCOUNT can hold rows from both: a Gmail account
|
|
37
|
+
* syncs bodies through the API, but a server search runs over IMAP and
|
|
38
|
+
* stores rows with IMAP UIDs. Those rows are unfetchable by the API path —
|
|
39
|
+
* it maps uid→id by listing the label and hashing each id, and an IMAP uid
|
|
40
|
+
* is not in that map at any depth. The body request simply returned nothing,
|
|
41
|
+
* forever, with the viewer counting seconds (Bob 2026-08-09: "why the
|
|
42
|
+
* forever fetch?" / "why did gmail search find them if they can't be read?").
|
|
43
|
+
*/
|
|
44
|
+
const IMAP_UID_MAX = 0xFFFFFFFF;
|
|
30
45
|
/** Parse X-Mailx-Retry* tracking headers from a raw RFC822 message. */
|
|
31
46
|
function parseRetryInfo(raw) {
|
|
32
47
|
const headerEnd = raw.search(/\r?\n\r?\n/);
|
|
@@ -937,6 +952,60 @@ export class ImapManager extends EventEmitter {
|
|
|
937
952
|
this.emit("rateLimitCleared", { accountId });
|
|
938
953
|
}
|
|
939
954
|
}
|
|
955
|
+
/**
|
|
956
|
+
* Fetch bodies for rows that carry IMAP uids on an account whose normal
|
|
957
|
+
* body path is a REST API (Gmail today). Uses the account's IMAP
|
|
958
|
+
* connection — the same one the server search used to find them.
|
|
959
|
+
*
|
|
960
|
+
* Returns how many bodies landed. Failures are logged per row and do not
|
|
961
|
+
* abort the rest: one unreadable message must not stall the queue.
|
|
962
|
+
*/
|
|
963
|
+
async fetchBodiesOverImap(accountId, rows) {
|
|
964
|
+
const folders = this.db.getFolders(accountId);
|
|
965
|
+
let fetched = 0;
|
|
966
|
+
let client = null;
|
|
967
|
+
try {
|
|
968
|
+
client = await this.newClient(accountId, "fetch");
|
|
969
|
+
await client.connect?.();
|
|
970
|
+
for (const row of rows) {
|
|
971
|
+
const folder = folders.find(f => f.id === row.folderId);
|
|
972
|
+
if (!folder)
|
|
973
|
+
continue;
|
|
974
|
+
try {
|
|
975
|
+
const msg = await client.fetchMessageByUid(folder.path, row.uid, { source: true });
|
|
976
|
+
const source = msg?.source;
|
|
977
|
+
if (!source) {
|
|
978
|
+
// Not on the server either — stop asking for it every
|
|
979
|
+
// sweep, but say so once rather than silently.
|
|
980
|
+
console.log(` [prefetch] ${accountId}/${folder.path} uid ${row.uid}: not on the server over IMAP either — marking empty`);
|
|
981
|
+
this.markPrefetchEmpty(accountId, row.folderId, row.uid);
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
const raw = Buffer.from(source, "utf-8");
|
|
985
|
+
const bodyPath = await this.bodyStore.putMessage(accountId, row.folderId, row.uid, raw);
|
|
986
|
+
const parsed = await extractPreview(source);
|
|
987
|
+
this.db.updateBodyMeta(accountId, row.folderId, row.uid, bodyPath, parsed.hasAttachments, parsed.preview);
|
|
988
|
+
this.emit("bodyCached", accountId, row.uid);
|
|
989
|
+
fetched++;
|
|
990
|
+
}
|
|
991
|
+
catch (e) {
|
|
992
|
+
console.error(` [prefetch] ${accountId}/${folder.path} uid ${row.uid}: IMAP body fetch failed: ${e?.message || e}`);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
catch (e) {
|
|
997
|
+
console.error(` [prefetch] ${accountId}: IMAP fallback could not open a connection: ${e?.message || e}`);
|
|
998
|
+
}
|
|
999
|
+
finally {
|
|
1000
|
+
try {
|
|
1001
|
+
await client?.logout?.();
|
|
1002
|
+
}
|
|
1003
|
+
catch { /* socket already gone */ }
|
|
1004
|
+
}
|
|
1005
|
+
if (fetched > 0)
|
|
1006
|
+
console.log(` [prefetch] ${accountId}: ${fetched} body(ies) fetched over IMAP for API-account rows`);
|
|
1007
|
+
return fetched;
|
|
1008
|
+
}
|
|
940
1009
|
async newClient(accountId, purpose = "?") {
|
|
941
1010
|
// Refuse to spend a LOGIN the server has already told us it will not
|
|
942
1011
|
// accept. Every lane funnels through newClient, so one check covers
|
|
@@ -3864,7 +3933,16 @@ export class ImapManager extends EventEmitter {
|
|
|
3864
3933
|
// message in multiple labels gets fetched twice under current
|
|
3865
3934
|
// grouping. A deeper label-native redesign is a separate TODO.
|
|
3866
3935
|
const byFolder = new Map();
|
|
3936
|
+
// Rows whose uid is in the IMAP space cannot be addressed by
|
|
3937
|
+
// the API provider at all. Route them to the IMAP fetcher —
|
|
3938
|
+
// the same transport that created them — instead of handing
|
|
3939
|
+
// the API a name it cannot resolve.
|
|
3940
|
+
const imapSpaceRows = [];
|
|
3867
3941
|
for (const m of missing) {
|
|
3942
|
+
if (m.uid <= IMAP_UID_MAX) {
|
|
3943
|
+
imapSpaceRows.push({ folderId: m.folderId, uid: m.uid });
|
|
3944
|
+
continue;
|
|
3945
|
+
}
|
|
3868
3946
|
let arr = byFolder.get(m.folderId);
|
|
3869
3947
|
if (!arr) {
|
|
3870
3948
|
arr = [];
|
|
@@ -3872,6 +3950,14 @@ export class ImapManager extends EventEmitter {
|
|
|
3872
3950
|
}
|
|
3873
3951
|
arr.push(m.uid);
|
|
3874
3952
|
}
|
|
3953
|
+
if (imapSpaceRows.length > 0) {
|
|
3954
|
+
console.log(` [prefetch] ${accountId}: ${imapSpaceRows.length} row(s) carry IMAP uids on an API account (server-search results) — fetching those over IMAP`);
|
|
3955
|
+
const fetchedOverImap = await this.fetchBodiesOverImap(accountId, imapSpaceRows);
|
|
3956
|
+
if (fetchedOverImap > 0) {
|
|
3957
|
+
madeProgress = true;
|
|
3958
|
+
counters.totalFetched += fetchedOverImap;
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3875
3961
|
const folders = this.db.getFolders(accountId);
|
|
3876
3962
|
const api = this.getApiProvider(accountId);
|
|
3877
3963
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.149",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@bobfrankston/mailx-types": "^0.1.
|
|
13
|
-
"@bobfrankston/mailx-settings": "^0.1.
|
|
12
|
+
"@bobfrankston/mailx-types": "^0.1.42",
|
|
13
|
+
"@bobfrankston/mailx-settings": "^0.1.51",
|
|
14
14
|
"@bobfrankston/mailx-store": "^0.1.83",
|
|
15
15
|
"@bobfrankston/iflow-direct": "^0.1.65",
|
|
16
16
|
"@bobfrankston/tcp-transport": "^0.1.8",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
},
|
|
38
38
|
".transformedSnapshot": {
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@bobfrankston/mailx-types": "^0.1.
|
|
41
|
-
"@bobfrankston/mailx-settings": "^0.1.
|
|
40
|
+
"@bobfrankston/mailx-types": "^0.1.42",
|
|
41
|
+
"@bobfrankston/mailx-settings": "^0.1.51",
|
|
42
42
|
"@bobfrankston/mailx-store": "^0.1.83",
|
|
43
43
|
"@bobfrankston/iflow-direct": "^0.1.65",
|
|
44
44
|
"@bobfrankston/tcp-transport": "^0.1.8",
|