@bobfrankston/rmfmail 1.2.323 → 1.2.325

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.
Files changed (40) hide show
  1. package/.commitmsg +17 -15
  2. package/.llm/config-cache-crash.md +26 -0
  3. package/.llm/trusted-lists.md +1 -0
  4. package/bin/mailx.js +96 -25
  5. package/bin/mailx.js.map +1 -1
  6. package/bin/mailx.ts +105 -16
  7. package/client/android-bootstrap.bundle.js +5 -2
  8. package/client/android-bootstrap.bundle.js.map +2 -2
  9. package/client/app.bundle.js +11 -3
  10. package/client/app.bundle.js.map +2 -2
  11. package/client/app.js +15 -1
  12. package/client/app.js.map +1 -1
  13. package/client/app.ts +13 -1
  14. package/client/components/message-viewer.js +12 -2
  15. package/client/components/message-viewer.js.map +1 -1
  16. package/client/components/message-viewer.ts +13 -3
  17. package/docs/allowlist.md +1 -0
  18. package/npmchanges.md +47 -0
  19. package/package.json +5 -5
  20. package/packages/mailx-imap/index.d.ts.map +1 -1
  21. package/packages/mailx-imap/index.js +7 -2
  22. package/packages/mailx-imap/index.js.map +1 -1
  23. package/packages/mailx-imap/index.ts +7 -2
  24. package/packages/mailx-imap/package-lock.json +2 -2
  25. package/packages/mailx-imap/package.json +1 -1
  26. package/packages/mailx-settings/docs/allowlist.md +1 -0
  27. package/packages/mailx-settings/index.d.ts +15 -0
  28. package/packages/mailx-settings/index.d.ts.map +1 -1
  29. package/packages/mailx-settings/index.js +71 -14
  30. package/packages/mailx-settings/index.js.map +1 -1
  31. package/packages/mailx-settings/index.ts +65 -14
  32. package/packages/mailx-settings/package.json +1 -1
  33. package/packages/mailx-store/package.json +1 -1
  34. package/packages/mailx-store-web/package.json +1 -1
  35. package/packages/mailx-types/package.json +1 -1
  36. package/packages/mailx-types/trust.d.ts +4 -0
  37. package/packages/mailx-types/trust.d.ts.map +1 -1
  38. package/packages/mailx-types/trust.js +17 -1
  39. package/packages/mailx-types/trust.js.map +1 -1
  40. package/packages/mailx-types/trust.ts +20 -1
package/.commitmsg CHANGED
@@ -1,17 +1,19 @@
1
- New event: the calendar picker is always there, and lists every calendar you can write to
1
+ accounts.jsonc: crash-safe local cache + live reconcile instead of "restart to apply"
2
2
 
3
- Bob 2026-09-11: "the add appointment box should give me a choice of
4
- calendars." v1.2.318 hid the picker whenever Google reported fewer than
5
- two writable calendars and his four are one owned calendar plus two
6
- holiday feeds and a shared one, so the row never appeared and read as a
7
- missing feature.
3
+ A Kernel-Power 41 crash (2026-09-13 16:36) left ~/.rmfmail/accounts.jsonc
4
+ zero-filled: cloudRead rewrote the local cache with a plain writeFileSync on
5
+ EVERY cloud read (every 3 min via pollCloud), so the file was perpetually in a
6
+ just-written window and NTFS committed the size but not the data. jsonc-parser
7
+ returned undefined for the NULs without a single log line, the daemon booted
8
+ with 0 accounts, and the cloud copy sat unused behind the "restart to apply"
9
+ banner.
8
10
 
