@bobfrankston/mailx 1.0.36 → 1.0.37

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 CHANGED
@@ -29,17 +29,11 @@ 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 -- set up from scratch
32
+ ### Option B: New user -- single machine, local settings
33
33
 
34
- **Step 1.** Create the settings directory:
34
+ **Step 1.** Create `~/.mailx/settings.jsonc` with your account(s):
35
35
 
36
- ```
37
- mkdir ~/.mailx
38
- ```
39
-
40
- (On Windows: `mkdir %USERPROFILE%\.mailx`)
41
-
42
- **Step 2.** Create `~/.mailx/settings.jsonc` with your account(s):
36
+ (On Windows `~` means `%USERPROFILE%`, e.g., `C:\Users\You\.mailx\settings.jsonc`)
43
37
 
44
38
  ```jsonc
45
39
  {
@@ -66,35 +60,57 @@ mkdir ~/.mailx
66
60
  "password": "your-password"
67
61
  },
68
62
  "enabled": true,
69
- "defaultSend": true // Use this account for sending when From doesn't match
63
+ "defaultSend": true, // Use this account for sending when From doesn't match
64
+ "relayDomains": [], // Domains to skip in Delivered-To (optional)
65
+ "deliveredToPrefix": [] // Prefixes to strip from Delivered-To alias (optional)
70
66
  }
71
67
  ],
72
68
  "sync": {
73
69
  "intervalMinutes": 5, // Full sync interval (minutes)
74
70
  "historyDays": 0 // Days of history to sync (0 = all)
71
+ },
72
+ "ui": {
73
+ "theme": "system", // "system", "dark", or "light"
74
+ "fontSize": 15
75
75
  }
76
76
  }
77
77
  ```
78
78
 
79
- **Step 3.** Run `mailx` and open `http://127.0.0.1:9333` in your browser.
79
+ No `config.jsonc` needed -- when it doesn't exist, mailx reads settings directly from `~/.mailx/`.
80
+
81
+ **Step 2.** Run `mailx` and open `http://127.0.0.1:9333` in your browser.
80
82
 
81
83
  ### Option C: Multi-machine with shared settings (OneDrive/Dropbox)
82
84
 
83
- Put `settings.jsonc` on a cloud-synced folder, then create a local pointer:
85
+ To share settings across machines, put `settings.jsonc` in a cloud-synced folder and create a local pointer file.
86
+
87
+ **Step 1.** Move `settings.jsonc` to a shared location:
88
+
89
+ ```
90
+ # Example: OneDrive
91
+ mkdir "%OneDrive%\home\.mailx"
92
+ move "%USERPROFILE%\.mailx\settings.jsonc" "%OneDrive%\home\.mailx\settings.jsonc"
93
+ ```
94
+
95
+ **Step 2.** Create `~/.mailx/config.jsonc` pointing to the shared location:
84
96
 
85
- **`~/.mailx/config.jsonc`:**
86
97
  ```jsonc
87
98
  {
88
- // Point to shared settings on OneDrive (or Dropbox, network share, etc.)
99
+ // Where the shared settings live (OneDrive, Dropbox, network share, etc.)
89
100
  "sharedDir": "C:/Users/You/OneDrive/home/.mailx",
90
- // Local-only: where cached messages are stored
91
- "storePath": "C:/Users/You/.mailx/mailxstore",
92
- // Per-machine override: days of history (0 = all)
93
- "historyDays": 90
101
+
102
+ // Local-only settings (not synced):
103
+ "storePath": "C:/Users/You/.mailx/mailxstore", // cached message bodies
104
+ "historyDays": 90 // override sync depth per machine
94
105
  }
95
106
  ```
96
107
 
97
- The shared directory contains: `settings.jsonc`, `allowlist.jsonc`, `preferences.jsonc`. Each machine caches them locally for offline use.
108
+ **How it works:**
109
+ - `config.jsonc` is local to each machine -- never synced
110
+ - `sharedDir` points to the cloud folder containing `settings.jsonc`, `allowlist.jsonc`, `preferences.jsonc`
111
+ - mailx caches shared files locally for offline use
112
+ - `historyDays` in `config.jsonc` overrides the shared default per machine
113
+ - On new machines, if `config.jsonc` doesn't exist, mailx auto-detects OneDrive at `%OneDrive%/home/.mailx/`
98
114
 
99
115
  ### Adding a Gmail account
100
116
 
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 (selectedAccountId && selectedFolderId !== null && selectedFolderId >= 0) {
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 in oklch */
12
- --color-brand: oklch(0.88 0.04 240); /* mailx light blue — toolbar, highlights */
13
- --color-brand-dark: oklch(0.45 0.10 240); /* darker brand for text on light bg */
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.90 0.01 270);
20
- --color-text-muted: oklch(0.55 0.02 270);
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.35 0.02 270);
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 theme -- follows system preference */
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.36",
3
+ "version": "1.0.37",
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.7",
23
+ "@bobfrankston/iflow": "^1.0.8",
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;
@@ -109,7 +109,7 @@ function saveFile(filename, data) {
109
109
  const DEFAULT_ACCOUNTS = [];
110
110
  const DEFAULT_PREFERENCES = {
111
111
  ui: {
112
- theme: "dark",
112
+ theme: "system",
113
113
  folderWidth: 220,
114
114
  listViewerSplit: 40,
115
115
  fontSize: 15,
@@ -155,9 +155,15 @@ export function loadAccounts() {
155
155
  export function saveAccounts(accounts) {
156
156
  saveFile("accounts.jsonc", { accounts });
157
157
  }
158
- /** Load preferences (shared + local overrides) */
158
+ /** Load preferences (shared + local overrides, with legacy fallback) */
159
159
  export function loadPreferences() {
160
- const shared = loadFile("preferences.jsonc", DEFAULT_PREFERENCES);
160
+ let shared = loadFile("preferences.jsonc", DEFAULT_PREFERENCES);
161
+ // Legacy fallback: read ui/sync from settings.jsonc if preferences.jsonc had only defaults
162
+ const legacy = loadLegacySettings();
163
+ if (legacy?.ui)
164
+ shared = { ...shared, ui: { ...shared.ui, ...legacy.ui } };
165
+ if (legacy?.sync)
166
+ shared = { ...shared, sync: { ...shared.sync, ...legacy.sync } };
161
167
  const localConfig = readLocalConfig();
162
168
  // Local overrides
163
169
  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;