@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.
Files changed (172) hide show
  1. package/bin/mailx.js +52 -28
  2. package/client/app.js +113 -30
  3. package/client/components/folder-tree.js +84 -3
  4. package/client/components/message-list.js +164 -10
  5. package/client/components/message-viewer.js +130 -13
  6. package/client/compose/compose.html +4 -4
  7. package/client/compose/compose.js +53 -34
  8. package/client/index.html +50 -21
  9. package/client/lib/api-client.js +112 -31
  10. package/client/lib/mailxapi.js +123 -0
  11. package/client/package.json +1 -1
  12. package/client/styles/components.css +206 -16
  13. package/client/styles/layout.css +2 -1
  14. package/killmail.cmd +6 -0
  15. package/launch.ps1 +47 -5
  16. package/launcher/bin/mailx-app-linux +0 -0
  17. package/launcher/bin/mailx-app.exe +0 -0
  18. package/launcher/builder/build-config.json +11 -0
  19. package/launcher/builder/postinstall.js +81 -0
  20. package/package.json +2 -4
  21. package/packages/mailx-api/index.js +125 -29
  22. package/packages/mailx-core/index.d.ts +129 -0
  23. package/packages/mailx-core/index.js +323 -0
  24. package/packages/mailx-core/ipc.d.ts +13 -0
  25. package/packages/mailx-core/ipc.js +56 -0
  26. package/packages/mailx-core/package.json +18 -0
  27. package/packages/mailx-imap/index.d.ts +7 -1
  28. package/packages/mailx-imap/index.js +89 -14
  29. package/packages/mailx-server/index.js +42 -31
  30. package/packages/mailx-server/package.json +1 -2
  31. package/packages/mailx-settings/index.d.ts +1 -1
  32. package/packages/mailx-settings/index.js +21 -12
  33. package/packages/mailx-store/db.d.ts +6 -2
  34. package/packages/mailx-store/db.js +78 -16
  35. package/packages/mailx-store/file-store.d.ts +2 -8
  36. package/packages/mailx-store/file-store.js +7 -31
  37. package/packages/mailx-types/index.d.ts +3 -1
  38. package/.tswalk.json +0 -7396
  39. package/launcher/release.cmd +0 -4
  40. package/mailx.json +0 -9
  41. package/packages/mailx-api/node_modules/nodemailer/.ncurc.js +0 -9
  42. package/packages/mailx-api/node_modules/nodemailer/.prettierignore +0 -8
  43. package/packages/mailx-api/node_modules/nodemailer/.prettierrc +0 -12
  44. package/packages/mailx-api/node_modules/nodemailer/.prettierrc.js +0 -10
  45. package/packages/mailx-api/node_modules/nodemailer/.release-please-config.json +0 -9
  46. package/packages/mailx-api/node_modules/nodemailer/LICENSE +0 -16
  47. package/packages/mailx-api/node_modules/nodemailer/README.md +0 -86
  48. package/packages/mailx-api/node_modules/nodemailer/SECURITY.txt +0 -22
  49. package/packages/mailx-api/node_modules/nodemailer/eslint.config.js +0 -88
  50. package/packages/mailx-api/node_modules/nodemailer/lib/addressparser/index.js +0 -383
  51. package/packages/mailx-api/node_modules/nodemailer/lib/base64/index.js +0 -139
  52. package/packages/mailx-api/node_modules/nodemailer/lib/dkim/index.js +0 -253
  53. package/packages/mailx-api/node_modules/nodemailer/lib/dkim/message-parser.js +0 -155
  54. package/packages/mailx-api/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
  55. package/packages/mailx-api/node_modules/nodemailer/lib/dkim/sign.js +0 -117
  56. package/packages/mailx-api/node_modules/nodemailer/lib/fetch/cookies.js +0 -281
  57. package/packages/mailx-api/node_modules/nodemailer/lib/fetch/index.js +0 -280
  58. package/packages/mailx-api/node_modules/nodemailer/lib/json-transport/index.js +0 -82
  59. package/packages/mailx-api/node_modules/nodemailer/lib/mail-composer/index.js +0 -629
  60. package/packages/mailx-api/node_modules/nodemailer/lib/mailer/index.js +0 -441
  61. package/packages/mailx-api/node_modules/nodemailer/lib/mailer/mail-message.js +0 -316
  62. package/packages/mailx-api/node_modules/nodemailer/lib/mime-funcs/index.js +0 -625
  63. package/packages/mailx-api/node_modules/nodemailer/lib/mime-funcs/mime-types.js +0 -2113
  64. package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/index.js +0 -1316
  65. package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/last-newline.js +0 -33
  66. package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/le-unix.js +0 -43
  67. package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/le-windows.js +0 -52
  68. package/packages/mailx-api/node_modules/nodemailer/lib/nodemailer.js +0 -157
  69. package/packages/mailx-api/node_modules/nodemailer/lib/punycode/index.js +0 -460
  70. package/packages/mailx-api/node_modules/nodemailer/lib/qp/index.js +0 -227
  71. package/packages/mailx-api/node_modules/nodemailer/lib/sendmail-transport/index.js +0 -210
  72. package/packages/mailx-api/node_modules/nodemailer/lib/ses-transport/index.js +0 -234
  73. package/packages/mailx-api/node_modules/nodemailer/lib/shared/index.js +0 -754
  74. package/packages/mailx-api/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -108
  75. package/packages/mailx-api/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +0 -143
  76. package/packages/mailx-api/node_modules/nodemailer/lib/smtp-connection/index.js +0 -1870
  77. package/packages/mailx-api/node_modules/nodemailer/lib/smtp-pool/index.js +0 -652
  78. package/packages/mailx-api/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +0 -259
  79. package/packages/mailx-api/node_modules/nodemailer/lib/smtp-transport/index.js +0 -421
  80. package/packages/mailx-api/node_modules/nodemailer/lib/stream-transport/index.js +0 -135
  81. package/packages/mailx-api/node_modules/nodemailer/lib/well-known/index.js +0 -47
  82. package/packages/mailx-api/node_modules/nodemailer/lib/well-known/services.json +0 -611
  83. package/packages/mailx-api/node_modules/nodemailer/lib/xoauth2/index.js +0 -427
  84. package/packages/mailx-api/node_modules/nodemailer/package.json +0 -47
  85. package/packages/mailx-imap/node_modules/nodemailer/.ncurc.js +0 -9
  86. package/packages/mailx-imap/node_modules/nodemailer/.prettierignore +0 -8
  87. package/packages/mailx-imap/node_modules/nodemailer/.prettierrc +0 -12
  88. package/packages/mailx-imap/node_modules/nodemailer/.prettierrc.js +0 -10
  89. package/packages/mailx-imap/node_modules/nodemailer/.release-please-config.json +0 -9
  90. package/packages/mailx-imap/node_modules/nodemailer/LICENSE +0 -16
  91. package/packages/mailx-imap/node_modules/nodemailer/README.md +0 -86
  92. package/packages/mailx-imap/node_modules/nodemailer/SECURITY.txt +0 -22
  93. package/packages/mailx-imap/node_modules/nodemailer/eslint.config.js +0 -88
  94. package/packages/mailx-imap/node_modules/nodemailer/lib/addressparser/index.js +0 -383
  95. package/packages/mailx-imap/node_modules/nodemailer/lib/base64/index.js +0 -139
  96. package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/index.js +0 -253
  97. package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/message-parser.js +0 -155
  98. package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
  99. package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/sign.js +0 -117
  100. package/packages/mailx-imap/node_modules/nodemailer/lib/fetch/cookies.js +0 -281
  101. package/packages/mailx-imap/node_modules/nodemailer/lib/fetch/index.js +0 -280
  102. package/packages/mailx-imap/node_modules/nodemailer/lib/json-transport/index.js +0 -82
  103. package/packages/mailx-imap/node_modules/nodemailer/lib/mail-composer/index.js +0 -629
  104. package/packages/mailx-imap/node_modules/nodemailer/lib/mailer/index.js +0 -441
  105. package/packages/mailx-imap/node_modules/nodemailer/lib/mailer/mail-message.js +0 -316
  106. package/packages/mailx-imap/node_modules/nodemailer/lib/mime-funcs/index.js +0 -625
  107. package/packages/mailx-imap/node_modules/nodemailer/lib/mime-funcs/mime-types.js +0 -2113
  108. package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/index.js +0 -1316
  109. package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/last-newline.js +0 -33
  110. package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/le-unix.js +0 -43
  111. package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/le-windows.js +0 -52
  112. package/packages/mailx-imap/node_modules/nodemailer/lib/nodemailer.js +0 -157
  113. package/packages/mailx-imap/node_modules/nodemailer/lib/punycode/index.js +0 -460
  114. package/packages/mailx-imap/node_modules/nodemailer/lib/qp/index.js +0 -227
  115. package/packages/mailx-imap/node_modules/nodemailer/lib/sendmail-transport/index.js +0 -210
  116. package/packages/mailx-imap/node_modules/nodemailer/lib/ses-transport/index.js +0 -234
  117. package/packages/mailx-imap/node_modules/nodemailer/lib/shared/index.js +0 -754
  118. package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -108
  119. package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +0 -143
  120. package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-connection/index.js +0 -1870
  121. package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-pool/index.js +0 -652
  122. package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +0 -259
  123. package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-transport/index.js +0 -421
  124. package/packages/mailx-imap/node_modules/nodemailer/lib/stream-transport/index.js +0 -135
  125. package/packages/mailx-imap/node_modules/nodemailer/lib/well-known/index.js +0 -47
  126. package/packages/mailx-imap/node_modules/nodemailer/lib/well-known/services.json +0 -611
  127. package/packages/mailx-imap/node_modules/nodemailer/lib/xoauth2/index.js +0 -427
  128. package/packages/mailx-imap/node_modules/nodemailer/package.json +0 -47
  129. package/packages/mailx-send/node_modules/nodemailer/.ncurc.js +0 -9
  130. package/packages/mailx-send/node_modules/nodemailer/.prettierignore +0 -8
  131. package/packages/mailx-send/node_modules/nodemailer/.prettierrc +0 -12
  132. package/packages/mailx-send/node_modules/nodemailer/.prettierrc.js +0 -10
  133. package/packages/mailx-send/node_modules/nodemailer/.release-please-config.json +0 -9
  134. package/packages/mailx-send/node_modules/nodemailer/LICENSE +0 -16
  135. package/packages/mailx-send/node_modules/nodemailer/README.md +0 -86
  136. package/packages/mailx-send/node_modules/nodemailer/SECURITY.txt +0 -22
  137. package/packages/mailx-send/node_modules/nodemailer/eslint.config.js +0 -88
  138. package/packages/mailx-send/node_modules/nodemailer/lib/addressparser/index.js +0 -383
  139. package/packages/mailx-send/node_modules/nodemailer/lib/base64/index.js +0 -139
  140. package/packages/mailx-send/node_modules/nodemailer/lib/dkim/index.js +0 -253
  141. package/packages/mailx-send/node_modules/nodemailer/lib/dkim/message-parser.js +0 -155
  142. package/packages/mailx-send/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
  143. package/packages/mailx-send/node_modules/nodemailer/lib/dkim/sign.js +0 -117
  144. package/packages/mailx-send/node_modules/nodemailer/lib/fetch/cookies.js +0 -281
  145. package/packages/mailx-send/node_modules/nodemailer/lib/fetch/index.js +0 -280
  146. package/packages/mailx-send/node_modules/nodemailer/lib/json-transport/index.js +0 -82
  147. package/packages/mailx-send/node_modules/nodemailer/lib/mail-composer/index.js +0 -629
  148. package/packages/mailx-send/node_modules/nodemailer/lib/mailer/index.js +0 -441
  149. package/packages/mailx-send/node_modules/nodemailer/lib/mailer/mail-message.js +0 -316
  150. package/packages/mailx-send/node_modules/nodemailer/lib/mime-funcs/index.js +0 -625
  151. package/packages/mailx-send/node_modules/nodemailer/lib/mime-funcs/mime-types.js +0 -2113
  152. package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/index.js +0 -1316
  153. package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/last-newline.js +0 -33
  154. package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/le-unix.js +0 -43
  155. package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/le-windows.js +0 -52
  156. package/packages/mailx-send/node_modules/nodemailer/lib/nodemailer.js +0 -157
  157. package/packages/mailx-send/node_modules/nodemailer/lib/punycode/index.js +0 -460
  158. package/packages/mailx-send/node_modules/nodemailer/lib/qp/index.js +0 -227
  159. package/packages/mailx-send/node_modules/nodemailer/lib/sendmail-transport/index.js +0 -210
  160. package/packages/mailx-send/node_modules/nodemailer/lib/ses-transport/index.js +0 -234
  161. package/packages/mailx-send/node_modules/nodemailer/lib/shared/index.js +0 -754
  162. package/packages/mailx-send/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -108
  163. package/packages/mailx-send/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +0 -143
  164. package/packages/mailx-send/node_modules/nodemailer/lib/smtp-connection/index.js +0 -1870
  165. package/packages/mailx-send/node_modules/nodemailer/lib/smtp-pool/index.js +0 -652
  166. package/packages/mailx-send/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +0 -259
  167. package/packages/mailx-send/node_modules/nodemailer/lib/smtp-transport/index.js +0 -421
  168. package/packages/mailx-send/node_modules/nodemailer/lib/stream-transport/index.js +0 -135
  169. package/packages/mailx-send/node_modules/nodemailer/lib/well-known/index.js +0 -47
  170. package/packages/mailx-send/node_modules/nodemailer/lib/well-known/services.json +0 -611
  171. package/packages/mailx-send/node_modules/nodemailer/lib/xoauth2/index.js +0 -427
  172. 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 — checks every 10 seconds */
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
- const dbFolders = this.db.getFolders(accountId);
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: account.smtp.auth === "password"
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 — checks every 10 seconds */
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 { InitServerAsync, InitServerOptions } from "@bobfrankston/certsupport";
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
- // Dev: restart server + reload clients
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
- // Touch the server file to trigger node --watch restart
105
- const serverFile = path.join(import.meta.dirname, "index.js");
106
- setTimeout(() => {
107
- const now = new Date();
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
- // ── HTTPS Server (certsupport with SNI) ──
117
- const certspath = path.join(import.meta.dirname, "certs");
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 — only rebuild if empty (new DB or after reset)
151
- try {
152
- const ftsCount = db.db.prepare("SELECT COUNT(*) as cnt FROM messages_fts").get();
153
- if (!ftsCount?.cnt) {
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
- console.log(` Search index built: ${indexed} messages`);
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 opts = new InitServerOptions({
195
- port: PORT,
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
- console.log(`mailx server running on port ${PORT} (http+https)`);
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.4",
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.json — pointer to shared dir + local-only settings (storePath, historyDays)
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.json — pointer to shared dir + local-only settings (storePath, historyDays)
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.json");
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
- if (!fs.existsSync(LOCAL_CONFIG_PATH))
27
- return {};
28
- try {
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
- catch {
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
- if (!fs.existsSync(filePath))
47
- return null;
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(filePath, "utf-8"));
58
+ return parseJsonc(fs.readFileSync(actual, "utf-8"));
50
59
  }
51
60
  catch (e) {
52
- console.error(`Failed to read ${filePath}: ${e.message}`);
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
- getMessageByUid(accountId: string, uid: number): MessageEnvelope;
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 */