@bobfrankston/mailx 1.0.138 → 1.0.140
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 +106 -207
- package/package.json +4 -4
- package/packages/mailx-api/index.js +1 -1
- package/packages/mailx-server/index.js +3 -2
- package/packages/mailx-settings/cloud.d.ts +16 -4
- package/packages/mailx-settings/cloud.js +95 -84
- package/packages/mailx-settings/index.d.ts +3 -2
- package/packages/mailx-settings/index.js +39 -14
- package/launcher/bin/mailx-app-linux +0 -0
- package/launcher/bin/mailx-app.exe +0 -0
- package/launcher/builder/build-config.json +0 -11
- package/launcher/builder/postinstall.js +0 -73
- package/launcher/mailx.ico +0 -0
package/bin/mailx.js
CHANGED
|
@@ -3,17 +3,23 @@
|
|
|
3
3
|
* mailx -- email client
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
|
-
* mailx
|
|
7
|
-
* mailx --server Start
|
|
8
|
-
* mailx --no-browser
|
|
9
|
-
* mailx --external Bind to all interfaces
|
|
6
|
+
* mailx Start server + open in msger
|
|
7
|
+
* mailx --server Start server + open msger (same as default)
|
|
8
|
+
* mailx --no-browser Start server only (headless)
|
|
9
|
+
* mailx --external Bind to all interfaces
|
|
10
10
|
* mailx --verbose Show detailed startup info
|
|
11
11
|
* mailx -v / --version Show version and exit
|
|
12
|
+
* mailx -kill Kill running mailx processes
|
|
13
|
+
* mailx -setup Interactive first-time setup
|
|
14
|
+
* mailx -test Test account connectivity
|
|
15
|
+
* mailx -rebuild Wipe local cache, keep accounts
|
|
12
16
|
*/
|
|
13
17
|
import fs from "node:fs";
|
|
14
18
|
import path from "node:path";
|
|
15
19
|
import net from "node:net";
|
|
16
|
-
|
|
20
|
+
import { ports } from "@bobfrankston/miscinfo";
|
|
21
|
+
import { showMessageBox } from "@bobfrankston/msger";
|
|
22
|
+
const PORT = ports.mailx;
|
|
17
23
|
const args = process.argv.slice(2);
|
|
18
24
|
// Normalize: accept both -flag and --flag
|
|
19
25
|
function hasFlag(name) { return args.includes(`-${name}`) || args.includes(`--${name}`); }
|
|
@@ -76,13 +82,6 @@ if (hasFlag("kill")) {
|
|
|
76
82
|
}
|
|
77
83
|
}
|
|
78
84
|
catch { /* */ }
|
|
79
|
-
// Kill mailx-app.exe
|
|
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
85
|
}
|
|
87
86
|
else {
|
|
88
87
|
try {
|
|
@@ -102,13 +101,6 @@ if (hasFlag("kill")) {
|
|
|
102
101
|
}
|
|
103
102
|
catch { /* */ }
|
|
104
103
|
}
|
|
105
|
-
// Remove lock file
|
|
106
|
-
const lockPath = path.join(mailxDir, "mailx-app.lock");
|
|
107
|
-
try {
|
|
108
|
-
fs.unlinkSync(lockPath);
|
|
109
|
-
log("Removed lock file");
|
|
110
|
-
}
|
|
111
|
-
catch { /* */ }
|
|
112
104
|
if (killed === 0)
|
|
113
105
|
console.log("No mailx processes found");
|
|
114
106
|
process.exit(0);
|
|
@@ -156,7 +148,7 @@ if (hasFlag("v") || hasFlag("version")) {
|
|
|
156
148
|
};
|
|
157
149
|
try {
|
|
158
150
|
const pkg = JSON.parse(fs.readFileSync(`${root}/package.json`, "utf-8"));
|
|
159
|
-
console.log(
|
|
151
|
+
console.log(`\x1b[1;97;44m mailx v${pkg.version} \x1b[0m`);
|
|
160
152
|
}
|
|
161
153
|
catch {
|
|
162
154
|
console.log("mailx (version unknown)");
|
|
@@ -178,59 +170,24 @@ function isPortInUse(port) {
|
|
|
178
170
|
socket.once("error", () => resolve(false));
|
|
179
171
|
});
|
|
180
172
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
exec(`start "" "${url}"`);
|
|
185
|
-
else if (process.platform === "darwin")
|
|
186
|
-
exec(`open "${url}"`);
|
|
187
|
-
else
|
|
188
|
-
exec(`xdg-open "${url}"`);
|
|
189
|
-
});
|
|
173
|
+
/** Launch msger pointing at the server URL */
|
|
174
|
+
function launchMsger(url) {
|
|
175
|
+
showMessageBox({ url, detach: true });
|
|
190
176
|
}
|
|
191
177
|
async function prompt(question) {
|
|
192
178
|
const readline = await import("readline");
|
|
193
179
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
194
180
|
return new Promise(resolve => rl.question(question, (ans) => { rl.close(); resolve(ans.trim()); }));
|
|
195
181
|
}
|
|
196
|
-
/**
|
|
197
|
-
function findCloudSettings() {
|
|
198
|
-
const home = process.env.USERPROFILE || process.env.HOME || "";
|
|
199
|
-
const checks = [
|
|
200
|
-
// OneDrive
|
|
201
|
-
{ provider: "onedrive", dir: process.env.OneDrive ? path.join(process.env.OneDrive, "home", ".mailx") : "" },
|
|
202
|
-
{ provider: "onedrive", dir: path.join(home, "OneDrive", "home", ".mailx") },
|
|
203
|
-
{ provider: "onedrive", dir: path.join(home, "onedrive", "home", ".mailx") },
|
|
204
|
-
// Google Drive
|
|
205
|
-
{ provider: "gdrive", dir: path.join(home, "Google Drive", "My Drive", "home", ".mailx") },
|
|
206
|
-
{ provider: "gdrive", dir: path.join(home, "Google Drive Streaming", "My Drive", "home", ".mailx") },
|
|
207
|
-
// Dropbox
|
|
208
|
-
{ provider: "dropbox", dir: path.join(home, "Dropbox", "home", ".mailx") },
|
|
209
|
-
];
|
|
210
|
-
// Drive letters on Windows
|
|
211
|
-
if (process.platform === "win32") {
|
|
212
|
-
for (const letter of ["G", "H", "I", "J", "K"]) {
|
|
213
|
-
checks.push({ provider: "gdrive", dir: path.join(`${letter}:`, "My Drive", "home", ".mailx") });
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
for (const c of checks) {
|
|
217
|
-
if (c.dir && fs.existsSync(c.dir) && (fs.existsSync(path.join(c.dir, "settings.jsonc")) || fs.existsSync(path.join(c.dir, "accounts.jsonc")))) {
|
|
218
|
-
return c;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return null;
|
|
222
|
-
}
|
|
223
|
-
/** Check if mailx is configured (has accounts) */
|
|
182
|
+
/** Check if mailx is configured (has local config or accounts) */
|
|
224
183
|
function hasConfig() {
|
|
225
184
|
const home = process.env.USERPROFILE || process.env.HOME || "";
|
|
226
185
|
const mailxDir = path.join(home, ".mailx");
|
|
227
|
-
|
|
228
|
-
for (const f of ["settings.jsonc", "accounts.jsonc", "config.jsonc"]) {
|
|
186
|
+
for (const f of ["config.jsonc", "accounts.jsonc", "settings.jsonc"]) {
|
|
229
187
|
if (fs.existsSync(path.join(mailxDir, f)))
|
|
230
188
|
return true;
|
|
231
189
|
}
|
|
232
|
-
|
|
233
|
-
return findCloudSettings() !== null;
|
|
190
|
+
return false;
|
|
234
191
|
}
|
|
235
192
|
/** Try to auto-discover mail server settings from email domain */
|
|
236
193
|
async function autoDiscover(domain) {
|
|
@@ -265,6 +222,29 @@ async function autoDiscover(domain) {
|
|
|
265
222
|
}
|
|
266
223
|
}
|
|
267
224
|
catch { /* DNS failed */ }
|
|
225
|
+
// 3. Try MX-based detection (Google Workspace, Microsoft 365)
|
|
226
|
+
try {
|
|
227
|
+
const dns = await import("node:dns/promises");
|
|
228
|
+
const records = await dns.resolveMx(domain);
|
|
229
|
+
for (const mx of records) {
|
|
230
|
+
const host = mx.exchange.toLowerCase();
|
|
231
|
+
if (host.endsWith(".google.com") || host.endsWith(".googlemail.com")) {
|
|
232
|
+
return {
|
|
233
|
+
imap: { host: "imap.gmail.com", port: 993, auth: "oauth2" },
|
|
234
|
+
smtp: { host: "smtp.gmail.com", port: 587, auth: "oauth2" },
|
|
235
|
+
source: `MX → Google (${host})`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (host.endsWith(".outlook.com") || host.endsWith(".protection.outlook.com")) {
|
|
239
|
+
return {
|
|
240
|
+
imap: { host: "outlook.office365.com", port: 993, auth: "oauth2" },
|
|
241
|
+
smtp: { host: "smtp.office365.com", port: 587, auth: "oauth2" },
|
|
242
|
+
source: `MX → Microsoft (${host})`,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch { /* DNS failed */ }
|
|
268
248
|
return null;
|
|
269
249
|
}
|
|
270
250
|
/** Prompt user for account details, auto-discover where possible */
|
|
@@ -307,44 +287,15 @@ async function promptForAccount(intro) {
|
|
|
307
287
|
smtp: { host: smtpHost },
|
|
308
288
|
};
|
|
309
289
|
}
|
|
310
|
-
/**
|
|
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
|
-
}
|
|
327
|
-
/** Interactive first-time setup */
|
|
290
|
+
/** Interactive first-time setup — GDrive API for cloud storage */
|
|
328
291
|
async function runSetup() {
|
|
329
292
|
console.log("\nmailx — first-time setup\n");
|
|
330
293
|
const home = process.env.USERPROFILE || process.env.HOME || "";
|
|
331
294
|
const mailxDir = path.join(home, ".mailx");
|
|
332
|
-
|
|
333
|
-
const
|
|
334
|
-
if (cloud) {
|
|
335
|
-
console.log(`Found existing settings on ${cloud.provider}: ${cloud.dir}`);
|
|
336
|
-
fs.mkdirSync(mailxDir, { recursive: true });
|
|
337
|
-
const config = { sharedDir: { provider: cloud.provider, path: "home/.mailx" } };
|
|
338
|
-
fs.writeFileSync(path.join(mailxDir, "config.jsonc"), JSON.stringify(config, null, 2));
|
|
339
|
-
console.log(`Created ~/.mailx/config.jsonc pointing to ${cloud.provider}`);
|
|
340
|
-
console.log("Setup complete. Starting mailx...\n");
|
|
341
|
-
return true;
|
|
342
|
-
}
|
|
343
|
-
// No cloud settings found — prompt for email
|
|
344
|
-
console.log("No existing settings found on OneDrive, Google Drive, or Dropbox.\n");
|
|
345
|
-
const account = await promptForAccount("Gmail is recommended as the first account (provides contacts + cloud sync).\n");
|
|
295
|
+
console.log("Gmail is recommended as the first account (provides contacts, calendar, and cloud settings sync).\n");
|
|
296
|
+
const account = await promptForAccount();
|
|
346
297
|
if (!account) {
|
|
347
|
-
console.log(`\
|
|
298
|
+
console.log(`\nNo account added. Open http://127.0.0.1:${PORT} in a browser to set up via the UI.`);
|
|
348
299
|
fs.mkdirSync(mailxDir, { recursive: true });
|
|
349
300
|
return false;
|
|
350
301
|
}
|
|
@@ -356,43 +307,47 @@ async function runSetup() {
|
|
|
356
307
|
sync: { intervalMinutes: 5, historyDays: 0 },
|
|
357
308
|
};
|
|
358
309
|
const domain = account.email.split("@")[1]?.toLowerCase() || "";
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
310
|
+
// Detect if this is a Google-hosted domain (for GDrive cloud storage)
|
|
311
|
+
let isGoogle = ["gmail.com", "googlemail.com"].includes(domain);
|
|
312
|
+
if (!isGoogle) {
|
|
313
|
+
try {
|
|
314
|
+
const dns = await import("node:dns/promises");
|
|
315
|
+
const records = await dns.resolveMx(domain);
|
|
316
|
+
isGoogle = records.some(mx => {
|
|
317
|
+
const host = mx.exchange.toLowerCase();
|
|
318
|
+
return host.endsWith(".google.com") || host.endsWith(".googlemail.com");
|
|
319
|
+
});
|
|
320
|
+
if (isGoogle)
|
|
321
|
+
console.log(` ${domain} is hosted on Google (detected via MX)`);
|
|
322
|
+
}
|
|
323
|
+
catch { /* DNS lookup failed */ }
|
|
372
324
|
}
|
|
373
325
|
fs.mkdirSync(mailxDir, { recursive: true });
|
|
374
|
-
if (
|
|
375
|
-
// Save to Google Drive via API (
|
|
326
|
+
if (isGoogle) {
|
|
327
|
+
// Save to Google Drive via API (folder-ID based, no path navigation)
|
|
376
328
|
console.log("\nSaving settings to Google Drive via API...");
|
|
377
329
|
try {
|
|
378
|
-
const { getCloudProvider } = await import("
|
|
379
|
-
const
|
|
380
|
-
if (
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
330
|
+
const { gDriveFindOrCreateFolder, getCloudProvider } = await import("@bobfrankston/mailx-settings/cloud.js");
|
|
331
|
+
const folderId = await gDriveFindOrCreateFolder();
|
|
332
|
+
if (folderId) {
|
|
333
|
+
const gdrive = getCloudProvider("gdrive", folderId);
|
|
334
|
+
if (gdrive) {
|
|
335
|
+
const content = JSON.stringify(settings, null, 2);
|
|
336
|
+
const ok = await gdrive.write("settings.jsonc", content);
|
|
337
|
+
if (ok) {
|
|
338
|
+
console.log("Settings saved to Google Drive (mailx folder)");
|
|
339
|
+
const config = { sharedDir: { provider: "gdrive", path: "mailx", folderId } };
|
|
340
|
+
fs.writeFileSync(path.join(mailxDir, "config.jsonc"), JSON.stringify(config, null, 2));
|
|
341
|
+
console.log("Local config created with Drive folder ID.");
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
console.log("Google Drive write failed — saving locally instead.");
|
|
345
|
+
fs.writeFileSync(path.join(mailxDir, "settings.jsonc"), JSON.stringify(settings, null, 2));
|
|
346
|
+
}
|
|
392
347
|
}
|
|
393
348
|
}
|
|
394
349
|
else {
|
|
395
|
-
console.log("Google Drive
|
|
350
|
+
console.log("Google Drive folder setup failed — saving locally.");
|
|
396
351
|
fs.writeFileSync(path.join(mailxDir, "settings.jsonc"), JSON.stringify(settings, null, 2));
|
|
397
352
|
}
|
|
398
353
|
}
|
|
@@ -401,17 +356,8 @@ async function runSetup() {
|
|
|
401
356
|
fs.writeFileSync(path.join(mailxDir, "settings.jsonc"), JSON.stringify(settings, null, 2));
|
|
402
357
|
}
|
|
403
358
|
}
|
|
404
|
-
else if (storageChoice !== "local" && mountedDrive) {
|
|
405
|
-
// Save to mounted cloud drive
|
|
406
|
-
console.log(`\nSaving settings to ${mountedDrive.provider} at ${mountedDrive.dir}...`);
|
|
407
|
-
fs.mkdirSync(mountedDrive.dir, { recursive: true });
|
|
408
|
-
fs.writeFileSync(path.join(mountedDrive.dir, "settings.jsonc"), JSON.stringify(settings, null, 2));
|
|
409
|
-
const config = { sharedDir: { provider: mountedDrive.provider, path: "home/.mailx" } };
|
|
410
|
-
fs.writeFileSync(path.join(mailxDir, "config.jsonc"), JSON.stringify(config, null, 2));
|
|
411
|
-
console.log("Settings saved to cloud drive + local config created.");
|
|
412
|
-
}
|
|
413
359
|
else {
|
|
414
|
-
//
|
|
360
|
+
// Non-Google account — save locally
|
|
415
361
|
console.log(`\nSaving settings to ${mailxDir}...`);
|
|
416
362
|
fs.writeFileSync(path.join(mailxDir, "settings.jsonc"), JSON.stringify(settings, null, 2));
|
|
417
363
|
}
|
|
@@ -507,7 +453,7 @@ async function runTest() {
|
|
|
507
453
|
async function main() {
|
|
508
454
|
log(`Platform: ${process.platform} ${process.arch}`);
|
|
509
455
|
log(`Node: ${process.version}`);
|
|
510
|
-
log(`Mode: ${serverMode ? "server" : setupMode ? "setup" : "auto
|
|
456
|
+
log(`Mode: ${serverMode ? "server" : setupMode ? "setup" : "auto"}`);
|
|
511
457
|
// Test connectivity
|
|
512
458
|
if (testMode) {
|
|
513
459
|
await runTest();
|
|
@@ -537,87 +483,40 @@ async function main() {
|
|
|
537
483
|
process.exit(0);
|
|
538
484
|
}
|
|
539
485
|
// Auto-detect first run — enter setup if no config exists
|
|
540
|
-
if (setupMode ||
|
|
486
|
+
if (setupMode || !hasConfig()) {
|
|
541
487
|
if (!setupMode)
|
|
542
488
|
console.log("No mailx configuration found.");
|
|
543
489
|
await runSetup();
|
|
544
490
|
}
|
|
545
|
-
if
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
if (!noBrowser)
|
|
551
|
-
openBrowser(`http://127.0.0.1:${PORT}`);
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
554
|
-
console.log("Starting mailx server...");
|
|
555
|
-
log(`Loading server from: ${path.join(import.meta.dirname, "..", "packages", "mailx-server", "index.js")}`);
|
|
556
|
-
if (hasFlag("external"))
|
|
557
|
-
process.argv.push("--external");
|
|
558
|
-
await import("../packages/mailx-server/index.js");
|
|
491
|
+
// Check if server is already running
|
|
492
|
+
const inUse = await isPortInUse(PORT);
|
|
493
|
+
if (inUse) {
|
|
494
|
+
console.log(`mailx server already running on port ${PORT}`);
|
|
495
|
+
const url = `http://127.0.0.1:${PORT}`;
|
|
559
496
|
if (!noBrowser) {
|
|
560
|
-
|
|
561
|
-
await new Promise(r => setTimeout(r, 200));
|
|
562
|
-
if (await isPortInUse(PORT))
|
|
563
|
-
break;
|
|
564
|
-
}
|
|
565
|
-
openBrowser(`http://127.0.0.1:${PORT}`);
|
|
566
|
-
console.log("mailx opened (browser)");
|
|
497
|
+
launchMsger(url);
|
|
567
498
|
}
|
|
568
|
-
|
|
569
|
-
await new Promise(() => { });
|
|
499
|
+
return;
|
|
570
500
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
log(`Looking for native launcher: ${binaryName}`);
|
|
585
|
-
for (const p of launcherPaths)
|
|
586
|
-
log(` ${fs.existsSync(p) ? "FOUND" : "not found"}: ${p}`);
|
|
587
|
-
let launcherPath = launcherPaths.find(p => fs.existsSync(p));
|
|
588
|
-
// On Linux, skip native launcher if no display server available
|
|
589
|
-
if (launcherPath && process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
|
|
590
|
-
log("No display server (DISPLAY/WAYLAND_DISPLAY not set) — skipping native launcher");
|
|
591
|
-
launcherPath = undefined;
|
|
592
|
-
}
|
|
593
|
-
if (launcherPath) {
|
|
594
|
-
console.log("Starting mailx...");
|
|
595
|
-
log(`Launching: ${launcherPath}`);
|
|
596
|
-
const { spawn } = await import("node:child_process");
|
|
597
|
-
try {
|
|
598
|
-
const child = spawn(launcherPath, args, { detached: true, stdio: "ignore" });
|
|
599
|
-
child.on("error", () => {
|
|
600
|
-
console.log("Native launcher failed, starting in browser mode...");
|
|
601
|
-
process.argv.push("--server");
|
|
602
|
-
main();
|
|
603
|
-
});
|
|
604
|
-
child.unref();
|
|
605
|
-
console.log("mailx launched");
|
|
606
|
-
}
|
|
607
|
-
catch (e) {
|
|
608
|
-
console.log(`Native launcher failed: ${e.message}`);
|
|
609
|
-
console.log("Starting in browser mode...");
|
|
610
|
-
process.argv.push("--server");
|
|
611
|
-
await main();
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
else {
|
|
615
|
-
console.log("Starting in browser mode...");
|
|
616
|
-
log("No native launcher — falling back to --server mode");
|
|
617
|
-
process.argv.push("--server");
|
|
618
|
-
await main(); // recurse with --server
|
|
501
|
+
// Start Express server in-process
|
|
502
|
+
console.log("Starting mailx server...");
|
|
503
|
+
log(`Loading server from: ${path.join(import.meta.dirname, "..", "packages", "mailx-server", "index.js")}`);
|
|
504
|
+
if (hasFlag("external"))
|
|
505
|
+
process.argv.push("--external");
|
|
506
|
+
await import("../packages/mailx-server/index.js");
|
|
507
|
+
// Open UI once server is ready
|
|
508
|
+
if (!noBrowser) {
|
|
509
|
+
const url = `http://127.0.0.1:${PORT}`;
|
|
510
|
+
for (let i = 0; i < 30; i++) {
|
|
511
|
+
await new Promise(r => setTimeout(r, 200));
|
|
512
|
+
if (await isPortInUse(PORT))
|
|
513
|
+
break;
|
|
619
514
|
}
|
|
515
|
+
launchMsger(url);
|
|
516
|
+
console.log("mailx opened");
|
|
620
517
|
}
|
|
518
|
+
// Keep process alive — server is running
|
|
519
|
+
await new Promise(() => { });
|
|
621
520
|
}
|
|
622
521
|
main().catch(console.error);
|
|
623
522
|
//# sourceMappingURL=mailx.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.140",
|
|
4
4
|
"description": "Local-first email client with IMAP sync and standalone native app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/mailx.js",
|
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
"start": "node --watch packages/mailx-server/index.js",
|
|
18
18
|
"start:prod": "node packages/mailx-server/index.js",
|
|
19
19
|
"release": "npmglobalize",
|
|
20
|
-
"postinstall": "node
|
|
20
|
+
"postinstall": "node bin/postinstall.js"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@bobfrankston/iflow": "^1.0.52",
|
|
24
24
|
"@bobfrankston/miscinfo": "^1.0.7",
|
|
25
25
|
"@bobfrankston/oauthsupport": "^1.0.20",
|
|
26
|
-
"@bobfrankston/
|
|
26
|
+
"@bobfrankston/msger": "^0.1.195",
|
|
27
27
|
"@capacitor/android": "^8.3.0",
|
|
28
28
|
"@capacitor/cli": "^8.3.0",
|
|
29
29
|
"@capacitor/core": "^8.3.0",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"@bobfrankston/iflow": "file:../MailApps/iflow",
|
|
58
58
|
"@bobfrankston/miscinfo": "file:../../projects/npm/miscinfo",
|
|
59
59
|
"@bobfrankston/oauthsupport": "file:../../projects/oauth/oauthsupport",
|
|
60
|
-
"@bobfrankston/
|
|
60
|
+
"@bobfrankston/msger": "file:../../utils/msgx/msger",
|
|
61
61
|
"@capacitor/android": "^8.3.0",
|
|
62
62
|
"@capacitor/cli": "^8.3.0",
|
|
63
63
|
"@capacitor/core": "^8.3.0",
|
|
@@ -148,7 +148,7 @@ export function createApiRouter(db, imapManager) {
|
|
|
148
148
|
const domain = email.split("@")[1]?.toLowerCase() || "";
|
|
149
149
|
const detected = await detectEmailProvider(domain);
|
|
150
150
|
if (detected?.cloud) {
|
|
151
|
-
initCloudConfig(detected.cloud);
|
|
151
|
+
await initCloudConfig(detected.cloud);
|
|
152
152
|
}
|
|
153
153
|
// Build account config
|
|
154
154
|
const account = { email, name: name || email.split("@")[0] };
|
|
@@ -9,7 +9,7 @@ import * as fs from "node:fs";
|
|
|
9
9
|
import { MailxDB } from "@bobfrankston/mailx-store";
|
|
10
10
|
import { ImapManager } from "@bobfrankston/mailx-imap";
|
|
11
11
|
import { createApiRouter } from "@bobfrankston/mailx-api";
|
|
12
|
-
import { loadSettings, loadAccountsAsync, getConfigDir, getStorePath, getStorageInfo
|
|
12
|
+
import { loadSettings, loadAccountsAsync, getConfigDir, getStorePath, getStorageInfo } from "@bobfrankston/mailx-settings";
|
|
13
13
|
import { ports } from "@bobfrankston/miscinfo";
|
|
14
14
|
import { createServer } from "node:http";
|
|
15
15
|
const PORT = ports.mailx;
|
|
@@ -45,7 +45,8 @@ console.error = (...args) => {
|
|
|
45
45
|
const rootPkg = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, "..", "..", "package.json"), "utf-8"));
|
|
46
46
|
const SERVER_VERSION = rootPkg.version;
|
|
47
47
|
// ── Initialize ──
|
|
48
|
-
initLocalConfig()
|
|
48
|
+
// No initLocalConfig() here — config is created when user adds an account (initCloudConfig).
|
|
49
|
+
// On fresh install, loadSettings() returns defaults and the setup form handles configuration.
|
|
49
50
|
let settings = loadSettings();
|
|
50
51
|
if (settings.accounts.length === 0) {
|
|
51
52
|
// Try cloud API fallback (Google Drive, OneDrive) if filesystem mount not available
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Cloud storage for mailx settings — Google Drive API.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Cloud storage for mailx settings — Google Drive API (drive.file scope).
|
|
3
|
+
*
|
|
4
|
+
* Uses a single app-owned "mailx" folder on Drive, accessed by folder ID.
|
|
5
|
+
* The drive.file scope only sees files/folders created by this OAuth client,
|
|
6
|
+
* which prevents conflicts with existing folders of the same name.
|
|
7
|
+
* All machines using the same OAuth client ID share the same folder.
|
|
5
8
|
*
|
|
6
9
|
* ── Restoring removed providers ──
|
|
7
10
|
* OneDrive (removed 2026-04-06): Required microsoft-credentials.json in ~/.mailx/,
|
|
@@ -13,11 +16,20 @@
|
|
|
13
16
|
* Dropbox (removed 2026-04-06): Never implemented — placeholder only.
|
|
14
17
|
* Would need Dropbox OAuth app, token management, and Dropbox API v2 calls.
|
|
15
18
|
*/
|
|
19
|
+
/** Find the app-owned "mailx" folder, or create it. Returns folder ID. */
|
|
20
|
+
export declare function gDriveFindOrCreateFolder(): Promise<string | null>;
|
|
16
21
|
export type CloudProvider = "gdrive" | "google" | "local";
|
|
17
22
|
export interface CloudFile {
|
|
23
|
+
/** Read a file. For gdrive, path is just the filename (folder ID is implicit). */
|
|
18
24
|
read(filePath: string): Promise<string | null>;
|
|
25
|
+
/** Write a file. For gdrive, path is just the filename. */
|
|
19
26
|
write(filePath: string, content: string): Promise<boolean>;
|
|
27
|
+
/** Check if a file exists. */
|
|
20
28
|
exists(filePath: string): Promise<boolean>;
|
|
21
29
|
}
|
|
22
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Get a cloud file provider. For gdrive, pass the folder ID.
|
|
32
|
+
* Files are stored flat in the folder (no subdirectory navigation).
|
|
33
|
+
*/
|
|
34
|
+
export declare function getCloudProvider(provider: string, folderId?: string): CloudFile | null;
|
|
23
35
|
//# sourceMappingURL=cloud.d.ts.map
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Cloud storage for mailx settings — Google Drive API.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Cloud storage for mailx settings — Google Drive API (drive.file scope).
|
|
3
|
+
*
|
|
4
|
+
* Uses a single app-owned "mailx" folder on Drive, accessed by folder ID.
|
|
5
|
+
* The drive.file scope only sees files/folders created by this OAuth client,
|
|
6
|
+
* which prevents conflicts with existing folders of the same name.
|
|
7
|
+
* All machines using the same OAuth client ID share the same folder.
|
|
5
8
|
*
|
|
6
9
|
* ── Restoring removed providers ──
|
|
7
10
|
* OneDrive (removed 2026-04-06): Required microsoft-credentials.json in ~/.mailx/,
|
|
@@ -24,9 +27,7 @@ function findGoogleCredentials() {
|
|
|
24
27
|
const local = path.join(SETTINGS_DIR, "google-credentials.json");
|
|
25
28
|
if (fs.existsSync(local))
|
|
26
29
|
return local;
|
|
27
|
-
// Try to find iflow's credentials via import.meta.resolve or node_modules walk
|
|
28
30
|
try {
|
|
29
|
-
// Walk up from this package to find iflow in node_modules
|
|
30
31
|
let dir = import.meta.dirname;
|
|
31
32
|
for (let i = 0; i < 5; i++) {
|
|
32
33
|
for (const name of ["iflow-credentials.json", "credentials.json"]) {
|
|
@@ -44,7 +45,10 @@ function findGoogleCredentials() {
|
|
|
44
45
|
return null;
|
|
45
46
|
}
|
|
46
47
|
const GDRIVE_TOKEN_DIR = path.join(SETTINGS_DIR, "tokens", "gdrive");
|
|
48
|
+
// drive.file: app can only see files it created. Safe, publishable without security audit.
|
|
49
|
+
// All machines sharing the same OAuth client ID see the same files.
|
|
47
50
|
const GDRIVE_SCOPES = "https://www.googleapis.com/auth/drive.file";
|
|
51
|
+
const GDRIVE_FOLDER_NAME = "mailx";
|
|
48
52
|
// ── Token helpers ──
|
|
49
53
|
async function getGoogleDriveToken() {
|
|
50
54
|
const creds = findGoogleCredentials();
|
|
@@ -52,6 +56,18 @@ async function getGoogleDriveToken() {
|
|
|
52
56
|
console.error(" [cloud] No Google credentials found (checked ~/.mailx/google-credentials.json and iflow package)");
|
|
53
57
|
return null;
|
|
54
58
|
}
|
|
59
|
+
// Delete stale token if scope changed (drive → drive.file or vice versa)
|
|
60
|
+
const tokenPath = path.join(GDRIVE_TOKEN_DIR, "token.json");
|
|
61
|
+
if (fs.existsSync(tokenPath)) {
|
|
62
|
+
try {
|
|
63
|
+
const existing = JSON.parse(fs.readFileSync(tokenPath, "utf-8"));
|
|
64
|
+
if (existing.scope && existing.scope !== GDRIVE_SCOPES) {
|
|
65
|
+
console.log(` [cloud] Scope changed — re-authenticating...`);
|
|
66
|
+
fs.unlinkSync(tokenPath);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch { /* ignore parse errors */ }
|
|
70
|
+
}
|
|
55
71
|
try {
|
|
56
72
|
const token = await authenticateOAuth(creds, {
|
|
57
73
|
scope: GDRIVE_SCOPES,
|
|
@@ -67,111 +83,97 @@ async function getGoogleDriveToken() {
|
|
|
67
83
|
return null;
|
|
68
84
|
}
|
|
69
85
|
}
|
|
70
|
-
// ── Google Drive API ──
|
|
71
|
-
|
|
86
|
+
// ── Google Drive API (folder-ID based) ──
|
|
87
|
+
/** Find the app-owned "mailx" folder, or create it. Returns folder ID. */
|
|
88
|
+
export async function gDriveFindOrCreateFolder() {
|
|
89
|
+
const token = await getGoogleDriveToken();
|
|
90
|
+
if (!token)
|
|
91
|
+
return null;
|
|
92
|
+
try {
|
|
93
|
+
// Search for existing folder (created by this OAuth client)
|
|
94
|
+
const query = `name='${GDRIVE_FOLDER_NAME}' and mimeType='application/vnd.google-apps.folder' and trashed=false`;
|
|
95
|
+
const res = await fetch(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id,name)`, {
|
|
96
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
97
|
+
});
|
|
98
|
+
if (!res.ok) {
|
|
99
|
+
console.error(` [cloud] gdrive folder search: ${res.status} ${res.statusText}`);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const data = await res.json();
|
|
103
|
+
if (data.files?.[0]) {
|
|
104
|
+
console.log(` [cloud] Found existing '${GDRIVE_FOLDER_NAME}' folder: ${data.files[0].id}`);
|
|
105
|
+
return data.files[0].id;
|
|
106
|
+
}
|
|
107
|
+
// Create folder
|
|
108
|
+
const createRes = await fetch("https://www.googleapis.com/drive/v3/files", {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
111
|
+
body: JSON.stringify({ name: GDRIVE_FOLDER_NAME, mimeType: "application/vnd.google-apps.folder" }),
|
|
112
|
+
});
|
|
113
|
+
if (!createRes.ok) {
|
|
114
|
+
console.error(` [cloud] gdrive folder create: ${createRes.status} ${createRes.statusText}`);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const created = await createRes.json();
|
|
118
|
+
console.log(` [cloud] Created '${GDRIVE_FOLDER_NAME}' folder: ${created.id}`);
|
|
119
|
+
return created.id;
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
console.error(` [cloud] gdrive folder setup: ${e.message}`);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Read a file by name from a folder (by ID) */
|
|
127
|
+
async function gDriveReadFromFolder(folderId, fileName) {
|
|
72
128
|
const token = await getGoogleDriveToken();
|
|
73
129
|
if (!token) {
|
|
74
|
-
console.error(` [cloud] gdrive read ${
|
|
130
|
+
console.error(` [cloud] gdrive read ${fileName}: no token`);
|
|
75
131
|
return null;
|
|
76
132
|
}
|
|
77
133
|
try {
|
|
78
|
-
//
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
let parentId = null;
|
|
82
|
-
// Navigate folder structure
|
|
83
|
-
for (const folder of parts) {
|
|
84
|
-
let query = `name='${folder}' and mimeType='application/vnd.google-apps.folder' and trashed=false`;
|
|
85
|
-
if (parentId)
|
|
86
|
-
query += ` and '${parentId}' in parents`;
|
|
87
|
-
const res = await fetch(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id)`, {
|
|
88
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
89
|
-
});
|
|
90
|
-
if (!res.ok) {
|
|
91
|
-
console.error(` [cloud] gdrive folder lookup '${folder}': ${res.status} ${res.statusText}`);
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
const data = await res.json();
|
|
95
|
-
if (!data.files?.[0]) {
|
|
96
|
-
console.error(` [cloud] gdrive folder '${folder}' not found (drive.file scope can only see app-created files)`);
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
parentId = data.files[0].id;
|
|
100
|
-
}
|
|
101
|
-
// Find the file
|
|
102
|
-
let query = `name='${fileName}' and trashed=false`;
|
|
103
|
-
if (parentId)
|
|
104
|
-
query += ` and '${parentId}' in parents`;
|
|
105
|
-
const fileRes = await fetch(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id)`, {
|
|
134
|
+
// Find file in folder
|
|
135
|
+
const query = `name='${fileName}' and '${folderId}' in parents and trashed=false`;
|
|
136
|
+
const res = await fetch(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id)`, {
|
|
106
137
|
headers: { Authorization: `Bearer ${token}` },
|
|
107
138
|
});
|
|
108
|
-
if (!
|
|
109
|
-
console.error(` [cloud] gdrive
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
console.error(` [cloud] gdrive lookup '${fileName}': ${res.status}`);
|
|
110
141
|
return null;
|
|
111
142
|
}
|
|
112
|
-
const
|
|
113
|
-
const fileId =
|
|
114
|
-
if (!fileId)
|
|
115
|
-
console.error(` [cloud] gdrive file '${fileName}' not found in ${parts.join("/")}`);
|
|
143
|
+
const data = await res.json();
|
|
144
|
+
const fileId = data.files?.[0]?.id;
|
|
145
|
+
if (!fileId)
|
|
116
146
|
return null;
|
|
117
|
-
}
|
|
118
147
|
// Download content
|
|
119
148
|
const contentRes = await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`, {
|
|
120
149
|
headers: { Authorization: `Bearer ${token}` },
|
|
121
150
|
});
|
|
122
151
|
if (!contentRes.ok) {
|
|
123
|
-
console.error(` [cloud] gdrive download ${
|
|
152
|
+
console.error(` [cloud] gdrive download ${fileName}: ${contentRes.status}`);
|
|
124
153
|
return null;
|
|
125
154
|
}
|
|
126
155
|
return await contentRes.text();
|
|
127
156
|
}
|
|
128
157
|
catch (e) {
|
|
129
|
-
console.error(` [cloud] gdrive read ${
|
|
158
|
+
console.error(` [cloud] gdrive read ${fileName}: ${e.message}`);
|
|
130
159
|
return null;
|
|
131
160
|
}
|
|
132
161
|
}
|
|
133
|
-
|
|
162
|
+
/** Write a file by name to a folder (by ID) — creates or updates */
|
|
163
|
+
async function gDriveWriteToFolder(folderId, fileName, content) {
|
|
134
164
|
const token = await getGoogleDriveToken();
|
|
135
165
|
if (!token)
|
|
136
166
|
return false;
|
|
137
167
|
try {
|
|
138
|
-
|
|
139
|
-
const fileName =
|
|
140
|
-
// Ensure folder structure exists
|
|
141
|
-
let parentId = null;
|
|
142
|
-
for (const folder of parts) {
|
|
143
|
-
let query = `name='${folder}' and mimeType='application/vnd.google-apps.folder' and trashed=false`;
|
|
144
|
-
if (parentId)
|
|
145
|
-
query += ` and '${parentId}' in parents`;
|
|
146
|
-
const res = await fetch(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id)`, {
|
|
147
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
148
|
-
});
|
|
149
|
-
const data = await res.json();
|
|
150
|
-
if (data.files?.[0]) {
|
|
151
|
-
parentId = data.files[0].id;
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
// Create folder
|
|
155
|
-
const createRes = await fetch("https://www.googleapis.com/drive/v3/files", {
|
|
156
|
-
method: "POST",
|
|
157
|
-
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
158
|
-
body: JSON.stringify({ name: folder, mimeType: "application/vnd.google-apps.folder", parents: parentId ? [parentId] : [] }),
|
|
159
|
-
});
|
|
160
|
-
const created = await createRes.json();
|
|
161
|
-
parentId = created.id;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
// Check if file exists
|
|
165
|
-
let query = `name='${fileName}' and trashed=false`;
|
|
166
|
-
if (parentId)
|
|
167
|
-
query += ` and '${parentId}' in parents`;
|
|
168
|
+
// Check if file exists in folder
|
|
169
|
+
const query = `name='${fileName}' and '${folderId}' in parents and trashed=false`;
|
|
168
170
|
const findRes = await fetch(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id)`, {
|
|
169
171
|
headers: { Authorization: `Bearer ${token}` },
|
|
170
172
|
});
|
|
171
173
|
const findData = await findRes.json();
|
|
172
174
|
const existingId = findData.files?.[0]?.id;
|
|
173
175
|
if (existingId) {
|
|
174
|
-
// Update existing
|
|
176
|
+
// Update existing file
|
|
175
177
|
const res = await fetch(`https://www.googleapis.com/upload/drive/v3/files/${existingId}?uploadType=media`, {
|
|
176
178
|
method: "PATCH",
|
|
177
179
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
@@ -180,9 +182,9 @@ async function gDriveWrite(filePath, content) {
|
|
|
180
182
|
return res.ok;
|
|
181
183
|
}
|
|
182
184
|
else {
|
|
183
|
-
// Create new
|
|
185
|
+
// Create new file in folder
|
|
184
186
|
const boundary = "mailx_boundary_" + Date.now();
|
|
185
|
-
const metadata = JSON.stringify({ name: fileName, parents:
|
|
187
|
+
const metadata = JSON.stringify({ name: fileName, parents: [folderId] });
|
|
186
188
|
const body = `--${boundary}\r\nContent-Type: application/json\r\n\r\n${metadata}\r\n--${boundary}\r\nContent-Type: application/json\r\n\r\n${content}\r\n--${boundary}--`;
|
|
187
189
|
const res = await fetch("https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart", {
|
|
188
190
|
method: "POST",
|
|
@@ -192,18 +194,27 @@ async function gDriveWrite(filePath, content) {
|
|
|
192
194
|
return res.ok;
|
|
193
195
|
}
|
|
194
196
|
}
|
|
195
|
-
catch {
|
|
197
|
+
catch (e) {
|
|
198
|
+
console.error(` [cloud] gdrive write ${fileName}: ${e.message}`);
|
|
196
199
|
return false;
|
|
197
200
|
}
|
|
198
201
|
}
|
|
199
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Get a cloud file provider. For gdrive, pass the folder ID.
|
|
204
|
+
* Files are stored flat in the folder (no subdirectory navigation).
|
|
205
|
+
*/
|
|
206
|
+
export function getCloudProvider(provider, folderId) {
|
|
200
207
|
switch (provider) {
|
|
201
208
|
case "google":
|
|
202
209
|
case "gdrive":
|
|
210
|
+
if (!folderId) {
|
|
211
|
+
console.error(" [cloud] gdrive requires a folder ID — run initCloudConfig first");
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
203
214
|
return {
|
|
204
|
-
read:
|
|
205
|
-
write:
|
|
206
|
-
exists: async (
|
|
215
|
+
read: (fileName) => gDriveReadFromFolder(folderId, fileName),
|
|
216
|
+
write: (fileName, content) => gDriveWriteToFolder(folderId, fileName, content),
|
|
217
|
+
exists: async (fileName) => (await gDriveReadFromFolder(folderId, fileName)) !== null,
|
|
207
218
|
};
|
|
208
219
|
case "local":
|
|
209
220
|
return {
|
|
@@ -90,8 +90,9 @@ export declare function getConfigDir(): string;
|
|
|
90
90
|
export { getSharedDir };
|
|
91
91
|
/** Initialize local config if it doesn't exist */
|
|
92
92
|
export declare function initLocalConfig(sharedDir?: string, storePath?: string): void;
|
|
93
|
-
/** Initialize config with Google Drive cloud storage
|
|
94
|
-
|
|
93
|
+
/** Initialize config with Google Drive cloud storage.
|
|
94
|
+
* Finds or creates the app-owned "mailx" folder on Drive and stores its ID. */
|
|
95
|
+
export declare function initCloudConfig(provider?: "gdrive"): Promise<void>;
|
|
95
96
|
declare const DEFAULT_SETTINGS: MailxSettings;
|
|
96
97
|
/** Get historyDays for an account: per-account override > system override > shared default */
|
|
97
98
|
export declare function getHistoryDays(accountId?: string): number;
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import * as fs from "node:fs";
|
|
19
19
|
import * as path from "node:path";
|
|
20
20
|
import { parse as parseJsonc } from "jsonc-parser";
|
|
21
|
-
import { getCloudProvider } from "./cloud.js";
|
|
21
|
+
import { getCloudProvider, gDriveFindOrCreateFolder } from "./cloud.js";
|
|
22
22
|
// ── Paths ──
|
|
23
23
|
const LOCAL_DIR = path.join(process.env.USERPROFILE || process.env.HOME || ".", ".mailx");
|
|
24
24
|
const LOCAL_CONFIG_PATH = path.join(LOCAL_DIR, "config.jsonc");
|
|
@@ -110,14 +110,19 @@ function getSharedDir() {
|
|
|
110
110
|
export async function cloudRead(filename) {
|
|
111
111
|
if (!pendingCloudConfig)
|
|
112
112
|
return null;
|
|
113
|
-
|
|
113
|
+
// Ensure we have a folder ID
|
|
114
|
+
if (!pendingCloudConfig.folderId) {
|
|
115
|
+
pendingCloudConfig.folderId = await gDriveFindOrCreateFolder() || undefined;
|
|
116
|
+
if (pendingCloudConfig.folderId)
|
|
117
|
+
saveFolderIdToConfig(pendingCloudConfig.folderId);
|
|
118
|
+
}
|
|
119
|
+
const provider = getCloudProvider(pendingCloudConfig.provider, pendingCloudConfig.folderId);
|
|
114
120
|
if (!provider) {
|
|
115
121
|
lastCloudError = `No cloud provider for ${pendingCloudConfig.provider}`;
|
|
116
122
|
return null;
|
|
117
123
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const content = await provider.read(cloudPath);
|
|
124
|
+
console.log(` [cloud] Reading ${filename} via ${pendingCloudConfig.provider} API...`);
|
|
125
|
+
const content = await provider.read(filename);
|
|
121
126
|
if (content) {
|
|
122
127
|
lastCloudError = null;
|
|
123
128
|
// Cache locally
|
|
@@ -128,7 +133,7 @@ export async function cloudRead(filename) {
|
|
|
128
133
|
catch { /* ignore cache write failure */ }
|
|
129
134
|
}
|
|
130
135
|
else {
|
|
131
|
-
lastCloudError = `Could not read ${filename} from ${pendingCloudConfig.provider} (check credentials
|
|
136
|
+
lastCloudError = `Could not read ${filename} from ${pendingCloudConfig.provider} (check credentials)`;
|
|
132
137
|
}
|
|
133
138
|
return content;
|
|
134
139
|
}
|
|
@@ -136,11 +141,27 @@ export async function cloudRead(filename) {
|
|
|
136
141
|
export async function cloudWrite(filename, content) {
|
|
137
142
|
if (!pendingCloudConfig)
|
|
138
143
|
return false;
|
|
139
|
-
|
|
144
|
+
// Ensure we have a folder ID
|
|
145
|
+
if (!pendingCloudConfig.folderId) {
|
|
146
|
+
pendingCloudConfig.folderId = await gDriveFindOrCreateFolder() || undefined;
|
|
147
|
+
if (pendingCloudConfig.folderId)
|
|
148
|
+
saveFolderIdToConfig(pendingCloudConfig.folderId);
|
|
149
|
+
}
|
|
150
|
+
const provider = getCloudProvider(pendingCloudConfig.provider, pendingCloudConfig.folderId);
|
|
140
151
|
if (!provider)
|
|
141
152
|
return false;
|
|
142
|
-
|
|
143
|
-
|
|
153
|
+
return provider.write(filename, content);
|
|
154
|
+
}
|
|
155
|
+
/** Persist the discovered folder ID back to config.jsonc so we don't search again */
|
|
156
|
+
function saveFolderIdToConfig(folderId) {
|
|
157
|
+
try {
|
|
158
|
+
const config = readLocalConfig();
|
|
159
|
+
if (config.sharedDir && typeof config.sharedDir === "object" && !Array.isArray(config.sharedDir)) {
|
|
160
|
+
config.sharedDir.folderId = folderId;
|
|
161
|
+
atomicWrite(LOCAL_CONFIG_PATH, config);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch { /* non-critical */ }
|
|
144
165
|
}
|
|
145
166
|
/** Whether cloud API fallback is active */
|
|
146
167
|
export function isCloudMode() {
|
|
@@ -526,21 +547,25 @@ export function initLocalConfig(sharedDir, storePath) {
|
|
|
526
547
|
fs.mkdirSync(LOCAL_DIR, { recursive: true });
|
|
527
548
|
atomicWrite(LOCAL_CONFIG_PATH, config);
|
|
528
549
|
}
|
|
529
|
-
/** Initialize config with Google Drive cloud storage
|
|
530
|
-
|
|
550
|
+
/** Initialize config with Google Drive cloud storage.
|
|
551
|
+
* Finds or creates the app-owned "mailx" folder on Drive and stores its ID. */
|
|
552
|
+
export async function initCloudConfig(provider = "gdrive") {
|
|
531
553
|
const existing = readLocalConfig();
|
|
532
554
|
if (existing.sharedDir)
|
|
533
555
|
return; // Already configured
|
|
556
|
+
// Find or create the "mailx" folder on Google Drive
|
|
557
|
+
const folderId = await gDriveFindOrCreateFolder();
|
|
558
|
+
const sharedDir = { provider, path: "mailx", folderId: folderId || undefined };
|
|
534
559
|
const config = {
|
|
535
560
|
...existing,
|
|
536
|
-
sharedDir:
|
|
561
|
+
sharedDir: sharedDir,
|
|
537
562
|
storePath: existing.storePath || DEFAULT_STORE_PATH,
|
|
538
563
|
};
|
|
539
564
|
fs.mkdirSync(LOCAL_DIR, { recursive: true });
|
|
540
565
|
atomicWrite(LOCAL_CONFIG_PATH, config);
|
|
541
566
|
// Set up cloud API fallback immediately
|
|
542
|
-
pendingCloudConfig =
|
|
543
|
-
console.log(` Initialized cloud config: ${provider}
|
|
567
|
+
pendingCloudConfig = sharedDir;
|
|
568
|
+
console.log(` Initialized cloud config: ${provider} (folder ID: ${folderId || "pending"})`);
|
|
544
569
|
}
|
|
545
570
|
const DEFAULT_SETTINGS = {
|
|
546
571
|
accounts: [],
|
|
Binary file
|
|
Binary file
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* mailx postinstall:
|
|
4
|
-
* 1. Link workspace packages so Node can resolve them after npm install
|
|
5
|
-
* 2. Set binary permissions on Linux/Mac
|
|
6
|
-
*/
|
|
7
|
-
import fs from "node:fs";
|
|
8
|
-
import path from "node:path";
|
|
9
|
-
import { fileURLToPath } from "url";
|
|
10
|
-
|
|
11
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
const root = path.join(__dirname, "..", "..");
|
|
13
|
-
|
|
14
|
-
// Link workspace packages into node_modules so imports resolve
|
|
15
|
-
const nodeModules = path.join(root, "node_modules", "@bobfrankston");
|
|
16
|
-
const packagesDir = path.join(root, "packages");
|
|
17
|
-
if (fs.existsSync(packagesDir)) {
|
|
18
|
-
fs.mkdirSync(nodeModules, { recursive: true });
|
|
19
|
-
for (const pkg of fs.readdirSync(packagesDir)) {
|
|
20
|
-
const pkgDir = path.join(packagesDir, pkg);
|
|
21
|
-
const pkgJson = path.join(pkgDir, "package.json");
|
|
22
|
-
if (!fs.existsSync(pkgJson)) continue;
|
|
23
|
-
try {
|
|
24
|
-
const meta = JSON.parse(fs.readFileSync(pkgJson, "utf-8"));
|
|
25
|
-
const name = meta.name?.replace("@bobfrankston/", "");
|
|
26
|
-
if (!name) continue;
|
|
27
|
-
const link = path.join(nodeModules, name);
|
|
28
|
-
// Remove stale junction/symlink that may point to wrong location
|
|
29
|
-
try { fs.unlinkSync(link); } catch { try { fs.rmSync(link, { recursive: true }); } catch { /* */ } }
|
|
30
|
-
// Create junction (Windows) or symlink (Unix) to local packages dir
|
|
31
|
-
if (process.platform === "win32") {
|
|
32
|
-
fs.symlinkSync(pkgDir, link, "junction");
|
|
33
|
-
} else {
|
|
34
|
-
fs.symlinkSync(pkgDir, link, "dir");
|
|
35
|
-
}
|
|
36
|
-
} catch { /* skip */ }
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Set binary permissions
|
|
41
|
-
try {
|
|
42
|
-
const { runPostinstall } = await import("@bobfrankston/rust-builder/postinstall");
|
|
43
|
-
const path = await import("path");
|
|
44
|
-
const { fileURLToPath } = await import("url");
|
|
45
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
46
|
-
|
|
47
|
-
runPostinstall({
|
|
48
|
-
binaryName: "mailx-app",
|
|
49
|
-
binDir: path.join(__dirname, "..", "bin"),
|
|
50
|
-
binaries: {
|
|
51
|
-
win32: "mailx-app.exe",
|
|
52
|
-
darwin: "mailx-app",
|
|
53
|
-
darwinArm64: "mailx-app-arm64",
|
|
54
|
-
linux: "mailx-app-linux",
|
|
55
|
-
linuxArm64: "mailx-app-linux-aarch64",
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
} catch {
|
|
59
|
-
// rust-builder not available (e.g., local dev with file: deps)
|
|
60
|
-
// On Windows, nothing to do. On Linux/Mac, try chmod directly.
|
|
61
|
-
if (process.platform !== "win32") {
|
|
62
|
-
const fs = await import("fs");
|
|
63
|
-
const path = await import("path");
|
|
64
|
-
const { fileURLToPath } = await import("url");
|
|
65
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
66
|
-
const arch = process.arch;
|
|
67
|
-
const name = process.platform === "darwin"
|
|
68
|
-
? (arch === "arm64" ? "mailx-app-arm64" : "mailx-app")
|
|
69
|
-
: (arch === "arm64" ? "mailx-app-linux-aarch64" : "mailx-app-linux");
|
|
70
|
-
const bin = path.join(__dirname, "..", "bin", name);
|
|
71
|
-
try { fs.chmodSync(bin, 0o755); } catch { /* binary may not exist */ }
|
|
72
|
-
}
|
|
73
|
-
}
|
package/launcher/mailx.ico
DELETED
|
Binary file
|