@bobfrankston/rmfmail 1.2.128 → 1.2.130
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/README.md +11 -0
- package/bin/mailx.js +27 -0
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +23 -0
- package/bin/mcp-server.js +203 -0
- package/bin/mcp-server.js.map +1 -0
- package/bin/mcp-server.ts +199 -0
- package/bin/popout-server.js +39 -5
- package/bin/popout-server.js.map +1 -1
- package/bin/popout-server.ts +41 -5
- package/client/compose/compose.bundle.js +11 -5
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/compose/compose.js +20 -6
- package/client/compose/compose.js.map +1 -1
- package/client/compose/compose.ts +18 -5
- package/package.json +5 -4
package/bin/popout-server.ts
CHANGED
|
@@ -29,6 +29,11 @@ import { dispatch } from "@bobfrankston/mailx-service/jsonrpc.js";
|
|
|
29
29
|
export interface PopoutServerInfo {
|
|
30
30
|
port: number;
|
|
31
31
|
token: string;
|
|
32
|
+
/** Agent tokens (written to ~/.rmfmail/agent.json for third-party apps
|
|
33
|
+
* and AI agents). `triage` = reads + flagging; `full` = everything the
|
|
34
|
+
* UI can do. Sending remains a human act by convention — the MCP layer
|
|
35
|
+
* exposes drafts, not send. */
|
|
36
|
+
agentTokens: { triage: string; full: string };
|
|
32
37
|
/** Push a daemon event to every connected popout/secondary window
|
|
33
38
|
* (SSE). The main window gets events over the msger service channel;
|
|
34
39
|
* popout-served pages subscribe to GET /events and receive the same
|
|
@@ -49,6 +54,18 @@ export interface PopoutAction {
|
|
|
49
54
|
* window. Returns null if the server can't bind (popout disabled). */
|
|
50
55
|
export async function startPopoutServer(svc: any, onAction?: (act: PopoutAction) => void): Promise<PopoutServerInfo | null> {
|
|
51
56
|
const token = crypto.randomBytes(18).toString("hex");
|
|
57
|
+
const agentTokens = { triage: crypto.randomBytes(18).toString("hex"), full: crypto.randomBytes(18).toString("hex") };
|
|
58
|
+
// Actions a TRIAGE-scoped agent may call: reads plus flag/read-state.
|
|
59
|
+
// Deliberately excludes send/move/delete/settings and every popout/window
|
|
60
|
+
// action. Full-scope agents get the whole dispatcher.
|
|
61
|
+
const TRIAGE_ACTIONS = new Set([
|
|
62
|
+
"getVersion", "getAccounts", "getFolders", "getMessages", "getUnifiedInbox",
|
|
63
|
+
"getMessage", "searchMessages", "getThreadMessages", "getAttachment",
|
|
64
|
+
"getMessageSource", "searchContacts", "listContacts", "getCalendarEvents",
|
|
65
|
+
"getCalendars", "getTasks", "getOutboxStatus", "getSyncPending",
|
|
66
|
+
"getDiagnostics", "getPriorityLists", "hasCcHistoryTo", "hasBccHistoryTo",
|
|
67
|
+
"updateFlags", "markFolderRead", "logClientEvent",
|
|
68
|
+
]);
|
|
52
69
|
// Static root for /app/<token>/... — the app package root, so pages under
|
|
53
70
|
// client/ can reach ../styles, ../../node_modules etc. exactly as they do
|
|
54
71
|
// through msger's custom protocol.
|
|
@@ -69,12 +86,18 @@ export async function startPopoutServer(svc: any, onAction?: (act: PopoutAction)
|
|
|
69
86
|
res.writeHead(403).end("forbidden");
|
|
70
87
|
return;
|
|
71
88
|
}
|
|
72
|
-
await serveStatic(appRoot, staticMatch[2], token, res);
|
|
89
|
+
await serveStatic(appRoot, staticMatch[2], token, res, svc);
|
|
73
90
|
return;
|
|
74
91
|
}
|
|
75
92
|
|
|
76
93
|
// Token gate on every request — loopback TCP is locally reachable.
|
|
77
|
-
|
|
94
|
+
// Three tokens: the per-launch window token (full surface), and two
|
|
95
|
+
// agent tokens published in ~/.rmfmail/agent.json.
|
|
96
|
+
const presented = url.searchParams.get("t");
|
|
97
|
+
const isWindowToken = presented === token;
|
|
98
|
+
const isFullAgent = presented === agentTokens.full;
|
|
99
|
+
const isTriageAgent = presented === agentTokens.triage;
|
|
100
|
+
if (!isWindowToken && !isFullAgent && !isTriageAgent) {
|
|
78
101
|
res.writeHead(403).end("forbidden");
|
|
79
102
|
return;
|
|
80
103
|
}
|
|
@@ -109,6 +132,11 @@ export async function startPopoutServer(svc: any, onAction?: (act: PopoutAction)
|
|
|
109
132
|
res.writeHead(400).end("bad rpc request");
|
|
110
133
|
return;
|
|
111
134
|
}
|
|
135
|
+
if (isTriageAgent && !TRIAGE_ACTIONS.has(rpcReq._action)) {
|
|
136
|
+
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
|
|
137
|
+
res.end(JSON.stringify({ _cbid: rpcReq._cbid, error: `action "${rpcReq._action}" requires the full agent token (triage scope is reads + flags)` }));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
112
140
|
const rpcRes = await dispatch(svc, rpcReq);
|
|
113
141
|
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
|
|
114
142
|
res.end(JSON.stringify(rpcRes));
|
|
@@ -183,7 +211,7 @@ export async function startPopoutServer(svc: any, onAction?: (act: PopoutAction)
|
|
|
183
211
|
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
184
212
|
if (port) {
|
|
185
213
|
console.log(` [popout] window server on http://127.0.0.1:${port} (loopback, token-gated)`);
|
|
186
|
-
resolve({ port, token, broadcastEvent });
|
|
214
|
+
resolve({ port, token, agentTokens, broadcastEvent });
|
|
187
215
|
} else {
|
|
188
216
|
resolve(null);
|
|
189
217
|
}
|
|
@@ -210,7 +238,7 @@ const STATIC_MIME: Record<string, string> = {
|
|
|
210
238
|
* the IPC-over-HTTP bridge injected at the top of <head>: config + bridge
|
|
211
239
|
* (window.ipc → POST /rpc) + the REAL mailxapi.js — so the page runs the
|
|
212
240
|
* byte-identical API surface the main WebView gets via initScript. */
|
|
213
|
-
async function serveStatic(appRoot: string, relRaw: string, token: string, res: http.ServerResponse): Promise<void> {
|
|
241
|
+
async function serveStatic(appRoot: string, relRaw: string, token: string, res: http.ServerResponse, svc?: any): Promise<void> {
|
|
214
242
|
const rel = decodeURIComponent(relRaw);
|
|
215
243
|
const abs = path.resolve(appRoot, rel);
|
|
216
244
|
// Traversal guard — resolved path must stay inside the app root.
|
|
@@ -237,7 +265,15 @@ async function serveStatic(appRoot: string, relRaw: string, token: string, res:
|
|
|
237
265
|
// after parse but in DOCUMENT ORDER, so the bridge still executes
|
|
238
266
|
// before the page's own module (compose.bundle.js) makes any RPC.
|
|
239
267
|
// mailxapi.js stays classic — it's the same file every host injects.
|
|
240
|
-
|
|
268
|
+
//
|
|
269
|
+
// Settings snapshot baked into the page: popout windows live on a
|
|
270
|
+
// per-port origin whose localStorage caches (editor type, font size)
|
|
271
|
+
// start EMPTY on every daemon restart — a reply opened in Quill when
|
|
272
|
+
// TinyMCE was selected (Bob 2026-07-12). Pages prefer this snapshot
|
|
273
|
+
// over their localStorage caches; ui subset keeps the payload small.
|
|
274
|
+
let bootUi: any = null;
|
|
275
|
+
try { bootUi = (await svc?.getSettings?.())?.ui ?? null; } catch { /* page falls back to defaults */ }
|
|
276
|
+
const inject = `<script>window.__rmfPopoutRpc={url:"/rpc?t=${token}",events:"/events?t=${token}"};window.__rmfBootSettings=${JSON.stringify({ ui: bootUi })};</script>`
|
|
241
277
|
+ `<script type="module" src="/app/${token}/client/lib/popout-bridge.js"></script>`
|
|
242
278
|
+ `<script src="/app/${token}/client/lib/mailxapi.js"></script>`;
|
|
243
279
|
const html = data.toString("utf-8").replace(/<head([^>]*)>/i, (m) => m + inject);
|
|
@@ -3851,10 +3851,15 @@ async function loadEditorAssets(type) {
|
|
|
3851
3851
|
}
|
|
3852
3852
|
var editorType = "quill";
|
|
3853
3853
|
var appSettings = null;
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
}
|
|
3854
|
+
var __bootUi = window.__rmfBootSettings?.ui || null;
|
|
3855
|
+
if (__bootUi?.editor === "tiptap" || __bootUi?.editor === "quill" || __bootUi?.editor === "tinymce") {
|
|
3856
|
+
editorType = __bootUi.editor;
|
|
3857
|
+
} else {
|
|
3858
|
+
try {
|
|
3859
|
+
const cached = localStorage.getItem("mailx-editor-type");
|
|
3860
|
+
if (cached === "tiptap" || cached === "quill" || cached === "tinymce") editorType = cached;
|
|
3861
|
+
} catch {
|
|
3862
|
+
}
|
|
3858
3863
|
}
|
|
3859
3864
|
var COMPOSE_FONT_MIN_PX = 8;
|
|
3860
3865
|
var COMPOSE_FONT_MAX_PX = 32;
|
|
@@ -3864,7 +3869,8 @@ function applyComposeFontSize(px) {
|
|
|
3864
3869
|
document.documentElement.style.setProperty("--compose-font-size", `${px}px`);
|
|
3865
3870
|
}
|
|
3866
3871
|
try {
|
|
3867
|
-
|
|
3872
|
+
const bootFont = Number(__bootUi?.composeFontSize);
|
|
3873
|
+
applyComposeFontSize(Number.isFinite(bootFont) && bootFont > 0 ? bootFont : Number(localStorage.getItem("mailx-compose-font-size")));
|
|
3868
3874
|
} catch {
|
|
3869
3875
|
applyComposeFontSize(COMPOSE_FONT_DEFAULT_PX);
|
|
3870
3876
|
}
|