@bobfrankston/mailx 1.0.117 → 1.0.119
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 +162 -151
- package/bin/postinstall.js +13 -15
- package/client/app.js +31 -1
- package/package.json +1 -1
- package/packages/mailx-api/index.js +42 -0
- package/packages/mailx-imap/index.d.ts +2 -0
- package/packages/mailx-imap/index.js +2 -0
- package/packages/mailx-server/index.js +7 -1
- package/packages/mailx-store/db.d.ts +6 -0
- package/packages/mailx-store/db.js +3 -0
package/bin/mailx.js
CHANGED
|
@@ -10,26 +10,20 @@
|
|
|
10
10
|
* mailx --verbose Show detailed startup info
|
|
11
11
|
* mailx -v / --version Show version and exit
|
|
12
12
|
*/
|
|
13
|
-
|
|
14
13
|
import fs from "node:fs";
|
|
15
14
|
import path from "node:path";
|
|
16
15
|
import net from "node:net";
|
|
17
|
-
|
|
18
16
|
const PORT = 9333;
|
|
19
17
|
const args = process.argv.slice(2);
|
|
20
|
-
|
|
21
18
|
// Normalize: accept both -flag and --flag
|
|
22
19
|
function hasFlag(name) { return args.includes(`-${name}`) || args.includes(`--${name}`); }
|
|
23
|
-
|
|
24
20
|
const serverMode = hasFlag("server");
|
|
25
21
|
const noBrowser = hasFlag("no-browser");
|
|
26
22
|
const verbose = hasFlag("verbose");
|
|
27
|
-
|
|
28
23
|
const setupMode = hasFlag("setup");
|
|
29
24
|
const addMode = hasFlag("add");
|
|
30
25
|
const testMode = hasFlag("test");
|
|
31
26
|
const rebuildMode = hasFlag("rebuild");
|
|
32
|
-
|
|
33
27
|
// Validate arguments
|
|
34
28
|
const knownFlags = ["server", "no-browser", "verbose", "external", "kill", "v", "version", "setup", "add", "test", "rebuild", "native-imap"];
|
|
35
29
|
for (const arg of args) {
|
|
@@ -40,105 +34,133 @@ for (const arg of args) {
|
|
|
40
34
|
process.exit(1);
|
|
41
35
|
}
|
|
42
36
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
37
|
+
function log(...msg) { if (verbose)
|
|
38
|
+
console.log("[mailx]", ...msg); }
|
|
46
39
|
// Kill any running mailx server
|
|
47
40
|
if (hasFlag("kill")) {
|
|
48
41
|
log("Killing mailx processes...");
|
|
49
42
|
const { execSync } = await import("node:child_process");
|
|
50
43
|
let killed = 0;
|
|
51
|
-
|
|
52
44
|
// Try graceful exit first
|
|
53
45
|
try {
|
|
54
46
|
execSync(`curl -s -m 2 http://localhost:${PORT}/api/exit`, { stdio: "pipe" });
|
|
55
47
|
log("Sent graceful exit");
|
|
56
48
|
execSync("timeout /t 1 /nobreak", { stdio: "pipe" });
|
|
57
|
-
}
|
|
58
|
-
|
|
49
|
+
}
|
|
50
|
+
catch { /* server may not be responding */ }
|
|
59
51
|
if (process.platform === "win32") {
|
|
60
52
|
// Kill by port
|
|
61
53
|
try {
|
|
62
54
|
const out = execSync(`netstat -ano | findstr :${PORT} | findstr LISTENING`, { encoding: "utf-8" }).trim();
|
|
63
55
|
const pids = [...new Set(out.split("\n").map(l => l.trim().split(/\s+/).pop()).filter(Boolean))];
|
|
64
56
|
for (const pid of pids) {
|
|
65
|
-
try {
|
|
57
|
+
try {
|
|
58
|
+
execSync(`taskkill /F /PID ${pid}`, { stdio: "pipe" });
|
|
59
|
+
console.log(`Killed PID ${pid} (port ${PORT})`);
|
|
60
|
+
killed++;
|
|
61
|
+
}
|
|
62
|
+
catch { /* */ }
|
|
66
63
|
}
|
|
67
|
-
}
|
|
68
|
-
|
|
64
|
+
}
|
|
65
|
+
catch { /* no process on port */ }
|
|
69
66
|
// Kill any node.exe running mailx-server (uses tasklist + powershell instead of wmic)
|
|
70
67
|
try {
|
|
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();
|
|
68
|
+
const ps = execSync(`powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"name='node.exe'\\" | Where-Object { $_.CommandLine -match 'mailx-server|mailx\\\\packages' } | Select-Object -ExpandProperty ProcessId"`, { encoding: "utf-8" }).trim();
|
|
75
69
|
for (const pid of ps.split("\n").map(s => s.trim()).filter(s => /^\d+$/.test(s))) {
|
|
76
|
-
try {
|
|
70
|
+
try {
|
|
71
|
+
execSync(`taskkill /F /PID ${pid}`, { stdio: "pipe" });
|
|
72
|
+
console.log(`Killed PID ${pid} (mailx node process)`);
|
|
73
|
+
killed++;
|
|
74
|
+
}
|
|
75
|
+
catch { /* */ }
|
|
77
76
|
}
|
|
78
|
-
}
|
|
79
|
-
|
|
77
|
+
}
|
|
78
|
+
catch { /* */ }
|
|
80
79
|
// Kill mailx-app.exe
|
|
81
|
-
try {
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
try {
|
|
81
|
+
execSync("taskkill /F /IM mailx-app.exe", { stdio: "pipe" });
|
|
82
|
+
console.log("Killed mailx-app.exe");
|
|
83
|
+
killed++;
|
|
84
|
+
}
|
|
85
|
+
catch { /* */ }
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
try {
|
|
89
|
+
execSync(`fuser -k ${PORT}/tcp`, { stdio: "pipe" });
|
|
90
|
+
console.log(`Killed process on port ${PORT}`);
|
|
91
|
+
killed++;
|
|
92
|
+
}
|
|
93
|
+
catch { /* */ }
|
|
84
94
|
}
|
|
85
|
-
|
|
86
95
|
// Clean up stale SQLite WAL/SHM files
|
|
87
96
|
const mailxDir = path.join(process.env.USERPROFILE || process.env.HOME || ".", ".mailx");
|
|
88
97
|
for (const ext of ["mailx.db-shm", "mailx.db-wal"]) {
|
|
89
98
|
const p = path.join(mailxDir, ext);
|
|
90
|
-
try {
|
|
99
|
+
try {
|
|
100
|
+
fs.unlinkSync(p);
|
|
101
|
+
log(`Cleaned ${ext}`);
|
|
102
|
+
}
|
|
103
|
+
catch { /* */ }
|
|
91
104
|
}
|
|
92
|
-
|
|
93
105
|
// Remove lock file
|
|
94
106
|
const lockPath = path.join(mailxDir, "mailx-app.lock");
|
|
95
|
-
try {
|
|
96
|
-
|
|
97
|
-
|
|
107
|
+
try {
|
|
108
|
+
fs.unlinkSync(lockPath);
|
|
109
|
+
log("Removed lock file");
|
|
110
|
+
}
|
|
111
|
+
catch { /* */ }
|
|
112
|
+
if (killed === 0)
|
|
113
|
+
console.log("No mailx processes found");
|
|
98
114
|
process.exit(0);
|
|
99
115
|
}
|
|
100
|
-
|
|
101
116
|
// Rebuild: wipe DB + message store, keep accounts/settings
|
|
102
117
|
if (rebuildMode) {
|
|
103
118
|
const { getConfigDir, getStorePath } = await import("@bobfrankston/mailx-settings");
|
|
104
119
|
const dbDir = getConfigDir();
|
|
105
120
|
const storePath = getStorePath();
|
|
106
|
-
|
|
107
121
|
console.log("Rebuilding mailx local cache...");
|
|
108
122
|
console.log(" Accounts and settings will be preserved.");
|
|
109
|
-
|
|
110
123
|
// Remove DB files
|
|
111
124
|
for (const f of ["mailx.db", "mailx.db-wal", "mailx.db-shm"]) {
|
|
112
125
|
const p = path.join(dbDir, f);
|
|
113
|
-
if (fs.existsSync(p)) {
|
|
126
|
+
if (fs.existsSync(p)) {
|
|
127
|
+
fs.unlinkSync(p);
|
|
128
|
+
console.log(` Deleted ${f}`);
|
|
129
|
+
}
|
|
114
130
|
}
|
|
115
|
-
|
|
116
131
|
// Remove message store
|
|
117
132
|
if (fs.existsSync(storePath)) {
|
|
118
133
|
fs.rmSync(storePath, { recursive: true });
|
|
119
134
|
console.log(` Deleted message store`);
|
|
120
135
|
}
|
|
121
|
-
|
|
122
136
|
console.log(" Rebuild complete. Run 'mailx' to start fresh.");
|
|
123
137
|
process.exit(0);
|
|
124
138
|
}
|
|
125
|
-
|
|
126
139
|
// Version
|
|
127
140
|
if (hasFlag("v") || hasFlag("version")) {
|
|
128
141
|
const root = path.join(import.meta.dirname, "..");
|
|
129
142
|
const ver = (pkg) => {
|
|
130
|
-
for (const dir of [`${root}/node_modules/${pkg}`, `${root}/node_modules/@bobfrankston/${pkg.replace("@bobfrankston/","")}`]) {
|
|
131
|
-
try {
|
|
143
|
+
for (const dir of [`${root}/node_modules/${pkg}`, `${root}/node_modules/@bobfrankston/${pkg.replace("@bobfrankston/", "")}`]) {
|
|
144
|
+
try {
|
|
145
|
+
return JSON.parse(fs.readFileSync(`${dir}/package.json`, "utf-8")).version;
|
|
146
|
+
}
|
|
147
|
+
catch { /* */ }
|
|
132
148
|
}
|
|
133
149
|
// Check workspace packages
|
|
134
|
-
const short = pkg.replace("@bobfrankston/","");
|
|
135
|
-
try {
|
|
150
|
+
const short = pkg.replace("@bobfrankston/", "");
|
|
151
|
+
try {
|
|
152
|
+
return JSON.parse(fs.readFileSync(`${root}/packages/${short}/package.json`, "utf-8")).version;
|
|
153
|
+
}
|
|
154
|
+
catch { /* */ }
|
|
136
155
|
return "not found";
|
|
137
156
|
};
|
|
138
157
|
try {
|
|
139
158
|
const pkg = JSON.parse(fs.readFileSync(`${root}/package.json`, "utf-8"));
|
|
140
159
|
console.log(`mailx v${pkg.version}`);
|
|
141
|
-
}
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
console.log("mailx (version unknown)");
|
|
163
|
+
}
|
|
142
164
|
console.log(` node ${process.version}`);
|
|
143
165
|
console.log(` iflow ${ver("@bobfrankston/iflow")}`);
|
|
144
166
|
console.log(` miscinfo ${ver("@bobfrankston/miscinfo")}`);
|
|
@@ -149,7 +171,6 @@ if (hasFlag("v") || hasFlag("version")) {
|
|
|
149
171
|
console.log(` platform ${process.platform} ${process.arch}`);
|
|
150
172
|
process.exit(0);
|
|
151
173
|
}
|
|
152
|
-
|
|
153
174
|
function isPortInUse(port) {
|
|
154
175
|
return new Promise((resolve) => {
|
|
155
176
|
const socket = net.createConnection({ port, host: "127.0.0.1" });
|
|
@@ -157,27 +178,27 @@ function isPortInUse(port) {
|
|
|
157
178
|
socket.once("error", () => resolve(false));
|
|
158
179
|
});
|
|
159
180
|
}
|
|
160
|
-
|
|
161
181
|
function openBrowser(url) {
|
|
162
182
|
import("node:child_process").then(({ exec }) => {
|
|
163
|
-
if (process.platform === "win32")
|
|
164
|
-
|
|
165
|
-
else
|
|
183
|
+
if (process.platform === "win32")
|
|
184
|
+
exec(`start "" "${url}"`);
|
|
185
|
+
else if (process.platform === "darwin")
|
|
186
|
+
exec(`open "${url}"`);
|
|
187
|
+
else
|
|
188
|
+
exec(`xdg-open "${url}"`);
|
|
166
189
|
});
|
|
167
190
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const readline = require("readline");
|
|
191
|
+
async function prompt(question) {
|
|
192
|
+
const readline = await import("readline");
|
|
171
193
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
172
|
-
return new Promise(resolve => rl.question(question, ans => { rl.close(); resolve(ans.trim()); }));
|
|
194
|
+
return new Promise(resolve => rl.question(question, (ans) => { rl.close(); resolve(ans.trim()); }));
|
|
173
195
|
}
|
|
174
|
-
|
|
175
196
|
/** Detect mounted cloud drives that might have settings */
|
|
176
197
|
function findCloudSettings() {
|
|
177
198
|
const home = process.env.USERPROFILE || process.env.HOME || "";
|
|
178
199
|
const checks = [
|
|
179
200
|
// OneDrive
|
|
180
|
-
{ provider: "onedrive", dir: process.env.OneDrive
|
|
201
|
+
{ provider: "onedrive", dir: process.env.OneDrive ? path.join(process.env.OneDrive, "home", ".mailx") : "" },
|
|
181
202
|
{ provider: "onedrive", dir: path.join(home, "OneDrive", "home", ".mailx") },
|
|
182
203
|
{ provider: "onedrive", dir: path.join(home, "onedrive", "home", ".mailx") },
|
|
183
204
|
// Google Drive
|
|
@@ -199,19 +220,18 @@ function findCloudSettings() {
|
|
|
199
220
|
}
|
|
200
221
|
return null;
|
|
201
222
|
}
|
|
202
|
-
|
|
203
223
|
/** Check if mailx is configured (has accounts) */
|
|
204
224
|
function hasConfig() {
|
|
205
225
|
const home = process.env.USERPROFILE || process.env.HOME || "";
|
|
206
226
|
const mailxDir = path.join(home, ".mailx");
|
|
207
227
|
// Check local settings
|
|
208
228
|
for (const f of ["settings.jsonc", "accounts.jsonc", "config.jsonc"]) {
|
|
209
|
-
if (fs.existsSync(path.join(mailxDir, f)))
|
|
229
|
+
if (fs.existsSync(path.join(mailxDir, f)))
|
|
230
|
+
return true;
|
|
210
231
|
}
|
|
211
232
|
// Check cloud drives
|
|
212
233
|
return findCloudSettings() !== null;
|
|
213
234
|
}
|
|
214
|
-
|
|
215
235
|
/** Try to auto-discover mail server settings from email domain */
|
|
216
236
|
async function autoDiscover(domain) {
|
|
217
237
|
// 1. Try Thunderbird ISPDB
|
|
@@ -229,8 +249,8 @@ async function autoDiscover(domain) {
|
|
|
229
249
|
};
|
|
230
250
|
}
|
|
231
251
|
}
|
|
232
|
-
}
|
|
233
|
-
|
|
252
|
+
}
|
|
253
|
+
catch { /* timeout or not found */ }
|
|
234
254
|
// 2. Try DNS SRV records
|
|
235
255
|
try {
|
|
236
256
|
const dns = await import("node:dns/promises");
|
|
@@ -243,29 +263,26 @@ async function autoDiscover(domain) {
|
|
|
243
263
|
source: "DNS SRV",
|
|
244
264
|
};
|
|
245
265
|
}
|
|
246
|
-
}
|
|
247
|
-
|
|
266
|
+
}
|
|
267
|
+
catch { /* DNS failed */ }
|
|
248
268
|
return null;
|
|
249
269
|
}
|
|
250
|
-
|
|
251
270
|
/** Prompt user for account details, auto-discover where possible */
|
|
252
271
|
async function promptForAccount(intro) {
|
|
253
|
-
if (intro)
|
|
272
|
+
if (intro)
|
|
273
|
+
console.log(intro);
|
|
254
274
|
const email = await prompt("Email address (or 'skip'): ");
|
|
255
|
-
if (!email || email.toLowerCase() === "skip")
|
|
256
|
-
|
|
275
|
+
if (!email || email.toLowerCase() === "skip")
|
|
276
|
+
return null;
|
|
257
277
|
const domain = email.split("@")[1]?.toLowerCase() || "";
|
|
258
278
|
const knownOAuth = ["gmail.com", "googlemail.com", "outlook.com", "hotmail.com"];
|
|
259
|
-
|
|
260
279
|
if (knownOAuth.includes(domain)) {
|
|
261
280
|
console.log(` ${domain}: OAuth2 — no password needed. Browser will prompt for authorization.`);
|
|
262
281
|
return { email };
|
|
263
282
|
}
|
|
264
|
-
|
|
265
283
|
// Try auto-discovery
|
|
266
284
|
console.log(` Looking up mail servers for ${domain}...`);
|
|
267
285
|
const discovered = await autoDiscover(domain);
|
|
268
|
-
|
|
269
286
|
if (discovered) {
|
|
270
287
|
console.log(` Found via ${discovered.source}: IMAP ${discovered.imap.host}:${discovered.imap.port}, SMTP ${discovered.smtp.host}:${discovered.smtp.port}`);
|
|
271
288
|
if (discovered.imap.auth === "oauth2") {
|
|
@@ -279,7 +296,6 @@ async function promptForAccount(intro) {
|
|
|
279
296
|
smtp: { host: discovered.smtp.host, port: discovered.smtp.port },
|
|
280
297
|
};
|
|
281
298
|
}
|
|
282
|
-
|
|
283
299
|
// Manual fallback
|
|
284
300
|
console.log(" Could not auto-detect servers. Enter manually:");
|
|
285
301
|
const password = await prompt("Password: ");
|
|
@@ -291,14 +307,28 @@ async function promptForAccount(intro) {
|
|
|
291
307
|
smtp: { host: smtpHost },
|
|
292
308
|
};
|
|
293
309
|
}
|
|
294
|
-
|
|
310
|
+
/** Find a mounted cloud drive (for saving new settings) */
|
|
311
|
+
function findMountedDrive() {
|
|
312
|
+
const home = process.env.USERPROFILE || process.env.HOME || "";
|
|
313
|
+
const checks = [
|
|
314
|
+
{ provider: "onedrive", base: process.env.OneDrive || "", dir: process.env.OneDrive ? path.join(process.env.OneDrive, "home", ".mailx") : "" },
|
|
315
|
+
{ provider: "onedrive", base: path.join(home, "OneDrive"), dir: path.join(home, "OneDrive", "home", ".mailx") },
|
|
316
|
+
{ provider: "onedrive", base: path.join(home, "onedrive"), dir: path.join(home, "onedrive", "home", ".mailx") },
|
|
317
|
+
{ provider: "gdrive", base: path.join(home, "Google Drive", "My Drive"), dir: path.join(home, "Google Drive", "My Drive", "home", ".mailx") },
|
|
318
|
+
{ provider: "gdrive", base: path.join(home, "Google Drive Streaming", "My Drive"), dir: path.join(home, "Google Drive Streaming", "My Drive", "home", ".mailx") },
|
|
319
|
+
{ provider: "dropbox", base: path.join(home, "Dropbox"), dir: path.join(home, "Dropbox", "home", ".mailx") },
|
|
320
|
+
];
|
|
321
|
+
for (const c of checks) {
|
|
322
|
+
if (c.base && fs.existsSync(c.base))
|
|
323
|
+
return c;
|
|
324
|
+
}
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
295
327
|
/** Interactive first-time setup */
|
|
296
328
|
async function runSetup() {
|
|
297
329
|
console.log("\nmailx — first-time setup\n");
|
|
298
|
-
|
|
299
330
|
const home = process.env.USERPROFILE || process.env.HOME || "";
|
|
300
331
|
const mailxDir = path.join(home, ".mailx");
|
|
301
|
-
|
|
302
332
|
// Check for existing cloud settings
|
|
303
333
|
const cloud = findCloudSettings();
|
|
304
334
|
if (cloud) {
|
|
@@ -310,7 +340,6 @@ async function runSetup() {
|
|
|
310
340
|
console.log("Setup complete. Starting mailx...\n");
|
|
311
341
|
return true;
|
|
312
342
|
}
|
|
313
|
-
|
|
314
343
|
// No cloud settings found — prompt for email
|
|
315
344
|
console.log("No existing settings found on OneDrive, Google Drive, or Dropbox.\n");
|
|
316
345
|
const account = await promptForAccount("Gmail is recommended as the first account (provides contacts + cloud sync).\n");
|
|
@@ -319,33 +348,29 @@ async function runSetup() {
|
|
|
319
348
|
fs.mkdirSync(mailxDir, { recursive: true });
|
|
320
349
|
return false;
|
|
321
350
|
}
|
|
322
|
-
|
|
323
351
|
const name = await prompt(`Your name (for From: header) [${account.email.split("@")[0]}]: `) || account.email.split("@")[0];
|
|
324
|
-
|
|
325
352
|
const settings = {
|
|
326
353
|
name,
|
|
327
354
|
accounts: [account],
|
|
328
355
|
ui: { theme: "system" },
|
|
329
356
|
sync: { intervalMinutes: 5, historyDays: 0 },
|
|
330
357
|
};
|
|
331
|
-
|
|
332
358
|
const domain = account.email.split("@")[1]?.toLowerCase() || "";
|
|
333
359
|
const isGmail = domain === "gmail.com" || domain === "googlemail.com";
|
|
334
|
-
|
|
335
360
|
// Ask where to store settings
|
|
336
361
|
const mountedDrive = findMountedDrive();
|
|
337
362
|
let storageChoice = "local";
|
|
338
|
-
|
|
339
363
|
if (mountedDrive) {
|
|
340
364
|
const useCloud = await prompt(`Store settings on ${mountedDrive.provider} (syncs across machines)? [Y/n]: `);
|
|
341
|
-
if (!useCloud || useCloud.toLowerCase() !== "n")
|
|
342
|
-
|
|
365
|
+
if (!useCloud || useCloud.toLowerCase() !== "n")
|
|
366
|
+
storageChoice = mountedDrive.provider;
|
|
367
|
+
}
|
|
368
|
+
else if (isGmail) {
|
|
343
369
|
const useGDrive = await prompt("Store settings on Google Drive (syncs across machines, editable via drive.google.com)? [Y/n]: ");
|
|
344
|
-
if (!useGDrive || useGDrive.toLowerCase() !== "n")
|
|
370
|
+
if (!useGDrive || useGDrive.toLowerCase() !== "n")
|
|
371
|
+
storageChoice = "gdrive-api";
|
|
345
372
|
}
|
|
346
|
-
|
|
347
373
|
fs.mkdirSync(mailxDir, { recursive: true });
|
|
348
|
-
|
|
349
374
|
if (storageChoice === "gdrive-api") {
|
|
350
375
|
// Save to Google Drive via API (not mounted)
|
|
351
376
|
console.log("\nSaving settings to Google Drive via API...");
|
|
@@ -360,19 +385,23 @@ async function runSetup() {
|
|
|
360
385
|
const config = { sharedDir: { provider: "gdrive", path: "home/.mailx" } };
|
|
361
386
|
fs.writeFileSync(path.join(mailxDir, "config.jsonc"), JSON.stringify(config, null, 2));
|
|
362
387
|
console.log("Local config created pointing to Google Drive.");
|
|
363
|
-
}
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
364
390
|
console.log("Google Drive write failed — saving locally instead.");
|
|
365
391
|
fs.writeFileSync(path.join(mailxDir, "settings.jsonc"), JSON.stringify(settings, null, 2));
|
|
366
392
|
}
|
|
367
|
-
}
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
368
395
|
console.log("Google Drive API not available — saving locally.");
|
|
369
396
|
fs.writeFileSync(path.join(mailxDir, "settings.jsonc"), JSON.stringify(settings, null, 2));
|
|
370
397
|
}
|
|
371
|
-
}
|
|
398
|
+
}
|
|
399
|
+
catch (e) {
|
|
372
400
|
console.log(`Google Drive error: ${e.message} — saving locally.`);
|
|
373
401
|
fs.writeFileSync(path.join(mailxDir, "settings.jsonc"), JSON.stringify(settings, null, 2));
|
|
374
402
|
}
|
|
375
|
-
}
|
|
403
|
+
}
|
|
404
|
+
else if (storageChoice !== "local" && mountedDrive) {
|
|
376
405
|
// Save to mounted cloud drive
|
|
377
406
|
console.log(`\nSaving settings to ${mountedDrive.provider} at ${mountedDrive.dir}...`);
|
|
378
407
|
fs.mkdirSync(mountedDrive.dir, { recursive: true });
|
|
@@ -380,57 +409,36 @@ async function runSetup() {
|
|
|
380
409
|
const config = { sharedDir: { provider: mountedDrive.provider, path: "home/.mailx" } };
|
|
381
410
|
fs.writeFileSync(path.join(mailxDir, "config.jsonc"), JSON.stringify(config, null, 2));
|
|
382
411
|
console.log("Settings saved to cloud drive + local config created.");
|
|
383
|
-
}
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
384
414
|
// Save locally
|
|
385
415
|
console.log(`\nSaving settings to ${mailxDir}...`);
|
|
386
416
|
fs.writeFileSync(path.join(mailxDir, "settings.jsonc"), JSON.stringify(settings, null, 2));
|
|
387
417
|
}
|
|
388
|
-
|
|
389
418
|
console.log("Setup complete. Starting mailx...\n");
|
|
390
419
|
return true;
|
|
391
420
|
}
|
|
392
|
-
|
|
393
|
-
/** Find a mounted cloud drive (for saving new settings) */
|
|
394
|
-
function findMountedDrive() {
|
|
395
|
-
const home = process.env.USERPROFILE || process.env.HOME || "";
|
|
396
|
-
const checks = [
|
|
397
|
-
{ provider: "onedrive", base: process.env.OneDrive, dir: process.env.OneDrive && path.join(process.env.OneDrive, "home", ".mailx") },
|
|
398
|
-
{ provider: "onedrive", base: path.join(home, "OneDrive"), dir: path.join(home, "OneDrive", "home", ".mailx") },
|
|
399
|
-
{ provider: "onedrive", base: path.join(home, "onedrive"), dir: path.join(home, "onedrive", "home", ".mailx") },
|
|
400
|
-
{ provider: "gdrive", base: path.join(home, "Google Drive", "My Drive"), dir: path.join(home, "Google Drive", "My Drive", "home", ".mailx") },
|
|
401
|
-
{ provider: "gdrive", base: path.join(home, "Google Drive Streaming", "My Drive"), dir: path.join(home, "Google Drive Streaming", "My Drive", "home", ".mailx") },
|
|
402
|
-
{ provider: "dropbox", base: path.join(home, "Dropbox"), dir: path.join(home, "Dropbox", "home", ".mailx") },
|
|
403
|
-
];
|
|
404
|
-
for (const c of checks) {
|
|
405
|
-
if (c.base && fs.existsSync(c.base)) return c;
|
|
406
|
-
}
|
|
407
|
-
return null;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
421
|
/** Test account connectivity — IMAP connect, SMTP send, sync round-trip */
|
|
411
422
|
async function runTest() {
|
|
412
423
|
console.log("\nmailx — connection test\n");
|
|
413
|
-
|
|
414
424
|
// Start server in-process to access settings
|
|
415
425
|
console.log("Loading settings...");
|
|
416
426
|
const { loadSettings, getSharedDir } = await import("../packages/mailx-settings/index.js");
|
|
417
427
|
const { initLocalConfig } = await import("../packages/mailx-settings/index.js");
|
|
418
428
|
initLocalConfig();
|
|
419
429
|
const settings = loadSettings();
|
|
420
|
-
|
|
421
430
|
if (settings.accounts.length === 0) {
|
|
422
431
|
console.log("No accounts configured. Run: mailx -setup");
|
|
423
432
|
process.exit(1);
|
|
424
433
|
}
|
|
425
|
-
|
|
426
434
|
console.log(`Shared dir: ${getSharedDir()}`);
|
|
427
|
-
console.log(`Accounts: ${settings.accounts.map(a => `${a.label || a.name} <${a.email}>`).join(", ")}\n`);
|
|
428
|
-
|
|
435
|
+
console.log(`Accounts: ${settings.accounts.map((a) => `${a.label || a.name} <${a.email}>`).join(", ")}\n`);
|
|
429
436
|
for (const account of settings.accounts) {
|
|
430
|
-
if (!account.enabled) {
|
|
431
|
-
|
|
437
|
+
if (!account.enabled) {
|
|
438
|
+
console.log(` ${account.label || account.id}: SKIPPED (disabled)\n`);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
432
441
|
console.log(`Testing ${account.label || account.id} (${account.email}):`);
|
|
433
|
-
|
|
434
442
|
// Test IMAP
|
|
435
443
|
try {
|
|
436
444
|
const { ImapClient, createAutoImapConfig } = await import("@bobfrankston/iflow");
|
|
@@ -444,19 +452,20 @@ async function runTest() {
|
|
|
444
452
|
const folders = await client.getFolderList();
|
|
445
453
|
await client.logout();
|
|
446
454
|
console.log(` IMAP: OK (${folders.length} folders)`);
|
|
447
|
-
}
|
|
455
|
+
}
|
|
456
|
+
catch (e) {
|
|
448
457
|
console.log(` IMAP: FAILED — ${e.message}`);
|
|
449
458
|
}
|
|
450
|
-
|
|
451
459
|
// Test SMTP
|
|
452
460
|
try {
|
|
453
461
|
const { createTransport } = await import("nodemailer");
|
|
454
462
|
let smtpAuth;
|
|
455
463
|
if (account.smtp.auth === "password") {
|
|
456
464
|
smtpAuth = { user: account.smtp.user, pass: account.smtp.password };
|
|
457
|
-
}
|
|
465
|
+
}
|
|
466
|
+
else if (account.smtp.auth === "oauth2") {
|
|
458
467
|
// Try to get OAuth token
|
|
459
|
-
const {
|
|
468
|
+
const { createAutoImapConfig } = await import("@bobfrankston/iflow");
|
|
460
469
|
const config = createAutoImapConfig({
|
|
461
470
|
server: account.imap.host,
|
|
462
471
|
port: account.imap.port,
|
|
@@ -476,7 +485,6 @@ async function runTest() {
|
|
|
476
485
|
});
|
|
477
486
|
await transport.verify();
|
|
478
487
|
console.log(` SMTP: OK`);
|
|
479
|
-
|
|
480
488
|
// Send test message to self
|
|
481
489
|
const testSubject = `mailx test — ${new Date().toLocaleString()}`;
|
|
482
490
|
await transport.sendMail({
|
|
@@ -487,28 +495,24 @@ async function runTest() {
|
|
|
487
495
|
});
|
|
488
496
|
console.log(` SEND: OK — test message sent to ${account.email}`);
|
|
489
497
|
console.log(` Subject: "${testSubject}"`);
|
|
490
|
-
}
|
|
498
|
+
}
|
|
499
|
+
catch (e) {
|
|
491
500
|
console.log(` SMTP: FAILED — ${e.message}`);
|
|
492
501
|
}
|
|
493
|
-
|
|
494
502
|
console.log();
|
|
495
503
|
}
|
|
496
|
-
|
|
497
504
|
console.log("Test complete. Check your inbox for the test message(s).");
|
|
498
505
|
process.exit(0);
|
|
499
506
|
}
|
|
500
|
-
|
|
501
507
|
async function main() {
|
|
502
508
|
log(`Platform: ${process.platform} ${process.arch}`);
|
|
503
509
|
log(`Node: ${process.version}`);
|
|
504
510
|
log(`Mode: ${serverMode ? "server" : setupMode ? "setup" : "auto-detect"}`);
|
|
505
|
-
|
|
506
511
|
// Test connectivity
|
|
507
512
|
if (testMode) {
|
|
508
513
|
await runTest();
|
|
509
514
|
return;
|
|
510
515
|
}
|
|
511
|
-
|
|
512
516
|
// Add account to existing config
|
|
513
517
|
if (addMode) {
|
|
514
518
|
const account = await promptForAccount();
|
|
@@ -517,8 +521,14 @@ async function main() {
|
|
|
517
521
|
const mailxDir = path.join(home, ".mailx");
|
|
518
522
|
const settingsPath = path.join(mailxDir, "settings.jsonc");
|
|
519
523
|
let settings;
|
|
520
|
-
try {
|
|
521
|
-
|
|
524
|
+
try {
|
|
525
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8").replace(/\r/g, "").replace(/\/\/.*/g, ""));
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
settings = { accounts: [] };
|
|
529
|
+
}
|
|
530
|
+
if (!settings.accounts)
|
|
531
|
+
settings.accounts = [];
|
|
522
532
|
settings.accounts.push(account);
|
|
523
533
|
fs.mkdirSync(mailxDir, { recursive: true });
|
|
524
534
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
@@ -526,61 +536,60 @@ async function main() {
|
|
|
526
536
|
}
|
|
527
537
|
process.exit(0);
|
|
528
538
|
}
|
|
529
|
-
|
|
530
539
|
// Auto-detect first run — enter setup if no config exists
|
|
531
540
|
if (setupMode || (!serverMode && !hasConfig())) {
|
|
532
|
-
if (!setupMode)
|
|
541
|
+
if (!setupMode)
|
|
542
|
+
console.log("No mailx configuration found.");
|
|
533
543
|
await runSetup();
|
|
534
544
|
}
|
|
535
|
-
|
|
536
545
|
if (serverMode) {
|
|
537
546
|
// Server mode — Express + WebSocket, open browser
|
|
538
547
|
const inUse = await isPortInUse(PORT);
|
|
539
548
|
if (inUse) {
|
|
540
549
|
console.log(`mailx server already running on port ${PORT}`);
|
|
541
|
-
if (!noBrowser)
|
|
550
|
+
if (!noBrowser)
|
|
551
|
+
openBrowser(`http://127.0.0.1:${PORT}`);
|
|
542
552
|
return;
|
|
543
553
|
}
|
|
544
|
-
|
|
545
554
|
console.log("Starting mailx server...");
|
|
546
555
|
log(`Loading server from: ${path.join(import.meta.dirname, "..", "packages", "mailx-server", "index.js")}`);
|
|
547
|
-
if (hasFlag("external"))
|
|
556
|
+
if (hasFlag("external"))
|
|
557
|
+
process.argv.push("--external");
|
|
548
558
|
await import("../packages/mailx-server/index.js");
|
|
549
|
-
|
|
550
559
|
if (!noBrowser) {
|
|
551
560
|
for (let i = 0; i < 30; i++) {
|
|
552
561
|
await new Promise(r => setTimeout(r, 200));
|
|
553
|
-
if (await isPortInUse(PORT))
|
|
562
|
+
if (await isPortInUse(PORT))
|
|
563
|
+
break;
|
|
554
564
|
}
|
|
555
565
|
openBrowser(`http://127.0.0.1:${PORT}`);
|
|
556
566
|
console.log("mailx opened (browser)");
|
|
557
567
|
}
|
|
558
|
-
|
|
559
568
|
// Keep process alive — server is running
|
|
560
|
-
await new Promise(() => {});
|
|
561
|
-
}
|
|
569
|
+
await new Promise(() => { });
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
562
572
|
// Default: launch native WebView app
|
|
563
573
|
let binaryName;
|
|
564
|
-
if (process.platform === "win32")
|
|
565
|
-
|
|
566
|
-
else
|
|
567
|
-
|
|
574
|
+
if (process.platform === "win32")
|
|
575
|
+
binaryName = "mailx-app.exe";
|
|
576
|
+
else if (process.platform === "darwin")
|
|
577
|
+
binaryName = process.arch === "arm64" ? "mailx-app-arm64" : "mailx-app";
|
|
578
|
+
else
|
|
579
|
+
binaryName = process.arch === "arm64" ? "mailx-app-linux-aarch64" : "mailx-app-linux";
|
|
568
580
|
const launcherPaths = [
|
|
569
581
|
path.join(import.meta.dirname, "..", "launcher", "bin", binaryName),
|
|
570
582
|
path.join(import.meta.dirname, "..", "launcher", "target", "release", binaryName),
|
|
571
583
|
];
|
|
572
|
-
|
|
573
584
|
log(`Looking for native launcher: ${binaryName}`);
|
|
574
|
-
for (const p of launcherPaths)
|
|
575
|
-
|
|
585
|
+
for (const p of launcherPaths)
|
|
586
|
+
log(` ${fs.existsSync(p) ? "FOUND" : "not found"}: ${p}`);
|
|
576
587
|
let launcherPath = launcherPaths.find(p => fs.existsSync(p));
|
|
577
|
-
|
|
578
588
|
// On Linux, skip native launcher if no display server available
|
|
579
589
|
if (launcherPath && process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
|
|
580
590
|
log("No display server (DISPLAY/WAYLAND_DISPLAY not set) — skipping native launcher");
|
|
581
591
|
launcherPath = undefined;
|
|
582
592
|
}
|
|
583
|
-
|
|
584
593
|
if (launcherPath) {
|
|
585
594
|
console.log("Starting mailx...");
|
|
586
595
|
log(`Launching: ${launcherPath}`);
|
|
@@ -594,13 +603,15 @@ async function main() {
|
|
|
594
603
|
});
|
|
595
604
|
child.unref();
|
|
596
605
|
console.log("mailx launched");
|
|
597
|
-
}
|
|
606
|
+
}
|
|
607
|
+
catch (e) {
|
|
598
608
|
console.log(`Native launcher failed: ${e.message}`);
|
|
599
609
|
console.log("Starting in browser mode...");
|
|
600
610
|
process.argv.push("--server");
|
|
601
611
|
await main();
|
|
602
612
|
}
|
|
603
|
-
}
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
604
615
|
console.log("Starting in browser mode...");
|
|
605
616
|
log("No native launcher — falling back to --server mode");
|
|
606
617
|
process.argv.push("--server");
|
|
@@ -608,5 +619,5 @@ async function main() {
|
|
|
608
619
|
}
|
|
609
620
|
}
|
|
610
621
|
}
|
|
611
|
-
|
|
612
622
|
main().catch(console.error);
|
|
623
|
+
//# sourceMappingURL=mailx.js.map
|
package/bin/postinstall.js
CHANGED
|
@@ -3,39 +3,37 @@
|
|
|
3
3
|
* Post-install script: creates symlinks for workspace packages
|
|
4
4
|
* so they resolve as @bobfrankston/mailx-* in node_modules.
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
6
|
import fs from "node:fs";
|
|
8
7
|
import path from "node:path";
|
|
9
|
-
|
|
10
8
|
const root = path.resolve(import.meta.dirname, "..");
|
|
11
9
|
const packagesDir = path.join(root, "packages");
|
|
12
10
|
const nmDir = path.join(root, "node_modules", "@bobfrankston");
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
if (!fs.existsSync(packagesDir))
|
|
12
|
+
process.exit(0); // not in workspace layout
|
|
16
13
|
fs.mkdirSync(nmDir, { recursive: true });
|
|
17
|
-
|
|
18
14
|
for (const dir of fs.readdirSync(packagesDir)) {
|
|
19
15
|
const pkgPath = path.join(packagesDir, dir, "package.json");
|
|
20
|
-
if (!fs.existsSync(pkgPath))
|
|
21
|
-
|
|
16
|
+
if (!fs.existsSync(pkgPath))
|
|
17
|
+
continue;
|
|
22
18
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
23
19
|
const name = pkg.name?.split("/")[1]; // e.g., "mailx-store" from "@bobfrankston/mailx-store"
|
|
24
|
-
if (!name)
|
|
25
|
-
|
|
20
|
+
if (!name)
|
|
21
|
+
continue;
|
|
26
22
|
const linkPath = path.join(nmDir, name);
|
|
27
23
|
const targetPath = path.join(packagesDir, dir);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
if (fs.existsSync(linkPath))
|
|
25
|
+
continue; // already linked
|
|
31
26
|
try {
|
|
32
27
|
// Use junction on Windows (no admin needed), symlink on Unix
|
|
33
28
|
if (process.platform === "win32") {
|
|
34
29
|
fs.symlinkSync(targetPath, linkPath, "junction");
|
|
35
|
-
}
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
36
32
|
fs.symlinkSync(targetPath, linkPath, "dir");
|
|
37
33
|
}
|
|
38
|
-
}
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
39
36
|
console.error(`Failed to link ${name}: ${e.message}`);
|
|
40
37
|
}
|
|
41
38
|
}
|
|
39
|
+
//# sourceMappingURL=postinstall.js.map
|
package/client/app.js
CHANGED
|
@@ -844,7 +844,37 @@ fetch("/api/version").then(r => r.json()).then(d => {
|
|
|
844
844
|
: "";
|
|
845
845
|
if (el)
|
|
846
846
|
el.textContent = `mailx v${d.version}${storageLabel}${isApp ? "" : " [browser]"}`;
|
|
847
|
-
if (
|
|
847
|
+
if (d.settingsError) {
|
|
848
|
+
showAlert(d.settingsError, "settings-error");
|
|
849
|
+
// Add repair button to the banner
|
|
850
|
+
const banner = document.getElementById("alert-banner");
|
|
851
|
+
if (banner && !banner.querySelector(".repair-btn")) {
|
|
852
|
+
const btn = document.createElement("button");
|
|
853
|
+
btn.className = "repair-btn status-action";
|
|
854
|
+
btn.textContent = "Repair: restore accounts from cache";
|
|
855
|
+
btn.style.cssText = "margin-left:1rem;padding:0.25rem 0.75rem;background:#a6e3a1;color:#1e1e2e;border:none;border-radius:4px;cursor:pointer;font-weight:bold";
|
|
856
|
+
btn.onclick = async () => {
|
|
857
|
+
btn.textContent = "Restoring...";
|
|
858
|
+
btn.disabled = true;
|
|
859
|
+
try {
|
|
860
|
+
const r = await fetch("/api/repair-accounts", { method: "POST" });
|
|
861
|
+
const data = await r.json();
|
|
862
|
+
if (data.ok) {
|
|
863
|
+
hideAlert();
|
|
864
|
+
setTimeout(() => location.reload(), 1000);
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
btn.textContent = `Failed: ${data.error}`;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
catch (e) {
|
|
871
|
+
btn.textContent = `Error: ${e.message}`;
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
banner.querySelector("#alert-text")?.after(btn);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
else if (storage.cloudError) {
|
|
848
878
|
showAlert(`Cloud storage error: ${storage.cloudError}`, "cloud-error");
|
|
849
879
|
}
|
|
850
880
|
}).catch(async () => {
|
package/package.json
CHANGED
|
@@ -158,6 +158,48 @@ export function createApiRouter(db, imapManager) {
|
|
|
158
158
|
res.status(500).json({ error: e.message });
|
|
159
159
|
}
|
|
160
160
|
});
|
|
161
|
+
// ── Repair: restore accounts from DB cache to settings, re-register IMAP ──
|
|
162
|
+
router.post("/repair-accounts", async (req, res) => {
|
|
163
|
+
try {
|
|
164
|
+
// Get accounts from DB (stale but present)
|
|
165
|
+
const dbAccounts = db.getAccountConfigs();
|
|
166
|
+
if (dbAccounts.length === 0) {
|
|
167
|
+
res.json({ ok: false, error: "No cached accounts in database" });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// Rebuild account configs from DB's stored config_json
|
|
171
|
+
const restored = [];
|
|
172
|
+
for (const a of dbAccounts) {
|
|
173
|
+
try {
|
|
174
|
+
const cfg = JSON.parse(a.configJson);
|
|
175
|
+
restored.push(cfg);
|
|
176
|
+
}
|
|
177
|
+
catch { /* skip corrupt entries */ }
|
|
178
|
+
}
|
|
179
|
+
if (restored.length === 0) {
|
|
180
|
+
res.json({ ok: false, error: "Could not parse cached account configs" });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// Save back to shared dir (and cloud API if active)
|
|
184
|
+
saveAccounts(restored);
|
|
185
|
+
// Re-register in IMAP manager
|
|
186
|
+
for (const acct of restored) {
|
|
187
|
+
try {
|
|
188
|
+
await imapManager.addAccount(acct);
|
|
189
|
+
console.log(` [repair] Re-registered account: ${acct.name} (${acct.id})`);
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
console.error(` [repair] Failed to register ${acct.id}: ${e.message}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Start sync
|
|
196
|
+
imapManager.syncAll().catch(() => { });
|
|
197
|
+
res.json({ ok: true, message: `Restored ${restored.length} account(s) and started sync.` });
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
res.status(500).json({ error: e.message });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
161
203
|
// ── Send ──
|
|
162
204
|
router.post("/send", async (req, res) => {
|
|
163
205
|
try {
|
|
@@ -56,6 +56,8 @@ export declare class ImapManager extends EventEmitter {
|
|
|
56
56
|
private createClient;
|
|
57
57
|
/** Track client logout for connection counting */
|
|
58
58
|
private trackLogout;
|
|
59
|
+
/** Number of registered IMAP accounts */
|
|
60
|
+
getAccountCount(): number;
|
|
59
61
|
/** Register an account */
|
|
60
62
|
addAccount(account: AccountConfig): Promise<void>;
|
|
61
63
|
/** Sync folder list for an account */
|
|
@@ -214,6 +214,8 @@ export class ImapManager extends EventEmitter {
|
|
|
214
214
|
this.activeConnections.set(accountId, count);
|
|
215
215
|
console.log(` [conn] ${accountId}: -1 (${count} active)`);
|
|
216
216
|
}
|
|
217
|
+
/** Number of registered IMAP accounts */
|
|
218
|
+
getAccountCount() { return this.configs.size; }
|
|
217
219
|
/** Register an account */
|
|
218
220
|
async addAccount(account) {
|
|
219
221
|
if (this.configs.has(account.id))
|
|
@@ -94,7 +94,13 @@ const apiRouter = createApiRouter(db, imapManager);
|
|
|
94
94
|
app.use("/api", apiRouter);
|
|
95
95
|
app.get("/api/version", (req, res) => {
|
|
96
96
|
const storage = getStorageInfo();
|
|
97
|
-
|
|
97
|
+
const imapAccounts = imapManager.getAccountCount();
|
|
98
|
+
const dbAccounts = db.getAccounts().length;
|
|
99
|
+
// Warn if DB has accounts but IMAP has none — stale DB, settings missing
|
|
100
|
+
const settingsError = (dbAccounts > 0 && imapAccounts === 0)
|
|
101
|
+
? "No accounts loaded from settings — showing stale data. Check accounts.jsonc on your cloud drive."
|
|
102
|
+
: undefined;
|
|
103
|
+
res.json({ version: SERVER_VERSION, theme: settings.ui?.theme || "system", storage, imapAccounts, settingsError });
|
|
98
104
|
});
|
|
99
105
|
app.all("/info", (req, res) => {
|
|
100
106
|
res.json({ version: SERVER_VERSION, uptime: Math.round(process.uptime()), port: PORT, imap: imapManager.useNativeClient ? "native" : "imapflow" });
|
|
@@ -15,6 +15,12 @@ export declare class MailxDB {
|
|
|
15
15
|
email: string;
|
|
16
16
|
lastSync: number;
|
|
17
17
|
}[];
|
|
18
|
+
getAccountConfigs(): {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
email: string;
|
|
22
|
+
configJson: string;
|
|
23
|
+
}[];
|
|
18
24
|
updateLastSync(accountId: string, timestamp: number): void;
|
|
19
25
|
upsertFolder(accountId: string, folderPath: string, name: string, specialUse: string, delimiter: string): number;
|
|
20
26
|
getFolders(accountId: string): Folder[];
|
|
@@ -137,6 +137,9 @@ export class MailxDB {
|
|
|
137
137
|
getAccounts() {
|
|
138
138
|
return this.db.prepare("SELECT id, name, email, last_sync as lastSync FROM accounts").all();
|
|
139
139
|
}
|
|
140
|
+
getAccountConfigs() {
|
|
141
|
+
return this.db.prepare("SELECT id, name, email, config_json as configJson FROM accounts").all();
|
|
142
|
+
}
|
|
140
143
|
updateLastSync(accountId, timestamp) {
|
|
141
144
|
this.db.prepare("UPDATE accounts SET last_sync = ? WHERE id = ?").run(timestamp, accountId);
|
|
142
145
|
}
|