@bobfrankston/mailx 1.0.37 → 1.0.39

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
@@ -35,47 +35,76 @@ Once OneDrive syncs, mailx picks up your accounts automatically.
35
35
 
36
36
  (On Windows `~` means `%USERPROFILE%`, e.g., `C:\Users\You\.mailx\settings.jsonc`)
37
37
 
38
+ **Gmail -- minimal config (just your email):**
39
+ ```jsonc
40
+ {
41
+ "accounts": [
42
+ { "email": "you@gmail.com" }
43
+ ]
44
+ }
45
+ ```
46
+
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:**
38
50
  ```jsonc
39
51
  {
40
52
  "accounts": [
41
53
  {
42
- "id": "mymail", // Internal ID (no spaces, lowercase)
43
- "name": "Your Name", // Sender name in From: header
44
- "label": "Work", // Display label in UI (optional, defaults to name)
45
- "email": "you@example.com", // Your email address
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
+ ```
62
+
63
+ Defaults: port 993/587, TLS on, auth password, username = email address. Only provide fields that differ from defaults.
64
+
65
+ **Full config with all optional fields:**
66
+ ```jsonc
67
+ {
68
+ "accounts": [
69
+ {
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",
46
74
  "imap": {
47
- "host": "imap.example.com", // IMAP server
48
- "port": 993, // Usually 993 for SSL/TLS
49
- "tls": true,
50
- "auth": "password", // "password" or "oauth2" (Gmail)
51
- "user": "you@example.com", // IMAP username
52
- "password": "your-password" // IMAP password (not needed for oauth2)
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"
53
81
  },
54
82
  "smtp": {
55
- "host": "smtp.example.com", // SMTP server
56
- "port": 587, // Usually 587 for STARTTLS
57
- "tls": true,
83
+ "host": "smtp.example.com", // Default: smtp.{domain}
84
+ "port": 587, // Default: 587
85
+ "tls": true, // Default: true
58
86
  "auth": "password",
59
87
  "user": "you@example.com",
60
88
  "password": "your-password"
61
89
  },
62
- "enabled": true,
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)
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
66
94
  }
67
95
  ],
68
96
  "sync": {
69
- "intervalMinutes": 5, // Full sync interval (minutes)
70
- "historyDays": 0 // Days of history to sync (0 = all)
97
+ "intervalMinutes": 5, // Default: 5
98
+ "historyDays": 0 // Default: 30 (0 = all)
71
99
  },
72
100
  "ui": {
73
- "theme": "system", // "system", "dark", or "light"
74
- "fontSize": 15
101
+ "theme": "system" // "system" (default), "dark", or "light"
75
102
  }
76
103
  }
77
104
  ```
78
105
 
106
+ **Known providers with automatic defaults:** Gmail, Google, Outlook, Hotmail, Yahoo, iCloud. For these, only `email` is required.
107
+
79
108
  No `config.jsonc` needed -- when it doesn't exist, mailx reads settings directly from `~/.mailx/`.
80
109
 
81
110
  **Step 2.** Run `mailx` and open `http://127.0.0.1:9333` in your browser.
@@ -112,28 +141,15 @@ move "%USERPROFILE%\.mailx\settings.jsonc" "%OneDrive%\home\.mailx\settings.json
112
141
  - `historyDays` in `config.jsonc` overrides the shared default per machine
113
142
  - On new machines, if `config.jsonc` doesn't exist, mailx auto-detects OneDrive at `%OneDrive%/home/.mailx/`
114
143
 
115
- ### Adding a Gmail account
144
+ ### Gmail OAuth setup
116
145
 
117
- Gmail requires OAuth2 instead of a password:
146
+ Gmail accounts auto-configure -- just add `{ "email": "you@gmail.com" }` to accounts. But OAuth requires a one-time Google Cloud setup:
118
147
 
