@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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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 —
|
|
481
|
-
const fetched = await client.fetchMessagesSinceUid(folder.path, highestUid, { source:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
-
|
|
709
|
-
console.error(`
|
|
710
|
-
|
|
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 */
|