@bobfrankston/rmfmail 1.2.232 → 1.2.233

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 (146) hide show
  1. package/client/android-bootstrap.bundle.js +164 -152
  2. package/client/android-bootstrap.bundle.js.map +4 -4
  3. package/client/app.bundle.js +10 -104
  4. package/client/app.bundle.js.map +4 -4
  5. package/client/components/message-list.js +0 -60
  6. package/client/components/message-list.js.map +1 -1
  7. package/client/components/message-list.ts +0 -56
  8. package/client/compose/compose.bundle.js +1 -12
  9. package/client/compose/compose.bundle.js.map +2 -2
  10. package/client/compose/compose.html +0 -8
  11. package/client/compose/ghost-text.js +0 -8
  12. package/client/compose/ghost-text.js.map +1 -1
  13. package/client/compose/ghost-text.ts +0 -6
  14. package/client/compose/spellcheck-core.js +0 -1
  15. package/client/compose/spellcheck-core.js.map +1 -1
  16. package/client/compose/spellcheck-core.ts +0 -1
  17. package/client/index.html +0 -8
  18. package/client/lib/local-service.js +0 -73
  19. package/client/lib/local-service.js.map +1 -1
  20. package/client/lib/local-service.ts +0 -71
  21. package/client/lib/local-store.js +0 -3
  22. package/client/lib/local-store.js.map +1 -1
  23. package/client/lib/local-store.ts +0 -4
  24. package/package.json +1 -3
  25. package/packages/mailx-host/index.d.ts +0 -1
  26. package/packages/mailx-host/index.d.ts.map +1 -1
  27. package/packages/mailx-host/index.js +0 -1
  28. package/packages/mailx-host/index.js.map +1 -1
  29. package/packages/mailx-host/index.ts +0 -1
  30. package/packages/mailx-host/package.json +1 -1
  31. package/packages/mailx-service/index.d.ts.map +1 -1
  32. package/packages/mailx-service/index.js +36 -139
  33. package/packages/mailx-service/index.js.map +1 -1
  34. package/packages/mailx-service/index.ts +36 -148
  35. package/packages/mailx-settings/cloud.d.ts +0 -5
  36. package/packages/mailx-settings/cloud.d.ts.map +1 -1
  37. package/packages/mailx-settings/cloud.js +0 -16
  38. package/packages/mailx-settings/cloud.js.map +1 -1
  39. package/packages/mailx-settings/cloud.ts +0 -12
  40. package/packages/mailx-settings/index.d.ts +0 -3
  41. package/packages/mailx-settings/index.d.ts.map +1 -1
  42. package/packages/mailx-settings/index.js +0 -5
  43. package/packages/mailx-settings/index.js.map +1 -1
  44. package/packages/mailx-settings/index.ts +0 -7
  45. package/packages/mailx-store-web/main-thread-host.d.ts +1 -1
  46. package/packages/mailx-store-web/main-thread-host.d.ts.map +1 -1
  47. package/packages/mailx-store-web/main-thread-host.js +1 -38
  48. package/packages/mailx-store-web/main-thread-host.js.map +1 -1
  49. package/packages/mailx-store-web/main-thread-host.ts +0 -42
  50. package/packages/mailx-store-web/package.json +1 -1
  51. package/packages/mailx-store-web/web-service.d.ts.map +1 -1
  52. package/packages/mailx-store-web/web-service.js +28 -114
  53. package/packages/mailx-store-web/web-service.js.map +1 -1
  54. package/packages/mailx-store-web/web-service.ts +28 -124
  55. package/packages/mailx-store-web/web-settings.d.ts +0 -4
  56. package/packages/mailx-store-web/web-settings.d.ts.map +1 -1
  57. package/packages/mailx-store-web/web-settings.js +0 -29
  58. package/packages/mailx-store-web/web-settings.js.map +1 -1
  59. package/packages/mailx-store-web/web-settings.ts +0 -24
  60. package/packages/mailx-types/index.d.ts +2 -0
  61. package/packages/mailx-types/index.d.ts.map +1 -1
  62. package/packages/mailx-types/index.js +1 -0
  63. package/packages/mailx-types/index.js.map +1 -1
  64. package/packages/mailx-types/index.ts +2 -0
  65. package/packages/mailx-types/mime-build.d.ts +74 -0
  66. package/packages/mailx-types/mime-build.d.ts.map +1 -0
  67. package/packages/mailx-types/mime-build.js +160 -0
  68. package/packages/mailx-types/mime-build.js.map +1 -0
  69. package/packages/mailx-types/mime-build.ts +217 -0
  70. package/packages/mailx-types/package.json +1 -1
  71. package/packages/mailx-types/test/mime-build.test.mjs +200 -0
  72. package/client/components/calendar.js +0 -220
  73. package/client/components/calendar.js.map +0 -1
  74. package/client/components/calendar.ts +0 -213
  75. package/client/components/tasks.js +0 -261
  76. package/client/components/tasks.js.map +0 -1
  77. package/client/components/tasks.ts +0 -234
  78. package/packages/mailx-compose/.gitattributes +0 -10
  79. package/packages/mailx-compose/index.d.ts +0 -21
  80. package/packages/mailx-compose/index.d.ts.map +0 -1
  81. package/packages/mailx-compose/index.js +0 -66
  82. package/packages/mailx-compose/index.js.map +0 -1
  83. package/packages/mailx-compose/index.ts +0 -91
  84. package/packages/mailx-compose/package.json +0 -18
  85. package/packages/mailx-compose/tsconfig.json +0 -9
  86. package/packages/mailx-core/index.d.ts +0 -142
  87. package/packages/mailx-core/index.d.ts.map +0 -1
  88. package/packages/mailx-core/index.js +0 -430
  89. package/packages/mailx-core/index.js.map +0 -1
  90. package/packages/mailx-core/index.ts +0 -450
  91. package/packages/mailx-core/ipc.d.ts +0 -13
  92. package/packages/mailx-core/ipc.d.ts.map +0 -1
  93. package/packages/mailx-core/ipc.js +0 -56
  94. package/packages/mailx-core/ipc.js.map +0 -1
  95. package/packages/mailx-core/ipc.ts +0 -62
  96. package/packages/mailx-core/package.json +0 -18
  97. package/packages/mailx-core/tsconfig.json +0 -9
  98. package/packages/mailx-send/.gitattributes +0 -10
  99. package/packages/mailx-send/README.md +0 -217
  100. package/packages/mailx-send/cli-queue.d.ts +0 -11
  101. package/packages/mailx-send/cli-queue.d.ts.map +0 -1
  102. package/packages/mailx-send/cli-queue.js +0 -59
  103. package/packages/mailx-send/cli-queue.js.map +0 -1
  104. package/packages/mailx-send/cli-queue.ts +0 -62
  105. package/packages/mailx-send/cli-send.d.ts +0 -17
  106. package/packages/mailx-send/cli-send.d.ts.map +0 -1
  107. package/packages/mailx-send/cli-send.js +0 -75
  108. package/packages/mailx-send/cli-send.js.map +0 -1
  109. package/packages/mailx-send/cli-send.ts +0 -83
  110. package/packages/mailx-send/cli.d.ts +0 -22
  111. package/packages/mailx-send/cli.d.ts.map +0 -1
  112. package/packages/mailx-send/cli.js +0 -115
  113. package/packages/mailx-send/cli.js.map +0 -1
  114. package/packages/mailx-send/cli.ts +0 -126
  115. package/packages/mailx-send/index.d.ts +0 -77
  116. package/packages/mailx-send/index.d.ts.map +0 -1
  117. package/packages/mailx-send/index.js +0 -269
  118. package/packages/mailx-send/index.js.map +0 -1
  119. package/packages/mailx-send/index.ts +0 -338
  120. package/packages/mailx-send/mailsend/README.md +0 -133
  121. package/packages/mailx-send/mailsend/cli.d.ts +0 -12
  122. package/packages/mailx-send/mailsend/cli.d.ts.map +0 -1
  123. package/packages/mailx-send/mailsend/cli.js +0 -70
  124. package/packages/mailx-send/mailsend/cli.js.map +0 -1
  125. package/packages/mailx-send/mailsend/cli.ts +0 -81
  126. package/packages/mailx-send/mailsend/index.d.ts +0 -77
  127. package/packages/mailx-send/mailsend/index.d.ts.map +0 -1
  128. package/packages/mailx-send/mailsend/index.js +0 -264
  129. package/packages/mailx-send/mailsend/index.js.map +0 -1
  130. package/packages/mailx-send/mailsend/index.ts +0 -336
  131. package/packages/mailx-send/mailsend/package-lock.json +0 -65
  132. package/packages/mailx-send/mailsend/package.json +0 -26
  133. package/packages/mailx-send/mailsend/tsconfig.json +0 -21
  134. package/packages/mailx-send/package-lock.json +0 -65
  135. package/packages/mailx-send/package.json +0 -37
  136. package/packages/mailx-send/tsconfig.json +0 -21
  137. package/packages/mailx-server/.gitattributes +0 -10
  138. package/packages/mailx-server/index.d.ts +0 -8
  139. package/packages/mailx-server/index.d.ts.map +0 -1
  140. package/packages/mailx-server/index.js +0 -412
  141. package/packages/mailx-server/index.js.map +0 -1
  142. package/packages/mailx-server/index.ts +0 -445
  143. package/packages/mailx-server/package.json +0 -28
  144. package/packages/mailx-server/tsconfig.json +0 -9
  145. package/packages/mailx-server/tsconfig.tsbuildinfo +0 -1
  146. /package/packages/mailx-imap/{node_modules.npmglobalize-stash-40260 → node_modules.npmglobalize-stash-40004}/.package-lock.json +0 -0
