@bobfrankston/mailx 1.0.12 → 1.0.14
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 +52 -28
- package/client/app.js +113 -30
- package/client/components/folder-tree.js +84 -3
- package/client/components/message-list.js +164 -10
- package/client/components/message-viewer.js +130 -13
- package/client/compose/compose.html +4 -4
- package/client/compose/compose.js +53 -34
- package/client/index.html +50 -21
- package/client/lib/api-client.js +112 -31
- package/client/lib/mailxapi.js +123 -0
- package/client/package.json +1 -1
- package/client/styles/components.css +206 -16
- package/client/styles/layout.css +2 -1
- package/killmail.cmd +6 -0
- package/launch.ps1 +47 -5
- package/launcher/bin/mailx-app-linux +0 -0
- package/launcher/bin/mailx-app.exe +0 -0
- package/launcher/builder/build-config.json +11 -0
- package/launcher/builder/postinstall.js +81 -0
- package/package.json +2 -4
- package/packages/mailx-api/index.js +125 -29
- package/packages/mailx-core/index.d.ts +129 -0
- package/packages/mailx-core/index.js +323 -0
- package/packages/mailx-core/ipc.d.ts +13 -0
- package/packages/mailx-core/ipc.js +56 -0
- package/packages/mailx-core/package.json +18 -0
- package/packages/mailx-imap/index.d.ts +7 -1
- package/packages/mailx-imap/index.js +89 -14
- package/packages/mailx-server/index.js +42 -31
- package/packages/mailx-server/package.json +1 -2
- package/packages/mailx-settings/index.d.ts +1 -1
- package/packages/mailx-settings/index.js +21 -12
- package/packages/mailx-store/db.d.ts +6 -2
- package/packages/mailx-store/db.js +78 -16
- package/packages/mailx-store/file-store.d.ts +2 -8
- package/packages/mailx-store/file-store.js +7 -31
- package/packages/mailx-types/index.d.ts +3 -1
- package/.tswalk.json +0 -7396
- package/launcher/release.cmd +0 -4
- package/mailx.json +0 -9
- package/packages/mailx-api/node_modules/nodemailer/.ncurc.js +0 -9
- package/packages/mailx-api/node_modules/nodemailer/.prettierignore +0 -8
- package/packages/mailx-api/node_modules/nodemailer/.prettierrc +0 -12
- package/packages/mailx-api/node_modules/nodemailer/.prettierrc.js +0 -10
- package/packages/mailx-api/node_modules/nodemailer/.release-please-config.json +0 -9
- package/packages/mailx-api/node_modules/nodemailer/LICENSE +0 -16
- package/packages/mailx-api/node_modules/nodemailer/README.md +0 -86
- package/packages/mailx-api/node_modules/nodemailer/SECURITY.txt +0 -22
- package/packages/mailx-api/node_modules/nodemailer/eslint.config.js +0 -88
- package/packages/mailx-api/node_modules/nodemailer/lib/addressparser/index.js +0 -383
- package/packages/mailx-api/node_modules/nodemailer/lib/base64/index.js +0 -139
- package/packages/mailx-api/node_modules/nodemailer/lib/dkim/index.js +0 -253
- package/packages/mailx-api/node_modules/nodemailer/lib/dkim/message-parser.js +0 -155
- package/packages/mailx-api/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
- package/packages/mailx-api/node_modules/nodemailer/lib/dkim/sign.js +0 -117
- package/packages/mailx-api/node_modules/nodemailer/lib/fetch/cookies.js +0 -281
- package/packages/mailx-api/node_modules/nodemailer/lib/fetch/index.js +0 -280
- package/packages/mailx-api/node_modules/nodemailer/lib/json-transport/index.js +0 -82
- package/packages/mailx-api/node_modules/nodemailer/lib/mail-composer/index.js +0 -629
- package/packages/mailx-api/node_modules/nodemailer/lib/mailer/index.js +0 -441
- package/packages/mailx-api/node_modules/nodemailer/lib/mailer/mail-message.js +0 -316
- package/packages/mailx-api/node_modules/nodemailer/lib/mime-funcs/index.js +0 -625
- package/packages/mailx-api/node_modules/nodemailer/lib/mime-funcs/mime-types.js +0 -2113
- package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/index.js +0 -1316
- package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/last-newline.js +0 -33
- package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/le-unix.js +0 -43
- package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/le-windows.js +0 -52
- package/packages/mailx-api/node_modules/nodemailer/lib/nodemailer.js +0 -157
- package/packages/mailx-api/node_modules/nodemailer/lib/punycode/index.js +0 -460
- package/packages/mailx-api/node_modules/nodemailer/lib/qp/index.js +0 -227
- package/packages/mailx-api/node_modules/nodemailer/lib/sendmail-transport/index.js +0 -210
- package/packages/mailx-api/node_modules/nodemailer/lib/ses-transport/index.js +0 -234
- package/packages/mailx-api/node_modules/nodemailer/lib/shared/index.js +0 -754
- package/packages/mailx-api/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -108
- package/packages/mailx-api/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +0 -143
- package/packages/mailx-api/node_modules/nodemailer/lib/smtp-connection/index.js +0 -1870
- package/packages/mailx-api/node_modules/nodemailer/lib/smtp-pool/index.js +0 -652
- package/packages/mailx-api/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +0 -259
- package/packages/mailx-api/node_modules/nodemailer/lib/smtp-transport/index.js +0 -421
- package/packages/mailx-api/node_modules/nodemailer/lib/stream-transport/index.js +0 -135
- package/packages/mailx-api/node_modules/nodemailer/lib/well-known/index.js +0 -47
- package/packages/mailx-api/node_modules/nodemailer/lib/well-known/services.json +0 -611
- package/packages/mailx-api/node_modules/nodemailer/lib/xoauth2/index.js +0 -427
- package/packages/mailx-api/node_modules/nodemailer/package.json +0 -47
- package/packages/mailx-imap/node_modules/nodemailer/.ncurc.js +0 -9
- package/packages/mailx-imap/node_modules/nodemailer/.prettierignore +0 -8
- package/packages/mailx-imap/node_modules/nodemailer/.prettierrc +0 -12
- package/packages/mailx-imap/node_modules/nodemailer/.prettierrc.js +0 -10
- package/packages/mailx-imap/node_modules/nodemailer/.release-please-config.json +0 -9
- package/packages/mailx-imap/node_modules/nodemailer/LICENSE +0 -16
- package/packages/mailx-imap/node_modules/nodemailer/README.md +0 -86
- package/packages/mailx-imap/node_modules/nodemailer/SECURITY.txt +0 -22
- package/packages/mailx-imap/node_modules/nodemailer/eslint.config.js +0 -88
- package/packages/mailx-imap/node_modules/nodemailer/lib/addressparser/index.js +0 -383
- package/packages/mailx-imap/node_modules/nodemailer/lib/base64/index.js +0 -139
- package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/index.js +0 -253
- package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/message-parser.js +0 -155
- package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
- package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/sign.js +0 -117
- package/packages/mailx-imap/node_modules/nodemailer/lib/fetch/cookies.js +0 -281
- package/packages/mailx-imap/node_modules/nodemailer/lib/fetch/index.js +0 -280
- package/packages/mailx-imap/node_modules/nodemailer/lib/json-transport/index.js +0 -82
- package/packages/mailx-imap/node_modules/nodemailer/lib/mail-composer/index.js +0 -629
- package/packages/mailx-imap/node_modules/nodemailer/lib/mailer/index.js +0 -441
- package/packages/mailx-imap/node_modules/nodemailer/lib/mailer/mail-message.js +0 -316
- package/packages/mailx-imap/node_modules/nodemailer/lib/mime-funcs/index.js +0 -625
- package/packages/mailx-imap/node_modules/nodemailer/lib/mime-funcs/mime-types.js +0 -2113
- package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/index.js +0 -1316
- package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/last-newline.js +0 -33
- package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/le-unix.js +0 -43
- package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/le-windows.js +0 -52
- package/packages/mailx-imap/node_modules/nodemailer/lib/nodemailer.js +0 -157
- package/packages/mailx-imap/node_modules/nodemailer/lib/punycode/index.js +0 -460
- package/packages/mailx-imap/node_modules/nodemailer/lib/qp/index.js +0 -227
- package/packages/mailx-imap/node_modules/nodemailer/lib/sendmail-transport/index.js +0 -210
- package/packages/mailx-imap/node_modules/nodemailer/lib/ses-transport/index.js +0 -234
- package/packages/mailx-imap/node_modules/nodemailer/lib/shared/index.js +0 -754
- package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -108
- package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +0 -143
- package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-connection/index.js +0 -1870
- package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-pool/index.js +0 -652
- package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +0 -259
- package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-transport/index.js +0 -421
- package/packages/mailx-imap/node_modules/nodemailer/lib/stream-transport/index.js +0 -135
- package/packages/mailx-imap/node_modules/nodemailer/lib/well-known/index.js +0 -47
- package/packages/mailx-imap/node_modules/nodemailer/lib/well-known/services.json +0 -611
- package/packages/mailx-imap/node_modules/nodemailer/lib/xoauth2/index.js +0 -427
- package/packages/mailx-imap/node_modules/nodemailer/package.json +0 -47
- package/packages/mailx-send/node_modules/nodemailer/.ncurc.js +0 -9
- package/packages/mailx-send/node_modules/nodemailer/.prettierignore +0 -8
- package/packages/mailx-send/node_modules/nodemailer/.prettierrc +0 -12
- package/packages/mailx-send/node_modules/nodemailer/.prettierrc.js +0 -10
- package/packages/mailx-send/node_modules/nodemailer/.release-please-config.json +0 -9
- package/packages/mailx-send/node_modules/nodemailer/LICENSE +0 -16
- package/packages/mailx-send/node_modules/nodemailer/README.md +0 -86
- package/packages/mailx-send/node_modules/nodemailer/SECURITY.txt +0 -22
- package/packages/mailx-send/node_modules/nodemailer/eslint.config.js +0 -88
- package/packages/mailx-send/node_modules/nodemailer/lib/addressparser/index.js +0 -383
- package/packages/mailx-send/node_modules/nodemailer/lib/base64/index.js +0 -139
- package/packages/mailx-send/node_modules/nodemailer/lib/dkim/index.js +0 -253
- package/packages/mailx-send/node_modules/nodemailer/lib/dkim/message-parser.js +0 -155
- package/packages/mailx-send/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
- package/packages/mailx-send/node_modules/nodemailer/lib/dkim/sign.js +0 -117
- package/packages/mailx-send/node_modules/nodemailer/lib/fetch/cookies.js +0 -281
- package/packages/mailx-send/node_modules/nodemailer/lib/fetch/index.js +0 -280
- package/packages/mailx-send/node_modules/nodemailer/lib/json-transport/index.js +0 -82
- package/packages/mailx-send/node_modules/nodemailer/lib/mail-composer/index.js +0 -629
- package/packages/mailx-send/node_modules/nodemailer/lib/mailer/index.js +0 -441
- package/packages/mailx-send/node_modules/nodemailer/lib/mailer/mail-message.js +0 -316
- package/packages/mailx-send/node_modules/nodemailer/lib/mime-funcs/index.js +0 -625
- package/packages/mailx-send/node_modules/nodemailer/lib/mime-funcs/mime-types.js +0 -2113
- package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/index.js +0 -1316
- package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/last-newline.js +0 -33
- package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/le-unix.js +0 -43
- package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/le-windows.js +0 -52
- package/packages/mailx-send/node_modules/nodemailer/lib/nodemailer.js +0 -157
- package/packages/mailx-send/node_modules/nodemailer/lib/punycode/index.js +0 -460
- package/packages/mailx-send/node_modules/nodemailer/lib/qp/index.js +0 -227
- package/packages/mailx-send/node_modules/nodemailer/lib/sendmail-transport/index.js +0 -210
- package/packages/mailx-send/node_modules/nodemailer/lib/ses-transport/index.js +0 -234
- package/packages/mailx-send/node_modules/nodemailer/lib/shared/index.js +0 -754
- package/packages/mailx-send/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -108
- package/packages/mailx-send/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +0 -143
- package/packages/mailx-send/node_modules/nodemailer/lib/smtp-connection/index.js +0 -1870
- package/packages/mailx-send/node_modules/nodemailer/lib/smtp-pool/index.js +0 -652
- package/packages/mailx-send/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +0 -259
- package/packages/mailx-send/node_modules/nodemailer/lib/smtp-transport/index.js +0 -421
- package/packages/mailx-send/node_modules/nodemailer/lib/stream-transport/index.js +0 -135
- package/packages/mailx-send/node_modules/nodemailer/lib/well-known/index.js +0 -47
- package/packages/mailx-send/node_modules/nodemailer/lib/well-known/services.json +0 -611
- package/packages/mailx-send/node_modules/nodemailer/lib/xoauth2/index.js +0 -427
- package/packages/mailx-send/node_modules/nodemailer/package.json +0 -47
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPC protocol — stdin/stdout JSON lines for Rust↔Node communication.
|
|
3
|
+
* Rust launcher sends JSON requests, Node dispatches to core functions,
|
|
4
|
+
* sends JSON responses back. Events pushed to stdout as well.
|
|
5
|
+
*
|
|
6
|
+
* Protocol:
|
|
7
|
+
* Request: { "_action": "getMessages", "_cbid": "5", "accountId": "bobma", ... }
|
|
8
|
+
* Response: { "_type": "response", "_cbid": "5", "result": { ... } }
|
|
9
|
+
* Error: { "_type": "error", "_cbid": "5", "error": "message" }
|
|
10
|
+
* Event: { "_type": "event", "data": { "type": "folderCountsChanged", ... } }
|
|
11
|
+
*/
|
|
12
|
+
export declare function startIPC(): Promise<void>;
|
|
13
|
+
//# sourceMappingURL=ipc.d.ts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPC protocol — stdin/stdout JSON lines for Rust↔Node communication.
|
|
3
|
+
* Rust launcher sends JSON requests, Node dispatches to core functions,
|
|
4
|
+
* sends JSON responses back. Events pushed to stdout as well.
|
|
5
|
+
*
|
|
6
|
+
* Protocol:
|
|
7
|
+
* Request: { "_action": "getMessages", "_cbid": "5", "accountId": "bobma", ... }
|
|
8
|
+
* Response: { "_type": "response", "_cbid": "5", "result": { ... } }
|
|
9
|
+
* Error: { "_type": "error", "_cbid": "5", "error": "message" }
|
|
10
|
+
* Event: { "_type": "event", "data": { "type": "folderCountsChanged", ... } }
|
|
11
|
+
*/
|
|
12
|
+
import * as readline from "node:readline";
|
|
13
|
+
import { dispatch, initialize, onEvent } from "./index.js";
|
|
14
|
+
export async function startIPC() {
|
|
15
|
+
// Initialize core
|
|
16
|
+
await initialize();
|
|
17
|
+
// Push events to stdout
|
|
18
|
+
onEvent((event) => {
|
|
19
|
+
const line = JSON.stringify({ _type: "event", data: event });
|
|
20
|
+
process.stdout.write(line + "\n");
|
|
21
|
+
});
|
|
22
|
+
// Read JSON lines from stdin
|
|
23
|
+
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
24
|
+
rl.on("line", async (line) => {
|
|
25
|
+
let msg;
|
|
26
|
+
try {
|
|
27
|
+
msg = JSON.parse(line);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return; // Skip malformed JSON
|
|
31
|
+
}
|
|
32
|
+
const { _action, _cbid, ...params } = msg;
|
|
33
|
+
if (!_action || !_cbid)
|
|
34
|
+
return;
|
|
35
|
+
try {
|
|
36
|
+
const result = await dispatch(_action, params);
|
|
37
|
+
process.stdout.write(JSON.stringify({ _type: "response", _cbid, result }) + "\n");
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
process.stdout.write(JSON.stringify({ _type: "error", _cbid, error: e.message }) + "\n");
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
rl.on("close", () => {
|
|
44
|
+
console.error("IPC stdin closed, shutting down");
|
|
45
|
+
process.exit(0);
|
|
46
|
+
});
|
|
47
|
+
console.error("IPC ready");
|
|
48
|
+
}
|
|
49
|
+
// If run directly, start IPC mode
|
|
50
|
+
if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("ipc.js")) {
|
|
51
|
+
startIPC().catch(e => {
|
|
52
|
+
console.error(`IPC startup error: ${e.message}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=ipc.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bobfrankston/mailx-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc"
|
|
9
|
+
},
|
|
10
|
+
"license": "ISC",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@bobfrankston/mailx-types": "file:../mailx-types",
|
|
13
|
+
"@bobfrankston/mailx-store": "file:../mailx-store",
|
|
14
|
+
"@bobfrankston/mailx-imap": "file:../mailx-imap",
|
|
15
|
+
"@bobfrankston/mailx-settings": "file:../mailx-settings",
|
|
16
|
+
"mailparser": "^3.7.2"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -28,6 +28,10 @@ export declare class ImapManager extends EventEmitter {
|
|
|
28
28
|
private syncing;
|
|
29
29
|
private inboxSyncing;
|
|
30
30
|
constructor(db: MailxDB);
|
|
31
|
+
/** Get OAuth access token for an account (for SMTP auth) */
|
|
32
|
+
getOAuthToken(accountId: string): Promise<string | null>;
|
|
33
|
+
/** Search messages on the IMAP server — returns matching UIDs */
|
|
34
|
+
searchOnServer(accountId: string, mailboxPath: string, criteria: any): Promise<number[]>;
|
|
31
35
|
/** Create a fresh ImapClient for an account (disposable, single-use) */
|
|
32
36
|
private createClient;
|
|
33
37
|
/** Register an account */
|
|
@@ -59,6 +63,8 @@ export declare class ImapManager extends EventEmitter {
|
|
|
59
63
|
trashMessage(accountId: string, folderId: number, uid: number): Promise<void>;
|
|
60
64
|
/** Move a message between folders — local-first, queues IMAP sync */
|
|
61
65
|
moveMessage(accountId: string, uid: number, fromFolderId: number, toFolderId: number): Promise<void>;
|
|
66
|
+
/** Move message across accounts using iflow's moveMessageToServer */
|
|
67
|
+
moveMessageCrossAccount(fromAccountId: string, uid: number, fromFolderId: number, toAccountId: string, toFolderId: number): Promise<void>;
|
|
62
68
|
/** Undelete — move from Trash back to original folder */
|
|
63
69
|
undeleteMessage(accountId: string, uid: number, originalFolderId: number): Promise<void>;
|
|
64
70
|
/** Update flags — local-first, queues IMAP sync */
|
|
@@ -88,7 +94,7 @@ export declare class ImapManager extends EventEmitter {
|
|
|
88
94
|
private processLocalQueue;
|
|
89
95
|
/** Process Outbox — send pending messages with flag-based interlock */
|
|
90
96
|
processOutbox(accountId: string): Promise<void>;
|
|
91
|
-
/** Start background Outbox worker —
|
|
97
|
+
/** Start background Outbox worker — runs immediately then every 10 seconds */
|
|
92
98
|
startOutboxWorker(): void;
|
|
93
99
|
/** Stop Outbox worker */
|
|
94
100
|
stopOutboxWorker(): void;
|
|
@@ -55,6 +55,26 @@ export class ImapManager extends EventEmitter {
|
|
|
55
55
|
const storePath = getStorePath();
|
|
56
56
|
this.bodyStore = new FileMessageStore(storePath);
|
|
57
57
|
}
|
|
58
|
+
/** Get OAuth access token for an account (for SMTP auth) */
|
|
59
|
+
async getOAuthToken(accountId) {
|
|
60
|
+
const config = this.configs.get(accountId);
|
|
61
|
+
if (!config || !config.tokenProvider)
|
|
62
|
+
return null;
|
|
63
|
+
return config.tokenProvider();
|
|
64
|
+
}
|
|
65
|
+
/** Search messages on the IMAP server — returns matching UIDs */
|
|
66
|
+
async searchOnServer(accountId, mailboxPath, criteria) {
|
|
67
|
+
const client = this.createClient(accountId);
|
|
68
|
+
try {
|
|
69
|
+
return await client.searchMessages(mailboxPath, criteria);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
try {
|
|
73
|
+
await client.logout();
|
|
74
|
+
}
|
|
75
|
+
catch { /* ignore */ }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
58
78
|
/** Create a fresh ImapClient for an account (disposable, single-use) */
|
|
59
79
|
createClient(accountId) {
|
|
60
80
|
const config = this.configs.get(accountId);
|
|
@@ -106,12 +126,7 @@ export class ImapManager extends EventEmitter {
|
|
|
106
126
|
this.db.upsertFolder(accountId, folder.path, folder.name || folder.path.split(folder.delimiter || "/").pop() || folder.path, specialUse, folder.delimiter || "/");
|
|
107
127
|
}
|
|
108
128
|
this.emit("syncProgress", accountId, "folders", 100);
|
|
109
|
-
|
|
110
|
-
// Register folder names for human-readable store paths
|
|
111
|
-
for (const f of dbFolders) {
|
|
112
|
-
this.bodyStore.registerFolder(f.id, f.path);
|
|
113
|
-
}
|
|
114
|
-
return dbFolders;
|
|
129
|
+
return this.db.getFolders(accountId);
|
|
115
130
|
}
|
|
116
131
|
/** Sync messages for a specific folder */
|
|
117
132
|
async syncFolder(accountId, folderId, client) {
|
|
@@ -271,8 +286,6 @@ export class ImapManager extends EventEmitter {
|
|
|
271
286
|
const folders = await this.syncFolders(accountId, client);
|
|
272
287
|
await client.logout();
|
|
273
288
|
client = null;
|
|
274
|
-
// Fresh client for message sync (getFolderList corrupts imapflow state)
|
|
275
|
-
client = this.createClient(accountId);
|
|
276
289
|
// INBOX first so it's available fastest
|
|
277
290
|
folders.sort((a, b) => {
|
|
278
291
|
if (a.specialUse === "inbox")
|
|
@@ -281,11 +294,22 @@ export class ImapManager extends EventEmitter {
|
|
|
281
294
|
return 1;
|
|
282
295
|
return 0;
|
|
283
296
|
});
|
|
297
|
+
// Fresh client per folder — IMAP connections drop mid-sync on large accounts
|
|
284
298
|
for (const folder of folders) {
|
|
285
299
|
try {
|
|
300
|
+
client = this.createClient(accountId);
|
|
286
301
|
await this.syncFolder(accountId, folder.id, client);
|
|
302
|
+
await client.logout();
|
|
303
|
+
client = null;
|
|
287
304
|
}
|
|
288
305
|
catch (e) {
|
|
306
|
+
if (client) {
|
|
307
|
+
try {
|
|
308
|
+
await client.logout();
|
|
309
|
+
}
|
|
310
|
+
catch { /* ignore */ }
|
|
311
|
+
client = null;
|
|
312
|
+
}
|
|
289
313
|
if (e.responseText?.includes("doesn't exist")) {
|
|
290
314
|
console.log(` Removing non-existent folder: ${folder.path}`);
|
|
291
315
|
this.db.deleteFolder(folder.id);
|
|
@@ -295,8 +319,6 @@ export class ImapManager extends EventEmitter {
|
|
|
295
319
|
}
|
|
296
320
|
}
|
|
297
321
|
}
|
|
298
|
-
await client.logout();
|
|
299
|
-
client = null;
|
|
300
322
|
this.emit("syncComplete", accountId);
|
|
301
323
|
}
|
|
302
324
|
catch (e) {
|
|
@@ -493,6 +515,38 @@ export class ImapManager extends EventEmitter {
|
|
|
493
515
|
// Try immediate sync
|
|
494
516
|
this.processSyncActions(accountId).catch(() => { });
|
|
495
517
|
}
|
|
518
|
+
/** Move message across accounts using iflow's moveMessageToServer */
|
|
519
|
+
async moveMessageCrossAccount(fromAccountId, uid, fromFolderId, toAccountId, toFolderId) {
|
|
520
|
+
const fromFolders = this.db.getFolders(fromAccountId);
|
|
521
|
+
const fromFolder = fromFolders.find(f => f.id === fromFolderId);
|
|
522
|
+
if (!fromFolder)
|
|
523
|
+
throw new Error(`Source folder ${fromFolderId} not found`);
|
|
524
|
+
const toFolders = this.db.getFolders(toAccountId);
|
|
525
|
+
const toFolder = toFolders.find(f => f.id === toFolderId);
|
|
526
|
+
if (!toFolder)
|
|
527
|
+
throw new Error(`Target folder ${toFolderId} not found`);
|
|
528
|
+
const sourceClient = this.createClient(fromAccountId);
|
|
529
|
+
const targetClient = this.createClient(toAccountId);
|
|
530
|
+
try {
|
|
531
|
+
const msg = await sourceClient.fetchMessageByUid(fromFolder.path, uid, { source: true });
|
|
532
|
+
if (!msg)
|
|
533
|
+
throw new Error(`Message UID ${uid} not found in ${fromFolder.path}`);
|
|
534
|
+
await sourceClient.moveMessageToServer(msg, fromFolder.path, targetClient, toFolder.path);
|
|
535
|
+
// Remove from local DB
|
|
536
|
+
this.db.deleteMessage(fromAccountId, uid);
|
|
537
|
+
console.log(` Cross-account move: ${fromAccountId}/${fromFolder.path} UID ${uid} → ${toAccountId}/${toFolder.path}`);
|
|
538
|
+
}
|
|
539
|
+
finally {
|
|
540
|
+
try {
|
|
541
|
+
await sourceClient.logout();
|
|
542
|
+
}
|
|
543
|
+
catch { /* ignore */ }
|
|
544
|
+
try {
|
|
545
|
+
await targetClient.logout();
|
|
546
|
+
}
|
|
547
|
+
catch { /* ignore */ }
|
|
548
|
+
}
|
|
549
|
+
}
|
|
496
550
|
/** Undelete — move from Trash back to original folder */
|
|
497
551
|
async undeleteMessage(accountId, uid, originalFolderId) {
|
|
498
552
|
const trash = this.findFolder(accountId, "trash");
|
|
@@ -817,13 +871,21 @@ export class ImapManager extends EventEmitter {
|
|
|
817
871
|
}
|
|
818
872
|
// Send via SMTP
|
|
819
873
|
try {
|
|
874
|
+
let smtpAuth;
|
|
875
|
+
if (account.smtp.auth === "password") {
|
|
876
|
+
smtpAuth = { user: account.smtp.user, pass: account.smtp.password };
|
|
877
|
+
}
|
|
878
|
+
else if (account.smtp.auth === "oauth2") {
|
|
879
|
+
const accessToken = await this.getOAuthToken(accountId);
|
|
880
|
+
if (!accessToken)
|
|
881
|
+
throw new Error("OAuth token not available — re-authenticate");
|
|
882
|
+
smtpAuth = { type: "OAuth2", user: account.smtp.user, accessToken };
|
|
883
|
+
}
|
|
820
884
|
const transport = createTransport({
|
|
821
885
|
host: account.smtp.host,
|
|
822
886
|
port: account.smtp.port,
|
|
823
887
|
secure: account.smtp.port === 465,
|
|
824
|
-
auth:
|
|
825
|
-
? { user: account.smtp.user, pass: account.smtp.password }
|
|
826
|
-
: undefined,
|
|
888
|
+
auth: smtpAuth,
|
|
827
889
|
tls: { rejectUnauthorized: false },
|
|
828
890
|
});
|
|
829
891
|
// Parse recipients from raw message headers for SMTP envelope
|
|
@@ -874,10 +936,23 @@ export class ImapManager extends EventEmitter {
|
|
|
874
936
|
catch { /* ignore */ }
|
|
875
937
|
}
|
|
876
938
|
}
|
|
877
|
-
/** Start background Outbox worker —
|
|
939
|
+
/** Start background Outbox worker — runs immediately then every 10 seconds */
|
|
878
940
|
startOutboxWorker() {
|
|
879
941
|
if (this.outboxInterval)
|
|
880
942
|
return;
|
|
943
|
+
// Run once immediately on startup
|
|
944
|
+
const processAll = async () => {
|
|
945
|
+
for (const [accountId] of this.configs) {
|
|
946
|
+
try {
|
|
947
|
+
await this.processLocalQueue(accountId);
|
|
948
|
+
await this.processOutbox(accountId);
|
|
949
|
+
}
|
|
950
|
+
catch (e) {
|
|
951
|
+
console.error(` [outbox] Error for ${accountId}: ${e.message}`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
setTimeout(() => processAll(), 3000); // 3s after startup (let connections settle)
|
|
881
956
|
this.outboxInterval = setInterval(async () => {
|
|
882
957
|
for (const [accountId] of this.configs) {
|
|
883
958
|
try {
|
|
@@ -11,10 +11,10 @@ import { ImapManager } from "@bobfrankston/mailx-imap";
|
|
|
11
11
|
import { createApiRouter } from "@bobfrankston/mailx-api";
|
|
12
12
|
import { loadSettings, getConfigDir, initLocalConfig } from "@bobfrankston/mailx-settings";
|
|
13
13
|
import { ports } from "@bobfrankston/miscinfo";
|
|
14
|
-
import {
|
|
14
|
+
import { createServer } from "node:http";
|
|
15
15
|
const PORT = ports.mailx;
|
|
16
16
|
// ── File logging ──
|
|
17
|
-
const logDir = path.join(process.env.USERPROFILE || process.env.HOME || ".", ".mailx");
|
|
17
|
+
const logDir = path.join(process.env.USERPROFILE || process.env.HOME || ".", ".mailx", "logs");
|
|
18
18
|
fs.mkdirSync(logDir, { recursive: true });
|
|
19
19
|
// Rotate: delete logs older than 7 days
|
|
20
20
|
try {
|
|
@@ -53,7 +53,19 @@ const db = new MailxDB(dbDir);
|
|
|
53
53
|
const imapManager = new ImapManager(db);
|
|
54
54
|
// ── Express App ──
|
|
55
55
|
const app = express();
|
|
56
|
-
app.use(express.json());
|
|
56
|
+
app.use(express.json({ limit: "Infinity" }));
|
|
57
|
+
// Request logging
|
|
58
|
+
app.use((req, res, next) => {
|
|
59
|
+
const start = Date.now();
|
|
60
|
+
res.on("finish", () => {
|
|
61
|
+
const ms = Date.now() - start;
|
|
62
|
+
// Skip noisy polling endpoints
|
|
63
|
+
if (req.path === "/api/sync/pending")
|
|
64
|
+
return;
|
|
65
|
+
console.log(` ${req.method} ${req.path} ${res.statusCode} ${ms}ms`);
|
|
66
|
+
});
|
|
67
|
+
next();
|
|
68
|
+
});
|
|
57
69
|
// Serve client static files
|
|
58
70
|
const clientDir = path.join(import.meta.dirname, "..", "..", "client");
|
|
59
71
|
const rootDir = path.join(import.meta.dirname, "..", "..");
|
|
@@ -97,15 +109,14 @@ ${accountInfo.map((a) => `<tr><td>${a.name}</td><td>${a.folders}</td><td>${a.inb
|
|
|
97
109
|
<p style="margin-top:2rem;font-size:0.8rem"><a href="/">Open mailx</a> | Auto-refreshes every 10s</p>
|
|
98
110
|
</body></html>`);
|
|
99
111
|
});
|
|
100
|
-
//
|
|
112
|
+
// Restart server + reload clients
|
|
101
113
|
app.post("/api/restart", (req, res) => {
|
|
102
114
|
res.json({ ok: true });
|
|
103
115
|
broadcast({ type: "reload" });
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
fs.utimesSync(serverFile, now, now);
|
|
116
|
+
// Graceful shutdown — node --watch will auto-restart
|
|
117
|
+
setTimeout(async () => {
|
|
118
|
+
console.log(" Restart requested via API");
|
|
119
|
+
await shutdown();
|
|
109
120
|
}, 500);
|
|
110
121
|
});
|
|
111
122
|
// SPA fallback
|
|
@@ -113,8 +124,13 @@ app.get("*", (req, res) => {
|
|
|
113
124
|
if (!req.path.startsWith("/api"))
|
|
114
125
|
res.sendFile(path.join(clientDir, "index.html"));
|
|
115
126
|
});
|
|
116
|
-
//
|
|
117
|
-
|
|
127
|
+
// JSON error handler — all errors return JSON, never HTML
|
|
128
|
+
app.use((err, _req, res, _next) => {
|
|
129
|
+
console.error(`ERROR ${err.message}`);
|
|
130
|
+
const status = err.status || err.statusCode || 500;
|
|
131
|
+
res.status(status).json({ error: err.message || "Internal server error" });
|
|
132
|
+
});
|
|
133
|
+
// ── HTTP Server ──
|
|
118
134
|
let server;
|
|
119
135
|
let wss;
|
|
120
136
|
const clients = new Set();
|
|
@@ -147,19 +163,19 @@ async function start() {
|
|
|
147
163
|
const seeded = db.seedContactsFromMessages();
|
|
148
164
|
if (seeded > 0)
|
|
149
165
|
console.log(` Seeded ${seeded} contacts`);
|
|
150
|
-
// Search index —
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
166
|
+
// Search index — rebuild in background after server starts (non-blocking)
|
|
167
|
+
setTimeout(() => {
|
|
168
|
+
let ftsCount = 0;
|
|
169
|
+
try {
|
|
170
|
+
ftsCount = db.db.prepare("SELECT COUNT(*) as cnt FROM messages_fts").get()?.cnt || 0;
|
|
171
|
+
}
|
|
172
|
+
catch { /* */ }
|
|
173
|
+
if (ftsCount === 0) {
|
|
154
174
|
const indexed = db.rebuildSearchIndex();
|
|
155
|
-
|
|
175
|
+
if (indexed > 0)
|
|
176
|
+
console.log(` Search index: ${indexed} messages`);
|
|
156
177
|
}
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
// FTS table might not exist yet
|
|
160
|
-
const indexed = db.rebuildSearchIndex();
|
|
161
|
-
console.log(` Search index built: ${indexed} messages`);
|
|
162
|
-
}
|
|
178
|
+
}, 5000);
|
|
163
179
|
// Add configured accounts
|
|
164
180
|
for (const account of settings.accounts) {
|
|
165
181
|
if (!account.enabled)
|
|
@@ -191,17 +207,12 @@ async function start() {
|
|
|
191
207
|
imapManager.startOutboxWorker();
|
|
192
208
|
// Start server — localhost only by default, --external for network access
|
|
193
209
|
const externalAccess = process.argv.includes("--external");
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
hostname: externalAccess ? undefined : "127.0.0.1",
|
|
197
|
-
app,
|
|
198
|
-
certspath,
|
|
199
|
-
msger: (msg) => console.log(` [cert] ${msg}`),
|
|
200
|
-
});
|
|
201
|
-
server = await InitServerAsync(opts);
|
|
210
|
+
const hostname = externalAccess ? "0.0.0.0" : "127.0.0.1";
|
|
211
|
+
server = createServer(app);
|
|
202
212
|
wss = new WebSocketServer({ server });
|
|
203
213
|
wireWebSocket();
|
|
204
|
-
|
|
214
|
+
await new Promise((resolve) => server.listen(PORT, hostname, resolve));
|
|
215
|
+
console.log(`mailx server running on http://${hostname}:${PORT}`);
|
|
205
216
|
}
|
|
206
217
|
// ── Graceful Shutdown ──
|
|
207
218
|
async function shutdown() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
"@bobfrankston/mailx-imap": "file:../mailx-imap",
|
|
15
15
|
"@bobfrankston/mailx-api": "file:../mailx-api",
|
|
16
16
|
"@bobfrankston/mailx-settings": "file:../mailx-settings",
|
|
17
|
-
"@bobfrankston/certsupport": "file:../../../../projects/nodejs/certsupport",
|
|
18
17
|
"express": "^4.21.0",
|
|
19
18
|
"ws": "^8.18.0"
|
|
20
19
|
},
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* allowlist.jsonc — remote content allow-list
|
|
9
9
|
*
|
|
10
10
|
* Local overrides (~/.mailx/):
|
|
11
|
-
* config.
|
|
11
|
+
* config.jsonc — pointer to shared dir + local-only settings (storePath, historyDays)
|
|
12
12
|
* accounts.jsonc — cached copy, fallback when shared unavailable
|
|
13
13
|
* preferences.jsonc — local overrides merged on top of shared
|
|
14
14
|
* allowlist.jsonc — cached copy
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* allowlist.jsonc — remote content allow-list
|
|
9
9
|
*
|
|
10
10
|
* Local overrides (~/.mailx/):
|
|
11
|
-
* config.
|
|
11
|
+
* config.jsonc — pointer to shared dir + local-only settings (storePath, historyDays)
|
|
12
12
|
* accounts.jsonc — cached copy, fallback when shared unavailable
|
|
13
13
|
* preferences.jsonc — local overrides merged on top of shared
|
|
14
14
|
* allowlist.jsonc — cached copy
|
|
@@ -20,17 +20,17 @@ import * as path from "node:path";
|
|
|
20
20
|
import { parse as parseJsonc } from "jsonc-parser";
|
|
21
21
|
// ── Paths ──
|
|
22
22
|
const LOCAL_DIR = path.join(process.env.USERPROFILE || process.env.HOME || ".", ".mailx");
|
|
23
|
-
const LOCAL_CONFIG_PATH = path.join(LOCAL_DIR, "config.
|
|
23
|
+
const LOCAL_CONFIG_PATH = path.join(LOCAL_DIR, "config.jsonc");
|
|
24
|
+
const LEGACY_CONFIG_PATH = path.join(LOCAL_DIR, "config.json");
|
|
24
25
|
const DEFAULT_STORE_PATH = path.join(LOCAL_DIR, "mailxstore");
|
|
25
26
|
function readLocalConfig() {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return JSON.parse(fs.readFileSync(LOCAL_CONFIG_PATH, "utf-8"));
|
|
27
|
+
// Migrate config.json → config.jsonc
|
|
28
|
+
if (!fs.existsSync(LOCAL_CONFIG_PATH) && fs.existsSync(LEGACY_CONFIG_PATH)) {
|
|
29
|
+
fs.renameSync(LEGACY_CONFIG_PATH, LOCAL_CONFIG_PATH);
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
if (!fs.existsSync(LOCAL_CONFIG_PATH))
|
|
32
32
|
return {};
|
|
33
|
-
}
|
|
33
|
+
return readJsonc(LOCAL_CONFIG_PATH) || {};
|
|
34
34
|
}
|
|
35
35
|
function getSharedDir() {
|
|
36
36
|
const config = readLocalConfig();
|
|
@@ -42,14 +42,23 @@ function getSharedDir() {
|
|
|
42
42
|
return LOCAL_DIR;
|
|
43
43
|
}
|
|
44
44
|
// ── File helpers ──
|
|
45
|
+
/** Read JSON or JSONC file. If exact path not found, tries .json/.jsonc variant. */
|
|
45
46
|
function readJsonc(filePath) {
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
let actual = filePath;
|
|
48
|
+
if (!fs.existsSync(actual)) {
|
|
49
|
+
// Try alternate extension
|
|
50
|
+
if (actual.endsWith(".jsonc"))
|
|
51
|
+
actual = actual.replace(/\.jsonc$/, ".json");
|
|
52
|
+
else if (actual.endsWith(".json"))
|
|
53
|
+
actual = actual.replace(/\.json$/, ".jsonc");
|
|
54
|
+
if (!fs.existsSync(actual))
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
48
57
|
try {
|
|
49
|
-
return parseJsonc(fs.readFileSync(
|
|
58
|
+
return parseJsonc(fs.readFileSync(actual, "utf-8"));
|
|
50
59
|
}
|
|
51
60
|
catch (e) {
|
|
52
|
-
console.error(`Failed to read ${
|
|
61
|
+
console.error(`Failed to read ${actual}: ${e.message}`);
|
|
53
62
|
return null;
|
|
54
63
|
}
|
|
55
64
|
}
|
|
@@ -44,7 +44,9 @@ export declare class MailxDB {
|
|
|
44
44
|
bodyPath: string;
|
|
45
45
|
}): number;
|
|
46
46
|
getMessages(query: MessageQuery): PagedResult<MessageEnvelope>;
|
|
47
|
-
|
|
47
|
+
/** Unified inbox: all inbox folders across accounts, sorted by date, paginated in SQL */
|
|
48
|
+
getUnifiedInbox(page?: number, pageSize?: number): PagedResult<MessageEnvelope>;
|
|
49
|
+
getMessageByUid(accountId: string, uid: number, folderId?: number): MessageEnvelope;
|
|
48
50
|
getMessageBodyPath(accountId: string, uid: number): string;
|
|
49
51
|
updateMessageFlags(accountId: string, uid: number, flags: string[]): void;
|
|
50
52
|
getHighestUid(accountId: string, folderId: number): number;
|
|
@@ -52,6 +54,8 @@ export declare class MailxDB {
|
|
|
52
54
|
getUidsForFolder(accountId: string, folderId: number): number[];
|
|
53
55
|
/** Delete a message by account + UID */
|
|
54
56
|
deleteMessage(accountId: string, uid: number): void;
|
|
57
|
+
/** Recalculate folder total/unread counts from actual messages */
|
|
58
|
+
recalcFolderCounts(folderId: number): void;
|
|
55
59
|
/** Bulk insert within a transaction for sync performance */
|
|
56
60
|
beginTransaction(): void;
|
|
57
61
|
commitTransaction(): void;
|
|
@@ -68,7 +72,7 @@ export declare class MailxDB {
|
|
|
68
72
|
useCount: number;
|
|
69
73
|
}[];
|
|
70
74
|
/** Full-text search across all messages. Supports qualifiers: from:, to:, subject: */
|
|
71
|
-
searchMessages(query: string, page?: number, pageSize?: number): PagedResult<MessageEnvelope>;
|
|
75
|
+
searchMessages(query: string, page?: number, pageSize?: number, accountId?: string, folderId?: number): PagedResult<MessageEnvelope>;
|
|
72
76
|
/** Rebuild FTS index from existing messages */
|
|
73
77
|
rebuildSearchIndex(): number;
|
|
74
78
|
/** Queue a local action for later sync to IMAP */
|