@bobfrankston/rmfmail 1.2.229 → 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.
@@ -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;
@@ -2430,7 +2708,7 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
2430
2708
  <div class="mv-system-title">Showing the first ${mb(trunc.sentBytes)} of a ${mb(trunc.totalBytes)} message</div>
2431
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.
2432
2710
  The complete message is on disk:<br>
2433
- <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>`;
2434
2712
  bodyEl.appendChild(note);
2435
2713
  }
2436
2714
  bodyEl.appendChild(pre);
@@ -2440,7 +2718,7 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
2440
2718
  bodyEl.innerHTML = `<div class="mv-system-message mv-system-error">
2441
2719
  <div class="mv-system-tag">mailx</div>
2442
2720
  <div class="mv-system-title">Body fetch failed</div>
2443
- <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>
2444
2722
  </div>`;
2445
2723
  } else {
2446
2724
  const emlPath = msg.emlPath || "";
@@ -2449,8 +2727,8 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
2449
2727
  const headline = attCount > 0 ? "This message has attachments but no body text." : "This message has no body text \u2014 only a subject.";
2450
2728
  const crumbs = `mailx${appVer ? " " + appVer : ""} \xB7 ${accountId}/${uid}${emlPath ? " \xB7 " + emlPath : ""}`;
2451
2729
  bodyEl.innerHTML = `<div class="mv-system-message">
2452
- <div class="mv-system-body">${escapeHtml2(headline)}</div>
2453
- <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>
2454
2732
  </div>`;
2455
2733
  }
2456
2734
  }
@@ -2682,7 +2960,7 @@ function renderHeaderFromEnvelope(headerEl, env) {
2682
2960
  }
2683
2961
  }
2684
2962
  }
2685
- function escapeHtml2(s) {
2963
+ function escapeHtml3(s) {
2686
2964
  return (s || "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
2687
2965
  }
2688
2966
  function renderTextProgressively(pre, text, gen) {
@@ -4831,7 +5109,7 @@ function formatDate(epochMs) {
4831
5109
  return d.toLocaleString(void 0, dateFmtSameYear);
4832
5110
  return d.toLocaleString(void 0, dateFmt);
4833
5111
  }
4834
- function escapeHtml3(s) {
5112
+ function escapeHtml4(s) {
4835
5113
  const div = document.createElement("div");
4836
5114
  div.textContent = s;
4837
5115
  return div.innerHTML;
@@ -4984,7 +5262,7 @@ var init_message_list = __esm({
4984
5262
  }
4985
5263
  const subject = document.createElement("span");
4986
5264
  subject.className = "ml-subject";
4987
- subject.innerHTML = escapeHtml3(msg.subject);
5265
+ subject.innerHTML = escapeHtml4(msg.subject);
4988
5266
  if (threadHead && threadCount > 1 && msg.threadId) {
4989
5267
  const threadPill = document.createElement("span");
4990
5268
  threadPill.className = "ml-thread-pill";
@@ -5400,7 +5678,7 @@ var init_message_list = __esm({
5400
5678
  }
5401
5679
  }
5402
5680
  ];
5403
- showContextMenu(e.clientX, e.clientY, items);
5681
+ showContextMenu(e.clientX, e.clientY, items, { editTarget: e.target });
5404
5682
  }
5405
5683
  };
5406
5684
  onEvent((ev) => {
@@ -5464,19 +5742,19 @@ async function openOutboxView() {
5464
5742
  return `
5465
5743
  <div class="ob-row ob-pink" data-idx="${i}">
5466
5744
  <div class="ob-row-hdr">
5467
- <span class="ob-acct">${escapeHtml4(m.accountId)}</span>
5468
- <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>
5469
5747
  <span class="ob-created">${fmtDate(m.createdAt)}</span>
5470
5748
  ${claimBadge}
5471
5749
  ${m.attempts > 0 ? `<span class="ob-badge ob-retry" title="Retry attempts made so far">retry \xD7${m.attempts}</span>` : ""}
5472
5750
  </div>
5473
5751
  <div class="ob-row-meta">
5474
- <span class="ob-from">${escapeHtml4(m.from || "")}</span>
5475
- \u2192 <span class="ob-to">${escapeHtml4(m.to || "")}</span>
5476
- ${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)}` : ""}
5477
5755
  <span class="ob-size">\xB7 ${(m.sizeBytes / 1024).toFixed(1)}kB</span>
5478
5756
  </div>
5479
- <div class="ob-row-path">${escapeHtml4(m.path)}</div>
5757
+ <div class="ob-row-path">${escapeHtml5(m.path)}</div>
5480
5758
  <div class="ob-row-actions">
5481
5759
  <button type="button" class="ob-cancel">Cancel</button>
5482
5760
  </div>
@@ -5512,7 +5790,7 @@ Subject: ${m.subject}`;
5512
5790
  const items = await listQueuedOutgoing();
