@bobfrankston/rmfmail 1.2.103 → 1.2.106
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 +1 -1
- package/bin/mailx.js +66 -3
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +58 -3
- package/bin/popout-server.js +73 -11
- package/bin/popout-server.js.map +1 -1
- package/bin/popout-server.ts +76 -11
- package/client/app.bundle.js +28 -26
- package/client/app.bundle.js.map +3 -3
- package/client/app.js +22 -5
- package/client/app.js.map +1 -1
- package/client/app.ts +21 -5
- package/client/components/message-viewer.js +18 -36
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +21 -35
- package/client/compose/compose.bundle.js +7 -0
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/index.html +1 -1
- package/client/lib/api-client.js +9 -0
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +9 -0
- package/client/lib/mailxapi.js +6 -0
- package/package.json +1 -1
- package/packages/mailx-service/index.d.ts +25 -0
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +17 -0
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +24 -0
- package/packages/mailx-service/jsonrpc.js +2 -0
- package/packages/mailx-service/jsonrpc.js.map +1 -1
- package/packages/mailx-service/jsonrpc.ts +2 -0
package/bin/mailx.ts
CHANGED
|
@@ -1918,15 +1918,37 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
|
|
|
1918
1918
|
void syncWorkerHandle;
|
|
1919
1919
|
// Native client is the only option (iflow-direct)
|
|
1920
1920
|
const svc = new MailxService(store, imapManager);
|
|
1921
|
-
// Loopback popout-window server (
|
|
1922
|
-
//
|
|
1921
|
+
// Loopback popout-window server (renders a single message for its own OS
|
|
1922
|
+
// window). Ephemeral port + per-launch token; best-effort. Toolbar actions
|
|
1923
|
+
// from popout windows are forwarded to the main window as `popoutAction`
|
|
1924
|
+
// events (app.ts opens the editable compose there — the popout page has no
|
|
1925
|
+
// IPC bridge so it can't host compose itself). `sendToClient` is late-bound:
|
|
1926
|
+
// the ServiceHandle doesn't exist yet at this point in startup.
|
|
1923
1927
|
let popoutInfo: { port: number; token: string } | null = null;
|
|
1928
|
+
let sendToClient: ((msg: any) => void) | null = null;
|
|
1929
|
+
const popoutWindows = new Map<string, ReturnType<typeof showMessageBoxEx>>();
|
|
1930
|
+
const popoutKey = (a: string, f: number | undefined, u: number): string => `${a}|${f ?? ""}|${u}`;
|
|
1924
1931
|
try {
|
|
1925
1932
|
const { startPopoutServer } = await import("./popout-server.js");
|
|
1926
|
-
popoutInfo = await startPopoutServer(svc)
|
|
1933
|
+
popoutInfo = await startPopoutServer(svc, (act) => {
|
|
1934
|
+
console.log(` [popout] action ${act.action} a=${act.accountId} u=${act.uid} f=${act.folderId ?? ""}`);
|
|
1935
|
+
sendToClient?.({ _event: "popoutAction", type: "popoutAction", ...act });
|
|
1936
|
+
if (act.action === "delete") {
|
|
1937
|
+
// The message is gone — close its window (browser-opened
|
|
1938
|
+
// fallback windows close themselves client-side).
|
|
1939
|
+
const h = popoutWindows.get(popoutKey(act.accountId, act.folderId, act.uid));
|
|
1940
|
+
if (h && !h.closed) { try { h.close(); } catch { /* already gone */ } }
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1927
1943
|
} catch (e: any) {
|
|
1928
1944
|
console.error(` [popout] disabled: ${e?.message || e}`);
|
|
1929
1945
|
}
|
|
1946
|
+
// Debug-server runs expose the popout token so an external driver (agent,
|
|
1947
|
+
// curl) can exercise /v and /action headlessly. Loopback-only surface;
|
|
1948
|
+
// never logged in normal runs.
|
|
1949
|
+
if (popoutInfo && hasFlag("debug-server")) {
|
|
1950
|
+
console.log(` [popout] DEBUG token=${popoutInfo.token} (logged because --debug-server)`);
|
|
1951
|
+
}
|
|
1930
1952
|
// Inject the popup function so MailxService.showReminderPopup can spawn
|
|
1931
1953
|
// an OS-level always-on-top window via mailx-host. Kept as injection
|
|
1932
1954
|
// (not import) so mailx-service stays host-agnostic.
|
|
@@ -2029,6 +2051,36 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
|
|
|
2029
2051
|
// the same way the `mailx` CLI does.
|
|
2030
2052
|
const __mailxJs = path.join(import.meta.dirname, "mailx.js");
|
|
2031
2053
|
const __relaunchCommand = `"${process.execPath}" "${__mailxJs}"`;
|
|
2054
|
+
// "Open in separate window" (🗗) spawns a NATIVE msger WebView window
|
|
2055
|
+
// pointed at the loopback popout server — not the OS browser (Bob
|
|
2056
|
+
// 2026-07-06). Injected here (not in mailx-service) because it needs
|
|
2057
|
+
// mailx-host + the icon paths. One window per message: a second request
|
|
2058
|
+
// for the same message is a no-op while its window is open. Child PIDs
|
|
2059
|
+
// are tracked in instance.json so `rmfmail -kill` reaps popouts too.
|
|
2060
|
+
if (popoutInfo) {
|
|
2061
|
+
const pi = popoutInfo;
|
|
2062
|
+
svc.setPopoutWindowFn(async ({ accountId, uid, folderId, subject }) => {
|
|
2063
|
+
const key = popoutKey(accountId, folderId, uid);
|
|
2064
|
+
const existing = popoutWindows.get(key);
|
|
2065
|
+
if (existing && !existing.closed) return { ok: true };
|
|
2066
|
+
const url = `http://127.0.0.1:${pi.port}/v?a=${encodeURIComponent(accountId)}&u=${uid}&f=${folderId ?? ""}&t=${encodeURIComponent(pi.token)}`;
|
|
2067
|
+
const h = showMessageBoxEx({
|
|
2068
|
+
title: subject || "Message",
|
|
2069
|
+
url,
|
|
2070
|
+
size: { width: 820, height: 900 },
|
|
2071
|
+
icon: __iconPathRuntime,
|
|
2072
|
+
aumid: "com.frankston.rmfmail",
|
|
2073
|
+
escapeCloses: true,
|
|
2074
|
+
});
|
|
2075
|
+
popoutWindows.set(key, h);
|
|
2076
|
+
if (typeof h.pid === "number") addChildPid(h.pid);
|
|
2077
|
+
void h.result.catch(() => { /* window died — cleanup below */ }).finally(() => {
|
|
2078
|
+
popoutWindows.delete(key);
|
|
2079
|
+
if (typeof h.pid === "number") removeChildPid(h.pid);
|
|
2080
|
+
});
|
|
2081
|
+
return { ok: true };
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2032
2084
|
_tick("calling showService (WebView opens here)");
|
|
2033
2085
|
const handle = showService({
|
|
2034
2086
|
title: `rmfmail v${rootPkgVersion}`,
|
|
@@ -2048,6 +2100,9 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
|
|
|
2048
2100
|
: undefined,
|
|
2049
2101
|
escapeCloses: false,
|
|
2050
2102
|
});
|
|
2103
|
+
// Late-bind the popout-action forwarder now that the main window's
|
|
2104
|
+
// service channel exists.
|
|
2105
|
+
sendToClient = (m: any) => handle.send(m);
|
|
2051
2106
|
|
|
2052
2107
|
// Register ourselves as the live instance so subsequent `rmfmail`
|
|
2053
2108
|
// invocations can detect version-mismatch and upgrade us (see top of
|
package/bin/popout-server.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Popout server — a tiny loopback HTTP server that renders a single message in
|
|
3
|
-
* its own OS window
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* its own OS window. The daemon points a native msger WebView window at it
|
|
4
|
+
* (svc.popoutWindow → showMessageBoxEx({url}) in bin/mailx.ts); a plain browser
|
|
5
|
+
* can load the same URL, which is the future "browser option". msger's custom
|
|
6
|
+
* protocol doesn't propagate to child windows, so a separate window can't use
|
|
7
|
+
* the normal IPC client; this serves a self-contained view instead.
|
|
7
8
|
*
|
|
8
9
|
* Transport choice (Bob 2026-06-29): loopback TCP on an OS-assigned EPHEMERAL
|
|
9
10
|
* port (`listen(0)`) — no hardcoded port to collide, identical on every
|
|
@@ -12,14 +13,20 @@
|
|
|
12
13
|
* reachable by any local process, so every route is gated on a per-launch random
|
|
13
14
|
* token. Future: the `msgapi` channel could serve this without HTTP at all.
|
|
14
15
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
16
|
+
* Toolbar actions (Bob 2026-07-06 "when I want to do the reply I need it to be
|
|
17
|
+
* editable"): the popout page can't host compose (no IPC bridge, no Quill), so
|
|
18
|
+
* Reply / Reply All / Forward / Edit-as-new / Flag / Delete POST back to
|
|
19
|
+
* /action, which hands them to `onAction` — bin/mailx.ts forwards that as a
|
|
20
|
+
* `popoutAction` event to the MAIN window, where app.ts opens the real,
|
|
21
|
+
* editable compose overlay (same path as the in-window popout's toolbar).
|
|
17
22
|
*/
|
|
18
23
|
import http from "node:http";
|
|
19
24
|
import crypto from "node:crypto";
|
|
20
25
|
/** svc must expose getMessage(accountId, uid, allowRemote, folderId) and
|
|
21
|
-
* openExternal(url).
|
|
22
|
-
|
|
26
|
+
* openExternal(url). `onAction` receives toolbar actions from popout windows
|
|
27
|
+
* (reply/replyAll/forward/editAsNew/flag/delete) for routing to the main
|
|
28
|
+
* window. Returns null if the server can't bind (popout disabled). */
|
|
29
|
+
export async function startPopoutServer(svc, onAction) {
|
|
23
30
|
const token = crypto.randomBytes(18).toString("hex");
|
|
24
31
|
const server = http.createServer(async (req, res) => {
|
|
25
32
|
try {
|
|
@@ -60,6 +67,25 @@ export async function startPopoutServer(svc) {
|
|
|
60
67
|
res.writeHead(204).end();
|
|
61
68
|
return;
|
|
62
69
|
}
|
|
70
|
+
if (req.method === "POST" && url.pathname === "/action") {
|
|
71
|
+
let body = "";
|
|
72
|
+
for await (const chunk of req)
|
|
73
|
+
body += chunk;
|
|
74
|
+
let action = "";
|
|
75
|
+
try {
|
|
76
|
+
action = (JSON.parse(body || "{}").action) || "";
|
|
77
|
+
}
|
|
78
|
+
catch { /* */ }
|
|
79
|
+
const known = ["reply", "replyAll", "forward", "editAsNew", "flag", "delete"];
|
|
80
|
+
if (action && known.includes(action) && onAction) {
|
|
81
|
+
onAction({ action, accountId: a, uid: u, folderId: f });
|
|
82
|
+
res.writeHead(204).end();
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
res.writeHead(400).end("unknown action");
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
63
89
|
res.writeHead(404).end("not found");
|
|
64
90
|
}
|
|
65
91
|
catch (e) {
|
|
@@ -95,35 +121,71 @@ function addrLine(list) {
|
|
|
95
121
|
return "";
|
|
96
122
|
return list.map((x) => esc(x?.name ? `${x.name} <${x.address}>` : (x?.address || ""))).filter(Boolean).join(", ");
|
|
97
123
|
}
|
|
98
|
-
/** The outer popout page: header + sandboxed body iframe.
|
|
99
|
-
*
|
|
124
|
+
/** The outer popout page: header + action toolbar + sandboxed body iframe.
|
|
125
|
+
* Toolbar buttons POST to /action (→ daemon → main window opens editable
|
|
126
|
+
* compose / flags / deletes). A small script relays the iframe's link clicks
|
|
127
|
+
* back to /open so they land in the OS browser. */
|
|
100
128
|
function renderPage(msg, a, u, f, token) {
|
|
101
129
|
const subject = esc(msg?.subject || "(no subject)");
|
|
102
130
|
const from = esc(msg?.from?.name ? `${msg.from.name} <${msg.from.address}>` : (msg?.from?.address || ""));
|
|
103
131
|
const to = addrLine(msg?.to);
|
|
104
132
|
const date = msg?.date ? esc(new Date(msg.date).toLocaleString()) : "";
|
|
105
133
|
const q = `a=${encodeURIComponent(a)}&u=${u}&f=${f ?? ""}&t=${encodeURIComponent(token)}`;
|
|
134
|
+
// Same glyph set as the in-window popout toolbar (message-viewer.ts) so
|
|
135
|
+
// the two surfaces read as the same feature.
|
|
136
|
+
const btn = (action, glyph, title) => `<button class="act" data-action="${action}" title="${esc(title)}">${glyph}</button>`;
|
|
106
137
|
return `<!doctype html><html><head><meta charset="utf-8"><title>${subject}</title>
|
|
107
138
|
<style>
|
|
108
139
|
html,body{margin:0;height:100%;font-family:system-ui,Segoe UI,sans-serif;color:#111;background:#fff;}
|
|
109
140
|
body{display:flex;flex-direction:column;}
|
|
110
|
-
.hdr{padding:10px 14px;border-bottom:1px solid #ddd;flex:0 0 auto;}
|
|
141
|
+
.hdr{padding:10px 14px 6px;border-bottom:1px solid #ddd;flex:0 0 auto;}
|
|
111
142
|
.subj{font-size:1.15em;font-weight:600;margin-bottom:4px;}
|
|
112
143
|
.meta{font-size:0.85em;color:#555;line-height:1.4;}
|
|
144
|
+
.bar{display:flex;gap:6px;padding:6px 14px 8px;align-items:center;}
|
|
145
|
+
.act{font-size:1em;line-height:1;padding:5px 10px;border:1px solid #ccc;border-radius:4px;background:#f6f6f6;cursor:pointer;}
|
|
146
|
+
.act:hover{background:#e9e9e9;}
|
|
147
|
+
.note{font-size:0.78em;color:#888;margin-left:auto;}
|
|
113
148
|
iframe{flex:1 1 auto;border:none;width:100%;}
|
|
114
149
|
</style></head><body>
|
|
115
150
|
<div class="hdr">
|
|
116
151
|
<div class="subj">${subject}</div>
|
|
117
152
|
<div class="meta">${from ? "From: " + from + "<br>" : ""}${to ? "To: " + to + "<br>" : ""}${date}</div>
|
|
153
|
+
<div class="bar">
|
|
154
|
+
${btn("reply", "↩", "Reply")}
|
|
155
|
+
${btn("replyAll", "↩↩", "Reply All")}
|
|
156
|
+
${btn("forward", "→", "Forward")}
|
|
157
|
+
${btn("editAsNew", "⧉", "Edit as new message")}
|
|
158
|
+
${btn("flag", "★", "Flag")}
|
|
159
|
+
${btn("delete", "🗑", "Delete")}
|
|
160
|
+
<span class="note" id="note"></span>
|
|
161
|
+
</div>
|
|
118
162
|
</div>
|
|
119
163
|
<iframe src="/body?${q}" sandbox="allow-scripts allow-same-origin"></iframe>
|
|
120
164
|
<script>
|
|
121
165
|
var T=${JSON.stringify(token)};
|
|
166
|
+
var Q=${JSON.stringify(q)};
|
|
122
167
|
window.addEventListener("message", function(e){
|
|
123
168
|
if(e.data && e.data.type==="popout-link" && e.data.url){
|
|
124
169
|
fetch("/open?t="+encodeURIComponent(T),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({url:e.data.url})});
|
|
125
170
|
}
|
|
126
171
|
});
|
|
172
|
+
document.querySelectorAll(".act").forEach(function(b){
|
|
173
|
+
b.addEventListener("click", function(){
|
|
174
|
+
var action=b.getAttribute("data-action");
|
|
175
|
+
fetch("/action?"+Q,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({action:action})})
|
|
176
|
+
.then(function(r){
|
|
177
|
+
if(!r.ok){document.getElementById("note").textContent="action failed";return;}
|
|
178
|
+
if(action==="reply"||action==="replyAll"||action==="forward"||action==="editAsNew"){
|
|
179
|
+
document.getElementById("note").textContent="compose opened in main window";
|
|
180
|
+
}else if(action==="delete"){
|
|
181
|
+
// Daemon closes the msger window; browser-opened windows can
|
|
182
|
+
// close themselves (script-opened → close() allowed).
|
|
183
|
+
try{window.close();}catch(_){}
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
.catch(function(){document.getElementById("note").textContent="action failed";});
|
|
187
|
+
});
|
|
188
|
+
});
|
|
127
189
|
</script>
|
|
128
190
|
</body></html>`;
|
|
129
191
|
}
|
package/bin/popout-server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"popout-server.js","sourceRoot":"","sources":["popout-server.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"popout-server.js","sourceRoot":"","sources":["popout-server.ts"],"names":[],"mappings":"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"}
|
package/bin/popout-server.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Popout server — a tiny loopback HTTP server that renders a single message in
|
|
3
|
-
* its own OS window
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* its own OS window. The daemon points a native msger WebView window at it
|
|
4
|
+
* (svc.popoutWindow → showMessageBoxEx({url}) in bin/mailx.ts); a plain browser
|
|
5
|
+
* can load the same URL, which is the future "browser option". msger's custom
|
|
6
|
+
* protocol doesn't propagate to child windows, so a separate window can't use
|
|
7
|
+
* the normal IPC client; this serves a self-contained view instead.
|
|
7
8
|
*
|
|
8
9
|
* Transport choice (Bob 2026-06-29): loopback TCP on an OS-assigned EPHEMERAL
|
|
9
10
|
* port (`listen(0)`) — no hardcoded port to collide, identical on every
|
|
@@ -12,8 +13,12 @@
|
|
|
12
13
|
* reachable by any local process, so every route is gated on a per-launch random
|
|
13
14
|
* token. Future: the `msgapi` channel could serve this without HTTP at all.
|
|
14
15
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
16
|
+
* Toolbar actions (Bob 2026-07-06 "when I want to do the reply I need it to be
|
|
17
|
+
* editable"): the popout page can't host compose (no IPC bridge, no Quill), so
|
|
18
|
+
* Reply / Reply All / Forward / Edit-as-new / Flag / Delete POST back to
|
|
19
|
+
* /action, which hands them to `onAction` — bin/mailx.ts forwards that as a
|
|
20
|
+
* `popoutAction` event to the MAIN window, where app.ts opens the real,
|
|
21
|
+
* editable compose overlay (same path as the in-window popout's toolbar).
|
|
17
22
|
*/
|
|
18
23
|
import http from "node:http";
|
|
19
24
|
import crypto from "node:crypto";
|
|
@@ -23,9 +28,18 @@ export interface PopoutServerInfo {
|
|
|
23
28
|
token: string;
|
|
24
29
|
}
|
|
25
30
|
|
|
31
|
+
export interface PopoutAction {
|
|
32
|
+
action: string;
|
|
33
|
+
accountId: string;
|
|
34
|
+
uid: number;
|
|
35
|
+
folderId?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
26
38
|
/** svc must expose getMessage(accountId, uid, allowRemote, folderId) and
|
|
27
|
-
* openExternal(url).
|
|
28
|
-
|
|
39
|
+
* openExternal(url). `onAction` receives toolbar actions from popout windows
|
|
40
|
+
* (reply/replyAll/forward/editAsNew/flag/delete) for routing to the main
|
|
41
|
+
* window. Returns null if the server can't bind (popout disabled). */
|
|
42
|
+
export async function startPopoutServer(svc: any, onAction?: (act: PopoutAction) => void): Promise<PopoutServerInfo | null> {
|
|
29
43
|
const token = crypto.randomBytes(18).toString("hex");
|
|
30
44
|
|
|
31
45
|
const server = http.createServer(async (req, res) => {
|
|
@@ -64,6 +78,20 @@ export async function startPopoutServer(svc: any): Promise<PopoutServerInfo | nu
|
|
|
64
78
|
res.writeHead(204).end();
|
|
65
79
|
return;
|
|
66
80
|
}
|
|
81
|
+
if (req.method === "POST" && url.pathname === "/action") {
|
|
82
|
+
let body = "";
|
|
83
|
+
for await (const chunk of req) body += chunk;
|
|
84
|
+
let action = "";
|
|
85
|
+
try { action = (JSON.parse(body || "{}").action) || ""; } catch { /* */ }
|
|
86
|
+
const known = ["reply", "replyAll", "forward", "editAsNew", "flag", "delete"];
|
|
87
|
+
if (action && known.includes(action) && onAction) {
|
|
88
|
+
onAction({ action, accountId: a, uid: u, folderId: f });
|
|
89
|
+
res.writeHead(204).end();
|
|
90
|
+
} else {
|
|
91
|
+
res.writeHead(400).end("unknown action");
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
67
95
|
res.writeHead(404).end("not found");
|
|
68
96
|
} catch (e: any) {
|
|
69
97
|
try { res.writeHead(500).end(String(e?.message || e)); } catch { /* */ }
|
|
@@ -97,35 +125,72 @@ function addrLine(list: any): string {
|
|
|
97
125
|
return list.map((x: any) => esc(x?.name ? `${x.name} <${x.address}>` : (x?.address || ""))).filter(Boolean).join(", ");
|
|
98
126
|
}
|
|
99
127
|
|
|
100
|
-
/** The outer popout page: header + sandboxed body iframe.
|
|
101
|
-
*
|
|
128
|
+
/** The outer popout page: header + action toolbar + sandboxed body iframe.
|
|
129
|
+
* Toolbar buttons POST to /action (→ daemon → main window opens editable
|
|
130
|
+
* compose / flags / deletes). A small script relays the iframe's link clicks
|
|
131
|
+
* back to /open so they land in the OS browser. */
|
|
102
132
|
function renderPage(msg: any, a: string, u: number, f: number | undefined, token: string): string {
|
|
103
133
|
const subject = esc(msg?.subject || "(no subject)");
|
|
104
134
|
const from = esc(msg?.from?.name ? `${msg.from.name} <${msg.from.address}>` : (msg?.from?.address || ""));
|
|
105
135
|
const to = addrLine(msg?.to);
|
|
106
136
|
const date = msg?.date ? esc(new Date(msg.date).toLocaleString()) : "";
|
|
107
137
|
const q = `a=${encodeURIComponent(a)}&u=${u}&f=${f ?? ""}&t=${encodeURIComponent(token)}`;
|
|
138
|
+
// Same glyph set as the in-window popout toolbar (message-viewer.ts) so
|
|
139
|
+
// the two surfaces read as the same feature.
|
|
140
|
+
const btn = (action: string, glyph: string, title: string): string =>
|
|
141
|
+
`<button class="act" data-action="${action}" title="${esc(title)}">${glyph}</button>`;
|
|
108
142
|
return `<!doctype html><html><head><meta charset="utf-8"><title>${subject}</title>
|
|
109
143
|
<style>
|
|
110
144
|
html,body{margin:0;height:100%;font-family:system-ui,Segoe UI,sans-serif;color:#111;background:#fff;}
|
|
111
145
|
body{display:flex;flex-direction:column;}
|
|
112
|
-
.hdr{padding:10px 14px;border-bottom:1px solid #ddd;flex:0 0 auto;}
|
|
146
|
+
.hdr{padding:10px 14px 6px;border-bottom:1px solid #ddd;flex:0 0 auto;}
|
|
113
147
|
.subj{font-size:1.15em;font-weight:600;margin-bottom:4px;}
|
|
114
148
|
.meta{font-size:0.85em;color:#555;line-height:1.4;}
|
|
149
|
+
.bar{display:flex;gap:6px;padding:6px 14px 8px;align-items:center;}
|
|
150
|
+
.act{font-size:1em;line-height:1;padding:5px 10px;border:1px solid #ccc;border-radius:4px;background:#f6f6f6;cursor:pointer;}
|
|
151
|
+
.act:hover{background:#e9e9e9;}
|
|
152
|
+
.note{font-size:0.78em;color:#888;margin-left:auto;}
|
|
115
153
|
iframe{flex:1 1 auto;border:none;width:100%;}
|
|
116
154
|
</style></head><body>
|
|
117
155
|
<div class="hdr">
|
|
118
156
|
<div class="subj">${subject}</div>
|
|
119
157
|
<div class="meta">${from ? "From: " + from + "<br>" : ""}${to ? "To: " + to + "<br>" : ""}${date}</div>
|
|
158
|
+
<div class="bar">
|
|
159
|
+
${btn("reply", "↩", "Reply")}
|
|
160
|
+
${btn("replyAll", "↩↩", "Reply All")}
|
|
161
|
+
${btn("forward", "→", "Forward")}
|
|
162
|
+
${btn("editAsNew", "⧉", "Edit as new message")}
|
|
163
|
+
${btn("flag", "★", "Flag")}
|
|
164
|
+
${btn("delete", "🗑", "Delete")}
|
|
165
|
+
<span class="note" id="note"></span>
|
|
166
|
+
</div>
|
|
120
167
|
</div>
|
|
121
168
|
<iframe src="/body?${q}" sandbox="allow-scripts allow-same-origin"></iframe>
|
|
122
169
|
<script>
|
|
123
170
|
var T=${JSON.stringify(token)};
|
|
171
|
+
var Q=${JSON.stringify(q)};
|
|
124
172
|
window.addEventListener("message", function(e){
|
|
125
173
|
if(e.data && e.data.type==="popout-link" && e.data.url){
|
|
126
174
|
fetch("/open?t="+encodeURIComponent(T),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({url:e.data.url})});
|
|
127
175
|
}
|
|
128
176
|
});
|
|
177
|
+
document.querySelectorAll(".act").forEach(function(b){
|
|
178
|
+
b.addEventListener("click", function(){
|
|
179
|
+
var action=b.getAttribute("data-action");
|
|
180
|
+
fetch("/action?"+Q,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({action:action})})
|
|
181
|
+
.then(function(r){
|
|
182
|
+
if(!r.ok){document.getElementById("note").textContent="action failed";return;}
|
|
183
|
+
if(action==="reply"||action==="replyAll"||action==="forward"||action==="editAsNew"){
|
|
184
|
+
document.getElementById("note").textContent="compose opened in main window";
|
|
185
|
+
}else if(action==="delete"){
|
|
186
|
+
// Daemon closes the msger window; browser-opened windows can
|
|
187
|
+
// close themselves (script-opened → close() allowed).
|
|
188
|
+
try{window.close();}catch(_){}
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
.catch(function(){document.getElementById("note").textContent="action failed";});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
129
194
|
</script>
|
|
130
195
|
</body></html>`;
|
|
131
196
|
}
|
package/client/app.bundle.js
CHANGED
|
@@ -85,6 +85,7 @@ __export(api_client_exports, {
|
|
|
85
85
|
openInTextEditor: () => openInTextEditor,
|
|
86
86
|
openInWord: () => openInWord,
|
|
87
87
|
openLocalPath: () => openLocalPath,
|
|
88
|
+
popoutWindow: () => popoutWindow,
|
|
88
89
|
readConfigHelp: () => readConfigHelp,
|
|
89
90
|
readJsoncFile: () => readJsoncFile,
|
|
90
91
|
reauthGoogleScopes: () => reauthGoogleScopes,
|
|
@@ -621,6 +622,12 @@ async function openAttachment(accountId, uid, attachmentId, folderId, filename)
|
|
|
621
622
|
async function getMessageSource(accountId, uid, folderId) {
|
|
622
623
|
return ipc().getMessageSource(accountId, uid, folderId);
|
|
623
624
|
}
|
|
625
|
+
async function popoutWindow(accountId, uid, folderId, subject) {
|
|
626
|
+
const fn = ipc().popoutWindow;
|
|
627
|
+
if (!fn)
|
|
628
|
+
return { ok: false, reason: "no popoutWindow bridge" };
|
|
629
|
+
return fn(accountId, uid, folderId, subject);
|
|
630
|
+
}
|
|
624
631
|
async function getDeviceAccounts() {
|
|
625
632
|
return ipc().getDeviceAccounts?.() ?? [];
|
|
626
633
|
}
|
|
@@ -2683,38 +2690,23 @@ function popOutCurrentMessage() {
|
|
|
2683
2690
|
function popOutToWindow() {
|
|
2684
2691
|
if (!currentMessage)
|
|
2685
2692
|
return;
|
|
2686
|
-
const p = window.__rmfPopout;
|
|
2687
|
-
if (!p?.base || !p?.token) {
|
|
2688
|
-
console.log("[popout-window] no server info (__rmfPopout missing) \u2192 overlay fallback");
|
|
2689
|
-
popOutCurrentMessage();
|
|
2690
|
-
return;
|
|
2691
|
-
}
|
|
2692
2693
|
const a = currentAccountId;
|
|
2693
2694
|
const u = currentMessage.uid;
|
|
2694
2695
|
const f = currentMessage.folderId;
|
|
2695
|
-
const
|
|
2696
|
-
|
|
2697
|
-
let w = null;
|
|
2698
|
-
try {
|
|
2699
|
-
w = window.open(url, `rmfmsg-${a}-${u}`, "width=820,height=900,resizable=yes,scrollbars=yes");
|
|
2700
|
-
} catch (e) {
|
|
2701
|
-
console.log(`[popout-window] window.open THREW: ${e?.message || e} \u2192 overlay fallback`);
|
|
2702
|
-
popOutCurrentMessage();
|
|
2703
|
-
return;
|
|
2704
|
-
}
|
|
2705
|
-
if (!w) {
|
|
2706
|
-
console.log("[popout-window] window.open returned NULL (host blocked it) \u2192 overlay fallback");
|
|
2707
|
-
popOutCurrentMessage();
|
|
2708
|
-
return;
|
|
2709
|
-
}
|
|
2710
|
-
console.log("[popout-window] window.open returned a window object; verifying it stays open\u2026");
|
|
2711
|
-
setTimeout(() => {
|
|
2696
|
+
const subject = currentMessage.subject || "";
|
|
2697
|
+
void (async () => {
|
|
2712
2698
|
try {
|
|
2713
|
-
|
|
2699
|
+
const { popoutWindow: popoutWindow2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
|
|
2700
|
+
const r = await popoutWindow2(a, u, f, subject);
|
|
2701
|
+
if (!r?.ok) {
|
|
2702
|
+
console.log(`[popout-window] daemon declined (${r?.reason || "no reason"}) \u2192 overlay fallback`);
|
|
2703
|
+
popOutCurrentMessage();
|
|
2704
|
+
}
|
|
2714
2705
|
} catch (e) {
|
|
2715
|
-
console.log(`[popout-window]
|
|
2706
|
+
console.log(`[popout-window] popoutWindow failed: ${e?.message || e} \u2192 overlay fallback`);
|
|
2707
|
+
popOutCurrentMessage();
|
|
2716
2708
|
}
|
|
2717
|
-
}
|
|
2709
|
+
})();
|
|
2718
2710
|
}
|
|
2719
2711
|
function buildPrintableDoc(m) {
|
|
2720
2712
|
if (!m)
|
|
@@ -10126,6 +10118,16 @@ onWsEvent((event) => {
|
|
|
10126
10118
|
}).catch((e) => console.error("openComposeFromMailto failed:", e?.message || e));
|
|
10127
10119
|
break;
|
|
10128
10120
|
}
|
|
10121
|
+
case "popoutAction": {
|
|
10122
|
+
const { action, accountId, uid, folderId } = event;
|
|
10123
|
+
if (!action || !accountId || !uid) break;
|
|
10124
|
+
(async () => {
|
|
10125
|
+
const { getMessage: getMessage2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
|
|
10126
|
+
const msg = await getMessage2(accountId, uid, false, folderId);
|
|
10127
|
+
document.dispatchEvent(new CustomEvent("mailx-popout-action", { detail: { action, msg, accountId } }));
|
|
10128
|
+
})().catch((e) => console.error(`[popout-window] ${action} failed: ${e?.message || e}`));
|
|
10129
|
+
break;
|
|
10130
|
+
}
|
|
10129
10131
|
}
|
|
10130
10132
|
});
|
|
10131
10133
|
subscribeStore("*", (ev) => {
|