9
- The row is now always shown. With one writable calendar the dropdown is
10
- disabled and its tooltip says that this is the only calendar Google lets
11
- you add events to, that the others are read-only, and where to change
12
- that (Google Calendar, then Refresh). getCalendars returns EVERY
13
- calendar with `selected` as a flag instead of a filter: the sidebar
14
- still lists the selected ones, but a calendar the reader owns and has
15
- unticked in Google is still one a new event can go to. The daemon logs
16
- each calendar's role and selection at enumeration so "why only one" can
17
- be answered from the log. mailx-types 0.1.98, mailx-service 0.1.39.
11
+ - mailx-settings 0.1.92: atomicWriteText (tmp + fsync + rename) exported and
12
+ used by every local-cache writer; writeTextIfChanged skips identical
13
+ content; readJsonc reports jsonc-parser errors instead of returning nothing.
14
+ - mailx-imap 0.1.185: pollCloud writes through atomicWriteText.
15
+ - bin/mailx.ts: reconcileAccountsFromDisk new enabled accounts are added
16
+ live (addAccount + inbox check + sync + IDLE) on any accounts.jsonc change,
17
+ including the boot-time cloud refresh; configChanged carries restartNeeded.
18
+ - client: the Restart banner shows only when restartNeeded; otherwise a
19
+ status-bar line names what changed.
@@ -0,0 +1,26 @@
1
+ # accounts.jsonc zeroed by a kernel crash — daemon booted with 0 accounts (2026-09-13)
2
+
3
+ Session: Claude Fable 5.1, started 2026-09-13 ~17:00 EDT.
4
+
5
+ ## Evidence
6
+ - Windows System log: Kernel-Power 41 + 6008 (unexpected shutdown) at 16:36 today.
7
+ - `~/.rmfmail/accounts.jsonc` (1932 bytes) came back as 1932 NUL bytes (NTFS commits size, not data).
8
+ - Boot 16:50:58: `settings loaded (0 accounts)`, `addAccount loop starting` / `complete` with NO accounts.
9
+ jsonc-parser `parse()` returns undefined on NULs (no throw) → readJsonc returned nothing, no log line.
10
+ - 16:51:12.489 background `loadAccountsAsync` read cloud (4 accounts), `cloudRead` rewrote the local cache
11
+ → fs.watch saw NULs→JSON → `configChanged` → client banner "accounts.jsonc changed — restart to apply".
12
+ - Why the file was in a fresh-write window: `cloudRead` rewrites the cache with plain `writeFileSync` on
13
+ EVERY read, and pollCloud reads every 3 min (log: `[watch] accounts.jsonc fs.watch fired but content
14
+ unchanged` every 3 min all day).
15
+
16
+ ## Fix (in progress)
17
+ 1. mailx-settings `index.ts`: cloudRead / loadAccounts cache writes → skip when identical, else
18
+ atomic tmp+fsync+rename (`atomicWriteText`, now exported, with fsync). readJsonc logs parse errors.
19
+ 2. mailx-imap `index.ts` pollCloud write → atomicWriteText.
20
+ 3. bin/mailx.ts: cloud account refresh at boot APPLIES new accounts in-process (addAccount +
21
+ quick check + syncAll + startWatching) instead of only logging "restart to apply".
22
+ 4. Bump mailx-settings, mailx-imap; rebuild.cmd.
23
+
24
+ ## Status
25
+ - [x] edits - [x] tsc + bundles - [ ] rebuild.cmd (running) - [ ] Bob restarts app
26
+ - DONE.md entry written as v1.2.325; .commitmsg written; versions: settings 0.1.92, imap 0.1.185
@@ -69,3 +69,4 @@ tests/trust.test.ts, .commitmsg, TODO.md C165, root version 1.2.311. Then rebuil
69
69
  - Bob confirmed 2026-09-10: 'quill worked' (v1.2.320 placeCaretAfterLink).
70
70
  - v1.2.322: trustedSendersVia ('sender via relay' pairs) — Ground News via amazonses.com. Banner offers 'Trust ‹sender› via ‹relay›' on the trusted-but-unproved caution; counts only when the relay DKIM-signed. 45/45 trust tests.
71
71
  - v1.2.323: New event picker always shown (disabled + explanatory tooltip when only one writable calendar); getCalendars returns all calendars with selected flag; daemon logs roles.