119
148
  1. Go to [Google Cloud Console](https://console.cloud.google.com/) and create a project
120
149
  2. Enable the **Gmail API** (and **People API** for contacts)
121
150
  3. Create **OAuth 2.0 credentials** (Desktop app type)
122
151
  4. Download `credentials.json` to the iflow package directory
123
- 5. Add the Gmail account to `settings.jsonc` with `"auth": "oauth2"`
124
- 6. First connection opens a browser for OAuth consent. Tokens are cached and refresh automatically.
125
-
126
- ```jsonc
127
- {
128
- "id": "gmail",
129
- "name": "Your Name",
130
- "label": "Gmail",
131
- "email": "you@gmail.com",
132
- "imap": { "host": "imap.gmail.com", "port": 993, "tls": true, "auth": "oauth2", "user": "you@gmail.com" },
133
- "smtp": { "host": "smtp.gmail.com", "port": 587, "tls": true, "auth": "oauth2", "user": "you@gmail.com" },
134
- "enabled": true
135
- }
136
- ```
152
+ 5. First connection opens a browser for OAuth consent. Tokens are cached and refresh automatically.
137
153
 
138
154
  ## Usage
139
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
- const child = spawn(launcherPath, args, { detached: true, stdio: "ignore" });
180
- child.unref();
181
- console.log("mailx launched");
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("Native launcher not found, starting in browser mode...");
184
- log("Falling back to --server mode");
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
  }
@@ -317,7 +317,33 @@ async function loadFolderTree(container) {
317
317
  try {
318
318
  const accounts = await getAccounts();
319
319
  if (accounts.length === 0) {
320
- container.innerHTML = `<div class="folder-loading">No accounts configured.<br>Edit ~/.mailx/settings.jsonc</div>`;
320
+ container.innerHTML = `<div class="folder-loading" style="padding:1rem;line-height:1.8">
321
+ <strong>No accounts configured</strong><br>
322
+ Create <code>~/.mailx/settings.jsonc</code> with your email accounts.<br><br>
323
+ Minimal Gmail example:<br>
324
+ <code style="display:block;padding:0.5rem;background:var(--color-bg);border-radius:4px;margin:0.5rem 0;white-space:pre">{ "name": "Your Name",
325
+ "accounts": [
326
+ { "email": "you@gmail.com" }
327
+ ]
328
+ }</code>
329
+ Standard IMAP:<br>
330
+ <code style="display:block;padding:0.5rem;background:var(--color-bg);border-radius:4px;margin:0.5rem 0;white-space:pre">{ "name": "Your Name",
331
+ "accounts": [
332
+ { "email": "you@example.com",
333
+ "password": "secret",
334
+ "imap": { "host": "imap.example.com" },
335
+ "smtp": { "host": "smtp.example.com" }
336
+ }
337
+ ]
338
+ }</code>
339
+ <a href="https://github.com/BobFrankston/mailx#first-time-setup" target="_blank" style="color:var(--color-accent)">Full setup guide</a>
340
+ </div>`;
341
+ // Dismiss startup overlay
342
+ const overlay = document.getElementById("startup-overlay");
343
+ if (overlay) {
344
+ overlay.classList.add("hidden");
345
+ setTimeout(() => overlay.remove(), 400);
346
+ }
321
347
  return;
322
348
  }
323
349
  // Clear loading state now that we have data
@@ -30,7 +30,7 @@ async function api(path, options) {
30
30
  // Network error — server is down
31
31
  if (e.name === "AbortError")
32
32
  throw e;
33
- throw new Error("Server offline — restart with launch.ps1");
33
+ throw new Error("Server offline — run: mailx -server");
34
34
  }
35
35
  if (!res.ok) {
36
36
  const err = await res.json().catch(() => ({ error: res.statusText }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.37",
3
+ "version": "1.0.39",
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.8",
23
+ "@bobfrankston/iflow": "^1.0.10",
24
24
  "@bobfrankston/miscinfo": "^1.0.6",
25
25
  "@bobfrankston/oauthsupport": "^1.0.11",
26
26
  "@bobfrankston/rust-builder": "^0.1.2",
@@ -41,10 +41,10 @@ console.error = (...args) => {
41
41
  origErr(msg);
42
42
  logStream.write(msg + "\n");
43
43
  };
44
- const serverPkg = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, "package.json"), "utf-8"));
45
- const clientPkg = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, "..", "..", "client", "package.json"), "utf-8"));
46
- const SERVER_VERSION = serverPkg.version;
47
- const CLIENT_VERSION = clientPkg.version;
44
+ // Read version from root package.json (the published version)
45
+ const rootPkg = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, "..", "..", "package.json"), "utf-8"));
46
+ const SERVER_VERSION = rootPkg.version;
47
+ const CLIENT_VERSION = rootPkg.version;
48
48
  // ── Initialize ──
