@bobfrankston/mailx 1.0.199 → 1.0.200
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/bin/bin/mailx.js +842 -0
- package/bin/bin/postinstall.js +39 -0
- package/bin/mailx.js +68 -19
- package/bin/packages/mailx-settings/cloud.js +243 -0
- package/bin/packages/mailx-settings/index.js +643 -0
- package/client/.msger-window.json +1 -1
- package/package.json +2 -2
- package/packages/mailx-store-web/gmail-api-web.js +4 -1
|
@@ -0,0 +1,39 @@
|
|
|
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
|
package/bin/mailx.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import fs from "node:fs";
|
|
20
20
|
import path from "node:path";
|
|
21
|
+
import os from "node:os";
|
|
21
22
|
import net from "node:net";
|
|
22
23
|
import { ports } from "@bobfrankston/miscinfo";
|
|
23
24
|
import { showMessageBox, showService } from "@bobfrankston/msger";
|
|
@@ -25,14 +26,12 @@ const PORT = ports.mailx;
|
|
|
25
26
|
const args = process.argv.slice(2);
|
|
26
27
|
// Normalize: accept both -flag and --flag
|
|
27
28
|
function hasFlag(name) { return args.includes(`-${name}`) || args.includes(`--${name}`); }
|
|
28
|
-
const serverMode = hasFlag("server");
|
|
29
|
-
const noBrowser = hasFlag("no-browser");
|
|
30
29
|
const verbose = hasFlag("verbose");
|
|
31
30
|
const isDaemon = hasFlag("daemon"); // internal: re-spawned detached process
|
|
32
31
|
// Auto-detach: re-spawn as background process so terminal returns immediately
|
|
33
|
-
// Skip for: --verbose (want console), --
|
|
32
|
+
// Skip for: --verbose (want console), --daemon (already detached),
|
|
34
33
|
// and any command flags (setup, kill, test, etc.)
|
|
35
|
-
if (!verbose && !
|
|
34
|
+
if (!verbose && !isDaemon && !process.argv.slice(2).some(a => /^-/.test(a) && !["--no-browser"].includes(a))) {
|
|
36
35
|
const { spawn } = await import("node:child_process");
|
|
37
36
|
const child = spawn(process.execPath, [...process.argv.slice(1), "--daemon"], {
|
|
38
37
|
detached: true,
|
|
@@ -49,12 +48,12 @@ const rebuildMode = hasFlag("rebuild");
|
|
|
49
48
|
const repairMode = hasFlag("repair");
|
|
50
49
|
const importMode = hasFlag("import");
|
|
51
50
|
// Validate arguments
|
|
52
|
-
const knownFlags = ["
|
|
51
|
+
const knownFlags = ["verbose", "kill", "v", "version", "setup", "add", "test", "rebuild", "repair", "log", "import", "email", "mail", "daemon"];
|
|
53
52
|
for (const arg of args) {
|
|
54
53
|
const flag = arg.replace(/^--?/, "");
|
|
55
54
|
if (arg.startsWith("-") && !knownFlags.includes(flag)) {
|
|
56
55
|
console.error(`Unknown option: ${arg}`);
|
|
57
|
-
console.error("Usage: mailx [-
|
|
56
|
+
console.error("Usage: mailx [-verbose] [-kill] [-rebuild] [-v] [-setup]");
|
|
58
57
|
process.exit(1);
|
|
59
58
|
}
|
|
60
59
|
}
|
|
@@ -578,10 +577,62 @@ async function runTest() {
|
|
|
578
577
|
console.log("Test complete. Check your inbox for the test message(s).");
|
|
579
578
|
process.exit(0);
|
|
580
579
|
}
|
|
580
|
+
/** Register this client on GDrive — writes/updates clients.jsonc with device info */
|
|
581
|
+
async function registerClient(settings) {
|
|
582
|
+
const { cloudRead, cloudWrite } = await import("@bobfrankston/mailx-settings");
|
|
583
|
+
// Device ID: stable per machine, stored locally
|
|
584
|
+
const deviceIdPath = path.join(os.homedir(), ".mailx", "device-id");
|
|
585
|
+
let deviceId;
|
|
586
|
+
if (fs.existsSync(deviceIdPath)) {
|
|
587
|
+
deviceId = fs.readFileSync(deviceIdPath, "utf-8").trim();
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
deviceId = `${os.hostname()}-${Date.now().toString(36)}`;
|
|
591
|
+
fs.mkdirSync(path.dirname(deviceIdPath), { recursive: true });
|
|
592
|
+
fs.writeFileSync(deviceIdPath, deviceId);
|
|
593
|
+
}
|
|
594
|
+
// Get local IP
|
|
595
|
+
let localIp = "";
|
|
596
|
+
try {
|
|
597
|
+
const nets = os.networkInterfaces();
|
|
598
|
+
for (const addrs of Object.values(nets)) {
|
|
599
|
+
for (const addr of addrs || []) {
|
|
600
|
+
if (addr.family === "IPv4" && !addr.internal) {
|
|
601
|
+
localIp = addr.address;
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (localIp)
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
catch { /* ignore */ }
|
|
610
|
+
// Read existing clients.jsonc from cloud
|
|
611
|
+
let clients = {};
|
|
612
|
+
try {
|
|
613
|
+
const content = await cloudRead("clients.jsonc");
|
|
614
|
+
if (content)
|
|
615
|
+
clients = JSON.parse(content);
|
|
616
|
+
}
|
|
617
|
+
catch { /* start fresh */ }
|
|
618
|
+
// Update this device's entry
|
|
619
|
+
clients[deviceId] = {
|
|
620
|
+
hostname: os.hostname(),
|
|
621
|
+
platform: `${process.platform} ${process.arch}`,
|
|
622
|
+
accounts: settings.accounts.map((a) => a.id),
|
|
623
|
+
lastSeen: new Date().toISOString(),
|
|
624
|
+
ip: localIp,
|
|
625
|
+
version: JSON.parse(fs.readFileSync(path.join(import.meta.dirname, "..", "package.json"), "utf-8")).version,
|
|
626
|
+
};
|
|
627
|
+
// Write back
|
|
628
|
+
const ok = await cloudWrite("clients.jsonc", JSON.stringify(clients, null, 2));
|
|
629
|
+
if (ok)
|
|
630
|
+
console.log(` [client] Registered device: ${deviceId}`);
|
|
631
|
+
}
|
|
581
632
|
async function main() {
|
|
582
633
|
log(`Platform: ${process.platform} ${process.arch}`);
|
|
583
634
|
log(`Node: ${process.version}`);
|
|
584
|
-
log(`Mode: ${
|
|
635
|
+
log(`Mode: ${setupMode ? "setup" : "auto"}`);
|
|
585
636
|
// Test connectivity
|
|
586
637
|
if (testMode) {
|
|
587
638
|
await runTest();
|
|
@@ -632,18 +683,7 @@ async function main() {
|
|
|
632
683
|
console.log = (...a) => { logStream.write(`${ts()} ${a.join(" ")}\n`); };
|
|
633
684
|
console.error = (...a) => { logStream.write(`${ts()} ERROR ${a.join(" ")}\n`); };
|
|
634
685
|
}
|
|
635
|
-
//
|
|
636
|
-
if (serverMode) {
|
|
637
|
-
console.log("Starting mailx HTTP server...");
|
|
638
|
-
if (hasFlag("external"))
|
|
639
|
-
process.argv.push("--external");
|
|
640
|
-
await import("../packages/mailx-server/index.js");
|
|
641
|
-
if (!noBrowser)
|
|
642
|
-
launchMsger(`http://127.0.0.1:${PORT}`);
|
|
643
|
-
await new Promise(() => { }); // keep alive
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
646
|
-
// Default: service mode — no TCP, IPC via msger
|
|
686
|
+
// IPC service mode — no HTTP server
|
|
647
687
|
console.log("Starting mailx service...");
|
|
648
688
|
const { MailxDB } = await import("@bobfrankston/mailx-store");
|
|
649
689
|
const { ImapManager } = await import("@bobfrankston/mailx-imap");
|
|
@@ -667,7 +707,9 @@ async function main() {
|
|
|
667
707
|
const clientDir = path.join(import.meta.dirname, "..", "client");
|
|
668
708
|
const mailxapiPath = path.join(clientDir, "lib", "mailxapi.js");
|
|
669
709
|
const mailxapiScript = fs.readFileSync(mailxapiPath, "utf-8");
|
|
710
|
+
const rootPkgVersion = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, "..", "package.json"), "utf-8")).version;
|
|
670
711
|
const handle = showService({
|
|
712
|
+
title: `mailx v${rootPkgVersion}`,
|
|
671
713
|
url: "index.html",
|
|
672
714
|
contentDir: clientDir,
|
|
673
715
|
initScript: mailxapiScript,
|
|
@@ -680,6 +722,11 @@ async function main() {
|
|
|
680
722
|
const rootPkg = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, "..", "package.json"), "utf-8"));
|
|
681
723
|
handle.onRequest(async (req) => {
|
|
682
724
|
if (!req._action) {
|
|
725
|
+
// msger sends {"button":"OK","closed":true} on navigation — ignore it, don't exit
|
|
726
|
+
if (req.closed || req.button) {
|
|
727
|
+
console.log(`[ipc] ← ignored close signal during navigation: ${JSON.stringify(req).substring(0, 100)}`);
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
683
730
|
console.log(`[ipc] ← ignored (no _action): ${JSON.stringify(req).substring(0, 100)}`);
|
|
684
731
|
return;
|
|
685
732
|
}
|
|
@@ -743,6 +790,8 @@ async function main() {
|
|
|
743
790
|
console.error(` Failed: ${account.id}: ${e.message}`);
|
|
744
791
|
}
|
|
745
792
|
}
|
|
793
|
+
// Register this client device on GDrive (fire-and-forget)
|
|
794
|
+
registerClient(settings).catch(() => { });
|
|
746
795
|
// Start sync in background — don't block
|
|
747
796
|
if (settings.accounts.some(a => a.enabled)) {
|
|
748
797
|
imapManager.syncAll().catch(e => console.error(` Sync error: ${e.message}`));
|
|
@@ -0,0 +1,243 @@
|
|
|
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
|