@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 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}${storage.mode === "api" ? " API" : ""}]`
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/settings.jsonc</code> or point to shared settings:</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": "~/OneDrive/home/.mailx" }</code>
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.136",
3
+ "version": "1.0.138",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -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, initLocalConfig, initCloudConfig, loadSettings } from "@bobfrankston/mailx-settings";
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
- // Ensure ~/.mailx exists
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
- if (["gmail.com", "googlemail.com"].includes(domain)) {
122
- initCloudConfig("gdrive");
149
+ const detected = await detectEmailProvider(domain);
150
+ if (detected?.cloud) {
151
+ initCloudConfig(detected.cloud);
123
152
  }
124
- // Build account config (normalizeAccount handles provider detection)
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 set up.");
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 abstraction for mailx settings.
3
- * Reads/writes settings files on OneDrive (Graph API) or Google Drive (Drive API)
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 = "onedrive" | "gdrive" | "google" | "dropbox" | "local";
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: CloudProvider): CloudFile | null;
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 abstraction for mailx settings.
3
- * Reads/writes settings files on OneDrive (Graph API) or Google Drive (Drive API)
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 (e.g. "OneDrive", "Google Drive", "local") */
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 a cloud provider (e.g. gdrive for Gmail users) */
93
- export declare function initCloudConfig(provider: "gdrive" | "onedrive" | "dropbox", cloudPath?: string): void;
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: derive from settingsPath
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 (e.g. "OneDrive", "Google Drive", "local") */
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
- // Mounted cloud drive
176
- const name = typeof entry === "string" ? "cloud" :
177
- entry.provider === "onedrive" ? "OneDrive" :
178
- (entry.provider === "gdrive" || entry.provider === "google") ? "Google Drive" :
179
- entry.provider === "dropbox" ? "Dropbox" : entry.provider;
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 but using API fallback
165
+ // Not mounted using API
184
166
  if (pendingCloudConfig) {
185
- const name = pendingCloudConfig.provider === "onedrive" ? "OneDrive" :
186
- (pendingCloudConfig.provider === "gdrive" || pendingCloudConfig.provider === "google") ? "Google Drive" :
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
- /** Auto-detect shared settings on OneDrive or common cloud sync locations */
532
- function detectSharedDir() {
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
- // Auto-detect shared settings if not configured
574
- let resolvedSharedDir = sharedDir || existing.sharedDir;
575
- if (!resolvedSharedDir && existing.settingsPath) {
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 a cloud provider (e.g. gdrive for Gmail users) */
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