5513
5791
  renderList(items || []);
5514
5792
  } catch (e) {
5515
- 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>`;
5516
5794
  }
5517
5795
  };
5518
5796
  const close = () => {
@@ -5537,7 +5815,7 @@ Subject: ${m.subject}`;
5537
5815
  });
5538
5816
  await reload();
5539
5817
  }
5540
- function escapeHtml4(s) {
5818
+ function escapeHtml5(s) {
5541
5819
  return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
5542
5820
  }
5543
5821
  var isOpen2;
@@ -5576,7 +5854,7 @@ function calendarKind(id, primary) {
5576
5854
  }
5577
5855
  function calIconHtml(info) {
5578
5856
  const kind = calendarKind(info.id, info.primary);
5579
- const t = escapeHtml5(info.name);
5857
+ const t = escapeHtml6(info.name);
5580
5858
  if (kind === "personal")
5581
5859
  return `<span class="cal-ico cal-ico-dot" title="${t}"></span>`;
5582
5860
  if (kind === "usHoliday")
@@ -5587,9 +5865,9 @@ function calIconHtml(info) {
5587
5865
  return `<span class="cal-ico cal-ico-emoji" title="${t}">\u{1F382}</span>`;
5588
5866
  if (kind === "otherHoliday")
5589
5867
  return `<span class="cal-ico cal-ico-emoji" title="${t}">\u2726</span>`;
5590
- const letter = escapeHtml5((info.name.trim()[0] || "?").toUpperCase());
5868
+ const letter = escapeHtml6((info.name.trim()[0] || "?").toUpperCase());
5591
5869
  const color = info.color || "#7a7a7a";
5592
- 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>`;
5593
5871
  }
5594
5872
  function calInfoFor(calendarId) {
5595
5873
  const id = calendarId || "primary";
@@ -5638,10 +5916,10 @@ async function renderCalendarList() {
5638
5916
  host.innerHTML = "";
5639
5917
  } else {
5640
5918
  const sorted = [...list].sort((a, b) => (a.primary ? 0 : 1) - (b.primary ? 0 : 1) || a.name.localeCompare(b.name));
5641
- host.innerHTML = sorted.map((c) => `<label class="cal-side-cal-row" title="${escapeHtml5(c.name)}">
5642
- <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"}>
5643
5921
  ${calIconHtml(c)}
5644
- <span class="cal-side-cal-name">${escapeHtml5(c.name)}</span>
5922
+ <span class="cal-side-cal-name">${escapeHtml6(c.name)}</span>
5645
5923
  </label>`).join("");
5646
5924
  host.querySelectorAll(".cal-side-cal-check").forEach((cb) => {
5647
5925
  cb.addEventListener("change", async () => {
@@ -5728,7 +6006,7 @@ function formatTime(e) {
5728
6006
  return "all day";
5729
6007
  return new Date(e.start).toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit", hour12: false });
5730
6008
  }
5731
- function escapeHtml5(s) {
6009
+ function escapeHtml6(s) {
5732
6010
  return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
5733
6011
  }
5734
6012
  function renderHead() {
@@ -5745,10 +6023,10 @@ function overdueTaskRowHtml(t) {
5745
6023
  const sameYear = d.getFullYear() === (/* @__PURE__ */ new Date()).getFullYear();
5746
6024
  dueLabel = sameYear ? `${d.getMonth() + 1}/${d.getDate()}` : `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
5747
6025
  }
5748
- 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)}">
5749
6027
  <input type="checkbox" class="cal-side-overdue-check" title="Mark done">
5750
- <span class="cal-side-task-title" title="${escapeHtml5(t.title)}">${escapeHtml5(t.title)}</span>
5751
- <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>
5752
6030
  </div>`;
5753
6031
  }
5754
6032
  function renderEvents(events) {
@@ -5805,10 +6083,10 @@ function renderEvents(events) {
5805
6083
  html += `<div class="cal-side-day cal-side-day-daily">Daily</div>`;
5806
6084
  for (const e of dailyHeads) {
5807
6085
  const link = e.htmlLink || "";
5808
- 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"' : ""}>
5809
6087
  ${calIconHtml(calInfoFor(e.calendarId))}
5810
- <span class="cal-side-event-time">${escapeHtml5(formatTime(e))}</span>
5811
- <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>
5812
6090
  </div>`;
5813
6091
  }
5814
6092
  }
