@bobfrankston/mailx 1.0.138 → 1.0.139
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
CHANGED
|
@@ -3,17 +3,22 @@
|
|
|
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 (or browser fallback)
|
|
7
|
+
* mailx --server Start server + open browser (skip msger)
|
|
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
|
+
const PORT = ports.mailx;
|
|
17
22
|
const args = process.argv.slice(2);
|
|
18
23
|
// Normalize: accept both -flag and --flag
|
|
19
24
|
function hasFlag(name) { return args.includes(`-${name}`) || args.includes(`--${name}`); }
|
|
@@ -76,13 +81,6 @@ if (hasFlag("kill")) {
|
|
|
76
81
|
}
|
|
77
82
|
}
|
|
78
83
|
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
84
|
}
|
|
87
85
|
else {
|
|
88
86
|
try {
|
|
@@ -102,13 +100,6 @@ if (hasFlag("kill")) {
|
|
|
102
100
|
}
|
|
103
101
|
catch { /* */ }
|
|
104
102
|
}
|
|
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
103
|
if (killed === 0)
|
|
113
104
|
console.log("No mailx processes found");
|
|
114
105
|
process.exit(0);
|
|
@@ -188,49 +179,31 @@ function openBrowser(url) {
|
|
|
188
179
|
exec(`xdg-open "${url}"`);
|
|
189
180
|
});
|
|
190
181
|
}
|
|
182
|
+
/** Launch msger pointing at the server URL; fall back to browser if msger not found */
|
|
183
|
+
function launchMsger(url) {
|
|
184
|
+
import("node:child_process").then(({ spawn }) => {
|
|
185
|
+
const child = spawn("msger", ["-url", url], { detached: true, stdio: "ignore" });
|
|
186
|
+
child.on("error", () => {
|
|
187
|
+
log("msger not found — opening in browser");
|
|
188
|
+
openBrowser(url);
|
|
189
|
+
});
|
|
190
|
+
child.unref();
|
|
191
|
+
});
|
|
192
|
+
}
|
|
191
193
|
async function prompt(question) {
|
|
192
194
|
const readline = await import("readline");
|
|
193
195
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
194
196
|
return new Promise(resolve => rl.question(question, (ans) => { rl.close(); resolve(ans.trim()); }));
|
|
195
197
|
}
|
|
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) */
|
|
198
|
+
/** Check if mailx is configured (has local config or accounts) */
|
|
224
199
|
function hasConfig() {
|
|
225
200
|
const home = process.env.USERPROFILE || process.env.HOME || "";
|
|
226
201
|
const mailxDir = path.join(home, ".mailx");
|
|
227
|
-
|
|
228
|
-
for (const f of ["settings.jsonc", "accounts.jsonc", "config.jsonc"]) {
|
|
202
|
+
for (const f of ["config.jsonc", "accounts.jsonc", "settings.jsonc"]) {
|
|
229
203
|
if (fs.existsSync(path.join(mailxDir, f)))
|
|
230
204
|
return true;
|
|
231
205
|
}
|
|
232
|
-
|
|
233
|
-
return findCloudSettings() !== null;
|
|
206
|
+
return false;
|
|
234
207
|
}
|
|
235
208
|
/** Try to auto-discover mail server settings from email domain */
|
|
236
209
|
async function autoDiscover(domain) {
|
|
@@ -265,6 +238,29 @@ async function autoDiscover(domain) {
|
|
|
265
238
|
}
|
|
266
239
|
}
|
|
267
240
|
catch { /* DNS failed */ }
|
|
241
|
+
// 3. Try MX-based detection (Google Workspace, Microsoft 365)
|
|
242
|
+
try {
|
|
243
|
+
const dns = await import("node:dns/promises");
|
|
244
|
+
const records = await dns.resolveMx(domain);
|
|
245
|
+
for (const mx of records) {
|
|
246
|
+
const host = mx.exchange.toLowerCase();
|
|
247
|
+
if (host.endsWith(".google.com") || host.endsWith(".googlemail.com")) {
|
|
248
|
+
return {
|
|
249
|
+
imap: { host: "imap.gmail.com", port: 993, auth: "oauth2" },
|
|
250
|
+
smtp: { host: "smtp.gmail.com", port: 587, auth: "oauth2" },
|
|
251
|
+
source: `MX → Google (${host})`,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (host.endsWith(".outlook.com") || host.endsWith(".protection.outlook.com")) {
|
|
255
|
+
return {
|
|
256
|
+
imap: { host: "outlook.office365.com", port: 993, auth: "oauth2" },
|
|
257
|
+
smtp: { host: "smtp.office365.com", port: 587, auth: "oauth2" },
|
|
258
|
+
source: `MX → Microsoft (${host})`,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch { /* DNS failed */ }
|
|
268
264
|
return null;
|
|
269
265
|
}
|
|
270
266
|
/** Prompt user for account details, auto-discover where possible */
|
|
@@ -307,44 +303,15 @@ async function promptForAccount(intro) {
|
|
|
307
303
|
smtp: { host: smtpHost },
|
|
308
304
|
};
|
|
309
305
|
}
|
|
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 */
|
|
306
|
+
/** Interactive first-time setup — GDrive API for cloud storage */
|
|
328
307
|
async function runSetup() {
|
|
329
308
|
console.log("\nmailx — first-time setup\n");
|
|
330
309
|
const home = process.env.USERPROFILE || process.env.HOME || "";
|
|
331
310
|
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");
|
|
311
|
+
console.log("Gmail is recommended as the first account (provides contacts, calendar, and cloud settings sync).\n");
|
|
312
|
+
const account = await promptForAccount();
|
|
346
313
|
if (!account) {
|
|
347
|
-
console.log(`\
|
|
314
|
+
console.log(`\nNo account added. Open http://127.0.0.1:${PORT} in a browser to set up via the UI.`);
|
|
348
315
|
fs.mkdirSync(mailxDir, { recursive: true });
|
|
349
316
|
return false;
|
|
350
317
|
}
|
|
@@ -356,43 +323,47 @@ async function runSetup() {
|
|
|
356
323
|
sync: { intervalMinutes: 5, historyDays: 0 },
|
|
357
324
|
};
|
|
358
325
|
const domain = account.email.split("@")[1]?.toLowerCase() || "";
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
326
|
+
// Detect if this is a Google-hosted domain (for GDrive cloud storage)
|
|
327
|
+
let isGoogle = ["gmail.com", "googlemail.com"].includes(domain);
|
|
328
|
+
if (!isGoogle) {
|
|
329
|
+
try {
|
|
330
|
+
const dns = await import("node:dns/promises");
|
|
331
|
+
const records = await dns.resolveMx(domain);
|
|
332
|
+
isGoogle = records.some(mx => {
|
|
333
|
+
const host = mx.exchange.toLowerCase();
|
|
334
|
+
return host.endsWith(".google.com") || host.endsWith(".googlemail.com");
|
|
335
|
+
});
|
|
336
|
+
if (isGoogle)
|
|
337
|
+
console.log(` ${domain} is hosted on Google (detected via MX)`);
|
|
338
|
+
}
|
|
339
|
+
catch { /* DNS lookup failed */ }
|
|
372
340
|
}
|
|
373
341
|
fs.mkdirSync(mailxDir, { recursive: true });
|
|
374
|
-
if (
|
|
375
|
-
// Save to Google Drive via API (
|
|
342
|
+
if (isGoogle) {
|
|
343
|
+
// Save to Google Drive via API (folder-ID based, no path navigation)
|
|
376
344
|
console.log("\nSaving settings to Google Drive via API...");
|
|
377
345
|
try {
|
|
378
|
-
const { getCloudProvider } = await import("
|
|
379
|
-
const
|
|
380
|
-
if (
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
346
|
+
const { gDriveFindOrCreateFolder, getCloudProvider } = await import("@bobfrankston/mailx-settings/cloud.js");
|
|
347
|
+
const folderId = await gDriveFindOrCreateFolder();
|
|
348
|
+
if (folderId) {
|
|
349
|
+
const gdrive = getCloudProvider("gdrive", folderId);
|
|
350
|
+
if (gdrive) {
|
|
351
|
+
const content = JSON.stringify(settings, null, 2);
|
|
352
|
+
const ok = await gdrive.write("settings.jsonc", content);
|
|
353
|
+
if (ok) {
|
|
354
|
+
console.log("Settings saved to Google Drive (mailx folder)");
|
|
355
|
+
const config = { sharedDir: { provider: "gdrive", path: "mailx", folderId } };
|
|
356
|
+
fs.writeFileSync(path.join(mailxDir, "config.jsonc"), JSON.stringify(config, null, 2));
|
|
357
|
+
console.log("Local config created with Drive folder ID.");
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
console.log("Google Drive write failed — saving locally instead.");
|
|
361
|
+
fs.writeFileSync(path.join(mailxDir, "settings.jsonc"), JSON.stringify(settings, null, 2));
|
|
362
|
+
}
|
|
392
363
|
}
|
|
393
364
|
}
|
|
394
365
|
else {
|
|
395
|
-
console.log("Google Drive
|
|
366
|
+
console.log("Google Drive folder setup failed — saving locally.");
|
|
396
367
|
fs.writeFileSync(path.join(mailxDir, "settings.jsonc"), JSON.stringify(settings, null, 2));
|
|
397
368
|
}
|
|
398
369
|
}
|
|
@@ -401,17 +372,8 @@ async function runSetup() {
|
|
|
401
372
|
fs.writeFileSync(path.join(mailxDir, "settings.jsonc"), JSON.stringify(settings, null, 2));
|
|
402
373
|
}
|
|
403
374
|
}
|
|
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
375
|
else {
|
|
414
|
-
//
|
|
376
|
+
// Non-Google account — save locally
|
|
415
377
|
console.log(`\nSaving settings to ${mailxDir}...`);
|
|
416
378
|
fs.writeFileSync(path.join(mailxDir, "settings.jsonc"), JSON.stringify(settings, null, 2));
|
|
417
379
|
}
|
|
@@ -507,7 +469,7 @@ async function runTest() {
|
|
|
507
469
|
async function main() {
|
|
508
470
|
log(`Platform: ${process.platform} ${process.arch}`);
|
|
509
471
|
log(`Node: ${process.version}`);
|
|
510
|
-
log(`Mode: ${serverMode ? "server" : setupMode ? "setup" : "auto
|
|
472
|
+
log(`Mode: ${serverMode ? "server" : setupMode ? "setup" : "auto"}`);
|
|
511
473
|
// Test connectivity
|
|
512
474
|
if (testMode) {
|
|
513
475
|
await runTest();
|
|
@@ -537,87 +499,49 @@ async function main() {
|
|
|
537
499
|
process.exit(0);
|
|
538
500
|
}
|
|
539
501
|
// Auto-detect first run — enter setup if no config exists
|
|
540
|
-
if (setupMode ||
|
|
502
|
+
if (setupMode || !hasConfig()) {
|
|
541
503
|
if (!setupMode)
|
|
542
504
|
console.log("No mailx configuration found.");
|
|
543
505
|
await runSetup();
|
|
544
506
|
}
|
|
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");
|
|
507
|
+
// Check if server is already running
|
|
508
|
+
const inUse = await isPortInUse(PORT);
|
|
509
|
+
if (inUse) {
|
|
510
|
+
console.log(`mailx server already running on port ${PORT}`);
|
|
511
|
+
const url = `http://127.0.0.1:${PORT}`;
|
|
559
512
|
if (!noBrowser) {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
}
|
|
565
|
-
openBrowser(`http://127.0.0.1:${PORT}`);
|
|
566
|
-
console.log("mailx opened (browser)");
|
|
513
|
+
if (serverMode)
|
|
514
|
+
openBrowser(url);
|
|
515
|
+
else
|
|
516
|
+
launchMsger(url);
|
|
567
517
|
}
|
|
568
|
-
|
|
569
|
-
await new Promise(() => { });
|
|
518
|
+
return;
|
|
570
519
|
}
|
|
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;
|
|
520
|
+
// Start Express server in-process
|
|
521
|
+
console.log("Starting mailx server...");
|
|
522
|
+
log(`Loading server from: ${path.join(import.meta.dirname, "..", "packages", "mailx-server", "index.js")}`);
|
|
523
|
+
if (hasFlag("external"))
|
|
524
|
+
process.argv.push("--external");
|
|
525
|
+
await import("../packages/mailx-server/index.js");
|
|
526
|
+
// Open UI once server is ready
|
|
527
|
+
if (!noBrowser) {
|
|
528
|
+
const url = `http://127.0.0.1:${PORT}`;
|
|
529
|
+
for (let i = 0; i < 30; i++) {
|
|
530
|
+
await new Promise(r => setTimeout(r, 200));
|
|
531
|
+
if (await isPortInUse(PORT))
|
|
532
|
+
break;
|
|
592
533
|
}
|
|
593
|
-
if (
|
|
594
|
-
|
|
595
|
-
log(
|
|
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
|
-
}
|
|
534
|
+
if (serverMode) {
|
|
535
|
+
openBrowser(url);
|
|
536
|
+
console.log("mailx opened (browser)");
|
|
613
537
|
}
|
|
614
538
|
else {
|
|
615
|
-
|
|
616
|
-
log("
|
|
617
|
-
process.argv.push("--server");
|
|
618
|
-
await main(); // recurse with --server
|
|
539
|
+
launchMsger(url);
|
|
540
|
+
console.log("mailx opened");
|
|
619
541
|
}
|
|
620
542
|
}
|
|
543
|
+
// Keep process alive — server is running
|
|
544
|
+
await new Promise(() => { });
|
|
621
545
|
}
|
|
622
546
|
main().catch(console.error);
|
|
623
547
|
//# sourceMappingURL=mailx.js.map
|
package/package.json
CHANGED
|
@@ -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] };
|
|
@@ -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: [],
|