@bobfrankston/mailx 1.0.151 → 1.0.153
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 +70 -58
- package/client/app.js +16 -8
- package/client/components/folder-tree.js +31 -9
- package/client/index.html +1 -1
- package/package.json +2 -2
- package/packages/mailx-imap/index.js +1 -1
- package/packages/mailx-service/index.js +23 -13
- package/packages/mailx-settings/index.js +25 -13
package/bin/mailx.js
CHANGED
|
@@ -72,9 +72,9 @@ if (hasFlag("kill")) {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
catch { /* no process on port */ }
|
|
75
|
-
// Kill any node.exe running mailx
|
|
75
|
+
// Kill any node.exe running mailx (server or IPC service)
|
|
76
76
|
try {
|
|
77
|
-
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();
|
|
77
|
+
const ps = execSync(`powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"name='node.exe'\\" | Where-Object { $_.CommandLine -match 'mailx-server|mailx\\\\packages|mailx\\\\bin' } | Select-Object -ExpandProperty ProcessId"`, { encoding: "utf-8" }).trim();
|
|
78
78
|
for (const pid of ps.split("\n").map(s => s.trim()).filter(s => /^\d+$/.test(s))) {
|
|
79
79
|
try {
|
|
80
80
|
execSync(`taskkill /F /PID ${pid}`, { stdio: "pipe" });
|
|
@@ -368,27 +368,20 @@ async function runSetup() {
|
|
|
368
368
|
console.log("\nmailx — first-time setup\n");
|
|
369
369
|
const home = process.env.USERPROFILE || process.env.HOME || "";
|
|
370
370
|
const mailxDir = path.join(home, ".mailx");
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
371
|
+
// Ask for email first — if it's Gmail, check Drive for existing accounts before prompting further
|
|
372
|
+
console.log("Enter your email address to get started (Gmail recommended for cloud sync).\n");
|
|
373
|
+
const email = await prompt("Email address: ");
|
|
374
|
+
if (!email || !email.includes("@")) {
|
|
375
|
+
console.log(`\nNo account added. The UI will show a setup form.`);
|
|
375
376
|
fs.mkdirSync(mailxDir, { recursive: true });
|
|
376
377
|
return false;
|
|
377
378
|
}
|
|
378
|
-
const
|
|
379
|
-
const settings = {
|
|
380
|
-
name,
|
|
381
|
-
accounts: [account],
|
|
382
|
-
ui: { theme: "system" },
|
|
383
|
-
sync: { intervalMinutes: 5, historyDays: 0 },
|
|
384
|
-
};
|
|
385
|
-
const domain = account.email.split("@")[1]?.toLowerCase() || "";
|
|
386
|
-
// Detect if this is a Google-hosted domain (for GDrive cloud storage)
|
|
379
|
+
const domain = email.split("@")[1]?.toLowerCase() || "";
|
|
387
380
|
let isGoogle = ["gmail.com", "googlemail.com"].includes(domain);
|
|
388
381
|
if (!isGoogle) {
|
|
389
382
|
try {
|
|
390
|
-
const
|
|
391
|
-
const records = await
|
|
383
|
+
const dnsmod = await import("node:dns/promises");
|
|
384
|
+
const records = await dnsmod.resolveMx(domain);
|
|
392
385
|
isGoogle = records.some(mx => {
|
|
393
386
|
const host = mx.exchange.toLowerCase();
|
|
394
387
|
return host.endsWith(".google.com") || host.endsWith(".googlemail.com");
|
|
@@ -398,67 +391,86 @@ async function runSetup() {
|
|
|
398
391
|
}
|
|
399
392
|
catch { /* DNS lookup failed */ }
|
|
400
393
|
}
|
|
401
|
-
|
|
394
|
+
// For Google-hosted accounts, check Drive for existing settings first
|
|
402
395
|
if (isGoogle) {
|
|
403
|
-
|
|
404
|
-
console.log("\
|
|
396
|
+
fs.mkdirSync(mailxDir, { recursive: true });
|
|
397
|
+
console.log("\nChecking Google Drive for existing mailx settings...");
|
|
405
398
|
try {
|
|
406
399
|
const { gDriveFindOrCreateFolder, getCloudProvider } = await import("@bobfrankston/mailx-settings/cloud.js");
|
|
407
400
|
const folderId = await gDriveFindOrCreateFolder();
|
|
408
401
|
if (folderId) {
|
|
402
|
+
console.log(` Drive folder: My Drive/mailx/ (${folderId})`);
|
|
409
403
|
const gdrive = getCloudProvider("gdrive", folderId);
|
|
410
404
|
if (gdrive) {
|
|
411
|
-
// Read
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
accountsList.push(account);
|
|
427
|
-
}
|
|
428
|
-
console.log(` Found ${prevAccounts.length} existing account(s) on Drive — merging`);
|
|
429
|
-
}
|
|
405
|
+
// Read accounts.jsonc (canonical) — ignore legacy settings.jsonc
|
|
406
|
+
const existing = await gdrive.read("accounts.jsonc");
|
|
407
|
+
if (existing) {
|
|
408
|
+
const { parse: parseJsonc } = await import("jsonc-parser");
|
|
409
|
+
const data = parseJsonc(existing);
|
|
410
|
+
const accts = data?.accounts || (Array.isArray(data) ? data : []);
|
|
411
|
+
if (accts.length > 0) {
|
|
412
|
+
console.log(`\nFound ${accts.length} existing account(s) on Google Drive (My Drive/mailx/accounts.jsonc):`);
|
|
413
|
+
for (const a of accts)
|
|
414
|
+
console.log(` • ${a.label || a.name || a.email}`);
|
|
415
|
+
// Save config pointing to Drive — no prompts needed
|
|
416
|
+
const config = { sharedDir: { provider: "gdrive", path: "mailx", folderId } };
|
|
417
|
+
fs.writeFileSync(path.join(mailxDir, "config.jsonc"), JSON.stringify(config, null, 2));
|
|
418
|
+
console.log("Local config created. Starting mailx...\n");
|
|
419
|
+
return true;
|
|
430
420
|
}
|
|
431
|
-
catch { /* parse failed, overwrite */ }
|
|
432
421
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
422
|
+
}
|
|
423
|
+
// No existing accounts — save Drive config for later
|
|
424
|
+
const config = { sharedDir: { provider: "gdrive", path: "mailx", folderId } };
|
|
425
|
+
fs.writeFileSync(path.join(mailxDir, "config.jsonc"), JSON.stringify(config, null, 2));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
catch (e) {
|
|
429
|
+
console.log(` Drive check failed: ${e.message} — continuing with manual setup`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
// No existing accounts found — build a new account
|
|
433
|
+
const account = { email };
|
|
434
|
+
const isOAuth = ["gmail.com", "googlemail.com", "outlook.com", "hotmail.com", "live.com"].includes(domain);
|
|
435
|
+
if (!isOAuth) {
|
|
436
|
+
account.password = await prompt("Password (app password for Yahoo/AOL/iCloud): ");
|
|
437
|
+
}
|
|
438
|
+
const name = await prompt(`Your name (for From: header) [${email.split("@")[0]}]: `) || email.split("@")[0];
|
|
439
|
+
fs.mkdirSync(mailxDir, { recursive: true });
|
|
440
|
+
if (isGoogle) {
|
|
441
|
+
// Save to Google Drive via API
|
|
442
|
+
console.log("\nSaving account to Google Drive...");
|
|
443
|
+
try {
|
|
444
|
+
const { gDriveFindOrCreateFolder, getCloudProvider } = await import("@bobfrankston/mailx-settings/cloud.js");
|
|
445
|
+
const folderId = await gDriveFindOrCreateFolder();
|
|
446
|
+
if (folderId) {
|
|
447
|
+
const gdrive = getCloudProvider("gdrive", folderId);
|
|
448
|
+
if (gdrive) {
|
|
449
|
+
const accountsData = { name, accounts: [account] };
|
|
450
|
+
const ok = await gdrive.write("accounts.jsonc", JSON.stringify(accountsData, null, 2));
|
|
436
451
|
if (ok) {
|
|
437
|
-
console.log("
|
|
438
|
-
|
|
439
|
-
fs.
|
|
440
|
-
|
|
452
|
+
console.log("Account saved to Google Drive.");
|
|
453
|
+
// config.jsonc may already exist from the Drive check above
|
|
454
|
+
if (!fs.existsSync(path.join(mailxDir, "config.jsonc"))) {
|
|
455
|
+
const config = { sharedDir: { provider: "gdrive", path: "mailx", folderId } };
|
|
456
|
+
fs.writeFileSync(path.join(mailxDir, "config.jsonc"), JSON.stringify(config, null, 2));
|
|
457
|
+
}
|
|
441
458
|
}
|
|
442
459
|
else {
|
|
443
|
-
console.log("
|
|
444
|
-
fs.writeFileSync(path.join(mailxDir, "
|
|
460
|
+
console.log("Drive write failed — saving locally.");
|
|
461
|
+
fs.writeFileSync(path.join(mailxDir, "accounts.jsonc"), JSON.stringify({ name, accounts: [account] }, null, 2));
|
|
445
462
|
}
|
|
446
463
|
}
|
|
447
464
|
}
|
|
448
|
-
else {
|
|
449
|
-
console.log("Google Drive folder setup failed — saving locally.");
|
|
450
|
-
fs.writeFileSync(path.join(mailxDir, "settings.jsonc"), JSON.stringify(settings, null, 2));
|
|
451
|
-
}
|
|
452
465
|
}
|
|
453
466
|
catch (e) {
|
|
454
|
-
console.log(`
|
|
455
|
-
fs.writeFileSync(path.join(mailxDir, "
|
|
467
|
+
console.log(`Drive error: ${e.message} — saving locally.`);
|
|
468
|
+
fs.writeFileSync(path.join(mailxDir, "accounts.jsonc"), JSON.stringify({ name, accounts: [account] }, null, 2));
|
|
456
469
|
}
|
|
457
470
|
}
|
|
458
471
|
else {
|
|
459
|
-
// Non-Google
|
|
460
|
-
|
|
461
|
-
fs.writeFileSync(path.join(mailxDir, "settings.jsonc"), JSON.stringify(settings, null, 2));
|
|
472
|
+
// Non-Google — save locally
|
|
473
|
+
fs.writeFileSync(path.join(mailxDir, "accounts.jsonc"), JSON.stringify({ name, accounts: [account] }, null, 2));
|
|
462
474
|
}
|
|
463
475
|
console.log("Setup complete. Starting mailx...\n");
|
|
464
476
|
return true;
|
package/client/app.js
CHANGED
|
@@ -898,11 +898,19 @@ optAutocomplete?.addEventListener("change", () => {
|
|
|
898
898
|
}).catch(() => { });
|
|
899
899
|
});
|
|
900
900
|
const isApp = typeof mailxapi !== "undefined" && mailxapi?.isApp;
|
|
901
|
-
//
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
901
|
+
// Retry getVersion — IPC may not be ready on first call
|
|
902
|
+
async function getVersionWithRetry() {
|
|
903
|
+
for (let i = 0; i < 5; i++) {
|
|
904
|
+
try {
|
|
905
|
+
return await getVersion();
|
|
906
|
+
}
|
|
907
|
+
catch {
|
|
908
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return { version: "?", storage: {} };
|
|
912
|
+
}
|
|
913
|
+
const versionPromise = getVersionWithRetry();
|
|
906
914
|
versionPromise.then((d) => {
|
|
907
915
|
const el = document.getElementById("app-version");
|
|
908
916
|
const storage = d.storage || {};
|
|
@@ -911,10 +919,10 @@ versionPromise.then((d) => {
|
|
|
911
919
|
: "";
|
|
912
920
|
if (el) {
|
|
913
921
|
el.textContent = `mailx v${d.version}${storageLabel}${isApp ? "" : " [browser]"}`;
|
|
914
|
-
// Tooltip: show
|
|
922
|
+
// Tooltip: show Drive path on hover
|
|
915
923
|
if (storage.provider && storage.provider !== "local") {
|
|
916
|
-
const
|
|
917
|
-
el.title =
|
|
924
|
+
const drivePath = storage.cloudPath ? `My Drive/${storage.cloudPath}/` : storage.provider;
|
|
925
|
+
el.title = drivePath;
|
|
918
926
|
}
|
|
919
927
|
}
|
|
920
928
|
if (d.settingsError) {
|
|
@@ -330,7 +330,14 @@ async function loadFolderTree(container) {
|
|
|
330
330
|
container.innerHTML = `<div class="folder-loading">Loading accounts...</div>`;
|
|
331
331
|
}
|
|
332
332
|
try {
|
|
333
|
-
|
|
333
|
+
let accounts = await getAccounts();
|
|
334
|
+
// Accounts may still be registering (OAuth in progress) — retry a few times
|
|
335
|
+
if (accounts.length === 0) {
|
|
336
|
+
for (let retry = 0; retry < 5 && accounts.length === 0; retry++) {
|
|
337
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
338
|
+
accounts = await getAccounts();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
334
341
|
if (accounts.length === 0) {
|
|
335
342
|
container.innerHTML = `<div class="folder-loading">No accounts</div>`;
|
|
336
343
|
// Hide the message list and show setup in the viewer pane (full width)
|
|
@@ -396,28 +403,43 @@ async function loadFolderTree(container) {
|
|
|
396
403
|
helpEl.textContent = help || "";
|
|
397
404
|
}
|
|
398
405
|
});
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
406
|
+
// When a valid email is entered, try setup immediately — if cloud has
|
|
407
|
+
// existing accounts, loads them without needing name/password
|
|
408
|
+
let setupTriggered = false;
|
|
409
|
+
async function trySetup() {
|
|
402
410
|
const email = emailInput.value.trim();
|
|
403
|
-
|
|
404
|
-
if (!email)
|
|
411
|
+
if (!email || !email.includes("@") || setupTriggered)
|
|
405
412
|
return;
|
|
406
|
-
|
|
413
|
+
const name = document.getElementById("setup-name").value.trim();
|
|
414
|
+
const password = document.getElementById("setup-password").value;
|
|
415
|
+
setupTriggered = true;
|
|
416
|
+
statusEl.textContent = "Checking for existing accounts...";
|
|
407
417
|
try {
|
|
408
418
|
const data = await setupAccount(name, email, password);
|
|
409
419
|
if (data.ok) {
|
|
410
|
-
statusEl.textContent = "
|
|
411
|
-
// Wait for sync to populate folders before reloading
|
|
420
|
+
statusEl.textContent = data.message || "Accounts loaded! Syncing...";
|
|
412
421
|
setTimeout(() => location.reload(), 5000);
|
|
413
422
|
}
|
|
414
423
|
else {
|
|
424
|
+
setupTriggered = false;
|
|
415
425
|
statusEl.textContent = `Error: ${data.error || "Setup failed"}`;
|
|
416
426
|
}
|
|
417
427
|
}
|
|
418
428
|
catch (err) {
|
|
429
|
+
setupTriggered = false;
|
|
419
430
|
statusEl.textContent = `Error: ${err.message}`;
|
|
420
431
|
}
|
|
432
|
+
}
|
|
433
|
+
form?.addEventListener("submit", async (e) => {
|
|
434
|
+
e.preventDefault();
|
|
435
|
+
await trySetup();
|
|
436
|
+
});
|
|
437
|
+
// Auto-trigger for OAuth providers when email looks complete
|
|
438
|
+
emailInput?.addEventListener("change", async () => {
|
|
439
|
+
const domain = emailInput.value.split("@")[1]?.toLowerCase() || "";
|
|
440
|
+
const isOAuth = ["gmail.com", "googlemail.com", "outlook.com", "hotmail.com", "live.com"].includes(domain);
|
|
441
|
+
if (isOAuth)
|
|
442
|
+
await trySetup();
|
|
421
443
|
});
|
|
422
444
|
// Show cloud storage status in setup form
|
|
423
445
|
getVersion().then((d) => {
|
package/client/index.html
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
<label class="tb-menu-item"><input type="checkbox" id="opt-autocomplete"> AI autocomplete</label>
|
|
40
40
|
</div>
|
|
41
41
|
</div>
|
|
42
|
-
<span id="app-version" class="app-version"
|
|
42
|
+
<span id="app-version" class="app-version">mailx</span>
|
|
43
43
|
</div>
|
|
44
44
|
<div class="toolbar-right">
|
|
45
45
|
<button class="tb-btn" id="btn-sync" title="Sync all folders (F5)">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.153",
|
|
4
4
|
"description": "Local-first email client with IMAP sync and standalone native app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/mailx.js",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"@bobfrankston/iflow": "^1.0.53",
|
|
24
24
|
"@bobfrankston/miscinfo": "^1.0.7",
|
|
25
25
|
"@bobfrankston/oauthsupport": "^1.0.20",
|
|
26
|
-
"@bobfrankston/msger": "^0.1.
|
|
26
|
+
"@bobfrankston/msger": "^0.1.203",
|
|
27
27
|
"@capacitor/android": "^8.3.0",
|
|
28
28
|
"@capacitor/cli": "^8.3.0",
|
|
29
29
|
"@capacitor/core": "^8.3.0",
|
|
@@ -108,7 +108,7 @@ export class ImapManager extends EventEmitter {
|
|
|
108
108
|
connectionBackoff = new Map();
|
|
109
109
|
/** Per-account connection semaphore — limits concurrent IMAP connections */
|
|
110
110
|
connectionSemaphore = new Map();
|
|
111
|
-
static MAX_CONNECTIONS =
|
|
111
|
+
static MAX_CONNECTIONS = 2; // 1 for all operations (sequential), 1 for IDLE
|
|
112
112
|
constructor(db) {
|
|
113
113
|
super();
|
|
114
114
|
this.db = db;
|
|
@@ -539,12 +539,30 @@ export class MailxService {
|
|
|
539
539
|
if (detected?.cloud) {
|
|
540
540
|
await initCloudConfig(detected.cloud);
|
|
541
541
|
}
|
|
542
|
-
//
|
|
542
|
+
// Check cloud for existing accounts — if found, just load them all
|
|
543
543
|
let accounts = loadAccounts();
|
|
544
544
|
if (accounts.length === 0) {
|
|
545
545
|
accounts = await loadAccountsAsync();
|
|
546
546
|
}
|
|
547
|
-
|
|
547
|
+
if (accounts.length > 0) {
|
|
548
|
+
// Existing accounts found on cloud — use them directly
|
|
549
|
+
console.log(` Found ${accounts.length} existing account(s) from cloud settings`);
|
|
550
|
+
const settings = loadSettings();
|
|
551
|
+
for (const acct of settings.accounts) {
|
|
552
|
+
if (!acct.enabled)
|
|
553
|
+
continue;
|
|
554
|
+
try {
|
|
555
|
+
await this.imapManager.addAccount(acct);
|
|
556
|
+
console.log(` Account loaded: ${acct.label || acct.name} (${acct.id})`);
|
|
557
|
+
}
|
|
558
|
+
catch (e) {
|
|
559
|
+
console.error(` Account ${acct.id} error: ${e.message}`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
this.imapManager.syncAll().catch(() => { });
|
|
563
|
+
return { ok: true, message: `Loaded ${accounts.length} existing account(s) from cloud.` };
|
|
564
|
+
}
|
|
565
|
+
// No existing accounts — create new one
|
|
548
566
|
const account = { email, name: name || email.split("@")[0] };
|
|
549
567
|
if (password)
|
|
550
568
|
account.password = password;
|
|
@@ -552,17 +570,9 @@ export class MailxService {
|
|
|
552
570
|
account.imap = { host: detected.imapHost, port: 993, tls: true, auth: detected.auth, user: email };
|
|
553
571
|
account.smtp = { host: detected.smtpHost, port: 587, tls: true, auth: detected.auth, user: email };
|
|
554
572
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
account.id = id;
|
|
559
|
-
accounts.push(account);
|
|
560
|
-
}
|
|
561
|
-
if (accounts.length > 0) {
|
|
562
|
-
console.log(` Saving ${accounts.length} account(s) (merged with existing cloud settings)`);
|
|
563
|
-
}
|
|
564
|
-
saveAccounts(accounts);
|
|
565
|
-
// Re-read normalized settings and register ALL accounts
|
|
573
|
+
account.id = domain.split(".")[0] || "account";
|
|
574
|
+
saveAccounts([account]);
|
|
575
|
+
// Re-read normalized settings and register
|
|
566
576
|
const settings = loadSettings();
|
|
567
577
|
for (const acct of settings.accounts) {
|
|
568
578
|
if (!acct.enabled)
|
|
@@ -78,13 +78,11 @@ function getSharedDir() {
|
|
|
78
78
|
if (resolved)
|
|
79
79
|
return resolved;
|
|
80
80
|
}
|
|
81
|
-
//
|
|
81
|
+
// Set pending cloud config for API access
|
|
82
82
|
if (!pendingCloudConfig) {
|
|
83
83
|
const lastProvider = [...entries].reverse().find(e => typeof e !== "string");
|
|
84
|
-
if (lastProvider)
|
|
84
|
+
if (lastProvider)
|
|
85
85
|
pendingCloudConfig = lastProvider;
|
|
86
|
-
console.log(` No cloud drive mounted — will try ${lastProvider.provider} API`);
|
|
87
|
-
}
|
|
88
86
|
}
|
|
89
87
|
}
|
|
90
88
|
// Legacy settingsPath no longer used for shared dir — use loadLegacySettings() for reading only.
|
|
@@ -388,7 +386,7 @@ export function loadAccounts() {
|
|
|
388
386
|
}
|
|
389
387
|
const raw = accounts.accounts || accounts;
|
|
390
388
|
const globalName = accounts.name || "";
|
|
391
|
-
const result = raw.map((a) => normalizeAccount(a, globalName));
|
|
389
|
+
const result = deduplicateAccounts(raw.map((a) => normalizeAccount(a, globalName)));
|
|
392
390
|
return applyAccountOverrides(result);
|
|
393
391
|
}
|
|
394
392
|
// Legacy: read from settings.jsonc
|
|
@@ -397,6 +395,27 @@ export function loadAccounts() {
|
|
|
397
395
|
return applyAccountOverrides(legacy.accounts.map((a) => normalizeAccount(a, legacy.name)));
|
|
398
396
|
return DEFAULT_ACCOUNTS;
|
|
399
397
|
}
|
|
398
|
+
/** Normalize email for dedup — Gmail ignores dots before @ */
|
|
399
|
+
function normalizeEmail(email) {
|
|
400
|
+
const [local, domain] = email.toLowerCase().split("@");
|
|
401
|
+
if (!domain)
|
|
402
|
+
return email.toLowerCase();
|
|
403
|
+
if (domain === "gmail.com" || domain === "googlemail.com") {
|
|
404
|
+
return local.replace(/\./g, "") + "@gmail.com";
|
|
405
|
+
}
|
|
406
|
+
return `${local}@${domain}`;
|
|
407
|
+
}
|
|
408
|
+
/** Remove duplicate accounts (same email after normalization) */
|
|
409
|
+
function deduplicateAccounts(accounts) {
|
|
410
|
+
const seen = new Set();
|
|
411
|
+
return accounts.filter(a => {
|
|
412
|
+
const key = normalizeEmail(a.email);
|
|
413
|
+
if (seen.has(key))
|
|
414
|
+
return false;
|
|
415
|
+
seen.add(key);
|
|
416
|
+
return true;
|
|
417
|
+
});
|
|
418
|
+
}
|
|
400
419
|
/** Apply local per-account overrides (enabled, etc.) */
|
|
401
420
|
function applyAccountOverrides(accounts) {
|
|
402
421
|
const localConfig = readLocalConfig();
|
|
@@ -430,14 +449,7 @@ export async function loadAccountsAsync() {
|
|
|
430
449
|
return applyAccountOverrides(raw.map((a) => normalizeAccount(a, globalName)));
|
|
431
450
|
}
|
|
432
451
|
}
|
|
433
|
-
//
|
|
434
|
-
const legacy = await cloudRead("settings.jsonc");
|
|
435
|
-
if (legacy) {
|
|
436
|
-
const data = parseJsonc(legacy);
|
|
437
|
-
if (data?.accounts) {
|
|
438
|
-
return applyAccountOverrides(data.accounts.map((a) => normalizeAccount(a, data.name)));
|
|
439
|
-
}
|
|
440
|
-
}
|
|
452
|
+
// Legacy settings.jsonc is no longer read — use accounts.jsonc only
|
|
441
453
|
}
|
|
442
454
|
return [];
|
|
443
455
|
}
|