@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 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
- // Detect mounted cloud drive to save settings to
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.43",
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",
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) => res.json({ server: SERVER_VERSION, client: CLIENT_VERSION, theme: settings.ui?.theme || "system" }));
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();