@bobfrankston/mailx 1.0.167 → 1.0.168
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/bin/mailx.js +1 -1
- package/client/components/message-list.js +2 -0
- package/package.json +2 -2
- package/packages/mailx-core/index.d.ts +1 -1
- package/packages/mailx-core/index.js +2 -2
- package/packages/mailx-imap/index.d.ts +1 -1
- package/packages/mailx-imap/index.js +23 -13
- package/packages/mailx-service/index.d.ts +1 -1
- package/packages/mailx-service/index.js +4 -4
- package/packages/mailx-settings/cloud.js +1 -1
- package/packages/mailx-settings/index.d.ts +5 -4
- package/packages/mailx-settings/index.js +42 -6
package/bin/mailx.js
CHANGED
|
@@ -201,7 +201,7 @@ if (importMode) {
|
|
|
201
201
|
const wrapper = { accounts: merged };
|
|
202
202
|
if (data?.name)
|
|
203
203
|
wrapper.name = data.name;
|
|
204
|
-
saveAccounts(merged);
|
|
204
|
+
await saveAccounts(merged);
|
|
205
205
|
console.log(`Saved ${merged.length} account(s). Run 'mailx' to start.`);
|
|
206
206
|
process.exit(0);
|
|
207
207
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.168",
|
|
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.218",
|
|
28
28
|
"@capacitor/android": "^8.3.0",
|
|
29
29
|
"@capacitor/cli": "^8.3.0",
|
|
30
30
|
"@capacitor/core": "^8.3.0",
|
|
@@ -113,7 +113,7 @@ export declare function getSyncPending(): {
|
|
|
113
113
|
export declare function allowRemoteContent(params: {
|
|
114
114
|
type: string;
|
|
115
115
|
value: string;
|
|
116
|
-
}): void
|
|
116
|
+
}): Promise<void>;
|
|
117
117
|
export declare function getSettings(): import("@bobfrankston/mailx-types").MailxSettings;
|
|
118
118
|
export declare function saveSettingsData(data: any): void;
|
|
119
119
|
export declare function rebuildSearchIndex(): number;
|
|
@@ -312,7 +312,7 @@ export async function syncAll() {
|
|
|
312
312
|
export function getSyncPending() {
|
|
313
313
|
return { pending: db.getTotalPendingSyncCount() };
|
|
314
314
|
}
|
|
315
|
-
export function allowRemoteContent(params) {
|
|
315
|
+
export async function allowRemoteContent(params) {
|
|
316
316
|
const list = loadAllowlist();
|
|
317
317
|
if (params.type === "sender" && !list.senders.includes(params.value))
|
|
318
318
|
list.senders.push(params.value);
|
|
@@ -324,7 +324,7 @@ export function allowRemoteContent(params) {
|
|
|
324
324
|
if (!list.recipients.includes(params.value))
|
|
325
325
|
list.recipients.push(params.value);
|
|
326
326
|
}
|
|
327
|
-
saveAllowlist(list);
|
|
327
|
+
await saveAllowlist(list);
|
|
328
328
|
}
|
|
329
329
|
export function getSettings() {
|
|
330
330
|
return loadSettings();
|
|
@@ -94,7 +94,7 @@ export declare class ImapManager extends EventEmitter {
|
|
|
94
94
|
* If message count changed, triggers inbox sync for that account. */
|
|
95
95
|
private lastInboxCounts;
|
|
96
96
|
private quickCheckRunning;
|
|
97
|
-
/** Check a single account's inbox */
|
|
97
|
+
/** Check a single account's inbox — uses its own connection, never blocked by sync */
|
|
98
98
|
quickInboxCheckAccount(accountId: string): Promise<void>;
|
|
99
99
|
/** Check all accounts (used by legacy callers) */
|
|
100
100
|
quickInboxCheck(): Promise<void>;
|
|
@@ -323,7 +323,7 @@ export class ImapManager extends EventEmitter {
|
|
|
323
323
|
if (!fs.existsSync(credPath)) {
|
|
324
324
|
try {
|
|
325
325
|
const pkgDir = path.dirname(import.meta.resolve("@bobfrankston/iflow-direct").replace("file:///", "").replace("file://", ""));
|
|
326
|
-
for (const name of ["
|
|
326
|
+
for (const name of ["iflow-credentials.json"]) {
|
|
327
327
|
const p = path.join(pkgDir, name);
|
|
328
328
|
if (fs.existsSync(p)) {
|
|
329
329
|
credPath = p;
|
|
@@ -338,6 +338,7 @@ export class ImapManager extends EventEmitter {
|
|
|
338
338
|
const result = await authenticateOAuth(credPath, {
|
|
339
339
|
scope: "https://mail.google.com/ https://www.googleapis.com/auth/contacts.readonly",
|
|
340
340
|
tokenDirectory: tokenDir,
|
|
341
|
+
credentialsKey: "installed",
|
|
341
342
|
loginHint: account.imap.user,
|
|
342
343
|
});
|
|
343
344
|
return result?.access_token || "";
|
|
@@ -825,31 +826,39 @@ export class ImapManager extends EventEmitter {
|
|
|
825
826
|
* If message count changed, triggers inbox sync for that account. */
|
|
826
827
|
lastInboxCounts = new Map();
|
|
827
828
|
quickCheckRunning = new Set(); // per-account guard
|
|
828
|
-
/** Check a single account's inbox */
|
|
829
|
+
/** Check a single account's inbox — uses its own connection, never blocked by sync */
|
|
829
830
|
async quickInboxCheckAccount(accountId) {
|
|
830
|
-
if (this.quickCheckRunning.has(accountId)
|
|
831
|
+
if (this.quickCheckRunning.has(accountId))
|
|
831
832
|
return;
|
|
832
833
|
if (this.reauthenticating.has(accountId))
|
|
833
834
|
return;
|
|
834
835
|
this.quickCheckRunning.add(accountId);
|
|
836
|
+
let client = null;
|
|
835
837
|
try {
|
|
836
838
|
const inbox = this.db.getFolders(accountId).find(f => f.specialUse === "inbox");
|
|
837
839
|
if (!inbox)
|
|
838
840
|
return;
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
841
|
+
client = this.newClient(accountId);
|
|
842
|
+
const count = await client.getMessagesCount("INBOX");
|
|
843
|
+
const prev = this.lastInboxCounts.get(accountId) ?? count;
|
|
844
|
+
this.lastInboxCounts.set(accountId, count);
|
|
845
|
+
if (count !== prev) {
|
|
846
|
+
console.log(` [check] ${accountId} INBOX: ${prev} → ${count}`);
|
|
847
|
+
await this.syncFolder(accountId, inbox.id, client);
|
|
848
|
+
}
|
|
849
|
+
await client.logout();
|
|
850
|
+
client = null;
|
|
848
851
|
}
|
|
849
852
|
catch {
|
|
850
853
|
// Lightweight check — silently ignore errors
|
|
851
854
|
}
|
|
852
855
|
finally {
|
|
856
|
+
if (client) {
|
|
857
|
+
try {
|
|
858
|
+
await client.logout();
|
|
859
|
+
}
|
|
860
|
+
catch { /* */ }
|
|
861
|
+
}
|
|
853
862
|
this.quickCheckRunning.delete(accountId);
|
|
854
863
|
}
|
|
855
864
|
}
|
|
@@ -867,7 +876,8 @@ export class ImapManager extends EventEmitter {
|
|
|
867
876
|
// Password (Dovecot etc): every 60s — conservative, 20-connection limit
|
|
868
877
|
// IDLE gives instant notification when working; STATUS is the fallback.
|
|
869
878
|
for (const [accountId] of this.configs) {
|
|
870
|
-
const
|
|
879
|
+
const QUICK_CHECK_INTERVAL = 2500; // STATUS INBOX is one command, very cheap
|
|
880
|
+
const interval = QUICK_CHECK_INTERVAL;
|
|
871
881
|
const timer = setInterval(() => {
|
|
872
882
|
this.quickInboxCheckAccount(accountId).catch(() => { });
|
|
873
883
|
}, interval);
|
|
@@ -20,7 +20,7 @@ export declare class MailxService {
|
|
|
20
20
|
getMessages(accountId: string, folderId: number, page?: number, pageSize?: number, sort?: string, sortDir?: string, search?: string): any;
|
|
21
21
|
getMessage(accountId: string, uid: number, allowRemote?: boolean, folderId?: number): Promise<any>;
|
|
22
22
|
updateFlags(accountId: string, uid: number, flags: string[]): Promise<void>;
|
|
23
|
-
allowRemoteContent(type: "sender" | "domain" | "recipient", value: string): void
|
|
23
|
+
allowRemoteContent(type: "sender" | "domain" | "recipient", value: string): Promise<void>;
|
|
24
24
|
search(q: string, page?: number, pageSize?: number, scope?: string, accountId?: string, folderId?: number): Promise<any>;
|
|
25
25
|
rebuildSearchIndex(): number;
|
|
26
26
|
getSyncPending(): {
|
|
@@ -213,7 +213,7 @@ export class MailxService {
|
|
|
213
213
|
await this.imapManager.updateFlagsLocal(accountId, uid, envelope?.folderId || 0, flags);
|
|
214
214
|
}
|
|
215
215
|
// ── Remote content allow-list ──
|
|
216
|
-
allowRemoteContent(type, value) {
|
|
216
|
+
async allowRemoteContent(type, value) {
|
|
217
217
|
const list = loadAllowlist();
|
|
218
218
|
if (type === "sender" && !list.senders.includes(value))
|
|
219
219
|
list.senders.push(value);
|
|
@@ -225,7 +225,7 @@ export class MailxService {
|
|
|
225
225
|
if (!list.recipients.includes(value))
|
|
226
226
|
list.recipients.push(value);
|
|
227
227
|
}
|
|
228
|
-
saveAllowlist(list);
|
|
228
|
+
await saveAllowlist(list);
|
|
229
229
|
console.log(` [allow] Added ${type}: ${value}`);
|
|
230
230
|
}
|
|
231
231
|
// ── Search ──
|
|
@@ -571,7 +571,7 @@ export class MailxService {
|
|
|
571
571
|
account.smtp = { host: detected.smtpHost, port: 587, tls: true, auth: detected.auth, user: email };
|
|
572
572
|
}
|
|
573
573
|
account.id = domain.split(".")[0] || "account";
|
|
574
|
-
saveAccounts([account]);
|
|
574
|
+
await saveAccounts([account]);
|
|
575
575
|
// Re-read normalized settings and register
|
|
576
576
|
const settings = loadSettings();
|
|
577
577
|
for (const acct of settings.accounts) {
|
|
@@ -603,7 +603,7 @@ export class MailxService {
|
|
|
603
603
|
if (restored.length === 0) {
|
|
604
604
|
return { ok: false, error: "Could not parse cached account configs" };
|
|
605
605
|
}
|
|
606
|
-
saveAccounts(restored);
|
|
606
|
+
await saveAccounts(restored);
|
|
607
607
|
for (const acct of restored) {
|
|
608
608
|
try {
|
|
609
609
|
await this.imapManager.addAccount(acct);
|
|
@@ -31,7 +31,7 @@ function findGoogleCredentials() {
|
|
|
31
31
|
let dir = import.meta.dirname;
|
|
32
32
|
for (let i = 0; i < 5; i++) {
|
|
33
33
|
for (const pkg of ["iflow-direct", "iflow"]) {
|
|
34
|
-
for (const name of ["iflow-credentials.json"
|
|
34
|
+
for (const name of ["iflow-credentials.json"]) {
|
|
35
35
|
const p = path.join(dir, "node_modules", "@bobfrankston", pkg, name);
|
|
36
36
|
if (fs.existsSync(p))
|
|
37
37
|
return p;
|
|
@@ -65,7 +65,8 @@ export declare function loadAccounts(): AccountConfig[];
|
|
|
65
65
|
/** Load accounts with cloud API fallback (async — use when cloud settings may not be mounted) */
|
|
66
66
|
export declare function loadAccountsAsync(): Promise<AccountConfig[]>;
|
|
67
67
|
/** Save account configs */
|
|
68
|
-
|
|
68
|
+
/** Save accounts — merges with cloud copy by email (multi-client safe) */
|
|
69
|
+
export declare function saveAccounts(accounts: AccountConfig[]): Promise<void>;
|
|
69
70
|
/** Load preferences (shared + local overrides, with legacy fallback) */
|
|
70
71
|
export declare function loadPreferences(): typeof DEFAULT_PREFERENCES;
|
|
71
72
|
/** Save preferences */
|
|
@@ -76,12 +77,12 @@ export declare function loadAutocomplete(): AutocompleteSettings;
|
|
|
76
77
|
export declare function saveAutocomplete(settings: AutocompleteSettings): void;
|
|
77
78
|
/** Load remote content allow-list */
|
|
78
79
|
export declare function loadAllowlist(): typeof DEFAULT_ALLOWLIST;
|
|
79
|
-
/** Save allow-list */
|
|
80
|
-
export declare function saveAllowlist(list: typeof DEFAULT_ALLOWLIST): void
|
|
80
|
+
/** Save allow-list — merges with existing cloud copy (multi-client safe) */
|
|
81
|
+
export declare function saveAllowlist(list: typeof DEFAULT_ALLOWLIST): Promise<void>;
|
|
81
82
|
/** Load settings — unified view combining all files (backward compatible) */
|
|
82
83
|
export declare function loadSettings(): MailxSettings;
|
|
83
84
|
/** Save settings — writes to split files */
|
|
84
|
-
export declare function saveSettings(settings: MailxSettings): void
|
|
85
|
+
export declare function saveSettings(settings: MailxSettings): Promise<void>;
|
|
85
86
|
/** Get the local store base path */
|
|
86
87
|
export declare function getStorePath(): string;
|
|
87
88
|
/** Get the local data directory (DB, store, etc.) */
|
|
@@ -454,7 +454,26 @@ export async function loadAccountsAsync() {
|
|
|
454
454
|
return [];
|
|
455
455
|
}
|
|
456
456
|
/** Save account configs */
|
|
457
|
-
|
|
457
|
+
/** Save accounts — merges with cloud copy by email (multi-client safe) */
|
|
458
|
+
export async function saveAccounts(accounts) {
|
|
459
|
+
// Merge with cloud: keep all accounts, deduplicate by normalized email
|
|
460
|
+
try {
|
|
461
|
+
const cloudContent = await cloudRead("accounts.jsonc");
|
|
462
|
+
if (cloudContent) {
|
|
463
|
+
const cloud = parseJsonc(cloudContent);
|
|
464
|
+
const cloudAccts = cloud?.accounts || (Array.isArray(cloud) ? cloud : []);
|
|
465
|
+
if (cloudAccts.length > 0) {
|
|
466
|
+
const seen = new Set(accounts.map(a => normalizeEmail(a.email)));
|
|
467
|
+
for (const ca of cloudAccts) {
|
|
468
|
+
if (ca.email && !seen.has(normalizeEmail(ca.email))) {
|
|
469
|
+
accounts.push(ca);
|
|
470
|
+
seen.add(normalizeEmail(ca.email));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
catch { /* cloud read failed — save local version */ }
|
|
458
477
|
saveFile("accounts.jsonc", { accounts });
|
|
459
478
|
}
|
|
460
479
|
/** Load preferences (shared + local overrides, with legacy fallback) */
|
|
@@ -496,9 +515,26 @@ export function saveAutocomplete(settings) {
|
|
|
496
515
|
export function loadAllowlist() {
|
|
497
516
|
return loadFile("allowlist.jsonc", DEFAULT_ALLOWLIST);
|
|
498
517
|
}
|
|
499
|
-
/** Save allow-list */
|
|
500
|
-
export function saveAllowlist(list) {
|
|
501
|
-
|
|
518
|
+
/** Save allow-list — merges with existing cloud copy (multi-client safe) */
|
|
519
|
+
export async function saveAllowlist(list) {
|
|
520
|
+
// Read current cloud version and merge (other clients may have added entries)
|
|
521
|
+
let merged = { ...list };
|
|
522
|
+
try {
|
|
523
|
+
const cloudContent = await cloudRead("allowlist.jsonc");
|
|
524
|
+
if (cloudContent) {
|
|
525
|
+
const cloud = parseJsonc(cloudContent);
|
|
526
|
+
if (cloud) {
|
|
527
|
+
const mergeArrays = (local, remote) => [...new Set([...local, ...remote])];
|
|
528
|
+
merged = {
|
|
529
|
+
senders: mergeArrays(list.senders || [], cloud.senders || []),
|
|
530
|
+
domains: mergeArrays(list.domains || [], cloud.domains || []),
|
|
531
|
+
recipients: mergeArrays(list.recipients || [], cloud.recipients || []),
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
catch { /* cloud read failed — save local version */ }
|
|
537
|
+
saveFile("allowlist.jsonc", merged);
|
|
502
538
|
}
|
|
503
539
|
// ── Legacy compatibility ──
|
|
504
540
|
function loadLegacySettings() {
|
|
@@ -529,8 +565,8 @@ export function loadSettings() {
|
|
|
529
565
|
};
|
|
530
566
|
}
|
|
531
567
|
/** Save settings — writes to split files */
|
|
532
|
-
export function saveSettings(settings) {
|
|
533
|
-
saveAccounts(settings.accounts);
|
|
568
|
+
export async function saveSettings(settings) {
|
|
569
|
+
await saveAccounts(settings.accounts);
|
|
534
570
|
savePreferences({ ui: settings.ui, sync: settings.sync });
|
|
535
571
|
}
|
|
536
572
|
/** Get the local store base path */
|