@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.
- package/.commitmsg +17 -15
- package/.llm/config-cache-crash.md +26 -0
- package/.llm/trusted-lists.md +1 -0
- package/bin/mailx.js +96 -25
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +105 -16
- package/client/android-bootstrap.bundle.js +5 -2
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/client/app.bundle.js +11 -3
- 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/components/message-viewer.js +12 -2
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +13 -3
- package/docs/allowlist.md +1 -0
- package/npmchanges.md +47 -0
- package/package.json +5 -5
- 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/docs/allowlist.md +1 -0
- 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/packages/mailx-store-web/package.json +1 -1
- package/packages/mailx-types/package.json +1 -1
- package/packages/mailx-types/trust.d.ts +4 -0
- package/packages/mailx-types/trust.d.ts.map +1 -1
- package/packages/mailx-types/trust.js +17 -1
- package/packages/mailx-types/trust.js.map +1 -1
- package/packages/mailx-types/trust.ts +20 -1
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;
|
|
@@ -2589,6 +2589,8 @@ function provedSenderDomain(input) {
|
|
|
2589
2589
|
function redirectorLink(input) {
|
|
2590
2590
|
const bodyHtml = input.bodyHtml || "";
|
|
2591
2591
|
const senderDomain = provedSenderDomain(input);
|
|
2592
|
+
if (senderDomain && input.senderTrusted)
|
|
2593
|
+
return null;
|
|
2592
2594
|
const intermediary = provedIntermediary(input);
|
|
2593
2595
|
const signers = dkimSigners(input);
|
|
2594
2596
|
for (const m of bodyHtml.matchAll(/href\s*=\s*["']([^"']+)["']/gi)) {
|
|
@@ -2618,13 +2620,14 @@ function redirectorLink(input) {
|
|
|
2618
2620
|
if (senderDomain && sameOrg(target.hostname, senderDomain))
|
|
2619
2621
|
continue;
|
|
2620
2622
|
const signer = signers.find((s) => sameOrg(s, url.hostname));
|
|
2621
|
-
const how = signer ? ` ${signer} signed this message; "Trust ${signer} as an intermediary" stops this warning for links through ${signer} in mail it signs.` : ` Nobody signed this message for ${url.hostname}, so there is no intermediary to trust; only the sender could vouch for the link.`;
|
|
2623
|
+
const how = signer ? ` ${signer} signed this message; "Trust ${signer} as an intermediary" stops this warning for links through ${signer} in mail it signs.` : senderDomain ? ` Nobody signed this message for ${url.hostname}, so there is no intermediary to trust; ${senderDomain} proved it sent this, and "Stop warning about *@${senderDomain}" lets it vouch for its own links.` : ` Nobody signed this message for ${url.hostname}, so there is no intermediary to trust; only the sender could vouch for the link, and nothing proves who the sender is.`;
|
|
2622
2624
|
return {
|
|
2623
2625
|
id: "redirector-link",
|
|
2624
2626
|
severity: "caution",
|
|
2625
2627
|
text: `A link hides where it goes: it passes through ${url.hostname} and ends at ${target.hostname}.`,
|
|
2626
2628
|
detail: `${url.hostname} -> ${target.hostname}.${how}`,
|
|
2627
|
-
...signer ? { via: signer } : {}
|
|
2629
|
+
...signer ? { via: signer } : {},
|
|
2630
|
+
...senderDomain && !signer ? { sender: senderDomain } : {}
|
|
2628
2631
|
};
|
|
2629
2632
|
}
|
|
2630
2633
|
}
|