@@ -5826,7 +6104,7 @@ function renderEvents(events) {
5826
6104
  const d = new Date(e.start);
5827
6105
  const dayKey = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
5828
6106
  if (dayKey !== lastDayKey) {
5829
- 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>`;
5830
6108
  lastDayKey = dayKey;
5831
6109
  }
5832
6110
  const recurMark = e.recurringEventId ? `<span class="cal-side-event-recur" title="Recurring event">\u21BB</span>` : "";
@@ -5834,14 +6112,14 @@ function renderEvents(events) {
5834
6112
  const recurAttr = e.recurringEventId ? ' data-recurring="1"' : "";
5835
6113
  if (isHolidayKind || kind === "birthday") {
5836
6114
  html += `<div class="cal-side-event" data-holiday="1" data-holiday-kind="${kind}" data-id="${e.id}">
5837
- <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>
5838
6116
  </div>`;
5839
6117
  } else {
5840
6118
  const titleAttr = link ? 'title="Click to open in Google Calendar"' : "";
5841
- 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}>
5842
6120
  ${calIconHtml(info)}
5843
- <span class="cal-side-event-time">${escapeHtml5(formatTime(e))}</span>
5844
- <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>
5845
6123
  </div>`;
5846
6124
  }
5847
6125
  }
@@ -5893,7 +6171,7 @@ function renderEvents(events) {
5893
6171
  action: () => openInBrowser("https://calendar.google.com/")
5894
6172
  });
5895
6173
  if (items.length > 0)
5896
- showContextMenu(e.clientX, e.clientY, items);
6174
+ showContextMenu(e.clientX, e.clientY, items, { editTarget: e.target });
5897
6175
  });
5898
6176
  });
5899
6177
  }
@@ -5926,7 +6204,7 @@ async function renderTasks(prefetched) {
5926
6204
  const sel = selectedTaskUuids.has(t.uuid) ? " selected" : "";
5927
6205
  html += `<div class="cal-side-task${sel}" data-uuid="${t.uuid}">
5928
6206
  <input type="checkbox" ${done ? "checked" : ""} class="cal-side-task-check">
5929
- <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>
5930
6208
  ${dueHtml}
5931
6209
  <button class="cal-side-task-delete" title="Delete task" aria-label="Delete task">\xD7</button>
5932
6210
  </div>`;
@@ -6011,7 +6289,7 @@ async function refresh() {
6011
6289
  } catch (e) {
6012
6290
  const body = document.getElementById("cal-side-body");
6013
6291
  if (body)
6014
- 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>`;
6015
6293
  }
6016
6294
  renderTasks(prefetchedTasks);
6017
6295
  }
@@ -6308,14 +6586,14 @@ function initCalendarSidebar() {
6308
6586
  const host = event.feature === "tasks" ? document.getElementById("cal-side-tasks") : document.getElementById("cal-side-body");
6309
6587
  if (host && !host.querySelector(".cal-side-quota-error")) {
6310
6588
  const msg = event.message || `Google ${event.feature} quota exceeded \u2014 try again later.`;
6311
- 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>`;
6312
6590
  }
6313
6591
  } else if (event?.type === "authScopeError") {
6314
6592
  const host = event.feature === "tasks" ? document.getElementById("cal-side-tasks") : document.getElementById("cal-side-body");
6315
6593
  if (host && !host.querySelector(".cal-side-auth-error")) {
6316
6594
  const msg = event.message || "Google access needs re-consent.";
6317
6595
  host.innerHTML = `<div class="cal-side-empty cal-side-auth-error">
6318
- <div style="margin-bottom:0.6em">${escapeHtml5(msg)}</div>
6596
+ <div style="margin-bottom:0.6em">${escapeHtml6(msg)}</div>
6319
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>
6320
6598
  </div>`;
6321
6599
  const btn = host.querySelector(".cal-side-reauth-btn");
@@ -6623,7 +6901,7 @@ function retractSuppressedPopups() {
6623
6901
  }
6624
6902
  }
6625
6903
  }
