@bobfrankston/rmfmail 1.2.324 → 1.2.326
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/.llm/config-cache-crash.md +34 -0
- package/bin/mailx.js +96 -25
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +105 -16
- package/client/app.bundle.js +8 -1
- package/client/app.bundle.js.map +2 -2
- package/client/app.js +15 -1
- package/client/app.js.map +1 -1
- package/client/app.ts +13 -1
- package/client/compose/compose.bundle.js +30 -19
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/compose/editor.js +51 -28
- package/client/compose/editor.js.map +1 -1
- package/client/compose/editor.ts +47 -25
- package/npmchanges.md +49 -0
- package/package.json +7 -7
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +7 -2
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +7 -2
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-settings/index.d.ts +15 -0
- package/packages/mailx-settings/index.d.ts.map +1 -1
- package/packages/mailx-settings/index.js +71 -14
- package/packages/mailx-settings/index.js.map +1 -1
- package/packages/mailx-settings/index.ts +65 -14
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/package.json +1 -1
- package/.commitmsg +0 -24
package/bin/mailx.ts
CHANGED
|
@@ -2025,7 +2025,7 @@ async function main(): Promise<void> {
|
|
|
2025
2025
|
{ ImapManager },
|
|
2026
2026
|
{ MailxService, spawnSyncWorker },
|
|
2027
2027
|
{ dispatch, setDebugEvalSink },
|
|
2028
|
-
{ loadSettings, loadAccountsAsync, loadAllowlistAsync, getConfigDir, getStorageInfo, getStorePath },
|
|
2028
|
+
{ loadSettings, loadAccounts, loadAccountsAsync, loadAllowlistAsync, getConfigDir, getStorageInfo, getStorePath },
|
|
2029
2029
|
{ NodeTcpTransport },
|
|
2030
2030
|
{ FileMessageStore },
|
|
2031
2031
|
] = await Promise.all([
|
|
@@ -2064,20 +2064,24 @@ async function main(): Promise<void> {
|
|
|
2064
2064
|
//
|
|
2065
2065
|
// The local accounts.jsonc cache (loaded synchronously above) has the
|
|
2066
2066
|
// last-known state; that's what every account+folder+message UI read
|
|
2067
|
-
// depends on.
|
|
2068
|
-
//
|
|
2069
|
-
//
|
|
2070
|
-
//
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2067
|
+
// depends on. The cloud refresh runs in the background; its result is
|
|
2068
|
+
// applied once the account machinery exists (see the continuation after
|
|
2069
|
+
// watchConfigFiles() below).
|
|
2070
|
+
//
|
|
2071
|
+
// 2026-09-13 17:20 EDT — Claude Code (Fable 5.1), at Bob's direction.
|
|
2072
|
+
// This used to log "accounts diverged — restart to apply" and stop. A
|
|
2073
|
+
// kernel crash at 16:36 today zero-filled the local accounts.jsonc, the
|
|
2074
|
+
// daemon booted with 0 accounts, and the cloud copy (4 accounts) sat
|
|
2075
|
+
// unused behind a banner. Now the promise is kept and the divergence is
|
|
2076
|
+
// reconciled in-process through the same configChanged path an on-disk
|
|
2077
|
+
// edit takes (reconcileAccountsFromDisk) — one mechanism, two triggers.
|
|
2078
|
+
// Typed off the loader: mailx-types is not a dependency of this package.
|
|
2079
|
+
type AccountList = Awaited<ReturnType<typeof loadAccountsAsync>>;
|
|
2080
|
+
const cloudAccountsRefresh = loadAccountsAsync().catch((e: any): AccountList => {
|
|
2081
|
+
// The boot snapshot stays in force; the 3-minute cloud poll retries.
|
|
2082
|
+
console.error(` [cloud] background account refresh failed: ${e?.message || e}`);
|
|
2083
|
+
return [];
|
|
2084
|
+
});
|
|
2081
2085
|
|
|
2082
2086
|
const db = new MailxDB(getConfigDir());
|
|
2083
2087
|
_tick("DB opened");
|
|
@@ -2870,8 +2874,78 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
|
|
|
2870
2874
|
imapManager.on("accountError", (accountId: string, error: string, hint: string, isOAuth: boolean) => {
|
|
2871
2875
|
handle.send({ _event: "accountError", type: "accountError", accountId, error, hint, isOAuth });
|
|
2872
2876
|
});
|
|
2877
|
+
/** Bring the running daemon in line with accounts.jsonc as it is on disk.
|
|
2878
|
+
* New enabled accounts are added live (addAccount + inbox check + sync +
|
|
2879
|
+
* IDLE). Anything that can't be applied to a live account — one removed,
|
|
2880
|
+
* disabled, or with changed settings — is reported with restartNeeded so
|
|
2881
|
+
* the client shows the Restart banner only when a restart really is the
|
|
2882
|
+
* fix. Idempotent: addAccount ignores ids it already has, and the boot
|
|
2883
|
+
* snapshot (settings.accounts) is updated in place.
|
|
2884
|
+
*
|
|
2885
|
+
* 2026-09-13 17:20 EDT — Claude Code (Fable 5.1), at Bob's direction.
|
|
2886
|
+
* Before this every accounts.jsonc change, including the daemon's own
|
|
2887
|
+
* repair of a crash-zeroed cache from the cloud copy, produced only
|
|
2888
|
+
* "restart to apply". */
|
|
2889
|
+
async function reconcileAccountsFromDisk(): Promise<{ restartNeeded: boolean; detail: string }> {
|
|
2890
|
+
const onDisk = loadAccounts();
|
|
2891
|
+
if (onDisk.length === 0) {
|
|
2892
|
+
// A file that reads as empty is a broken file, not a request to
|
|
2893
|
+
// drop every account (readJsonc has already logged the parse error).
|
|
2894
|
+
return { restartNeeded: false, detail: "accounts.jsonc has no accounts — keeping the running set" };
|
|
2895
|
+
}
|
|
2896
|
+
const snapshot = new Map(settings.accounts.map(a => [a.id, a]));
|
|
2897
|
+
const diskIds = new Set(onDisk.map(a => a.id));
|
|
2898
|
+
const added = onDisk.filter(a => a.enabled && !snapshot.has(a.id));
|
|
2899
|
+
const removed = settings.accounts.filter(a => !diskIds.has(a.id)).map(a => a.id);
|
|
2900
|
+
const changed = onDisk
|
|
2901
|
+
.filter(a => snapshot.has(a.id) && JSON.stringify(snapshot.get(a.id)) !== JSON.stringify(a))
|
|
2902
|
+
.map(a => a.id);
|
|
2903
|
+
if (added.length === 0 && removed.length === 0 && changed.length === 0) {
|
|
2904
|
+
return { restartNeeded: false, detail: "accounts.jsonc updated — no account changes" };
|
|
2905
|
+
}
|
|
2906
|
+
const applied: string[] = [];
|
|
2907
|
+
for (const account of added) {
|
|
2908
|
+
try {
|
|
2909
|
+
await imapManager.addAccount(account);
|
|
2910
|
+
settings.accounts.push(account);
|
|
2911
|
+
applied.push(account.label || account.name || account.id);
|
|
2912
|
+
console.log(` Account: ${account.label || account.name} (${account.id}) — added from accounts.jsonc`);
|
|
2913
|
+
imapManager.quickInboxCheckAccount(account.id).catch(e =>
|
|
2914
|
+
console.error(` [startup-check] ${account.id}: ${e?.message || e}`));
|
|
2915
|
+
} catch (e: any) {
|
|
2916
|
+
console.error(` Failed: ${account.id}: ${e.message}`);
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
if (applied.length > 0) {
|
|
2920
|
+
// syncAll is a no-op while a sync is already running; the periodic
|
|
2921
|
+
// sweep picks the new account up then. startWatching is per-config
|
|
2922
|
+
// and idempotent, so IDLE starts for the new account either way.
|
|
2923
|
+
imapManager.syncAll()
|
|
2924
|
+
.then(() => imapManager.startWatching())
|
|
2925
|
+
.catch(e => console.error(` Sync error: ${e.message}`));
|
|
2926
|
+
}
|
|
2927
|
+
const parts: string[] = [];
|
|
2928
|
+
if (applied.length) parts.push(`added ${applied.join(", ")}`);
|
|
2929
|
+
if (removed.length) parts.push(`removed ${removed.join(", ")}`);
|
|
2930
|
+
if (changed.length) parts.push(`changed ${changed.join(", ")}`);
|
|
2931
|
+
const restartNeeded = removed.length > 0 || changed.length > 0;
|
|
2932
|
+
const detail = `accounts.jsonc: ${parts.join("; ")}${restartNeeded ? " — restart to apply" : ""}`;
|
|
2933
|
+
console.log(` [accounts] ${detail}`);
|
|
2934
|
+
return { restartNeeded, detail };
|
|
2935
|
+
}
|
|
2873
2936
|
imapManager.on("configChanged", (filename: string) => {
|
|
2874
|
-
|
|
2937
|
+
if (filename !== "accounts.jsonc") {
|
|
2938
|
+
handle.send({ _event: "configChanged", type: "configChanged", filename });
|
|
2939
|
+
return;
|
|
2940
|
+
}
|
|
2941
|
+
reconcileAccountsFromDisk()
|
|
2942
|
+
.then(r => handle.send({ _event: "configChanged", type: "configChanged", filename, restartNeeded: r.restartNeeded, detail: r.detail }))
|
|
2943
|
+
.catch((e: any) => {
|
|
2944
|
+
// Reconcile failed: fall back to the old behaviour — the user can
|
|
2945
|
+
// always restart — and say why in the log.
|
|
2946
|
+
console.error(` [accounts] reconcile failed: ${e?.message || e}`);
|
|
2947
|
+
handle.send({ _event: "configChanged", type: "configChanged", filename, restartNeeded: true, detail: `accounts.jsonc changed — restart to apply (${e?.message || e})` });
|
|
2948
|
+
});
|
|
2875
2949
|
});
|
|
2876
2950
|
imapManager.on("outboxStatus", (status: any) => {
|
|
2877
2951
|
handle.send({ _event: "outboxStatus", type: "outboxStatus", ...status });
|
|
@@ -3231,6 +3305,21 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
|
|
|
3231
3305
|
imapManager.startSentSweep();
|
|
3232
3306
|
imapManager.watchConfigFiles();
|
|
3233
3307
|
|
|
3308
|
+
// 2026-09-13 17:20 EDT — Claude Code (Fable 5.1), at Bob's direction.
|
|
3309
|
+
// Apply the background cloud refresh started before the DB was even open.
|
|
3310
|
+
// cloudRead has already written the cloud copy into the local cache; if
|
|
3311
|
+
// it differs from the boot snapshot, run it through the same configChanged
|
|
3312
|
+
// path an on-disk edit takes (MailxService drops its accounts cache, then
|
|
3313
|
+
// reconcileAccountsFromDisk adds new accounts live and tells the client).
|
|
3314
|
+
// Emitting here rather than relying on fs.watch closes the race where the
|
|
3315
|
+
// cloud read lands before watchConfigFiles() registered the watcher.
|
|
3316
|
+
void cloudAccountsRefresh.then((cloudAccounts) => {
|
|
3317
|
+
if (cloudAccounts.length === 0) return;
|
|
3318
|
+
if (JSON.stringify(cloudAccounts) === JSON.stringify(settings.accounts)) return;
|
|
3319
|
+
console.log(` [cloud] accounts.jsonc differs from the boot snapshot (cache=${settings.accounts.length} cloud=${cloudAccounts.length}) — reconciling`);
|
|
3320
|
+
imapManager.emit("configChanged", "accounts.jsonc");
|
|
3321
|
+
});
|
|
3322
|
+
|
|
3234
3323
|
// Deploy app-owned reference docs (.md per JSONC schema) to the cloud
|
|
3235
3324
|
// folder so users see them next to the .jsonc they document. Versioned
|
|
3236
3325
|
// by app version — only redeploys when the app updates. Fire-and-forget;
|
package/client/app.bundle.js
CHANGED
|
@@ -12119,7 +12119,14 @@ onWsEvent((event) => {
|
|
|
12119
12119
|
}, 8e3);
|
|
12120
12120
|
}
|
|
12121
12121
|
if (event.filename && /accounts\.jsonc/i.test(String(event.filename))) {
|
|
12122
|
-
|
|
12122
|
+
if (event.restartNeeded) {
|
|
12123
|
+
showRestartForConfigBanner();
|
|
12124
|
+
} else if (event.detail && statusSync) {
|
|
12125
|
+
statusSync.textContent = String(event.detail);
|
|
12126
|
+
setTimeout(() => {
|
|
12127
|
+
if (statusSync.textContent === String(event.detail)) statusSync.textContent = "";
|
|
12128
|
+
}, 8e3);
|
|
12129
|
+
}
|
|
12123
12130
|
}
|
|
12124
12131
|
break;
|
|
12125
12132
|
case "cloudError":
|