@bobfrankston/rmfmail 1.2.115 → 1.2.117
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/TODO.md +1217 -1217
- package/bin/mailx.js +41 -0
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +34 -0
- package/bin/popout-server.js +98 -0
- package/bin/popout-server.js.map +1 -1
- package/bin/popout-server.ts +97 -0
- package/client/app.bundle.js +19 -0
- package/client/app.bundle.js.map +2 -2
- package/client/compose/compose.bundle.js +118 -8
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/compose/compose.html +1 -0
- package/client/compose/compose.js +130 -16
- package/client/compose/compose.js.map +1 -1
- package/client/compose/compose.ts +122 -17
- package/client/lib/api-client.js +21 -0
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +22 -0
- package/client/lib/mailxapi.js +31 -4
- package/client/lib/popout-bridge.js +55 -0
- package/client/lib/popout-bridge.js.map +1 -0
- package/client/lib/popout-bridge.ts +48 -0
- package/package.json +1 -1
- package/packages/mailx-service/index.d.ts +36 -0
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +46 -0
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +56 -0
- package/packages/mailx-service/jsonrpc.js +6 -0
- package/packages/mailx-service/jsonrpc.js.map +1 -1
- package/packages/mailx-service/jsonrpc.ts +6 -0
package/bin/popout-server.js
CHANGED
|
@@ -22,20 +22,62 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import http from "node:http";
|
|
24
24
|
import crypto from "node:crypto";
|
|
25
|
+
import fs from "node:fs/promises";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { dispatch } from "@bobfrankston/mailx-service/jsonrpc.js";
|
|
25
28
|
/** svc must expose getMessage(accountId, uid, allowRemote, folderId) and
|
|
26
29
|
* openExternal(url). `onAction` receives toolbar actions from popout windows
|
|
27
30
|
* (reply/replyAll/forward/editAsNew/flag/delete) for routing to the main
|
|
28
31
|
* window. Returns null if the server can't bind (popout disabled). */
|
|
29
32
|
export async function startPopoutServer(svc, onAction) {
|
|
30
33
|
const token = crypto.randomBytes(18).toString("hex");
|
|
34
|
+
// Static root for /app/<token>/... — the app package root, so pages under
|
|
35
|
+
// client/ can reach ../styles, ../../node_modules etc. exactly as they do
|
|
36
|
+
// through msger's custom protocol.
|
|
37
|
+
const appRoot = path.resolve(import.meta.dirname, "..");
|
|
31
38
|
const server = http.createServer(async (req, res) => {
|
|
32
39
|
try {
|
|
33
40
|
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
41
|
+
// Static app files: /app/<token>/<relpath>. Token travels in the
|
|
42
|
+
// path so the page's RELATIVE asset/module loads (css, bundles,
|
|
43
|
+
// node_modules imports) stay inside the gate without rewriting.
|
|
44
|
+
const staticMatch = url.pathname.match(/^\/app\/([0-9a-f]+)\/(.*)$/);
|
|
45
|
+
if (req.method === "GET" && staticMatch) {
|
|
46
|
+
if (staticMatch[1] !== token) {
|
|
47
|
+
res.writeHead(403).end("forbidden");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
await serveStatic(appRoot, staticMatch[2], token, res);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
34
53
|
// Token gate on every request — loopback TCP is locally reachable.
|
|
35
54
|
if (url.searchParams.get("t") !== token) {
|
|
36
55
|
res.writeHead(403).end("forbidden");
|
|
37
56
|
return;
|
|
38
57
|
}
|
|
58
|
+
// Full mailxapi surface over HTTP: the popout compose page's
|
|
59
|
+
// bridge (client/lib/popout-bridge.ts) posts the SAME
|
|
60
|
+
// { _action, _cbid, ...params } payload the WebView IPC channel
|
|
61
|
+
// carries, and gets the dispatcher's { _cbid, result | error }
|
|
62
|
+
// back. One endpoint = the whole service, no per-method routes.
|
|
63
|
+
if (req.method === "POST" && url.pathname === "/rpc") {
|
|
64
|
+
let body = "";
|
|
65
|
+
for await (const chunk of req)
|
|
66
|
+
body += chunk;
|
|
67
|
+
let rpcReq = null;
|
|
68
|
+
try {
|
|
69
|
+
rpcReq = JSON.parse(body);
|
|
70
|
+
}
|
|
71
|
+
catch { /* handled below */ }
|
|
72
|
+
if (!rpcReq || typeof rpcReq._action !== "string") {
|
|
73
|
+
res.writeHead(400).end("bad rpc request");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const rpcRes = await dispatch(svc, rpcReq);
|
|
77
|
+
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
|
|
78
|
+
res.end(JSON.stringify(rpcRes));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
39
81
|
const a = url.searchParams.get("a") || "";
|
|
40
82
|
const u = Number(url.searchParams.get("u"));
|
|
41
83
|
const fRaw = url.searchParams.get("f");
|
|
@@ -113,6 +155,62 @@ export async function startPopoutServer(svc, onAction) {
|
|
|
113
155
|
});
|
|
114
156
|
});
|
|
115
157
|
}
|
|
158
|
+
const STATIC_MIME = {
|
|
159
|
+
html: "text/html; charset=utf-8",
|
|
160
|
+
js: "text/javascript; charset=utf-8",
|
|
161
|
+
mjs: "text/javascript; charset=utf-8",
|
|
162
|
+
css: "text/css; charset=utf-8",
|
|
163
|
+
json: "application/json",
|
|
164
|
+
map: "application/json",
|
|
165
|
+
svg: "image/svg+xml",
|
|
166
|
+
png: "image/png",
|
|
167
|
+
ico: "image/x-icon",
|
|
168
|
+
woff2: "font/woff2",
|
|
169
|
+
woff: "font/woff",
|
|
170
|
+
wasm: "application/wasm",
|
|
171
|
+
};
|
|
172
|
+
/** Serve one file from the app root for the popout compose window. HTML gets
|
|
173
|
+
* the IPC-over-HTTP bridge injected at the top of <head>: config + bridge
|
|
174
|
+
* (window.ipc → POST /rpc) + the REAL mailxapi.js — so the page runs the
|
|
175
|
+
* byte-identical API surface the main WebView gets via initScript. */
|
|
176
|
+
async function serveStatic(appRoot, relRaw, token, res) {
|
|
177
|
+
const rel = decodeURIComponent(relRaw);
|
|
178
|
+
const abs = path.resolve(appRoot, rel);
|
|
179
|
+
// Traversal guard — resolved path must stay inside the app root.
|
|
180
|
+
if (abs !== appRoot && !abs.startsWith(appRoot + path.sep)) {
|
|
181
|
+
res.writeHead(403).end("forbidden");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const ext = (abs.split(".").pop() || "").toLowerCase();
|
|
185
|
+
const mime = STATIC_MIME[ext] || "application/octet-stream";
|
|
186
|
+
let data;
|
|
187
|
+
try {
|
|
188
|
+
data = await fs.readFile(abs);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
res.writeHead(404).end("not found");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
// no-store everywhere: the token (and port) change per daemon run, but a
|
|
195
|
+
// stale cached bundle behind a same-shaped URL is exactly the class of
|
|
196
|
+
// bug that burned us before (see memory: webview_bundle_cache_stale).
|
|
197
|
+
const headers = { "content-type": mime, "cache-control": "no-store" };
|
|
198
|
+
if (ext === "html") {
|
|
199
|
+
// popout-bridge.js is type="module": tsc (NodeNext) emits `export {}`
|
|
200
|
+
// which is a SyntaxError in a classic script. Module scripts run
|
|
201
|
+
// after parse but in DOCUMENT ORDER, so the bridge still executes
|
|
202
|
+
// before the page's own module (compose.bundle.js) makes any RPC.
|
|
203
|
+
// mailxapi.js stays classic — it's the same file every host injects.
|
|
204
|
+
const inject = `<script>window.__rmfPopoutRpc={url:"/rpc?t=${token}"};</script>`
|
|
205
|
+
+ `<script type="module" src="/app/${token}/client/lib/popout-bridge.js"></script>`
|
|
206
|
+
+ `<script src="/app/${token}/client/lib/mailxapi.js"></script>`;
|
|
207
|
+
const html = data.toString("utf-8").replace(/<head([^>]*)>/i, (m) => m + inject);
|
|
208
|
+
res.writeHead(200, headers).end(html);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
res.writeHead(200, headers).end(data);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
116
214
|
function esc(s) {
|
|
117
215
|
return (s || "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
118
216
|
}
|
package/bin/popout-server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"popout-server.js","sourceRoot":"","sources":["popout-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,MAAM,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"popout-server.js","sourceRoot":"","sources":["popout-server.ts"],"names":[],"mappings":"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"}
|
package/bin/popout-server.ts
CHANGED
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import http from "node:http";
|
|
24
24
|
import crypto from "node:crypto";
|
|
25
|
+
import fs from "node:fs/promises";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { dispatch } from "@bobfrankston/mailx-service/jsonrpc.js";
|
|
25
28
|
|
|
26
29
|
export interface PopoutServerInfo {
|
|
27
30
|
port: number;
|
|
@@ -41,15 +44,53 @@ export interface PopoutAction {
|
|
|
41
44
|
* window. Returns null if the server can't bind (popout disabled). */
|
|
42
45
|
export async function startPopoutServer(svc: any, onAction?: (act: PopoutAction) => void): Promise<PopoutServerInfo | null> {
|
|
43
46
|
const token = crypto.randomBytes(18).toString("hex");
|
|
47
|
+
// Static root for /app/<token>/... — the app package root, so pages under
|
|
48
|
+
// client/ can reach ../styles, ../../node_modules etc. exactly as they do
|
|
49
|
+
// through msger's custom protocol.
|
|
50
|
+
const appRoot = path.resolve(import.meta.dirname, "..");
|
|
44
51
|
|
|
45
52
|
const server = http.createServer(async (req, res) => {
|
|
46
53
|
try {
|
|
47
54
|
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
55
|
+
|
|
56
|
+
// Static app files: /app/<token>/<relpath>. Token travels in the
|
|
57
|
+
// path so the page's RELATIVE asset/module loads (css, bundles,
|
|
58
|
+
// node_modules imports) stay inside the gate without rewriting.
|
|
59
|
+
const staticMatch = url.pathname.match(/^\/app\/([0-9a-f]+)\/(.*)$/);
|
|
60
|
+
if (req.method === "GET" && staticMatch) {
|
|
61
|
+
if (staticMatch[1] !== token) {
|
|
62
|
+
res.writeHead(403).end("forbidden");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
await serveStatic(appRoot, staticMatch[2], token, res);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
48
69
|
// Token gate on every request — loopback TCP is locally reachable.
|
|
49
70
|
if (url.searchParams.get("t") !== token) {
|
|
50
71
|
res.writeHead(403).end("forbidden");
|
|
51
72
|
return;
|
|
52
73
|
}
|
|
74
|
+
|
|
75
|
+
// Full mailxapi surface over HTTP: the popout compose page's
|
|
76
|
+
// bridge (client/lib/popout-bridge.ts) posts the SAME
|
|
77
|
+
// { _action, _cbid, ...params } payload the WebView IPC channel
|
|
78
|
+
// carries, and gets the dispatcher's { _cbid, result | error }
|
|
79
|
+
// back. One endpoint = the whole service, no per-method routes.
|
|
80
|
+
if (req.method === "POST" && url.pathname === "/rpc") {
|
|
81
|
+
let body = "";
|
|
82
|
+
for await (const chunk of req) body += chunk;
|
|
83
|
+
let rpcReq: any = null;
|
|
84
|
+
try { rpcReq = JSON.parse(body); } catch { /* handled below */ }
|
|
85
|
+
if (!rpcReq || typeof rpcReq._action !== "string") {
|
|
86
|
+
res.writeHead(400).end("bad rpc request");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const rpcRes = await dispatch(svc, rpcReq);
|
|
90
|
+
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
|
|
91
|
+
res.end(JSON.stringify(rpcRes));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
53
94
|
const a = url.searchParams.get("a") || "";
|
|
54
95
|
const u = Number(url.searchParams.get("u"));
|
|
55
96
|
const fRaw = url.searchParams.get("f");
|
|
@@ -116,6 +157,62 @@ export async function startPopoutServer(svc: any, onAction?: (act: PopoutAction)
|
|
|
116
157
|
});
|
|
117
158
|
}
|
|
118
159
|
|
|
160
|
+
const STATIC_MIME: Record<string, string> = {
|
|
161
|
+
html: "text/html; charset=utf-8",
|
|
162
|
+
js: "text/javascript; charset=utf-8",
|
|
163
|
+
mjs: "text/javascript; charset=utf-8",
|
|
164
|
+
css: "text/css; charset=utf-8",
|
|
165
|
+
json: "application/json",
|
|
166
|
+
map: "application/json",
|
|
167
|
+
svg: "image/svg+xml",
|
|
168
|
+
png: "image/png",
|
|
169
|
+
ico: "image/x-icon",
|
|
170
|
+
woff2: "font/woff2",
|
|
171
|
+
woff: "font/woff",
|
|
172
|
+
wasm: "application/wasm",
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/** Serve one file from the app root for the popout compose window. HTML gets
|
|
176
|
+
* the IPC-over-HTTP bridge injected at the top of <head>: config + bridge
|
|
177
|
+
* (window.ipc → POST /rpc) + the REAL mailxapi.js — so the page runs the
|
|
178
|
+
* byte-identical API surface the main WebView gets via initScript. */
|
|
179
|
+
async function serveStatic(appRoot: string, relRaw: string, token: string, res: http.ServerResponse): Promise<void> {
|
|
180
|
+
const rel = decodeURIComponent(relRaw);
|
|
181
|
+
const abs = path.resolve(appRoot, rel);
|
|
182
|
+
// Traversal guard — resolved path must stay inside the app root.
|
|
183
|
+
if (abs !== appRoot && !abs.startsWith(appRoot + path.sep)) {
|
|
184
|
+
res.writeHead(403).end("forbidden");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const ext = (abs.split(".").pop() || "").toLowerCase();
|
|
188
|
+
const mime = STATIC_MIME[ext] || "application/octet-stream";
|
|
189
|
+
let data: Buffer;
|
|
190
|
+
try {
|
|
191
|
+
data = await fs.readFile(abs);
|
|
192
|
+
} catch {
|
|
193
|
+
res.writeHead(404).end("not found");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
// no-store everywhere: the token (and port) change per daemon run, but a
|
|
197
|
+
// stale cached bundle behind a same-shaped URL is exactly the class of
|
|
198
|
+
// bug that burned us before (see memory: webview_bundle_cache_stale).
|
|
199
|
+
const headers = { "content-type": mime, "cache-control": "no-store" };
|
|
200
|
+
if (ext === "html") {
|
|
201
|
+
// popout-bridge.js is type="module": tsc (NodeNext) emits `export {}`
|
|
202
|
+
// which is a SyntaxError in a classic script. Module scripts run
|
|
203
|
+
// after parse but in DOCUMENT ORDER, so the bridge still executes
|
|
204
|
+
// before the page's own module (compose.bundle.js) makes any RPC.
|
|
205
|
+
// mailxapi.js stays classic — it's the same file every host injects.
|
|
206
|
+
const inject = `<script>window.__rmfPopoutRpc={url:"/rpc?t=${token}"};</script>`
|
|
207
|
+
+ `<script type="module" src="/app/${token}/client/lib/popout-bridge.js"></script>`
|
|
208
|
+
+ `<script src="/app/${token}/client/lib/mailxapi.js"></script>`;
|
|
209
|
+
const html = data.toString("utf-8").replace(/<head([^>]*)>/i, (m) => m + inject);
|
|
210
|
+
res.writeHead(200, headers).end(html);
|
|
211
|
+
} else {
|
|
212
|
+
res.writeHead(200, headers).end(data);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
119
216
|
function esc(s: string): string {
|
|
120
217
|
return (s || "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
121
218
|
}
|
package/client/app.bundle.js
CHANGED
|
@@ -27,10 +27,12 @@ __export(api_client_exports, {
|
|
|
27
27
|
autocomplete: () => autocomplete,
|
|
28
28
|
cancelQueuedOutgoing: () => cancelQueuedOutgoing,
|
|
29
29
|
cancelServerSearch: () => cancelServerSearch,
|
|
30
|
+
closeComposePopout: () => closeComposePopout,
|
|
30
31
|
closeWordEdit: () => closeWordEdit,
|
|
31
32
|
connectEvents: () => connectEvents,
|
|
32
33
|
connectWebSocket: () => connectWebSocket,
|
|
33
34
|
consumePendingMailto: () => consumePendingMailto,
|
|
35
|
+
consumePopoutComposeInit: () => consumePopoutComposeInit,
|
|
34
36
|
copyMessages: () => copyMessages,
|
|
35
37
|
createCalendarEvent: () => createCalendarEvent,
|
|
36
38
|
createFolder: () => createFolder,
|
|
@@ -85,6 +87,7 @@ __export(api_client_exports, {
|
|
|
85
87
|
openInTextEditor: () => openInTextEditor,
|
|
86
88
|
openInWord: () => openInWord,
|
|
87
89
|
openLocalPath: () => openLocalPath,
|
|
90
|
+
popoutCompose: () => popoutCompose,
|
|
88
91
|
popoutWindow: () => popoutWindow,
|
|
89
92
|
readConfigHelp: () => readConfigHelp,
|
|
90
93
|
readJsoncFile: () => readJsoncFile,
|
|
@@ -628,6 +631,22 @@ async function popoutWindow(accountId, uid, folderId, subject) {
|
|
|
628
631
|
return { ok: false, reason: "no popoutWindow bridge" };
|
|
629
632
|
return fn(accountId, uid, folderId, subject);
|
|
630
633
|
}
|
|
634
|
+
async function popoutCompose(init) {
|
|
635
|
+
const fn = ipc().popoutCompose;
|
|
636
|
+
if (!fn)
|
|
637
|
+
return { ok: false, reason: "no popoutCompose bridge" };
|
|
638
|
+
return fn(init);
|
|
639
|
+
}
|
|
640
|
+
async function consumePopoutComposeInit(id) {
|
|
641
|
+
const fn = ipc().consumePopoutComposeInit;
|
|
642
|
+
if (!fn)
|
|
643
|
+
return { init: null };
|
|
644
|
+
return fn(id);
|
|
645
|
+
}
|
|
646
|
+
function closeComposePopout(id) {
|
|
647
|
+
const fn = ipc().closeComposePopout;
|
|
648
|
+
return fn ? fn(id) : Promise.resolve({ ok: false });
|
|
649
|
+
}
|
|
631
650
|
async function getDeviceAccounts() {
|
|
632
651
|
return ipc().getDeviceAccounts?.() ?? [];
|
|
633
652
|
}
|