@bobfrankston/rmfmail 1.2.228 → 1.2.230

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 (33) hide show
  1. package/client/app.bundle.js +383 -68
  2. package/client/app.bundle.js.map +4 -4
  3. package/client/app.js +16 -2
  4. package/client/app.js.map +1 -1
  5. package/client/app.ts +14 -1
  6. package/client/components/calendar-sidebar.js +1 -1
  7. package/client/components/calendar-sidebar.js.map +1 -1
  8. package/client/components/calendar-sidebar.ts +1 -1
  9. package/client/components/context-menu.js +12 -1
  10. package/client/components/context-menu.js.map +1 -1
  11. package/client/components/context-menu.ts +36 -1
  12. package/client/components/edit-menu.js +339 -0
  13. package/client/components/edit-menu.js.map +1 -0
  14. package/client/components/edit-menu.ts +343 -0
  15. package/client/components/folder-tree.js +2 -2
  16. package/client/components/folder-tree.js.map +1 -1
  17. package/client/components/folder-tree.ts +2 -2
  18. package/client/components/message-list.js +3 -1
  19. package/client/components/message-list.js.map +1 -1
  20. package/client/components/message-list.ts +3 -1
  21. package/client/components/message-viewer.js +71 -2
  22. package/client/components/message-viewer.js.map +1 -1
  23. package/client/components/message-viewer.ts +69 -2
  24. package/client/compose/compose.bundle.js +258 -83
  25. package/client/compose/compose.bundle.js.map +4 -4
  26. package/client/compose/compose.js +5 -28
  27. package/client/compose/compose.js.map +1 -1
  28. package/client/compose/compose.ts +4 -13
  29. package/client/compose/edit-commands.js +15 -89
  30. package/client/compose/edit-commands.js.map +1 -1
  31. package/client/compose/edit-commands.ts +17 -77
  32. package/package.json +1 -1
  33. /package/packages/mailx-imap/{node_modules.npmglobalize-stash-78836 → node_modules.npmglobalize-stash-38284}/.package-lock.json +0 -0
@@ -717,6 +717,276 @@ var init_api_client = __esm({
717
717
  }
718
718
  });
719
719
 