6626
- function escapeHtml6(s) {
6904
+ function escapeHtml7(s) {
6627
6905
  return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
6628
6906
  }
6629
6907
  function showInWebViewPopup(opts, onRegisterClose) {
@@ -6632,11 +6910,11 @@ function showInWebViewPopup(opts, onRegisterClose) {
6632
6910
  overlay.className = "alarm-overlay";
6633
6911
  const panel = document.createElement("div");
6634
6912
  panel.className = "alarm-panel";
6635
- 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("");
6636
6914
  panel.innerHTML = `
6637
6915
  <div class="alarm-head">
6638
6916
  <span class="alarm-icon">\u23F0</span>
6639
- <span class="alarm-title">${escapeHtml6(opts.title)}</span>
6917
+ <span class="alarm-title">${escapeHtml7(opts.title)}</span>
6640
6918
  <button type="button" class="alarm-close" data-button="" aria-label="Close">&times;</button>
6641
6919
  </div>
6642
6920
  <div class="alarm-body">${opts.html}</div>
@@ -6695,7 +6973,7 @@ async function firePopupForItem(item) {
6695
6973
  const actionBtns = ["Dismiss", "Open"];
6696
6974
  if (item.kind === "calendar")
6697
6975
  actionBtns.push("Delete");
6698
- 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("");
6699
6977
  const html = `<!DOCTYPE html>
6700
6978
  <html><head><meta charset="utf-8"><style>
6701
6979
  html, body { height: 100%; }
@@ -6721,8 +6999,8 @@ async function firePopupForItem(item) {
6721
6999
  .actions button.action[data-btn="Delete"] { background: #b00; }
6722
7000
  .actions button.action[data-btn="Delete"]:hover { background: #800; }
6723
7001
  </style></head><body>
6724
- <div class="title"><span class="icon">${icon}</span>${escapeHtml6(item.title)}</div>
6725
- <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>
6726
7004
  <div class="kind">${kindLabel}</div>
6727
7005
  <div class="row">
6728
7006
  <span class="row-label">Snooze:</span>
@@ -7564,7 +7842,7 @@ function renderNode(node, container, depth) {
7564
7842
  }
7565
7843
  } });
7566
7844
  }
7567
- showContextMenu(e.clientX, e.clientY, items);
7845
+ showContextMenu(e.clientX, e.clientY, items, { editTarget: e.target });
7568
7846
  });
7569
7847
  if (node.id !== -1) {
7570
7848
  let dragExpandTimer = null;
@@ -8135,7 +8413,7 @@ async function loadFolderTree(container) {
8135
8413
  }
8136
8414
  } }
8137
8415
  ];
8138
- showContextMenu(e.clientX, e.clientY, items);
8416
+ showContextMenu(e.clientX, e.clientY, items, { editTarget: e.target });
8139
8417
  });
8140
8418
  accountEl.appendChild(header);
8141
8419
  if (accountExpanded && folders.length > 0) {
@@ -8359,6 +8637,8 @@ init_mailx_types();
8359
8637
  init_message_viewer();
8360
8638
  init_api_client();
8361
8639
  init_message_state();
8640
+ init_edit_menu();
8641
+ init_context_menu();
8362
8642
  installConsoleCapture();
8363
8643
  (function installStallWatchdog() {
8364
8644
  const EXPECTED_MS = 1e3;
@@ -8418,7 +8698,10 @@ window.__btick && window.__btick("app.ts module body executing");
8418
8698
  }
8419
8699
  }, true);
8420
8700
  document.addEventListener("contextmenu", (e) => {
8421
- 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);
8422
8705
  });
8423
8706
  })();
8424
8707
  var baseTitle = APP_NAME;
@@ -10096,11 +10379,11 @@ document.addEventListener("mailx-share-intent", ((e) => {
10096
10379
  const bcc = sp.get("bcc");
10097
10380
  if (bcc) init.bcc = bcc.split(",").map((s) => s.trim()).filter(Boolean);
10098
10381
  } catch {
10099
- init.bodyHtml = `<p>${escapeHtml7(detail.mailto)}</p>`;
10382
+ init.bodyHtml = `<p>${escapeHtml8(detail.mailto)}</p>`;
10100
10383
  }
10101
10384
  } else {
10102
10385
  if (detail.subject) init.subject = detail.subject;
10103
- 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>`;
10104
10387
  }
10105
10388
  if (Array.isArray(detail.attachments) && detail.attachments.length) {
10106
10389
  init.attachments = detail.attachments.filter((a) => a?.filename && a?.dataBase64);
@@ -12423,13 +12706,13 @@ async function openAboutDialog() {
12423
12706
  rows.push(["Window", `${window.innerWidth}\xD7${window.innerHeight}`]);
12424
12707
  body.innerHTML = `
12425
12708
  <dl class="mailx-about-dl">
12426
- ${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("")}
12427
12710
  </dl>
12428
12711
  ${(accounts || []).length ? `
12429
12712
  <div class="mailx-about-accounts">
12430
12713
  <div class="mailx-about-section">Accounts</div>
12431
12714
  <ul>
12432
- ${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("")}
12433
12716
  </ul>
12434
12717
  </div>` : ""}
12435
12718
  <div class="mailx-about-foot">${APP_NAME} \u2014 local-first mail client</div>`;
@@ -12437,7 +12720,7 @@ async function openAboutDialog() {
12437
12720
  body.textContent = `Failed to load: ${e.message}`;
12438
12721
  }
12439
12722
  }
12440
- function escapeHtml7(s) {
12723
+ function escapeHtml8(s) {
12441
12724
  return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
12442
12725
  }
12443
12726
  optThreaded?.addEventListener("change", () => {