@bobfrankston/mailx 1.0.36 → 1.0.38
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/README.md +80 -48
- package/bin/mailx.js +23 -5
- package/client/app.js +8 -0
- package/client/components/folder-tree.js +10 -1
- package/client/styles/variables.css +27 -8
- package/package.json +2 -2
- package/packages/mailx-server/index.js +1 -1
- package/packages/mailx-settings/index.d.ts +2 -2
- package/packages/mailx-settings/index.js +72 -5
- package/packages/mailx-types/index.d.ts +1 -1
package/README.md
CHANGED
|
@@ -29,95 +29,127 @@ If you already have mailx configured on another machine with settings on OneDriv
|
|
|
29
29
|
|
|
30
30
|
Once OneDrive syncs, mailx picks up your accounts automatically.
|
|
31
31
|
|
|
32
|
-
### Option B: New user --
|
|
32
|
+
### Option B: New user -- single machine, local settings
|
|
33
33
|
|
|
34
|
-
**Step 1.** Create
|
|
34
|
+
**Step 1.** Create `~/.mailx/settings.jsonc` with your account(s):
|
|
35
35
|
|
|
36
|
+
(On Windows `~` means `%USERPROFILE%`, e.g., `C:\Users\You\.mailx\settings.jsonc`)
|
|
37
|
+
|
|
38
|
+
**Gmail -- minimal config (just your email):**
|
|
39
|
+
```jsonc
|
|
40
|
+
{
|
|
41
|
+
"accounts": [
|
|
42
|
+
{ "email": "you@gmail.com" }
|
|
43
|
+
]
|
|
44
|
+
}
|
|
36
45
|
```
|
|
37
|
-
mkdir ~/.mailx
|
|
38
|
-
```
|
|
39
46
|
|
|
40
|
-
|
|
47
|
+
mailx auto-fills Gmail's IMAP/SMTP/OAuth settings. First run opens a browser for OAuth consent.
|
|
48
|
+
|
|
49
|
+
**Standard IMAP -- just host and password:**
|
|
50
|
+
```jsonc
|
|
51
|
+
{
|
|
52
|
+
"accounts": [
|
|
53
|
+
{
|
|
54
|
+
"email": "you@example.com",
|
|
55
|
+
"password": "your-password",
|
|
56
|
+
"imap": { "host": "imap.example.com" },
|
|
57
|
+
"smtp": { "host": "smtp.example.com" }
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
```
|
|
41
62
|
|
|
42
|
-
|
|
63
|
+
Defaults: port 993/587, TLS on, auth password, username = email address. Only provide fields that differ from defaults.
|
|
43
64
|
|
|
65
|
+
**Full config with all optional fields:**
|
|
44
66
|
```jsonc
|
|
45
67
|
{
|
|
46
68
|
"accounts": [
|
|
47
69
|
{
|
|
48
|
-
"id": "mymail", // Internal ID (
|
|
49
|
-
"name": "Your Name", //
|
|
50
|
-
"label": "Work", //
|
|
51
|
-
"email": "you@example.com",
|
|
70
|
+
"id": "mymail", // Internal ID (default: domain name)
|
|
71
|
+
"name": "Your Name", // From: header name (default: local part of email)
|
|
72
|
+
"label": "Work", // UI display label (default: name)
|
|
73
|
+
"email": "you@example.com",
|
|
52
74
|
"imap": {
|
|
53
|
-
"host": "imap.example.com", //
|
|
54
|
-
"port": 993, //
|
|
55
|
-
"tls": true,
|
|
56
|
-
"auth": "password", // "password" or "oauth2"
|
|
57
|
-
"user": "you@example.com", //
|
|
58
|
-
"password": "your-password"
|
|
75
|
+
"host": "imap.example.com", // Default: imap.{domain}
|
|
76
|
+
"port": 993, // Default: 993
|
|
77
|
+
"tls": true, // Default: true
|
|
78
|
+
"auth": "password", // Default: "password" (or "oauth2" for Gmail/Outlook)
|
|
79
|
+
"user": "you@example.com", // Default: email address
|
|
80
|
+
"password": "your-password"
|
|
59
81
|
},
|
|
60
82
|
"smtp": {
|
|
61
|
-
"host": "smtp.example.com", //
|
|
62
|
-
"port": 587, //
|
|
63
|
-
"tls": true,
|
|
83
|
+
"host": "smtp.example.com", // Default: smtp.{domain}
|
|
84
|
+
"port": 587, // Default: 587
|
|
85
|
+
"tls": true, // Default: true
|
|
64
86
|
"auth": "password",
|
|
65
87
|
"user": "you@example.com",
|
|
66
88
|
"password": "your-password"
|
|
67
89
|
},
|
|
68
|
-
"enabled": true,
|
|
69
|
-
"defaultSend": true
|
|
90
|
+
"enabled": true, // Default: true
|
|
91
|
+
"defaultSend": true, // Use this account when From doesn't match
|
|
92
|
+
"relayDomains": [], // Domains to skip in Delivered-To chain
|
|
93
|
+
"deliveredToPrefix": [] // Prefixes to strip from Delivered-To alias
|
|
70
94
|
}
|
|
71
95
|
],
|
|
72
96
|
"sync": {
|
|
73
|
-
"intervalMinutes": 5, //
|
|
74
|
-
"historyDays": 0 //
|
|
97
|
+
"intervalMinutes": 5, // Default: 5
|
|
98
|
+
"historyDays": 0 // Default: 30 (0 = all)
|
|
99
|
+
},
|
|
100
|
+
"ui": {
|
|
101
|
+
"theme": "system" // "system" (default), "dark", or "light"
|
|
75
102
|
}
|
|
76
103
|
}
|
|
77
104
|
```
|
|
78
105
|
|
|
79
|
-
**
|
|
106
|
+
**Known providers with automatic defaults:** Gmail, Google, Outlook, Hotmail, Yahoo, iCloud. For these, only `email` is required.
|
|
107
|
+
|
|
108
|
+
No `config.jsonc` needed -- when it doesn't exist, mailx reads settings directly from `~/.mailx/`.
|
|
109
|
+
|
|
110
|
+
**Step 2.** Run `mailx` and open `http://127.0.0.1:9333` in your browser.
|
|
80
111
|
|
|
81
112
|
### Option C: Multi-machine with shared settings (OneDrive/Dropbox)
|
|
82
113
|
|
|
83
|
-
|
|
114
|
+
To share settings across machines, put `settings.jsonc` in a cloud-synced folder and create a local pointer file.
|
|
115
|
+
|
|
116
|
+
**Step 1.** Move `settings.jsonc` to a shared location:
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
# Example: OneDrive
|
|
120
|
+
mkdir "%OneDrive%\home\.mailx"
|
|
121
|
+
move "%USERPROFILE%\.mailx\settings.jsonc" "%OneDrive%\home\.mailx\settings.jsonc"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Step 2.** Create `~/.mailx/config.jsonc` pointing to the shared location:
|
|
84
125
|
|
|
85
|
-
**`~/.mailx/config.jsonc`:**
|
|
86
126
|
```jsonc
|
|
87
127
|
{
|
|
88
|
-
//
|
|
128
|
+
// Where the shared settings live (OneDrive, Dropbox, network share, etc.)
|
|
89
129
|
"sharedDir": "C:/Users/You/OneDrive/home/.mailx",
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
"historyDays": 90
|
|
130
|
+
|
|
131
|
+
// Local-only settings (not synced):
|
|
132
|
+
"storePath": "C:/Users/You/.mailx/mailxstore", // cached message bodies
|
|
133
|
+
"historyDays": 90 // override sync depth per machine
|
|
94
134
|
}
|
|
95
135
|
```
|
|
96
136
|
|
|
97
|
-
|
|
137
|
+
**How it works:**
|
|
138
|
+
- `config.jsonc` is local to each machine -- never synced
|
|
139
|
+
- `sharedDir` points to the cloud folder containing `settings.jsonc`, `allowlist.jsonc`, `preferences.jsonc`
|
|
140
|
+
- mailx caches shared files locally for offline use
|
|
141
|
+
- `historyDays` in `config.jsonc` overrides the shared default per machine
|
|
142
|
+
- On new machines, if `config.jsonc` doesn't exist, mailx auto-detects OneDrive at `%OneDrive%/home/.mailx/`
|
|
98
143
|
|
|
99
|
-
###
|
|
144
|
+
### Gmail OAuth setup
|
|
100
145
|
|
|
101
|
-
Gmail
|
|
146
|
+
Gmail accounts auto-configure -- just add `{ "email": "you@gmail.com" }` to accounts. But OAuth requires a one-time Google Cloud setup:
|
|
102
147
|
|
|
103
148
|
1. Go to [Google Cloud Console](https://console.cloud.google.com/) and create a project
|
|
104
149
|
2. Enable the **Gmail API** (and **People API** for contacts)
|
|
105
150
|
3. Create **OAuth 2.0 credentials** (Desktop app type)
|
|
106
151
|
4. Download `credentials.json` to the iflow package directory
|
|
107
|
-
5.
|
|
108
|
-
6. First connection opens a browser for OAuth consent. Tokens are cached and refresh automatically.
|
|
109
|
-
|
|
110
|
-
```jsonc
|
|
111
|
-
{
|
|
112
|
-
"id": "gmail",
|
|
113
|
-
"name": "Your Name",
|
|
114
|
-
"label": "Gmail",
|
|
115
|
-
"email": "you@gmail.com",
|
|
116
|
-
"imap": { "host": "imap.gmail.com", "port": 993, "tls": true, "auth": "oauth2", "user": "you@gmail.com" },
|
|
117
|
-
"smtp": { "host": "smtp.gmail.com", "port": 587, "tls": true, "auth": "oauth2", "user": "you@gmail.com" },
|
|
118
|
-
"enabled": true
|
|
119
|
-
}
|
|
120
|
-
```
|
|
152
|
+
5. First connection opens a browser for OAuth consent. Tokens are cached and refresh automatically.
|
|
121
153
|
|
|
122
154
|
## Usage
|
|
123
155
|
|
package/bin/mailx.js
CHANGED
|
@@ -172,16 +172,34 @@ async function main() {
|
|
|
172
172
|
|
|
173
173
|
let launcherPath = launcherPaths.find(p => fs.existsSync(p));
|
|
174
174
|
|
|
175
|
+
// On Linux, skip native launcher if no display server available
|
|
176
|
+
if (launcherPath && process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
|
|
177
|
+
log("No display server (DISPLAY/WAYLAND_DISPLAY not set) — skipping native launcher");
|
|
178
|
+
launcherPath = undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
175
181
|
if (launcherPath) {
|
|
176
182
|
console.log("Starting mailx...");
|
|
177
183
|
log(`Launching: ${launcherPath}`);
|
|
178
184
|
const { spawn } = await import("node:child_process");
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
185
|
+
try {
|
|
186
|
+
const child = spawn(launcherPath, args, { detached: true, stdio: "ignore" });
|
|
187
|
+
child.on("error", () => {
|
|
188
|
+
console.log("Native launcher failed, starting in browser mode...");
|
|
189
|
+
process.argv.push("--server");
|
|
190
|
+
main();
|
|
191
|
+
});
|
|
192
|
+
child.unref();
|
|
193
|
+
console.log("mailx launched");
|
|
194
|
+
} catch (e) {
|
|
195
|
+
console.log(`Native launcher failed: ${e.message}`);
|
|
196
|
+
console.log("Starting in browser mode...");
|
|
197
|
+
process.argv.push("--server");
|
|
198
|
+
await main();
|
|
199
|
+
}
|
|
182
200
|
} else {
|
|
183
|
-
console.log("
|
|
184
|
-
log("
|
|
201
|
+
console.log("Starting in browser mode...");
|
|
202
|
+
log("No native launcher — falling back to --server mode");
|
|
185
203
|
process.argv.push("--server");
|
|
186
204
|
await main(); // recurse with --server
|
|
187
205
|
}
|
package/client/app.js
CHANGED
|
@@ -639,6 +639,14 @@ setInterval(async () => {
|
|
|
639
639
|
}, 5000);
|
|
640
640
|
console.log("mailx client initialized, location:", location.href);
|
|
641
641
|
updateNewMessageCount();
|
|
642
|
+
// ── Apply theme from settings ──
|
|
643
|
+
fetch("/api/version").then(r => r.json()).then(d => {
|
|
644
|
+
if (d.theme === "dark")
|
|
645
|
+
document.documentElement.classList.add("theme-dark");
|
|
646
|
+
else if (d.theme === "light")
|
|
647
|
+
document.documentElement.classList.add("theme-light");
|
|
648
|
+
// "system" or missing = no class, CSS media query handles it
|
|
649
|
+
}).catch(() => { });
|
|
642
650
|
// Diagnostic: test API connectivity (helps debug WebView2 blank screen)
|
|
643
651
|
fetch("/api/version").then(r => r.json()).then(d => {
|
|
644
652
|
console.log("API reachable:", d);
|
|
@@ -387,7 +387,16 @@ async function loadFolderTree(container) {
|
|
|
387
387
|
// Re-select previous folder on refresh, or auto-select Inbox on first load
|
|
388
388
|
const allFolderEls = container.querySelectorAll('.ft-folder');
|
|
389
389
|
let target = null;
|
|
390
|
-
if (
|
|
390
|
+
if (selectedFolderId === -1) {
|
|
391
|
+
// Unified inbox was selected — just re-highlight it, don't click
|
|
392
|
+
const unified = container.querySelector('.ft-unified');
|
|
393
|
+
if (unified) {
|
|
394
|
+
unified.classList.add("selected");
|
|
395
|
+
selectedElement = unified;
|
|
396
|
+
target = unified;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
else if (selectedAccountId && selectedFolderId !== null && selectedFolderId >= 0) {
|
|
391
400
|
for (const f of allFolderEls) {
|
|
392
401
|
const el = f;
|
|
393
402
|
if (el.dataset.accountId === selectedAccountId && el.dataset.folderId === String(selectedFolderId)) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* mailx design tokens -- all theming via custom properties */
|
|
2
|
+
/* Theme controlled by class on <html>: theme-dark, theme-light, or absent (system) */
|
|
2
3
|
|
|
3
4
|
:root {
|
|
4
5
|
/* Layout */
|
|
@@ -8,19 +9,19 @@
|
|
|
8
9
|
--splitter-size: 5px;
|
|
9
10
|
--list-viewer-split: 40%;
|
|
10
11
|
|
|
11
|
-
/* Colors
|
|
12
|
-
--color-brand: oklch(0.88 0.04 240);
|
|
13
|
-
--color-brand-dark: oklch(0.45 0.10 240);
|
|
12
|
+
/* Colors — dark by default */
|
|
13
|
+
--color-brand: oklch(0.88 0.04 240);
|
|
14
|
+
--color-brand-dark: oklch(0.45 0.10 240);
|
|
14
15
|
--color-bg: oklch(0.20 0.02 270);
|
|
15
16
|
--color-bg-surface: oklch(0.25 0.03 270);
|
|
16
17
|
--color-bg-hover: color-mix(in oklch, var(--color-bg-surface) 80%, white);
|
|
17
18
|
--color-bg-selected: var(--color-brand);
|
|
18
19
|
--color-bg-toolbar: oklch(0.22 0.02 270);
|
|
19
|
-
--color-text: oklch(0.
|
|
20
|
-
--color-text-muted: oklch(0.
|
|
20
|
+
--color-text: oklch(0.92 0.01 270);
|
|
21
|
+
--color-text-muted: oklch(0.68 0.02 270);
|
|
21
22
|
--color-accent: var(--color-brand-dark);
|
|
22
23
|
--color-unread: oklch(0.95 0.02 270);
|
|
23
|
-
--color-border: oklch(0.
|
|
24
|
+
--color-border: oklch(0.40 0.02 270);
|
|
24
25
|
--color-danger: oklch(0.65 0.15 15);
|
|
25
26
|
--color-success: oklch(0.65 0.15 145);
|
|
26
27
|
--color-badge: oklch(0.60 0.18 250);
|
|
@@ -46,9 +47,26 @@
|
|
|
46
47
|
color-scheme: dark light;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
/* Light
|
|
50
|
+
/* Light overrides */
|
|
51
|
+
.theme-light {
|
|
52
|
+
--color-bg: oklch(0.97 0.005 270);
|
|
53
|
+
--color-bg-surface: oklch(1.0 0 0);
|
|
54
|
+
--color-bg-hover: oklch(0.95 0.005 270);
|
|
55
|
+
--color-brand: oklch(0.88 0.04 240);
|
|
56
|
+
--color-brand-dark: oklch(0.35 0.12 240);
|
|
57
|
+
--color-bg-selected: var(--color-brand);
|
|
58
|
+
--color-bg-toolbar: oklch(0.95 0.005 270);
|
|
59
|
+
--color-text: oklch(0.20 0.02 270);
|
|
60
|
+
--color-text-muted: oklch(0.45 0.02 270);
|
|
61
|
+
--color-accent: var(--color-brand-dark);
|
|
62
|
+
--color-unread: oklch(0.10 0.02 270);
|
|
63
|
+
--color-border: oklch(0.85 0.01 270);
|
|
64
|
+
color-scheme: light;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* System preference: light — apply when no explicit theme class */
|
|
50
68
|
@media (prefers-color-scheme: light) {
|
|
51
|
-
:root {
|
|
69
|
+
:root:not(.theme-dark):not(.theme-light) {
|
|
52
70
|
--color-bg: oklch(0.97 0.005 270);
|
|
53
71
|
--color-bg-surface: oklch(1.0 0 0);
|
|
54
72
|
--color-bg-hover: oklch(0.95 0.005 270);
|
|
@@ -61,5 +79,6 @@
|
|
|
61
79
|
--color-accent: var(--color-brand-dark);
|
|
62
80
|
--color-unread: oklch(0.10 0.02 270);
|
|
63
81
|
--color-border: oklch(0.85 0.01 270);
|
|
82
|
+
color-scheme: light;
|
|
64
83
|
}
|
|
65
84
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.38",
|
|
4
4
|
"description": "Local-first email client with IMAP sync and standalone native app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/mailx.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"postinstall": "node launcher/builder/postinstall.js"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@bobfrankston/iflow": "^1.0.
|
|
23
|
+
"@bobfrankston/iflow": "^1.0.9",
|
|
24
24
|
"@bobfrankston/miscinfo": "^1.0.6",
|
|
25
25
|
"@bobfrankston/oauthsupport": "^1.0.11",
|
|
26
26
|
"@bobfrankston/rust-builder": "^0.1.2",
|
|
@@ -81,7 +81,7 @@ app.use("/node_modules", express.static(path.join(rootDir, "node_modules"), { et
|
|
|
81
81
|
// Mount API
|
|
82
82
|
const apiRouter = createApiRouter(db, imapManager);
|
|
83
83
|
app.use("/api", apiRouter);
|
|
84
|
-
app.get("/api/version", (req, res) => res.json({ server: SERVER_VERSION, client: CLIENT_VERSION }));
|
|
84
|
+
app.get("/api/version", (req, res) => res.json({ server: SERVER_VERSION, client: CLIENT_VERSION, theme: settings.ui?.theme || "system" }));
|
|
85
85
|
app.get("/status", (req, res) => {
|
|
86
86
|
const accounts = db.getAccounts();
|
|
87
87
|
const pendingSync = db.getTotalPendingSyncCount();
|
|
@@ -20,7 +20,7 @@ declare const LOCAL_DIR: string;
|
|
|
20
20
|
declare function getSharedDir(): string;
|
|
21
21
|
declare const DEFAULT_PREFERENCES: {
|
|
22
22
|
ui: {
|
|
23
|
-
theme: "dark" | "light";
|
|
23
|
+
theme: "system" | "dark" | "light";
|
|
24
24
|
folderWidth: number;
|
|
25
25
|
listViewerSplit: number;
|
|
26
26
|
fontSize: number;
|
|
@@ -39,7 +39,7 @@ declare const DEFAULT_ALLOWLIST: {
|
|
|
39
39
|
export declare function loadAccounts(): AccountConfig[];
|
|
40
40
|
/** Save account configs */
|
|
41
41
|
export declare function saveAccounts(accounts: AccountConfig[]): void;
|
|
42
|
-
/** Load preferences (shared + local overrides) */
|
|
42
|
+
/** Load preferences (shared + local overrides, with legacy fallback) */
|
|
43
43
|
export declare function loadPreferences(): typeof DEFAULT_PREFERENCES;
|
|
44
44
|
/** Save preferences */
|
|
45
45
|
export declare function savePreferences(prefs: typeof DEFAULT_PREFERENCES): void;
|
|
@@ -105,11 +105,70 @@ function saveFile(filename, data) {
|
|
|
105
105
|
catch { /* ignore */ }
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
+
const PROVIDERS = {
|
|
109
|
+
"gmail.com": {
|
|
110
|
+
imap: { host: "imap.gmail.com", port: 993, tls: true, auth: "oauth2" },
|
|
111
|
+
smtp: { host: "smtp.gmail.com", port: 587, tls: true, auth: "oauth2" },
|
|
112
|
+
},
|
|
113
|
+
"googlemail.com": {
|
|
114
|
+
imap: { host: "imap.gmail.com", port: 993, tls: true, auth: "oauth2" },
|
|
115
|
+
smtp: { host: "smtp.gmail.com", port: 587, tls: true, auth: "oauth2" },
|
|
116
|
+
},
|
|
117
|
+
"outlook.com": {
|
|
118
|
+
imap: { host: "outlook.office365.com", port: 993, tls: true, auth: "oauth2" },
|
|
119
|
+
smtp: { host: "smtp.office365.com", port: 587, tls: true, auth: "oauth2" },
|
|
120
|
+
},
|
|
121
|
+
"hotmail.com": {
|
|
122
|
+
imap: { host: "outlook.office365.com", port: 993, tls: true, auth: "oauth2" },
|
|
123
|
+
smtp: { host: "smtp.office365.com", port: 587, tls: true, auth: "oauth2" },
|
|
124
|
+
},
|
|
125
|
+
"yahoo.com": {
|
|
126
|
+
imap: { host: "imap.mail.yahoo.com", port: 993, tls: true, auth: "password" },
|
|
127
|
+
smtp: { host: "smtp.mail.yahoo.com", port: 587, tls: true, auth: "password" },
|
|
128
|
+
},
|
|
129
|
+
"icloud.com": {
|
|
130
|
+
imap: { host: "imap.mail.me.com", port: 993, tls: true, auth: "password" },
|
|
131
|
+
smtp: { host: "smtp.mail.me.com", port: 587, tls: true, auth: "password" },
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
/** Fill in provider defaults for an account based on email domain */
|
|
135
|
+
function normalizeAccount(acct, globalName) {
|
|
136
|
+
const email = acct.email || "";
|
|
137
|
+
const domain = email.split("@")[1]?.toLowerCase() || "";
|
|
138
|
+
const provider = PROVIDERS[domain];
|
|
139
|
+
const user = acct.imap?.user || acct.user || email;
|
|
140
|
+
return {
|
|
141
|
+
id: acct.id || domain.split(".")[0] || "account",
|
|
142
|
+
name: acct.name || globalName || email.split("@")[0],
|
|
143
|
+
label: acct.label,
|
|
144
|
+
email,
|
|
145
|
+
imap: {
|
|
146
|
+
host: acct.imap?.host || provider?.imap.host || `imap.${domain}`,
|
|
147
|
+
port: acct.imap?.port || provider?.imap.port || 993,
|
|
148
|
+
tls: acct.imap?.tls ?? provider?.imap.tls ?? true,
|
|
149
|
+
auth: acct.imap?.auth || provider?.imap.auth || "password",
|
|
150
|
+
user: acct.imap?.user || user,
|
|
151
|
+
password: acct.imap?.password || acct.password,
|
|
152
|
+
},
|
|
153
|
+
smtp: {
|
|
154
|
+
host: acct.smtp?.host || provider?.smtp.host || `smtp.${domain}`,
|
|
155
|
+
port: acct.smtp?.port || provider?.smtp.port || 587,
|
|
156
|
+
tls: acct.smtp?.tls ?? provider?.smtp.tls ?? true,
|
|
157
|
+
auth: acct.smtp?.auth || provider?.smtp.auth || "password",
|
|
158
|
+
user: acct.smtp?.user || user,
|
|
159
|
+
password: acct.smtp?.password || acct.password,
|
|
160
|
+
},
|
|
161
|
+
enabled: acct.enabled ?? true,
|
|
162
|
+
defaultSend: acct.defaultSend,
|
|
163
|
+
relayDomains: acct.relayDomains,
|
|
164
|
+
deliveredToPrefix: acct.deliveredToPrefix,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
108
167
|
// ── Defaults ──
|
|
109
168
|
const DEFAULT_ACCOUNTS = [];
|
|
110
169
|
const DEFAULT_PREFERENCES = {
|
|
111
170
|
ui: {
|
|
112
|
-
theme: "
|
|
171
|
+
theme: "system",
|
|
113
172
|
folderWidth: 220,
|
|
114
173
|
listViewerSplit: 40,
|
|
115
174
|
fontSize: 15,
|
|
@@ -143,21 +202,29 @@ export function loadAccounts() {
|
|
|
143
202
|
}
|
|
144
203
|
catch { /* ignore */ }
|
|
145
204
|
}
|
|
146
|
-
|
|
205
|
+
const raw = accounts.accounts || accounts;
|
|
206
|
+
const globalName = accounts.name || "";
|
|
207
|
+
return raw.map((a) => normalizeAccount(a, globalName));
|
|
147
208
|
}
|
|
148
209
|
// Legacy: read from settings.jsonc
|
|
149
210
|
const legacy = loadLegacySettings();
|
|
150
211
|
if (legacy?.accounts)
|
|
151
|
-
return legacy.accounts;
|
|
212
|
+
return legacy.accounts.map((a) => normalizeAccount(a, legacy.name));
|
|
152
213
|
return DEFAULT_ACCOUNTS;
|
|
153
214
|
}
|
|
154
215
|
/** Save account configs */
|
|
155
216
|
export function saveAccounts(accounts) {
|
|
156
217
|
saveFile("accounts.jsonc", { accounts });
|
|
157
218
|
}
|
|
158
|
-
/** Load preferences (shared + local overrides) */
|
|
219
|
+
/** Load preferences (shared + local overrides, with legacy fallback) */
|
|
159
220
|
export function loadPreferences() {
|
|
160
|
-
|
|
221
|
+
let shared = loadFile("preferences.jsonc", DEFAULT_PREFERENCES);
|
|
222
|
+
// Legacy fallback: read ui/sync from settings.jsonc if preferences.jsonc had only defaults
|
|
223
|
+
const legacy = loadLegacySettings();
|
|
224
|
+
if (legacy?.ui)
|
|
225
|
+
shared = { ...shared, ui: { ...shared.ui, ...legacy.ui } };
|
|
226
|
+
if (legacy?.sync)
|
|
227
|
+
shared = { ...shared, sync: { ...shared.sync, ...legacy.sync } };
|
|
161
228
|
const localConfig = readLocalConfig();
|
|
162
229
|
// Local overrides
|
|
163
230
|
if (localConfig.historyDays !== undefined) {
|
|
@@ -175,7 +175,7 @@ export type WsEvent = {
|
|
|
175
175
|
export interface MailxSettings {
|
|
176
176
|
accounts: AccountConfig[];
|
|
177
177
|
ui: {
|
|
178
|
-
theme: "dark" | "light";
|
|
178
|
+
theme: "system" | "dark" | "light";
|
|
179
179
|
folderWidth: number;
|
|
180
180
|
listViewerSplit: number; /** Percentage for message list height */
|
|
181
181
|
fontSize: number;
|