@bobfrankston/mailx 1.0.98 → 1.0.99
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 +22 -11
- package/killmail.cmd +8 -1
- package/package.json +1 -1
- package/packages/mailx-imap/index.js +42 -10
- package/packages/mailx-server/index.js +7 -1
package/bin/mailx.js
CHANGED
|
@@ -49,6 +49,13 @@ if (hasFlag("kill")) {
|
|
|
49
49
|
const { execSync } = await import("node:child_process");
|
|
50
50
|
let killed = 0;
|
|
51
51
|
|
|
52
|
+
// Try graceful exit first
|
|
53
|
+
try {
|
|
54
|
+
execSync(`curl -s -m 2 http://localhost:${PORT}/api/exit`, { stdio: "pipe" });
|
|
55
|
+
log("Sent graceful exit");
|
|
56
|
+
execSync("timeout /t 1 /nobreak", { stdio: "pipe" });
|
|
57
|
+
} catch { /* server may not be responding */ }
|
|
58
|
+
|
|
52
59
|
if (process.platform === "win32") {
|
|
53
60
|
// Kill by port
|
|
54
61
|
try {
|
|
@@ -59,17 +66,14 @@ if (hasFlag("kill")) {
|
|
|
59
66
|
}
|
|
60
67
|
} catch { /* no process on port */ }
|
|
61
68
|
|
|
62
|
-
// Kill any node.exe
|
|
63
|
-
const installDir = path.join(import.meta.dirname, "..");
|
|
69
|
+
// Kill any node.exe running mailx-server (uses tasklist + powershell instead of wmic)
|
|
64
70
|
try {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
}
|
|
71
|
+
const ps = execSync(
|
|
72
|
+
`powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"name='node.exe'\\" | Where-Object { $_.CommandLine -match 'mailx-server|mailx\\\\packages' } | Select-Object -ExpandProperty ProcessId"`,
|
|
73
|
+
{ encoding: "utf-8" }
|
|
74
|
+
).trim();
|
|
75
|
+
for (const pid of ps.split("\n").map(s => s.trim()).filter(s => /^\d+$/.test(s))) {
|
|
76
|
+
try { execSync(`taskkill /F /PID ${pid}`, { stdio: "pipe" }); console.log(`Killed PID ${pid} (mailx node process)`); killed++; } catch { /* */ }
|
|
73
77
|
}
|
|
74
78
|
} catch { /* */ }
|
|
75
79
|
|
|
@@ -79,8 +83,15 @@ if (hasFlag("kill")) {
|
|
|
79
83
|
try { execSync(`fuser -k ${PORT}/tcp`, { stdio: "pipe" }); console.log(`Killed process on port ${PORT}`); killed++; } catch { /* */ }
|
|
80
84
|
}
|
|
81
85
|
|
|
86
|
+
// Clean up stale SQLite WAL/SHM files
|
|
87
|
+
const mailxDir = path.join(process.env.USERPROFILE || process.env.HOME || ".", ".mailx");
|
|
88
|
+
for (const ext of ["mailx.db-shm", "mailx.db-wal"]) {
|
|
89
|
+
const p = path.join(mailxDir, ext);
|
|
90
|
+
try { fs.unlinkSync(p); log(`Cleaned ${ext}`); } catch { /* */ }
|
|
91
|
+
}
|
|
92
|
+
|
|
82
93
|
// Remove lock file
|
|
83
|
-
const lockPath = path.join(
|
|
94
|
+
const lockPath = path.join(mailxDir, "mailx-app.lock");
|
|
84
95
|
try { fs.unlinkSync(lockPath); log("Removed lock file"); } catch { /* */ }
|
|
85
96
|
|
|
86
97
|
if (killed === 0) console.log("No mailx processes found");
|
package/killmail.cmd
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
@echo off
|
|
2
|
-
REM Kill mailx server (port 9333) and
|
|
2
|
+
REM Kill mailx server (port 9333), launcher, and clean up stale DB locks
|
|
3
3
|
REM Safe: only kills mailx processes, never all node.exe
|
|
4
4
|
|
|
5
|
+
REM Try graceful exit first, then force kill
|
|
6
|
+
curl -s -m 2 http://localhost:9333/api/exit >nul 2>&1
|
|
7
|
+
timeout /t 1 /nobreak >nul 2>&1
|
|
5
8
|
taskkill /F /IM mailx-app.exe >nul 2>&1 && echo Killed mailx-app.exe || echo mailx-app.exe not running
|
|
6
9
|
killport 9333
|
|
10
|
+
|
|
11
|
+
REM Clean up stale SQLite WAL/SHM files (left by zombie processes)
|
|
12
|
+
if exist "%USERPROFILE%\.mailx\mailx.db-shm" del "%USERPROFILE%\.mailx\mailx.db-shm" && echo Cleaned mailx.db-shm
|
|
13
|
+
if exist "%USERPROFILE%\.mailx\mailx.db-wal" del "%USERPROFILE%\.mailx\mailx.db-wal" && echo Cleaned mailx.db-wal
|
package/package.json
CHANGED
|
@@ -12,13 +12,26 @@ import * as path from "node:path";
|
|
|
12
12
|
import { simpleParser } from "mailparser";
|
|
13
13
|
import { createTransport } from "nodemailer";
|
|
14
14
|
import * as os from "node:os";
|
|
15
|
-
/** Extract full error detail
|
|
15
|
+
/** Extract full error detail with provenance */
|
|
16
16
|
function imapError(err) {
|
|
17
|
-
const
|
|
17
|
+
const msg = err.message || err.reason || err.code || (typeof err === "string" ? err : "");
|
|
18
|
+
const parts = [];
|
|
19
|
+
if (msg)
|
|
20
|
+
parts.push(msg);
|
|
18
21
|
if (err.responseText)
|
|
19
22
|
parts.push(err.responseText);
|
|
20
23
|
if (err.responseStatus)
|
|
21
24
|
parts.push(`[${err.responseStatus}]`);
|
|
25
|
+
if (err.code && err.code !== msg)
|
|
26
|
+
parts.push(`[${err.code}]`);
|
|
27
|
+
if (parts.length === 0)
|
|
28
|
+
parts.push(`Unexpected error: ${JSON.stringify(err).slice(0, 200)}`);
|
|
29
|
+
// Add source location if available
|
|
30
|
+
if (err.stack) {
|
|
31
|
+
const frame = err.stack.split("\n").find((l) => l.includes("imap") || l.includes("transport") || l.includes("compat"));
|
|
32
|
+
if (frame)
|
|
33
|
+
parts.push(`(${frame.trim().replace(/^at\s+/, "")})`);
|
|
34
|
+
}
|
|
22
35
|
return parts.join(" — ");
|
|
23
36
|
}
|
|
24
37
|
/** Convert iflow address objects to our EmailAddress */
|
|
@@ -466,20 +479,39 @@ export class ImapManager extends EventEmitter {
|
|
|
466
479
|
const errMsg = imapError(e);
|
|
467
480
|
this.emit("syncError", accountId, errMsg);
|
|
468
481
|
console.error(`Sync error for ${accountId}: ${errMsg}`);
|
|
482
|
+
const config = this.configs.get(accountId);
|
|
483
|
+
const isOAuth = !!config?.tokenProvider;
|
|
469
484
|
// Connection limit — back off for 60 seconds
|
|
470
485
|
if (errMsg.includes("max_userip_connections") || errMsg.includes("Too many simultaneous")) {
|
|
471
486
|
this.connectionBackoff.set(accountId, Date.now() + 60000);
|
|
472
487
|
console.log(` [backoff] ${accountId}: connection limit hit, backing off 60s`);
|
|
473
488
|
}
|
|
474
|
-
//
|
|
475
|
-
|
|
489
|
+
// Classify error: transient (timeout, connection) vs auth (credentials, token)
|
|
490
|
+
const isTransient = /timeout|ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENETUNREACH|Too many/i.test(errMsg);
|
|
491
|
+
const isAuth = /auth|login|credential|token|AUTHENTICATIONFAILED/i.test(errMsg);
|
|
492
|
+
if (isTransient) {
|
|
493
|
+
// Transient: just log, will auto-retry on next sync cycle
|
|
494
|
+
console.log(` [transient] ${accountId}: ${errMsg} — will retry next cycle`);
|
|
495
|
+
}
|
|
496
|
+
else if (isAuth && isOAuth) {
|
|
497
|
+
// OAuth auth error: auto-attempt re-auth
|
|
498
|
+
console.log(` [auth] ${accountId}: attempting automatic re-authentication...`);
|
|
499
|
+
this.reauthenticate(accountId).then(ok => {
|
|
500
|
+
if (!ok && !this.accountErrorShown.has(accountId)) {
|
|
501
|
+
this.accountErrorShown.add(accountId);
|
|
502
|
+
this.emit("accountError", accountId, errMsg, "Authentication expired — re-authenticate", true);
|
|
503
|
+
}
|
|
504
|
+
}).catch(() => {
|
|
505
|
+
if (!this.accountErrorShown.has(accountId)) {
|
|
506
|
+
this.accountErrorShown.add(accountId);
|
|
507
|
+
this.emit("accountError", accountId, errMsg, "Authentication expired — re-authenticate", true);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
else if (!this.accountErrorShown.has(accountId)) {
|
|
512
|
+
// Non-transient, non-OAuth: show error banner
|
|
476
513
|
this.accountErrorShown.add(accountId);
|
|
477
|
-
|
|
478
|
-
const isOAuth = !!config?.tokenProvider;
|
|
479
|
-
const hint = errMsg.includes("max_userip_connections") || errMsg.includes("Too many")
|
|
480
|
-
? "Too many connections — backing off"
|
|
481
|
-
: isOAuth ? "Authentication may have expired" : "Check server connectivity";
|
|
482
|
-
this.emit("accountError", accountId, errMsg, hint, isOAuth);
|
|
514
|
+
this.emit("accountError", accountId, errMsg, isOAuth ? "Authentication may have expired" : "Check server connectivity", isOAuth);
|
|
483
515
|
}
|
|
484
516
|
}
|
|
485
517
|
finally {
|
|
@@ -57,7 +57,10 @@ if (settings.accounts.length === 0) {
|
|
|
57
57
|
const dbDir = getConfigDir();
|
|
58
58
|
const db = new MailxDB(dbDir);
|
|
59
59
|
const imapManager = new ImapManager(db);
|
|
60
|
-
if (process.argv.includes("--
|
|
60
|
+
if (process.argv.includes("--legacy-imap") || process.argv.includes("-legacy-imap")) {
|
|
61
|
+
console.log(" Using legacy IMAP client (imapflow)");
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
61
64
|
imapManager.useNativeClient = true;
|
|
62
65
|
console.log(" Using native IMAP client (transport-agnostic)");
|
|
63
66
|
}
|
|
@@ -88,6 +91,9 @@ app.get("/api/version", (req, res) => {
|
|
|
88
91
|
const storage = getStorageInfo();
|
|
89
92
|
res.json({ version: SERVER_VERSION, theme: settings.ui?.theme || "system", storage });
|
|
90
93
|
});
|
|
94
|
+
app.all("/info", (req, res) => {
|
|
95
|
+
res.json({ version: SERVER_VERSION, uptime: Math.round(process.uptime()), port: PORT });
|
|
96
|
+
});
|
|
91
97
|
app.get("/status", (req, res) => {
|
|
92
98
|
const accounts = db.getAccounts();
|
|
93
99
|
const pendingSync = db.getTotalPendingSyncCount();
|