@bobfrankston/mailx 1.0.146 → 1.0.150

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.146",
3
+ "version": "1.0.150",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -20,10 +20,10 @@
20
20
  "postinstall": "node bin/postinstall.js"
21
21
  },
22
22
  "dependencies": {
23
- "@bobfrankston/iflow": "^1.0.52",
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.198",
26
+ "@bobfrankston/msger": "^0.1.200",
27
27
  "@capacitor/android": "^8.3.0",
28
28
  "@capacitor/cli": "^8.3.0",
29
29
  "@capacitor/core": "^8.3.0",
@@ -3,38 +3,7 @@
3
3
  * Thin Express Router — delegates all logic to mailx-service.
4
4
  */
5
5
  import { Router } from "express";
6
- import * as dns from "node:dns/promises";
7
6
  import { MailxService } from "@bobfrankston/mailx-service";
8
- import { loadAccounts, loadAccountsAsync, saveAccounts, initCloudConfig, loadSettings } from "@bobfrankston/mailx-settings";
9
- /** Detect email provider via MX records (Google Workspace, Microsoft 365 custom domains).
10
- * Cloud storage is always gdrive (only for Google-hosted domains).
11
- * Microsoft domains get correct IMAP/SMTP but no cloud auto-config. */
12
- async function detectEmailProvider(domain) {
13
- // Known domains — no MX lookup needed
14
- const GOOGLE_DOMAINS = ["gmail.com", "googlemail.com"];
15
- const MS_DOMAINS = ["outlook.com", "hotmail.com", "live.com"];
16
- if (GOOGLE_DOMAINS.includes(domain))
17
- return { cloud: "gdrive", imapHost: "imap.gmail.com", smtpHost: "smtp.gmail.com", auth: "oauth2" };
18
- if (MS_DOMAINS.includes(domain))
19
- return { imapHost: "outlook.office365.com", smtpHost: "smtp.office365.com", auth: "oauth2" };
20
- // MX lookup for custom domains (Google Workspace, Microsoft 365)
21
- try {
22
- const records = await dns.resolveMx(domain);
23
- for (const mx of records) {
24
- const host = mx.exchange.toLowerCase();
25
- if (host.endsWith(".google.com") || host.endsWith(".googlemail.com")) {
26
- console.log(` [setup] MX for ${domain} → Google (${host})`);
27
- return { cloud: "gdrive", imapHost: "imap.gmail.com", smtpHost: "smtp.gmail.com", auth: "oauth2" };
28
- }
29
- if (host.endsWith(".outlook.com") || host.endsWith(".protection.outlook.com")) {
30
- console.log(` [setup] MX for ${domain} → Microsoft (${host})`);
31
- return { imapHost: "outlook.office365.com", smtpHost: "smtp.office365.com", auth: "oauth2" };
32
- }
33
- }
34
- }
35
- catch { /* DNS lookup failed — not critical */ }
36
- return null;
37
- }
38
7
  export function createApiRouter(db, imapManager) {
39
8
  const svc = new MailxService(db, imapManager);
40
9
  const router = Router();
@@ -140,72 +109,10 @@ export function createApiRouter(db, imapManager) {
140
109
  router.post("/setup", async (req, res) => {
141
110
  try {
142
111
  const { name, email, password } = req.body;
143
- if (!email) {
144
- res.status(400).json({ error: "Email address required" });
145
- return;
146
- }
147
- // Detect provider via domain or MX records (Google Workspace, Microsoft 365 custom domains)
148
- const domain = email.split("@")[1]?.toLowerCase() || "";
149
- const detected = await detectEmailProvider(domain);
150
- if (detected?.cloud) {
151
- await initCloudConfig(detected.cloud);
152
- }
153
- // Check if cloud config found existing accounts (e.g., from GDrive mount or API)
154
- let accounts = loadAccounts();
155
- if (accounts.length === 0) {
156
- accounts = await loadAccountsAsync();
157
- }
158
- if (accounts.length > 0) {
159
- // Existing accounts found on cloud — use them, don't create new
160
- console.log(` Found ${accounts.length} existing account(s) from cloud settings`);
161
- const settings = loadSettings();
162
- for (const acct of settings.accounts) {
163
- if (!acct.enabled)
164
- continue;
165
- try {
166
- await imapManager.addAccount(acct);
167
- console.log(` Account loaded: ${acct.label || acct.name} (${acct.id})`);
168
- }
169
- catch (e) {
170
- console.error(` Account ${acct.id} error: ${e.message}`);
171
- }
172
- }
173
- imapManager.syncAll().catch(() => { });
174
- res.json({ ok: true, message: `Loaded ${accounts.length} existing account(s) from cloud settings.` });
175
- return;
176
- }
177
- // No existing accounts — build new account config
178
- const account = { email, name: name || email.split("@")[0] };
179
- if (password)
180
- account.password = password;
181
- // For custom domains hosted on Google/Microsoft, set correct IMAP/SMTP servers
182
- if (detected && !["gmail.com", "googlemail.com", "outlook.com", "hotmail.com", "live.com"].includes(domain)) {
183
- account.imap = { host: detected.imapHost, port: 993, tls: true, auth: detected.auth, user: email };
184
- account.smtp = { host: detected.smtpHost, port: 587, tls: true, auth: detected.auth, user: email };
185
- }
186
- const id = domain.split(".")[0] || "account";
187
- if (accounts.some((a) => a.email === email)) {
188
- res.json({ ok: false, error: "Account already exists" });
189
- return;
190
- }
191
- account.id = id;
192
- accounts.push(account);
193
- saveAccounts(accounts);
194
- // Reload settings and register the new account in DB + IMAP so it works immediately
195
- const settings = loadSettings();
196
- const normalized = settings.accounts.find(a => a.id === id);
197
- if (normalized) {
198
- try {
199
- await imapManager.addAccount(normalized);
200
- console.log(` Account added: ${normalized.name} (${normalized.id})`);
201
- // Start syncing in background
202
- imapManager.syncAll().catch(() => { });
203
- }
204
- catch (e) {
205
- console.error(` Account setup IMAP error: ${e.message}`);
206
- }
207
- }
208
- res.json({ ok: true, message: "Account added and syncing." });
112
+ const result = await svc.setupAccount(name, email, password);
113
+ if (!result.ok)
114
+ res.status(400);
115
+ res.json(result);
209
116
  }
210
117
  catch (e) {
211
118
  res.status(500).json({ error: e.message });
@@ -214,40 +121,8 @@ export function createApiRouter(db, imapManager) {
214
121
  // ── Repair: restore accounts from DB cache to settings, re-register IMAP ──
215
122
  router.post("/repair-accounts", async (req, res) => {
216
123
  try {
217
- // Get accounts from DB (stale but present)
218
- const dbAccounts = db.getAccountConfigs();
219
- if (dbAccounts.length === 0) {
220
- res.json({ ok: false, error: "No cached accounts in database" });
221
- return;
222
- }
223
- // Rebuild account configs from DB's stored config_json
224
- const restored = [];
225
- for (const a of dbAccounts) {
226
- try {
227
- const cfg = JSON.parse(a.configJson);
228
- restored.push(cfg);
229
- }
230
- catch { /* skip corrupt entries */ }
231
- }
232
- if (restored.length === 0) {
233
- res.json({ ok: false, error: "Could not parse cached account configs" });
234
- return;
235
- }
236
- // Save back to shared dir (and cloud API if active)
237
- saveAccounts(restored);
238
- // Re-register in IMAP manager
239
- for (const acct of restored) {
240
- try {
241
- await imapManager.addAccount(acct);
242
- console.log(` [repair] Re-registered account: ${acct.name} (${acct.id})`);
243
- }
244
- catch (e) {
245
- console.error(` [repair] Failed to register ${acct.id}: ${e.message}`);
246
- }
247
- }
248
- // Start sync
249
- imapManager.syncAll().catch(() => { });
250
- res.json({ ok: true, message: `Restored ${restored.length} account(s) and started sync.` });
124
+ const result = await svc.repairAccounts();
125
+ res.json(result);
251
126
  }
252
127
  catch (e) {
253
128
  res.status(500).json({ error: e.message });
@@ -164,6 +164,7 @@ export declare class ImapManager extends EventEmitter {
164
164
  processOutbox(accountId: string): Promise<void>;
165
165
  /** Start background Outbox worker — runs immediately then every 10 seconds */
166
166
  private outboxBackoff;
167
+ private outboxBackoffDelay;
167
168
  startOutboxWorker(): void;
168
169
  /** Stop Outbox worker */
169
170
  stopOutboxWorker(): void;
@@ -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 = 2; // 1 for sync/fetch, 1 for IDLE
111
+ static MAX_CONNECTIONS = 5; // sync, fetch, IDLE, outbox, sync-actions
112
112
  constructor(db) {
113
113
  super();
114
114
  this.db = db;
@@ -1144,7 +1144,7 @@ export class ImapManager extends EventEmitter {
1144
1144
  if (actions.length === 0)
1145
1145
  return;
1146
1146
  const folders = this.db.getFolders(accountId);
1147
- const client = this.createClient(accountId);
1147
+ const client = await this.createClientWithLimit(accountId);
1148
1148
  try {
1149
1149
  for (const action of actions) {
1150
1150
  const folder = folders.find(f => f.id === action.folderId);
@@ -1592,7 +1592,8 @@ export class ImapManager extends EventEmitter {
1592
1592
  }
1593
1593
  }
1594
1594
  /** Start background Outbox worker — runs immediately then every 10 seconds */
1595
- outboxBackoff = new Map(); // accountId → next retry time
1595
+ outboxBackoff = new Map(); // accountId → next retry timestamp
1596
+ outboxBackoffDelay = new Map(); // accountId → current delay ms
1596
1597
  startOutboxWorker() {
1597
1598
  if (this.outboxInterval)
1598
1599
  return;
@@ -1606,12 +1607,15 @@ export class ImapManager extends EventEmitter {
1606
1607
  try {
1607
1608
  await this.processLocalQueue(accountId);
1608
1609
  await this.processOutbox(accountId);
1609
- this.outboxBackoff.delete(accountId); // success — clear backoff
1610
+ // Success — clear backoff
1611
+ this.outboxBackoff.delete(accountId);
1612
+ this.outboxBackoffDelay.delete(accountId);
1610
1613
  }
1611
1614
  catch (e) {
1612
- // Exponential backoff: 30s, 60s, 120s, max 5min
1613
- const prev = this.outboxBackoff.get(accountId);
1614
- const delay = prev ? Math.min((now - prev + 30000) * 2, 300000) : 30000;
1615
+ // Exponential backoff: 30s 60s 120s → 300s (max 5min)
1616
+ const prevDelay = this.outboxBackoffDelay.get(accountId) || 0;
1617
+ const delay = prevDelay ? Math.min(prevDelay * 2, 300000) : 30000;
1618
+ this.outboxBackoffDelay.set(accountId, delay);
1615
1619
  this.outboxBackoff.set(accountId, now + delay);
1616
1620
  console.error(` [outbox] Error for ${accountId}: ${imapError(e)} (retry in ${Math.round(delay / 1000)}s)`);
1617
1621
  }
@@ -1723,6 +1727,7 @@ export class ImapManager extends EventEmitter {
1723
1727
  this.stopPeriodicSync();
1724
1728
  this.stopOutboxWorker();
1725
1729
  await this.stopWatching();
1730
+ // Disconnect all persistent fetch clients
1726
1731
  for (const [, client] of this.fetchClients) {
1727
1732
  try {
1728
1733
  await client.logout();
@@ -1730,6 +1735,19 @@ export class ImapManager extends EventEmitter {
1730
1735
  catch { /* ignore */ }
1731
1736
  }
1732
1737
  this.fetchClients.clear();
1738
+ // Force-release all semaphore slots to unblock any waiting operations
1739
+ for (const [accountId, sem] of this.connectionSemaphore) {
1740
+ sem.active = 0;
1741
+ for (const waiter of sem.waiting) {
1742
+ try {
1743
+ waiter();
1744
+ }
1745
+ catch { /* */ }
1746
+ }
1747
+ sem.waiting.length = 0;
1748
+ }
1749
+ this.connectionSemaphore.clear();
1750
+ this.activeConnections.clear();
1733
1751
  }
1734
1752
  }
1735
1753
  //# sourceMappingURL=index.js.map
@@ -61,6 +61,16 @@ export declare class MailxService {
61
61
  provider: string;
62
62
  mode: string;
63
63
  };
64
+ setupAccount(name: string, email: string, password?: string): Promise<{
65
+ ok: boolean;
66
+ error?: string;
67
+ message?: string;
68
+ }>;
69
+ repairAccounts(): Promise<{
70
+ ok: boolean;
71
+ error?: string;
72
+ message?: string;
73
+ }>;
64
74
  getAutocompleteSettings(): AutocompleteSettings;
65
75
  saveAutocompleteSettings(settings: AutocompleteSettings): void;
66
76
  autocomplete(req: AutocompleteRequest): Promise<AutocompleteResponse>;
@@ -3,8 +3,32 @@
3
3
  * Pure business logic — no HTTP, no Express.
4
4
  * Both the Express API (mailx-api) and the Android bridge call these functions.
5
5
  */
6
- import { loadSettings, saveSettings, loadAllowlist, saveAllowlist, loadAutocomplete, saveAutocomplete, getStorePath, getStorageInfo } from "@bobfrankston/mailx-settings";
6
+ import * as dns from "node:dns/promises";
7
+ import { loadSettings, saveSettings, loadAccounts, loadAccountsAsync, saveAccounts, initCloudConfig, loadAllowlist, saveAllowlist, loadAutocomplete, saveAutocomplete, getStorePath, getStorageInfo } from "@bobfrankston/mailx-settings";
7
8
  import { simpleParser } from "mailparser";
9
+ // ── Email provider detection (MX-based) ──
10
+ const GOOGLE_DOMAINS = ["gmail.com", "googlemail.com"];
11
+ const MS_DOMAINS = ["outlook.com", "hotmail.com", "live.com"];
12
+ async function detectEmailProvider(domain) {
13
+ if (GOOGLE_DOMAINS.includes(domain))
14
+ return { cloud: "gdrive", imapHost: "imap.gmail.com", smtpHost: "smtp.gmail.com", auth: "oauth2" };
15
+ if (MS_DOMAINS.includes(domain))
16
+ return { imapHost: "outlook.office365.com", smtpHost: "smtp.office365.com", auth: "oauth2" };
17
+ try {
18
+ const records = await dns.resolveMx(domain);
19
+ for (const mx of records) {
20
+ const host = mx.exchange.toLowerCase();
21
+ if (host.endsWith(".google.com") || host.endsWith(".googlemail.com")) {
22
+ return { cloud: "gdrive", imapHost: "imap.gmail.com", smtpHost: "smtp.gmail.com", auth: "oauth2" };
23
+ }
24
+ if (host.endsWith(".outlook.com") || host.endsWith(".protection.outlook.com")) {
25
+ return { imapHost: "outlook.office365.com", smtpHost: "smtp.office365.com", auth: "oauth2" };
26
+ }
27
+ }
28
+ }
29
+ catch { /* DNS lookup failed */ }
30
+ return null;
31
+ }
8
32
  // ── Sanitize ──
9
33
  export function sanitizeHtml(html) {
10
34
  let hasRemoteContent = false;
@@ -506,6 +530,82 @@ export class MailxService {
506
530
  getStorageInfo() {
507
531
  return getStorageInfo();
508
532
  }
533
+ // ── Setup & Repair ──
534
+ async setupAccount(name, email, password) {
535
+ if (!email)
536
+ return { ok: false, error: "Email address required" };
537
+ const domain = email.split("@")[1]?.toLowerCase() || "";
538
+ const detected = await detectEmailProvider(domain);
539
+ if (detected?.cloud) {
540
+ await initCloudConfig(detected.cloud);
541
+ }
542
+ // Try to load existing accounts from cloud (merge, don't overwrite)
543
+ let accounts = loadAccounts();
544
+ if (accounts.length === 0) {
545
+ accounts = await loadAccountsAsync();
546
+ }
547
+ // Build the new account entry
548
+ const account = { email, name: name || email.split("@")[0] };
549
+ if (password)
550
+ account.password = password;
551
+ if (detected && !["gmail.com", "googlemail.com", "outlook.com", "hotmail.com", "live.com"].includes(domain)) {
552
+ account.imap = { host: detected.imapHost, port: 993, tls: true, auth: detected.auth, user: email };
553
+ account.smtp = { host: detected.smtpHost, port: 587, tls: true, auth: detected.auth, user: email };
554
+ }
555
+ const id = domain.split(".")[0] || "account";
556
+ // Add new account if not already present
557
+ if (!accounts.some((a) => a.email?.toLowerCase() === email.toLowerCase())) {
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
566
+ const settings = loadSettings();
567
+ for (const acct of settings.accounts) {
568
+ if (!acct.enabled)
569
+ continue;
570
+ try {
571
+ await this.imapManager.addAccount(acct);
572
+ console.log(` Account loaded: ${acct.label || acct.name} (${acct.id})`);
573
+ }
574
+ catch (e) {
575
+ console.error(` Account ${acct.id} error: ${e.message}`);
576
+ }
577
+ }
578
+ this.imapManager.syncAll().catch(() => { });
579
+ return { ok: true, message: `${settings.accounts.length} account(s) configured and syncing.` };
580
+ }
581
+ async repairAccounts() {
582
+ const dbAccounts = this.db.getAccountConfigs();
583
+ if (dbAccounts.length === 0) {
584
+ return { ok: false, error: "No cached accounts in database" };
585
+ }
586
+ const restored = [];
587
+ for (const a of dbAccounts) {
588
+ try {
589
+ restored.push(JSON.parse(a.configJson));
590
+ }
591
+ catch { /* skip corrupt */ }
592
+ }
593
+ if (restored.length === 0) {
594
+ return { ok: false, error: "Could not parse cached account configs" };
595
+ }
596
+ saveAccounts(restored);
597
+ for (const acct of restored) {
598
+ try {
599
+ await this.imapManager.addAccount(acct);
600
+ console.log(` [repair] Re-registered account: ${acct.name} (${acct.id})`);
601
+ }
602
+ catch (e) {
603
+ console.error(` [repair] Failed to register ${acct.id}: ${e.message}`);
604
+ }
605
+ }
606
+ this.imapManager.syncAll().catch(() => { });
607
+ return { ok: true, message: `Restored ${restored.length} account(s) and started sync.` };
608
+ }
509
609
  // ── Autocomplete ──
510
610
  getAutocompleteSettings() {
511
611
  return loadAutocomplete();
@@ -4,6 +4,11 @@
4
4
  * Used by msger's bidirectional IPC (Phase 3) and can also
5
5
  * serve as a stdio JSON-RPC interface for testing.
6
6
  */
7
+ import { readFileSync } from "node:fs";
8
+ import { join, dirname } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const rootPkg = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8"));
7
12
  /** Dispatch an incoming mailxapi call to the appropriate MailxService method */
8
13
  export async function dispatch(svc, req) {
9
14
  const { _action, _cbid, ...params } = req;
@@ -93,11 +98,24 @@ async function dispatchAction(svc, action, p) {
93
98
  case "allowRemoteContent":
94
99
  svc.allowRemoteContent(p.type, p.value);
95
100
  return { ok: true };
96
- case "getVersion":
97
- return svc.getStorageInfo();
101
+ case "getVersion": {
102
+ const settings = svc.getSettings();
103
+ const storage = svc.getStorageInfo();
104
+ return { version: rootPkg.version || "dev", theme: settings.ui?.theme || "system", storage };
105
+ }
98
106
  // Autocomplete
99
107
  case "autocomplete":
100
108
  return svc.autocomplete(p);
109
+ case "getAutocompleteSettings":
110
+ return svc.getAutocompleteSettings();
111
+ case "saveAutocompleteSettings":
112
+ svc.saveAutocompleteSettings(p);
113
+ return { ok: true };
114
+ // Setup & Repair
115
+ case "setupAccount":
116
+ return svc.setupAccount(p.name, p.email, p.password);
117
+ case "repairAccounts":
118
+ return svc.repairAccounts();
101
119
  default:
102
120
  throw new Error(`Unknown action: ${action}`);
103
121
  }
@@ -51,28 +51,12 @@ function readLocalConfig() {
51
51
  }
52
52
  /** Resolve provider config to a filesystem path (checks for local Google Drive mount) */
53
53
  function resolveProvider(cfg) {
54
- const home = process.env.USERPROFILE || process.env.HOME || "";
55
- const rel = cfg.path; // e.g., "home/.mailx"
56
- switch (cfg.provider) {
57
- case "google":
58
- case "gdrive": {
59
- const candidates = [
60
- home && path.join(home, "Google Drive", "My Drive", rel),
61
- home && path.join(home, "Google Drive Streaming", "My Drive", rel),
62
- ];
63
- // Check drive letters on Windows
64
- if (process.platform === "win32") {
65
- for (const letter of ["G", "H", "I", "J", "K"]) {
66
- candidates.push(path.join(`${letter}:`, "My Drive", rel));
67
- }
68
- }
69
- return candidates.filter(Boolean).find(p => fs.existsSync(p));
70
- }
71
- case "local":
72
- return resolvePath(rel);
73
- default:
74
- return undefined;
75
- }
54
+ // Cloud providers use API only no filesystem mount scanning
55
+ if (cfg.provider === "gdrive" || cfg.provider === "google")
56
+ return undefined;
57
+ if (cfg.provider === "local")
58
+ return resolvePath(cfg.path);
59
+ return undefined;
76
60
  }
77
61
  /** Pending cloud config for API fallback (set when mount not found) */
78
62
  let pendingCloudConfig = null;
@@ -173,17 +157,25 @@ export function getStorageInfo() {
173
157
  if (config.sharedDir) {
174
158
  const entries = Array.isArray(config.sharedDir) ? config.sharedDir : [config.sharedDir];
175
159
  for (const entry of entries) {
176
- const resolved = resolveSharedEntry(entry);
177
- if (resolved && resolved !== LOCAL_DIR) {
178
- if (typeof entry === "string") {
179
- // Legacy string path — filesystem only, no cloud label
160
+ if (typeof entry === "string") {
161
+ const resolved = resolveSharedEntry(entry);
162
+ if (resolved && resolved !== LOCAL_DIR) {
180
163
  return { provider: "local", mode: "local", cloudPath: resolved };
181
164
  }
182
- const name = (entry.provider === "gdrive" || entry.provider === "google") ? "gdrive" : entry.provider;
165
+ continue;
166
+ }
167
+ // Provider-based entry — check for API mode (folderId) first, then mount
168
+ const name = (entry.provider === "gdrive" || entry.provider === "google") ? "gdrive" : entry.provider;
169
+ if (entry.folderId) {
170
+ // Has folder ID → API mode (don't scan filesystem for mounts)
171
+ return { provider: name, mode: "api", cloudPath: entry.path, cloudError: lastCloudError || undefined };
172
+ }
173
+ const resolved = resolveSharedEntry(entry);
174
+ if (resolved && resolved !== LOCAL_DIR) {
183
175
  return { provider: name, mode: "mount", cloudPath: entry.path };
184
176
  }
185
177
  }
186
- // Not mounted — using API
178
+ // Not mounted and no folderId check pendingCloudConfig from initCloudConfig()
187
179
  if (pendingCloudConfig) {
188
180
  const name = (pendingCloudConfig.provider === "gdrive" || pendingCloudConfig.provider === "google") ? "gdrive" : pendingCloudConfig.provider;
189
181
  return { provider: name, mode: "api", cloudPath: pendingCloudConfig.path, cloudError: lastCloudError || undefined };
@@ -266,26 +258,37 @@ function saveFile(filename, data) {
266
258
  }
267
259
  const PROVIDERS = {
268
260
  "gmail.com": {
261
+ label: "Gmail",
269
262
  imap: { host: "imap.gmail.com", port: 993, tls: true, auth: "oauth2" },
270
263
  smtp: { host: "smtp.gmail.com", port: 587, tls: true, auth: "oauth2" },
271
264
  },
272
265
  "googlemail.com": {
266
+ label: "Gmail",
273
267
  imap: { host: "imap.gmail.com", port: 993, tls: true, auth: "oauth2" },
274
268
  smtp: { host: "smtp.gmail.com", port: 587, tls: true, auth: "oauth2" },
275
269
  },
276
270
  "outlook.com": {
271
+ label: "Outlook",
277
272
  imap: { host: "outlook.office365.com", port: 993, tls: true, auth: "oauth2" },
278
273
  smtp: { host: "smtp.office365.com", port: 587, tls: true, auth: "oauth2" },
279
274
  },
280
275
  "hotmail.com": {
276
+ label: "Hotmail",
281
277
  imap: { host: "outlook.office365.com", port: 993, tls: true, auth: "oauth2" },
282
278
  smtp: { host: "smtp.office365.com", port: 587, tls: true, auth: "oauth2" },
283
279
  },
284
280
  "yahoo.com": {
281
+ label: "Yahoo",
285
282
  imap: { host: "imap.mail.yahoo.com", port: 993, tls: true, auth: "password" },
286
283
  smtp: { host: "smtp.mail.yahoo.com", port: 587, tls: true, auth: "password" },
287
284
  },
285
+ "aol.com": {
286
+ label: "AOL",
287
+ imap: { host: "imap.aol.com", port: 993, tls: true, auth: "password" },
288
+ smtp: { host: "smtp.aol.com", port: 587, tls: true, auth: "password" },
289
+ },
288
290
  "icloud.com": {
291
+ label: "iCloud",
289
292
  imap: { host: "imap.mail.me.com", port: 993, tls: true, auth: "password" },
290
293
  smtp: { host: "smtp.mail.me.com", port: 587, tls: true, auth: "password" },
291
294
  },
@@ -299,7 +302,7 @@ function normalizeAccount(acct, globalName) {
299
302
  return {
300
303
  id: acct.id || domain.split(".")[0] || "account",
301
304
  name: acct.name || globalName || email.split("@")[0],
302
- label: acct.label,
305
+ label: acct.label || provider?.label,
303
306
  email,
304
307
  imap: {
305
308
  host: acct.imap?.host || provider?.imap.host || `imap.${domain}`,