@@ -1,445 +0,0 @@
1
- /**
2
- * @bobfrankston/mailx-server
3
- * Express server: serves client, mounts API, manages WebSocket push.
4
- */
5
-
6
- import express from "express";
7
- import { WebSocketServer, WebSocket } from "ws";
8
- import * as path from "node:path";
9
- import * as fs from "node:fs";
10
- import { MailxDB, FileMessageStore, Store } from "@bobfrankston/mailx-store";
11
- import { ImapManager } from "@bobfrankston/mailx-imap";
12
- import { createApiRouter } from "@bobfrankston/mailx-api";
13
- import { loadSettings, loadAccountsAsync, getConfigDir, getStorePath, getSharedDir, getStorageInfo } from "@bobfrankston/mailx-settings";
14
- import type { WsEvent } from "@bobfrankston/mailx-types";
15
- import { ports } from "@bobfrankston/miscinfo";
16
- import { createServer } from "node:http";
17
-
18
- const PORT = ports.mailx;
19
-
20
- // ── File logging ──
21
- const logDir = path.join(process.env.USERPROFILE || process.env.HOME || ".", ".rmfmail", "logs");
22
- fs.mkdirSync(logDir, { recursive: true });
23
- // Rotate: delete logs older than 7 days
24
- try {
25
- for (const f of fs.readdirSync(logDir).filter(f => f.startsWith("mailx-") && f.endsWith(".log"))) {
26
- const stat = fs.statSync(path.join(logDir, f));
27
- if (Date.now() - stat.mtimeMs > 7 * 86400000) fs.unlinkSync(path.join(logDir, f));
28
- }
29
- } catch { /* ignore cleanup errors */ }
30
- const logDate = new Date().toISOString().slice(0, 10);
31
- const logPath = path.join(logDir, `mailx-${logDate}.log`);
32
- const logStream = fs.createWriteStream(logPath, { flags: "a" });
33
- const origLog = console.log;
34
- const origErr = console.error;
35
- function logTs(): string { return new Date().toLocaleString("se", { hour12: false }); }
36
- console.log = (...args: any[]) => {
37
- const msg = `${logTs()} ${args.join(" ")}`;
38
- origLog(msg);
39
- logStream.write(msg + "\n");
40
- };
41
- console.error = (...args: any[]) => {
42
- const msg = `${logTs()} ERROR ${args.join(" ")}`;
43
- origErr(msg);
44
- logStream.write(msg + "\n");
45
- };
46
- // Read version from root package.json (the published version)
47
- const rootPkg = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, "..", "..", "package.json"), "utf-8"));
48
- const SERVER_VERSION = rootPkg.version;
49
-
50
- // ── Initialize ──
51
- // No initLocalConfig() here — config is created when user adds an account (initCloudConfig).
52
- // On fresh install, loadSettings() returns defaults and the setup form handles configuration.
53
-
54
- let settings = loadSettings();
55
- if (settings.accounts.length === 0) {
56
- // Try cloud API fallback (Google Drive, OneDrive) if filesystem mount not available
57
- const cloudAccounts = await loadAccountsAsync();
58
- if (cloudAccounts.length > 0) {
59
- settings = { ...settings, accounts: cloudAccounts };
60
- console.log(` Loaded ${cloudAccounts.length} account(s) from cloud API`);
61
- } else {
62
- console.log(" No accounts configured. Open http://127.0.0.1:9333 to add your email account.");
63
- }
64
- }
65
- const dbDir = getConfigDir();
66
- const db = new MailxDB(dbDir);
67
- import { NodeTcpTransport } from "@bobfrankston/node-tcp-transport";
68
- const store = new Store(db, new FileMessageStore(getStorePath()));
69
- const imapManager = new ImapManager(store, () => new NodeTcpTransport());
70
-
71
- // ── Express App ──
72
-
73
- const app = express();
74
- app.use(express.json({ limit: "Infinity" }));
75
-
76
- // Optional token gate — required whenever the server binds to anything other
77
- // than a loopback address. Set via `MAILX_SERVER_TOKEN=<secret>` (or the
78
- // shorter `MAILX_TOKEN`). When the server is loopback-only (the default), the
79
- // gate is a no-op since nothing outside the machine can reach it. When bound
80
- // externally (`MAILX_SERVER_HOST=0.0.0.0` or `--external`), connections must
81
- // present the token in either `?t=<token>` or the `x-mailx-token` header. No
82
- // token configured + external bind → server refuses to start (safer than
83
- // serving open).
84
- const SERVER_TOKEN = process.env.MAILX_SERVER_TOKEN || process.env.MAILX_TOKEN || "";
85
- const SERVER_HOST = process.env.MAILX_SERVER_HOST || "";
86
- const IS_EXTERNAL_BIND = SERVER_HOST && SERVER_HOST !== "127.0.0.1" && SERVER_HOST !== "localhost" && SERVER_HOST !== "::1";
87
- if (IS_EXTERNAL_BIND && !SERVER_TOKEN) {
88
- console.error("[server] Refusing to bind externally without MAILX_SERVER_TOKEN. Set the env var or drop the external host.");
89
- process.exit(2);
90
- }
91
- app.use((req, res, next) => {
92
- if (!IS_EXTERNAL_BIND) return next();
93
- // Allow the bare static file fetch so the login page can render; every
94
- // /api/* path requires the token.
95
- if (!req.path.startsWith("/api/")) return next();
96
- const provided = (req.query.t as string) || req.header("x-mailx-token") || "";
97
- if (provided !== SERVER_TOKEN) {
98
- res.status(401).json({ error: "missing or invalid token" });
99
- return;
100
- }
101
- next();
102
- });
103
-
104
- // Request logging
105
- app.use((req, res, next) => {
106
- const start = Date.now();
107
- res.on("finish", () => {
108
- const ms = Date.now() - start;
109
- // Skip noisy polling endpoints
110
- if (req.path.endsWith("/sync/pending")) return;
111
- console.log(` ${req.method} ${req.path} ${res.statusCode} ${ms}ms`);
112
- });
113
- next();
114
- });
115
-
116
- // Serve client static files
117
- const clientDir = path.join(import.meta.dirname, "..", "..", "client");
118
- const rootDir = path.join(import.meta.dirname, "..", "..");
119
- app.use(express.static(clientDir, { etag: false, lastModified: false, maxAge: 0 }));
120
- app.use("/node_modules", express.static(path.join(rootDir, "node_modules"), { etag: false, lastModified: false, maxAge: 0 }));
121
-
122
- // Mount API
123
- const apiRouter = createApiRouter(store, imapManager);
124
- app.use("/api", apiRouter);
125
-
126
- app.get("/api/version", (req, res) => {
127
- const storage = getStorageInfo();
128
- const imapAccounts = imapManager.getAccountCount();
129
- const dbAccounts = db.getAccounts().length;
130
- // Warn if DB has accounts but IMAP has none — stale DB, settings missing
131
- const settingsError = (dbAccounts > 0 && imapAccounts === 0)
132
- ? "No accounts loaded from settings — showing stale data. Check accounts.jsonc on your cloud drive."
133
- : undefined;
134
- res.json({ version: SERVER_VERSION, theme: settings.ui?.theme || "system", storage, imapAccounts, hasAccounts: imapAccounts > 0 || dbAccounts > 0, settingsError });
135
- });
136
-
137
- app.all("/info", (req, res) => {
138
- res.json({ version: SERVER_VERSION, uptime: Math.round(process.uptime()), port: PORT, imap: imapManager.useNativeClient ? "native" : "imapflow" });
139
- });
140
-
141
- app.get("/status", (req, res) => {
142
- const accounts = db.getAccounts();
143
- const pendingSync = db.getTotalPendingSyncCount();
144
- const uptime = process.uptime();
145
- const mem = process.memoryUsage();
146
-
147
- const accountInfo = accounts.map((a: any) => {
148
- const folders = db.getFolders(a.id);
149
- const inbox = folders.find((f: any) => f.specialUse === "inbox");
150
- return {
151
- id: a.id, name: a.name, folders: folders.length,
152
- inboxTotal: inbox?.totalCount || 0,
153
- inboxUnread: inbox?.unreadCount || 0,
154
- lastSync: a.lastSync ? new Date(a.lastSync).toLocaleString() : "never",
155
- };
156
- });
157
-
158
- res.type("html").send(`<!DOCTYPE html>
159
- <html><head><meta charset="UTF-8"><title>mailx status</title>
160
- <meta http-equiv="refresh" content="10">
161
- <style>body{font-family:system-ui;padding:2rem;max-width:600px;color:#cdd6f4;background:#1e1e2e}
162
- table{border-collapse:collapse;width:100%}td,th{text-align:left;padding:4px 12px;border-bottom:1px solid #333}
163
- h1{font-size:1.2rem}h2{font-size:1rem;margin-top:1.5rem}.ok{color:#a6e3a1}.warn{color:#f9e2af}
164
- a{color:#89b4fa}</style></head>
165
- <body>
166
- <h1>mailx status</h1>
167
- <p>mailx v${SERVER_VERSION}</p>
168
- <p>Uptime: ${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m | Memory: ${Math.round(mem.rss / 1048576)} MB</p>
169
- <p>Pending sync: <span class="${pendingSync > 0 ? "warn" : "ok"}">${pendingSync}</span></p>
170
- <h2>Accounts</h2>
171
- <table><tr><th>Account</th><th>Folders</th><th>Inbox</th><th>Unread</th><th>Last Sync</th></tr>
172
- ${accountInfo.map((a: any) => `<tr><td>${a.name}</td><td>${a.folders}</td><td>${a.inboxTotal}</td><td>${a.inboxUnread}</td><td>${a.lastSync}</td></tr>`).join("")}
173
- </table>
174
- <p style="margin-top:2rem;font-size:0.8rem"><a href="/">Open mailx</a> | Auto-refreshes every 10s</p>
175
- </body></html>`);
176
- });
177
-
178
- // Graceful exit — close IMAP, DB, HTTP then exit
179
- app.all("/api/exit", (req, res) => {
180
- res.json({ ok: true });
181
- setTimeout(() => shutdown(), 100);
182
- });
183
-
184
- // Restart server + reload clients
185
- app.post("/api/restart", (req, res) => {
186
- res.json({ ok: true });
187
- broadcast({ type: "reload" });
188
- setTimeout(async () => {
189
- console.log(" Restart requested via API");
190
- await shutdown();
191
- }, 500);
192
- });
193
-
194
- // Rebuild: wipe DB + message store, keep accounts/settings, restart
195
- app.post("/api/rebuild", (req, res) => {
196
- res.json({ ok: true });
197
- broadcast({ type: "reload" });
198
- setTimeout(async () => {
199
- console.log(" Rebuild requested — wiping DB and message store...");
200
- imapManager.stopPeriodicSync();
201
- try { await imapManager.shutdown(); } catch { /* proceed */ }
202
- db.close();
203
-
204
- // Remove DB files
205
- const dbDir = getConfigDir();
206
- for (const f of ["mailx.db", "mailx.db-wal", "mailx.db-shm"]) {
207
- const p = path.join(dbDir, f);
208
- if (fs.existsSync(p)) { fs.unlinkSync(p); console.log(` Deleted ${f}`); }
209
- }
210
-
211
- // Remove message store
212
- const storePath = getStorePath();
213
- if (fs.existsSync(storePath)) {
214
- fs.rmSync(storePath, { recursive: true });
215
- console.log(` Deleted ${storePath}`);
216
- }
217
-
218
- console.log(" Rebuild complete — restarting...");
219
- server?.close();
220
- process.exit(0);
221
- }, 500);
222
- });
223
-
224
- // SPA fallback
225
- app.get("*", (req, res) => {
226
- if (!req.path.startsWith("/api"))
227
- res.sendFile(path.join(clientDir, "index.html"));
228
- });
229
-
230
- // JSON error handler — all errors return JSON, never HTML
231
- app.use((err: any, _req: any, res: any, _next: any) => {
232
- console.error(`ERROR ${err.message}`);
233
- const status = err.status || err.statusCode || 500;
234
- res.status(status).json({ error: err.message || "Internal server error" });
235
- });
236
-
237
- // ── HTTP Server ──
238
-
239
- let server: ReturnType<typeof createServer>;
240
- let wss: WebSocketServer;
241
-
242
- const clients = new Set<WebSocket>();
243
-
244
- function wireWebSocket(): void {
245
- wss.on("connection", (ws) => {
246
- clients.add(ws);
247
- (ws as any).__alive = true;
248
- const connected: WsEvent = { type: "connected" };
249
- ws.send(JSON.stringify(connected));
250
-
251
- ws.on("pong", () => { (ws as any).__alive = true; });
252
- ws.on("close", () => clients.delete(ws));
253
- });
254
-
255
- // Heartbeat — detect dead connections, prevent CLOSE_WAIT zombie accumulation
256
- setInterval(() => {
257
- for (const ws of clients) {
258
- if ((ws as any).__alive === false) {
259
- clients.delete(ws);
260
- ws.terminate();
261
- continue;
262
- }
263
- (ws as any).__alive = false;
264
- ws.ping();
265
- }
266
- }, 30000);
267
- }
268
-
269
- function broadcast(event: WsEvent): void {
270
- const data = JSON.stringify(event);
271
- for (const ws of clients) {
272
- if (ws.readyState === WebSocket.OPEN)
273
- ws.send(data);
274
- }
275
- }
276
-
277
- // Wire IMAP events to WebSocket broadcasts
278
- imapManager.on("syncProgress", (accountId: string, phase: string, progress: number) => {
279
- broadcast({ type: "syncProgress", accountId, phase, progress });
280
- });
281
-
282
- imapManager.on("folderCountsChanged", (accountId: string, counts: Record<number, { total: number; unread: number }>) => {
283
- broadcast({ type: "folderCountsChanged", accountId, counts });
284
- });
285
-
286
- imapManager.on("folderSynced", (accountId: string, folderId: number, syncedAt: number) => {
287
- broadcast({ type: "folderSynced", accountId, entries: [{ folderId, syncedAt }] });
288
- });
289
-
290
- imapManager.on("syncError", (accountId: string, error: string) => {
291
- broadcast({ type: "error", message: `${accountId}: ${error}` });
292
- });
293
-
294
- imapManager.on("accountError", (accountId: string, error: string, hint: string, isOAuth: boolean) => {
295
- broadcast({ type: "accountError", accountId, error, hint, isOAuth });
296
- });
297
-
298
- // Local-first: body fetch lifecycle for the HTTP/WebSocket dev mode.
299
- // Mirrors the bin/mailx.ts IPC forwarders so a click on an uncached row
300
- // gets `cached:false` envelope back instantly and the body lands via a
301
- // WebSocket event instead of a per-click await.
302
- imapManager.on("bodyAvailable", (payload: any) => {
303
- broadcast({ type: "bodyAvailable", ...payload });
304
- });
305
- imapManager.on("bodyFetchError", (payload: any) => {
306
- broadcast({ type: "bodyFetchError", ...payload });
307
- });
308
- imapManager.on("messageRemoved", (payload: any) => {
309
- broadcast({ type: "messageRemoved", ...payload });
310
- });
311
-
312
- // ── Startup ──
313
-
314
- async function start(): Promise<void> {
315
- console.log("rmfmail server starting...");
316
-
317
- // Start HTTP server FIRST so UI is always reachable (even during IMAP startup)
318
- const externalAccess = process.argv.includes("--external");
319
- const hostname = externalAccess ? "0.0.0.0" : "127.0.0.1";
320
-
321
- // Retry listen with backoff — Windows CLOSE_WAIT zombies can hold the port for minutes after a crash
322
- for (let attempt = 0; attempt < 30; attempt++) {
323
- server = createServer(app);
324
- // Prevent CLOSE_WAIT accumulation: short keepAlive timeout + connection close headers
325
- server.keepAliveTimeout = 5000; // close idle keep-alive connections after 5s
326
- server.headersTimeout = 10000; // kill connections with no headers after 10s
327
- // Track connections for clean shutdown (prevents CLOSE_WAIT zombies on Windows)
328
- server.on("connection", (conn) => {
329
- openConnections.add(conn);
330
- conn.on("close", () => openConnections.delete(conn));
331
- });
332
- // Suppress EADDRINUSE from bubbling to uncaughtException — we handle it here
333
- server.on("error", () => {}); // will be replaced by listen handler below
334
- wss = new WebSocketServer({ server });
335
- wireWebSocket();
336
-
337
- const listenResult = await new Promise<"ok" | string>((resolve) => {
338
- server.removeAllListeners("error");
339
- server.once("error", (e: any) => { resolve(e.code || e.message); });
340
- server.listen({ port: PORT, host: hostname, exclusive: false }, () => {
341
- server.removeAllListeners("error");
342
- resolve("ok");
343
- });
344
- });
345
- if (listenResult === "ok") break;
346
- if (listenResult === "EADDRINUSE" && attempt < 29) {
347
- const wait = Math.min(2000 + attempt * 1000, 10000);
348
- console.log(` Port ${PORT} in use (CLOSE_WAIT zombies?) — retry ${attempt + 1}/30 in ${wait / 1000}s...`);
349
- server.close();
350
- await new Promise(r => setTimeout(r, wait));
351
- } else {
352
- throw new Error(`Cannot bind port ${PORT}: ${listenResult}`);
353
- }
354
- }
355
- console.log(`rmfmail server running on http://${hostname}:${PORT}`);
356
-
357
- // Seed contacts — async + chunked, runs in the background.
358
- db.seedContactsFromMessages()
359
- .then(seeded => { if (seeded > 0) console.log(` Seeded ${seeded} contacts`); })
360
- .catch(e => console.error(` contact seed failed: ${e?.message || e}`));
361
-
362
- // Search index — rebuild in background (non-blocking)
363
- setTimeout(() => {
364
- let ftsCount = 0;
365
- try { ftsCount = ((db as any).db.prepare("SELECT COUNT(*) as cnt FROM messages_fts").get() as any)?.cnt || 0; } catch { /* */ }
366
- if (ftsCount === 0) {
367
- const indexed = db.rebuildSearchIndex();
368
- if (indexed > 0) console.log(` Search index: ${indexed} messages`);
369
- }
370
- }, 5000);
371
-
372
- // Add configured accounts
373
- for (const account of settings.accounts) {
374
- if (!account.enabled && !(account as any).syncContacts) continue;
375
- try {
376
- await imapManager.addAccount(account);
377
- console.log(` Account added: ${account.name} (${account.id})${!account.enabled ? " [contacts only]" : ""}`);
378
- } catch (e: any) {
379
- console.error(` Failed to add account ${account.id}: ${e.message}`);
380
- }
381
- }
382
-
383
- // Initial sync, then start IDLE watchers
384
- if (settings.accounts.some(a => a.enabled)) {
385
- console.log(" Starting initial sync...");
386
- imapManager.syncAll().then(async () => {
387
- console.log(" Initial sync complete");
388
- // Start IDLE watchers for instant new-mail notifications
389
- await imapManager.startWatching();
390
- // Sync Google Contacts for OAuth accounts
391
- imapManager.syncAllContacts().catch(e =>
392
- console.error(` Google Contacts sync error: ${e.message}`)
393
- );
394
- }).catch(e => {
395
- console.error(` Initial sync error: ${e.message}`);
396
- });
397
- }
398
-
399
- // Periodic sync as fallback (IDLE handles most updates)
400
- imapManager.startPeriodicSync(settings.sync.intervalMinutes);
401
-
402
- // Outbox worker — processes queued outgoing messages
403
- imapManager.startOutboxWorker();
404
- }
405
-
406
- // ── Graceful Shutdown ──
407
-
408
- /** Track all open connections so we can destroy them on shutdown (prevents CLOSE_WAIT zombies) */
409
- const openConnections = new Set<import("node:net").Socket>();
410
-
411
- async function shutdown(): Promise<void> {
412
- console.log("\nShutting down...");
413
- const forceExit = setTimeout(() => { console.log("Force exit"); process.exit(1); }, 3000);
414
- imapManager.stopPeriodicSync();
415
- try { await imapManager.shutdown(); } catch { /* proceed */ }
416
- db.close();
417
- // Destroy all open connections immediately — prevents CLOSE_WAIT zombies on Windows
418
- for (const conn of openConnections) {
419
- conn.destroy();
420
- }
421
- openConnections.clear();
422
- server?.close();
423
- clearTimeout(forceExit);
424
- process.exit(0);
425
- }
426
-
427
- process.on("SIGINT", shutdown);
428
- process.on("SIGTERM", shutdown);
429
- process.on("unhandledRejection", (err: any) => {
430
- console.error("Unhandled rejection:", err?.message || err);
431
- });
432
- process.on("uncaughtException", (err) => {
433
- console.error("FATAL uncaught exception:", err.message);
434
- console.error(err.stack);
435
- // Other exceptions: stay alive, let node --watch handle file-change restarts
436
- });
437
- process.on("exit", (code) => {
438
- console.log(`Process exiting with code ${code}`);
439
- });
440
-
441
- // ── Entry Point ──
442
-
443
- await start();
444
-
445
- export { app, start };
@@ -1,28 +0,0 @@
1
- {
2
- "name": "@bobfrankston/mailx-server",
3
- "version": "1.0.9",
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-api": "file:../mailx-api",
16
- "@bobfrankston/mailx-settings": "file:../mailx-settings",
17
- "express": "^4.21.0",
18
- "ws": "^8.18.0"
19
- },
20
- "devDependencies": {
21
- "@types/express": "^5.0.0",
22
- "@types/ws": "^8.5.13"
23
- },
24
- "repository": {
25
- "type": "git",
26
- "url": "https://github.com/BobFrankston/mailx-server.git"
27
- }
28
- }
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": ".",
5
- "rootDir": "."
6
- },
7
- "include": ["*.ts"],
8
- "exclude": ["node_modules", "prev", "cruft", "tests"]
9
- }
@@ -1 +0,0 @@
1
- {"root":["./index.ts"],"version":"5.9.2"}