@bobfrankston/mailx 1.0.291 → 1.0.292

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.291",
3
+ "version": "1.0.292",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -24,7 +24,7 @@
24
24
  "@bobfrankston/iflow-node": "^0.1.7",
25
25
  "@bobfrankston/miscinfo": "^1.0.8",
26
26
  "@bobfrankston/oauthsupport": "^1.0.24",
27
- "@bobfrankston/msger": "^0.1.324",
27
+ "@bobfrankston/msger": "^0.1.325",
28
28
  "@capacitor/android": "^8.3.0",
29
29
  "@capacitor/cli": "^8.3.0",
30
30
  "@capacitor/core": "^8.3.0",
@@ -86,7 +86,7 @@
86
86
  "@bobfrankston/iflow-node": "^0.1.7",
87
87
  "@bobfrankston/miscinfo": "^1.0.8",
88
88
  "@bobfrankston/oauthsupport": "^1.0.24",
89
- "@bobfrankston/msger": "^0.1.324",
89
+ "@bobfrankston/msger": "^0.1.325",
90
90
  "@capacitor/android": "^8.3.0",
91
91
  "@capacitor/cli": "^8.3.0",
92
92
  "@capacitor/core": "^8.3.0",
@@ -16,7 +16,10 @@
16
16
  * Dropbox (removed 2026-04-06): Never implemented — placeholder only.
17
17
  * Would need Dropbox OAuth app, token management, and Dropbox API v2 calls.
18
18
  */
19
- /** Find the app-owned "mailx" folder, or create it. Returns folder ID. */
19
+ /** Find the settings folder on GDrive, or create a flat "mailx" folder.
20
+ * Supports nested paths from config (e.g. "home/.mailx") by walking each
21
+ * segment. Falls back to the folderId in config if set, so a stale ID that
22
+ * points to the wrong folder gets corrected on next lookup. */
20
23
  export declare function gDriveFindOrCreateFolder(): Promise<string | null>;
21
24
  export type CloudProvider = "gdrive" | "google" | "local";
