@bobfrankston/mailx 1.0.292 → 1.0.294
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
|
@@ -47,13 +47,17 @@ function findGoogleCredentials() {
|
|
|
47
47
|
return null;
|
|
48
48
|
}
|
|
49
49
|
const GDRIVE_TOKEN_DIR = path.join(SETTINGS_DIR, "tokens", "gdrive");
|
|
50
|
-
// drive
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
50
|
+
// drive: full access so we can find pre-existing folders like "home/.mailx"
|
|
51
|
+
// that weren't created by this OAuth client. drive.file was safer but couldn't
|
|
52
|
+
// discover folders created manually or by other tools — which broke first-run
|
|
53
|
+
// setup when the user's convention is a nested path.
|
|
54
|
+
// The Gmail scope is already mail.google.com (full email), so this isn't a
|
|
55
|
+
// bigger privacy ask. Token cache at tokens/gdrive/ will re-auth on scope change.
|
|
56
|
+
const GDRIVE_SCOPES = "https://www.googleapis.com/auth/drive";
|
|
57
|
+
/** Paths to try when no config.path is set (fresh install). Order matters:
|
|
58
|
+
* "home/.mailx" is the user convention for shared family settings; "mailx"
|
|
59
|
+
* is the auto-created default. First one that exists on GDrive wins. */
|
|
60
|
+
const GDRIVE_PATH_SEARCH_ORDER = ["home/.mailx", "mailx"];
|
|
57
61
|
// ── Token helpers ──
|
|
58
62
|
async function getGoogleDriveToken() {
|
|
59
63
|
const creds = findGoogleCredentials();
|
|
@@ -102,6 +106,55 @@ async function getGoogleDriveToken() {
|
|
|
102
106
|
}
|
|
103
107
|
}
|
|
104
108
|
// ── Google Drive API (folder-ID based) ──
|
|
109
|
+
/** Walk a nested folder path on GDrive ("home/.mailx" → find "home", find
|
|
110
|
+
* ".mailx" inside it). Returns the leaf folder ID, or null if any segment is
|
|
111
|
+
* missing. When `create` is true, creates the LAST segment if missing (won't
|
|
112
|
+
* create intermediate segments — that's a user error). */
|
|
113
|
+
async function walkGDrivePath(token, segments, create) {
|
|
114
|
+
const fullPath = segments.join("/");
|
|
115
|
+
let parentId;
|
|
116
|
+
for (let i = 0; i < segments.length; i++) {
|
|
117
|
+
const seg = segments[i];
|
|
118
|
+
const found = await gDriveFindFolder(token, seg, parentId);
|
|
119
|
+
if (found) {
|
|
120
|
+
console.log(` [cloud] walk '${fullPath}': '${seg}' → ${found}${parentId ? ` (in ${parentId})` : ""}`);
|
|
121
|
+
parentId = found;
|
|
122
|
+
}
|
|
123
|
+
else if (create && i === segments.length - 1) {
|
|
124
|
+
const body = { name: seg, mimeType: "application/vnd.google-apps.folder" };
|
|
125
|
+
if (parentId)
|
|
126
|
+
body.parents = [parentId];
|
|
127
|
+
const res = await fetch("https://www.googleapis.com/drive/v3/files", {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
130
|
+
body: JSON.stringify(body),
|
|
131
|
+
});
|
|
132
|
+
if (!res.ok)
|
|
133
|
+
return null;
|
|
134
|
+
const created = await res.json();
|
|
135
|
+
parentId = created.id;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return parentId || null;
|
|
142
|
+
}
|
|
143
|
+
/** Resolve a folder ID back to its name (for diagnostic logging). */
|
|
144
|
+
async function gDriveGetFolderName(token, folderId) {
|
|
145
|
+
try {
|
|
146
|
+
const res = await fetch(`https://www.googleapis.com/drive/v3/files/${folderId}?fields=name,parents`, {
|
|
147
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
148
|
+
});
|
|
149
|
+
if (!res.ok)
|
|
150
|
+
return null;
|
|
151
|
+
const data = await res.json();
|
|
152
|
+
return data.name || null;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
105
158
|
/** Find a single folder by name, optionally inside a parent. */
|
|
106
159
|
async function gDriveFindFolder(token, name, parentId) {
|
|
107
160
|
let query = `name='${name}' and mimeType='application/vnd.google-apps.folder' and trashed=false`;
|
|
@@ -135,52 +188,31 @@ export async function gDriveFindOrCreateFolder() {
|
|
|
135
188
|
}
|
|
136
189
|
}
|
|
137
190
|
catch { /* ignore */ }
|
|
138
|
-
|
|
139
|
-
const
|
|
191
|
+
// Build search list: configured path first, then common conventions
|
|
192
|
+
const configuredPath = (typeof cfgEntry === "object" && cfgEntry?.path) ? cfgEntry.path : null;
|
|
193
|
+
const pathsToTry = configuredPath
|
|
194
|
+
? [configuredPath]
|
|
195
|
+
: GDRIVE_PATH_SEARCH_ORDER;
|
|
140
196
|
try {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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;
|
|
197
|
+
for (const tryPath of pathsToTry) {
|
|
198
|
+
console.log(` [cloud] Trying GDrive path: '${tryPath}'`);
|
|
199
|
+
const segments = tryPath.split(/[/\\]/).filter(Boolean);
|
|
200
|
+
const folderId = await walkGDrivePath(token, segments, false);
|
|
201
|
+
if (folderId) {
|
|
202
|
+
// Resolve folder ID back to name for verification
|
|
203
|
+
const name = await gDriveGetFolderName(token, folderId);
|
|
204
|
+
console.log(` [cloud] Found existing '${tryPath}' → folder '${name || "?"}' (${folderId})`);
|
|
205
|
+
return folderId;
|
|
178
206
|
}
|
|
207
|
+
console.log(` [cloud] Path '${tryPath}' not found on GDrive`);
|
|
179
208
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
209
|
+
// None found — create the last (simplest) path
|
|
210
|
+
const createPath = configuredPath || GDRIVE_PATH_SEARCH_ORDER[GDRIVE_PATH_SEARCH_ORDER.length - 1];
|
|
211
|
+
const segments = createPath.split(/[/\\]/).filter(Boolean);
|
|
212
|
+
const created = await walkGDrivePath(token, segments, true);
|
|
213
|
+
if (created)
|
|
214
|
+
console.log(` [cloud] Created '${createPath}' folder: ${created}`);
|
|
215
|
+
return created;
|
|
184
216
|
}
|
|
185
217
|
catch (e) {
|
|
186
218
|
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
|
|
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
|
-
|
|
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);
|