@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
|
-
//
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
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
|
-
|
|
529
|
-
|
|
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
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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 };
|