@bobfrankston/mailx 1.0.209 → 1.0.211

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.
@@ -1,39 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Post-install script: creates symlinks for workspace packages
4
- * so they resolve as @bobfrankston/mailx-* in node_modules.
5
- */
6
- import fs from "node:fs";
7
- import path from "node:path";
8
- const root = path.resolve(import.meta.dirname, "..");
9
- const packagesDir = path.join(root, "packages");
10
- const nmDir = path.join(root, "node_modules", "@bobfrankston");
11
- if (!fs.existsSync(packagesDir))
12
- process.exit(0); // not in workspace layout
13
- fs.mkdirSync(nmDir, { recursive: true });
14
- for (const dir of fs.readdirSync(packagesDir)) {
15
- const pkgPath = path.join(packagesDir, dir, "package.json");
16
- if (!fs.existsSync(pkgPath))
17
- continue;
18
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
19
- const name = pkg.name?.split("/")[1]; // e.g., "mailx-store" from "@bobfrankston/mailx-store"
20
- if (!name)
21
- continue;
22
- const linkPath = path.join(nmDir, name);
23
- const targetPath = path.join(packagesDir, dir);
24
- if (fs.existsSync(linkPath))
25
- continue; // already linked
26
- try {
27
- // Use junction on Windows (no admin needed), symlink on Unix
28
- if (process.platform === "win32") {
29
- fs.symlinkSync(targetPath, linkPath, "junction");
30
- }
31
- else {
32
- fs.symlinkSync(targetPath, linkPath, "dir");
33
- }
34
- }
35
- catch (e) {
36
- console.error(`Failed to link ${name}: ${e.message}`);
37
- }
38
- }
39
- //# sourceMappingURL=postinstall.js.map
@@ -1,243 +0,0 @@
1
- /**
2
- * Cloud storage for mailx settings — Google Drive API (drive.file scope).
3
- *
4
- * Uses a single app-owned "mailx" folder on Drive, accessed by folder ID.
5
- * The drive.file scope only sees files/folders created by this OAuth client,
6
- * which prevents conflicts with existing folders of the same name.
7
- * All machines using the same OAuth client ID share the same folder.
8
- *
9
- * ── Restoring removed providers ──
10
- * OneDrive (removed 2026-04-06): Required microsoft-credentials.json in ~/.mailx/,
11
- * MS_SCOPES = "https://graph.microsoft.com/Files.ReadWrite offline_access",
12
- * authenticateOAuth with tokenDir ~/.mailx/tokens/microsoft/.
13
- * Read/write via Graph API: GET/PUT https://graph.microsoft.com/v1.0/me/drive/root:/{path}:/content
14
- * Azure AD app registration needed: https://portal.azure.com → App registrations → Desktop app
15
- *
16
- * Dropbox (removed 2026-04-06): Never implemented — placeholder only.
17
- * Would need Dropbox OAuth app, token management, and Dropbox API v2 calls.
18
- */
19
- import fs from "node:fs";
20
- import path from "node:path";
21
- import { authenticateOAuth } from "@bobfrankston/oauthsupport";
22
- const SETTINGS_DIR = path.join(process.env.USERPROFILE || process.env.HOME || ".", ".mailx");
23
- // ── Credentials ──
24
- // Google Drive: reuse iflow's OAuth credentials (same Google Cloud project)
25
- function findGoogleCredentials() {
26
- // Check mailx local dir first, then iflow package
27
- const local = path.join(SETTINGS_DIR, "google-credentials.json");
28
- if (fs.existsSync(local))
29
- return local;
30
- try {
31
- let dir = import.meta.dirname;
32
- for (let i = 0; i < 5; i++) {
33
- for (const pkg of ["iflow-direct", "iflow"]) {
34
- for (const name of ["iflow-credentials.json"]) {
35
- const p = path.join(dir, "node_modules", "@bobfrankston", pkg, name);
36
- if (fs.existsSync(p))
37
- return p;
38
- }
39
- }
40
- const parent = path.dirname(dir);
41
- if (parent === dir)
42
- break;
43
- dir = parent;
44
- }
45
- }
46
- catch { /* iflow not installed */ }
47
- return null;
48
- }
49
- const GDRIVE_TOKEN_DIR = path.join(SETTINGS_DIR, "tokens", "gdrive");
50
- // drive.file: app can only see files it created. Safe, publishable without security audit.
51
- // All machines sharing the same OAuth client ID see the same files.
52
- const GDRIVE_SCOPES = "https://www.googleapis.com/auth/drive.file";
53
- const GDRIVE_FOLDER_NAME = "mailx";
54
- // ── Token helpers ──
55
- async function getGoogleDriveToken() {
56
- const creds = findGoogleCredentials();
57
- if (!creds) {
58
- console.error(" [cloud] No Google credentials found (checked ~/.mailx/google-credentials.json and iflow package)");
59
- return null;
60
- }
61
- // Delete stale token if scope changed (drive → drive.file or vice versa)
62
- const tokenPath = path.join(GDRIVE_TOKEN_DIR, "token.json");
63
- if (fs.existsSync(tokenPath)) {
64
- try {
65
- const existing = JSON.parse(fs.readFileSync(tokenPath, "utf-8"));
66
- if (existing.scope && existing.scope !== GDRIVE_SCOPES) {
67
- console.log(` [cloud] Scope changed — re-authenticating...`);
68
- fs.unlinkSync(tokenPath);
69
- }
70
- }
71
- catch { /* ignore parse errors */ }
72
- }
73
- try {
74
- const token = await authenticateOAuth(creds, {
75
- scope: GDRIVE_SCOPES,
76
- tokenDirectory: GDRIVE_TOKEN_DIR,
77
- tokenFileName: "token.json",
78
- credentialsKey: "installed",
79
- includeOfflineAccess: true,
80
- });
81
- return token?.access_token || null;
82
- }
83
- catch (e) {
84
- console.error(` [cloud] Google Drive auth failed: ${e.message}`);
85
- return null;
86
- }
87
- }
88
- // ── Google Drive API (folder-ID based) ──
89
- /** Find the app-owned "mailx" folder, or create it. Returns folder ID. */
90
- export async function gDriveFindOrCreateFolder() {
91
- const token = await getGoogleDriveToken();
92
- if (!token)
93
- return null;
94
- try {
95
- // Search for existing folder (created by this OAuth client)
96
- const query = `name='${GDRIVE_FOLDER_NAME}' and mimeType='application/vnd.google-apps.folder' and trashed=false`;
97
- const res = await fetch(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id,name)`, {
98
- headers: { Authorization: `Bearer ${token}` },
99
- });
100
- if (!res.ok) {
101
- console.error(` [cloud] gdrive folder search: ${res.status} ${res.statusText}`);
102
- return null;
103
- }
104
- const data = await res.json();
105
- if (data.files?.[0]) {
106
- console.log(` [cloud] Found existing '${GDRIVE_FOLDER_NAME}' folder: ${data.files[0].id}`);
107
- return data.files[0].id;
108
- }
109
- // Create folder
110
- const createRes = await fetch("https://www.googleapis.com/drive/v3/files", {
111
- method: "POST",
112
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
113
- body: JSON.stringify({ name: GDRIVE_FOLDER_NAME, mimeType: "application/vnd.google-apps.folder" }),
114
- });
115
- if (!createRes.ok) {
116
- console.error(` [cloud] gdrive folder create: ${createRes.status} ${createRes.statusText}`);
117
- return null;
118
- }
119
- const created = await createRes.json();
120
- console.log(` [cloud] Created '${GDRIVE_FOLDER_NAME}' folder: ${created.id}`);
121
- return created.id;
122
- }
123
- catch (e) {
124
- console.error(` [cloud] gdrive folder setup: ${e.message}`);
125
- return null;
126
- }
127
- }
128
- /** Read a file by name from a folder (by ID) */
129
- async function gDriveReadFromFolder(folderId, fileName) {
130
- const token = await getGoogleDriveToken();
131
- if (!token) {
132
- console.error(` [cloud] gdrive read ${fileName}: no token`);
133
- return null;
134
- }
135
- try {
136
- // Find file in folder
137
- const query = `name='${fileName}' and '${folderId}' in parents and trashed=false`;
138
- const res = await fetch(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id)`, {
139
- headers: { Authorization: `Bearer ${token}` },
140
- });
141
- if (!res.ok) {
142
- console.error(` [cloud] gdrive lookup '${fileName}': ${res.status}`);
143
- return null;
144
- }
145
- const data = await res.json();
146
- const fileId = data.files?.[0]?.id;
147
- if (!fileId)
148
- return null;
149
- // Download content
150
- const contentRes = await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`, {
151
- headers: { Authorization: `Bearer ${token}` },
152
- });
153
- if (!contentRes.ok) {
154
- console.error(` [cloud] gdrive download ${fileName}: ${contentRes.status}`);
155
- return null;
156
- }
157
- return await contentRes.text();
158
- }
159
- catch (e) {
160
- console.error(` [cloud] gdrive read ${fileName}: ${e.message}`);
161
- return null;
162
- }
163
- }
164
- /** Write a file by name to a folder (by ID) — creates or updates */
165
- async function gDriveWriteToFolder(folderId, fileName, content) {
166
- const token = await getGoogleDriveToken();
167
- if (!token)
168
- return false;
169
- try {
170
- // Check if file exists in folder
171
- const query = `name='${fileName}' and '${folderId}' in parents and trashed=false`;
172
- const findRes = await fetch(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id)`, {
173
- headers: { Authorization: `Bearer ${token}` },
174
- });
175
- const findData = await findRes.json();
176
- const existingId = findData.files?.[0]?.id;
177
- if (existingId) {
178
- // Update existing file
179
- const res = await fetch(`https://www.googleapis.com/upload/drive/v3/files/${existingId}?uploadType=media`, {
180
- method: "PATCH",
181
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
182
- body: content,
183
- });
184
- return res.ok;
185
- }
186
- else {
187
- // Create new file in folder
188
- const boundary = "mailx_boundary_" + Date.now();
189
- const metadata = JSON.stringify({ name: fileName, parents: [folderId] });
190
- const body = `--${boundary}\r\nContent-Type: application/json\r\n\r\n${metadata}\r\n--${boundary}\r\nContent-Type: application/json\r\n\r\n${content}\r\n--${boundary}--`;
191
- const res = await fetch("https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart", {
192
- method: "POST",
193
- headers: { Authorization: `Bearer ${token}`, "Content-Type": `multipart/related; boundary=${boundary}` },
194
- body,
195
- });
196
- return res.ok;
197
- }
198
- }
199
- catch (e) {
200
- console.error(` [cloud] gdrive write ${fileName}: ${e.message}`);
201
- return false;
202
- }
203
- }
204
- /**
205
- * Get a cloud file provider. For gdrive, pass the folder ID.
206
- * Files are stored flat in the folder (no subdirectory navigation).
207
- */
208
- export function getCloudProvider(provider, folderId) {
209
- switch (provider) {
210
- case "google":
211
- case "gdrive":
212
- if (!folderId) {
213
- console.error(" [cloud] gdrive requires a folder ID — run initCloudConfig first");
214
- return null;
215
- }
216
- return {
217
- read: (fileName) => gDriveReadFromFolder(folderId, fileName),
218
- write: (fileName, content) => gDriveWriteToFolder(folderId, fileName, content),
219
- exists: async (fileName) => (await gDriveReadFromFolder(folderId, fileName)) !== null,
220
- };
221
- case "local":
222
- return {
223
- read: async (p) => { try {
224
- return fs.readFileSync(p, "utf-8");
225
- }
226
- catch {
227
- return null;
228
- } },
229
- write: async (p, c) => { try {
230
- fs.writeFileSync(p, c);
231
- return true;
232
- }
233
- catch {
234
- return false;
235
- } },
236
- exists: async (p) => fs.existsSync(p),
237
- };
238
- default:
239
- console.error(` [cloud] Provider "${provider}" not supported — only gdrive is available`);
240
- return null;
241
- }
242
- }
243
- //# sourceMappingURL=cloud.js.map