@bobfrankston/mailx 1.0.43 → 1.0.45
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/bin/mailx.js +142 -5
- package/client/app.js +2 -1
- package/client/compose/compose.js +2 -0
- package/package.json +2 -2
- package/packages/mailx-server/index.js +7 -2
package/bin/mailx.js
CHANGED
|
@@ -27,9 +27,10 @@ const verbose = hasFlag("verbose");
|
|
|
27
27
|
|
|
28
28
|
const setupMode = hasFlag("setup");
|
|
29
29
|
const addMode = hasFlag("add");
|
|
30
|
+
const testMode = hasFlag("test");
|
|
30
31
|
|
|
31
32
|
// Validate arguments
|
|
32
|
-
const knownFlags = ["server", "no-browser", "verbose", "external", "kill", "v", "version", "setup", "add"];
|
|
33
|
+
const knownFlags = ["server", "no-browser", "verbose", "external", "kill", "v", "version", "setup", "add", "test"];
|
|
33
34
|
for (const arg of args) {
|
|
34
35
|
const flag = arg.replace(/^--?/, "");
|
|
35
36
|
if (arg.startsWith("-") && !knownFlags.includes(flag)) {
|
|
@@ -291,21 +292,60 @@ async function runSetup() {
|
|
|
291
292
|
sync: { intervalMinutes: 5, historyDays: 0 },
|
|
292
293
|
};
|
|
293
294
|
|
|
294
|
-
|
|
295
|
+
const domain = account.email.split("@")[1]?.toLowerCase() || "";
|
|
296
|
+
const isGmail = domain === "gmail.com" || domain === "googlemail.com";
|
|
297
|
+
|
|
298
|
+
// Ask where to store settings
|
|
295
299
|
const mountedDrive = findMountedDrive();
|
|
300
|
+
let storageChoice = "local";
|
|
301
|
+
|
|
296
302
|
if (mountedDrive) {
|
|
303
|
+
const useCloud = await prompt(`Store settings on ${mountedDrive.provider} (syncs across machines)? [Y/n]: `);
|
|
304
|
+
if (!useCloud || useCloud.toLowerCase() !== "n") storageChoice = mountedDrive.provider;
|
|
305
|
+
} else if (isGmail) {
|
|
306
|
+
const useGDrive = await prompt("Store settings on Google Drive (syncs across machines, editable via drive.google.com)? [Y/n]: ");
|
|
307
|
+
if (!useGDrive || useGDrive.toLowerCase() !== "n") storageChoice = "gdrive-api";
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
fs.mkdirSync(mailxDir, { recursive: true });
|
|
311
|
+
|
|
312
|
+
if (storageChoice === "gdrive-api") {
|
|
313
|
+
// Save to Google Drive via API (not mounted)
|
|
314
|
+
console.log("\nSaving settings to Google Drive via API...");
|
|
315
|
+
try {
|
|
316
|
+
const { getCloudProvider } = await import("../packages/mailx-settings/cloud.js");
|
|
317
|
+
const gdrive = getCloudProvider("gdrive");
|
|
318
|
+
if (gdrive) {
|
|
319
|
+
const content = JSON.stringify(settings, null, 2);
|
|
320
|
+
const ok = await gdrive.write("home/.mailx/settings.jsonc", content);
|
|
321
|
+
if (ok) {
|
|
322
|
+
console.log("Settings saved to Google Drive: home/.mailx/settings.jsonc");
|
|
323
|
+
const config = { sharedDir: { provider: "gdrive", path: "home/.mailx" } };
|
|
324
|
+
fs.writeFileSync(path.join(mailxDir, "config.jsonc"), JSON.stringify(config, null, 2));
|
|
325
|
+
console.log("Local config created pointing to Google Drive.");
|
|
326
|
+
} else {
|
|
327
|
+
console.log("Google Drive write failed — saving locally instead.");
|
|
328
|
+
fs.writeFileSync(path.join(mailxDir, "settings.jsonc"), JSON.stringify(settings, null, 2));
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
console.log("Google Drive API not available — saving locally.");
|
|
332
|
+
fs.writeFileSync(path.join(mailxDir, "settings.jsonc"), JSON.stringify(settings, null, 2));
|
|
333
|
+
}
|
|
334
|
+
} catch (e) {
|
|
335
|
+
console.log(`Google Drive error: ${e.message} — saving locally.`);
|
|
336
|
+
fs.writeFileSync(path.join(mailxDir, "settings.jsonc"), JSON.stringify(settings, null, 2));
|
|
337
|
+
}
|
|
338
|
+
} else if (storageChoice !== "local" && mountedDrive) {
|
|
339
|
+
// Save to mounted cloud drive
|
|
297
340
|
console.log(`\nSaving settings to ${mountedDrive.provider} at ${mountedDrive.dir}...`);
|
|
298
341
|
fs.mkdirSync(mountedDrive.dir, { recursive: true });
|
|
299
342
|
fs.writeFileSync(path.join(mountedDrive.dir, "settings.jsonc"), JSON.stringify(settings, null, 2));
|
|
300
|
-
// Create local config pointing to cloud
|
|
301
|
-
fs.mkdirSync(mailxDir, { recursive: true });
|
|
302
343
|
const config = { sharedDir: { provider: mountedDrive.provider, path: "home/.mailx" } };
|
|
303
344
|
fs.writeFileSync(path.join(mailxDir, "config.jsonc"), JSON.stringify(config, null, 2));
|
|
304
345
|
console.log("Settings saved to cloud drive + local config created.");
|
|
305
346
|
} else {
|
|
306
347
|
// Save locally
|
|
307
348
|
console.log(`\nSaving settings to ${mailxDir}...`);
|
|
308
|
-
fs.mkdirSync(mailxDir, { recursive: true });
|
|
309
349
|
fs.writeFileSync(path.join(mailxDir, "settings.jsonc"), JSON.stringify(settings, null, 2));
|
|
310
350
|
}
|
|
311
351
|
|
|
@@ -330,11 +370,108 @@ function findMountedDrive() {
|
|
|
330
370
|
return null;
|
|
331
371
|
}
|
|
332
372
|
|
|
373
|
+
/** Test account connectivity — IMAP connect, SMTP send, sync round-trip */
|
|
374
|
+
async function runTest() {
|
|
375
|
+
console.log("\nmailx — connection test\n");
|
|
376
|
+
|
|
377
|
+
// Start server in-process to access settings
|
|
378
|
+
console.log("Loading settings...");
|
|
379
|
+
const { loadSettings, getSharedDir } = await import("../packages/mailx-settings/index.js");
|
|
380
|
+
const { initLocalConfig } = await import("../packages/mailx-settings/index.js");
|
|
381
|
+
initLocalConfig();
|
|
382
|
+
const settings = loadSettings();
|
|
383
|
+
|
|
384
|
+
if (settings.accounts.length === 0) {
|
|
385
|
+
console.log("No accounts configured. Run: mailx -setup");
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
console.log(`Shared dir: ${getSharedDir()}`);
|
|
390
|
+
console.log(`Accounts: ${settings.accounts.map(a => `${a.label || a.name} <${a.email}>`).join(", ")}\n`);
|
|
391
|
+
|
|
392
|
+
for (const account of settings.accounts) {
|
|
393
|
+
if (!account.enabled) { console.log(` ${account.label || account.id}: SKIPPED (disabled)\n`); continue; }
|
|
394
|
+
|
|
395
|
+
console.log(`Testing ${account.label || account.id} (${account.email}):`);
|
|
396
|
+
|
|
397
|
+
// Test IMAP
|
|
398
|
+
try {
|
|
399
|
+
const { ImapClient, createAutoImapConfig } = await import("@bobfrankston/iflow");
|
|
400
|
+
const config = createAutoImapConfig({
|
|
401
|
+
server: account.imap.host,
|
|
402
|
+
port: account.imap.port,
|
|
403
|
+
username: account.imap.user,
|
|
404
|
+
password: account.imap.password
|
|
405
|
+
});
|
|
406
|
+
const client = new ImapClient(config);
|
|
407
|
+
const folders = await client.getFolderList();
|
|
408
|
+
await client.logout();
|
|
409
|
+
console.log(` IMAP: OK (${folders.length} folders)`);
|
|
410
|
+
} catch (e) {
|
|
411
|
+
console.log(` IMAP: FAILED — ${e.message}`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Test SMTP
|
|
415
|
+
try {
|
|
416
|
+
const { createTransport } = await import("nodemailer");
|
|
417
|
+
let smtpAuth;
|
|
418
|
+
if (account.smtp.auth === "password") {
|
|
419
|
+
smtpAuth = { user: account.smtp.user, pass: account.smtp.password };
|
|
420
|
+
} else if (account.smtp.auth === "oauth2") {
|
|
421
|
+
// Try to get OAuth token
|
|
422
|
+
const { ImapClient, createAutoImapConfig } = await import("@bobfrankston/iflow");
|
|
423
|
+
const config = createAutoImapConfig({
|
|
424
|
+
server: account.imap.host,
|
|
425
|
+
port: account.imap.port,
|
|
426
|
+
username: account.imap.user,
|
|
427
|
+
});
|
|
428
|
+
if (config.tokenProvider) {
|
|
429
|
+
const accessToken = await config.tokenProvider();
|
|
430
|
+
smtpAuth = { type: "OAuth2", user: account.smtp.user, accessToken };
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const transport = createTransport({
|
|
434
|
+
host: account.smtp.host,
|
|
435
|
+
port: account.smtp.port,
|
|
436
|
+
secure: account.smtp.port === 465,
|
|
437
|
+
auth: smtpAuth,
|
|
438
|
+
tls: { rejectUnauthorized: false },
|
|
439
|
+
});
|
|
440
|
+
await transport.verify();
|
|
441
|
+
console.log(` SMTP: OK`);
|
|
442
|
+
|
|
443
|
+
// Send test message to self
|
|
444
|
+
const testSubject = `mailx test — ${new Date().toLocaleString()}`;
|
|
445
|
+
await transport.sendMail({
|
|
446
|
+
from: `${account.name} <${account.email}>`,
|
|
447
|
+
to: account.email,
|
|
448
|
+
subject: testSubject,
|
|
449
|
+
text: `This is a test message from mailx -test.\nSent: ${new Date().toISOString()}\nAccount: ${account.id}`,
|
|
450
|
+
});
|
|
451
|
+
console.log(` SEND: OK — test message sent to ${account.email}`);
|
|
452
|
+
console.log(` Subject: "${testSubject}"`);
|
|
453
|
+
} catch (e) {
|
|
454
|
+
console.log(` SMTP: FAILED — ${e.message}`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
console.log();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
console.log("Test complete. Check your inbox for the test message(s).");
|
|
461
|
+
process.exit(0);
|
|
462
|
+
}
|
|
463
|
+
|
|
333
464
|
async function main() {
|
|
334
465
|
log(`Platform: ${process.platform} ${process.arch}`);
|
|
335
466
|
log(`Node: ${process.version}`);
|
|
336
467
|
log(`Mode: ${serverMode ? "server" : setupMode ? "setup" : "auto-detect"}`);
|
|
337
468
|
|
|
469
|
+
// Test connectivity
|
|
470
|
+
if (testMode) {
|
|
471
|
+
await runTest();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
338
475
|
// Add account to existing config
|
|
339
476
|
if (addMode) {
|
|
340
477
|
const account = await promptForAccount();
|
package/client/app.js
CHANGED
|
@@ -614,8 +614,9 @@ optFlagged?.addEventListener("change", () => {
|
|
|
614
614
|
const isApp = typeof mailxapi !== "undefined" && mailxapi?.isApp;
|
|
615
615
|
fetch("/api/version").then(r => r.json()).then(d => {
|
|
616
616
|
const el = document.getElementById("app-version");
|
|
617
|
+
const driveName = d.drive === "local" ? "" : ` [${d.drive?.split(/[/\\]/).pop() || d.drive}]`;
|
|
617
618
|
if (el)
|
|
618
|
-
el.textContent = `mailx s${d.server}/c${d.client}${isApp ? "" : " [browser]"}`;
|
|
619
|
+
el.textContent = `mailx s${d.server}/c${d.client}${driveName}${isApp ? "" : " [browser]"}`;
|
|
619
620
|
}).catch(async () => {
|
|
620
621
|
// Server not running — try to start it if we're in the app
|
|
621
622
|
const startupStatus = document.getElementById("startup-status");
|
|
@@ -17,6 +17,8 @@ const editor = new Quill("#compose-editor", {
|
|
|
17
17
|
]
|
|
18
18
|
}
|
|
19
19
|
});
|
|
20
|
+
// Make toolbar buttons non-tabbable so Tab goes straight to editor body
|
|
21
|
+
document.querySelectorAll(".ql-toolbar button, .ql-toolbar select, .ql-toolbar .ql-picker-label").forEach(el => el.setAttribute("tabindex", "-1"));
|
|
20
22
|
// ── Populate from init data ──
|
|
21
23
|
const fromSelect = document.getElementById("compose-from-select");
|
|
22
24
|
const fromCustom = document.getElementById("compose-from-custom");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.45",
|
|
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.25",
|
|
24
24
|
"@bobfrankston/miscinfo": "^1.0.6",
|
|
25
25
|
"@bobfrankston/oauthsupport": "^1.0.11",
|
|
26
26
|
"@bobfrankston/rust-builder": "^0.1.2",
|
|
@@ -9,7 +9,7 @@ import * as fs from "node:fs";
|
|
|
9
9
|
import { MailxDB } from "@bobfrankston/mailx-store";
|
|
10
10
|
import { ImapManager } from "@bobfrankston/mailx-imap";
|
|
11
11
|
import { createApiRouter } from "@bobfrankston/mailx-api";
|
|
12
|
-
import { loadSettings, getConfigDir, initLocalConfig } from "@bobfrankston/mailx-settings";
|
|
12
|
+
import { loadSettings, getConfigDir, getSharedDir, initLocalConfig } from "@bobfrankston/mailx-settings";
|
|
13
13
|
import { ports } from "@bobfrankston/miscinfo";
|
|
14
14
|
import { createServer } from "node:http";
|
|
15
15
|
const PORT = ports.mailx;
|
|
@@ -81,7 +81,12 @@ 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) =>
|
|
84
|
+
app.get("/api/version", (req, res) => {
|
|
85
|
+
const sharedDir = getSharedDir();
|
|
86
|
+
const localDir = getConfigDir();
|
|
87
|
+
const drive = sharedDir === localDir ? "local" : sharedDir;
|
|
88
|
+
res.json({ server: SERVER_VERSION, client: CLIENT_VERSION, theme: settings.ui?.theme || "system", drive });
|
|
89
|
+
});
|
|
85
90
|
app.get("/status", (req, res) => {
|
|
86
91
|
const accounts = db.getAccounts();
|
|
87
92
|
const pendingSync = db.getTotalPendingSyncCount();
|