@bobfrankston/rmfmail 1.2.96 → 1.2.98

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.
@@ -55,6 +55,7 @@ __export(api_client_exports, {
55
55
  getDiagnostics: () => getDiagnostics,
56
56
  getFolders: () => getFolders,
57
57
  getMessage: () => getMessage,
58
+ getMessageSource: () => getMessageSource,
58
59
  getMessages: () => getMessages,
59
60
  getOutboxStatus: () => getOutboxStatus,
60
61
  getPrimaryAccount: () => getPrimaryAccount,
@@ -617,6 +618,9 @@ async function openAttachment(accountId, uid, attachmentId, folderId, filename)
617
618
  const fn = ipc().openAttachment;
618
619
  return fn ? fn(accountId, uid, attachmentId, folderId, filename) : void 0;
619
620
  }
621
+ async function getMessageSource(accountId, uid, folderId) {
622
+ return ipc().getMessageSource(accountId, uid, folderId);
623
+ }
620
624
  async function getDeviceAccounts() {
621
625
  return ipc().getDeviceAccounts?.() ?? [];
622
626
  }
@@ -1311,7 +1315,13 @@ function showPreviewBodyMenu(absX, absY, selectedText, sourceWindow, linkUrl, li
1311
1315
  } },
1312
1316
  { label: "", action: () => {
1313
1317
  }, separator: true },
1314
- { label: "Print\u2026 (Ctrl+P)", action: () => printCurrentMessage() }
1318
+ { label: "Print\u2026 (Ctrl+P)", action: () => printCurrentMessage() },
1319
+ { label: "Save message (.eml)\u2026", action: () => {
1320
+ void saveCurrentMessageAsEml();
1321
+ } },
1322
+ { label: "Save message as HTML\u2026", action: () => {
1323
+ void saveCurrentMessageAsHtml();
1324
+ } }
1315
1325
  );
1316
1326
  showContextMenu(absX, absY, items);
1317
1327
  }
@@ -2060,6 +2070,29 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
2060
2070
  attEl.hidden = true;
2061
2071
  if (msg.attachments?.length) {
2062
2072
  attEl.hidden = false;
2073
+ for (const u of dragOutUrls) {
2074
+ try {
2075
+ URL.revokeObjectURL(u);
2076
+ } catch {
2077
+ }
2078
+ }
2079
+ dragOutUrls.length = 0;
2080
+ const dragCache = /* @__PURE__ */ new Map();
2081
+ void (async () => {
2082
+ for (const a1 of msg.attachments) {
2083
+ if ((a1.size || 0) > 50 * 1024 * 1024)
2084
+ continue;
2085
+ try {
2086
+ const data = await getAttachment(accountId, uid, a1.id, msg.folderId);
2087
+ const bytes = Uint8Array.from(atob(data.content), (c) => c.charCodeAt(0));
2088
+ const blob = new Blob([bytes], { type: data.contentType || "application/octet-stream" });
2089
+ const url = URL.createObjectURL(blob);
2090
+ dragOutUrls.push(url);
2091
+ dragCache.set(a1.id, { url, contentType: data.contentType || "application/octet-stream" });
2092
+ } catch {
2093
+ }
2094
+ }
2095
+ })();
2063
2096
  const saveAttachmentAt = async (idx) => {
2064
2097
  const a0 = msg.attachments[idx];
2065
2098
  const name = a0.filename || "attachment";
@@ -2158,21 +2191,20 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
2158
2191
  }
2159
2192
  });
2160
2193
  chip.draggable = true;
