@bobfrankston/mailx 1.0.382 → 1.0.383
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.383",
|
|
4
4
|
"description": "Local-first email client with IMAP sync and standalone native app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/mailx.js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@bobfrankston/tcp-transport": "^0.1.4",
|
|
40
40
|
"@bobfrankston/node-tcp-transport": "^0.1.4",
|
|
41
41
|
"@bobfrankston/smtp-direct": "^0.1.4",
|
|
42
|
-
"@bobfrankston/mailx-sync": "^0.1.
|
|
42
|
+
"@bobfrankston/mailx-sync": "^0.1.9"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/mailparser": "^3.4.6"
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
"@bobfrankston/tcp-transport": "^0.1.4",
|
|
104
104
|
"@bobfrankston/node-tcp-transport": "^0.1.4",
|
|
105
105
|
"@bobfrankston/smtp-direct": "^0.1.4",
|
|
106
|
-
"@bobfrankston/mailx-sync": "^0.1.
|
|
106
|
+
"@bobfrankston/mailx-sync": "^0.1.9"
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
}
|
|
@@ -227,6 +227,20 @@ export declare class ImapManager extends EventEmitter {
|
|
|
227
227
|
* prefetch session alongside any still in flight, blowing through Gmail's
|
|
228
228
|
* per-minute quota and racing on disk writes. One prefetch per account. */
|
|
229
229
|
private prefetchingAccounts;
|
|
230
|
+
/** Per-folder error cooldowns — `accountId:folderPath` → [timestamps].
|
|
231
|
+
* Used to skip folders that repeatedly time out (Dovecot on slow shared
|
|
232
|
+
* hosting SELECTs big `_Spam` / archive folders at 300s+ latency; the
|
|
233
|
+
* prefetch error budget was burning out on a handful of bad folders
|
|
234
|
+
* before the INBOX could finish). A folder with 2+ errors in the last
|
|
235
|
+
* 15 minutes is skipped until the cooldown passes. User-reported via
|
|
236
|
+
* log analysis 2026-04-23: bobma prefetch timing out on Added2.organizations,
|
|
237
|
+
* Added2.technews, _Spam, "Prefirst.Jerry's Retreat", Added2.zines. */
|
|
238
|
+
private folderErrorCooldown;
|
|
239
|
+
private readonly FOLDER_ERROR_WINDOW_MS;
|
|
240
|
+
private readonly FOLDER_ERROR_THRESHOLD;
|
|
241
|
+
private shouldSkipFolder;
|
|
242
|
+
private recordFolderError;
|
|
243
|
+
private clearFolderErrors;
|
|
230
244
|
private prefetchBodies;
|
|
231
245
|
private _prefetchBodies;
|
|
232
246
|
/** Get the body store for direct access */
|
|
@@ -1925,6 +1925,34 @@ export class ImapManager extends EventEmitter {
|
|
|
1925
1925
|
* prefetch session alongside any still in flight, blowing through Gmail's
|
|
1926
1926
|
* per-minute quota and racing on disk writes. One prefetch per account. */
|
|
1927
1927
|
prefetchingAccounts = new Set();
|
|
1928
|
+
/** Per-folder error cooldowns — `accountId:folderPath` → [timestamps].
|
|
1929
|
+
* Used to skip folders that repeatedly time out (Dovecot on slow shared
|
|
1930
|
+
* hosting SELECTs big `_Spam` / archive folders at 300s+ latency; the
|
|
1931
|
+
* prefetch error budget was burning out on a handful of bad folders
|
|
1932
|
+
* before the INBOX could finish). A folder with 2+ errors in the last
|
|
1933
|
+
* 15 minutes is skipped until the cooldown passes. User-reported via
|
|
1934
|
+
* log analysis 2026-04-23: bobma prefetch timing out on Added2.organizations,
|
|
1935
|
+
* Added2.technews, _Spam, "Prefirst.Jerry's Retreat", Added2.zines. */
|
|
1936
|
+
folderErrorCooldown = new Map();
|
|
1937
|
+
FOLDER_ERROR_WINDOW_MS = 15 * 60_000;
|
|
1938
|
+
FOLDER_ERROR_THRESHOLD = 2;
|
|
1939
|
+
shouldSkipFolder(accountId, folderPath) {
|
|
1940
|
+
const key = `${accountId}:${folderPath}`;
|
|
1941
|
+
const now = Date.now();
|
|
1942
|
+
const errors = (this.folderErrorCooldown.get(key) || [])
|
|
1943
|
+
.filter(t => now - t < this.FOLDER_ERROR_WINDOW_MS);
|
|
1944
|
+
this.folderErrorCooldown.set(key, errors);
|
|
1945
|
+
return errors.length >= this.FOLDER_ERROR_THRESHOLD;
|
|
1946
|
+
}
|
|
1947
|
+
recordFolderError(accountId, folderPath) {
|
|
1948
|
+
const key = `${accountId}:${folderPath}`;
|
|
1949
|
+
const arr = this.folderErrorCooldown.get(key) || [];
|
|
1950
|
+
arr.push(Date.now());
|
|
1951
|
+
this.folderErrorCooldown.set(key, arr);
|
|
1952
|
+
}
|
|
1953
|
+
clearFolderErrors(accountId, folderPath) {
|
|
1954
|
+
this.folderErrorCooldown.delete(`${accountId}:${folderPath}`);
|
|
1955
|
+
}
|
|
1928
1956
|
async prefetchBodies(accountId) {
|
|
1929
1957
|
if (this.prefetchingAccounts.has(accountId))
|
|
1930
1958
|
return;
|
|
@@ -2061,10 +2089,26 @@ export class ImapManager extends EventEmitter {
|
|
|
2061
2089
|
let client = null;
|
|
2062
2090
|
try {
|
|
2063
2091
|
client = await this.createClientWithLimit(accountId);
|
|
2064
|
-
|
|
2092
|
+
// INBOX-first ordering so the folder the user actually looks at
|
|
2093
|
+
// gets its bodies even if a later folder eats the error budget.
|
|
2094
|
+
const orderedFolders = Array.from(byFolder.entries()).sort(([aid], [bid]) => {
|
|
2095
|
+
const af = folders.find(f => f.id === aid);
|
|
2096
|
+
const bf = folders.find(f => f.id === bid);
|
|
2097
|
+
const ai = af?.specialUse === "inbox" ? 0 : 1;
|
|
2098
|
+
const bi = bf?.specialUse === "inbox" ? 0 : 1;
|
|
2099
|
+
return ai - bi;
|
|
2100
|
+
});
|
|
2101
|
+
for (const [folderId, uids] of orderedFolders) {
|
|
2065
2102
|
const folder = folders.find(f => f.id === folderId);
|
|
2066
2103
|
if (!folder)
|
|
2067
2104
|
continue;
|
|
2105
|
+
// Skip folders that have repeatedly timed out — keeps one
|
|
2106
|
+
// slow folder from burning the whole error budget and
|
|
2107
|
+
// starving the folder the user is actually looking at.
|
|
2108
|
+
if (this.shouldSkipFolder(accountId, folder.path)) {
|
|
2109
|
+
console.log(` [prefetch] ${accountId}: skipping ${folder.path} (recent timeouts — cooling down)`);
|
|
2110
|
+
continue;
|
|
2111
|
+
}
|
|
2068
2112
|
const received = new Set();
|
|
2069
2113
|
// onBody fires synchronously as each message streams in from the server.
|
|
2070
2114
|
// Disk/DB writes are kicked off fire-and-forget; we await them after the
|
|
@@ -2091,10 +2135,13 @@ export class ImapManager extends EventEmitter {
|
|
|
2091
2135
|
})());
|
|
2092
2136
|
});
|
|
2093
2137
|
batchSucceeded = true;
|
|
2138
|
+
// Folder responded — clear its error history.
|
|
2139
|
+
this.clearFolderErrors(accountId, folder.path);
|
|
2094
2140
|
}
|
|
2095
2141
|
catch (e) {
|
|
2096
2142
|
console.error(` [prefetch] ${accountId} folder ${folder.path}: batch fetch failed: ${e.message}`);
|
|
2097
2143
|
counters.errors++;
|
|
2144
|
+
this.recordFolderError(accountId, folder.path);
|
|
2098
2145
|
if (counters.errors >= ERROR_BUDGET)
|
|
2099
2146
|
break;
|
|
2100
2147
|
}
|