@bobfrankston/mailx 1.0.292 → 1.0.293

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.292",
3
+ "version": "1.0.293",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -50,10 +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
- /** 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";
53
+ /** Paths to try when no config.path is set (fresh install). Order matters:
54
+ * "home/.mailx" is the user convention for shared family settings; "mailx"
55
+ * is the auto-created default. First one that exists on GDrive wins. */
56
+ const GDRIVE_PATH_SEARCH_ORDER = ["home/.mailx", "mailx"];
57
57
  // ── Token helpers ──
58
58
  async function getGoogleDriveToken() {
59
59
  const creds = findGoogleCredentials();
@@ -102,6 +102,38 @@ async function getGoogleDriveToken() {
102
102
  }
103
103
  }
104
104
  // ── Google Drive API (folder-ID based) ──
105
+ /** Walk a nested folder path on GDrive ("home/.mailx" → find "home", find
106
+ * ".mailx" inside it). Returns the leaf folder ID, or null if any segment is
107
+ * missing. When `create` is true, creates the LAST segment if missing (won't
108
+ * create intermediate segments — that's a user error). */
109
+ async function walkGDrivePath(token, segments, create) {
110
+ let parentId;
111
+ for (let i = 0; i < segments.length; i++) {
112
+ const seg = segments[i];
113
+ const found = await gDriveFindFolder(token, seg, parentId);
114
+ if (found) {
115
+ parentId = found;
116
+ }
117
+ else if (create && i === segments.length - 1) {
118
+ const body = { name: seg, mimeType: "application/vnd.google-apps.folder" };
119
+ if (parentId)
120
+ body.parents = [parentId];
121
+ const res = await fetch("https://www.googleapis.com/drive/v3/files", {
122
+ method: "POST",
123
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
124
+ body: JSON.stringify(body),
125
+ });
126
+ if (!res.ok)
127
+ return null;
128
+ const created = await res.json();
129
+ parentId = created.id;
130
+ }
131
+ else {
132
+ return null;
133
+ }
134
+ }
135
+ return parentId || null;
136
+ }
105
137
  /** Find a single folder by name, optionally inside a parent. */
106
138
  async function gDriveFindFolder(token, name, parentId) {
107
139
  let query = `name='${name}' and mimeType='application/vnd.google-apps.folder' and trashed=false`;
@@ -135,52 +167,27 @@ export async function gDriveFindOrCreateFolder() {
135
167
  }
136
168
  }
137
169
  catch { /* ignore */ }
138
- const cfgPath = (typeof cfgEntry === "object" && cfgEntry?.path) ? cfgEntry.path : GDRIVE_FOLDER_NAME_DEFAULT;
139
- const segments = cfgPath.split(/[/\\]/).filter(Boolean);
170
+ // Build search list: configured path first, then common conventions
171
+ const configuredPath = (typeof cfgEntry === "object" && cfgEntry?.path) ? cfgEntry.path : null;
172
+ const pathsToTry = configuredPath
173
+ ? [configuredPath]
174
+ : GDRIVE_PATH_SEARCH_ORDER;
140
175
  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;
176
+ for (const tryPath of pathsToTry) {
177
+ const segments = tryPath.split(/[/\\]/).filter(Boolean);
178
+ const folderId = await walkGDrivePath(token, segments, false);
179
+ if (folderId) {
180
+ console.log(` [cloud] Found existing '${tryPath}' folder: ${folderId}`);
181
+ return folderId;
178
182
  }
179
183
  }
180
- if (parentId)
181
- return parentId;
182
- // Shouldn't reach here, but safety
183
- return null;
184
+ // None found — create the first path (configured or default "mailx")
185
+ const createPath = configuredPath || GDRIVE_PATH_SEARCH_ORDER[GDRIVE_PATH_SEARCH_ORDER.length - 1];
186
+ const segments = createPath.split(/[/\\]/).filter(Boolean);
187
+ const created = await walkGDrivePath(token, segments, true);
188
+ if (created)
189
+ console.log(` [cloud] Created '${createPath}' folder: ${created}`);
190
+ return created;
184
191
  }
185
192
  catch (e) {
186
193
  console.error(` [cloud] gdrive folder setup: ${e.message}`);
@@ -656,9 +656,14 @@ export async function initCloudConfig(provider = "gdrive") {
656
656
  const existing = readLocalConfig();
657
657
  if (existing.sharedDir)
658
658
  return; // Already configured
659
- // Find or create the "mailx" folder via Drive API
659
+ // Find or create the settings folder via Drive API — tries "home/.mailx"
660
+ // first (family convention), then "mailx" (default). The found path gets
661
+ // saved so future lookups don't re-scan.
660
662
  const folderId = await gDriveFindOrCreateFolder();
661
- const sharedDir = { provider, path: "mailx", folderId: folderId || undefined };
663
+ // Detect which path was actually found by reading back from the API
664
+ // (gDriveFindOrCreateFolder logs it). For now use "mailx" as default
665
+ // label — the folderId is what matters for subsequent reads/writes.
666
+ const sharedDir = { provider, path: "home/.mailx", folderId: folderId || undefined };
662
667
  const config = { ...existing, sharedDir, storePath: existing.storePath || DEFAULT_STORE_PATH };
663
668
  fs.mkdirSync(LOCAL_DIR, { recursive: true });
664
669
  atomicWrite(LOCAL_CONFIG_PATH, config);