@bobfrankston/mailx 1.0.236 → 1.0.237

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.236",
3
+ "version": "1.0.237",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -24,7 +24,7 @@
24
24
  "@bobfrankston/iflow-node": "^0.1.2",
25
25
  "@bobfrankston/miscinfo": "^1.0.8",
26
26
  "@bobfrankston/oauthsupport": "^1.0.22",
27
- "@bobfrankston/msger": "^0.1.298",
27
+ "@bobfrankston/msger": "^0.1.299",
28
28
  "@capacitor/android": "^8.3.0",
29
29
  "@capacitor/cli": "^8.3.0",
30
30
  "@capacitor/core": "^8.3.0",
@@ -78,7 +78,7 @@
78
78
  "@bobfrankston/iflow-node": "^0.1.2",
79
79
  "@bobfrankston/miscinfo": "^1.0.8",
80
80
  "@bobfrankston/oauthsupport": "^1.0.22",
81
- "@bobfrankston/msger": "^0.1.298",
81
+ "@bobfrankston/msger": "^0.1.299",
82
82
  "@capacitor/android": "^8.3.0",
83
83
  "@capacitor/cli": "^8.3.0",
84
84
  "@capacitor/core": "^8.3.0",
@@ -137,7 +137,11 @@ export declare class ImapManager extends EventEmitter {
137
137
  * (deleted from another device, for example). The caller uses that to
138
138
  * delete the stale row locally instead of showing a generic error. */
139
139
  private fetchMessageBodyViaApi;
140
- /** Background body prefetch — download bodies for messages that don't have them */
140
+ /** Background body prefetch — download bodies for messages that don't have them.
141
+ * Server-side deletions (isNotFound) aren't errors here: we delete the
142
+ * stale row locally and keep going. Only unrelated errors (network,
143
+ * auth, rate limits) count against the error budget, and the budget is
144
+ * generous so a few transient failures don't kill the whole run. */
141
145
  private prefetchBodies;
142
146
  /** Get the body store for direct access */
143
147
  getBodyStore(): FileMessageStore;
@@ -1402,34 +1402,60 @@ export class ImapManager extends EventEmitter {
1402
1402
  return null;
1403
1403
  }
1404
1404
  }
1405
- /** Background body prefetch — download bodies for messages that don't have them */
1405
+ /** Background body prefetch — download bodies for messages that don't have them.
1406
+ * Server-side deletions (isNotFound) aren't errors here: we delete the
1407
+ * stale row locally and keep going. Only unrelated errors (network,
1408
+ * auth, rate limits) count against the error budget, and the budget is
1409
+ * generous so a few transient failures don't kill the whole run. */
1406
1410
  async prefetchBodies(accountId) {
1407
- // Fetch ALL missing bodies in one pass — don't wait for next sync cycle
1408
1411
  let totalFetched = 0;
1412
+ let deleted = 0;
1409
1413
  let errors = 0;
1414
+ const ERROR_BUDGET = 20;
1410
1415
  while (true) {
1411
1416
  const missing = this.db.getMessagesWithoutBody(accountId, 100);
1412
1417
  if (missing.length === 0)
1413
1418
  break;
1414
- if (totalFetched === 0)
1419
+ if (totalFetched === 0 && deleted === 0)
1415
1420
  console.log(` [prefetch] ${accountId}: ${missing.length}+ bodies to fetch`);
1421
+ let madeProgress = false;
1416
1422
  for (const msg of missing) {
1417
1423
  try {
1418
1424
  const result = await this.fetchMessageBody(accountId, msg.folderId, msg.uid);
1419
- if (result)
1425
+ if (result) {
1420
1426
  totalFetched++;
1427
+ madeProgress = true;
1428
+ }
1421
1429
  }
1422
1430
  catch (e) {
1431
+ if (e?.isNotFound) {
1432
+ // Message deleted on the server — drop the stale row so
1433
+ // we stop re-asking. This also moves the loop forward
1434
+ // (next getMessagesWithoutBody call won't return it).
1435
+ try {
1436
+ this.db.deleteMessage(accountId, msg.uid);
1437
+ this.bodyStore.deleteMessage(accountId, msg.folderId, msg.uid).catch(() => { });
1438
+ deleted++;
1439
+ madeProgress = true;
1440
+ }
1441
+ catch { /* ignore */ }
1442
+ continue;
1443
+ }
1423
1444
  errors++;
1424
- if (errors >= 3) {
1425
- console.error(` [prefetch] ${accountId}: stopping after ${errors} errors (${totalFetched} cached)`);
1445
+ if (errors >= ERROR_BUDGET) {
1446
+ console.error(` [prefetch] ${accountId}: stopping after ${errors} errors (${totalFetched} cached, ${deleted} pruned)`);
1426
1447
  return;
1427
1448
  }
1428
1449
  }
1429
1450
  }
1451
+ // Safety: if we made zero progress this iteration, bail — otherwise
1452
+ // we'd loop forever on rows that keep failing without isNotFound.
1453
+ if (!madeProgress)
1454
+ break;
1455
+ }
1456
+ if (totalFetched > 0 || deleted > 0) {
1457
+ console.log(` [prefetch] ${accountId}: ${totalFetched} bodies cached, ${deleted} stale rows pruned (done)`);
1430
1458
  }
1431
- if (totalFetched > 0)
1432
- console.log(` [prefetch] ${accountId}: ${totalFetched} bodies cached (done)`);
1433
1459
  }
1434
1460
  /** Get the body store for direct access */
1435
1461
  getBodyStore() {