72
+ - v1.2.324: redirector rule 5 — proved + trusted sender vouches for its own links (Toast via toasttakeout.page.link). Banner 'Stop warning about' now offered when every finding is verdict/redirector and sender proved. 48/48.
package/bin/mailx.js CHANGED
@@ -2098,7 +2098,7 @@ async function main() {
2098
2098
  // for nothing — each was independent. Same for node-tcp-transport
2099
2099
  // and mailx-store/file-store.js further down; folded in here so the
2100
2100
  // resolution + module-init cost happens once, in parallel.
2101
- const [{ MailxDB, prewarmParseWorker, storeBus, Store }, { ImapManager }, { MailxService, spawnSyncWorker }, { dispatch, setDebugEvalSink }, { loadSettings, loadAccountsAsync, loadAllowlistAsync, getConfigDir, getStorageInfo, getStorePath }, { NodeTcpTransport }, { FileMessageStore },] = await Promise.all([
2101
+ const [{ MailxDB, prewarmParseWorker, storeBus, Store }, { ImapManager }, { MailxService, spawnSyncWorker }, { dispatch, setDebugEvalSink }, { loadSettings, loadAccounts, loadAccountsAsync, loadAllowlistAsync, getConfigDir, getStorageInfo, getStorePath }, { NodeTcpTransport }, { FileMessageStore },] = await Promise.all([
2102
2102
  import("@bobfrankston/mailx-store"),
2103
2103
  import("@bobfrankston/mailx-imap"),
2104
2104
  import("@bobfrankston/mailx-service"),
@@ -2124,29 +2124,11 @@ async function main() {
2124
2124
  prewarmParseWorker();
2125
2125
  let settings = loadSettings();
2126
2126
  _tick(`settings loaded (${settings.accounts?.length || 0} accounts)`);
2127
- // CRITICAL: don't await cloud refresh of accounts.jsonc here — it adds
2128
- // 1-3s of GDrive latency to startup BEFORE the IPC dispatcher is even
2129
- // registered. WebView opens, hits "Connecting to server..." overlay,
2130
- // and sits there waiting for the daemon to finish a cloud read it
2131
- // doesn't actually need at boot.
2132
- //
2133
- // The local accounts.jsonc cache (loaded synchronously above) has the
2134
- // last-known state; that's what every account+folder+message UI read
2135
- // depends on. Cloud refresh's only job is "another device added an
2136
- // account" — fire-and-forget, log + warn if it diverges, the user can
2137
- // restart for it to take effect. Was a band-aid hidden in a sync
2138
- // path; per feedback_no_bandaids, fix the real cause.
2139
- void (async () => {
2140
- try {
2141
- const cloudAccounts = await loadAccountsAsync();
2142
- if (cloudAccounts.length > 0 && cloudAccounts.length !== settings.accounts.length) {
2143
- console.log(` [cloud] accounts diverged: cache=${settings.accounts.length} cloud=${cloudAccounts.length} — restart to apply`);
2144
- }
2145
- }
2146
- catch (e) {
2147
- console.error(` [cloud] background account refresh failed: ${e?.message || e}`);
2148
- }
2149
- })();
2127
+ const cloudAccountsRefresh = loadAccountsAsync().catch((e) => {
2128
+ // The boot snapshot stays in force; the 3-minute cloud poll retries.
2129
+ console.error(` [cloud] background account refresh failed: ${e?.message || e}`);
2130
+ return [];
2131
+ });
2150
2132
  const db = new MailxDB(getConfigDir());
2151
2133
  _tick("DB opened");
2152
2134
  // Store — the architectural nexus (DB + .eml files + bus + operations).
@@ -2984,8 +2966,81 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
2984
2966
  imapManager.on("accountError", (accountId, error, hint, isOAuth) => {
2985
2967
  handle.send({ _event: "accountError", type: "accountError", accountId, error, hint, isOAuth });
2986
2968
  });
2969
+ /** Bring the running daemon in line with accounts.jsonc as it is on disk.
2970
+ * New enabled accounts are added live (addAccount + inbox check + sync +
2971
+ * IDLE). Anything that can't be applied to a live account — one removed,
2972
+ * disabled, or with changed settings — is reported with restartNeeded so
2973
+ * the client shows the Restart banner only when a restart really is the
2974
+ * fix. Idempotent: addAccount ignores ids it already has, and the boot
2975
+ * snapshot (settings.accounts) is updated in place.
2976
+ *
2977
+ * 2026-09-13 17:20 EDT — Claude Code (Fable 5.1), at Bob's direction.
2978
+ * Before this every accounts.jsonc change, including the daemon's own
2979
+ * repair of a crash-zeroed cache from the cloud copy, produced only
2980
+ * "restart to apply". */
2981
+ async function reconcileAccountsFromDisk() {
2982
+ const onDisk = loadAccounts();
2983
+ if (onDisk.length === 0) {
2984
+ // A file that reads as empty is a broken file, not a request to
2985
+ // drop every account (readJsonc has already logged the parse error).
2986
+ return { restartNeeded: false, detail: "accounts.jsonc has no accounts — keeping the running set" };
2987
+ }
2988
+ const snapshot = new Map(settings.accounts.map(a => [a.id, a]));
2989
+ const diskIds = new Set(onDisk.map(a => a.id));
2990
+ const added = onDisk.filter(a => a.enabled && !snapshot.has(a.id));
2991
+ const removed = settings.accounts.filter(a => !diskIds.has(a.id)).map(a => a.id);
2992
+ const changed = onDisk
2993
+ .filter(a => snapshot.has(a.id) && JSON.stringify(snapshot.get(a.id)) !== JSON.stringify(a))
2994
+ .map(a => a.id);
2995
+ if (added.length === 0 && removed.length === 0 && changed.length === 0) {
2996
+ return { restartNeeded: false, detail: "accounts.jsonc updated — no account changes" };
2997
+ }
2998
+ const applied = [];
2999
+ for (const account of added) {
3000
+ try {
3001
+ await imapManager.addAccount(account);
3002
+ settings.accounts.push(account);
3003
+ applied.push(account.label || account.name || account.id);
3004
+ console.log(` Account: ${account.label || account.name} (${account.id}) — added from accounts.jsonc`);
3005
+ imapManager.quickInboxCheckAccount(account.id).catch(e => console.error(` [startup-check] ${account.id}: ${e?.message || e}`));
3006
+ }
3007
+ catch (e) {
3008
+ console.error(` Failed: ${account.id}: ${e.message}`);
3009
+ }
3010
+ }
3011
+ if (applied.length > 0) {
3012
+ // syncAll is a no-op while a sync is already running; the periodic
3013
+ // sweep picks the new account up then. startWatching is per-config
3014
+ // and idempotent, so IDLE starts for the new account either way.
3015
+ imapManager.syncAll()
3016
+ .then(() => imapManager.startWatching())
3017
+ .catch(e => console.error(` Sync error: ${e.message}`));
3018
+ }
3019
+ const parts = [];
3020
+ if (applied.length)
3021
+ parts.push(`added ${applied.join(", ")}`);
3022
+ if (removed.length)
3023
+ parts.push(`removed ${removed.join(", ")}`);
3024
+ if (changed.length)
3025
+ parts.push(`changed ${changed.join(", ")}`);
3026
+ const restartNeeded = removed.length > 0 || changed.length > 0;
3027
+ const detail = `accounts.jsonc: ${parts.join("; ")}${restartNeeded ? " — restart to apply" : ""}`;
3028
+ console.log(` [accounts] ${detail}`);
3029
+ return { restartNeeded, detail };
3030
+ }
2987
3031
  imapManager.on("configChanged", (filename) => {
2988
- handle.send({ _event: "configChanged", type: "configChanged", filename });
3032
+ if (filename !== "accounts.jsonc") {
3033
+ handle.send({ _event: "configChanged", type: "configChanged", filename });
3034
+ return;
3035
+ }
3036
+ reconcileAccountsFromDisk()
3037
+ .then(r => handle.send({ _event: "configChanged", type: "configChanged", filename, restartNeeded: r.restartNeeded, detail: r.detail }))
3038
+ .catch((e) => {
3039
+ // Reconcile failed: fall back to the old behaviour — the user can
3040
+ // always restart — and say why in the log.
3041
+ console.error(` [accounts] reconcile failed: ${e?.message || e}`);
3042
+ handle.send({ _event: "configChanged", type: "configChanged", filename, restartNeeded: true, detail: `accounts.jsonc changed — restart to apply (${e?.message || e})` });
3043
+ });
2989
3044
  });
2990
3045
  imapManager.on("outboxStatus", (status) => {
2991
3046
  handle.send({ _event: "outboxStatus", type: "outboxStatus", ...status });
@@ -3369,6 +3424,22 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
3369
3424
  imapManager.startOutboxWorker();
3370
3425
  imapManager.startSentSweep();
3371
3426
  imapManager.watchConfigFiles();
3427
+ // 2026-09-13 17:20 EDT — Claude Code (Fable 5.1), at Bob's direction.
3428
+ // Apply the background cloud refresh started before the DB was even open.
3429
+ // cloudRead has already written the cloud copy into the local cache; if
3430
+ // it differs from the boot snapshot, run it through the same configChanged
3431
+ // path an on-disk edit takes (MailxService drops its accounts cache, then
3432
+ // reconcileAccountsFromDisk adds new accounts live and tells the client).
3433
+ // Emitting here rather than relying on fs.watch closes the race where the
3434
+ // cloud read lands before watchConfigFiles() registered the watcher.
3435
+ void cloudAccountsRefresh.then((cloudAccounts) => {
3436
+ if (cloudAccounts.length === 0)
3437
+ return;
3438
+ if (JSON.stringify(cloudAccounts) === JSON.stringify(settings.accounts))
3439
+ return;
3440
+ console.log(` [cloud] accounts.jsonc differs from the boot snapshot (cache=${settings.accounts.length} cloud=${cloudAccounts.length}) — reconciling`);
3441
+ imapManager.emit("configChanged", "accounts.jsonc");
3442
+ });
3372
3443
  // Deploy app-owned reference docs (.md per JSONC schema) to the cloud
3373
3444
  // folder so users see them next to the .jsonc they document. Versioned
3374
3445
  // by app version — only redeploys when the app updates. Fire-and-forget;