720
+ // client/components/edit-menu.js
721
+ function isTinyEngine(ne) {
722
+ return !!ne && typeof ne.execCommand === "function" && !!ne.selection;
723
+ }
724
+ function describeEditTarget(target) {
725
+ const none = { el: null, editable: false, selection: "", canRead: true, isField: false };
726
+ const node = target;
727
+ if (!node)
728
+ return { ...none, selection: pageSelection() };
729
+ const start = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
730
+ if (!start)
731
+ return { ...none, selection: pageSelection() };
732
+ const field = start.closest("input, textarea");
733
+ if (field) {
734
+ const type = (field instanceof HTMLInputElement ? field.type : "text").toLowerCase();
735
+ if (!TEXTUAL_INPUT_TYPES.has(type))
736
+ return { ...none, selection: pageSelection() };
737
+ const s = field.selectionStart ?? 0, e = field.selectionEnd ?? 0;
738
+ return {
739
+ el: field,
740
+ editable: !field.readOnly && !field.disabled,
741
+ selection: e > s ? field.value.slice(s, e) : "",
742
+ canRead: type !== "password",
743
+ isField: true
744
+ };
745
+ }
746
+ const ce = start.closest("[contenteditable]");
747
+ const editable = !!ce && ce.isContentEditable;
748
+ return { el: ce || start, editable, selection: pageSelection(), canRead: true, isField: false };
749
+ }
750
+ function pageSelection() {
751
+ try {
752
+ return window.getSelection()?.toString() || "";
753
+ } catch {
754
+ return "";
755
+ }
756
+ }
757
+ function escapeHtml(s) {
758
+ return s.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
759
+ }
760
+ async function readClipboard() {
761
+ let html = "", text = "";
762
+ if (navigator.clipboard?.read) {
763
+ for (const item of await navigator.clipboard.read()) {
764
+ if (item.types.includes("text/html"))
765
+ html = await (await item.getType("text/html")).text();
766
+ if (item.types.includes("text/plain"))
767
+ text = await (await item.getType("text/plain")).text();
768
+ }
769
+ if (!html && !text)
770
+ text = await navigator.clipboard.readText();
771
+ } else {
772
+ text = await navigator.clipboard.readText();
773
+ }
774
+ return { html, text };
775
+ }
776
+ async function writeClipboard(text, html) {
777
+ if (html && typeof ClipboardItem === "function" && navigator.clipboard?.write) {
778
+ await navigator.clipboard.write([new ClipboardItem({
779
+ "text/html": new Blob([html], { type: "text/html" }),
780
+ "text/plain": new Blob([text], { type: "text/plain" })
781
+ })]);
782
+ return;
783
+ }
784
+ await navigator.clipboard.writeText(text);
785
+ }
786
+ function expandToWord(ne) {
787
+ if (isTinyEngine(ne)) {
788
+ try {
789
+ if (ne.selection.isCollapsed() && typeof ne.selection.expand === "function") {
790
+ ne.selection.expand({ type: "word" });
791
+ }
792
+ } catch {
793
+ }
794
+ return;
795
+ }
796
+ if (ne && typeof ne.getSelection === "function" && typeof ne.setSelection === "function") {
797
+ const sel = ne.getSelection();
798
+ if (!sel || sel.length > 0)
799
+ return;
800
+ const text = ne.getText();
801
+ let a = sel.index, b = sel.index;
802
+ while (a > 0 && /\S/.test(text[a - 1]))
803
+ a--;
804
+ while (b < text.length && /\S/.test(text[b]))
805
+ b++;
806
+ if (b > a)
807
+ ne.setSelection(a, b - a);
808
+ }
809
+ }
810
+ function replaceFieldSelection(field, text) {
811
+ const s = field.selectionStart ?? field.value.length;
812
+ const e = field.selectionEnd ?? field.value.length;
813
+ field.value = field.value.slice(0, s) + text + field.value.slice(e);
814
+ field.selectionStart = field.selectionEnd = s + text.length;
815
+ field.dispatchEvent(new Event("input", { bubbles: true }));
816
+ }
817
+ async function runClipboard(ctx, id) {
818
+ if (ctx.engine)
819
+ return runEngineClipboard(ctx.engine, id);
820
+ const info = describeEditTarget(ctx.target);
821
+ if (id === "paste") {
822
+ if (!info.editable || !info.el)
823
+ throw new Error("nothing here accepts a paste");
824
+ const { html, text } = await readClipboard();
825
+ if (info.isField) {
826
+ if (!text && !html)
827
+ return;
828
+ replaceFieldSelection(info.el, text || stripHtml(html));
829
+ return;
830
+ }
831
+ const content = html || (text ? escapeHtml(text).replace(/\r?\n/g, "<br>") : "");
832
+ if (!content)
833
+ return;
834
+ insertHtmlAtSelection(info.el, content);
835
+ return;
836
+ }
837
+ if (!info.canRead)
838
+ throw new Error("this field's contents can't be copied");
839
+ if (!info.selection)
840
+ return;
841
+ if (info.isField) {
842
+ await writeClipboard(info.selection);
843
+ } else {
844
+ await writeClipboard(info.selection, selectionHtml());
845
+ }
846
+ if (id !== "cut")
847
+ return;
848
+ if (!info.editable)
849
+ throw new Error("this text is read-only \u2014 copied instead of cut");
850
+ if (info.isField) {
851
+ replaceFieldSelection(info.el, "");
852
+ return;
853
+ }
854
+ try {
855
+ window.getSelection()?.deleteFromDocument();
856
+ } catch {
857
+ }
858
+ }
859
+ async function runEngineClipboard(ne, id) {
860
+ const isTiny = isTinyEngine(ne);
861
+ if (id === "paste") {
862
+ const { html: html2, text: text2 } = await readClipboard();
863
+ const content = html2 || (text2 ? escapeHtml(text2).replace(/\r?\n/g, "<br>") : "");
864
+ if (!content)
865
+ return;
866
+ if (isTiny) {
867
+ ne.execCommand("mceInsertContent", false, content);
868
+ return;
869
+ }
870
+ if (ne?.clipboard?.dangerouslyPasteHTML) {
871
+ const sel = ne.getSelection(true);
872
+ ne.clipboard.dangerouslyPasteHTML(sel?.index ?? 0, content);
873
+ return;
874
+ }
875
+ throw new Error("editor doesn't support paste");
876
+ }
877
+ expandToWord(ne);
878
+ let html = "", text = "";
879
+ if (isTiny) {
880
+ html = ne.selection.getContent({ format: "html" });
881
+ text = ne.selection.getContent({ format: "text" });
882
+ } else {
883
+ text = pageSelection();
884
+ html = selectionHtml();
885
+ }
886
+ if (!text && !html)
887
+ return;
888
+ await writeClipboard(text, html || void 0);
889
+ if (id !== "cut")
890
+ return;
891
+ if (isTiny) {
892
+ ne.execCommand("Delete");
893
+ return;
894
+ }
895
+ if (typeof ne?.deleteText === "function") {
896
+ const sel = ne.getSelection();
897
+ if (sel?.length)
898
+ ne.deleteText(sel.index, sel.length);
899
+ }
900
+ }
901
+ function selectionHtml() {
902
+ try {
903
+ const sel = window.getSelection();
904
+ if (!sel || sel.rangeCount === 0 || sel.isCollapsed)
905
+ return "";
906
+ const div = document.createElement("div");
907
+ div.appendChild(sel.getRangeAt(0).cloneContents());
908
+ return div.innerHTML;
909
+ } catch {
910
+ return "";
911
+ }
912
+ }
913
+ function stripHtml(html) {
914
+ const div = document.createElement("div");
915
+ div.innerHTML = html;
916
+ return div.textContent || "";
917
+ }
918
+ function insertHtmlAtSelection(host, html) {
919
+ const sel = window.getSelection();
920
+ const frag = document.createRange().createContextualFragment(html);
921
+ if (!sel || sel.rangeCount === 0) {
922
+ host?.appendChild(frag);
923
+ return;
924
+ }
925
+ const range = sel.getRangeAt(0);
926
+ range.deleteContents();
927
+ const last = frag.lastChild;
928
+ range.insertNode(frag);
929
+ if (last) {
930
+ range.setStartAfter(last);
931
+ range.collapse(true);
932
+ sel.removeAllRanges();
933
+ sel.addRange(range);
934
+ }
935
+ }
936
+ function selectAll(info) {
937
+ if (info.isField && info.el) {
938
+ info.el.select();
939
+ return;
940
+ }
941
+ const host = info.el;
942
+ if (!host)
943
+ return;
944
+ const range = document.createRange();
945
+ range.selectNodeContents(host);
946
+ const sel = window.getSelection();
947
+ sel?.removeAllRanges();
948
+ sel?.addRange(range);
949
+ }
950
+ function editMenuItems(ctx, opts = {}) {
951
+ const report = opts.onError || ((m) => console.warn(`[edit-menu] ${m}`));
952
+ const info = ctx.engine ? { el: null, editable: true, selection: pageSelection(), canRead: true, isField: false } : describeEditTarget(ctx.target);
953
+ if (!ctx.engine && !info.editable && !info.selection)
954
+ return [];
955
+ const run = (id) => () => {
956
+ void runClipboard(ctx, id).catch((e) => {
957
+ const key = id === "cut" ? "Ctrl+X" : id === "copy" ? "Ctrl+C" : "Ctrl+V";
958
+ const what = id[0].toUpperCase() + id.slice(1);
959
+ report(`${what} failed: ${e?.message || e}. ${key} still works.`);
960
+ });
961
+ };
962
+ const hasSel = !!ctx.engine || !!info.selection;
963
+ const items = [
964
+ { label: "Cut", action: run("cut"), disabled: !info.editable || !hasSel || !info.canRead },
965
+ { label: "Copy", action: run("copy"), disabled: !hasSel || !info.canRead },
966
+ { label: "Paste", action: run("paste"), disabled: !info.editable }
967
+ ];
968
+ if (opts.selectAll !== false && !ctx.engine) {
969
+ items.push({ label: "Select all", action: () => selectAll(info) });
970
+ }
971
+ return items;
972
+ }
973
+ var TEXTUAL_INPUT_TYPES;
974
+ var init_edit_menu = __esm({
975
+ "client/components/edit-menu.js"() {
976
+ "use strict";
977
+ TEXTUAL_INPUT_TYPES = /* @__PURE__ */ new Set([
978
+ "text",
979
+ "search",
980
+ "email",
981
+ "url",
982
+ "tel",
983
+ "number",
984
+ "password",
985
+ ""
986
+ ]);
987
+ }
988
+ });
989
+
720
990
  // client/components/context-menu.js
721
991
  var context_menu_exports = {};
