@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.
- package/client/app.bundle.js +383 -68
- package/client/app.bundle.js.map +4 -4
- package/client/app.js +16 -2
- package/client/app.js.map +1 -1
- package/client/app.ts +14 -1
- package/client/components/calendar-sidebar.js +1 -1
- package/client/components/calendar-sidebar.js.map +1 -1
- package/client/components/calendar-sidebar.ts +1 -1
- package/client/components/context-menu.js +12 -1
- package/client/components/context-menu.js.map +1 -1
- package/client/components/context-menu.ts +36 -1
- package/client/components/edit-menu.js +339 -0
- package/client/components/edit-menu.js.map +1 -0
- package/client/components/edit-menu.ts +343 -0
- package/client/components/folder-tree.js +2 -2
- package/client/components/folder-tree.js.map +1 -1
- package/client/components/folder-tree.ts +2 -2
- package/client/components/message-list.js +3 -1
- package/client/components/message-list.js.map +1 -1
- package/client/components/message-list.ts +3 -1
- package/client/components/message-viewer.js +71 -2
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +69 -2
- package/client/compose/compose.bundle.js +258 -83
- package/client/compose/compose.bundle.js.map +4 -4
- package/client/compose/compose.js +5 -28
- package/client/compose/compose.js.map +1 -1
- package/client/compose/compose.ts +4 -13
- package/client/compose/edit-commands.js +15 -89
- package/client/compose/edit-commands.js.map +1 -1
- package/client/compose/edit-commands.ts +17 -77
- package/package.json +1 -1
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-78836 → node_modules.npmglobalize-stash-38284}/.package-lock.json +0 -0
package/client/app.bundle.js
CHANGED
|
@@ -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) => ({ "&": "&", "<": "<", ">": ">" })[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))}">${
|
|
1136
|
-
<span class="ab-email">${
|
|
1137
|
-
<span class="ab-source">${
|
|
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>${
|
|
1175
|
-
${f.multiline ? `<textarea class="mailx-modal-input" rows="2" 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: ${
|
|
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
|
|
1589
|
+
function escapeHtml2(s) {
|
|
1312
1590
|
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
1313
1591
|
}
|
|
1314
1592
|
function escapeAttr(s) {
|
|
1315
|
-
return
|
|
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">${
|
|
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">${
|
|
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">${
|
|
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">${
|
|
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
|
-
|
|
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">${
|
|
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">${
|
|
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">${
|
|
2449
|
-
<div class="mv-system-body" style="color:var(--color-text-muted);font-size:0.8em;margin-top:8px">${
|
|
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
|
|
2963
|
+
function escapeHtml3(s) {
|
|
2682
2964
|
return (s || "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
|
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 =
|
|
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">${
|
|
5436
|
-
<span class="ob-subject">${
|
|
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">${
|
|
5443
|
-
\u2192 <span class="ob-to">${
|
|
5444
|
-
${m.cc ? ` \xB7 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">${
|
|
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: ${
|
|
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
|
|
5818
|
+
function escapeHtml5(s) {
|
|
5509
5819
|
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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 =
|
|
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 =
|
|
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:${
|
|
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="${
|
|
5610
|
-
<input type="checkbox" class="cal-side-cal-check" data-cal-id="${
|
|
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">${
|
|
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
|
|
6009
|
+
function escapeHtml6(s) {
|
|
5700
6010
|
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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="${
|
|
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="${
|
|
5719
|
-
<span class="cal-side-task-due overdue">${
|
|
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="${
|
|
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">${
|
|
5779
|
-
<span class="cal-side-event-title" title="${
|
|
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">${
|
|
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="${
|
|
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="${
|
|
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">${
|
|
5812
|
-
<span class="cal-side-event-title" title="${
|
|
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="${
|
|
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: ${
|
|
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">${
|
|
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">${
|
|
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
|
|
6904
|
+
function escapeHtml7(s) {
|
|
6595
6905
|
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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="${
|
|
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">${
|
|
6917
|
+
<span class="alarm-title">${escapeHtml7(opts.title)}</span>
|
|
6608
6918
|
<button type="button" class="alarm-close" data-button="" aria-label="Close">×</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="${
|
|
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>${
|
|
6693
|
-
<div class="when">${
|
|
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 (
|
|
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>${
|
|
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>${
|
|
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 :
|
|
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>${
|
|
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
|
|
12723
|
+
function escapeHtml8(s) {
|
|
12409
12724
|
return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
12410
12725
|
}
|
|
12411
12726
|
optThreaded?.addEventListener("change", () => {
|