@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.382",
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.8"
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.8"
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
- for (const [folderId, uids] of byFolder) {
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
  }