@bobfrankston/rmfmail 1.2.102 → 1.2.105

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.
Files changed (37) hide show
  1. package/TODO.md +1 -1
  2. package/bin/mailx.js +60 -3
  3. package/bin/mailx.js.map +1 -1
  4. package/bin/mailx.ts +52 -3
  5. package/bin/popout-server.js +73 -11
  6. package/bin/popout-server.js.map +1 -1
  7. package/bin/popout-server.ts +76 -11
  8. package/client/app.bundle.js +72 -33
  9. package/client/app.bundle.js.map +3 -3
  10. package/client/app.js +50 -12
  11. package/client/app.js.map +1 -1
  12. package/client/app.ts +48 -12
  13. package/client/components/folder-tree.js +18 -0
  14. package/client/components/folder-tree.js.map +1 -1
  15. package/client/components/folder-tree.ts +17 -0
  16. package/client/components/message-list.js +24 -2
  17. package/client/components/message-list.js.map +1 -1
  18. package/client/components/message-list.ts +22 -2
  19. package/client/components/message-viewer.js +18 -36
  20. package/client/components/message-viewer.js.map +1 -1
  21. package/client/components/message-viewer.ts +21 -35
  22. package/client/compose/compose.bundle.js +7 -0
  23. package/client/compose/compose.bundle.js.map +2 -2
  24. package/client/index.html +1 -1
  25. package/client/lib/api-client.js +9 -0
  26. package/client/lib/api-client.js.map +1 -1
  27. package/client/lib/api-client.ts +9 -0
  28. package/client/lib/mailxapi.js +6 -0
  29. package/package.json +1 -1
  30. package/packages/mailx-service/index.d.ts +25 -0
  31. package/packages/mailx-service/index.d.ts.map +1 -1
  32. package/packages/mailx-service/index.js +17 -0
  33. package/packages/mailx-service/index.js.map +1 -1
  34. package/packages/mailx-service/index.ts +24 -0
  35. package/packages/mailx-service/jsonrpc.js +2 -0
  36. package/packages/mailx-service/jsonrpc.js.map +1 -1
  37. package/packages/mailx-service/jsonrpc.ts +2 -0
package/bin/mailx.ts CHANGED
@@ -1918,12 +1918,28 @@ 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
  }
@@ -2029,6 +2045,36 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
2029
2045
  // the same way the `mailx` CLI does.
2030
2046
  const __mailxJs = path.join(import.meta.dirname, "mailx.js");
2031
2047
  const __relaunchCommand = `"${process.execPath}" "${__mailxJs}"`;
2048
+ // "Open in separate window" (🗗) spawns a NATIVE msger WebView window
2049
+ // pointed at the loopback popout server — not the OS browser (Bob
2050
+ // 2026-07-06). Injected here (not in mailx-service) because it needs
2051
+ // mailx-host + the icon paths. One window per message: a second request
2052
+ // for the same message is a no-op while its window is open. Child PIDs
2053
+ // are tracked in instance.json so `rmfmail -kill` reaps popouts too.
2054
+ if (popoutInfo) {
2055
+ const pi = popoutInfo;
2056
+ svc.setPopoutWindowFn(async ({ accountId, uid, folderId, subject }) => {
2057
+ const key = popoutKey(accountId, folderId, uid);
2058
+ const existing = popoutWindows.get(key);
2059
+ if (existing && !existing.closed) return { ok: true };
2060
+ const url = `http://127.0.0.1:${pi.port}/v?a=${encodeURIComponent(accountId)}&u=${uid}&f=${folderId ?? ""}&t=${encodeURIComponent(pi.token)}`;
2061
+ const h = showMessageBoxEx({
2062
+ title: subject || "Message",
2063
+ url,
2064
+ size: { width: 820, height: 900 },
2065
+ icon: __iconPathRuntime,
2066
+ aumid: "com.frankston.rmfmail",
2067
+ escapeCloses: true,
2068
+ });
2069
+ popoutWindows.set(key, h);
2070
+ if (typeof h.pid === "number") addChildPid(h.pid);
2071
+ void h.result.catch(() => { /* window died — cleanup below */ }).finally(() => {
2072
+ popoutWindows.delete(key);
2073
+ if (typeof h.pid === "number") removeChildPid(h.pid);
2074
+ });
2075
+ return { ok: true };
2076
+ });
2077
+ }
2032
2078
  _tick("calling showService (WebView opens here)");
2033
2079
  const handle = showService({
2034
2080
  title: `rmfmail v${rootPkgVersion}`,
@@ -2048,6 +2094,9 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
2048
2094
  : undefined,
2049
2095
  escapeCloses: false,
2050
2096
  });
2097
+ // Late-bind the popout-action forwarder now that the main window's
2098
+ // service channel exists.
2099
+ sendToClient = (m: any) => handle.send(m);
2051
2100
 
2052
2101
  // Register ourselves as the live instance so subsequent `rmfmail`
