@bobfrankston/mailx 1.0.441 → 1.0.442
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/client/app.js +47 -4
- package/client/lib/api-client.js +3 -0
- package/client/lib/mailxapi.js +3 -0
- package/package.json +3 -3
- package/packages/mailx-service/index.d.ts +10 -0
- package/packages/mailx-service/index.js +25 -1
- package/packages/mailx-service/jsonrpc.js +2 -0
- package/packages/mailx-settings/index.d.ts +24 -1
- package/packages/mailx-settings/index.js +108 -5
package/client/app.js
CHANGED
|
@@ -2051,11 +2051,21 @@ viewBtn?.addEventListener("click", (e) => {
|
|
|
2051
2051
|
if (viewDropdown)
|
|
2052
2052
|
viewDropdown.hidden = !viewDropdown.hidden;
|
|
2053
2053
|
});
|
|
2054
|
-
document.addEventListener("click", () => {
|
|
2055
|
-
|
|
2054
|
+
document.addEventListener("click", (e) => {
|
|
2055
|
+
// Only close when the click is genuinely outside the menu container.
|
|
2056
|
+
// The earlier unconditional close had two problems: clicks on radio
|
|
2057
|
+
// buttons / checkboxes INSIDE the menu also closed it (so the user
|
|
2058
|
+
// couldn't toggle anything without reopening), and any handler
|
|
2059
|
+
// upstream that consumed the event (preventDefault paths, focus
|
|
2060
|
+
// shifts) could keep the menu open inappropriately. The closest()
|
|
2061
|
+
// check ensures inside-clicks pass through and outside-clicks close.
|
|
2062
|
+
const target = e.target;
|
|
2063
|
+
if (viewDropdown && !viewDropdown.hidden && !target?.closest("#view-menu") && !target?.closest("#view-dropdown")) {
|
|
2056
2064
|
viewDropdown.hidden = true;
|
|
2057
|
-
|
|
2065
|
+
}
|
|
2066
|
+
if (settingsDropdown && !settingsDropdown.hidden && !target?.closest("#settings-menu") && !target?.closest("#settings-dropdown")) {
|
|
2058
2067
|
settingsDropdown.hidden = true;
|
|
2068
|
+
}
|
|
2059
2069
|
});
|
|
2060
2070
|
// Restore saved view settings
|
|
2061
2071
|
const savedTwoLine = localStorage.getItem("mailx-two-line") === "true";
|
|
@@ -2225,7 +2235,7 @@ document.getElementById("btn-open-log")?.addEventListener("click", async () => {
|
|
|
2225
2235
|
}
|
|
2226
2236
|
});
|
|
2227
2237
|
async function openJsoncEditor(initialFile) {
|
|
2228
|
-
const { readJsoncFile, writeJsoncFile, readConfigHelp, formatJsonc } = await import("./lib/api-client.js");
|
|
2238
|
+
const { readJsoncFile, writeJsoncFile, readConfigHelp, formatJsonc, leanAccountsJsonc } = await import("./lib/api-client.js");
|
|
2229
2239
|
const backdrop = document.createElement("div");
|
|
2230
2240
|
backdrop.className = "mailx-modal-backdrop";
|
|
2231
2241
|
const panel = document.createElement("div");
|
|
@@ -2261,6 +2271,7 @@ async function openJsoncEditor(initialFile) {
|
|
|
2261
2271
|
<div class="mailx-modal-error" id="jsonc-error" hidden></div>
|
|
2262
2272
|
<div class="mailx-modal-buttons">
|
|
2263
2273
|
<button type="button" class="mailx-modal-btn" data-action="format" title="Reformat indentation while preserving comments and trailing commas">Format</button>
|
|
2274
|
+
<button type="button" class="mailx-modal-btn" data-action="lean" title="accounts.jsonc only — strip default-valued fields (port, tls, auth, etc.) so the file stays compact. Comments are dropped (the lean output is regenerated from values, not the original text)">Lean</button>
|
|
2264
2275
|
<span class="mailx-modal-spacer"></span>
|
|
2265
2276
|
<button type="button" class="mailx-modal-btn" data-action="cancel">Cancel</button>
|
|
2266
2277
|
<button type="button" class="mailx-modal-btn mailx-modal-btn-primary" data-action="save">Save</button>
|
|
@@ -2427,6 +2438,38 @@ async function openJsoncEditor(initialFile) {
|
|
|
2427
2438
|
}
|
|
2428
2439
|
return;
|
|
2429
2440
|
}
|
|
2441
|
+
if (action === "lean") {
|
|
2442
|
+
// accounts.jsonc-only: strip defaulted fields and re-emit
|
|
2443
|
+
// a compact form. User reviews it in the editor, then Save
|
|
2444
|
+
// commits. Lean drops comments because the output is
|
|
2445
|
+
// regenerated from canonical values, not the original text.
|
|
2446
|
+
if (fileSelect.value !== "accounts.jsonc") {
|
|
2447
|
+
errorEl.textContent = "Lean is only available for accounts.jsonc.";
|
|
2448
|
+
errorEl.hidden = false;
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
btn.disabled = true;
|
|
2452
|
+
const orig = btn.textContent;
|
|
2453
|
+
btn.textContent = "Leaning…";
|
|
2454
|
+
try {
|
|
2455
|
+
const r = await leanAccountsJsonc(textarea.value);
|
|
2456
|
+
if (r?.content !== undefined) {
|
|
2457
|
+
textarea.value = r.content;
|
|
2458
|
+
renderGutter();
|
|
2459
|
+
scheduleValidate();
|
|
2460
|
+
errorEl.hidden = true;
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
catch (e) {
|
|
2464
|
+
errorEl.textContent = `Lean failed: ${e.message}`;
|
|
2465
|
+
errorEl.hidden = false;
|
|
2466
|
+
}
|
|
2467
|
+
finally {
|
|
2468
|
+
btn.disabled = false;
|
|
2469
|
+
btn.textContent = orig || "Lean";
|
|
2470
|
+
}
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2430
2473
|
if (action === "save") {
|
|
2431
2474
|
// Final sync-check; refuse to save if it doesn't parse
|
|
2432
2475
|
const err = validateJsonc(textarea.value);
|
package/client/lib/api-client.js
CHANGED
|
@@ -351,6 +351,9 @@ export function readConfigHelp(name) {
|
|
|
351
351
|
export function unsubscribeOneClick(url) {
|
|
352
352
|
return ipc().unsubscribeOneClick?.(url);
|
|
353
353
|
}
|
|
354
|
+
export function leanAccountsJsonc(content) {
|
|
355
|
+
return ipc().leanAccountsJsonc?.(content) ?? Promise.resolve({ content });
|
|
356
|
+
}
|
|
354
357
|
export function openInWord(editId, html) {
|
|
355
358
|
return ipc().openInWord?.(editId, html) ?? Promise.resolve({ ok: false, path: "", opener: "none" });
|
|
356
359
|
}
|
package/client/lib/mailxapi.js
CHANGED
|
@@ -134,6 +134,9 @@
|
|
|
134
134
|
formatJsonc: function(content) {
|
|
135
135
|
return callNode("formatJsonc", { content: content });
|
|
136
136
|
},
|
|
137
|
+
leanAccountsJsonc: function(content) {
|
|
138
|
+
return callNode("leanAccountsJsonc", { content: content });
|
|
139
|
+
},
|
|
137
140
|
readConfigHelp: function(name) {
|
|
138
141
|
return callNode("readConfigHelp", { name: name });
|
|
139
142
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.442",
|
|
4
4
|
"description": "Local-first email client with IMAP sync and standalone native app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/mailx.js",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"@bobfrankston/iflow-node": "^0.1.8",
|
|
37
37
|
"@bobfrankston/miscinfo": "^1.0.10",
|
|
38
38
|
"@bobfrankston/oauthsupport": "^1.0.25",
|
|
39
|
-
"@bobfrankston/msger": "^0.1.
|
|
39
|
+
"@bobfrankston/msger": "^0.1.366",
|
|
40
40
|
"@bobfrankston/mailx-host": "^0.1.8",
|
|
41
41
|
"@capacitor/android": "^8.3.0",
|
|
42
42
|
"@capacitor/cli": "^8.3.0",
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"@bobfrankston/iflow-node": "^0.1.8",
|
|
101
101
|
"@bobfrankston/miscinfo": "^1.0.10",
|
|
102
102
|
"@bobfrankston/oauthsupport": "^1.0.25",
|
|
103
|
-
"@bobfrankston/msger": "^0.1.
|
|
103
|
+
"@bobfrankston/msger": "^0.1.366",
|
|
104
104
|
"@bobfrankston/mailx-host": "^0.1.8",
|
|
105
105
|
"@capacitor/android": "^8.3.0",
|
|
106
106
|
"@capacitor/cli": "^8.3.0",
|
|
@@ -316,6 +316,16 @@ export declare class MailxService {
|
|
|
316
316
|
* `config.jsonc` is the local per-machine config (not cloud-synced). */
|
|
317
317
|
readJsoncFile(name: string): Promise<string | null>;
|
|
318
318
|
formatJsonc(content: string): Promise<string>;
|
|
319
|
+
/** Strip default-valued fields from accounts.jsonc and return the lean
|
|
320
|
+
* form. Called from the JSONC editor's "Lean" button so the user can
|
|
321
|
+
* see the tidy version before saving. Round-trips through normalize→
|
|
322
|
+
* denormalize so the canonicalization rules stay in one place
|
|
323
|
+
* (mailx-settings). Drops port: 993, tls: true, auth: "password",
|
|
324
|
+
* enabled: true, sig.html: false, etc. when they match the defaults
|
|
325
|
+
* for the email's provider. Promotes a shared `name` to the file
|
|
326
|
+
* level when every account has the same name. Only handles
|
|
327
|
+
* accounts.jsonc — other JSONC files have hand-curated shapes. */
|
|
328
|
+
leanAccountsJsonc(content: string): Promise<string>;
|
|
319
329
|
/** Return the help section for a named config file, extracted from docs/config-help.md.
|
|
320
330
|
* Matches a level-2 heading whose text equals the filename. Returns markdown. */
|
|
321
331
|
readConfigHelp(name: string): Promise<string>;
|
|
@@ -9,7 +9,7 @@ import * as path from "node:path";
|
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
import * as gsync from "./google-sync.js";
|
|
12
|
-
import { loadSettings, saveSettings, loadAccounts, loadAccountsAsync, saveAccounts, initCloudConfig, loadAllowlist, saveAllowlist, loadAutocomplete, saveAutocomplete, getStorageInfo, getConfigDir } from "@bobfrankston/mailx-settings";
|
|
12
|
+
import { loadSettings, saveSettings, loadAccounts, loadAccountsAsync, saveAccounts, initCloudConfig, loadAllowlist, saveAllowlist, loadAutocomplete, saveAutocomplete, getStorageInfo, getConfigDir, normalizeAccount, denormalizeAccount } from "@bobfrankston/mailx-settings";
|
|
13
13
|
import { sanitizeHtml, encodeQuotedPrintable, htmlToPlainText } from "@bobfrankston/mailx-types";
|
|
14
14
|
import { simpleParser } from "mailparser";
|
|
15
15
|
/** Parse `List-Unsubscribe` (RFC 2369) and `List-Unsubscribe-Post` (RFC 8058).
|
|
@@ -2041,6 +2041,30 @@ export class MailxService {
|
|
|
2041
2041
|
});
|
|
2042
2042
|
return applyEdits(content, edits);
|
|
2043
2043
|
}
|
|
2044
|
+
/** Strip default-valued fields from accounts.jsonc and return the lean
|
|
2045
|
+
* form. Called from the JSONC editor's "Lean" button so the user can
|
|
2046
|
+
* see the tidy version before saving. Round-trips through normalize→
|
|
2047
|
+
* denormalize so the canonicalization rules stay in one place
|
|
2048
|
+
* (mailx-settings). Drops port: 993, tls: true, auth: "password",
|
|
2049
|
+
* enabled: true, sig.html: false, etc. when they match the defaults
|
|
2050
|
+
* for the email's provider. Promotes a shared `name` to the file
|
|
2051
|
+
* level when every account has the same name. Only handles
|
|
2052
|
+
* accounts.jsonc — other JSONC files have hand-curated shapes. */
|
|
2053
|
+
async leanAccountsJsonc(content) {
|
|
2054
|
+
const { parse: parseJsonc } = await import("jsonc-parser");
|
|
2055
|
+
const errors = [];
|
|
2056
|
+
const cfg = parseJsonc(content, errors, { allowTrailingComma: true });
|
|
2057
|
+
if (errors.length)
|
|
2058
|
+
throw new Error(`JSONC parse error: ${errors.map((e) => e.error).join(", ")}`);
|
|
2059
|
+
const raw = cfg?.accounts || (Array.isArray(cfg) ? cfg : []);
|
|
2060
|
+
const globalName = cfg?.name;
|
|
2061
|
+
const normalized = raw.map((a) => normalizeAccount(a, globalName));
|
|
2062
|
+
const names = new Set(normalized.map((a) => a.name).filter(Boolean));
|
|
2063
|
+
const sharedName = names.size === 1 ? [...names][0] : globalName;
|
|
2064
|
+
const lean = normalized.map((a) => denormalizeAccount(a, sharedName));
|
|
2065
|
+
const payload = sharedName ? { name: sharedName, accounts: lean } : { accounts: lean };
|
|
2066
|
+
return JSON.stringify(payload, null, 2);
|
|
2067
|
+
}
|
|
2044
2068
|
/** Return the help section for a named config file, extracted from docs/config-help.md.
|
|
2045
2069
|
* Matches a level-2 heading whose text equals the filename. Returns markdown. */
|
|
2046
2070
|
async readConfigHelp(name) {
|
|
@@ -177,6 +177,8 @@ async function dispatchAction(svc, action, p) {
|
|
|
177
177
|
return await svc.unsubscribeOneClick(p.url);
|
|
178
178
|
case "openInWord":
|
|
179
179
|
return await svc.openInWord(p.editId, p.html);
|
|
180
|
+
case "leanAccountsJsonc":
|
|
181
|
+
return { content: await svc.leanAccountsJsonc(p.content) };
|
|
180
182
|
case "closeWordEdit":
|
|
181
183
|
return await svc.closeWordEdit(p.editId);
|
|
182
184
|
// Client-side tracing — lets webview / iframe code ship events to the
|
|
@@ -40,6 +40,10 @@ export declare function getStorageInfo(): {
|
|
|
40
40
|
cloudPath?: string;
|
|
41
41
|
cloudError?: string;
|
|
42
42
|
};
|
|
43
|
+
/** Fill in provider defaults for an account based on email domain.
|
|
44
|
+
* Exported so mailx-service's leanAccountsJsonc helper can reuse the same
|
|
45
|
+
* canonicalization rules that loadAccounts uses. */
|
|
46
|
+
export declare function normalizeAccount(acct: any, globalName?: string): AccountConfig;
|
|
43
47
|
declare const DEFAULT_PREFERENCES: {
|
|
44
48
|
ui: {
|
|
45
49
|
theme: "system" | "dark" | "light";
|
|
@@ -76,8 +80,27 @@ declare const DEFAULT_ALLOWLIST: {
|
|
|
76
80
|
export declare function loadAccounts(): AccountConfig[];
|
|
77
81
|
/** Load accounts with cloud API fallback (async — use when cloud settings may not be mounted) */
|
|
78
82
|
export declare function loadAccountsAsync(): Promise<AccountConfig[]>;
|
|
83
|
+
/** Strip default-valued fields from a normalized AccountConfig so the
|
|
84
|
+
* serialized JSONC stays compact and human-editable. The previous version
|
|
85
|
+
* was round-tripping every defaulted field (port: 993, tls: true, auth:
|
|
86
|
+
* "password", enabled: true, etc.), which bloated a 10-line accounts.jsonc
|
|
87
|
+
* to 60+ lines and embedded unnecessary "knowledge" about defaults.
|
|
88
|
+
*
|
|
89
|
+
* Rules:
|
|
90
|
+
* - Drop fields that match the provider default (host/port/tls/auth derived
|
|
91
|
+
* from email domain via PROVIDERS).
|
|
92
|
+
* - Drop `enabled: true` (default).
|
|
93
|
+
* - Drop `name` if equal to the file-level `globalName`.
|
|
94
|
+
* - Drop `imap.user` / `smtp.user` if they equal the email.
|
|
95
|
+
* - Drop `sig.html: false` (default; only keep when user enabled HTML sigs).
|
|
96
|
+
* - Keep field order: id → label → email → primary* → imap → smtp → defaultSend
|
|
97
|
+
* → relayDomains → deliveredToPrefix → identityDomains → syncContacts → sig.
|
|
98
|
+
* Curated for readability, not alphabetic. */
|
|
99
|
+
export declare function denormalizeAccount(acct: AccountConfig, globalName?: string): any;
|
|
79
100
|
/** Save account configs */
|
|
80
|
-
/** Save accounts — merges with cloud copy by email (multi-client safe)
|
|
101
|
+
/** Save accounts — merges with cloud copy by email (multi-client safe).
|
|
102
|
+
* Writes the lean form via denormalizeAccount so accounts.jsonc stays
|
|
103
|
+
* compact and human-editable. */
|
|
81
104
|
export declare function saveAccounts(accounts: AccountConfig[]): Promise<void>;
|
|
82
105
|
/** Load preferences (shared + local overrides, with legacy fallback) */
|
|
83
106
|
export declare function loadPreferences(): typeof DEFAULT_PREFERENCES;
|
|
@@ -339,8 +339,10 @@ const PROVIDERS = {
|
|
|
339
339
|
smtp: { host: "smtp.mail.me.com", port: 587, tls: true, auth: "password" },
|
|
340
340
|
},
|
|
341
341
|
};
|
|
342
|
-
/** Fill in provider defaults for an account based on email domain
|
|
343
|
-
|
|
342
|
+
/** Fill in provider defaults for an account based on email domain.
|
|
343
|
+
* Exported so mailx-service's leanAccountsJsonc helper can reuse the same
|
|
344
|
+
* canonicalization rules that loadAccounts uses. */
|
|
345
|
+
export function normalizeAccount(acct, globalName) {
|
|
344
346
|
const email = acct.email || "";
|
|
345
347
|
const localPart = email.split("@")[0]?.toLowerCase() || "";
|
|
346
348
|
const domain = email.split("@")[1]?.toLowerCase() || "";
|
|
@@ -552,8 +554,100 @@ export async function loadAccountsAsync() {
|
|
|
552
554
|
}
|
|
553
555
|
return [];
|
|
554
556
|
}
|
|
557
|
+
/** Strip default-valued fields from a normalized AccountConfig so the
|
|
558
|
+
* serialized JSONC stays compact and human-editable. The previous version
|
|
559
|
+
* was round-tripping every defaulted field (port: 993, tls: true, auth:
|
|
560
|
+
* "password", enabled: true, etc.), which bloated a 10-line accounts.jsonc
|
|
561
|
+
* to 60+ lines and embedded unnecessary "knowledge" about defaults.
|
|
562
|
+
*
|
|
563
|
+
* Rules:
|
|
564
|
+
* - Drop fields that match the provider default (host/port/tls/auth derived
|
|
565
|
+
* from email domain via PROVIDERS).
|
|
566
|
+
* - Drop `enabled: true` (default).
|
|
567
|
+
* - Drop `name` if equal to the file-level `globalName`.
|
|
568
|
+
* - Drop `imap.user` / `smtp.user` if they equal the email.
|
|
569
|
+
* - Drop `sig.html: false` (default; only keep when user enabled HTML sigs).
|
|
570
|
+
* - Keep field order: id → label → email → primary* → imap → smtp → defaultSend
|
|
571
|
+
* → relayDomains → deliveredToPrefix → identityDomains → syncContacts → sig.
|
|
572
|
+
* Curated for readability, not alphabetic. */
|
|
573
|
+
export function denormalizeAccount(acct, globalName) {
|
|
574
|
+
const domain = (acct.email || "").split("@")[1]?.toLowerCase() || "";
|
|
575
|
+
const provider = PROVIDERS[domain];
|
|
576
|
+
const out = {};
|
|
577
|
+
out.id = acct.id;
|
|
578
|
+
if (acct.label && acct.label !== provider?.label && acct.label !== acct.id)
|
|
579
|
+
out.label = acct.label;
|
|
580
|
+
out.email = acct.email;
|
|
581
|
+
if (acct.name && acct.name !== globalName)
|
|
582
|
+
out.name = acct.name;
|
|
583
|
+
if (acct.primary)
|
|
584
|
+
out.primary = true;
|
|
585
|
+
if (acct.primaryCalendar !== undefined)
|
|
586
|
+
out.primaryCalendar = acct.primaryCalendar;
|
|
587
|
+
if (acct.primaryTasks !== undefined)
|
|
588
|
+
out.primaryTasks = acct.primaryTasks;
|
|
589
|
+
if (acct.primaryContacts !== undefined)
|
|
590
|
+
out.primaryContacts = acct.primaryContacts;
|
|
591
|
+
// imap — keep only fields that differ from provider defaults.
|
|
592
|
+
const imapOut = {};
|
|
593
|
+
if (acct.imap?.host && acct.imap.host !== provider?.imap.host)
|
|
594
|
+
imapOut.host = acct.imap.host;
|
|
595
|
+
if (acct.imap?.user && acct.imap.user !== acct.email)
|
|
596
|
+
imapOut.user = acct.imap.user;
|
|
597
|
+
if (acct.imap?.password)
|
|
598
|
+
imapOut.password = acct.imap.password;
|
|
599
|
+
if (acct.imap?.port && acct.imap.port !== (provider?.imap.port ?? 993))
|
|
600
|
+
imapOut.port = acct.imap.port;
|
|
601
|
+
if (acct.imap?.tls !== undefined && acct.imap.tls !== (provider?.imap.tls ?? true))
|
|
602
|
+
imapOut.tls = acct.imap.tls;
|
|
603
|
+
if (acct.imap?.auth && acct.imap.auth !== (provider?.imap.auth ?? "password"))
|
|
604
|
+
imapOut.auth = acct.imap.auth;
|
|
605
|
+
if (Object.keys(imapOut).length > 0)
|
|
606
|
+
out.imap = imapOut;
|
|
607
|
+
// smtp — same treatment.
|
|
608
|
+
const smtpOut = {};
|
|
609
|
+
if (acct.smtp?.host && acct.smtp.host !== provider?.smtp.host)
|
|
610
|
+
smtpOut.host = acct.smtp.host;
|
|
611
|
+
if (acct.smtp?.user && acct.smtp.user !== acct.email && acct.smtp.user !== acct.imap?.user)
|
|
612
|
+
smtpOut.user = acct.smtp.user;
|
|
613
|
+
if (acct.smtp?.password)
|
|
614
|
+
smtpOut.password = acct.smtp.password;
|
|
615
|
+
if (acct.smtp?.port && acct.smtp.port !== (provider?.smtp.port ?? 587))
|
|
616
|
+
smtpOut.port = acct.smtp.port;
|
|
617
|
+
if (acct.smtp?.tls !== undefined && acct.smtp.tls !== (provider?.smtp.tls ?? true))
|
|
618
|
+
smtpOut.tls = acct.smtp.tls;
|
|
619
|
+
if (acct.smtp?.auth && acct.smtp.auth !== (provider?.smtp.auth ?? "password"))
|
|
620
|
+
smtpOut.auth = acct.smtp.auth;
|
|
621
|
+
if (Object.keys(smtpOut).length > 0)
|
|
622
|
+
out.smtp = smtpOut;
|
|
623
|
+
if (acct.defaultSend)
|
|
624
|
+
out.defaultSend = true;
|
|
625
|
+
if (acct.enabled === false)
|
|
626
|
+
out.enabled = false; // default true → omit
|
|
627
|
+
if (acct.relayDomains && acct.relayDomains.length > 0)
|
|
628
|
+
out.relayDomains = acct.relayDomains;
|
|
629
|
+
if (acct.deliveredToPrefix && acct.deliveredToPrefix.length > 0)
|
|
630
|
+
out.deliveredToPrefix = acct.deliveredToPrefix;
|
|
631
|
+
if (acct.identityDomains && acct.identityDomains.length > 0)
|
|
632
|
+
out.identityDomains = acct.identityDomains;
|
|
633
|
+
// syncContacts default: true for OAuth, false otherwise. Only emit when
|
|
634
|
+
// the user overrode the default.
|
|
635
|
+
const syncContactsDefault = provider?.imap.auth === "oauth2";
|
|
636
|
+
if (acct.syncContacts !== undefined && acct.syncContacts !== syncContactsDefault) {
|
|
637
|
+
out.syncContacts = acct.syncContacts;
|
|
638
|
+
}
|
|
639
|
+
if (acct.signature)
|
|
640
|
+
out.signature = acct.signature;
|
|
641
|
+
if (acct.sig?.text) {
|
|
642
|
+
// html: false is default; only keep the html flag when explicitly true.
|
|
643
|
+
out.sig = acct.sig.html ? { text: acct.sig.text, html: true } : { text: acct.sig.text };
|
|
644
|
+
}
|
|
645
|
+
return out;
|
|
646
|
+
}
|
|
555
647
|
/** Save account configs */
|
|
556
|
-
/** Save accounts — merges with cloud copy by email (multi-client safe)
|
|
648
|
+
/** Save accounts — merges with cloud copy by email (multi-client safe).
|
|
649
|
+
* Writes the lean form via denormalizeAccount so accounts.jsonc stays
|
|
650
|
+
* compact and human-editable. */
|
|
557
651
|
export async function saveAccounts(accounts) {
|
|
558
652
|
// Merge with cloud: keep all accounts, deduplicate by normalized email
|
|
559
653
|
try {
|
|
@@ -565,7 +659,9 @@ export async function saveAccounts(accounts) {
|
|
|
565
659
|
const seen = new Set(accounts.map(a => normalizeEmail(a.email)));
|
|
566
660
|
for (const ca of cloudAccts) {
|
|
567
661
|
if (ca.email && !seen.has(normalizeEmail(ca.email))) {
|
|
568
|
-
|
|
662
|
+
// Cloud entries are already lean — feed through
|
|
663
|
+
// normalizeAccount to coerce to AccountConfig shape.
|
|
664
|
+
accounts.push(normalizeAccount(ca));
|
|
569
665
|
seen.add(normalizeEmail(ca.email));
|
|
570
666
|
}
|
|
571
667
|
}
|
|
@@ -573,7 +669,14 @@ export async function saveAccounts(accounts) {
|
|
|
573
669
|
}
|
|
574
670
|
}
|
|
575
671
|
catch { /* cloud read failed — save local version */ }
|
|
576
|
-
|
|
672
|
+
// Promote a shared "name" to file level when every account has the same
|
|
673
|
+
// name — keeps the JSONC tidy ({ "name": "Bob Frankston", "accounts": [...] }
|
|
674
|
+
// instead of repeating "name" on each entry).
|
|
675
|
+
const names = new Set(accounts.map(a => a.name).filter(Boolean));
|
|
676
|
+
const globalName = names.size === 1 ? [...names][0] : undefined;
|
|
677
|
+
const lean = accounts.map(a => denormalizeAccount(a, globalName));
|
|
678
|
+
const payload = globalName ? { name: globalName, accounts: lean } : { accounts: lean };
|
|
679
|
+
saveFile("accounts.jsonc", payload);
|
|
577
680
|
}
|
|
578
681
|
/** Load preferences (shared + local overrides, with legacy fallback) */
|
|
579
682
|
export function loadPreferences() {
|