@bobfrankston/rmfmail 1.2.3 → 1.2.4

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.
@@ -2844,6 +2844,51 @@ export class MailxService {
2844
2844
  }
2845
2845
  return { ok: true, path: target };
2846
2846
  }
2847
+ /** Open an http/https/mailto URL in the OS default browser/handler from the
2848
+ * Node process. The UI used to rely on `window.open(url, "_blank")`, but
2849
+ * inside msger's WebView2 that opens a local in-app window (or no-ops)
2850
+ * rather than the system browser — so clicking a link in a message, or an
2851
+ * unsubscribe "click here", "stayed local" (Bob 2026-06-13, "old bug
2852
+ * back"). `mailxapi.openExternal` now routes here.
2853
+ *
2854
+ * Security: the URL comes from untrusted email. We (1) parse it and allow
2855
+ * ONLY http/https/mailto — no file:, javascript:, data:, etc. — and (2)
2856
+ * pass it as a single spawn ARG with no shell, and on Windows use rundll32
2857
+ * (not `cmd /c start`, which re-parses `&`/`^` in query strings and is
2858
+ * injection-prone). Same cross-platform spawn pattern as openAttachment. */
2859
+ async openExternal(url) {
2860
+ let u;
2861
+ try {
2862
+ u = new URL(String(url));
2863
+ }
2864
+ catch {
2865
+ return { ok: false, reason: "unparseable URL" };
2866
+ }
2867
+ if (!["http:", "https:", "mailto:"].includes(u.protocol)) {
2868
+ console.error(` [openExternal] refused non-web scheme: ${u.protocol}`);
2869
+ return { ok: false, reason: `unsupported scheme ${u.protocol}` };
2870
+ }
2871
+ const target = u.href;
2872
+ try {
2873
+ const { spawn } = await import("node:child_process");
2874
+ if (process.platform === "win32") {
2875
+ // rundll32 receives the URL as a direct argv — no cmd, so query
2876
+ // strings with & can't break out into a second command.
2877
+ spawn("rundll32", ["url.dll,FileProtocolHandler", target], { detached: true, stdio: "ignore", windowsHide: true }).unref();
2878
+ }
2879
+ else if (process.platform === "darwin") {
2880
+ spawn("open", [target], { detached: true, stdio: "ignore" }).unref();
2881
+ }
2882
+ else {
2883
+ spawn("xdg-open", [target], { detached: true, stdio: "ignore" }).unref();
2884
+ }
2885
+ return { ok: true };
2886
+ }
2887
+ catch (e) {
2888
+ console.error(` [openExternal] spawn failed: ${e?.message || e}`);
2889
+ return { ok: false, reason: e?.message || String(e) };
2890
+ }
2891
+ }
2847
2892
  // ── Drafts ──
2848
2893
  async saveDraft(accountId, subject, bodyHtml, bodyText, to, cc, previousDraftUid, draftId) {
2849
2894
  // Local-first: commit the draft to the local filesystem synchronously