22
25
  export interface CloudFile {
@@ -50,7 +50,10 @@ const GDRIVE_TOKEN_DIR = path.join(SETTINGS_DIR, "tokens", "gdrive");
50
50
  // drive.file: app can only see files it created. Safe, publishable without security audit.
51
51
  // All machines sharing the same OAuth client ID see the same files.
52
52
  const GDRIVE_SCOPES = "https://www.googleapis.com/auth/drive.file";
53
- const GDRIVE_FOLDER_NAME = "mailx";
53
+ /** Default folder name when config.path isn't set. The actual folder name
54
+ * is taken from pendingCloudConfig.path or readLocalConfig().sharedDir.path
55
+ * so each user can use their own convention (e.g. "home/.mailx"). */
56
+ const GDRIVE_FOLDER_NAME_DEFAULT = "mailx";
54
57
  // ── Token helpers ──
55
58
  async function getGoogleDriveToken() {
56
59
  const creds = findGoogleCredentials();
@@ -99,39 +102,85 @@ async function getGoogleDriveToken() {
99
102
  }
100
103
  }
101
104
  // ── Google Drive API (folder-ID based) ──
102
- /** Find the app-owned "mailx" folder, or create it. Returns folder ID. */
105
+ /** Find a single folder by name, optionally inside a parent. */
106
+ async function gDriveFindFolder(token, name, parentId) {
107
+ let query = `name='${name}' and mimeType='application/vnd.google-apps.folder' and trashed=false`;
108
+ if (parentId)
109
+ query += ` and '${parentId}' in parents`;
110
+ const res = await fetch(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id,name)`, {
111
+ headers: { Authorization: `Bearer ${token}` },
112
+ });
113
+ if (!res.ok) {
114
+ console.error(` [cloud] gdrive folder search '${name}': ${res.status} ${res.statusText}`);
115
+ return null;
116
+ }
117
+ const data = await res.json();
118
+ return data.files?.[0]?.id || null;
119
+ }
120
+ /** Find the settings folder on GDrive, or create a flat "mailx" folder.
121
+ * Supports nested paths from config (e.g. "home/.mailx") by walking each
122
+ * segment. Falls back to the folderId in config if set, so a stale ID that
123
+ * points to the wrong folder gets corrected on next lookup. */
103
124
  export async function gDriveFindOrCreateFolder() {
104
125
  const token = await getGoogleDriveToken();
105
126
  if (!token)
106
127
  return null;
128
+ // Read path from config — supports nested like "home/.mailx"
129
+ let cfgEntry = null;
107
130
  try {
108
- // Search for existing folder (created by this OAuth client)
109
- const query = `name='${GDRIVE_FOLDER_NAME}' and mimeType='application/vnd.google-apps.folder' and trashed=false`;
110
- const res = await fetch(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id,name)`, {
111
- headers: { Authorization: `Bearer ${token}` },
112
- });
113
- if (!res.ok) {
114
- console.error(` [cloud] gdrive folder search: ${res.status} ${res.statusText}`);
115
- return null;
116
- }
117
- const data = await res.json();
118
- if (data.files?.[0]) {
119
- console.log(` [cloud] Found existing '${GDRIVE_FOLDER_NAME}' folder: ${data.files[0].id}`);
120
- return data.files[0].id;
131
+ const cfgPath = path.join(SETTINGS_DIR, "config.jsonc");
132
+ if (fs.existsSync(cfgPath)) {
133
+ const raw = fs.readFileSync(cfgPath, "utf-8").replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
134
+ cfgEntry = JSON.parse(raw).sharedDir;
121
135
  }
122
- // Create folder
123
- const createRes = await fetch("https://www.googleapis.com/drive/v3/files", {
124
- method: "POST",
125
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
126
- body: JSON.stringify({ name: GDRIVE_FOLDER_NAME, mimeType: "application/vnd.google-apps.folder" }),
127
- });
128
- if (!createRes.ok) {
129
- console.error(` [cloud] gdrive folder create: ${createRes.status} ${createRes.statusText}`);
130
- return null;
136
+ }
137
+ catch { /* ignore */ }
138
+ const cfgPath = (typeof cfgEntry === "object" && cfgEntry?.path) ? cfgEntry.path : GDRIVE_FOLDER_NAME_DEFAULT;
139
+ const segments = cfgPath.split(/[/\\]/).filter(Boolean);
140
+ try {
141
+ // Walk nested path segments: "home/.mailx" → find "home", then ".mailx" inside it
142
+ let parentId;
143
+ for (let i = 0; i < segments.length; i++) {
144
+ const seg = segments[i];
145
+ const found = await gDriveFindFolder(token, seg, parentId);
146
+ if (found) {
147
+ console.log(` [cloud] Found folder '${seg}': ${found}${parentId ? ` (in ${parentId})` : ""}`);
148
+ parentId = found;
149
+ }
150
+ else if (i === segments.length - 1) {
151
+ // Last segment missing — create it (don't create intermediate folders)
152
+ const body = { name: seg, mimeType: "application/vnd.google-apps.folder" };
153
+ if (parentId)
154
+ body.parents = [parentId];
155
+ const createRes = await fetch("https://www.googleapis.com/drive/v3/files", {
156
+ method: "POST",
157
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
158
+ body: JSON.stringify(body),
159
+ });
160
+ if (!createRes.ok) {
161
+ console.error(` [cloud] gdrive folder create '${seg}': ${createRes.status}`);
162
+ return null;
163
+ }
164
+ const created = await createRes.json();
165
+ console.log(` [cloud] Created '${seg}' folder: ${created.id}`);
166
+ parentId = created.id;
167
+ }
168
+ else {
169
+ // Intermediate segment missing — can't create the whole tree
170
+ console.error(` [cloud] gdrive path '${cfgPath}': intermediate folder '${seg}' not found`);
171
+ // Fall back to flat search for the leaf name
172
+ const leafId = await gDriveFindFolder(token, segments[segments.length - 1]);
173
+ if (leafId) {
174
+ console.log(` [cloud] Fallback: found '${segments[segments.length - 1]}' at root: ${leafId}`);
175
+ return leafId;
176
+ }
177
+ return null;
178
+ }
131
179
  }
132
- const created = await createRes.json();
133
- console.log(` [cloud] Created '${GDRIVE_FOLDER_NAME}' folder: ${created.id}`);
134
- return created.id;
180
+ if (parentId)
181
+ return parentId;
182
+ // Shouldn't reach here, but safety
183
+ return null;
135
184
  }
136
185
  catch (e) {
137
186
  console.error(` [cloud] gdrive folder setup: ${e.message}`);