@bobfrankston/mailx 1.0.142 → 1.0.144

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
@@ -18,7 +18,7 @@ import fs from "node:fs";
18
18
  import path from "node:path";
19
19
  import net from "node:net";
20
20
  import { ports } from "@bobfrankston/miscinfo";
21
- import { showMessageBox } from "@bobfrankston/msger";
21
+ import { showMessageBox, showService } from "@bobfrankston/msger";
22
22
  const PORT = ports.mailx;
23
23
  const args = process.argv.slice(2);
24
24
  // Normalize: accept both -flag and --flag
@@ -488,16 +488,7 @@ async function main() {
488
488
  console.log("No mailx configuration found.");
489
489
  await runSetup();
490
490
  }
491
- // Check if server is already running
492
- const inUse = await isPortInUse(PORT);
493
- if (inUse) {
494
- const url = `http://127.0.0.1:${PORT}`;
495
- if (!noBrowser)
496
- launchMsger(url);
497
- return;
498
- }
499
491
  // Redirect console to log file — keep terminal clean
500
- // Server also logs, but this catches CLI startup messages
501
492
  if (!verbose) {
502
493
  const home = process.env.USERPROFILE || process.env.HOME || ".";
503
494
  const logDir = path.join(home, ".mailx", "logs");
@@ -505,28 +496,95 @@ async function main() {
505
496
  const logDate = new Date().toISOString().slice(0, 10);
506
497
  const logPath = path.join(logDir, `mailx-${logDate}.log`);
507
498
  const logStream = fs.createWriteStream(logPath, { flags: "a" });
508
- const origLog = console.log;
509
- const origErr = console.error;
510
499
  console.log = (...a) => { logStream.write(a.join(" ") + "\n"); };
511
500
  console.error = (...a) => { logStream.write("ERROR " + a.join(" ") + "\n"); };
512
501
  }
513
- // Start Express server in-process
514
- console.log("Starting mailx server...");
515
- if (hasFlag("external"))
516
- process.argv.push("--external");
517
- await import("../packages/mailx-server/index.js");
518
- // Open UI once server is ready
519
- if (!noBrowser) {
520
- const url = `http://127.0.0.1:${PORT}`;
521
- for (let i = 0; i < 30; i++) {
522
- await new Promise(r => setTimeout(r, 200));
523
- if (await isPortInUse(PORT))
524
- break;
502
+ // --server mode: Express + HTTP (for dev/remote access)
503
+ if (serverMode) {
504
+ console.log("Starting mailx HTTP server...");
505
+ if (hasFlag("external"))
506
+ process.argv.push("--external");
507
+ await import("../packages/mailx-server/index.js");
508
+ if (!noBrowser)
509
+ launchMsger(`http://127.0.0.1:${PORT}`);
510
+ await new Promise(() => { }); // keep alive
511
+ return;
512
+ }
513
+ // Default: service mode — no TCP, IPC via msger
514
+ console.log("Starting mailx service...");
515
+ const { MailxDB } = await import("@bobfrankston/mailx-store");
516
+ const { ImapManager } = await import("@bobfrankston/mailx-imap");
517
+ const { MailxService } = await import("@bobfrankston/mailx-service");
518
+ const { dispatch } = await import("@bobfrankston/mailx-service/jsonrpc.js");
519
+ const { loadSettings, loadAccountsAsync, getConfigDir, getStorageInfo } = await import("@bobfrankston/mailx-settings");
520
+ let settings = loadSettings();
521
+ if (settings.accounts.length === 0) {
522
+ const cloudAccounts = await loadAccountsAsync();
523
+ if (cloudAccounts.length > 0) {
524
+ settings = { ...settings, accounts: cloudAccounts };
525
+ console.log(` Loaded ${cloudAccounts.length} account(s) from cloud`);
526
+ }
527
+ }
528
+ const db = new MailxDB(getConfigDir());
529
+ const imapManager = new ImapManager(db);
530
+ imapManager.useNativeClient = true;
531
+ const svc = new MailxService(db, imapManager);
532
+ // Read mailxapi.js for injection into WebView
533
+ const mailxapiPath = path.join(import.meta.dirname, "..", "client", "lib", "mailxapi.js");
534
+ const mailxapiScript = fs.readFileSync(mailxapiPath, "utf-8");
535
+ // Open msger in service mode — file:// URL, no HTTP
536
+ const clientDir = path.join(import.meta.dirname, "..", "client");
537
+ const indexPath = path.join(clientDir, "index.html");
538
+ const handle = showService({
539
+ url: indexPath,
540
+ initScript: mailxapiScript,
541
+ size: { width: 1400, height: 900 },
542
+ });
543
+ // Handle requests from WebView → dispatch to MailxService
544
+ handle.onRequest(async (req) => {
545
+ const response = await dispatch(svc, req);
546
+ handle.send(response);
547
+ });
548
+ // Wire IMAP events → push to WebView
549
+ imapManager.on("syncProgress", (accountId, phase, progress) => {
550
+ handle.send({ _event: "syncProgress", type: "syncProgress", accountId, phase, progress });
551
+ });
552
+ imapManager.on("folderCountsChanged", (accountId, counts) => {
553
+ handle.send({ _event: "folderCountsChanged", type: "folderCountsChanged", accountId, counts });
554
+ });
555
+ imapManager.on("syncError", (accountId, error) => {
556
+ handle.send({ _event: "error", type: "error", message: `${accountId}: ${error}` });
557
+ });
558
+ imapManager.on("accountError", (accountId, error, hint, isOAuth) => {
559
+ handle.send({ _event: "accountError", type: "accountError", accountId, error, hint, isOAuth });
560
+ });
561
+ // Add accounts and start sync
562
+ for (const account of settings.accounts) {
563
+ if (!account.enabled)
564
+ continue;
565
+ try {
566
+ await imapManager.addAccount(account);
567
+ console.log(` Account: ${account.label || account.name} (${account.id})`);
525
568
  }
526
- launchMsger(url);
569
+ catch (e) {
570
+ console.error(` Failed: ${account.id}: ${e.message}`);
571
+ }
572
+ }
573
+ if (settings.accounts.some(a => a.enabled)) {
574
+ imapManager.syncAll().catch(e => console.error(` Sync error: ${e.message}`));
527
575
  }
528
- // Keep process alive — server is running
529
- await new Promise(() => { });
576
+ imapManager.startPeriodicSync(settings.sync.intervalMinutes);
577
+ imapManager.startOutboxWorker();
578
+ // Wait for window close, then shut down
579
+ await handle.closed;
580
+ console.log("Window closed — shutting down");
581
+ imapManager.stopPeriodicSync();
582
+ try {
583
+ await imapManager.shutdown();
584
+ }
585
+ catch { /* proceed */ }
586
+ db.close();
587
+ process.exit(0);
530
588
  }
531
589
  main().catch(console.error);
532
590
  //# sourceMappingURL=mailx.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.142",
3
+ "version": "1.0.144",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -0,0 +1,25 @@
1
+ /**
2
+ * JSON-RPC dispatcher for MailxService.
3
+ * Maps mailxapi action names to MailxService method calls.
4
+ * Used by msger's bidirectional IPC (Phase 3) and can also
5
+ * serve as a stdio JSON-RPC interface for testing.
6
+ */
7
+ type ServiceLike = Record<string, (...args: any[]) => any>;
8
+ export interface RpcRequest {
9
+ _action: string;
10
+ _cbid: string;
11
+ [key: string]: any;
12
+ }
13
+ export interface RpcResponse {
14
+ _cbid: string;
15
+ result?: any;
16
+ error?: string;
17
+ }
18
+ export interface RpcEvent {
19
+ _event: string;
20
+ [key: string]: any;
21
+ }
22
+ /** Dispatch an incoming mailxapi call to the appropriate MailxService method */
23
+ export declare function dispatch(svc: ServiceLike, req: RpcRequest): Promise<RpcResponse>;
24
+ export {};
25
+ //# sourceMappingURL=jsonrpc.d.ts.map
@@ -0,0 +1,105 @@
1
+ /**
2
+ * JSON-RPC dispatcher for MailxService.
3
+ * Maps mailxapi action names to MailxService method calls.
4
+ * Used by msger's bidirectional IPC (Phase 3) and can also
5
+ * serve as a stdio JSON-RPC interface for testing.
6
+ */
7
+ /** Dispatch an incoming mailxapi call to the appropriate MailxService method */
8
+ export async function dispatch(svc, req) {
9
+ const { _action, _cbid, ...params } = req;
10
+ try {
11
+ const result = await dispatchAction(svc, _action, params);
12
+ return { _cbid, result };
13
+ }
14
+ catch (e) {
15
+ return { _cbid, error: e.message || String(e) };
16
+ }
17
+ }
18
+ async function dispatchAction(svc, action, p) {
19
+ switch (action) {
20
+ // Accounts
21
+ case "getAccounts":
22
+ return svc.getAccounts();
23
+ case "getFolders":
24
+ return svc.getFolders(p.accountId);
25
+ // Messages
26
+ case "getMessages":
27
+ return svc.getMessages(p.accountId, p.folderId, p.page, p.pageSize);
28
+ case "getUnifiedInbox":
29
+ return svc.getUnifiedInbox(p.page || 1, p.pageSize || 50);
30
+ case "getMessage":
31
+ return svc.getMessage(p.accountId, p.uid, p.allowRemote, p.folderId);
32
+ // Flags & actions
33
+ case "updateFlags":
34
+ await svc.updateFlags(p.accountId, p.uid, p.flags);
35
+ return { ok: true };
36
+ case "deleteMessage":
37
+ await svc.deleteMessage(p.accountId, p.uid);
38
+ return { ok: true };
39
+ case "deleteMessages":
40
+ await svc.deleteMessages(p.accountId, p.uids);
41
+ return { ok: true, count: p.uids.length };
42
+ case "undeleteMessage":
43
+ await svc.undeleteMessage(p.accountId, p.uid, p.folderId);
44
+ return { ok: true };
45
+ case "moveMessage":
46
+ await svc.moveMessage(p.accountId, p.uid, p.targetFolderId, p.targetAccountId);
47
+ return { ok: true };
48
+ case "moveMessages":
49
+ await svc.moveMessages(p.accountId, p.uids, p.targetFolderId);
50
+ return { ok: true, count: p.uids.length };
51
+ // Folders
52
+ case "markFolderRead":
53
+ svc.markFolderRead(p.folderId);
54
+ return { ok: true };
55
+ case "createFolder":
56
+ await svc.createFolder(p.accountId, p.parentPath, p.name);
57
+ return { ok: true };
58
+ case "renameFolder":
59
+ await svc.renameFolder(p.accountId, p.folderId, p.newName);
60
+ return { ok: true };
61
+ case "deleteFolder":
62
+ await svc.deleteFolder(p.accountId, p.folderId);
63
+ return { ok: true };
64
+ case "emptyFolder":
65
+ await svc.emptyFolder(p.accountId, p.folderId);
66
+ return { ok: true };
67
+ // Compose
68
+ case "sendMessage":
69
+ await svc.send(p);
70
+ return { ok: true };
71
+ case "saveDraft":
72
+ return svc.saveDraft(p.accountId, p.subject, p.bodyHtml, p.bodyText, p.to, p.cc, p.previousDraftUid, p.draftId);
73
+ case "deleteDraft":
74
+ await svc.deleteDraft(p.accountId, p.draftUid);
75
+ return { ok: true };
76
+ // Sync
77
+ case "syncAll":
78
+ await svc.syncAll();
79
+ return { ok: true };
80
+ case "getSyncPending":
81
+ return svc.getSyncPending();
82
+ // Search & contacts
83
+ case "searchMessages":
84
+ return svc.search(p.query, p.page, p.pageSize, p.scope, p.accountId, p.folderId);
85
+ case "searchContacts":
86
+ return svc.searchContacts(p.query);
87
+ // Settings
88
+ case "getSettings":
89
+ return svc.getSettings();
90
+ case "saveSettingsData":
91
+ svc.saveSettings(p);
92
+ return { ok: true };
93
+ case "allowRemoteContent":
94
+ svc.allowRemoteContent(p.type, p.value);
95
+ return { ok: true };
96
+ case "getVersion":
97
+ return svc.getStorageInfo();
98
+ // Autocomplete
99
+ case "autocomplete":
100
+ return svc.autocomplete(p);
101
+ default:
102
+ throw new Error(`Unknown action: ${action}`);
103
+ }
104
+ }
105
+ //# sourceMappingURL=jsonrpc.js.map
@@ -91,7 +91,9 @@ export { getSharedDir };
91
91
  /** Initialize local config if it doesn't exist */
92
92
  export declare function initLocalConfig(sharedDir?: string, storePath?: string): void;
93
93
  /** Initialize config with Google Drive cloud storage.
94
- * Checks for existing settings on GDrive mount first, then falls back to API folder. */
94
+ * Finds or creates the app-owned "mailx" folder via Drive API and stores its ID.
95
+ * No mount scanning — API only. Existing settings at other paths (e.g., home/.mailx
96
+ * from Desktop sync) must be migrated manually or via config.jsonc importPath. */
95
97
  export declare function initCloudConfig(provider?: "gdrive"): Promise<void>;
96
98
  declare const DEFAULT_SETTINGS: MailxSettings;
97
99
  /** Get historyDays for an account: per-account override > system override > shared default */
@@ -548,28 +548,14 @@ export function initLocalConfig(sharedDir, storePath) {
548
548
  atomicWrite(LOCAL_CONFIG_PATH, config);
549
549
  }
550
550
  /** Initialize config with Google Drive cloud storage.
551
- * Checks for existing settings on GDrive mount first, then falls back to API folder. */
551
+ * Finds or creates the app-owned "mailx" folder via Drive API and stores its ID.
552
+ * No mount scanning — API only. Existing settings at other paths (e.g., home/.mailx
553
+ * from Desktop sync) must be migrated manually or via config.jsonc importPath. */
552
554
  export async function initCloudConfig(provider = "gdrive") {
553
555
  const existing = readLocalConfig();
554
556
  if (existing.sharedDir)
555
557
  return; // Already configured
556
- // Check if settings already exist on a GDrive mount (e.g., home/.mailx from a previous setup)
557
- const home = process.env.USERPROFILE || process.env.HOME || "";
558
- const mountPaths = [
559
- path.join(home, "Google Drive", "My Drive", "home", ".mailx"),
560
- path.join(home, "Google Drive Streaming", "My Drive", "home", ".mailx"),
561
- ];
562
- for (const mountPath of mountPaths) {
563
- if (fs.existsSync(path.join(mountPath, "accounts.jsonc")) || fs.existsSync(path.join(mountPath, "settings.jsonc"))) {
564
- console.log(` Found existing settings on GDrive mount: ${mountPath}`);
565
- const sharedDir = { provider, path: "home/.mailx" };
566
- const config = { ...existing, sharedDir, storePath: existing.storePath || DEFAULT_STORE_PATH };
567
- fs.mkdirSync(LOCAL_DIR, { recursive: true });
568
- atomicWrite(LOCAL_CONFIG_PATH, config);
569
- return;
570
- }
571
- }
572
- // No mount — find or create the "mailx" folder via Drive API
558
+ // Find or create the "mailx" folder via Drive API
573
559
  const folderId = await gDriveFindOrCreateFolder();
574
560
  const sharedDir = { provider, path: "mailx", folderId: folderId || undefined };
575
561
  const config = { ...existing, sharedDir, storePath: existing.storePath || DEFAULT_STORE_PATH };