49
49
  initLocalConfig();
50
50
  const settings = loadSettings();
@@ -23,6 +23,22 @@ const LOCAL_DIR = path.join(process.env.USERPROFILE || process.env.HOME || ".",
23
23
  const LOCAL_CONFIG_PATH = path.join(LOCAL_DIR, "config.jsonc");
24
24
  const LEGACY_CONFIG_PATH = path.join(LOCAL_DIR, "config.json");
25
25
  const DEFAULT_STORE_PATH = path.join(LOCAL_DIR, "mailxstore");
26
+ /** Resolve a path from config — relative to ~/.mailx/, ~ expands to home */
27
+ function resolvePath(p) {
28
+ if (!p)
29
+ return p;
30
+ const home = process.env.USERPROFILE || process.env.HOME || ".";
31
+ // Expand ~ to home directory
32
+ if (p.startsWith("~/") || p.startsWith("~\\"))
33
+ return path.join(home, p.slice(2));
34
+ if (p === "~")
35
+ return home;
36
+ // Absolute path — use as-is
37
+ if (path.isAbsolute(p))
38
+ return p;
39
+ // Relative — resolve from config directory (~/.mailx/)
40
+ return path.resolve(LOCAL_DIR, p);
41
+ }
26
42
  function readLocalConfig() {
27
43
  // Migrate config.json → config.jsonc
28
44
  if (!fs.existsSync(LOCAL_CONFIG_PATH) && fs.existsSync(LEGACY_CONFIG_PATH)) {
@@ -35,10 +51,10 @@ function readLocalConfig() {
35
51
  function getSharedDir() {
36
52
  const config = readLocalConfig();
37
53
  if (config.sharedDir)
38
- return config.sharedDir;
54
+ return resolvePath(config.sharedDir);
39
55
  // Legacy: derive from settingsPath
40
56
  if (config.settingsPath)
41
- return path.dirname(config.settingsPath);
57
+ return path.dirname(resolvePath(config.settingsPath));
42
58
  return LOCAL_DIR;
43
59
  }
44
60
  // ── File helpers ──
@@ -105,6 +121,65 @@ function saveFile(filename, data) {
105
121
  catch { /* ignore */ }
106
122
  }
107
123
  }
124
+ const PROVIDERS = {
125
+ "gmail.com": {
126
+ imap: { host: "imap.gmail.com", port: 993, tls: true, auth: "oauth2" },
127
+ smtp: { host: "smtp.gmail.com", port: 587, tls: true, auth: "oauth2" },
128
+ },
129
+ "googlemail.com": {
130
+ imap: { host: "imap.gmail.com", port: 993, tls: true, auth: "oauth2" },
131
+ smtp: { host: "smtp.gmail.com", port: 587, tls: true, auth: "oauth2" },
132
+ },
133
+ "outlook.com": {
134
+ imap: { host: "outlook.office365.com", port: 993, tls: true, auth: "oauth2" },
135
+ smtp: { host: "smtp.office365.com", port: 587, tls: true, auth: "oauth2" },
136
+ },
137
+ "hotmail.com": {
138
+ imap: { host: "outlook.office365.com", port: 993, tls: true, auth: "oauth2" },
139
+ smtp: { host: "smtp.office365.com", port: 587, tls: true, auth: "oauth2" },
140
+ },
141
+ "yahoo.com": {
142
+ imap: { host: "imap.mail.yahoo.com", port: 993, tls: true, auth: "password" },
143
+ smtp: { host: "smtp.mail.yahoo.com", port: 587, tls: true, auth: "password" },
144
+ },
145
+ "icloud.com": {
146
+ imap: { host: "imap.mail.me.com", port: 993, tls: true, auth: "password" },
147
+ smtp: { host: "smtp.mail.me.com", port: 587, tls: true, auth: "password" },
148
+ },
149
+ };
150
+ /** Fill in provider defaults for an account based on email domain */
151
+ function normalizeAccount(acct, globalName) {
152
+ const email = acct.email || "";
153
+ const domain = email.split("@")[1]?.toLowerCase() || "";
154
+ const provider = PROVIDERS[domain];
155
+ const user = acct.imap?.user || acct.user || email;
156
+ return {
157
+ id: acct.id || domain.split(".")[0] || "account",
158
+ name: acct.name || globalName || email.split("@")[0],
159
+ label: acct.label,
160
+ email,
161
+ imap: {
162
+ host: acct.imap?.host || provider?.imap.host || `imap.${domain}`,
163
+ port: acct.imap?.port || provider?.imap.port || 993,
164
+ tls: acct.imap?.tls ?? provider?.imap.tls ?? true,
165
+ auth: acct.imap?.auth || provider?.imap.auth || "password",
166
+ user: acct.imap?.user || user,
167
+ password: acct.imap?.password || acct.password,
168
+ },
169
+ smtp: {
170
+ host: acct.smtp?.host || provider?.smtp.host || `smtp.${domain}`,
171
+ port: acct.smtp?.port || provider?.smtp.port || 587,
172
+ tls: acct.smtp?.tls ?? provider?.smtp.tls ?? true,
173
+ auth: acct.smtp?.auth || provider?.smtp.auth || "password",
174
+ user: acct.smtp?.user || user,
175
+ password: acct.smtp?.password || acct.password,
176
+ },
177
+ enabled: acct.enabled ?? true,
178
+ defaultSend: acct.defaultSend,
179
+ relayDomains: acct.relayDomains,
180
+ deliveredToPrefix: acct.deliveredToPrefix,
181
+ };
182
+ }
108
183
  // ── Defaults ──
109
184
  const DEFAULT_ACCOUNTS = [];
110
185
  const DEFAULT_PREFERENCES = {
@@ -143,12 +218,14 @@ export function loadAccounts() {
143
218
  }
144
219
  catch { /* ignore */ }
145
220
  }
146
- return accounts.accounts || accounts;
221
+ const raw = accounts.accounts || accounts;
222
+ const globalName = accounts.name || "";
223
+ return raw.map((a) => normalizeAccount(a, globalName));
147
224
  }
148
225
  // Legacy: read from settings.jsonc
149
226
  const legacy = loadLegacySettings();
150
227
  if (legacy?.accounts)
151
- return legacy.accounts;
228
+ return legacy.accounts.map((a) => normalizeAccount(a, legacy.name));
152
229
  return DEFAULT_ACCOUNTS;
153
230
  }
154
231
  /** Save account configs */
@@ -215,7 +292,7 @@ export function saveSettings(settings) {
215
292
  /** Get the local store base path */
216
293
  export function getStorePath() {
217
294
  const config = readLocalConfig();
218
- return config.storePath || DEFAULT_STORE_PATH;
295
+ return config.storePath ? resolvePath(config.storePath) : DEFAULT_STORE_PATH;
219
296
  }
220
297
  /** Get the local data directory (DB, store, etc.) */
221
298
  export function getConfigDir() {