722
992
  __export(context_menu_exports, {
@@ -783,8 +1053,15 @@ function openSubmenu(parentRow, items) {
783
1053
  sub.style.top = `${Math.max(4, top)}px`;
784
1054
  activeSubmenu = sub;
785
1055
  }
786
- function showContextMenu(x, y, items) {
1056
+ function showContextMenu(x, y, items, opts = {}) {
787
1057
  closeContextMenu();
1058
+ if (opts.editTarget !== void 0 || opts.editEngine) {
1059
+ const edit = editMenuItems({ target: opts.editTarget, engine: opts.editEngine }, { onError: opts.onEditError });
1060
+ if (edit.length > 0) {
1061
+ items = items.length > 0 ? [...edit, { label: "", action: () => {
1062
+ }, separator: true }, ...items] : edit;
1063
+ }
1064
+ }
788
1065
  const menu = document.createElement("div");
789
1066
  menu.className = "ctx-menu";
790
1067
  for (const item of items) {
@@ -871,6 +1148,7 @@ var activeMenu, dismissListener, escapeListener, activeSubmenu;
871
1148
  var init_context_menu = __esm({
872
1149
  "client/components/context-menu.js"() {
873
1150
  "use strict";
1151
+ init_edit_menu();
874
1152
  activeMenu = null;
875
1153
  dismissListener = null;
876
1154
  escapeListener = null;
@@ -1132,9 +1410,9 @@ async function openAddressBook(prefillSearch) {
1132
1410
  <span class="ab-actions"></span>
1133
1411
  </div>` + items.map((c) => `
1134
1412
  <div class="ab-row" data-email="${escapeAttr(c.email)}">
1135
- <span class="ab-name" title="${escapeAttr(cardSummary(c))}">${escapeHtml(c.name || "")}${cardSummary(c) ? ' <span class="ab-hascard" aria-hidden="true">\u2022</span>' : ""}</span>
1136
- <span class="ab-email">${escapeHtml(c.email)}</span>
1137
- <span class="ab-source">${escapeHtml(c.source)}</span>
1413
+ <span class="ab-name" title="${escapeAttr(cardSummary(c))}">${escapeHtml2(c.name || "")}${cardSummary(c) ? ' <span class="ab-hascard" aria-hidden="true">\u2022</span>' : ""}</span>
1414
+ <span class="ab-email">${escapeHtml2(c.email)}</span>
1415
+ <span class="ab-source">${escapeHtml2(c.source)}</span>
1138
1416
  <span class="ab-count-cell">${c.useCount || 0}</span>
1139
1417
  <span class="ab-last">${fmtDate(c.lastUsed)}</span>
1140
1418
  <span class="ab-actions">
@@ -1171,8 +1449,8 @@ async function openAddressBook(prefillSearch) {
1171
1449
  <input type="email" class="mailx-modal-input" value="${escapeAttr(c.email)}" disabled
1172
1450
  title="The address identifies the contact \u2014 delete and re-add to change it"></label>
1173
1451
  ${CONTACT_FIELDS.map((f) => `
1174
- <label class="ab-field"><span>${escapeHtml(f.label)}</span>
1175
- ${f.multiline ? `<textarea class="mailx-modal-input" rows="2" data-field="${f.key}">${escapeHtml(c[f.key] || "")}</textarea>` : `<input type="${f.type || "text"}" class="mailx-modal-input" data-field="${f.key}"
1452
+ <label class="ab-field"><span>${escapeHtml2(f.label)}</span>
1453
+ ${f.multiline ? `<textarea class="mailx-modal-input" rows="2" data-field="${f.key}">${escapeHtml2(c[f.key] || "")}</textarea>` : `<input type="${f.type || "text"}" class="mailx-modal-input" data-field="${f.key}"
1176
1454
  value="${escapeAttr(c[f.key] || "")}"
1177
1455
  ${f.placeholder ? `placeholder="${escapeAttr(f.placeholder)}"` : ""}>`}
1178
1456
  </label>`).join("")}
@@ -1259,7 +1537,7 @@ async function openAddressBook(prefillSearch) {
1259
1537
  const r = await listContacts(searchInput2.value, 1, 200);
1260
1538
  render2(r.items, r.total);
1261
1539
  } catch (e) {
1262
- listEl.innerHTML = `<div class="ab-empty">Load failed: ${escapeHtml(e?.message || String(e))}</div>`;
1540
+ listEl.innerHTML = `<div class="ab-empty">Load failed: ${escapeHtml2(e?.message || String(e))}</div>`;
1263
1541
  }
1264
1542
  };
1265
1543
  const scheduleReload = () => {
@@ -1308,11 +1586,11 @@ async function openAddressBook(prefillSearch) {
1308
1586
  function cardSummary(c) {
1309
1587
  return CONTACT_FIELDS.map((f) => ({ label: f.label, value: (c[f.key] || "").trim() })).filter((x) => x.value).map((x) => `${x.label}: ${x.value.replace(/\s+/g, " ")}`).join("\n");
1310
1588
  }
1311
- function escapeHtml(s) {
1589
+ function escapeHtml2(s) {
1312
1590
  return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
1313
1591
  }
1314
1592
  function escapeAttr(s) {
1315
- return escapeHtml(s);
1593
+ return escapeHtml2(s);
1316
1594
  }
1317
1595
  var CONTACT_FIELDS, isOpen;
1318
1596
  var init_address_book = __esm({
@@ -1855,7 +2133,7 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
1855
2133
  }
1856
2134
  if (!cachedMsg) {
1857
2135
  const previewText = (cached.preview || "").trim();
1858
- bodyEl.innerHTML = previewText ? `<div class="mv-preview-placeholder">${escapeHtml2(previewText)}</div>` : `<div class="mv-empty">Loading body\u2026</div>`;
2136
+ bodyEl.innerHTML = previewText ? `<div class="mv-preview-placeholder">${escapeHtml3(previewText)}</div>` : `<div class="mv-empty">Loading body\u2026</div>`;
1859
2137
  }
1860
2138
  } else if (!cachedMsg) {
1861
2139
  bodyEl.innerHTML = `<div class="mv-empty">Loading body\u2026</div>`;
@@ -1868,7 +2146,7 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
1868
2146
  <div class="mv-system-tag">mailx</div>
1869
2147
  <div class="mv-system-title">Render failed for this message</div>
1870
2148
  <div class="mv-system-body">The display engine stopped while drawing this message last time, so it hasn't been drawn again automatically.
1871
- Usually this means an unusually large body.${emlHint ? `<br><code style="user-select:all;font-size:0.9em">${escapeHtml2(emlHint)}</code>` : ""}
2149
+ Usually this means an unusually large body.${emlHint ? `<br><code style="user-select:all;font-size:0.9em">${escapeHtml3(emlHint)}</code>` : ""}
1872
2150
  <br><br><button type="button" id="mv-render-anyway" class="mailx-modal-btn">Try anyway</button></div>
1873
2151
  </div>`;
1874
2152
  document.getElementById("mv-render-anyway")?.addEventListener("click", () => {
@@ -1901,7 +2179,7 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
1901
2179
  const previewText = (msg.preview || cached?.preview || "").trim();
1902
2180
  const waitStart = Date.now();
1903
2181
  const indicatorHtml = `<span class="mv-wait-elapsed" data-start="${waitStart}">(0s)</span>`;
1904
- bodyEl.innerHTML = previewText ? `<div class="mv-preview-placeholder">${escapeHtml2(previewText)}<div class="mv-tear-line" aria-label="snippet ends here, full message loading"><span>\u2702 snippet \u2014 fetching full message \u2702</span></div><div class="mv-wait-line">Fetching body from server\u2026 ${indicatorHtml}</div></div>` : `<div class="mv-empty">Fetching body from server\u2026 ${indicatorHtml}</div>`;
2182
+ bodyEl.innerHTML = previewText ? `<div class="mv-preview-placeholder">${escapeHtml3(previewText)}<div class="mv-tear-line" aria-label="snippet ends here, full message loading"><span>\u2702 snippet \u2014 fetching full message \u2702</span></div><div class="mv-wait-line">Fetching body from server\u2026 ${indicatorHtml}</div></div>` : `<div class="mv-empty">Fetching body from server\u2026 ${indicatorHtml}</div>`;
1905
2183
  const captureGen = gen;
1906
2184
  const tick = setInterval(() => {
1907
2185
  if (captureGen !== showMessageGeneration) {
@@ -1945,7 +2223,7 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
1945
2223
  bodyEl.innerHTML = `<div class="mv-system-message mv-system-error">
1946
2224
  <div class="mv-system-tag">mailx</div>
1947
2225
  <div class="mv-system-title">Body fetch failed</div>
1948
- <div class="mv-system-body">${escapeHtml2(msg.bodyError)}<br><span style="color:var(--color-text-muted);font-size:0.9em">${transient ? "Reopen this message to retry." : "The server reports this message no longer exists (deleted or moved by another client)."}</span></div>
2226
+ <div class="mv-system-body">${escapeHtml3(msg.bodyError)}<br><span style="color:var(--color-text-muted);font-size:0.9em">${transient ? "Reopen this message to retry." : "The server reports this message no longer exists (deleted or moved by another client)."}</span></div>
1949
2227
  </div>`;
1950
2228
  currentMessage = msg;
1951
2229
  currentAccountId = accountId;
@@ -2415,7 +2693,11 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
2415
2693
  } else if (msg.bodyText) {
2416
2694
  const pre = document.createElement("pre");
2417
2695
  pre.style.cssText = "padding: 1rem; white-space: pre-wrap; word-break: break-word; font-family: system-ui, sans-serif; font-size: 17.5px; line-height: 1.5; color: #1a1a2e; background: #fff; margin: 0; height: 100%; overflow: auto;";
2418
- pre.innerHTML = linkifyText(msg.bodyText);
2696
+ if (msg.bodyText.length > PROGRESSIVE_TEXT_THRESHOLD) {
2697
+ renderTextProgressively(pre, msg.bodyText, gen);
2698
+ } else {
2699
+ pre.innerHTML = linkifyText(msg.bodyText);
2700
+ }
2419
2701
  const trunc = msg.bodyTruncated;
2420
2702
  if (trunc) {
2421
2703
  const mb = (n) => `${(n / 1048576).toFixed(1)} MB`;
@@ -2426,7 +2708,7 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
2426
2708
  <div class="mv-system-title">Showing the first ${mb(trunc.sentBytes)} of a ${mb(trunc.totalBytes)} message</div>
2427
2709
  <div class="mv-system-body">This message has no MIME structure, so its entire payload is one plain-text part; the remainder is almost always the base64 of a returned attachment.
2428
2710
  The complete message is on disk:<br>
2429
- <code style="user-select:all;font-size:0.9em">${escapeHtml2(trunc.emlPath || "(path unavailable)")}</code></div>`;
2711
+ <code style="user-select:all;font-size:0.9em">${escapeHtml3(trunc.emlPath || "(path unavailable)")}</code></div>`;
2430
2712
  bodyEl.appendChild(note);
2431
2713
  }
2432
2714
  bodyEl.appendChild(pre);
@@ -2436,7 +2718,7 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
2436
2718
  bodyEl.innerHTML = `<div class="mv-system-message mv-system-error">
2437
2719
  <div class="mv-system-tag">mailx</div>
2438
2720
  <div class="mv-system-title">Body fetch failed</div>
2439
- <div class="mv-system-body">${escapeHtml2(fetchErr.error)}<br><span style="color:var(--color-text-muted);font-size:0.9em">Recorded ${Math.round((Date.now() - fetchErr.when) / 1e3)}s ago. ${fetchErr.transient ? "Will retry automatically." : "Permanent \u2014 server-side delete may have raced."}</span></div>
2721
+ <div class="mv-system-body">${escapeHtml3(fetchErr.error)}<br><span style="color:var(--color-text-muted);font-size:0.9em">Recorded ${Math.round((Date.now() - fetchErr.when) / 1e3)}s ago. ${fetchErr.transient ? "Will retry automatically." : "Permanent \u2014 server-side delete may have raced."}</span></div>
2440
2722
  </div>`;
2441
2723
  } else {
2442
2724
  const emlPath = msg.emlPath || "";
@@ -2445,8 +2727,8 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
2445
2727
  const headline = attCount > 0 ? "This message has attachments but no body text." : "This message has no body text \u2014 only a subject.";
2446
2728
  const crumbs = `mailx${appVer ? " " + appVer : ""} \xB7 ${accountId}/${uid}${emlPath ? " \xB7 " + emlPath : ""}`;
2447
2729
  bodyEl.innerHTML = `<div class="mv-system-message">
2448
- <div class="mv-system-body">${escapeHtml2(headline)}</div>
2449
- <div class="mv-system-body" style="color:var(--color-text-muted);font-size:0.8em;margin-top:8px">${escapeHtml2(crumbs)}</div>
2730
+ <div class="mv-system-body">${escapeHtml3(headline)}</div>
2731
+ <div class="mv-system-body" style="color:var(--color-text-muted);font-size:0.8em;margin-top:8px">${escapeHtml3(crumbs)}</div>
2450
2732
  </div>`;
2451
2733
  }
2452
2734
  }
@@ -2678,9 +2960,35 @@ function renderHeaderFromEnvelope(headerEl, env) {
2678
2960
  }
2679
2961
  }
2680
2962
  }
2681
- function escapeHtml2(s) {
2963
+ function escapeHtml3(s) {
2682
2964
  return (s || "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
2683
2965
  }
2966
+ function renderTextProgressively(pre, text, gen) {
2967
+ const chunks = [];
2968
+ let at = 0;
2969
+ while (at < text.length) {
2970
+ let end = Math.min(at + TEXT_CHUNK_BYTES, text.length);
2971
+ if (end < text.length) {
2972
+ const nl = text.lastIndexOf("\n", end);
2973
+ if (nl > at)
2974
+ end = nl + 1;
2975
+ }
2976
+ chunks.push(text.slice(at, end));
2977
+ at = end;
2978
+ }
2979
+ let i = 0;
2980
+ const step = () => {
2981
+ if (gen !== showMessageGeneration)
2982
+ return;
2983
+ if (i >= chunks.length)
2984
+ return;
2985
+ const span = document.createElement("span");
2986
+ span.innerHTML = linkifyText(chunks[i++]);
2987
+ pre.appendChild(span);
2988
+ requestAnimationFrame(step);
2989
+ };
2990
+ step();
2991
+ }
2684
2992
  function linkifyText(text) {
2685
2993
  const escaped = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2686
2994
  return escaped.replace(/(https?:\/\/[^\s<>"')\]]+)/g, (match) => {
@@ -3318,7 +3626,7 @@ function spawnDesktopPopout(msg, accountId) {
3318
3626
  function escapeHtmlLocal(s) {
3319
3627
  return (s || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3320
3628
  }
3321
- var currentMessage, currentAccountId, dragOutUrls, showMessageGeneration, retryCount, lastEnvelope, PARSED_CACHE_LIMIT, parsedCache, sessionAllowedRemote, recentFetchErrors, ZOOM_KEY, ZOOM_MIN, ZOOM_MAX, ZOOM_STEP, previewZoom, RENDER_MARK_KEY, RENDER_MARK_MAX_AGE_MS, poisonedMessages, renderKey;
3629
+ var currentMessage, currentAccountId, dragOutUrls, showMessageGeneration, retryCount, lastEnvelope, PARSED_CACHE_LIMIT, parsedCache, sessionAllowedRemote, recentFetchErrors, ZOOM_KEY, ZOOM_MIN, ZOOM_MAX, ZOOM_STEP, previewZoom, RENDER_MARK_KEY, RENDER_MARK_MAX_AGE_MS, poisonedMessages, renderKey, PROGRESSIVE_TEXT_THRESHOLD, TEXT_CHUNK_BYTES;
3322
3630
  var init_message_viewer = __esm({
3323
3631
  "client/components/message-viewer.js"() {
3324
3632
  "use strict";
@@ -3385,6 +3693,8 @@ var init_message_viewer = __esm({
3385
3693
  } catch {
3386
3694
  }
3387
3695
  })();
3696
+ PROGRESSIVE_TEXT_THRESHOLD = 200 * 1024;
3697
+ TEXT_CHUNK_BYTES = 64 * 1024;
3388
3698
  subscribeStore("*", (ev) => {
3389
3699
  if (ev.kind !== "bodyFetchError")
3390
3700
  return;
@@ -4799,7 +5109,7 @@ function formatDate(epochMs) {
4799
5109
  return d.toLocaleString(void 0, dateFmtSameYear);
4800
5110
  return d.toLocaleString(void 0, dateFmt);
4801
5111
  }
4802
- function escapeHtml3(s) {
5112
+ function escapeHtml4(s) {
4803
5113
  const div = document.createElement("div");
4804
5114
  div.textContent = s;
4805
5115
  return div.innerHTML;
@@ -4952,7 +5262,7 @@ var init_message_list = __esm({
4952
5262
  }
4953
5263
  const subject = document.createElement("span");
4954
5264
  subject.className = "ml-subject";
4955
- subject.innerHTML = escapeHtml3(msg.subject);
5265
+ subject.innerHTML = escapeHtml4(msg.subject);
4956
5266
  if (threadHead && threadCount > 1 && msg.threadId) {
4957
5267
  const threadPill = document.createElement("span");
4958
5268
  threadPill.className = "ml-thread-pill";
@@ -5368,7 +5678,7 @@ var init_message_list = __esm({
5368
5678
  }
5369
5679
  }
5370
5680
  ];
5371
- showContextMenu(e.clientX, e.clientY, items);
5681
+ showContextMenu(e.clientX, e.clientY, items, { editTarget: e.target });
5372
5682
  }
5373
5683
  };
5374
5684
  onEvent((ev) => {
@@ -5432,19 +5742,19 @@ async function openOutboxView() {
5432
5742
  return `
5433
5743
  <div class="ob-row ob-pink" data-idx="${i}">
5434
5744
  <div class="ob-row-hdr">
5435
- <span class="ob-acct">${escapeHtml4(m.accountId)}</span>
5436
- <span class="ob-subject">${escapeHtml4(m.subject || "(no subject)")}</span>
5745
+ <span class="ob-acct">${escapeHtml5(m.accountId)}</span>
5746
+ <span class="ob-subject">${escapeHtml5(m.subject || "(no subject)")}</span>
5437
5747
  <span class="ob-created">${fmtDate(m.createdAt)}</span>
5438
5748
  ${claimBadge}
5439
5749
  ${m.attempts > 0 ? `<span class="ob-badge ob-retry" title="Retry attempts made so far">retry \xD7${m.attempts}</span>` : ""}
5440
5750
  </div>
5441
5751
  <div class="ob-row-meta">
5442
- <span class="ob-from">${escapeHtml4(m.from || "")}</span>
5443
- \u2192 <span class="ob-to">${escapeHtml4(m.to || "")}</span>
5444
- ${m.cc ? ` \xB7 Cc: ${escapeHtml4(m.cc)}` : ""}
5752
+ <span class="ob-from">${escapeHtml5(m.from || "")}</span>
5753
+ \u2192 <span class="ob-to">${escapeHtml5(m.to || "")}</span>
5754
+ ${m.cc ? ` \xB7 Cc: ${escapeHtml5(m.cc)}` : ""}
5445
5755
  <span class="ob-size">\xB7 ${(m.sizeBytes / 1024).toFixed(1)}kB</span>
5446
5756
  </div>
5447
- <div class="ob-row-path">${escapeHtml4(m.path)}</div>
5757
+ <div class="ob-row-path">${escapeHtml5(m.path)}</div>
5448
5758
  <div class="ob-row-actions">
5449
5759
  <button type="button" class="ob-cancel">Cancel</button>
5450
5760
  </div>
@@ -5480,7 +5790,7 @@ Subject: ${m.subject}`;
5480
5790
  const items = await listQueuedOutgoing();
5481
5791
  renderList(items || []);
5482
5792
  } catch (e) {
5483
- listEl.innerHTML = `<div class="ob-empty">Load failed: ${escapeHtml4(e?.message || String(e))}</div>`;
5793
+ listEl.innerHTML = `<div class="ob-empty">Load failed: ${escapeHtml5(e?.message || String(e))}</div>`;
5484
5794
  }
5485
5795
  };
5486
5796
  const close = () => {
@@ -5505,7 +5815,7 @@ Subject: ${m.subject}`;
5505
5815
  });
5506
5816
  await reload();
5507
5817
  }
5508
- function escapeHtml4(s) {
5818
+ function escapeHtml5(s) {
5509
5819
  return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
5510
5820
  }
5511
5821
  var isOpen2;
@@ -5544,7 +5854,7 @@ function calendarKind(id, primary) {
5544
5854
  }
5545
5855
  function calIconHtml(info) {
5546
5856
  const kind = calendarKind(info.id, info.primary);
5547
- const t = escapeHtml5(info.name);
5857
+ const t = escapeHtml6(info.name);
5548
5858
  if (kind === "personal")
5549
5859
  return `<span class="cal-ico cal-ico-dot" title="${t}"></span>`;
5550
5860
  if (kind === "usHoliday")
@@ -5555,9 +5865,9 @@ function calIconHtml(info) {
5555
5865
  return `<span class="cal-ico cal-ico-emoji" title="${t}">\u{1F382}</span>`;
5556
5866
  if (kind === "otherHoliday")
5557
5867
  return `<span class="cal-ico cal-ico-emoji" title="${t}">\u2726</span>`;
5558
- const letter = escapeHtml5((info.name.trim()[0] || "?").toUpperCase());
5868
+ const letter = escapeHtml6((info.name.trim()[0] || "?").toUpperCase());
5559
5869
  const color = info.color || "#7a7a7a";
5560
- return `<span class="cal-ico cal-ico-mono" style="background:${escapeHtml5(color)}" title="${t}">${letter}</span>`;
5870
+ return `<span class="cal-ico cal-ico-mono" style="background:${escapeHtml6(color)}" title="${t}">${letter}</span>`;
5561
5871
  }
5562
5872
  function calInfoFor(calendarId) {
5563
5873
  const id = calendarId || "primary";
@@ -5606,10 +5916,10 @@ async function renderCalendarList() {
5606
5916
  host.innerHTML = "";
5607
5917
  } else {
5608
5918
  const sorted = [...list].sort((a, b) => (a.primary ? 0 : 1) - (b.primary ? 0 : 1) || a.name.localeCompare(b.name));
5609
- host.innerHTML = sorted.map((c) => `<label class="cal-side-cal-row" title="${escapeHtml5(c.name)}">
5610
- <input type="checkbox" class="cal-side-cal-check" data-cal-id="${escapeHtml5(c.id)}" ${hiddenCalendars.has(c.id) ? "" : "checked"}>
5919
+ host.innerHTML = sorted.map((c) => `<label class="cal-side-cal-row" title="${escapeHtml6(c.name)}">
5920
+ <input type="checkbox" class="cal-side-cal-check" data-cal-id="${escapeHtml6(c.id)}" ${hiddenCalendars.has(c.id) ? "" : "checked"}>
5611
5921
  ${calIconHtml(c)}
5612
- <span class="cal-side-cal-name">${escapeHtml5(c.name)}</span>
5922
+ <span class="cal-side-cal-name">${escapeHtml6(c.name)}</span>
5613
5923
  </label>`).join("");
5614
5924
  host.querySelectorAll(".cal-side-cal-check").forEach((cb) => {
5615
5925
  cb.addEventListener("change", async () => {
@@ -5696,7 +6006,7 @@ function formatTime(e) {
5696
6006
  return "all day";
5697
6007
  return new Date(e.start).toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit", hour12: false });
5698
6008
  }
5699
- function escapeHtml5(s) {
6009
+ function escapeHtml6(s) {
5700
6010
  return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
5701
6011
  }
5702
6012
  function renderHead() {
@@ -5713,10 +6023,10 @@ function overdueTaskRowHtml(t) {
5713
6023
  const sameYear = d.getFullYear() === (/* @__PURE__ */ new Date()).getFullYear();
5714
6024
  dueLabel = sameYear ? `${d.getMonth() + 1}/${d.getDate()}` : `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
5715
6025
  }
5716
- return `<div class="cal-side-task cal-side-task-overdue" data-uuid="${escapeHtml5(t.uuid)}">
6026
+ return `<div class="cal-side-task cal-side-task-overdue" data-uuid="${escapeHtml6(t.uuid)}">
5717
6027
  <input type="checkbox" class="cal-side-overdue-check" title="Mark done">
5718
- <span class="cal-side-task-title" title="${escapeHtml5(t.title)}">${escapeHtml5(t.title)}</span>
5719
- <span class="cal-side-task-due overdue">${escapeHtml5(dueLabel)}</span>
6028
+ <span class="cal-side-task-title" title="${escapeHtml6(t.title)}">${escapeHtml6(t.title)}</span>
6029
+ <span class="cal-side-task-due overdue">${escapeHtml6(dueLabel)}</span>
5720
6030
  </div>`;
5721
6031
  }
5722
6032
  function renderEvents(events) {
@@ -5773,10 +6083,10 @@ function renderEvents(events) {
5773
6083
  html += `<div class="cal-side-day cal-side-day-daily">Daily</div>`;
5774
6084
  for (const e of dailyHeads) {
5775
6085
  const link = e.htmlLink || "";
5776
- html += `<div class="cal-side-event" data-id="${e.id}" data-link="${escapeHtml5(link)}" ${link ? 'title="Click to open in Google Calendar"' : ""}>
6086
+ html += `<div class="cal-side-event" data-id="${e.id}" data-link="${escapeHtml6(link)}" ${link ? 'title="Click to open in Google Calendar"' : ""}>
5777
6087
  ${calIconHtml(calInfoFor(e.calendarId))}
5778
- <span class="cal-side-event-time">${escapeHtml5(formatTime(e))}</span>
5779
- <span class="cal-side-event-title" title="${escapeHtml5(e.title)}">${escapeHtml5(e.title)}<span class="cal-side-event-recur" title="Daily">\u21BB</span></span>
6088
+ <span class="cal-side-event-time">${escapeHtml6(formatTime(e))}</span>
6089
+ <span class="cal-side-event-title" title="${escapeHtml6(e.title)}">${escapeHtml6(e.title)}<span class="cal-side-event-recur" title="Daily">\u21BB</span></span>
5780
6090
  </div>`;
5781
6091
  }
5782
6092
  }
@@ -5794,7 +6104,7 @@ function renderEvents(events) {
5794
6104
  const d = new Date(e.start);
5795
6105
  const dayKey = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
5796
6106
  if (dayKey !== lastDayKey) {
5797
- html += `<div class="cal-side-day">${escapeHtml5(formatDayHeader(d, today, tomorrow))}</div>`;
6107
+ html += `<div class="cal-side-day">${escapeHtml6(formatDayHeader(d, today, tomorrow))}</div>`;
5798
6108
  lastDayKey = dayKey;
5799
6109
  }
5800
6110
  const recurMark = e.recurringEventId ? `<span class="cal-side-event-recur" title="Recurring event">\u21BB</span>` : "";
@@ -5802,14 +6112,14 @@ function renderEvents(events) {
5802
6112
  const recurAttr = e.recurringEventId ? ' data-recurring="1"' : "";
5803
6113
  if (isHolidayKind || kind === "birthday") {
5804
6114
  html += `<div class="cal-side-event" data-holiday="1" data-holiday-kind="${kind}" data-id="${e.id}">
5805
- <span class="cal-side-event-title cal-side-event-holiday-title" title="${escapeHtml5(e.title)}">${calIconHtml(info)} ${escapeHtml5(e.title)}</span>
6115
+ <span class="cal-side-event-title cal-side-event-holiday-title" title="${escapeHtml6(e.title)}">${calIconHtml(info)} ${escapeHtml6(e.title)}</span>
5806
6116
  </div>`;
5807
6117
  } else {
5808
6118
  const titleAttr = link ? 'title="Click to open in Google Calendar"' : "";
5809
- html += `<div class="cal-side-event" data-id="${e.id}"${recurAttr} data-link="${escapeHtml5(link)}" ${titleAttr}>
6119
+ html += `<div class="cal-side-event" data-id="${e.id}"${recurAttr} data-link="${escapeHtml6(link)}" ${titleAttr}>
5810
6120
  ${calIconHtml(info)}
5811
- <span class="cal-side-event-time">${escapeHtml5(formatTime(e))}</span>
5812
- <span class="cal-side-event-title" title="${escapeHtml5(e.title)}">${escapeHtml5(e.title)}${recurMark}</span>
6121
+ <span class="cal-side-event-time">${escapeHtml6(formatTime(e))}</span>
6122
+ <span class="cal-side-event-title" title="${escapeHtml6(e.title)}">${escapeHtml6(e.title)}${recurMark}</span>
5813
6123
  </div>`;
5814
6124
  }
5815
6125
  }
@@ -5861,7 +6171,7 @@ function renderEvents(events) {
5861
6171
  action: () => openInBrowser("https://calendar.google.com/")
5862
6172
  });
5863
6173
  if (items.length > 0)
5864
- showContextMenu(e.clientX, e.clientY, items);
6174
+ showContextMenu(e.clientX, e.clientY, items, { editTarget: e.target });
5865
6175
  });
5866
6176
  });
5867
6177
  }
@@ -5894,7 +6204,7 @@ async function renderTasks(prefetched) {
5894
6204
  const sel = selectedTaskUuids.has(t.uuid) ? " selected" : "";
5895
6205
  html += `<div class="cal-side-task${sel}" data-uuid="${t.uuid}">
5896
6206
  <input type="checkbox" ${done ? "checked" : ""} class="cal-side-task-check">
5897
- <span class="cal-side-task-title${done ? " done" : ""}" title="${escapeHtml5(t.title)}">${escapeHtml5(t.title)}</span>
6207
+ <span class="cal-side-task-title${done ? " done" : ""}" title="${escapeHtml6(t.title)}">${escapeHtml6(t.title)}</span>
5898
6208
  ${dueHtml}
5899
6209
  <button class="cal-side-task-delete" title="Delete task" aria-label="Delete task">\xD7</button>
5900
6210
  </div>`;
@@ -5979,7 +6289,7 @@ async function refresh() {
5979
6289
  } catch (e) {
5980
6290
  const body = document.getElementById("cal-side-body");
5981
6291
  if (body)
5982
- body.innerHTML = `<div class="cal-side-empty cal-side-quota-error">Couldn't load calendar: ${escapeHtml5(e?.message || String(e))}</div>`;
6292
+ body.innerHTML = `<div class="cal-side-empty cal-side-quota-error">Couldn't load calendar: ${escapeHtml6(e?.message || String(e))}</div>`;
5983
6293
  }
5984
6294
  renderTasks(prefetchedTasks);
5985
6295
  }
@@ -6276,14 +6586,14 @@ function initCalendarSidebar() {
6276
6586
  const host = event.feature === "tasks" ? document.getElementById("cal-side-tasks") : document.getElementById("cal-side-body");
6277
6587
  if (host && !host.querySelector(".cal-side-quota-error")) {
6278
6588
  const msg = event.message || `Google ${event.feature} quota exceeded \u2014 try again later.`;
6279
- host.innerHTML = `<div class="cal-side-empty cal-side-quota-error">${escapeHtml5(msg)}</div>`;
6589
+ host.innerHTML = `<div class="cal-side-empty cal-side-quota-error">${escapeHtml6(msg)}</div>`;
6280
6590
  }
6281
6591
  } else if (event?.type === "authScopeError") {
6282
6592
  const host = event.feature === "tasks" ? document.getElementById("cal-side-tasks") : document.getElementById("cal-side-body");
6283
6593
  if (host && !host.querySelector(".cal-side-auth-error")) {
6284
6594
  const msg = event.message || "Google access needs re-consent.";
6285
6595
  host.innerHTML = `<div class="cal-side-empty cal-side-auth-error">
6286
- <div style="margin-bottom:0.6em">${escapeHtml5(msg)}</div>
6596
+ <div style="margin-bottom:0.6em">${escapeHtml6(msg)}</div>
6287
6597
  <button type="button" class="cal-side-reauth-btn" style="padding:0.3em 0.8em;border-radius:4px;border:1px solid currentColor;background:transparent;color:inherit;cursor:pointer;font-size:0.9em">Re-authenticate Now</button>
6288
6598
  </div>`;
6289
6599
  const btn = host.querySelector(".cal-side-reauth-btn");
@@ -6591,7 +6901,7 @@ function retractSuppressedPopups() {
6591
6901
  }
6592
6902
  }
6593
6903
  }
6594
- function escapeHtml6(s) {
6904
+ function escapeHtml7(s) {
6595
6905
  return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
6596
6906
  }
6597
6907
  function showInWebViewPopup(opts, onRegisterClose) {
@@ -6600,11 +6910,11 @@ function showInWebViewPopup(opts, onRegisterClose) {
6600
6910
  overlay.className = "alarm-overlay";
6601
6911
  const panel = document.createElement("div");
6602
6912
  panel.className = "alarm-panel";
6603
- const buttonsHtml = opts.buttons.map((b) => `<button type="button" class="alarm-btn${b === "Open" || b === "Dismiss" ? " alarm-btn-primary" : ""}" data-button="${escapeHtml6(b)}">${escapeHtml6(b)}</button>`).join("");
6913
+ const buttonsHtml = opts.buttons.map((b) => `<button type="button" class="alarm-btn${b === "Open" || b === "Dismiss" ? " alarm-btn-primary" : ""}" data-button="${escapeHtml7(b)}">${escapeHtml7(b)}</button>`).join("");
6604
6914
  panel.innerHTML = `
6605
6915
  <div class="alarm-head">
6606
6916
  <span class="alarm-icon">\u23F0</span>
6607
- <span class="alarm-title">${escapeHtml6(opts.title)}</span>
6917
+ <span class="alarm-title">${escapeHtml7(opts.title)}</span>
6608
6918
  <button type="button" class="alarm-close" data-button="" aria-label="Close">&times;</button>
6609
6919
  </div>
6610
6920
  <div class="alarm-body">${opts.html}</div>
@@ -6663,7 +6973,7 @@ async function firePopupForItem(item) {
6663
6973
  const actionBtns = ["Dismiss", "Open"];
6664
6974
  if (item.kind === "calendar")
6665
6975
  actionBtns.push("Delete");
6666
- const actionHtml = actionBtns.map((b) => `<button type="button" class="action" data-btn="${escapeHtml6(b)}">${escapeHtml6(b)}</button>`).join("");
6976
+ const actionHtml = actionBtns.map((b) => `<button type="button" class="action" data-btn="${escapeHtml7(b)}">${escapeHtml7(b)}</button>`).join("");
6667
6977
  const html = `<!DOCTYPE html>
6668
6978
  <html><head><meta charset="utf-8"><style>
6669
6979
  html, body { height: 100%; }
@@ -6689,8 +6999,8 @@ async function firePopupForItem(item) {
6689
6999
  .actions button.action[data-btn="Delete"] { background: #b00; }
6690
7000
  .actions button.action[data-btn="Delete"]:hover { background: #800; }
6691
7001
  </style></head><body>
6692
- <div class="title"><span class="icon">${icon}</span>${escapeHtml6(item.title)}</div>
6693
- <div class="when">${escapeHtml6(formatWhen(item.whenMs))}</div>
7002
+ <div class="title"><span class="icon">${icon}</span>${escapeHtml7(item.title)}</div>
7003
+ <div class="when">${escapeHtml7(formatWhen(item.whenMs))}</div>
6694
7004
  <div class="kind">${kindLabel}</div>
6695
7005
  <div class="row">
6696
7006
  <span class="row-label">Snooze:</span>
@@ -7532,7 +7842,7 @@ function renderNode(node, container, depth) {
7532
7842
  }
7533
7843
  } });
7534
7844
  }
7535
- showContextMenu(e.clientX, e.clientY, items);
7845
+ showContextMenu(e.clientX, e.clientY, items, { editTarget: e.target });
7536
7846
  });
7537
7847
  if (node.id !== -1) {
7538
7848
  let dragExpandTimer = null;
@@ -8103,7 +8413,7 @@ async function loadFolderTree(container) {
8103
8413
  }
8104
8414
  } }
8105
8415
  ];
8106
- showContextMenu(e.clientX, e.clientY, items);
8416
+ showContextMenu(e.clientX, e.clientY, items, { editTarget: e.target });
8107
8417
  });
8108
8418
  accountEl.appendChild(header);
8109
8419
  if (accountExpanded && folders.length > 0) {
@@ -8327,6 +8637,8 @@ init_mailx_types();
8327
8637
  init_message_viewer();
8328
8638
  init_api_client();
8329
8639
  init_message_state();
8640
+ init_edit_menu();
8641
+ init_context_menu();
8330
8642
  installConsoleCapture();
8331
8643
  (function installStallWatchdog() {
8332
8644
  const EXPECTED_MS = 1e3;
@@ -8386,7 +8698,10 @@ window.__btick && window.__btick("app.ts module body executing");
8386
8698
  }
8387
8699
  }, true);
8388
8700
  document.addEventListener("contextmenu", (e) => {
8389
- if (!e.defaultPrevented) e.preventDefault();
8701
+ if (e.defaultPrevented) return;
8702
+ e.preventDefault();
8703
+ const items = editMenuItems({ target: e.target });
8704
+ if (items.length > 0) showContextMenu(e.clientX, e.clientY, items);
8390
8705
  });
8391
8706
  })();
8392
8707
  var baseTitle = APP_NAME;
@@ -10064,11 +10379,11 @@ document.addEventListener("mailx-share-intent", ((e) => {
10064
10379
  const bcc = sp.get("bcc");
10065
10380
  if (bcc) init.bcc = bcc.split(",").map((s) => s.trim()).filter(Boolean);
10066
10381
  } catch {
10067
- init.bodyHtml = `<p>${escapeHtml7(detail.mailto)}</p>`;
10382
+ init.bodyHtml = `<p>${escapeHtml8(detail.mailto)}</p>`;
10068
10383
  }
10069
10384
  } else {
10070
10385
  if (detail.subject) init.subject = detail.subject;
10071
- if (detail.text) init.bodyHtml = `<p>${escapeHtml7(String(detail.text)).replace(/\n/g, "<br>")}</p>`;
10386
+ if (detail.text) init.bodyHtml = `<p>${escapeHtml8(String(detail.text)).replace(/\n/g, "<br>")}</p>`;
10072
10387
  }
10073
10388
  if (Array.isArray(detail.attachments) && detail.attachments.length) {
10074
10389
  init.attachments = detail.attachments.filter((a) => a?.filename && a?.dataBase64);
@@ -12391,13 +12706,13 @@ async function openAboutDialog() {
12391
12706
  rows.push(["Window", `${window.innerWidth}\xD7${window.innerHeight}`]);
12392
12707
  body.innerHTML = `
12393
12708
  <dl class="mailx-about-dl">
12394
- ${rows.map(([k, val]) => `<dt>${k}</dt><dd>${k === "Version" ? val : escapeHtml7(val)}</dd>`).join("")}
12709
+ ${rows.map(([k, val]) => `<dt>${k}</dt><dd>${k === "Version" ? val : escapeHtml8(val)}</dd>`).join("")}
12395
12710
  </dl>
12396
12711
  ${(accounts || []).length ? `
12397
12712
  <div class="mailx-about-accounts">
12398
12713
  <div class="mailx-about-section">Accounts</div>
12399
12714
  <ul>
12400
- ${accounts.map((a) => `<li>${escapeHtml7(a.email || a.id)}${a.name ? ` \u2014 ${escapeHtml7(a.name)}` : ""}</li>`).join("")}
12715
+ ${accounts.map((a) => `<li>${escapeHtml8(a.email || a.id)}${a.name ? ` \u2014 ${escapeHtml8(a.name)}` : ""}</li>`).join("")}
12401
12716
  </ul>
12402
12717
  </div>` : ""}
12403
12718
  <div class="mailx-about-foot">${APP_NAME} \u2014 local-first mail client</div>`;
@@ -12405,7 +12720,7 @@ async function openAboutDialog() {
12405
12720
  body.textContent = `Failed to load: ${e.message}`;
12406
12721
  }
12407
12722
  }
12408
- function escapeHtml7(s) {
12723
+ function escapeHtml8(s) {
12409
12724
  return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
12410
12725
  }
12411
12726
  optThreaded?.addEventListener("change", () => {