@bobfrankston/mailx 1.0.169 → 1.0.171

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.169",
3
+ "version": "1.0.171",
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.1",
25
25
  "@bobfrankston/miscinfo": "^1.0.7",
26
26
  "@bobfrankston/oauthsupport": "^1.0.20",
27
- "@bobfrankston/msger": "^0.1.219",
27
+ "@bobfrankston/msger": "^0.1.220",
28
28
  "@capacitor/android": "^8.3.0",
29
29
  "@capacitor/cli": "^8.3.0",
30
30
  "@capacitor/core": "^8.3.0",
@@ -6,7 +6,7 @@
6
6
  import { createAutoImapConfig, CompatImapClient } from "@bobfrankston/iflow-direct";
7
7
  import { authenticateOAuth } from "@bobfrankston/oauthsupport";
8
8
  import { FileMessageStore } from "@bobfrankston/mailx-store";
9
- import { loadSettings, getStorePath, getConfigDir, getHistoryDays } from "@bobfrankston/mailx-settings";
9
+ import { loadSettings, getStorePath, getConfigDir, getHistoryDays, getPrefetch } from "@bobfrankston/mailx-settings";
10
10
  import { EventEmitter } from "node:events";
11
11
  import * as fs from "node:fs";
12
12
  import * as path from "node:path";
@@ -412,7 +412,7 @@ export class ImapManager extends EventEmitter {
412
412
  return dbFolders;
413
413
  }
414
414
  /** Store a batch of messages to DB immediately — used by onChunk for incremental sync */
