@bobfrankston/mailx 1.0.115 → 1.0.116

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
@@ -839,9 +839,14 @@ const isApp = typeof mailxapi !== "undefined" && mailxapi?.isApp;
839
839
  fetch("/api/version").then(r => r.json()).then(d => {
840
840
  const el = document.getElementById("app-version");
841
841
  const storage = d.storage || {};
842
- const storageLabel = storage.provider && storage.provider !== "local" ? ` [${storage.provider}]` : "";
842
+ const storageLabel = storage.provider && storage.provider !== "local"
843
+ ? ` [${storage.provider}${storage.mode === "api" ? " API" : ""}]`
844
+ : "";
843
845
  if (el)
844
846
  el.textContent = `mailx v${d.version}${storageLabel}${isApp ? "" : " [browser]"}`;
847
+ if (storage.cloudError) {
848
+ showAlert(`Cloud storage error: ${storage.cloudError}`, "cloud-error");
849
+ }
845
850
  }).catch(async () => {
846
851
  // Server not running — try to start it if we're in the app
847
852
  const startupStatus = document.getElementById("startup-status");
@@ -322,6 +322,7 @@ async function loadFolderTree(container) {
322
322
  if (mainBody) {
323
323
  mainBody.innerHTML = `<div style="padding:2rem;line-height:1.8;max-width:500px">
324
324
  <h2 style="margin-bottom:1rem">Welcome to mailx</h2>
325
+ <div id="setup-cloud-status"></div>
325
326
  <p>Add your email account to get started.</p>
326
327
  <form id="setup-form" style="margin-top:1rem">
327
328
  <label style="display:block;margin-bottom:0.5rem">
@@ -384,6 +385,24 @@ async function loadFolderTree(container) {
384
385
  statusEl.textContent = `Error: ${err.message}`;
385
386
  }
386
387
  });
388
+ // Show cloud storage status in setup form
389
+ fetch("/api/version").then(r => r.json()).then(d => {
390
+ const cloudEl = document.getElementById("setup-cloud-status");
391
+ if (!cloudEl)
392
+ return;
393
+ const s = d.storage || {};
394
+ if (s.cloudError) {
395
+ cloudEl.innerHTML = `<div style="padding:0.75rem;margin-bottom:1rem;background:#5c1a1a;color:#fca;border:1px solid #a33;border-radius:4px">
396
+ <strong>Cloud storage unavailable:</strong> ${s.cloudError}<br>
397
+ <span style="font-size:0.85rem">Settings on ${s.provider || "cloud"} cannot be read. Add an account below to initialize cloud storage.</span>
398
+ </div>`;
399
+ }
400
+ else if (s.mode === "api") {
401
+ cloudEl.innerHTML = `<div style="padding:0.5rem;margin-bottom:1rem;background:var(--color-bg-surface);border:1px solid var(--color-border);border-radius:4px;color:var(--color-text-muted)">
402
+ Using ${s.provider} API (no local mount)
403
+ </div>`;
404
+ }
405
+ }).catch(() => { });
387
406
  }
388
407
  // Dismiss startup overlay
389
408
  const overlay = document.getElementById("startup-overlay");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.115",
3
+ "version": "1.0.116",
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,7 +20,7 @@
20
20
  "postinstall": "node launcher/builder/postinstall.js"
21
21
  },
22
22
  "dependencies": {
23
- "@bobfrankston/iflow": "^1.0.45",
23
+ "@bobfrankston/iflow": "^1.0.46",
24
24
  "@bobfrankston/miscinfo": "^1.0.7",
25
25
  "@bobfrankston/oauthsupport": "^1.0.20",
26
26
  "@bobfrankston/rust-builder": "^0.1.3",
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { Router } from "express";
6
6
  import { MailxService } from "@bobfrankston/mailx-service";
7
- import { loadAccounts, saveAccounts, initLocalConfig } from "@bobfrankston/mailx-settings";
7
+ import { loadAccounts, loadAccountsAsync, saveAccounts, initLocalConfig, initCloudConfig, loadSettings } from "@bobfrankston/mailx-settings";
8
8
  export function createApiRouter(db, imapManager) {
9
9
  const svc = new MailxService(db, imapManager);
10
10
  const router = Router();
@@ -116,13 +116,20 @@ export function createApiRouter(db, imapManager) {
116
116
  }
117
117
  // Ensure ~/.mailx exists
118
118
  initLocalConfig();
119
+ // Default to gdrive for Gmail users (creates config.jsonc with gdrive provider)
120
+ const domain = email.split("@")[1]?.toLowerCase() || "";
121
+ if (["gmail.com", "googlemail.com"].includes(domain)) {
122
+ initCloudConfig("gdrive");
123
+ }
119
124
  // Build account config (normalizeAccount handles provider detection)
120
125
  const account = { email, name: name || email.split("@")[0] };
121
126
  if (password)
122
127
  account.password = password;
123
- // Load existing accounts, add new one, save
124
- const accounts = loadAccounts();
125
- const domain = email.split("@")[1]?.toLowerCase() || "";
128
+ // Load existing accounts try cloud API in case home/.mailx already exists on Drive
129
+ let accounts = loadAccounts();
130
+ if (accounts.length === 0) {
131
+ accounts = await loadAccountsAsync();
132
+ }
126
133
  const id = domain.split(".")[0] || "account";
127
134
  if (accounts.some((a) => a.email === email)) {
128
135
  res.json({ ok: false, error: "Account already exists" });
@@ -131,7 +138,21 @@ export function createApiRouter(db, imapManager) {
131
138
  account.id = id;
132
139
  accounts.push(account);
133
140
  saveAccounts(accounts);
134
- res.json({ ok: true, message: "Account added. Restart to begin syncing." });
141
+ // Reload settings and register the new account in DB + IMAP so it works immediately
142
+ const settings = loadSettings();
143
+ const normalized = settings.accounts.find(a => a.id === id);
144
+ if (normalized) {
145
+ try {
146
+ await imapManager.addAccount(normalized);
147
+ console.log(` Account added: ${normalized.name} (${normalized.id})`);
148
+ // Start syncing in background
149
+ imapManager.syncAll().catch(() => { });
150
+ }
151
+ catch (e) {
152
+ console.error(` Account setup IMAP error: ${e.message}`);
153
+ }
154
+ }
155
+ res.json({ ok: true, message: "Account added and syncing." });
135
156
  }
136
157
  catch (e) {
137
158
  res.status(500).json({ error: e.message });
@@ -20,14 +20,20 @@ function findGoogleCredentials() {
20
20
  const local = path.join(SETTINGS_DIR, "google-credentials.json");
21
21
  if (fs.existsSync(local))
22
22
  return local;
23
- // Try to find iflow's credentials
23
+ // Try to find iflow's credentials via import.meta.resolve or node_modules walk
24
24
  try {
25
- const iflowPkg = require.resolve("@bobfrankston/iflow/package.json");
26
- const iflowDir = path.dirname(iflowPkg);
27
- for (const name of ["iflow-credentials.json", "credentials.json"]) {
28
- const p = path.join(iflowDir, name);
29
- if (fs.existsSync(p))
30
- return p;
25
+ // Walk up from this package to find iflow in node_modules
26
+ let dir = import.meta.dirname;
27
+ for (let i = 0; i < 5; i++) {
28
+ for (const name of ["iflow-credentials.json", "credentials.json"]) {
29
+ const p = path.join(dir, "node_modules", "@bobfrankston", "iflow", name);
30
+ if (fs.existsSync(p))
31
+ return p;
32
+ }
33
+ const parent = path.dirname(dir);
34
+ if (parent === dir)
35
+ break;
36
+ dir = parent;
31
37
  }
32
38
  }
33
39
  catch { /* iflow not installed */ }
@@ -55,8 +61,10 @@ async function getMicrosoftToken() {
55
61
  }
56
62
  async function getGoogleDriveToken() {
57
63
  const creds = findGoogleCredentials();
58
- if (!creds)
64
+ if (!creds) {
65
+ console.error(" [cloud] No Google credentials found (checked ~/.mailx/google-credentials.json and iflow package)");
59
66
  return null;
67
+ }
60
68
  try {
61
69
  const token = await authenticateOAuth(creds, {
62
70
  scope: GDRIVE_SCOPES,
@@ -154,8 +162,10 @@ async function gDriveFind(fileName, parentName) {
154
162
  }
155
163
  async function gDriveRead(filePath) {
156
164
  const token = await getGoogleDriveToken();
157
- if (!token)
165
+ if (!token) {
166
+ console.error(` [cloud] gdrive read ${filePath}: no token`);
158
167
  return null;
168
+ }
159
169
  try {
160
170
  // Parse path: "home/.mailx/settings.jsonc" → find by folder structure
161
171
  const parts = filePath.split("/");
@@ -169,11 +179,15 @@ async function gDriveRead(filePath) {
169
179
  const res = await fetch(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id)`, {
170
180
  headers: { Authorization: `Bearer ${token}` },
171
181
  });
172
- if (!res.ok)
182
+ if (!res.ok) {
183
+ console.error(` [cloud] gdrive folder lookup '${folder}': ${res.status} ${res.statusText}`);
173
184
  return null;
185
+ }
174
186
  const data = await res.json();
175
- if (!data.files?.[0])
187
+ if (!data.files?.[0]) {
188
+ console.error(` [cloud] gdrive folder '${folder}' not found (drive.file scope can only see app-created files)`);
176
189
  return null;
190
+ }
177
191
  parentId = data.files[0].id;
178
192
  }
179
193
  // Find the file
@@ -183,21 +197,28 @@ async function gDriveRead(filePath) {
183
197
  const fileRes = await fetch(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id)`, {
184
198
  headers: { Authorization: `Bearer ${token}` },
185
199
  });
186
- if (!fileRes.ok)
200
+ if (!fileRes.ok) {
201
+ console.error(` [cloud] gdrive file lookup '${fileName}': ${fileRes.status} ${fileRes.statusText}`);
187
202
  return null;
203
+ }
188
204
  const fileData = await fileRes.json();
189
205
  const fileId = fileData.files?.[0]?.id;
190
- if (!fileId)
206
+ if (!fileId) {
207
+ console.error(` [cloud] gdrive file '${fileName}' not found in ${parts.join("/")}`);
191
208
  return null;
209
+ }
192
210
  // Download content
193
211
  const contentRes = await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`, {
194
212
  headers: { Authorization: `Bearer ${token}` },
195
213
  });
196
- if (!contentRes.ok)
214
+ if (!contentRes.ok) {
215
+ console.error(` [cloud] gdrive download ${fileId}: ${contentRes.status} ${contentRes.statusText}`);
197
216
  return null;
217
+ }
198
218
  return await contentRes.text();
199
219
  }
200
- catch {
220
+ catch (e) {
221
+ console.error(` [cloud] gdrive read ${filePath}: ${e.message}`);
201
222
  return null;
202
223
  }
203
224
  }
@@ -28,6 +28,7 @@ export declare function isCloudMode(): boolean;
28
28
  export declare function getStorageInfo(): {
29
29
  provider: string;
30
30
  mode: "mount" | "api" | "local";
31
+ cloudError?: string;
31
32
  };
32
33
  declare const DEFAULT_PREFERENCES: {
33
34
  ui: {
@@ -88,6 +89,8 @@ export declare function getConfigDir(): string;
88
89
  export { getSharedDir };
89
90
  /** Initialize local config if it doesn't exist */
90
91
  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;
91
94
  declare const DEFAULT_SETTINGS: MailxSettings;
92
95
  /** Get historyDays for an account: per-account override > system override > shared default */
93
96
  export declare function getHistoryDays(accountId?: string): number;
@@ -92,6 +92,8 @@ function resolveProvider(cfg) {
92
92
  }
93
93
  /** Pending cloud config for API fallback (set when mount not found) */
94
94
  let pendingCloudConfig = null;
95
+ /** Last cloud API error (for UI display) */
96
+ let lastCloudError = null;
95
97
  function resolveSharedEntry(entry) {
96
98
  if (typeof entry === "string") {
97
99
  const p = resolvePath(entry);
@@ -127,12 +129,15 @@ export async function cloudRead(filename) {
127
129
  if (!pendingCloudConfig)
128
130
  return null;
129
131
  const provider = getCloudProvider(pendingCloudConfig.provider);
130
- if (!provider)
132
+ if (!provider) {
133
+ lastCloudError = `No cloud provider for ${pendingCloudConfig.provider}`;
131
134
  return null;
135
+ }
132
136
  const cloudPath = `${pendingCloudConfig.path}/${filename}`;
133
137
  console.log(` [cloud] Reading ${cloudPath} via ${pendingCloudConfig.provider} API...`);
134
138
  const content = await provider.read(cloudPath);
135
139
  if (content) {
140
+ lastCloudError = null;
136
141
  // Cache locally
137
142
  try {
138
143
  fs.mkdirSync(LOCAL_DIR, { recursive: true });
@@ -140,6 +145,9 @@ export async function cloudRead(filename) {
140
145
  }
141
146
  catch { /* ignore cache write failure */ }
142
147
  }
148
+ else {
149
+ lastCloudError = `Could not read ${filename} from ${pendingCloudConfig.provider} (check credentials and drive.file scope)`;
150
+ }
143
151
  return content;
144
152
  }
145
153
  /** Write a file via cloud API */
@@ -177,7 +185,7 @@ export function getStorageInfo() {
177
185
  const name = pendingCloudConfig.provider === "onedrive" ? "OneDrive" :
178
186
  (pendingCloudConfig.provider === "gdrive" || pendingCloudConfig.provider === "google") ? "Google Drive" :
179
187
  pendingCloudConfig.provider === "dropbox" ? "Dropbox" : pendingCloudConfig.provider;
180
- return { provider: name, mode: "api" };
188
+ return { provider: name, mode: "api", cloudError: lastCloudError || undefined };
181
189
  }
182
190
  }
183
191
  return { provider: "local", mode: "local" };
@@ -234,7 +242,7 @@ function loadFile(filename, defaults) {
234
242
  }
235
243
  return { ...defaults, ...data };
236
244
  }
237
- /** Save a config file to the shared directory */
245
+ /** Save a config file to the shared directory (and cloud API if active) */
238
246
  function saveFile(filename, data) {
239
247
  const sharedDir = getSharedDir();
240
248
  atomicWrite(path.join(sharedDir, filename), data);
@@ -245,6 +253,15 @@ function saveFile(filename, data) {
245
253
  }
246
254
  catch { /* ignore */ }
247
255
  }
256
+ // Write to cloud API if filesystem mount unavailable (creates app-owned file for drive.file scope)
257
+ if (pendingCloudConfig) {
258
+ cloudWrite(filename, JSON.stringify(data, null, 2)).then(ok => {
259
+ if (ok)
260
+ console.log(` [cloud] Saved ${filename} via ${pendingCloudConfig.provider} API`);
261
+ else
262
+ console.error(` [cloud] Failed to save ${filename} via ${pendingCloudConfig.provider} API`);
263
+ }).catch(() => { });
264
+ }
248
265
  }
249
266
  const PROVIDERS = {
250
267
  "gmail.com": {
@@ -571,6 +588,22 @@ export function initLocalConfig(sharedDir, storePath) {
571
588
  fs.mkdirSync(LOCAL_DIR, { recursive: true });
572
589
  atomicWrite(LOCAL_CONFIG_PATH, config);
573
590
  }
591
+ /** Initialize config with a cloud provider (e.g. gdrive for Gmail users) */
592
+ export function initCloudConfig(provider, cloudPath = "home/.mailx") {
593
+ const existing = readLocalConfig();
594
+ if (existing.sharedDir)
595
+ return; // Already configured
596
+ const config = {
597
+ ...existing,
598
+ sharedDir: { provider, path: cloudPath },
599
+ storePath: existing.storePath || DEFAULT_STORE_PATH,
600
+ };
601
+ fs.mkdirSync(LOCAL_DIR, { recursive: true });
602
+ atomicWrite(LOCAL_CONFIG_PATH, config);
603
+ // Set up cloud API fallback immediately
604
+ pendingCloudConfig = { provider, path: cloudPath };
605
+ console.log(` Initialized cloud config: ${provider} → ${cloudPath}`);
606
+ }
574
607
  const DEFAULT_SETTINGS = {
575
608
  accounts: [],
576
609
  ui: DEFAULT_PREFERENCES.ui,