@bobfrankston/mailx 1.0.209 → 1.0.211
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 +8 -17
- package/client/.msger-window.json +1 -1
- package/client/package.json +3 -0
- package/package.json +3 -3
- package/bin/bin/mailx.js +0 -842
- package/bin/bin/postinstall.js +0 -39
- package/bin/packages/mailx-settings/cloud.js +0 -243
- package/bin/packages/mailx-settings/index.js +0 -643
package/bin/bin/mailx.js
DELETED
|
@@ -1,842 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* mailx -- email client
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* mailx Start service + open in msger (IPC, no TCP)
|
|
7
|
-
* mailx --server Start Express HTTP server (dev/remote)
|
|
8
|
-
* mailx --no-browser Start server only (headless)
|
|
9
|
-
* mailx --verbose Show console output (default: log file only)
|
|
10
|
-
* mailx --email <addr> First-time setup with email (skips prompt)
|
|
11
|
-
* mailx --import <file> Import accounts.jsonc into GDrive and merge
|
|
12
|
-
* mailx -v / --version Show version and exit
|
|
13
|
-
* mailx -kill Kill running mailx processes
|
|
14
|
-
* mailx -setup Interactive first-time setup (CLI)
|
|
15
|
-
* mailx -test Test IMAP/SMTP connectivity
|
|
16
|
-
* mailx -rebuild Wipe local cache, re-sync from IMAP
|
|
17
|
-
* mailx -repair Re-sync metadata (fix corrupt subjects) keeping .eml files
|
|
18
|
-
*/
|
|
19
|
-
import fs from "node:fs";
|
|
20
|
-
import path from "node:path";
|
|
21
|
-
import os from "node:os";
|
|
22
|
-
import net from "node:net";
|
|
23
|
-
import { ports } from "@bobfrankston/miscinfo";
|
|
24
|
-
import { showMessageBox, showService } from "@bobfrankston/msger";
|
|
25
|
-
const PORT = ports.mailx;
|
|
26
|
-
const args = process.argv.slice(2);
|
|
27
|
-
// Normalize: accept both -flag and --flag
|
|
28
|
-
function hasFlag(name) { return args.includes(`-${name}`) || args.includes(`--${name}`); }
|
|
29
|
-
const verbose = hasFlag("verbose");
|
|
30
|
-
const isDaemon = hasFlag("daemon"); // internal: re-spawned detached process
|
|
31
|
-
// Auto-detach: re-spawn as background process so terminal returns immediately
|
|
32
|
-
// Skip for: --verbose (want console), --daemon (already detached),
|
|
33
|
-
// and any command flags (setup, kill, test, etc.)
|
|
34
|
-
if (!verbose && !isDaemon && !process.argv.slice(2).some(a => /^-/.test(a) && !["--no-browser"].includes(a))) {
|
|
35
|
-
const { spawn } = await import("node:child_process");
|
|
36
|
-
const child = spawn(process.execPath, [...process.argv.slice(1), "--daemon"], {
|
|
37
|
-
detached: true,
|
|
38
|
-
stdio: "ignore",
|
|
39
|
-
windowsHide: true,
|
|
40
|
-
});
|
|
41
|
-
child.unref();
|
|
42
|
-
process.exit(0);
|
|
43
|
-
}
|
|
44
|
-
const setupMode = hasFlag("setup");
|
|
45
|
-
const addMode = hasFlag("add");
|
|
46
|
-
const testMode = hasFlag("test");
|
|
47
|
-
const rebuildMode = hasFlag("rebuild");
|
|
48
|
-
const repairMode = hasFlag("repair");
|
|
49
|
-
const importMode = hasFlag("import");
|
|
50
|
-
// Validate arguments
|
|
51
|
-
const knownFlags = ["verbose", "kill", "v", "version", "setup", "add", "test", "rebuild", "repair", "log", "import", "email", "mail", "daemon"];
|
|
52
|
-
for (const arg of args) {
|
|
53
|
-
const flag = arg.replace(/^--?/, "");
|
|
54
|
-
if (arg.startsWith("-") && !knownFlags.includes(flag)) {
|
|
55
|
-
console.error(`Unknown option: ${arg}`);
|
|
56
|
-
console.error("Usage: mailx [-verbose] [-kill] [-rebuild] [-v] [-setup]");
|
|
57
|
-
process.exit(1);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
function log(...msg) { if (verbose)
|
|
61
|
-
console.log("[mailx]", ...msg); }
|
|
62
|
-
// Kill any running mailx server
|
|
63
|
-
if (hasFlag("kill")) {
|
|
64
|
-
log("Killing mailx processes...");
|
|
65
|
-
const { execSync } = await import("node:child_process");
|
|
66
|
-
let killed = 0;
|
|
67
|
-
// Try graceful exit first
|
|
68
|
-
try {
|
|
69
|
-
execSync(`curl -s -m 2 http://localhost:${PORT}/api/exit`, { stdio: "pipe" });
|
|
70
|
-
log("Sent graceful exit");
|
|
71
|
-
execSync("timeout /t 1 /nobreak", { stdio: "pipe" });
|
|
72
|
-
}
|
|
73
|
-
catch { /* server may not be responding */ }
|
|
74
|
-
if (process.platform === "win32") {
|
|
75
|
-
// Kill by port
|
|
76
|
-
try {
|
|
77
|
-
const out = execSync(`netstat -ano | findstr :${PORT} | findstr LISTENING`, { encoding: "utf-8" }).trim();
|
|
78
|
-
const pids = [...new Set(out.split("\n").map(l => l.trim().split(/\s+/).pop()).filter(Boolean))];
|
|
79
|
-
for (const pid of pids) {
|
|
80
|
-
try {
|
|
81
|
-
execSync(`taskkill /F /PID ${pid}`, { stdio: "pipe" });
|
|
82
|
-
console.log(`Killed PID ${pid} (port ${PORT})`);
|
|
83
|
-
killed++;
|
|
84
|
-
}
|
|
85
|
-
catch { /* */ }
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
catch { /* no process on port */ }
|
|
89
|
-
// Kill any node.exe running mailx (server or IPC service)
|
|
90
|
-
try {
|
|
91
|
-
const ps = execSync(`powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"name='node.exe'\\" | Where-Object { $_.CommandLine -match 'mailx-server|mailx\\\\packages|mailx\\\\bin' } | Select-Object -ExpandProperty ProcessId"`, { encoding: "utf-8" }).trim();
|
|
92
|
-
for (const pid of ps.split("\n").map(s => s.trim()).filter(s => /^\d+$/.test(s))) {
|
|
93
|
-
try {
|
|
94
|
-
execSync(`taskkill /F /PID ${pid}`, { stdio: "pipe" });
|
|
95
|
-
console.log(`Killed PID ${pid} (mailx node process)`);
|
|
96
|
-
killed++;
|
|
97
|
-
}
|
|
98
|
-
catch { /* */ }
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
catch { /* */ }
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
try {
|
|
105
|
-
execSync(`fuser -k ${PORT}/tcp`, { stdio: "pipe" });
|
|
106
|
-
console.log(`Killed process on port ${PORT}`);
|
|
107
|
-
killed++;
|
|
108
|
-
}
|
|
109
|
-
catch { /* */ }
|
|
110
|
-
}
|
|
111
|
-
// Clean up stale SQLite WAL/SHM files
|
|
112
|
-
const mailxDir = path.join(process.env.USERPROFILE || process.env.HOME || ".", ".mailx");
|
|
113
|
-
for (const ext of ["mailx.db-shm", "mailx.db-wal"]) {
|
|
114
|
-
const p = path.join(mailxDir, ext);
|
|
115
|
-
try {
|
|
116
|
-
fs.unlinkSync(p);
|
|
117
|
-
log(`Cleaned ${ext}`);
|
|
118
|
-
}
|
|
119
|
-
catch { /* */ }
|
|
120
|
-
}
|
|
121
|
-
if (killed === 0)
|
|
122
|
-
console.log("No mailx processes found");
|
|
123
|
-
process.exit(0);
|
|
124
|
-
}
|
|
125
|
-
// Rebuild: wipe DB + message store, keep accounts/settings
|
|
126
|
-
if (rebuildMode) {
|
|
127
|
-
const { getConfigDir, getStorePath } = await import("@bobfrankston/mailx-settings");
|
|
128
|
-
const dbDir = getConfigDir();
|
|
129
|
-
const storePath = getStorePath();
|
|
130
|
-
console.log("Rebuilding mailx local cache...");
|
|
131
|
-
console.log(" Accounts and settings will be preserved.");
|
|
132
|
-
// Remove DB files
|
|
133
|
-
for (const f of ["mailx.db", "mailx.db-wal", "mailx.db-shm"]) {
|
|
134
|
-
const p = path.join(dbDir, f);
|
|
135
|
-
if (fs.existsSync(p)) {
|
|
136
|
-
fs.unlinkSync(p);
|
|
137
|
-
console.log(` Deleted ${f}`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
// Remove message store
|
|
141
|
-
if (fs.existsSync(storePath)) {
|
|
142
|
-
fs.rmSync(storePath, { recursive: true });
|
|
143
|
-
console.log(` Deleted message store`);
|
|
144
|
-
}
|
|
145
|
-
console.log(" Rebuild complete. Run 'mailx' to start fresh.");
|
|
146
|
-
process.exit(0);
|
|
147
|
-
}
|
|
148
|
-
// Repair: re-sync metadata (subjects, flags, envelopes) without deleting stored .eml files
|
|
149
|
-
if (repairMode) {
|
|
150
|
-
const { getConfigDir } = await import("@bobfrankston/mailx-settings");
|
|
151
|
-
const dbDir = getConfigDir();
|
|
152
|
-
const dbPath = path.join(dbDir, "mailx.db");
|
|
153
|
-
if (!fs.existsSync(dbPath)) {
|
|
154
|
-
console.error("No database found. Run 'mailx' first to create one.");
|
|
155
|
-
process.exit(1);
|
|
156
|
-
}
|
|
157
|
-
console.log("Repairing mailx metadata...");
|
|
158
|
-
console.log(" Message bodies (.eml files) will be preserved.");
|
|
159
|
-
console.log(" Clearing message metadata for re-sync...");
|
|
160
|
-
// Dynamic require — better-sqlite3 is a native module, not typed in bin/
|
|
161
|
-
const mod = "better-sqlite3";
|
|
162
|
-
const Database = (await import(/* webpackIgnore: true */ mod)).default;
|
|
163
|
-
const db = Database(dbPath);
|
|
164
|
-
db.pragma("journal_mode = WAL");
|
|
165
|
-
const count = db.prepare("SELECT COUNT(*) as cnt FROM messages").get().cnt;
|
|
166
|
-
db.exec("DELETE FROM messages");
|
|
167
|
-
db.exec("DELETE FROM messages_fts");
|
|
168
|
-
// Reset folder sync state so IMAP re-syncs all envelopes
|
|
169
|
-
db.exec("UPDATE folders SET total = 0, unread = 0");
|
|
170
|
-
db.close();
|
|
171
|
-
console.log(` Cleared ${count} message entries. Folder sync state reset.`);
|
|
172
|
-
console.log(" Run 'mailx' to re-sync from IMAP with correct encoding.");
|
|
173
|
-
process.exit(0);
|
|
174
|
-
}
|
|
175
|
-
// Import accounts from a local file into GDrive
|
|
176
|
-
if (importMode) {
|
|
177
|
-
const importPath = args.find(a => !a.startsWith("-"));
|
|
178
|
-
if (!importPath) {
|
|
179
|
-
console.error("Usage: mailx --import <path-to-accounts.jsonc>");
|
|
180
|
-
console.error(" Reads accounts from a local file and saves to Google Drive.");
|
|
181
|
-
console.error(" Example: mailx --import ~/OneDrive/home/.mailx/accounts.jsonc");
|
|
182
|
-
process.exit(1);
|
|
183
|
-
}
|
|
184
|
-
const { parse: parseJsonc } = await import("jsonc-parser");
|
|
185
|
-
const absPath = path.resolve(importPath);
|
|
186
|
-
if (!fs.existsSync(absPath)) {
|
|
187
|
-
console.error(`File not found: ${absPath}`);
|
|
188
|
-
process.exit(1);
|
|
189
|
-
}
|
|
190
|
-
const content = fs.readFileSync(absPath, "utf-8").replace(/\r/g, "");
|
|
191
|
-
const data = parseJsonc(content);
|
|
192
|
-
const accounts = data?.accounts || (Array.isArray(data) ? data : null);
|
|
193
|
-
if (!accounts || accounts.length === 0) {
|
|
194
|
-
console.error("No accounts found in file. Expected { accounts: [...] } or [...]");
|
|
195
|
-
process.exit(1);
|
|
196
|
-
}
|
|
197
|
-
console.log(`Found ${accounts.length} account(s) in ${absPath}`);
|
|
198
|
-
// Initialize cloud config (GDrive) and save
|
|
199
|
-
const { initCloudConfig, loadAccounts, saveAccounts } = await import("@bobfrankston/mailx-settings");
|
|
200
|
-
await initCloudConfig("gdrive");
|
|
201
|
-
// Merge: existing cloud accounts + imported, deduplicate by email
|
|
202
|
-
const existing = loadAccounts();
|
|
203
|
-
const merged = [...existing];
|
|
204
|
-
for (const acct of accounts) {
|
|
205
|
-
if (!merged.some(e => e.email === acct.email)) {
|
|
206
|
-
merged.push(acct);
|
|
207
|
-
console.log(` + ${acct.label || acct.email}`);
|
|
208
|
-
}
|
|
209
|
-
else {
|
|
210
|
-
console.log(` = ${acct.label || acct.email} (already exists)`);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
// Wrap with name if the source had one
|
|
214
|
-
const wrapper = { accounts: merged };
|
|
215
|
-
if (data?.name)
|
|
216
|
-
wrapper.name = data.name;
|
|
217
|
-
await saveAccounts(merged);
|
|
218
|
-
console.log(`Saved ${merged.length} account(s). Run 'mailx' to start.`);
|
|
219
|
-
process.exit(0);
|
|
220
|
-
}
|
|
221
|
-
// Version
|
|
222
|
-
if (hasFlag("v") || hasFlag("version")) {
|
|
223
|
-
const root = path.join(import.meta.dirname, "..");
|
|
224
|
-
const ver = (pkg) => {
|
|
225
|
-
for (const dir of [`${root}/node_modules/${pkg}`, `${root}/node_modules/@bobfrankston/${pkg.replace("@bobfrankston/", "")}`]) {
|
|
226
|
-
try {
|
|
227
|
-
return JSON.parse(fs.readFileSync(`${dir}/package.json`, "utf-8")).version;
|
|
228
|
-
}
|
|
229
|
-
catch { /* */ }
|
|
230
|
-
}
|
|
231
|
-
// Check workspace packages
|
|
232
|
-
const short = pkg.replace("@bobfrankston/", "");
|
|
233
|
-
try {
|
|
234
|
-
return JSON.parse(fs.readFileSync(`${root}/packages/${short}/package.json`, "utf-8")).version;
|
|
235
|
-
}
|
|
236
|
-
catch { /* */ }
|
|
237
|
-
return "not found";
|
|
238
|
-
};
|
|
239
|
-
try {
|
|
240
|
-
const pkg = JSON.parse(fs.readFileSync(`${root}/package.json`, "utf-8"));
|
|
241
|
-
console.log(`\x1b[1;97;44m mailx v${pkg.version} \x1b[0m`);
|
|
242
|
-
}
|
|
243
|
-
catch {
|
|
244
|
-
console.log("mailx (version unknown)");
|
|
245
|
-
}
|
|
246
|
-
console.log(` node ${process.version}`);
|
|
247
|
-
console.log(` iflow-direct ${ver("@bobfrankston/iflow-direct")}`);
|
|
248
|
-
console.log(` miscinfo ${ver("@bobfrankston/miscinfo")}`);
|
|
249
|
-
console.log(` oauth ${ver("@bobfrankston/oauthsupport")}`);
|
|
250
|
-
console.log(` store ${ver("@bobfrankston/mailx-store")}`);
|
|
251
|
-
console.log(` server ${ver("@bobfrankston/mailx-server")}`);
|
|
252
|
-
console.log(` api ${ver("@bobfrankston/mailx-api")}`);
|
|
253
|
-
console.log(` platform ${process.platform} ${process.arch}`);
|
|
254
|
-
process.exit(0);
|
|
255
|
-
}
|
|
256
|
-
function isPortInUse(port) {
|
|
257
|
-
return new Promise((resolve) => {
|
|
258
|
-
const socket = net.createConnection({ port, host: "127.0.0.1" });
|
|
259
|
-
socket.once("connect", () => { socket.destroy(); resolve(true); });
|
|
260
|
-
socket.once("error", () => resolve(false));
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
/** Launch msger pointing at the server URL */
|
|
264
|
-
function launchMsger(url) {
|
|
265
|
-
showMessageBox({ url, detach: true, size: { width: 1400, height: 900 } });
|
|
266
|
-
}
|
|
267
|
-
async function prompt(question) {
|
|
268
|
-
const readline = await import("readline");
|
|
269
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
270
|
-
return new Promise(resolve => rl.question(question, (ans) => { rl.close(); resolve(ans.trim()); }));
|
|
271
|
-
}
|
|
272
|
-
/** Check if mailx is configured (has local config or accounts) */
|
|
273
|
-
function hasConfig() {
|
|
274
|
-
const home = process.env.USERPROFILE || process.env.HOME || "";
|
|
275
|
-
const mailxDir = path.join(home, ".mailx");
|
|
276
|
-
for (const f of ["config.jsonc", "accounts.jsonc", "settings.jsonc"]) {
|
|
277
|
-
if (fs.existsSync(path.join(mailxDir, f)))
|
|
278
|
-
return true;
|
|
279
|
-
}
|
|
280
|
-
return false;
|
|
281
|
-
}
|
|
282
|
-
/** Try to auto-discover mail server settings from email domain */
|
|
283
|
-
async function autoDiscover(domain) {
|
|
284
|
-
// 1. Try Thunderbird ISPDB
|
|
285
|
-
try {
|
|
286
|
-
const res = await fetch(`https://autoconfig.thunderbird.net/v1.1/${domain}`, { signal: AbortSignal.timeout(5000) });
|
|
287
|
-
if (res.ok) {
|
|
288
|
-
const xml = await res.text();
|
|
289
|
-
const imap = xml.match(/<incomingServer type="imap">[\s\S]*?<hostname>(.*?)<\/hostname>[\s\S]*?<port>(\d+)<\/port>[\s\S]*?<authentication>(.*?)<\/authentication>/);
|
|
290
|
-
const smtp = xml.match(/<outgoingServer type="smtp">[\s\S]*?<hostname>(.*?)<\/hostname>[\s\S]*?<port>(\d+)<\/port>[\s\S]*?<authentication>(.*?)<\/authentication>/);
|
|
291
|
-
if (imap && smtp) {
|
|
292
|
-
return {
|
|
293
|
-
imap: { host: imap[1], port: parseInt(imap[2]), auth: imap[3].includes("OAuth2") ? "oauth2" : "password" },
|
|
294
|
-
smtp: { host: smtp[1], port: parseInt(smtp[2]), auth: smtp[3].includes("OAuth2") ? "oauth2" : "password" },
|
|
295
|
-
source: "ISPDB",
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
catch { /* timeout or not found */ }
|
|
301
|
-
// 2. Try DNS SRV records
|
|
302
|
-
try {
|
|
303
|
-
const dns = await import("node:dns/promises");
|
|
304
|
-
const imapSrv = await dns.resolveSrv(`_imaps._tcp.${domain}`).catch(() => null);
|
|
305
|
-
const smtpSrv = await dns.resolveSrv(`_submission._tcp.${domain}`).catch(() => null);
|
|
306
|
-
if (imapSrv?.[0] && smtpSrv?.[0]) {
|
|
307
|
-
return {
|
|
308
|
-
imap: { host: imapSrv[0].name, port: imapSrv[0].port, auth: "password" },
|
|
309
|
-
smtp: { host: smtpSrv[0].name, port: smtpSrv[0].port, auth: "password" },
|
|
310
|
-
source: "DNS SRV",
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
catch { /* DNS failed */ }
|
|
315
|
-
// 3. Try MX-based detection (Google Workspace, Microsoft 365)
|
|
316
|
-
try {
|
|
317
|
-
const dns = await import("node:dns/promises");
|
|
318
|
-
const records = await dns.resolveMx(domain);
|
|
319
|
-
for (const mx of records) {
|
|
320
|
-
const host = mx.exchange.toLowerCase();
|
|
321
|
-
if (host.endsWith(".google.com") || host.endsWith(".googlemail.com")) {
|
|
322
|
-
return {
|
|
323
|
-
imap: { host: "imap.gmail.com", port: 993, auth: "oauth2" },
|
|
324
|
-
smtp: { host: "smtp.gmail.com", port: 587, auth: "oauth2" },
|
|
325
|
-
source: `MX → Google (${host})`,
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
if (host.endsWith(".outlook.com") || host.endsWith(".protection.outlook.com")) {
|
|
329
|
-
return {
|
|
330
|
-
imap: { host: "outlook.office365.com", port: 993, auth: "oauth2" },
|
|
331
|
-
smtp: { host: "smtp.office365.com", port: 587, auth: "oauth2" },
|
|
332
|
-
source: `MX → Microsoft (${host})`,
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
catch { /* DNS failed */ }
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
340
|
-
/** Prompt user for account details, auto-discover where possible */
|
|
341
|
-
async function promptForAccount(intro) {
|
|
342
|
-
if (intro)
|
|
343
|
-
console.log(intro);
|
|
344
|
-
const email = await prompt("Email address (or 'skip'): ");
|
|
345
|
-
if (!email || email.toLowerCase() === "skip")
|
|
346
|
-
return null;
|
|
347
|
-
const domain = email.split("@")[1]?.toLowerCase() || "";
|
|
348
|
-
const knownOAuth = ["gmail.com", "googlemail.com", "outlook.com", "hotmail.com"];
|
|
349
|
-
if (knownOAuth.includes(domain)) {
|
|
350
|
-
console.log(` ${domain}: OAuth2 — no password needed. Browser will prompt for authorization.`);
|
|
351
|
-
return { email };
|
|
352
|
-
}
|
|
353
|
-
// Try auto-discovery
|
|
354
|
-
console.log(` Looking up mail servers for ${domain}...`);
|
|
355
|
-
const discovered = await autoDiscover(domain);
|
|
356
|
-
if (discovered) {
|
|
357
|
-
console.log(` Found via ${discovered.source}: IMAP ${discovered.imap.host}:${discovered.imap.port}, SMTP ${discovered.smtp.host}:${discovered.smtp.port}`);
|
|
358
|
-
if (discovered.imap.auth === "oauth2") {
|
|
359
|
-
console.log(" OAuth2 authentication — browser will prompt for authorization.");
|
|
360
|
-
return { email };
|
|
361
|
-
}
|
|
362
|
-
const password = await prompt("Password: ");
|
|
363
|
-
return {
|
|
364
|
-
email, password,
|
|
365
|
-
imap: { host: discovered.imap.host, port: discovered.imap.port },
|
|
366
|
-
smtp: { host: discovered.smtp.host, port: discovered.smtp.port },
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
// Manual fallback
|
|
370
|
-
console.log(" Could not auto-detect servers. Enter manually:");
|
|
371
|
-
const password = await prompt("Password: ");
|
|
372
|
-
const imapHost = await prompt(`IMAP host [imap.${domain}]: `) || `imap.${domain}`;
|
|
373
|
-
const smtpHost = await prompt(`SMTP host [smtp.${domain}]: `) || `smtp.${domain}`;
|
|
374
|
-
return {
|
|
375
|
-
email, password,
|
|
376
|
-
imap: { host: imapHost },
|
|
377
|
-
smtp: { host: smtpHost },
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
/** Interactive first-time setup — GDrive API for cloud storage */
|
|
381
|
-
async function runSetup(providedEmail) {
|
|
382
|
-
console.log("\nmailx — first-time setup\n");
|
|
383
|
-
const home = process.env.USERPROFILE || process.env.HOME || "";
|
|
384
|
-
const mailxDir = path.join(home, ".mailx");
|
|
385
|
-
// Use --email flag or prompt interactively
|
|
386
|
-
const email = providedEmail || await prompt("Email address (Gmail recommended): ");
|
|
387
|
-
if (!email || !email.includes("@")) {
|
|
388
|
-
console.log(`\nNo account added. The UI will show a setup form.`);
|
|
389
|
-
fs.mkdirSync(mailxDir, { recursive: true });
|
|
390
|
-
return false;
|
|
391
|
-
}
|
|
392
|
-
if (providedEmail)
|
|
393
|
-
console.log(`Using email: ${email}`);
|
|
394
|
-
const domain = email.split("@")[1]?.toLowerCase() || "";
|
|
395
|
-
let isGoogle = ["gmail.com", "googlemail.com"].includes(domain);
|
|
396
|
-
if (!isGoogle) {
|
|
397
|
-
try {
|
|
398
|
-
const dnsmod = await import("node:dns/promises");
|
|
399
|
-
const records = await dnsmod.resolveMx(domain);
|
|
400
|
-
isGoogle = records.some(mx => {
|
|
401
|
-
const host = mx.exchange.toLowerCase();
|
|
402
|
-
return host.endsWith(".google.com") || host.endsWith(".googlemail.com");
|
|
403
|
-
});
|
|
404
|
-
if (isGoogle)
|
|
405
|
-
console.log(` ${domain} is hosted on Google (detected via MX)`);
|
|
406
|
-
}
|
|
407
|
-
catch { /* DNS lookup failed */ }
|
|
408
|
-
}
|
|
409
|
-
// For Google-hosted accounts, check Drive for existing settings first
|
|
410
|
-
if (isGoogle) {
|
|
411
|
-
fs.mkdirSync(mailxDir, { recursive: true });
|
|
412
|
-
console.log("\nChecking Google Drive for existing mailx settings...");
|
|
413
|
-
try {
|
|
414
|
-
const { gDriveFindOrCreateFolder, getCloudProvider } = await import("@bobfrankston/mailx-settings/cloud.js");
|
|
415
|
-
const folderId = await gDriveFindOrCreateFolder();
|
|
416
|
-
if (folderId) {
|
|
417
|
-
console.log(` Drive folder: My Drive/mailx/ (${folderId})`);
|
|
418
|
-
const gdrive = getCloudProvider("gdrive", folderId);
|
|
419
|
-
if (gdrive) {
|
|
420
|
-
// Read accounts.jsonc (canonical) — ignore legacy settings.jsonc
|
|
421
|
-
const existing = await gdrive.read("accounts.jsonc");
|
|
422
|
-
if (existing) {
|
|
423
|
-
const { parse: parseJsonc } = await import("jsonc-parser");
|
|
424
|
-
const data = parseJsonc(existing);
|
|
425
|
-
const accts = data?.accounts || (Array.isArray(data) ? data : []);
|
|
426
|
-
if (accts.length > 0) {
|
|
427
|
-
console.log(`\nFound ${accts.length} existing account(s) on Google Drive (My Drive/mailx/accounts.jsonc):`);
|
|
428
|
-
for (const a of accts)
|
|
429
|
-
console.log(` • ${a.label || a.name || a.email}`);
|
|
430
|
-
// Save config pointing to Drive — no prompts needed
|
|
431
|
-
const config = { sharedDir: { provider: "gdrive", path: "mailx", folderId } };
|
|
432
|
-
fs.writeFileSync(path.join(mailxDir, "config.jsonc"), JSON.stringify(config, null, 2));
|
|
433
|
-
console.log("Local config created. Starting mailx...\n");
|
|
434
|
-
return true;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
// No existing accounts — save Drive config for later
|
|
439
|
-
const config = { sharedDir: { provider: "gdrive", path: "mailx", folderId } };
|
|
440
|
-
fs.writeFileSync(path.join(mailxDir, "config.jsonc"), JSON.stringify(config, null, 2));
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
catch (e) {
|
|
444
|
-
console.log(` Drive check failed: ${e.message} — continuing with manual setup`);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
// No existing accounts found — build a new account
|
|
448
|
-
const account = { email };
|
|
449
|
-
const isOAuth = ["gmail.com", "googlemail.com", "outlook.com", "hotmail.com", "live.com"].includes(domain);
|
|
450
|
-
if (!isOAuth) {
|
|
451
|
-
account.password = await prompt("Password (app password for Yahoo/AOL/iCloud): ");
|
|
452
|
-
}
|
|
453
|
-
const name = await prompt(`Your name (for From: header) [${email.split("@")[0]}]: `) || email.split("@")[0];
|
|
454
|
-
fs.mkdirSync(mailxDir, { recursive: true });
|
|
455
|
-
if (isGoogle) {
|
|
456
|
-
// Save to Google Drive via API
|
|
457
|
-
console.log("\nSaving account to Google Drive...");
|
|
458
|
-
try {
|
|
459
|
-
const { gDriveFindOrCreateFolder, getCloudProvider } = await import("@bobfrankston/mailx-settings/cloud.js");
|
|
460
|
-
const folderId = await gDriveFindOrCreateFolder();
|
|
461
|
-
if (folderId) {
|
|
462
|
-
const gdrive = getCloudProvider("gdrive", folderId);
|
|
463
|
-
if (gdrive) {
|
|
464
|
-
const accountsData = { name, accounts: [account] };
|
|
465
|
-
const ok = await gdrive.write("accounts.jsonc", JSON.stringify(accountsData, null, 2));
|
|
466
|
-
if (ok) {
|
|
467
|
-
console.log("Account saved to Google Drive.");
|
|
468
|
-
// config.jsonc may already exist from the Drive check above
|
|
469
|
-
if (!fs.existsSync(path.join(mailxDir, "config.jsonc"))) {
|
|
470
|
-
const config = { sharedDir: { provider: "gdrive", path: "mailx", folderId } };
|
|
471
|
-
fs.writeFileSync(path.join(mailxDir, "config.jsonc"), JSON.stringify(config, null, 2));
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
else {
|
|
475
|
-
console.log("Drive write failed — saving locally.");
|
|
476
|
-
fs.writeFileSync(path.join(mailxDir, "accounts.jsonc"), JSON.stringify({ name, accounts: [account] }, null, 2));
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
catch (e) {
|
|
482
|
-
console.log(`Drive error: ${e.message} — saving locally.`);
|
|
483
|
-
fs.writeFileSync(path.join(mailxDir, "accounts.jsonc"), JSON.stringify({ name, accounts: [account] }, null, 2));
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
else {
|
|
487
|
-
// Non-Google — save locally
|
|
488
|
-
fs.writeFileSync(path.join(mailxDir, "accounts.jsonc"), JSON.stringify({ name, accounts: [account] }, null, 2));
|
|
489
|
-
}
|
|
490
|
-
console.log("Setup complete. Starting mailx...\n");
|
|
491
|
-
return true;
|
|
492
|
-
}
|
|
493
|
-
/** Test account connectivity — IMAP connect, SMTP send, sync round-trip */
|
|
494
|
-
async function runTest() {
|
|
495
|
-
console.log("\nmailx — connection test\n");
|
|
496
|
-
// Start server in-process to access settings
|
|
497
|
-
console.log("Loading settings...");
|
|
498
|
-
const { loadSettings, getSharedDir } = await import("../packages/mailx-settings/index.js");
|
|
499
|
-
const { initLocalConfig } = await import("../packages/mailx-settings/index.js");
|
|
500
|
-
initLocalConfig();
|
|
501
|
-
const settings = loadSettings();
|
|
502
|
-
if (settings.accounts.length === 0) {
|
|
503
|
-
console.log("No accounts configured. Run: mailx -setup");
|
|
504
|
-
process.exit(1);
|
|
505
|
-
}
|
|
506
|
-
console.log(`Shared dir: ${getSharedDir()}`);
|
|
507
|
-
console.log(`Accounts: ${settings.accounts.map((a) => `${a.label || a.name} <${a.email}>`).join(", ")}\n`);
|
|
508
|
-
for (const account of settings.accounts) {
|
|
509
|
-
if (!account.enabled) {
|
|
510
|
-
console.log(` ${account.label || account.id}: SKIPPED (disabled)\n`);
|
|
511
|
-
continue;
|
|
512
|
-
}
|
|
513
|
-
console.log(`Testing ${account.label || account.id} (${account.email}):`);
|
|
514
|
-
// Test IMAP
|
|
515
|
-
try {
|
|
516
|
-
const { createAutoImapConfig, CompatImapClient } = await import("@bobfrankston/iflow-direct");
|
|
517
|
-
const { NodeTransport } = await import("@bobfrankston/iflow-node");
|
|
518
|
-
const config = createAutoImapConfig({
|
|
519
|
-
server: account.imap.host,
|
|
520
|
-
port: account.imap.port,
|
|
521
|
-
username: account.imap.user,
|
|
522
|
-
password: account.imap.password
|
|
523
|
-
});
|
|
524
|
-
const client = new CompatImapClient(config, () => new NodeTransport());
|
|
525
|
-
const folders = await client.getFolderList();
|
|
526
|
-
await client.logout();
|
|
527
|
-
console.log(` IMAP: OK (${folders.length} folders)`);
|
|
528
|
-
}
|
|
529
|
-
catch (e) {
|
|
530
|
-
console.log(` IMAP: FAILED — ${e.message}`);
|
|
531
|
-
}
|
|
532
|
-
// Test SMTP
|
|
533
|
-
try {
|
|
534
|
-
const { createTransport } = await import("nodemailer");
|
|
535
|
-
let smtpAuth;
|
|
536
|
-
if (account.smtp.auth === "password") {
|
|
537
|
-
smtpAuth = { user: account.smtp.user, pass: account.smtp.password };
|
|
538
|
-
}
|
|
539
|
-
else if (account.smtp.auth === "oauth2") {
|
|
540
|
-
// Try to get OAuth token
|
|
541
|
-
const { createAutoImapConfig } = await import("@bobfrankston/iflow-direct");
|
|
542
|
-
const config = createAutoImapConfig({
|
|
543
|
-
server: account.imap.host,
|
|
544
|
-
port: account.imap.port,
|
|
545
|
-
username: account.imap.user,
|
|
546
|
-
});
|
|
547
|
-
if (config.tokenProvider) {
|
|
548
|
-
const accessToken = await config.tokenProvider();
|
|
549
|
-
smtpAuth = { type: "OAuth2", user: account.smtp.user, accessToken };
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
const transport = createTransport({
|
|
553
|
-
host: account.smtp.host,
|
|
554
|
-
port: account.smtp.port,
|
|
555
|
-
secure: account.smtp.port === 465,
|
|
556
|
-
auth: smtpAuth,
|
|
557
|
-
tls: { rejectUnauthorized: false },
|
|
558
|
-
});
|
|
559
|
-
await transport.verify();
|
|
560
|
-
console.log(` SMTP: OK`);
|
|
561
|
-
// Send test message to self
|
|
562
|
-
const testSubject = `mailx test — ${new Date().toLocaleString()}`;
|
|
563
|
-
await transport.sendMail({
|
|
564
|
-
from: `${account.name} <${account.email}>`,
|
|
565
|
-
to: account.email,
|
|
566
|
-
subject: testSubject,
|
|
567
|
-
text: `This is a test message from mailx -test.\nSent: ${new Date().toISOString()}\nAccount: ${account.id}`,
|
|
568
|
-
});
|
|
569
|
-
console.log(` SEND: OK — test message sent to ${account.email}`);
|
|
570
|
-
console.log(` Subject: "${testSubject}"`);
|
|
571
|
-
}
|
|
572
|
-
catch (e) {
|
|
573
|
-
console.log(` SMTP: FAILED — ${e.message}`);
|
|
574
|
-
}
|
|
575
|
-
console.log();
|
|
576
|
-
}
|
|
577
|
-
console.log("Test complete. Check your inbox for the test message(s).");
|
|
578
|
-
process.exit(0);
|
|
579
|
-
}
|
|
580
|
-
/** Register this client on GDrive — writes/updates clients.jsonc with device info */
|
|
581
|
-
async function registerClient(settings) {
|
|
582
|
-
const { cloudRead, cloudWrite } = await import("@bobfrankston/mailx-settings");
|
|
583
|
-
// Device ID: stable per machine, stored locally
|
|
584
|
-
const deviceIdPath = path.join(os.homedir(), ".mailx", "device-id");
|
|
585
|
-
let deviceId;
|
|
586
|
-
if (fs.existsSync(deviceIdPath)) {
|
|
587
|
-
deviceId = fs.readFileSync(deviceIdPath, "utf-8").trim();
|
|
588
|
-
}
|
|
589
|
-
else {
|
|
590
|
-
deviceId = `${os.hostname()}-${Date.now().toString(36)}`;
|
|
591
|
-
fs.mkdirSync(path.dirname(deviceIdPath), { recursive: true });
|
|
592
|
-
fs.writeFileSync(deviceIdPath, deviceId);
|
|
593
|
-
}
|
|
594
|
-
// Get local IP
|
|
595
|
-
let localIp = "";
|
|
596
|
-
try {
|
|
597
|
-
const nets = os.networkInterfaces();
|
|
598
|
-
for (const addrs of Object.values(nets)) {
|
|
599
|
-
for (const addr of addrs || []) {
|
|
600
|
-
if (addr.family === "IPv4" && !addr.internal) {
|
|
601
|
-
localIp = addr.address;
|
|
602
|
-
break;
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
if (localIp)
|
|
606
|
-
break;
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
catch { /* ignore */ }
|
|
610
|
-
// Read existing clients.jsonc from cloud
|
|
611
|
-
let clients = {};
|
|
612
|
-
try {
|
|
613
|
-
const content = await cloudRead("clients.jsonc");
|
|
614
|
-
if (content)
|
|
615
|
-
clients = JSON.parse(content);
|
|
616
|
-
}
|
|
617
|
-
catch { /* start fresh */ }
|
|
618
|
-
// Update this device's entry
|
|
619
|
-
clients[deviceId] = {
|
|
620
|
-
hostname: os.hostname(),
|
|
621
|
-
platform: `${process.platform} ${process.arch}`,
|
|
622
|
-
accounts: settings.accounts.map((a) => a.id),
|
|
623
|
-
lastSeen: new Date().toISOString(),
|
|
624
|
-
ip: localIp,
|
|
625
|
-
version: JSON.parse(fs.readFileSync(path.join(import.meta.dirname, "..", "package.json"), "utf-8")).version,
|
|
626
|
-
};
|
|
627
|
-
// Write back
|
|
628
|
-
const ok = await cloudWrite("clients.jsonc", JSON.stringify(clients, null, 2));
|
|
629
|
-
if (ok)
|
|
630
|
-
console.log(` [client] Registered device: ${deviceId}`);
|
|
631
|
-
}
|
|
632
|
-
async function main() {
|
|
633
|
-
log(`Platform: ${process.platform} ${process.arch}`);
|
|
634
|
-
log(`Node: ${process.version}`);
|
|
635
|
-
log(`Mode: ${setupMode ? "setup" : "auto"}`);
|
|
636
|
-
// Test connectivity
|
|
637
|
-
if (testMode) {
|
|
638
|
-
await runTest();
|
|
639
|
-
return;
|
|
640
|
-
}
|
|
641
|
-
// Add account to existing config
|
|
642
|
-
if (addMode) {
|
|
643
|
-
const account = await promptForAccount();
|
|
644
|
-
if (account) {
|
|
645
|
-
const home = process.env.USERPROFILE || process.env.HOME || "";
|
|
646
|
-
const mailxDir = path.join(home, ".mailx");
|
|
647
|
-
const settingsPath = path.join(mailxDir, "settings.jsonc");
|
|
648
|
-
let settings;
|
|
649
|
-
try {
|
|
650
|
-
settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8").replace(/\r/g, "").replace(/\/\/.*/g, ""));
|
|
651
|
-
}
|
|
652
|
-
catch {
|
|
653
|
-
settings = { accounts: [] };
|
|
654
|
-
}
|
|
655
|
-
if (!settings.accounts)
|
|
656
|
-
settings.accounts = [];
|
|
657
|
-
settings.accounts.push(account);
|
|
658
|
-
fs.mkdirSync(mailxDir, { recursive: true });
|
|
659
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
660
|
-
console.log(`Added ${account.email} to settings. Restart mailx to connect.`);
|
|
661
|
-
}
|
|
662
|
-
process.exit(0);
|
|
663
|
-
}
|
|
664
|
-
// Auto-detect first run — enter setup if no config exists
|
|
665
|
-
if (setupMode || !hasConfig()) {
|
|
666
|
-
if (!setupMode)
|
|
667
|
-
console.log("No mailx configuration found.");
|
|
668
|
-
// -email or -mail flag skips the interactive prompt
|
|
669
|
-
const emailFlag = args.findIndex(a => a === "-email" || a === "--email" || a === "-mail" || a === "--mail");
|
|
670
|
-
const emailArg = args.find(a => a.startsWith("-email=") || a.startsWith("--email=") || a.startsWith("-mail=") || a.startsWith("--mail="))?.split("=")[1]
|
|
671
|
-
|| (emailFlag >= 0 ? args[emailFlag + 1] : undefined);
|
|
672
|
-
await runSetup(emailArg);
|
|
673
|
-
}
|
|
674
|
-
// Redirect console to log file — keep terminal clean
|
|
675
|
-
if (!verbose) {
|
|
676
|
-
const home = process.env.USERPROFILE || process.env.HOME || ".";
|
|
677
|
-
const logDir = path.join(home, ".mailx", "logs");
|
|
678
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
679
|
-
const logDate = new Date().toISOString().slice(0, 10);
|
|
680
|
-
const logPath = path.join(logDir, `mailx-${logDate}.log`);
|
|
681
|
-
const logStream = fs.createWriteStream(logPath, { flags: "a" });
|
|
682
|
-
const ts = () => new Date().toISOString().slice(11, 23);
|
|
683
|
-
console.log = (...a) => { logStream.write(`${ts()} ${a.join(" ")}\n`); };
|
|
684
|
-
console.error = (...a) => { logStream.write(`${ts()} ERROR ${a.join(" ")}\n`); };
|
|
685
|
-
}
|
|
686
|
-
// IPC service mode — no HTTP server
|
|
687
|
-
console.log("Starting mailx service...");
|
|
688
|
-
const { MailxDB } = await import("@bobfrankston/mailx-store");
|
|
689
|
-
const { ImapManager } = await import("@bobfrankston/mailx-imap");
|
|
690
|
-
const { MailxService } = await import("@bobfrankston/mailx-service");
|
|
691
|
-
const { dispatch } = await import("@bobfrankston/mailx-service/jsonrpc.js");
|
|
692
|
-
const { loadSettings, loadAccountsAsync, getConfigDir, getStorageInfo } = await import("@bobfrankston/mailx-settings");
|
|
693
|
-
let settings = loadSettings();
|
|
694
|
-
if (settings.accounts.length === 0) {
|
|
695
|
-
const cloudAccounts = await loadAccountsAsync();
|
|
696
|
-
if (cloudAccounts.length > 0) {
|
|
697
|
-
settings = { ...settings, accounts: cloudAccounts };
|
|
698
|
-
console.log(` Loaded ${cloudAccounts.length} account(s) from cloud`);
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
const db = new MailxDB(getConfigDir());
|
|
702
|
-
const { NodeTransport } = await import("@bobfrankston/iflow-node");
|
|
703
|
-
const imapManager = new ImapManager(db, () => new NodeTransport());
|
|
704
|
-
// Native client is the only option (iflow-direct)
|
|
705
|
-
const svc = new MailxService(db, imapManager);
|
|
706
|
-
// Open msger in service mode — custom protocol serves files from client dir
|
|
707
|
-
const clientDir = path.join(import.meta.dirname, "..", "client");
|
|
708
|
-
const mailxapiPath = path.join(clientDir, "lib", "mailxapi.js");
|
|
709
|
-
const mailxapiScript = fs.readFileSync(mailxapiPath, "utf-8");
|
|
710
|
-
const rootPkgVersion = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, "..", "package.json"), "utf-8")).version;
|
|
711
|
-
const handle = showService({
|
|
712
|
-
title: `mailx v${rootPkgVersion}`,
|
|
713
|
-
url: "index.html",
|
|
714
|
-
contentDir: clientDir,
|
|
715
|
-
initScript: mailxapiScript,
|
|
716
|
-
icon: path.join(clientDir, "icon.png"),
|
|
717
|
-
size: { width: 1400, height: 900 },
|
|
718
|
-
escapeCloses: false,
|
|
719
|
-
});
|
|
720
|
-
// Handle requests from WebView → dispatch to MailxService
|
|
721
|
-
// Pass server version to dispatch so getVersion returns it
|
|
722
|
-
const rootPkg = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, "..", "package.json"), "utf-8"));
|
|
723
|
-
handle.onRequest(async (req) => {
|
|
724
|
-
if (!req._action) {
|
|
725
|
-
// msger sends {"button":"OK","closed":true} on navigation — ignore it, don't exit
|
|
726
|
-
if (req.closed || req.button) {
|
|
727
|
-
console.log(`[ipc] ← ignored close signal during navigation: ${JSON.stringify(req).substring(0, 100)}`);
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
console.log(`[ipc] ← ignored (no _action): ${JSON.stringify(req).substring(0, 100)}`);
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
console.log(`[ipc] ← ${req._action} (${req._cbid})`);
|
|
734
|
-
try {
|
|
735
|
-
const response = await dispatch(svc, req);
|
|
736
|
-
console.log(`[ipc] → ${req._action} (${req._cbid}) ok`);
|
|
737
|
-
handle.send(response);
|
|
738
|
-
}
|
|
739
|
-
catch (e) {
|
|
740
|
-
console.error(`[ipc] → ${req._action} (${req._cbid}) error: ${e.message}`);
|
|
741
|
-
handle.send({ _cbid: req._cbid, error: e.message });
|
|
742
|
-
}
|
|
743
|
-
});
|
|
744
|
-
// Wire IMAP events → push to WebView (throttled to avoid flooding stdin)
|
|
745
|
-
let pendingSyncProgress = {};
|
|
746
|
-
let syncProgressTimer = null;
|
|
747
|
-
imapManager.on("syncProgress", (accountId, phase, progress) => {
|
|
748
|
-
pendingSyncProgress[accountId] = { phase, progress };
|
|
749
|
-
if (!syncProgressTimer) {
|
|
750
|
-
syncProgressTimer = setTimeout(() => {
|
|
751
|
-
syncProgressTimer = null;
|
|
752
|
-
for (const [id, p] of Object.entries(pendingSyncProgress)) {
|
|
753
|
-
handle.send({ _event: "syncProgress", type: "syncProgress", accountId: id, phase: p.phase, progress: p.progress });
|
|
754
|
-
}
|
|
755
|
-
pendingSyncProgress = {};
|
|
756
|
-
}, 500); // batch sync events every 500ms
|
|
757
|
-
}
|
|
758
|
-
});
|
|
759
|
-
let pendingCounts = {};
|
|
760
|
-
let countsTimer = null;
|
|
761
|
-
imapManager.on("folderCountsChanged", (accountId, counts) => {
|
|
762
|
-
pendingCounts[accountId] = counts;
|
|
763
|
-
if (!countsTimer) {
|
|
764
|
-
countsTimer = setTimeout(() => {
|
|
765
|
-
countsTimer = null;
|
|
766
|
-
for (const [id, c] of Object.entries(pendingCounts)) {
|
|
767
|
-
handle.send({ _event: "folderCountsChanged", type: "folderCountsChanged", accountId: id, ...c });
|
|
768
|
-
}
|
|
769
|
-
pendingCounts = {};
|
|
770
|
-
}, 1000); // batch count updates every 1s
|
|
771
|
-
}
|
|
772
|
-
});
|
|
773
|
-
imapManager.on("syncError", (accountId, error) => {
|
|
774
|
-
handle.send({ _event: "error", type: "error", message: `${accountId}: ${error}` });
|
|
775
|
-
});
|
|
776
|
-
imapManager.on("accountError", (accountId, error, hint, isOAuth) => {
|
|
777
|
-
handle.send({ _event: "accountError", type: "accountError", accountId, error, hint, isOAuth });
|
|
778
|
-
});
|
|
779
|
-
// Brief pause for WebView2 to initialize before starting IMAP (avoids stdin writes during init)
|
|
780
|
-
await new Promise(r => setTimeout(r, 500));
|
|
781
|
-
// Register all accounts (OAuth may open browser for Gmail — event loop stays free for IPC)
|
|
782
|
-
for (const account of settings.accounts) {
|
|
783
|
-
if (!account.enabled)
|
|
784
|
-
continue;
|
|
785
|
-
try {
|
|
786
|
-
await imapManager.addAccount(account);
|
|
787
|
-
console.log(` Account: ${account.label || account.name} (${account.id})`);
|
|
788
|
-
}
|
|
789
|
-
catch (e) {
|
|
790
|
-
console.error(` Failed: ${account.id}: ${e.message}`);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
// Register this client device on GDrive (fire-and-forget)
|
|
794
|
-
registerClient(settings).catch(() => { });
|
|
795
|
-
// Start sync in background — don't block
|
|
796
|
-
if (settings.accounts.some(a => a.enabled)) {
|
|
797
|
-
imapManager.syncAll().catch(e => console.error(` Sync error: ${e.message}`));
|
|
798
|
-
}
|
|
799
|
-
imapManager.startPeriodicSync(settings.sync.intervalMinutes);
|
|
800
|
-
imapManager.startOutboxWorker();
|
|
801
|
-
// Graceful shutdown — close IMAP connections, stop timers, close DB
|
|
802
|
-
let shuttingDown = false;
|
|
803
|
-
async function gracefulShutdown(reason) {
|
|
804
|
-
if (shuttingDown)
|
|
805
|
-
return;
|
|
806
|
-
shuttingDown = true;
|
|
807
|
-
console.log(`${reason} — shutting down`);
|
|
808
|
-
imapManager.stopPeriodicSync();
|
|
809
|
-
imapManager.stopOutboxWorker();
|
|
810
|
-
// 3s hard timeout — don't hang on broken IMAP connections
|
|
811
|
-
const forceExit = setTimeout(() => { console.log("Forced exit"); process.exit(0); }, 3000);
|
|
812
|
-
try {
|
|
813
|
-
await imapManager.shutdown();
|
|
814
|
-
}
|
|
815
|
-
catch { /* proceed */ }
|
|
816
|
-
clearTimeout(forceExit);
|
|
817
|
-
db.close();
|
|
818
|
-
process.exit(0);
|
|
819
|
-
}
|
|
820
|
-
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
|
|
821
|
-
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
|
822
|
-
process.on("uncaughtException", (err) => {
|
|
823
|
-
console.error(`UNCAUGHT EXCEPTION: ${err.stack || err.message}`);
|
|
824
|
-
gracefulShutdown("uncaughtException");
|
|
825
|
-
});
|
|
826
|
-
process.on("unhandledRejection", (reason) => {
|
|
827
|
-
console.error(`UNHANDLED REJECTION: ${reason?.stack || reason?.message || reason}`);
|
|
828
|
-
});
|
|
829
|
-
process.on("exit", (code) => {
|
|
830
|
-
console.log(`Process exit (code ${code})`);
|
|
831
|
-
if (!shuttingDown) {
|
|
832
|
-
imapManager.stopPeriodicSync();
|
|
833
|
-
imapManager.stopOutboxWorker();
|
|
834
|
-
db.close();
|
|
835
|
-
}
|
|
836
|
-
});
|
|
837
|
-
// Wait for window close, then shut down
|
|
838
|
-
await handle.closed;
|
|
839
|
-
await gracefulShutdown("Window closed");
|
|
840
|
-
}
|
|
841
|
-
main().catch(console.error);
|
|
842
|
-
//# sourceMappingURL=mailx.js.map
|