@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 +53 -37
- package/bin/mailx.js +23 -5
- package/client/components/folder-tree.js +27 -1
- package/client/lib/api-client.js +1 -1
- package/package.json +2 -2
- package/packages/mailx-server/index.js +4 -4
- package/packages/mailx-settings/index.js +82 -5
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
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
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", //
|
|
48
|
-
"port": 993, //
|
|
49
|
-
"tls": true,
|
|
50
|
-
"auth": "password", // "password" or "oauth2"
|
|
51
|
-
"user": "you@example.com", //
|
|
52
|
-
"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"
|
|
53
81
|
},
|
|
54
82
|
"smtp": {
|
|
55
|
-
"host": "smtp.example.com", //
|
|
56
|
-
"port": 587, //
|
|
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
|
|
64
|
-
"relayDomains": [], // Domains to skip in Delivered-To
|
|
65
|
-
"deliveredToPrefix": [] // Prefixes to strip from Delivered-To alias
|
|
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, //
|
|
70
|
-
"historyDays": 0 //
|
|
97
|
+
"intervalMinutes": 5, // Default: 5
|
|
98
|
+
"historyDays": 0 // Default: 30 (0 = all)
|
|
71
99
|
},
|
|
72
100
|
"ui": {
|
|
73
|
-
"theme": "system"
|
|
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
|
-
###
|
|
144
|
+
### Gmail OAuth setup
|
|
116
145
|
|
|
117
|
-
Gmail
|
|
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.
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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"
|
|
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
|
package/client/lib/api-client.js
CHANGED
|
@@ -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 —
|
|
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.
|
|
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.
|
|
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
|
-
|
|
45
|
-
const
|
|
46
|
-
const SERVER_VERSION =
|
|
47
|
-
const CLIENT_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
|
-
|
|
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
|
|
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() {
|