@bobfrankston/mailx 1.0.241 → 1.0.243

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.
@@ -1 +1 @@
1
- {"height":1344,"width":2151,"x":524,"y":191}
1
+ {"height":1344,"width":2151,"x":419,"y":152}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.241",
3
+ "version": "1.0.243",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -20,11 +20,11 @@
20
20
  "postinstall": "node bin/postinstall.js"
21
21
  },
22
22
  "dependencies": {
23
- "@bobfrankston/iflow-direct": "^0.1.11",
23
+ "@bobfrankston/iflow-direct": "^0.1.12",
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.303",
27
+ "@bobfrankston/msger": "^0.1.305",
28
28
  "@capacitor/android": "^8.3.0",
29
29
  "@capacitor/cli": "^8.3.0",
30
30
  "@capacitor/core": "^8.3.0",
@@ -74,11 +74,11 @@
74
74
  },
75
75
  ".transformedSnapshot": {
76
76
  "dependencies": {
77
- "@bobfrankston/iflow-direct": "^0.1.11",
77
+ "@bobfrankston/iflow-direct": "^0.1.12",
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.303",
81
+ "@bobfrankston/msger": "^0.1.305",
82
82
  "@capacitor/android": "^8.3.0",
83
83
  "@capacitor/cli": "^8.3.0",
84
84
  "@capacitor/core": "^8.3.0",
@@ -788,15 +788,21 @@ export class ImapManager extends EventEmitter {
788
788
  }
789
789
  async _syncAll() {
790
790
  const priorityOrder = ["sent", "drafts", "archive", "junk", "trash"];
791
- // Sync all accounts in parallel — each manages its own connection
792
- const syncPromises = [...this.configs.keys()].map(accountId => this.syncAccount(accountId, priorityOrder));
793
- await Promise.allSettled(syncPromises);
794
- // Background body prefetch after sync, fetch bodies for messages that don't have them
795
- if (getPrefetch()) {
796
- for (const accountId of this.configs.keys()) {
791
+ // Sync all accounts in parallel — each manages its own connection.
792
+ // Prefetch runs per-account immediately after that account's sync
793
+ // completes, NOT after all accounts finish. This way a slow account
794
+ // (bobma with 300s timeouts) doesn't block prefetch for a fast account
795
+ // (Gmail). The old code put prefetch after `allSettled`, but syncAll
796
+ // has a 10-minute wall-clock timeout that killed it first — so
797
+ // prefetch never ran.
798
+ const syncAndPrefetch = async (accountId) => {
799
+ await this.syncAccount(accountId, priorityOrder);
800
+ if (getPrefetch()) {
797
801
  this.prefetchBodies(accountId).catch(e => console.error(` [prefetch] ${accountId}: ${e.message}`));
798
802
  }
799
- }
803
+ };
804
+ const syncPromises = [...this.configs.keys()].map(syncAndPrefetch);
805
+ await Promise.allSettled(syncPromises);
800
806
  }
801
807
  /** Sync a single account — manages its own connection lifecycle */
802
808
  async syncAccount(accountId, priorityOrder) {
@@ -1424,7 +1430,13 @@ export class ImapManager extends EventEmitter {
1424
1430
  let totalFetched = 0;
1425
1431
  let deleted = 0;
1426
1432
  let errors = 0;
1433
+ let rateLimited = false;
1427
1434
  const ERROR_BUDGET = 20;
1435
+ // Pace body fetches to avoid slamming Gmail's rate limit. Without a
1436
+ // delay, 500+ body-fetch API calls fire in a burst, every one hits 429,
1437
+ // and the error budget drains before any bodies land.
1438
+ const FETCH_DELAY_MS = this.isGmailAccount(accountId) ? 1000 : 200;
1439
+ const RATE_LIMIT_PAUSE_MS = 30000;
1428
1440
  while (true) {
1429
1441
  const missing = this.db.getMessagesWithoutBody(accountId, 100);
1430
1442
  if (missing.length === 0)
@@ -1433,6 +1445,15 @@ export class ImapManager extends EventEmitter {
1433
1445
  console.log(` [prefetch] ${accountId}: ${missing.length}+ bodies to fetch`);
1434
1446
  let madeProgress = false;
1435
1447
  for (const msg of missing) {
1448
+ // If we hit a rate limit, pause before the next fetch
1449
+ if (rateLimited) {
1450
+ console.log(` [prefetch] ${accountId}: rate-limited — pausing ${RATE_LIMIT_PAUSE_MS / 1000}s`);
1451
+ await new Promise(r => setTimeout(r, RATE_LIMIT_PAUSE_MS));
1452
+ rateLimited = false;
1453
+ }
1454
+ else if (FETCH_DELAY_MS > 0) {
1455
+ await new Promise(r => setTimeout(r, FETCH_DELAY_MS));
1456
+ }
1436
1457
  try {
1437
1458
  const result = await this.fetchMessageBody(accountId, msg.folderId, msg.uid);
1438
1459
  if (result) {
@@ -1454,7 +1475,15 @@ export class ImapManager extends EventEmitter {
1454
1475
  catch { /* ignore */ }
1455
1476
  continue;
1456
1477
  }
1457
- errors++;
1478
+ // If the error is a rate limit (429), don't count against
1479
+ // the budget — just slow down. The API will accept requests
1480
+ // again after a brief pause.
1481
+ if (/429|rate|too many/i.test(String(e?.message || ""))) {
1482
+ rateLimited = true;
1483
+ }
1484
+ else {
1485
+ errors++;
1486
+ }
1458
1487
  if (errors >= ERROR_BUDGET) {
1459
1488
  console.error(` [prefetch] ${accountId}: stopping after ${errors} errors (${totalFetched} cached, ${deleted} pruned)`);
1460
1489
  return;