2053
2102
  // 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":"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"}
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)
@@ -3117,6 +3109,7 @@ __export(message_list_exports, {
3117
3109
  clearSearchMode: () => clearSearchMode,
3118
3110
  exitMultiSelect: () => exitMultiSelect,
3119
3111
  getCurrentFocused: () => getCurrentFocused,
3112
+ getCurrentView: () => getCurrentView,
3120
3113
  getDateBasis: () => getDateBasis,
3121
3114
  getSelectedMessages: () => getSelectedMessages,
3122
3115
  initMessageList: () => initMessageList,
@@ -3954,8 +3947,20 @@ async function loadSearchResults(query, scope = "all", accountId = "", folderId
3954
3947
  body.innerHTML = `<div class="ml-empty">Search error: ${e.message}</div>`;
3955
3948
  }
3956
3949
  }
3957
- function revealMessage(accountId, folderId, uid, specialUse = "") {
3958
- loadMessages(accountId, folderId, 1, specialUse, true, uid);
3950
+ async function revealMessage(accountId, folderId, uid, specialUse = "") {
3951
+ const prevUnified = unifiedMode;
3952
+ const prevAcct = currentAccountId2, prevFolder = currentFolderId, prevSpecial = currentSpecialUse;
3953
+ await loadMessages(accountId, folderId, 1, specialUse, true, uid);
3954
+ if (getMessages2().some((m) => m.accountId === accountId && m.uid === uid))
3955
+ return true;
3956
+ if (!prevUnified && prevAcct && prevFolder)
3957
+ await loadMessages(prevAcct, prevFolder, 1, prevSpecial, true);
3958
+ else
3959
+ await loadUnifiedInbox(true);
3960
+ return false;
3961
+ }
3962
+ function getCurrentView() {
3963
+ return { unified: unifiedMode, accountId: currentAccountId2, folderId: currentFolderId, specialUse: currentSpecialUse };
3959
3964
  }
3960
3965
  async function loadMessages(accountId, folderId, page = 1, specialUse = "", autoSelect = true, focusUid) {
3961
3966
  const myGen = ++loadGen;
@@ -6571,6 +6576,18 @@ function setFolderSynced(accountId, folderId, syncedAt) {
6571
6576
  function getFolderSynced(accountId, folderId) {
6572
6577
  return folderLastSync.get(syncKey(accountId, folderId));
6573
6578
  }
6579
+ function highlightFolder(accountId, folderId) {
6580
+ const el = accountId === null || folderId === -1 ? document.querySelector(".ft-folder.ft-unified") : document.querySelector(`.ft-folder[data-account-id="${CSS.escape(accountId)}"][data-folder-id="${folderId}"]`);
6581
+ if (!el)
6582
+ return null;
6583
+ if (selectedElement)
6584
+ selectedElement.classList.remove("selected");
6585
+ el.classList.add("selected");
6586
+ selectedElement = el;
6587
+ selectedAccountId = accountId;
6588
+ selectedFolderId = folderId;
6589
+ return el.querySelector(".ft-folder-name")?.textContent || null;
6590
+ }
6574
6591
  function formatAge(ms) {
6575
6592
  const secs = Math.round(ms / 1e3);
6576
6593
  if (secs < 60)
@@ -9410,19 +9427,31 @@ var currentAccountId3 = "";
9410
9427
  var currentFolderId2 = 0;
9411
9428
  var reloadDebounceTimer = null;
9412
9429
  var lastListReloadAt = 0;
9413
- function closeSearchAndReveal() {
9430
+ async function closeSearchAndReveal() {
9414
9431
  const focused = getCurrentFocused();
9415
9432
  clearSearchMode();
9416
9433
  const body = document.getElementById("ml-body");
9417
9434
  if (body) body.querySelectorAll(".filter-hidden").forEach((r) => r.classList.remove("filter-hidden"));
9418
9435
  if (focused && focused.folderId) {
9419
- currentAccountId3 = focused.accountId;
9420
- currentFolderId2 = focused.folderId;
9421
- revealMessage(focused.accountId, focused.folderId, focused.uid);
9436
+ await revealMessage(focused.accountId, focused.folderId, focused.uid);
9422
9437
  } else {
9423
9438
  reloadCurrentFolder();
9424
9439
  }
9425
- setTitle(APP_NAME);
9440
+ const vw = getCurrentView();
9441
+ if (vw.unified) {
9442
+ highlightFolder(null, -1);
9443
+ setActiveView({ kind: "unified" }, "All Inboxes");
9444
+ setTitle(`${APP_NAME} - All Inboxes`);
9445
+ setNarrowFolderTitle("All Inboxes");
9446
+ } else {
9447
+ currentAccountId3 = vw.accountId;
9448
+ currentFolderId2 = vw.folderId;
9449
+ currentFolderSpecialUse = vw.specialUse;
9450
+ const name = highlightFolder(vw.accountId, vw.folderId) || "Mail";
9451
+ setActiveView({ kind: "folder", accountId: vw.accountId, folderId: vw.folderId, specialUse: vw.specialUse }, name);
9452
+ setTitle(`${APP_NAME} - ${name}`);
9453
+ setNarrowFolderTitle(name);
9454
+ }
9426
9455
  }
9427
9456
  searchInput?.addEventListener("input", () => {
9428
9457
  clearTimeout(searchTimeout);
@@ -10089,6 +10118,16 @@ onWsEvent((event) => {
10089
10118
  }).catch((e) => console.error("openComposeFromMailto failed:", e?.message || e));
10090
10119
  break;
10091
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
+ }
10092
10131
  }
10093
10132
  });
10094
10133
  subscribeStore("*", (ev) => {