@bobfrankston/rmfmail 1.2.95 → 1.2.97

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();
@@ -2663,16 +2695,22 @@ function popOutToWindow() {
2663
2695
  }
2664
2696
  }, 600);
2665
2697
  }
2666
- function printCurrentMessage() {
2667
- if (!currentMessage)
2668
- return;
2669
- const m = currentMessage;
2698
+ function buildPrintableDoc(m) {
2699
+ if (!m)
2700
+ return null;
2670
2701
  const fmt = (a) => a?.name ? `${a.name} <${a.address || ""}>` : a?.address || "";
2671
2702
  const esc = escapeHtmlLocal;
2672
2703
  const row = (label, value) => value ? `<div><strong>${label}:</strong> ${esc(value)}</div>` : "";
2673
2704
  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
2705
  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>`;
2706
+ 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>`;
2707
+ }
2708
+ function printCurrentMessage() {
2709
+ if (!currentMessage)
2710
+ return;
2711
+ const doc = buildPrintableDoc(currentMessage);
2712
+ if (!doc)
2713
+ return;
2676
2714
  const iframe = document.createElement("iframe");
2677
2715
  iframe.style.cssText = "position:fixed;left:-9999px;width:1px;height:1px;border:0;";
2678
2716
  iframe.srcdoc = doc;
@@ -2701,6 +2739,58 @@ function printCurrentMessage() {
2701
2739
  }, { once: true });
2702
2740
  document.body.appendChild(iframe);
2703
2741
  }
2742
+ async function saveBlobAs(suggestedName, blob) {
2743
+ const picker = window.showSaveFilePicker;
2744
+ if (typeof picker === "function") {
2745
+ let handle;
2746
+ try {
2747
+ handle = await picker({ suggestedName });
2748
+ } catch (e) {
2749
+ if (e?.name === "AbortError")
2750
+ return;
2751
+ throw e;
2752
+ }
2753
+ const writable = await handle.createWritable();
2754
+ await writable.write(blob);
2755
+ await writable.close();
2756
+ return;
2757
+ }
2758
+ const url = URL.createObjectURL(blob);
2759
+ const a = document.createElement("a");
2760
+ a.href = url;
2761
+ a.download = suggestedName;
2762
+ a.style.display = "none";
2763
+ document.body.appendChild(a);
2764
+ a.click();
2765
+ setTimeout(() => {
2766
+ a.remove();
2767
+ URL.revokeObjectURL(url);
2768
+ }, 5e3);
2769
+ }
2770
+ async function saveCurrentMessageAsEml() {
2771
+ if (!currentMessage)
2772
+ return;
2773
+ try {
2774
+ const src = await getMessageSource(currentAccountId, currentMessage.uid, currentMessage.folderId);
2775
+ const bytes = Uint8Array.from(atob(src.dataBase64), (c) => c.charCodeAt(0));
2776
+ await saveBlobAs(src.filename || "message.eml", new Blob([bytes], { type: "message/rfc822" }));
2777
+ } catch (e) {
2778
+ window.dispatchEvent(new CustomEvent("mailx-alert", { detail: { message: `Couldn't save message: ${e?.message || e}`, key: "message-save" } }));
2779
+ }
2780
+ }
2781
+ async function saveCurrentMessageAsHtml() {
2782
+ if (!currentMessage)
2783
+ return;
2784
+ const doc = buildPrintableDoc(currentMessage);
2785
+ if (!doc)
2786
+ return;
2787
+ const base = (currentMessage.subject || "message").replace(/[<>:"/\\|?*\r\n]/g, "_").slice(0, 80).trim() || "message";
2788
+ try {
2789
+ await saveBlobAs(`${base}.html`, new Blob([doc], { type: "text/html" }));
2790
+ } catch (e) {
2791
+ window.dispatchEvent(new CustomEvent("mailx-alert", { detail: { message: `Couldn't save message: ${e?.message || e}`, key: "message-save" } }));
2792
+ }
2793
+ }
2704
2794
  function spawnDesktopPopout(msg, accountId) {
2705
2795
  const wrapper = document.createElement("div");
2706
2796
  wrapper.className = "compose-overlay viewer-popout";
@@ -2817,7 +2907,7 @@ function spawnDesktopPopout(msg, accountId) {
2817
2907
  function escapeHtmlLocal(s) {
2818
2908
  return (s || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2819
2909
  }
2820
- var currentMessage, currentAccountId, showMessageGeneration, retryCount, lastEnvelope, PARSED_CACHE_LIMIT, parsedCache, sessionAllowedRemote, recentFetchErrors, ZOOM_KEY, ZOOM_MIN, ZOOM_MAX, ZOOM_STEP, previewZoom;
2910
+ var currentMessage, currentAccountId, dragOutUrls, showMessageGeneration, retryCount, lastEnvelope, PARSED_CACHE_LIMIT, parsedCache, sessionAllowedRemote, recentFetchErrors, ZOOM_KEY, ZOOM_MIN, ZOOM_MAX, ZOOM_STEP, previewZoom;
2821
2911
  var init_message_viewer = __esm({
2822
2912
  "client/components/message-viewer.js"() {
2823
2913
  "use strict";
@@ -2828,6 +2918,7 @@ var init_message_viewer = __esm({
2828
2918
  init_mailx_types();
2829
2919
  currentMessage = null;
2830
2920
  currentAccountId = "";
2921
+ dragOutUrls = [];
2831
2922
  showMessageGeneration = 0;
2832
2923
  retryCount = 0;
2833
2924
  lastEnvelope = null;
@@ -6434,6 +6525,14 @@ var refreshDebounceTimer = null;
6434
6525
  var DRAG_HOVER_EXPAND_MS = 600;
6435
6526
  var dragAutoExpanded = /* @__PURE__ */ new Set();
6436
6527
  var dragInFlight = false;
6528
+ function collapseDragExpanded() {
6529
+ if (dragAutoExpanded.size === 0)
6530
+ return false;
6531
+ for (const key of dragAutoExpanded)
6532
+ delete expandState[key];
6533
+ dragAutoExpanded.clear();
6534
+ return true;
6535
+ }
6437
6536
  var expandState = JSON.parse(localStorage.getItem("mailx-folders-expanded") || "{}");
6438
6537
  function saveExpandState() {
6439
6538
  localStorage.setItem("mailx-folders-expanded", JSON.stringify(expandState));
@@ -6812,6 +6911,14 @@ function renderNode(node, container, depth) {
6812
6911
  clearTimeout(dragExpandTimer);
6813
6912
  dragExpandTimer = null;
6814
6913
  }
6914
+ const springCollapsed = collapseDragExpanded();
6915
+ const rerenderTree = () => {
6916
+ const tc = document.getElementById("folder-tree");
6917
+ if (tc)
6918
+ loadFolderTree(tc);
6919
+ };
6920
+ if (springCollapsed)
6921
+ rerenderTree();
6815
6922
  const folderData = e.dataTransfer.getData("application/x-mailx-folder");
6816
6923
  if (folderData) {
6817
6924
  let src;
@@ -6920,11 +7027,8 @@ document.addEventListener("dragend", () => {
6920
7027
  if (!dragInFlight)
6921
7028
  return;
6922
7029
  dragInFlight = false;
6923
- if (dragAutoExpanded.size === 0)
7030
+ if (!collapseDragExpanded())
6924
7031
  return;
6925
- for (const key of dragAutoExpanded)
6926
- delete expandState[key];
6927
- dragAutoExpanded.clear();
6928
7032
  const treeContainer = document.getElementById("folder-tree");
6929
7033
  if (treeContainer)
6930
7034
  loadFolderTree(treeContainer);