@bobfrankston/mailx 1.0.136 → 1.0.138
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/client/app.js +8 -2
- package/client/components/folder-tree.js +3 -2
- package/package.json +1 -1
- package/packages/mailx-api/index.js +41 -7
- package/packages/mailx-server/index.js +16 -2
- package/packages/mailx-settings/cloud.d.ts +14 -5
- package/packages/mailx-settings/cloud.js +13 -113
- package/packages/mailx-settings/index.d.ts +4 -3
- package/packages/mailx-settings/index.js +19 -81
package/client/app.js
CHANGED
|
@@ -904,10 +904,16 @@ fetch("/api/version").then(r => r.json()).then(d => {
|
|
|
904
904
|
const el = document.getElementById("app-version");
|
|
905
905
|
const storage = d.storage || {};
|
|
906
906
|
const storageLabel = storage.provider && storage.provider !== "local"
|
|
907
|
-
? ` [${storage.provider}
|
|
907
|
+
? ` [${storage.provider}]`
|
|
908
908
|
: "";
|
|
909
|
-
if (el)
|
|
909
|
+
if (el) {
|
|
910
910
|
el.textContent = `mailx v${d.version}${storageLabel}${isApp ? "" : " [browser]"}`;
|
|
911
|
+
// Tooltip: show cloud path and access mode on hover
|
|
912
|
+
if (storage.provider && storage.provider !== "local") {
|
|
913
|
+
const modeDesc = storage.mode === "api" ? "API" : "mount";
|
|
914
|
+
el.title = `${storage.cloudPath || storage.provider} (${modeDesc})`;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
911
917
|
if (d.settingsError) {
|
|
912
918
|
showAlert(d.settingsError, "settings-error");
|
|
913
919
|
// Add repair button to the banner
|
|
@@ -367,8 +367,9 @@ async function loadFolderTree(container) {
|
|
|
367
367
|
</form>
|
|
368
368
|
<details style="margin-top:2rem;color:var(--color-text-muted)">
|
|
369
369
|
<summary>Manual setup (advanced)</summary>
|
|
370
|
-
<p style="margin-top:0.5rem">Create <code>~/.mailx/
|
|
371
|
-
<code style="display:block;padding:0.75rem;background:var(--color-bg-surface);border:1px solid var(--color-border);border-radius:4px;margin:0.5rem 0;white-space:pre;font-size:0.85rem">{ "sharedDir": "
|
|
370
|
+
<p style="margin-top:0.5rem">Create <code>~/.mailx/config.jsonc</code> with a cloud provider:</p>
|
|
371
|
+
<code style="display:block;padding:0.75rem;background:var(--color-bg-surface);border:1px solid var(--color-border);border-radius:4px;margin:0.5rem 0;white-space:pre;font-size:0.85rem">{ "sharedDir": { "provider": "gdrive", "path": "home/.mailx" } }</code>
|
|
372
|
+
<p style="margin-top:0.5rem;font-size:0.85rem">Settings sync via Google Drive API (auto-configured for Gmail accounts).</p>
|
|
372
373
|
</details>
|
|
373
374
|
</div>`;
|
|
374
375
|
// Wire up the setup form
|
package/package.json
CHANGED
|
@@ -3,8 +3,38 @@
|
|
|
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";
|
|
6
7
|
import { MailxService } from "@bobfrankston/mailx-service";
|
|
7
|
-
import { loadAccounts, loadAccountsAsync, saveAccounts,
|
|
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
|
+
}
|
|
8
38
|
export function createApiRouter(db, imapManager) {
|
|
9
39
|
const svc = new MailxService(db, imapManager);
|
|
10
40
|
const router = Router();
|
|
@@ -114,17 +144,21 @@ export function createApiRouter(db, imapManager) {
|
|
|
114
144
|
res.status(400).json({ error: "Email address required" });
|
|
115
145
|
return;
|
|
116
146
|
}
|
|
117
|
-
//
|
|
118
|
-
initLocalConfig();
|
|
119
|
-
// Default to gdrive for Gmail users (creates config.jsonc with gdrive provider)
|
|
147
|
+
// Detect provider via domain or MX records (Google Workspace, Microsoft 365 custom domains)
|
|
120
148
|
const domain = email.split("@")[1]?.toLowerCase() || "";
|
|
121
|
-
|
|
122
|
-
|
|
149
|
+
const detected = await detectEmailProvider(domain);
|
|
150
|
+
if (detected?.cloud) {
|
|
151
|
+
initCloudConfig(detected.cloud);
|
|
123
152
|
}
|
|
124
|
-
// Build account config
|
|
153
|
+
// Build account config
|
|
125
154
|
const account = { email, name: name || email.split("@")[0] };
|
|
126
155
|
if (password)
|
|
127
156
|
account.password = password;
|
|
157
|
+
// For custom domains hosted on Google/Microsoft, set correct IMAP/SMTP servers
|
|
158
|
+
if (detected && !["gmail.com", "googlemail.com", "outlook.com", "hotmail.com", "live.com"].includes(domain)) {
|
|
159
|
+
account.imap = { host: detected.imapHost, port: 993, tls: true, auth: detected.auth, user: email };
|
|
160
|
+
account.smtp = { host: detected.smtpHost, port: 587, tls: true, auth: detected.auth, user: email };
|
|
161
|
+
}
|
|
128
162
|
// Load existing accounts — try cloud API in case home/.mailx already exists on Drive
|
|
129
163
|
let accounts = loadAccounts();
|
|
130
164
|
if (accounts.length === 0) {
|
|
@@ -55,7 +55,7 @@ if (settings.accounts.length === 0) {
|
|
|
55
55
|
console.log(` Loaded ${cloudAccounts.length} account(s) from cloud API`);
|
|
56
56
|
}
|
|
57
57
|
else {
|
|
58
|
-
console.log(" No accounts configured. Open http://127.0.0.1:9333 to
|
|
58
|
+
console.log(" No accounts configured. Open http://127.0.0.1:9333 to add your email account.");
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
const dbDir = getConfigDir();
|
|
@@ -100,7 +100,7 @@ app.get("/api/version", (req, res) => {
|
|
|
100
100
|
const settingsError = (dbAccounts > 0 && imapAccounts === 0)
|
|
101
101
|
? "No accounts loaded from settings — showing stale data. Check accounts.jsonc on your cloud drive."
|
|
102
102
|
: undefined;
|
|
103
|
-
res.json({ version: SERVER_VERSION, theme: settings.ui?.theme || "system", storage, imapAccounts, settingsError });
|
|
103
|
+
res.json({ version: SERVER_VERSION, theme: settings.ui?.theme || "system", storage, imapAccounts, hasAccounts: imapAccounts > 0 || dbAccounts > 0, settingsError });
|
|
104
104
|
});
|
|
105
105
|
app.all("/info", (req, res) => {
|
|
106
106
|
res.json({ version: SERVER_VERSION, uptime: Math.round(process.uptime()), port: PORT, imap: imapManager.useNativeClient ? "native" : "imapflow" });
|
|
@@ -203,10 +203,24 @@ const clients = new Set();
|
|
|
203
203
|
function wireWebSocket() {
|
|
204
204
|
wss.on("connection", (ws) => {
|
|
205
205
|
clients.add(ws);
|
|
206
|
+
ws.__alive = true;
|
|
206
207
|
const connected = { type: "connected" };
|
|
207
208
|
ws.send(JSON.stringify(connected));
|
|
209
|
+
ws.on("pong", () => { ws.__alive = true; });
|
|
208
210
|
ws.on("close", () => clients.delete(ws));
|
|
209
211
|
});
|
|
212
|
+
// Heartbeat — detect dead connections, prevent CLOSE_WAIT zombie accumulation
|
|
213
|
+
setInterval(() => {
|
|
214
|
+
for (const ws of clients) {
|
|
215
|
+
if (ws.__alive === false) {
|
|
216
|
+
clients.delete(ws);
|
|
217
|
+
ws.terminate();
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
ws.__alive = false;
|
|
221
|
+
ws.ping();
|
|
222
|
+
}
|
|
223
|
+
}, 30000);
|
|
210
224
|
}
|
|
211
225
|
function broadcast(event) {
|
|
212
226
|
const data = JSON.stringify(event);
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Cloud storage
|
|
3
|
-
* Reads/writes settings files on
|
|
4
|
-
* when the cloud drive is not mounted locally.
|
|
2
|
+
* Cloud storage for mailx settings — Google Drive API.
|
|
3
|
+
* Reads/writes settings files on Google Drive when no local mount is available.
|
|
5
4
|
* Falls back to local cache when offline.
|
|
5
|
+
*
|
|
6
|
+
* ── Restoring removed providers ──
|
|
7
|
+
* OneDrive (removed 2026-04-06): Required microsoft-credentials.json in ~/.mailx/,
|
|
8
|
+
* MS_SCOPES = "https://graph.microsoft.com/Files.ReadWrite offline_access",
|
|
9
|
+
* authenticateOAuth with tokenDir ~/.mailx/tokens/microsoft/.
|
|
10
|
+
* Read/write via Graph API: GET/PUT https://graph.microsoft.com/v1.0/me/drive/root:/{path}:/content
|
|
11
|
+
* Azure AD app registration needed: https://portal.azure.com → App registrations → Desktop app
|
|
12
|
+
*
|
|
13
|
+
* Dropbox (removed 2026-04-06): Never implemented — placeholder only.
|
|
14
|
+
* Would need Dropbox OAuth app, token management, and Dropbox API v2 calls.
|
|
6
15
|
*/
|
|
7
|
-
export type CloudProvider = "
|
|
16
|
+
export type CloudProvider = "gdrive" | "google" | "local";
|
|
8
17
|
export interface CloudFile {
|
|
9
18
|
read(filePath: string): Promise<string | null>;
|
|
10
19
|
write(filePath: string, content: string): Promise<boolean>;
|
|
11
20
|
exists(filePath: string): Promise<boolean>;
|
|
12
21
|
}
|
|
13
|
-
export declare function getCloudProvider(provider:
|
|
22
|
+
export declare function getCloudProvider(provider: string): CloudFile | null;
|
|
14
23
|
//# sourceMappingURL=cloud.d.ts.map
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Cloud storage
|
|
3
|
-
* Reads/writes settings files on
|
|
4
|
-
* when the cloud drive is not mounted locally.
|
|
2
|
+
* Cloud storage for mailx settings — Google Drive API.
|
|
3
|
+
* Reads/writes settings files on Google Drive when no local mount is available.
|
|
5
4
|
* Falls back to local cache when offline.
|
|
5
|
+
*
|
|
6
|
+
* ── Restoring removed providers ──
|
|
7
|
+
* OneDrive (removed 2026-04-06): Required microsoft-credentials.json in ~/.mailx/,
|
|
8
|
+
* MS_SCOPES = "https://graph.microsoft.com/Files.ReadWrite offline_access",
|
|
9
|
+
* authenticateOAuth with tokenDir ~/.mailx/tokens/microsoft/.
|
|
10
|
+
* Read/write via Graph API: GET/PUT https://graph.microsoft.com/v1.0/me/drive/root:/{path}:/content
|
|
11
|
+
* Azure AD app registration needed: https://portal.azure.com → App registrations → Desktop app
|
|
12
|
+
*
|
|
13
|
+
* Dropbox (removed 2026-04-06): Never implemented — placeholder only.
|
|
14
|
+
* Would need Dropbox OAuth app, token management, and Dropbox API v2 calls.
|
|
6
15
|
*/
|
|
7
16
|
import fs from "node:fs";
|
|
8
17
|
import path from "node:path";
|
|
9
18
|
import { authenticateOAuth } from "@bobfrankston/oauthsupport";
|
|
10
19
|
const SETTINGS_DIR = path.join(process.env.USERPROFILE || process.env.HOME || ".", ".mailx");
|
|
11
20
|
// ── Credentials ──
|
|
12
|
-
// Microsoft Graph: needs app registration in Azure AD
|
|
13
|
-
// Create at: https://portal.azure.com → App registrations → New → Desktop app
|
|
14
|
-
const MS_CREDENTIALS_PATH = path.join(SETTINGS_DIR, "microsoft-credentials.json");
|
|
15
|
-
const MS_TOKEN_DIR = path.join(SETTINGS_DIR, "tokens", "microsoft");
|
|
16
|
-
const MS_SCOPES = "https://graph.microsoft.com/Files.ReadWrite offline_access";
|
|
17
21
|
// Google Drive: reuse iflow's OAuth credentials (same Google Cloud project)
|
|
18
22
|
function findGoogleCredentials() {
|
|
19
23
|
// Check mailx local dir first, then iflow package
|
|
@@ -42,23 +46,6 @@ function findGoogleCredentials() {
|
|
|
42
46
|
const GDRIVE_TOKEN_DIR = path.join(SETTINGS_DIR, "tokens", "gdrive");
|
|
43
47
|
const GDRIVE_SCOPES = "https://www.googleapis.com/auth/drive.file";
|
|
44
48
|
// ── Token helpers ──
|
|
45
|
-
async function getMicrosoftToken() {
|
|
46
|
-
if (!fs.existsSync(MS_CREDENTIALS_PATH))
|
|
47
|
-
return null;
|
|
48
|
-
try {
|
|
49
|
-
const token = await authenticateOAuth(MS_CREDENTIALS_PATH, {
|
|
50
|
-
scope: MS_SCOPES,
|
|
51
|
-
tokenDirectory: MS_TOKEN_DIR,
|
|
52
|
-
tokenFileName: "token.json",
|
|
53
|
-
includeOfflineAccess: true,
|
|
54
|
-
});
|
|
55
|
-
return token?.access_token || null;
|
|
56
|
-
}
|
|
57
|
-
catch (e) {
|
|
58
|
-
console.error(` [cloud] Microsoft auth failed: ${e.message}`);
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
49
|
async function getGoogleDriveToken() {
|
|
63
50
|
const creds = findGoogleCredentials();
|
|
64
51
|
if (!creds) {
|
|
@@ -80,86 +67,7 @@ async function getGoogleDriveToken() {
|
|
|
80
67
|
return null;
|
|
81
68
|
}
|
|
82
69
|
}
|
|
83
|
-
// ── OneDrive Graph API ──
|
|
84
|
-
async function oneDriveRead(filePath) {
|
|
85
|
-
const token = await getMicrosoftToken();
|
|
86
|
-
if (!token)
|
|
87
|
-
return null;
|
|
88
|
-
try {
|
|
89
|
-
const encoded = filePath.split("/").map(encodeURIComponent).join("/");
|
|
90
|
-
const res = await fetch(`https://graph.microsoft.com/v1.0/me/drive/root:/${encoded}:/content`, {
|
|
91
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
92
|
-
});
|
|
93
|
-
if (!res.ok)
|
|
94
|
-
return null;
|
|
95
|
-
return await res.text();
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
async function oneDriveWrite(filePath, content) {
|
|
102
|
-
const token = await getMicrosoftToken();
|
|
103
|
-
if (!token)
|
|
104
|
-
return false;
|
|
105
|
-
try {
|
|
106
|
-
const encoded = filePath.split("/").map(encodeURIComponent).join("/");
|
|
107
|
-
const res = await fetch(`https://graph.microsoft.com/v1.0/me/drive/root:/${encoded}:/content`, {
|
|
108
|
-
method: "PUT",
|
|
109
|
-
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
110
|
-
body: content,
|
|
111
|
-
});
|
|
112
|
-
return res.ok;
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
async function oneDriveExists(filePath) {
|
|
119
|
-
const token = await getMicrosoftToken();
|
|
120
|
-
if (!token)
|
|
121
|
-
return false;
|
|
122
|
-
try {
|
|
123
|
-
const encoded = filePath.split("/").map(encodeURIComponent).join("/");
|
|
124
|
-
const res = await fetch(`https://graph.microsoft.com/v1.0/me/drive/root:/${encoded}`, {
|
|
125
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
126
|
-
});
|
|
127
|
-
return res.ok;
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
70
|
// ── Google Drive API ──
|
|
134
|
-
async function gDriveFind(fileName, parentName) {
|
|
135
|
-
const token = await getGoogleDriveToken();
|
|
136
|
-
if (!token)
|
|
137
|
-
return null;
|
|
138
|
-
try {
|
|
139
|
-
let query = `name='${fileName}' and trashed=false`;
|
|
140
|
-
if (parentName) {
|
|
141
|
-
// Find parent folder first
|
|
142
|
-
const parentRes = await fetch(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(`name='${parentName}' and mimeType='application/vnd.google-apps.folder' and trashed=false`)}&fields=files(id)`, {
|
|
143
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
144
|
-
});
|
|
145
|
-
if (parentRes.ok) {
|
|
146
|
-
const data = await parentRes.json();
|
|
147
|
-
if (data.files?.[0])
|
|
148
|
-
query += ` and '${data.files[0].id}' in parents`;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
const res = await fetch(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id,name)`, {
|
|
152
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
153
|
-
});
|
|
154
|
-
if (!res.ok)
|
|
155
|
-
return null;
|
|
156
|
-
const data = await res.json();
|
|
157
|
-
return data.files?.[0]?.id || null;
|
|
158
|
-
}
|
|
159
|
-
catch {
|
|
160
|
-
return null;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
71
|
async function gDriveRead(filePath) {
|
|
164
72
|
const token = await getGoogleDriveToken();
|
|
165
73
|
if (!token) {
|
|
@@ -290,12 +198,6 @@ async function gDriveWrite(filePath, content) {
|
|
|
290
198
|
}
|
|
291
199
|
export function getCloudProvider(provider) {
|
|
292
200
|
switch (provider) {
|
|
293
|
-
case "onedrive":
|
|
294
|
-
return {
|
|
295
|
-
read: oneDriveRead,
|
|
296
|
-
write: oneDriveWrite,
|
|
297
|
-
exists: oneDriveExists,
|
|
298
|
-
};
|
|
299
201
|
case "google":
|
|
300
202
|
case "gdrive":
|
|
301
203
|
return {
|
|
@@ -303,9 +205,6 @@ export function getCloudProvider(provider) {
|
|
|
303
205
|
write: gDriveWrite,
|
|
304
206
|
exists: async (p) => (await gDriveRead(p)) !== null,
|
|
305
207
|
};
|
|
306
|
-
case "dropbox":
|
|
307
|
-
// TODO: Dropbox API
|
|
308
|
-
return null;
|
|
309
208
|
case "local":
|
|
310
209
|
return {
|
|
311
210
|
read: async (p) => { try {
|
|
@@ -324,6 +223,7 @@ export function getCloudProvider(provider) {
|
|
|
324
223
|
exists: async (p) => fs.existsSync(p),
|
|
325
224
|
};
|
|
326
225
|
default:
|
|
226
|
+
console.error(` [cloud] Provider "${provider}" not supported — only gdrive is available`);
|
|
327
227
|
return null;
|
|
328
228
|
}
|
|
329
229
|
}
|
|
@@ -24,10 +24,11 @@ export declare function cloudRead(filename: string): Promise<string | null>;
|
|
|
24
24
|
export declare function cloudWrite(filename: string, content: string): Promise<boolean>;
|
|
25
25
|
/** Whether cloud API fallback is active */
|
|
26
26
|
export declare function isCloudMode(): boolean;
|
|
27
|
-
/** Get storage provider info for display
|
|
27
|
+
/** Get storage provider info for display */
|
|
28
28
|
export declare function getStorageInfo(): {
|
|
29
29
|
provider: string;
|
|
30
30
|
mode: "mount" | "api" | "local";
|
|
31
|
+
cloudPath?: string;
|
|
31
32
|
cloudError?: string;
|
|
32
33
|
};
|
|
33
34
|
declare const DEFAULT_PREFERENCES: {
|
|
@@ -89,8 +90,8 @@ export declare function getConfigDir(): string;
|
|
|
89
90
|
export { getSharedDir };
|
|
90
91
|
/** Initialize local config if it doesn't exist */
|
|
91
92
|
export declare function initLocalConfig(sharedDir?: string, storePath?: string): void;
|
|
92
|
-
/** Initialize config with
|
|
93
|
-
export declare function initCloudConfig(provider
|
|
93
|
+
/** Initialize config with Google Drive cloud storage */
|
|
94
|
+
export declare function initCloudConfig(provider?: "gdrive", cloudPath?: string): void;
|
|
94
95
|
declare const DEFAULT_SETTINGS: MailxSettings;
|
|
95
96
|
/** Get historyDays for an account: per-account override > system override > shared default */
|
|
96
97
|
export declare function getHistoryDays(accountId?: string): number;
|
|
@@ -49,20 +49,11 @@ function readLocalConfig() {
|
|
|
49
49
|
return {};
|
|
50
50
|
return readJsonc(LOCAL_CONFIG_PATH) || {};
|
|
51
51
|
}
|
|
52
|
-
/** Resolve provider config to a filesystem path */
|
|
52
|
+
/** Resolve provider config to a filesystem path (checks for local Google Drive mount) */
|
|
53
53
|
function resolveProvider(cfg) {
|
|
54
54
|
const home = process.env.USERPROFILE || process.env.HOME || "";
|
|
55
55
|
const rel = cfg.path; // e.g., "home/.mailx"
|
|
56
56
|
switch (cfg.provider) {
|
|
57
|
-
case "onedrive": {
|
|
58
|
-
const candidates = [
|
|
59
|
-
process.env.OneDrive && path.join(process.env.OneDrive, rel),
|
|
60
|
-
process.env.OneDriveConsumer && path.join(process.env.OneDriveConsumer, rel),
|
|
61
|
-
home && path.join(home, "OneDrive", rel),
|
|
62
|
-
home && path.join(home, "onedrive", rel),
|
|
63
|
-
].filter(Boolean);
|
|
64
|
-
return candidates.find(p => fs.existsSync(p));
|
|
65
|
-
}
|
|
66
57
|
case "google":
|
|
67
58
|
case "gdrive": {
|
|
68
59
|
const candidates = [
|
|
@@ -77,13 +68,6 @@ function resolveProvider(cfg) {
|
|
|
77
68
|
}
|
|
78
69
|
return candidates.filter(Boolean).find(p => fs.existsSync(p));
|
|
79
70
|
}
|
|
80
|
-
case "dropbox": {
|
|
81
|
-
const candidates = [
|
|
82
|
-
home && path.join(home, "Dropbox", rel),
|
|
83
|
-
home && path.join(home, "dropbox", rel),
|
|
84
|
-
].filter(Boolean);
|
|
85
|
-
return candidates.find(p => fs.existsSync(p));
|
|
86
|
-
}
|
|
87
71
|
case "local":
|
|
88
72
|
return resolvePath(rel);
|
|
89
73
|
default:
|
|
@@ -119,9 +103,7 @@ function getSharedDir() {
|
|
|
119
103
|
}
|
|
120
104
|
}
|
|
121
105
|
}
|
|
122
|
-
// Legacy
|
|
123
|
-
if (config.settingsPath)
|
|
124
|
-
return path.dirname(resolvePath(config.settingsPath));
|
|
106
|
+
// Legacy settingsPath no longer used for shared dir — use loadLegacySettings() for reading only.
|
|
125
107
|
return LOCAL_DIR;
|
|
126
108
|
}
|
|
127
109
|
/** Read a file via cloud API (when filesystem mount not available) */
|
|
@@ -164,7 +146,7 @@ export async function cloudWrite(filename, content) {
|
|
|
164
146
|
export function isCloudMode() {
|
|
165
147
|
return pendingCloudConfig !== null;
|
|
166
148
|
}
|
|
167
|
-
/** Get storage provider info for display
|
|
149
|
+
/** Get storage provider info for display */
|
|
168
150
|
export function getStorageInfo() {
|
|
169
151
|
const config = readLocalConfig();
|
|
170
152
|
if (config.sharedDir) {
|
|
@@ -172,20 +154,18 @@ export function getStorageInfo() {
|
|
|
172
154
|
for (const entry of entries) {
|
|
173
155
|
const resolved = resolveSharedEntry(entry);
|
|
174
156
|
if (resolved && resolved !== LOCAL_DIR) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
return { provider: name, mode: "mount" };
|
|
157
|
+
if (typeof entry === "string") {
|
|
158
|
+
// Legacy string path — filesystem only, no cloud label
|
|
159
|
+
return { provider: "local", mode: "local", cloudPath: resolved };
|
|
160
|
+
}
|
|
161
|
+
const name = (entry.provider === "gdrive" || entry.provider === "google") ? "gdrive" : entry.provider;
|
|
162
|
+
return { provider: name, mode: "mount", cloudPath: entry.path };
|
|
181
163
|
}
|
|
182
164
|
}
|
|
183
|
-
// Not mounted
|
|
165
|
+
// Not mounted — using API
|
|
184
166
|
if (pendingCloudConfig) {
|
|
185
|
-
const name = pendingCloudConfig.provider === "
|
|
186
|
-
|
|
187
|
-
pendingCloudConfig.provider === "dropbox" ? "Dropbox" : pendingCloudConfig.provider;
|
|
188
|
-
return { provider: name, mode: "api", cloudError: lastCloudError || undefined };
|
|
167
|
+
const name = (pendingCloudConfig.provider === "gdrive" || pendingCloudConfig.provider === "google") ? "gdrive" : pendingCloudConfig.provider;
|
|
168
|
+
return { provider: name, mode: "api", cloudPath: pendingCloudConfig.path, cloudError: lastCloudError || undefined };
|
|
189
169
|
}
|
|
190
170
|
}
|
|
191
171
|
return { provider: "local", mode: "local" };
|
|
@@ -528,58 +508,16 @@ export function getConfigDir() {
|
|
|
528
508
|
}
|
|
529
509
|
/** Get the shared settings directory */
|
|
530
510
|
export { getSharedDir };
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
const home = process.env.USERPROFILE || process.env.HOME || "";
|
|
534
|
-
// Scan common drive letters for Google Drive mount
|
|
535
|
-
const driveLetters = [];
|
|
536
|
-
if (process.platform === "win32") {
|
|
537
|
-
for (const letter of ["G", "H", "I", "J", "K"]) {
|
|
538
|
-
driveLetters.push(path.join(`${letter}:`, "My Drive", "home", ".mailx"));
|
|
539
|
-
driveLetters.push(path.join(`${letter}:`, "My Drive", "mailx"));
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
const candidates = [
|
|
543
|
-
// OneDrive (Windows env vars)
|
|
544
|
-
process.env.OneDrive && path.join(process.env.OneDrive, "home", ".mailx"),
|
|
545
|
-
process.env.OneDriveConsumer && path.join(process.env.OneDriveConsumer, "home", ".mailx"),
|
|
546
|
-
// OneDrive (standard paths)
|
|
547
|
-
home && path.join(home, "OneDrive", "home", ".mailx"),
|
|
548
|
-
home && path.join(home, "onedrive", "home", ".mailx"),
|
|
549
|
-
// Google Drive for Desktop — home/.mailx convention (matches OneDrive)
|
|
550
|
-
home && path.join(home, "Google Drive", "My Drive", "home", ".mailx"),
|
|
551
|
-
home && path.join(home, "Google Drive Streaming", "My Drive", "home", ".mailx"),
|
|
552
|
-
// Google Drive — also check mailx at root
|
|
553
|
-
home && path.join(home, "Google Drive", "My Drive", "mailx"),
|
|
554
|
-
home && path.join(home, "Google Drive Streaming", "My Drive", "mailx"),
|
|
555
|
-
// Google Drive mount letters (Windows)
|
|
556
|
-
...driveLetters,
|
|
557
|
-
// Dropbox
|
|
558
|
-
home && path.join(home, "Dropbox", ".mailx"),
|
|
559
|
-
home && path.join(home, "dropbox", ".mailx"),
|
|
560
|
-
].filter(Boolean);
|
|
561
|
-
for (const dir of candidates) {
|
|
562
|
-
if (fs.existsSync(path.join(dir, "settings.jsonc")) || fs.existsSync(path.join(dir, "accounts.jsonc"))) {
|
|
563
|
-
return dir;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
return undefined;
|
|
567
|
-
}
|
|
511
|
+
// detectSharedDir() removed — cloud storage is configured via API (gdrive/onedrive),
|
|
512
|
+
// not auto-detected from filesystem mounts. Setup form triggers initCloudConfig().
|
|
568
513
|
/** Initialize local config if it doesn't exist */
|
|
569
514
|
export function initLocalConfig(sharedDir, storePath) {
|
|
570
515
|
if (fs.existsSync(LOCAL_CONFIG_PATH) && !sharedDir && !storePath)
|
|
571
516
|
return;
|
|
572
517
|
const existing = readLocalConfig();
|
|
573
|
-
//
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
resolvedSharedDir = path.dirname(existing.settingsPath);
|
|
577
|
-
}
|
|
578
|
-
if (!resolvedSharedDir) {
|
|
579
|
-
resolvedSharedDir = detectSharedDir();
|
|
580
|
-
if (resolvedSharedDir)
|
|
581
|
-
console.log(` Auto-detected shared settings: ${resolvedSharedDir}`);
|
|
582
|
-
}
|
|
518
|
+
// Use explicit sharedDir or preserve existing — no auto-detection.
|
|
519
|
+
// Cloud storage is configured when user adds an account (initCloudConfig).
|
|
520
|
+
const resolvedSharedDir = sharedDir || existing.sharedDir;
|
|
583
521
|
const config = {
|
|
584
522
|
...existing,
|
|
585
523
|
sharedDir: resolvedSharedDir,
|
|
@@ -588,8 +526,8 @@ export function initLocalConfig(sharedDir, storePath) {
|
|
|
588
526
|
fs.mkdirSync(LOCAL_DIR, { recursive: true });
|
|
589
527
|
atomicWrite(LOCAL_CONFIG_PATH, config);
|
|
590
528
|
}
|
|
591
|
-
/** Initialize config with
|
|
592
|
-
export function initCloudConfig(provider, cloudPath = "home/.mailx") {
|
|
529
|
+
/** Initialize config with Google Drive cloud storage */
|
|
530
|
+
export function initCloudConfig(provider = "gdrive", cloudPath = "home/.mailx") {
|
|
593
531
|
const existing = readLocalConfig();
|
|
594
532
|
if (existing.sharedDir)
|
|
595
533
|
return; // Already configured
|