2161
- chip.addEventListener("dragstart", async (e) => {
2194
+ chip.addEventListener("dragstart", (e) => {
2162
2195
  if (!e.dataTransfer)
2163
2196
  return;
2164
- try {
2165
- const data = await getAttachment(accountId, uid, att.id, msg.folderId);
2166
- const bytes = Uint8Array.from(atob(data.content), (c) => c.charCodeAt(0));
2167
- const blob = new Blob([bytes], { type: data.contentType || "application/octet-stream" });
2168
- const url = URL.createObjectURL(blob);
2169
- const safeName = (att.filename || "attachment").replace(/[\r\n"\/\\]/g, "_");
2170
- const downloadUrl = `${data.contentType || "application/octet-stream"}:${safeName}:${url}`;
2171
- e.dataTransfer.setData("DownloadURL", downloadUrl);
2172
- e.dataTransfer.effectAllowed = "copy";
2173
- } catch (err) {
2174
- console.error(`Attachment drag-out failed: ${err.message || err}`);
2197
+ const cached2 = dragCache.get(att.id);
2198
+ if (!cached2) {
2199
+ e.preventDefault();
2200
+ const statusEl = document.getElementById("status-sync");
2201
+ if (statusEl)
2202
+ statusEl.textContent = "Attachment still loading \u2014 try dragging again in a moment.";
2203
+ return;
2175
2204
  }
2205
+ const safeName = (att.filename || "attachment").replace(/[\r\n"\/\\]/g, "_");
2206
+ e.dataTransfer.setData("DownloadURL", `${cached2.contentType}:${safeName}:${cached2.url}`);
2207
+ e.dataTransfer.effectAllowed = "copy";
2176
2208
  });
2177
2209
  chip.addEventListener("contextmenu", (e) => {
2178
2210
  e.preventDefault();
@@ -2590,7 +2622,12 @@ ${csp}
2590
2622
  var k = (e.key || "").toLowerCase();
2591
2623
  var isShortcut = e.ctrlKey && !e.altKey && !e.metaKey && (
2592
2624
  k === "r" || k === "f" || k === "n" || k === "a" || k === "d" ||
2593
- k === "z" || k === "y" || k === "k"
2625
+ k === "z" || k === "y" || k === "k" ||
2626
+ // Ctrl+P: without this suppress, WebView2's native print fires on
2627
+ // the iframe's default and prints the WHOLE APP window instead of
2628
+ // the letter (Bob 2026-07-02). The forwarded event reaches app.ts,
2629
+ // which runs printCurrentMessage (letter + header block).
2630
+ k === "p"
2594
2631
  );
2595
2632
  if (isShortcut) e.preventDefault();
2596
2633
  window.parent.postMessage({
@@ -2663,16 +2700,22 @@ function popOutToWindow() {
2663
2700
  }
2664
2701
  }, 600);
2665
2702
  }
2666
- function printCurrentMessage() {
2667
- if (!currentMessage)
2668
- return;
2669
- const m = currentMessage;
2703
+ function buildPrintableDoc(m) {
2704
+ if (!m)
2705
+ return null;
2670
2706
  const fmt = (a) => a?.name ? `${a.name} <${a.address || ""}>` : a?.address || "";
2671
2707
  const esc = escapeHtmlLocal;
2672
2708
  const row = (label, value) => value ? `<div><strong>${label}:</strong> ${esc(value)}</div>` : "";
2673
2709
  const headerHtml = `<div style="font-family:system-ui,Segoe UI,sans-serif;font-size:11pt;border-bottom:1px solid #999;padding-bottom:8px;margin-bottom:14px;line-height:1.5;">` + row("From", fmt(m.from || {})) + row("To", (m.to || []).map(fmt).join(", ")) + (m.cc?.length ? row("Cc", m.cc.map(fmt).join(", ")) : "") + row("Subject", m.subject || "") + row("Date", m.date ? new Date(m.date).toLocaleString() : "") + `</div>`;
2674
2710
  const bodyHtml = m.bodyHtml ? m.bodyHtml : `<pre style="white-space:pre-wrap;word-break:break-word;font-family:system-ui,sans-serif;">${esc(m.bodyText || "")}</pre>`;
2675
- const doc = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${esc(m.subject || "Message")}</title><style>@media print{body{margin:0;}}body{margin:16px;}img{max-width:100%;}</style></head><body>${headerHtml}${bodyHtml}</body></html>`;
2711
+ return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${esc(m.subject || "Message")}</title><style>@media print{body{margin:0;}}body{margin:16px;}img{max-width:100%;}</style></head><body>${headerHtml}${bodyHtml}</body></html>`;
2712
+ }
2713
+ function printCurrentMessage() {
2714
+ if (!currentMessage)
2715
+ return;
2716
+ const doc = buildPrintableDoc(currentMessage);
2717
+ if (!doc)
2718
+ return;
2676
2719
  const iframe = document.createElement("iframe");
2677
2720
  iframe.style.cssText = "position:fixed;left:-9999px;width:1px;height:1px;border:0;";
2678
2721
  iframe.srcdoc = doc;
@@ -2701,6 +2744,58 @@ function printCurrentMessage() {
2701
2744
  }, { once: true });
2702
2745
  document.body.appendChild(iframe);
2703
2746
  }
2747
+ async function saveBlobAs(suggestedName, blob) {
2748
+ const picker = window.showSaveFilePicker;
2749
+ if (typeof picker === "function") {
2750
+ let handle;
2751
+ try {
2752
+ handle = await picker({ suggestedName });
2753
+ } catch (e) {
2754
+ if (e?.name === "AbortError")
2755
+ return;
2756
+ throw e;
2757
+ }
2758
+ const writable = await handle.createWritable();
2759
+ await writable.write(blob);
2760
+ await writable.close();
2761
+ return;
2762
+ }
2763
+ const url = URL.createObjectURL(blob);
2764
+ const a = document.createElement("a");
2765
+ a.href = url;
2766
+ a.download = suggestedName;
2767
+ a.style.display = "none";
2768
+ document.body.appendChild(a);
2769
+ a.click();
2770
+ setTimeout(() => {
2771
+ a.remove();
2772
+ URL.revokeObjectURL(url);
2773
+ }, 5e3);
2774
+ }
2775
+ async function saveCurrentMessageAsEml() {
2776
+ if (!currentMessage)
2777
+ return;
2778
+ try {
2779
+ const src = await getMessageSource(currentAccountId, currentMessage.uid, currentMessage.folderId);
2780
+ const bytes = Uint8Array.from(atob(src.dataBase64), (c) => c.charCodeAt(0));
2781
+ await saveBlobAs(src.filename || "message.eml", new Blob([bytes], { type: "message/rfc822" }));
2782
+ } catch (e) {
2783
+ window.dispatchEvent(new CustomEvent("mailx-alert", { detail: { message: `Couldn't save message: ${e?.message || e}`, key: "message-save" } }));
2784
+ }
2785
+ }
2786
+ async function saveCurrentMessageAsHtml() {
2787
+ if (!currentMessage)
2788
+ return;
2789
+ const doc = buildPrintableDoc(currentMessage);
2790
+ if (!doc)
2791
+ return;
2792
+ const base = (currentMessage.subject || "message").replace(/[<>:"/\\|?*\r\n]/g, "_").slice(0, 80).trim() || "message";
2793
+ try {
2794
+ await saveBlobAs(`${base}.html`, new Blob([doc], { type: "text/html" }));
2795
+ } catch (e) {
2796
+ window.dispatchEvent(new CustomEvent("mailx-alert", { detail: { message: `Couldn't save message: ${e?.message || e}`, key: "message-save" } }));
2797
+ }
2798
+ }
2704
2799
  function spawnDesktopPopout(msg, accountId) {
2705
2800
  const wrapper = document.createElement("div");
2706
2801
  wrapper.className = "compose-overlay viewer-popout";
@@ -2817,7 +2912,7 @@ function spawnDesktopPopout(msg, accountId) {
2817
2912
  function escapeHtmlLocal(s) {
2818
2913
  return (s || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2819
2914
  }
2820
- var currentMessage, currentAccountId, showMessageGeneration, retryCount, lastEnvelope, PARSED_CACHE_LIMIT, parsedCache, sessionAllowedRemote, recentFetchErrors, ZOOM_KEY, ZOOM_MIN, ZOOM_MAX, ZOOM_STEP, previewZoom;
2915
+ var currentMessage, currentAccountId, dragOutUrls, showMessageGeneration, retryCount, lastEnvelope, PARSED_CACHE_LIMIT, parsedCache, sessionAllowedRemote, recentFetchErrors, ZOOM_KEY, ZOOM_MIN, ZOOM_MAX, ZOOM_STEP, previewZoom;
2821
2916
  var init_message_viewer = __esm({
2822
2917
  "client/components/message-viewer.js"() {
2823
2918
  "use strict";
@@ -2828,6 +2923,7 @@ var init_message_viewer = __esm({
2828
2923
  init_mailx_types();
2829
2924
  currentMessage = null;
2830
2925
  currentAccountId = "";
2926
+ dragOutUrls = [];
2831
2927
  showMessageGeneration = 0;
2832
2928
  retryCount = 0;
2833
2929
  lastEnvelope = null;