415
- storeMessages(accountId, folderId, folder, msgs, highestUid) {
415
+ async storeMessages(accountId, folderId, folder, msgs, highestUid) {
416
416
  let stored = 0;
417
417
  this.db.beginTransaction();
418
418
  try {
@@ -426,7 +426,14 @@ export class ImapManager extends EventEmitter {
426
426
  continue; // already have it
427
427
  const source = msg.source || "";
428
428
  let bodyPath = "";
429
- // Skip body storage during sync — bodies fetched on demand
429
+ let preview = "";
430
+ let hasAttachments = false;
431
+ if (source) {
432
+ bodyPath = await this.bodyStore.putMessage(accountId, folderId, msg.uid, Buffer.from(source, "utf-8"));
433
+ const parsed = await extractPreview(source);
434
+ preview = parsed.preview;
435
+ hasAttachments = parsed.hasAttachments;
436
+ }
430
437
  const flags = [];
431
438
  if (msg.seen)
432
439
  flags.push("\\Seen");
@@ -444,7 +451,7 @@ export class ImapManager extends EventEmitter {
444
451
  from: toEmailAddress(msg.from?.[0] || {}),
445
452
  to: toEmailAddresses(msg.to || []),
446
453
  cc: toEmailAddresses(msg.cc || []),
447
- flags, size: msg.size || 0, hasAttachments: false, preview: "", bodyPath
454
+ flags, size: msg.size || 0, hasAttachments, preview, bodyPath
448
455
  });
449
456
  stored++;
450
457
  }
@@ -460,6 +467,7 @@ export class ImapManager extends EventEmitter {
460
467
  async syncFolder(accountId, folderId, client) {
461
468
  if (!client)
462
469
  client = this.createClient(accountId);
470
+ const prefetch = getPrefetch();
463
471
  const folders = this.db.getFolders(accountId);
464
472
  const folder = folders.find(f => f.id === folderId);
465
473
  if (!folder)
@@ -477,8 +485,8 @@ export class ImapManager extends EventEmitter {
477
485
  ? new Date(Date.now() - effectiveDays * 86400000)
478
486
  : new Date(0);
479
487
  if (highestUid > 0) {
480
- // Incremental: fetch new messages — metadata only for speed, bodies on demand
481
- const fetched = await client.fetchMessagesSinceUid(folder.path, highestUid, { source: false });
488
+ // Incremental: fetch new messages — prefetch bodies for offline access
489
+ const fetched = await client.fetchMessagesSinceUid(folder.path, highestUid, { source: prefetch });
482
490
  // Filter out the last known message (IMAP * always returns at least one)
483
491
  messages = fetched.filter((m) => m.uid > highestUid);
484
492
  // Gap detection: check for missing UIDs within the range we've already synced
@@ -499,7 +507,7 @@ export class ImapManager extends EventEmitter {
499
507
  for (let i = 0; i < missingUids.length; i += chunkSize) {
500
508
  const chunk = missingUids.slice(i, i + chunkSize);
501
509
  const range = chunk.join(",");
502
- const recovered = await client.fetchMessages(folder.path, range, { source: false });
510
+ const recovered = await client.fetchMessages(folder.path, range, { source: prefetch });
503
511
  messages.push(...recovered);
504
512
  }
505
513
  }
@@ -515,7 +523,7 @@ export class ImapManager extends EventEmitter {
515
523
  const oldestDate = this.db.getOldestDate(accountId, folderId);
516
524
  if (oldestDate > 0 && startDate.getTime() < oldestDate) {
517
525
  const existingUids = new Set(this.db.getUidsForFolder(accountId, folderId));
518
- const backfill = await client.fetchMessageByDate(folder.path, startDate, new Date(oldestDate), { source: false });
526
+ const backfill = await client.fetchMessageByDate(folder.path, startDate, new Date(oldestDate), { source: prefetch });
519
527
  const newBackfill = backfill.filter((m) => !existingUids.has(m.uid));
520
528
  if (newBackfill.length > 0) {
521
529
  console.log(` ${folder.path}: backfilling ${newBackfill.length} older messages`);
@@ -526,8 +534,8 @@ export class ImapManager extends EventEmitter {
526
534
  else {
527
535
  // First sync: fetch in chunks, store each chunk immediately for instant UI
528
536
  let totalStored = 0;
529
- const onChunk = (chunk) => {
530
- const stored = this.storeMessages(accountId, folderId, folder, chunk, highestUid);
537
+ const onChunk = async (chunk) => {
538
+ const stored = await this.storeMessages(accountId, folderId, folder, chunk, highestUid);
531
539
  totalStored += stored;
532
540
  if (stored > 0) {
533
541
  this.db.recalcFolderCounts(folderId);
@@ -535,7 +543,7 @@ export class ImapManager extends EventEmitter {
535
543
  }
536
544
  };
537
545
  const tomorrow = new Date(Date.now() + 86400000); // IMAP BEFORE is exclusive
538
- messages = await client.fetchMessageByDate(folder.path, startDate, tomorrow, { source: false }, onChunk);
546
+ messages = await client.fetchMessageByDate(folder.path, startDate, tomorrow, { source: prefetch }, onChunk);
539
547
  if (totalStored > 0) {
540
548
  console.log(` ${folder.path}: ${totalStored} messages (streamed)`);
541
549
  this.db.recalcFolderCounts(folderId);
@@ -695,19 +703,34 @@ export class ImapManager extends EventEmitter {
695
703
  const t0 = Date.now();
696
704
  const folders = await this.syncFolders(accountId, client);
697
705
  console.log(` [timing] ${accountId}: folder list ${Date.now() - t0}ms (${folders.length} folders)`);
698
- // Step 2: Sync INBOX first
706
+ // Step 2: Sync INBOX first — keep retrying on failure (most important folder)
699
707
  const inbox = folders.find(f => f.specialUse === "inbox");
700
708
  if (inbox) {
701
709
  console.log(` [sync] ${accountId}: starting INBOX sync (folder ${inbox.id})`);
702
- try {
703
- client = await this.getOpsClient(accountId);
704
- console.log(` [sync] ${accountId}: got client, calling syncFolder for INBOX`);
705
- await this.syncFolder(accountId, inbox.id, client);
706
- console.log(` [sync] ${accountId}: INBOX sync complete`);
710
+ const maxAttempts = 5;
711
+ let inboxDone = false;
712
+ for (let attempt = 1; attempt <= maxAttempts && !inboxDone; attempt++) {
713
+ try {
714
+ client = await this.getOpsClient(accountId);
715
+ if (attempt > 1)
716
+ console.log(` [sync] ${accountId}: INBOX retry #${attempt}`);
717
+ await this.syncFolder(accountId, inbox.id, client);
718
+ console.log(` [sync] ${accountId}: INBOX sync complete`);
719
+ inboxDone = true;
720
+ }
721
+ catch (e) {
722
+ console.error(` Inbox sync error for ${accountId} (attempt ${attempt}/${maxAttempts}): ${e.message}`);
723
+ await this.reconnectOps(accountId);
724
+ if (attempt < maxAttempts) {
725
+ const delay = Math.min(attempt * 5000, 15000);
726
+ console.log(` [sync] ${accountId}: waiting ${delay / 1000}s before INBOX retry`);
727
+ await new Promise(r => setTimeout(r, delay));
728
+ }
729
+ }
707
730
  }
708
- catch (e) {
709
- console.error(` Inbox sync error for ${accountId}: ${e.message}`);
710
- await this.reconnectOps(accountId);
731
+ if (!inboxDone) {
732
+ console.error(` [sync] ${accountId}: INBOX failed after ${maxAttempts} attempts — will retry next sync cycle`);
733
+ this.emit("syncError", accountId, `INBOX sync failed after ${maxAttempts} attempts`);
711
734
  }
712
735
  }
713
736
  else {
@@ -42,6 +42,7 @@ declare const DEFAULT_PREFERENCES: {
42
42
  sync: {
43
43
  intervalMinutes: number;
44
44
  historyDays: number;
45
+ prefetch: boolean;
45
46
  };
46
47
  autocomplete: {
47
48
  enabled: boolean;
@@ -99,5 +100,7 @@ export declare function initCloudConfig(provider?: "gdrive"): Promise<void>;
99
100
  declare const DEFAULT_SETTINGS: MailxSettings;
100
101
  /** Get historyDays for an account: per-account override > system override > shared default */
101
102
  export declare function getHistoryDays(accountId?: string): number;
103
+ /** Get prefetch setting: download bodies during sync (default true) */
104
+ export declare function getPrefetch(): boolean;
102
105
  export { DEFAULT_SETTINGS, DEFAULT_ALLOWLIST, DEFAULT_PREFERENCES, DEFAULT_AUTOCOMPLETE, LOCAL_DIR };
103
106
  //# sourceMappingURL=index.d.ts.map
@@ -338,6 +338,7 @@ const DEFAULT_PREFERENCES = {
338
338
  sync: {
339
339
  intervalMinutes: 5,
340
340
  historyDays: 30,
341
+ prefetch: true,
341
342
  },
342
343
  autocomplete: {
343
344
  enabled: false,
@@ -633,5 +634,10 @@ export function getHistoryDays(accountId) {
633
634
  const prefs = loadPreferences();
634
635
  return prefs.sync.historyDays || 0;
635
636
  }
637
+ /** Get prefetch setting: download bodies during sync (default true) */
638
+ export function getPrefetch() {
639
+ const prefs = loadPreferences();
640
+ return prefs.sync.prefetch !== false;
641
+ }
636
642
  export { DEFAULT_SETTINGS, DEFAULT_ALLOWLIST, DEFAULT_PREFERENCES, DEFAULT_AUTOCOMPLETE, LOCAL_DIR };
637
643
  //# sourceMappingURL=index.js.map
@@ -191,6 +191,7 @@ export interface MailxSettings {
191
191
  sync: {
192
192
  intervalMinutes: number;
193
193
  historyDays: number; /** 0 = all history */
194
+ prefetch: boolean; /** Download message bodies during sync (default true) */
194
195
  };
195
196
  store: {
196
197
  basePath: string; /** Where message bodies are stored */