@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/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 (lets the client window.open a message into
1922
- // its own OS window). Ephemeral port + per-launch token; best-effort.
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
@@ -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 (`window.open` from the client points a real browser window
4
- * at it). msger's custom protocol doesn't propagate to child windows, so a
5
- * separate window can't use the normal IPC client; this serves a self-contained
6
- * read view instead.
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
- * Cut 1 = read + links (links POST back here svc.openExternal OS browser).
16
- * Toolbar actions (reply/forward/delete/flag) are a follow-up.
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). Returns null if the server can't bind (popout disabled). */
22
- export async function startPopoutServer(svc) {
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. A small script relays
99
- * the iframe's link clicks back to /open so they land in the OS browser. */
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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"popout-server.js","sourceRoot":"","sources":["popout-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,MAAM,MAAM,aAAa,CAAC;AAOjC;kFACkF;AAClF,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,GAAQ;IAC5C,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAErD,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;QAChD,IAAI,CAAC;YACD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,kBAAkB,CAAC,CAAC;YACxD,mEAAmE;YACnE,IAAI,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,KAAK,EAAE,CAAC;gBACtC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;gBACpC,OAAO;YACX,CAAC;YACD,MAAM,CAAC,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;YAC1C,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;YAC5C,MAAM,IAAI,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACvC,MAAM,CAAC,GAAG,IAAI,IAAI,IAAI,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAEjE,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;gBAChD,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;gBACjD,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,0BAA0B,EAAE,eAAe,EAAE,UAAU,EAAE,CAAC,CAAC;gBAChG,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;gBACzC,OAAO;YACX,CAAC;YACD,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;gBACnD,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;gBACjD,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,0BAA0B,EAAE,eAAe,EAAE,UAAU,EAAE,CAAC,CAAC;gBAChG,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;gBACzB,OAAO;YACX,CAAC;YACD,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;gBACpD,IAAI,IAAI,GAAG,EAAE,CAAC;gBACd,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG;oBAAE,IAAI,IAAI,KAAK,CAAC;gBAC7C,IAAI,IAAI,GAAG,EAAE,CAAC;gBACd,IAAI,CAAC;oBAAC,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;gBAAC,CAAC;gBAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC;gBACpE,IAAI,IAAI,IAAI,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;oBACrC,MAAM,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAqB,CAAC,CAAC,CAAC;gBACpE,CAAC;gBACD,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;gBACzB,OAAO;YACX,CAAC;YACD,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACxC,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YACd,IAAI,CAAC;gBAAC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC;YAAC,CAAC;YAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC;QAC5E,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC3B,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAM,EAAE,EAAE;YAC5B,OAAO,CAAC,KAAK,CAAC,sCAAsC,CAAC,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC,CAAC;YACvE,OAAO,CAAC,IAAI,CAAC,CAAC;QAClB,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE;YAC/B,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;YAC9B,MAAM,IAAI,GAAG,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;YAC9D,IAAI,IAAI,EAAE,CAAC;gBACP,OAAO,CAAC,GAAG,CAAC,gDAAgD,IAAI,0BAA0B,CAAC,CAAC;gBAC5F,OAAO,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YAC7B,CAAC;iBAAM,CAAC;gBACJ,OAAO,CAAC,IAAI,CAAC,CAAC;YAClB,CAAC;QACL,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;AACP,CAAC;AAED,SAAS,GAAG,CAAC,CAAS;IAClB,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAChH,CAAC;AAED,SAAS,QAAQ,CAAC,IAAS;IACvB,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACpC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC3H,CAAC;AAED;6EAC6E;AAC7E,SAAS,UAAU,CAAC,GAAQ,EAAE,CAAS,EAAE,CAAS,EAAE,CAAqB,EAAE,KAAa;IACpF,MAAM,OAAO,GAAG,GAAG,CAAC,GAAG,EAAE,OAAO,IAAI,cAAc,CAAC,CAAC;IACpD,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC;IAC1G,MAAM,EAAE,GAAG,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAC7B,MAAM,IAAI,GAAG,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACvE,MAAM,CAAC,GAAG,KAAK,kBAAkB,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC;IAC1F,OAAO,2DAA2D,OAAO;;;;;;;;;;wBAUrD,OAAO;wBACP,IAAI,CAAC,CAAC,CAAC,QAAQ,GAAG,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI;;uBAE7E,CAAC;;YAEZ,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;;;;;;;eAOlB,CAAC;AAChB,CAAC;AAED;;kFAEkF;AAClF,SAAS,UAAU,CAAC,GAAQ;IACxB,MAAM,IAAI,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC,qCAAqC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC;IAC3I,OAAO;;eAEI,IAAI;;;;;;;;wBAQK,CAAC;AACzB,CAAC"}
1
+ {"version":3,"file":"popout-server.js","sourceRoot":"","sources":["popout-server.ts"],"names":[],"mappings":"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"}
@@ -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 (`window.open` from the client points a real browser window
4
- * at it). msger's custom protocol doesn't propagate to child windows, so a
5
- * separate window can't use the normal IPC client; this serves a self-contained
6
- * read view instead.
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
- * Cut 1 = read + links (links POST back here svc.openExternal OS browser).
16
- * Toolbar actions (reply/forward/delete/flag) are a follow-up.
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). Returns null if the server can't bind (popout disabled). */
28
- export async function startPopoutServer(svc: any): Promise<PopoutServerInfo | null> {
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. A small script relays
101
- * the iframe's link clicks back to /open so they land in the OS browser. */
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
  }
@@ -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 url = `${p.base}/v?a=${encodeURIComponent(a)}&u=${u}&f=${f ?? ""}&t=${encodeURIComponent(p.token)}`;
2696
- console.log(`[popout-window] opening ${url}`);
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
- console.log(`[popout-window] +600ms: window.closed=${w.closed}`);
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] +600ms: cannot read .closed (${e?.message || e})`);
2706
+ console.log(`[popout-window] popoutWindow failed: ${e?.message || e} \u2192 overlay fallback`);
2707
+ popOutCurrentMessage();
2716
2708
  }
2717
- }, 600);
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) => {