@bobfrankston/mailx 1.0.291 → 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.
|
|
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",
|
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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"];
|
|
54
57
|
// ── Token helpers ──
|
|
55
58
|
async function getGoogleDriveToken() {
|
|
56
59
|
const creds = findGoogleCredentials();
|
|
@@ -99,39 +102,92 @@ async function getGoogleDriveToken() {
|
|
|
99
102
|
}
|
|
100
103
|
}
|
|
101
104
|
// ── Google Drive API (folder-ID based) ──
|
|
102
|
-
/**
|
|
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
|
+
}
|
|
137
|
+
/** Find a single folder by name, optionally inside a parent. */
|
|
138
|
+
async function gDriveFindFolder(token, name, parentId) {
|
|
139
|
+
let query = `name='${name}' and mimeType='application/vnd.google-apps.folder' and trashed=false`;
|
|
140
|
+
if (parentId)
|
|
141
|
+
query += ` and '${parentId}' in parents`;
|
|
142
|
+
const res = await fetch(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id,name)`, {
|
|
143
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
144
|
+
});
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
console.error(` [cloud] gdrive folder search '${name}': ${res.status} ${res.statusText}`);
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const data = await res.json();
|
|
150
|
+
return data.files?.[0]?.id || null;
|
|
151
|
+
}
|
|
152
|
+
/** Find the settings folder on GDrive, or create a flat "mailx" folder.
|
|
153
|
+
* Supports nested paths from config (e.g. "home/.mailx") by walking each
|
|
154
|
+
* segment. Falls back to the folderId in config if set, so a stale ID that
|
|
155
|
+
* points to the wrong folder gets corrected on next lookup. */
|
|
103
156
|
export async function gDriveFindOrCreateFolder() {
|
|
104
157
|
const token = await getGoogleDriveToken();
|
|
105
158
|
if (!token)
|
|
106
159
|
return null;
|
|
160
|
+
// Read path from config — supports nested like "home/.mailx"
|
|
161
|
+
let cfgEntry = null;
|
|
107
162
|
try {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
});
|
|
113
|
-
if (!res.ok) {
|
|
114
|
-
console.error(` [cloud] gdrive folder search: ${res.status} ${res.statusText}`);
|
|
115
|
-
return null;
|
|
163
|
+
const cfgPath = path.join(SETTINGS_DIR, "config.jsonc");
|
|
164
|
+
if (fs.existsSync(cfgPath)) {
|
|
165
|
+
const raw = fs.readFileSync(cfgPath, "utf-8").replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
166
|
+
cfgEntry = JSON.parse(raw).sharedDir;
|
|
116
167
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
168
|
+
}
|
|
169
|
+
catch { /* ignore */ }
|
|
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;
|
|
175
|
+
try {
|
|
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;
|
|
182
|
+
}
|
|
131
183
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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;
|
|
135
191
|
}
|
|
136
192
|
catch (e) {
|
|
137
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
|
|
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);
|