@bobfrankston/mailx 1.0.147 → 1.0.151
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/README.md +275 -165
- package/bin/mailx.js +142 -21
- package/client/app.js +35 -36
- package/client/components/folder-tree.js +21 -13
- package/client/compose/compose.js +20 -45
- package/client/icon.png +0 -0
- package/client/lib/api-client.js +34 -0
- package/client/package.json +1 -1
- package/package.json +3 -3
- package/packages/mailx-api/index.js +6 -131
- package/packages/mailx-imap/index.js +16 -2
- package/packages/mailx-service/index.d.ts +10 -0
- package/packages/mailx-service/index.js +101 -1
- package/packages/mailx-service/jsonrpc.js +24 -3
- package/packages/mailx-settings/index.js +32 -29
|
@@ -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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
218
|
-
|
|
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 });
|
|
@@ -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 = 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.
|
|
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);
|
|
@@ -1727,6 +1727,7 @@ export class ImapManager extends EventEmitter {
|
|
|
1727
1727
|
this.stopPeriodicSync();
|
|
1728
1728
|
this.stopOutboxWorker();
|
|
1729
1729
|
await this.stopWatching();
|
|
1730
|
+
// Disconnect all persistent fetch clients
|
|
1730
1731
|
for (const [, client] of this.fetchClients) {
|
|
1731
1732
|
try {
|
|
1732
1733
|
await client.logout();
|
|
@@ -1734,6 +1735,19 @@ export class ImapManager extends EventEmitter {
|
|
|
1734
1735
|
catch { /* ignore */ }
|
|
1735
1736
|
}
|
|
1736
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();
|
|
1737
1751
|
}
|
|
1738
1752
|
}
|
|
1739
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
|
|
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;
|
|
@@ -94,13 +99,29 @@ async function dispatchAction(svc, action, p) {
|
|
|
94
99
|
svc.allowRemoteContent(p.type, p.value);
|
|
95
100
|
return { ok: true };
|
|
96
101
|
case "getVersion": {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
try {
|
|
103
|
+
const settings = svc.getSettings();
|
|
104
|
+
const storage = svc.getStorageInfo();
|
|
105
|
+
return { version: rootPkg.version || "dev", theme: settings.ui?.theme || "system", storage };
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
console.error(` [jsonrpc] getVersion error: ${e.message}`);
|
|
109
|
+
return { version: rootPkg.version || "dev", theme: "system", storage: {} };
|
|
110
|
+
}
|
|
100
111
|
}
|
|
101
112
|
// Autocomplete
|
|
102
113
|
case "autocomplete":
|
|
103
114
|
return svc.autocomplete(p);
|
|
115
|
+
case "getAutocompleteSettings":
|
|
116
|
+
return svc.getAutocompleteSettings();
|
|
117
|
+
case "saveAutocompleteSettings":
|
|
118
|
+
svc.saveAutocompleteSettings(p);
|
|
119
|
+
return { ok: true };
|
|
120
|
+
// Setup & Repair
|
|
121
|
+
case "setupAccount":
|
|
122
|
+
return svc.setupAccount(p.name, p.email, p.password);
|
|
123
|
+
case "repairAccounts":
|
|
124
|
+
return svc.repairAccounts();
|
|
104
125
|
default:
|
|
105
126
|
throw new Error(`Unknown action: ${action}`);
|
|
106
127
|
}
|
|
@@ -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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
if (
|
|
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
|
-
|
|
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 —
|
|
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}`,
|