@bobfrankston/mailx 1.0.38 → 1.0.40
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/components/folder-tree.js +34 -1
- package/client/components/message-list.js +13 -8
- package/client/lib/api-client.js +1 -1
- package/package.json +2 -2
- package/packages/mailx-imap/index.js +1 -1
- package/packages/mailx-server/index.js +6 -6
- package/packages/mailx-settings/index.js +33 -8
- package/packages/mailx-types/index.d.ts +1 -0
|
@@ -317,7 +317,40 @@ 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
|
|
320
|
+
container.innerHTML = `<div class="folder-loading">No accounts</div>`;
|
|
321
|
+
// Show setup instructions in the main content area (full width)
|
|
322
|
+
const mainBody = document.getElementById("ml-body");
|
|
323
|
+
if (mainBody) {
|
|
324
|
+
mainBody.innerHTML = `<div style="padding:2rem;line-height:1.8;max-width:600px">
|
|
325
|
+
<h2 style="margin-bottom:1rem">Welcome to mailx</h2>
|
|
326
|
+
<p>Create <code>~/.mailx/settings.jsonc</code> with your email accounts.</p>
|
|
327
|
+
<p>If you have settings on OneDrive, create <code>~/.mailx/config.jsonc</code>:</p>
|
|
328
|
+
<code style="display:block;padding:0.75rem;background:var(--color-bg-surface);border:1px solid var(--color-border);border-radius:4px;margin:0.5rem 0;white-space:pre;font-size:0.85rem">{ "sharedDir": "~/OneDrive/home/.mailx" }</code>
|
|
329
|
+
<h3 style="margin-top:1.5rem">Gmail (just your email):</h3>
|
|
330
|
+
<code style="display:block;padding:0.75rem;background:var(--color-bg-surface);border:1px solid var(--color-border);border-radius:4px;margin:0.5rem 0;white-space:pre;font-size:0.85rem">{ "name": "Your Name",
|
|
331
|
+
"accounts": [
|
|
332
|
+
{ "email": "you@gmail.com" }
|
|
333
|
+
]
|
|
334
|
+
}</code>
|
|
335
|
+
<h3 style="margin-top:1rem">Standard IMAP:</h3>
|
|
336
|
+
<code style="display:block;padding:0.75rem;background:var(--color-bg-surface);border:1px solid var(--color-border);border-radius:4px;margin:0.5rem 0;white-space:pre;font-size:0.85rem">{ "name": "Your Name",
|
|
337
|
+
"accounts": [
|
|
338
|
+
{ "email": "you@example.com",
|
|
339
|
+
"password": "secret",
|
|
340
|
+
"imap": { "host": "imap.example.com" },
|
|
341
|
+
"smtp": { "host": "smtp.example.com" }
|
|
342
|
+
}
|
|
343
|
+
]
|
|
344
|
+
}</code>
|
|
345
|
+
<p style="margin-top:1rem;color:var(--color-text-muted)">Known providers (Gmail, Outlook, Yahoo, iCloud) auto-configure from just the email address. For other providers, specify IMAP/SMTP hosts. Restart after editing settings.</p>
|
|
346
|
+
</div>`;
|
|
347
|
+
}
|
|
348
|
+
// Dismiss startup overlay
|
|
349
|
+
const overlay = document.getElementById("startup-overlay");
|
|
350
|
+
if (overlay) {
|
|
351
|
+
overlay.classList.add("hidden");
|
|
352
|
+
setTimeout(() => overlay.remove(), 400);
|
|
353
|
+
}
|
|
321
354
|
return;
|
|
322
355
|
}
|
|
323
356
|
// Clear loading state now that we have data
|
|
@@ -197,7 +197,10 @@ export async function loadMessages(accountId, folderId, page = 1, specialUse = "
|
|
|
197
197
|
// Save scroll position and selected UID for non-autoSelect reloads
|
|
198
198
|
const savedScroll = !autoSelect ? body.scrollTop : 0;
|
|
199
199
|
const savedUid = !autoSelect ? body.querySelector(".ml-row.selected")?.getAttribute("data-uid") : null;
|
|
200
|
-
|
|
200
|
+
// Only show loading indicator on fresh navigation, not reloads
|
|
201
|
+
if (autoSelect) {
|
|
202
|
+
body.innerHTML = `<div class="ml-empty">Loading...</div>`;
|
|
203
|
+
}
|
|
201
204
|
try {
|
|
202
205
|
const result = await getMessages(accountId, folderId, 1);
|
|
203
206
|
totalMessages = result.total;
|
|
@@ -215,13 +218,15 @@ export async function loadMessages(accountId, folderId, page = 1, specialUse = "
|
|
|
215
218
|
firstRow.click();
|
|
216
219
|
}
|
|
217
220
|
else {
|
|
218
|
-
// Sync reload — restore scroll position and selection
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
row
|
|
224
|
-
|
|
221
|
+
// Sync reload — restore scroll position and selection after layout
|
|
222
|
+
requestAnimationFrame(() => {
|
|
223
|
+
body.scrollTop = savedScroll;
|
|
224
|
+
if (savedUid) {
|
|
225
|
+
const row = body.querySelector(`.ml-row[data-uid="${savedUid}"]`);
|
|
226
|
+
if (row)
|
|
227
|
+
row.classList.add("selected");
|
|
228
|
+
}
|
|
229
|
+
});
|
|
225
230
|
}
|
|
226
231
|
}
|
|
227
232
|
catch (e) {
|
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.40",
|
|
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.20",
|
|
24
24
|
"@bobfrankston/miscinfo": "^1.0.6",
|
|
25
25
|
"@bobfrankston/oauthsupport": "^1.0.11",
|
|
26
26
|
"@bobfrankston/rust-builder": "^0.1.2",
|
|
@@ -1103,7 +1103,7 @@ export class ImapManager extends EventEmitter {
|
|
|
1103
1103
|
async syncAllContacts() {
|
|
1104
1104
|
const settings = loadSettings();
|
|
1105
1105
|
for (const account of settings.accounts) {
|
|
1106
|
-
if (account.imap.auth === "oauth2" && account.enabled) {
|
|
1106
|
+
if (account.imap.auth === "oauth2" && (account.enabled || account.syncContacts)) {
|
|
1107
1107
|
await this.syncGoogleContacts(account.id);
|
|
1108
1108
|
}
|
|
1109
1109
|
}
|
|
@@ -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();
|
|
@@ -185,11 +185,11 @@ async function start() {
|
|
|
185
185
|
}, 5000);
|
|
186
186
|
// Add configured accounts
|
|
187
187
|
for (const account of settings.accounts) {
|
|
188
|
-
if (!account.enabled)
|
|
188
|
+
if (!account.enabled && !account.syncContacts)
|
|
189
189
|
continue;
|
|
190
190
|
try {
|
|
191
191
|
await imapManager.addAccount(account);
|
|
192
|
-
console.log(` Account added: ${account.name} (${account.id})`);
|
|
192
|
+
console.log(` Account added: ${account.name} (${account.id})${!account.enabled ? " [contacts only]" : ""}`);
|
|
193
193
|
}
|
|
194
194
|
catch (e) {
|
|
195
195
|
console.error(` Failed to add account ${account.id}: ${e.message}`);
|
|
@@ -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 ──
|
|
@@ -55,7 +71,7 @@ function readJsonc(filePath) {
|
|
|
55
71
|
return null;
|
|
56
72
|
}
|
|
57
73
|
try {
|
|
58
|
-
return parseJsonc(fs.readFileSync(actual, "utf-8"));
|
|
74
|
+
return parseJsonc(fs.readFileSync(actual, "utf-8").replace(/\r/g, ""));
|
|
59
75
|
}
|
|
60
76
|
catch (e) {
|
|
61
77
|
console.error(`Failed to read ${actual}: ${e.message}`);
|
|
@@ -160,6 +176,7 @@ function normalizeAccount(acct, globalName) {
|
|
|
160
176
|
},
|
|
161
177
|
enabled: acct.enabled ?? true,
|
|
162
178
|
defaultSend: acct.defaultSend,
|
|
179
|
+
syncContacts: acct.syncContacts ?? (provider?.imap.auth === "oauth2"),
|
|
163
180
|
relayDomains: acct.relayDomains,
|
|
164
181
|
deliveredToPrefix: acct.deliveredToPrefix,
|
|
165
182
|
};
|
|
@@ -250,8 +267,14 @@ export function saveAllowlist(list) {
|
|
|
250
267
|
// ── Legacy compatibility ──
|
|
251
268
|
function loadLegacySettings() {
|
|
252
269
|
const config = readLocalConfig();
|
|
253
|
-
|
|
254
|
-
|
|
270
|
+
if (config.settingsPath)
|
|
271
|
+
return readJsonc(resolvePath(config.settingsPath));
|
|
272
|
+
// Try shared dir first, then local
|
|
273
|
+
const sharedDir = getSharedDir();
|
|
274
|
+
const shared = readJsonc(path.join(sharedDir, "settings.jsonc"));
|
|
275
|
+
if (shared)
|
|
276
|
+
return shared;
|
|
277
|
+
return readJsonc(path.join(LOCAL_DIR, "settings.jsonc"));
|
|
255
278
|
}
|
|
256
279
|
/** Load settings — unified view combining all files (backward compatible) */
|
|
257
280
|
export function loadSettings() {
|
|
@@ -276,7 +299,7 @@ export function saveSettings(settings) {
|
|
|
276
299
|
/** Get the local store base path */
|
|
277
300
|
export function getStorePath() {
|
|
278
301
|
const config = readLocalConfig();
|
|
279
|
-
return config.storePath
|
|
302
|
+
return config.storePath ? resolvePath(config.storePath) : DEFAULT_STORE_PATH;
|
|
280
303
|
}
|
|
281
304
|
/** Get the local data directory (DB, store, etc.) */
|
|
282
305
|
export function getConfigDir() {
|
|
@@ -292,10 +315,12 @@ function detectSharedDir() {
|
|
|
292
315
|
process.env.OneDrive && path.join(process.env.OneDrive, "home", ".mailx"),
|
|
293
316
|
process.env.OneDriveConsumer && path.join(process.env.OneDriveConsumer, "home", ".mailx"),
|
|
294
317
|
home && path.join(home, "OneDrive", "home", ".mailx"),
|
|
295
|
-
// Linux/Mac
|
|
296
|
-
home && path.join(home, "
|
|
318
|
+
// Linux/Mac — case variations
|
|
319
|
+
home && path.join(home, "onedrive", "home", ".mailx"),
|
|
320
|
+
home && path.join(home, "Onedrive", "home", ".mailx"),
|
|
297
321
|
// Dropbox
|
|
298
322
|
home && path.join(home, "Dropbox", ".mailx"),
|
|
323
|
+
home && path.join(home, "dropbox", ".mailx"),
|
|
299
324
|
// Local fallback — just use ~/.mailx itself
|
|
300
325
|
].filter(Boolean);
|
|
301
326
|
for (const dir of candidates) {
|
|
@@ -30,6 +30,7 @@ export interface AccountConfig {
|
|
|
30
30
|
};
|
|
31
31
|
enabled: boolean;
|
|
32
32
|
defaultSend?: boolean; /** Use this account's SMTP when From doesn't match any account */
|
|
33
|
+
syncContacts?: boolean; /** Sync contacts even when account is disabled (contacts-only Gmail) */
|
|
33
34
|
relayDomains?: string[]; /** Domains to skip in Delivered-To chain (e.g., ["m.connectivity.xyz"]) */
|
|
34
35
|
deliveredToPrefix?: string[]; /** Prefixes to strip from Delivered-To to get clean alias (e.g., ["bobf-ma-", "bobf-"]) — order matters, longest first */
|
|
35
36
|
}
|