@bobfrankston/rmfmail 1.2.87 → 1.2.95

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 (81) hide show
  1. package/chk-search-tmp.mjs +14 -0
  2. package/cleanup-sent-tmp.mjs +43 -0
  3. package/client/android-bootstrap.bundle.js +65 -15
  4. package/client/android-bootstrap.bundle.js.map +2 -2
  5. package/client/app.bundle.js +361 -282
  6. package/client/app.bundle.js.map +4 -4
  7. package/client/app.js +23 -1
  8. package/client/app.js.map +1 -1
  9. package/client/app.ts +20 -1
  10. package/client/components/address-book.js +5 -1
  11. package/client/components/address-book.js.map +1 -1
  12. package/client/components/address-book.ts +5 -1
  13. package/client/components/message-list.js +164 -54
  14. package/client/components/message-list.js.map +1 -1
  15. package/client/components/message-list.ts +159 -51
  16. package/client/components/message-viewer.js +7 -0
  17. package/client/components/message-viewer.js.map +1 -1
  18. package/client/components/message-viewer.ts +7 -0
  19. package/client/compose/compose.bundle.js +4 -4
  20. package/client/compose/compose.bundle.js.map +2 -2
  21. package/client/index.html +4 -0
  22. package/client/lib/api-client.js +4 -4
  23. package/client/lib/api-client.js.map +1 -1
  24. package/client/lib/api-client.ts +4 -4
  25. package/client/lib/mailxapi.js +4 -4
  26. package/client/styles/components.css +20 -0
  27. package/package.json +1 -1
  28. package/packages/mailx-imap/index.d.ts +17 -0
  29. package/packages/mailx-imap/index.d.ts.map +1 -1
  30. package/packages/mailx-imap/index.js +86 -4
  31. package/packages/mailx-imap/index.js.map +1 -1
  32. package/packages/mailx-imap/index.ts +85 -4
  33. package/packages/mailx-imap/package-lock.json +2 -2
  34. package/packages/mailx-imap/package.json +1 -1
  35. package/packages/mailx-service/index.d.ts +2 -2
  36. package/packages/mailx-service/index.d.ts.map +1 -1
  37. package/packages/mailx-service/index.js +6 -4
  38. package/packages/mailx-service/index.js.map +1 -1
  39. package/packages/mailx-service/index.ts +7 -5
  40. package/packages/mailx-service/jsonrpc.js +2 -2
  41. package/packages/mailx-service/jsonrpc.js.map +1 -1
  42. package/packages/mailx-service/jsonrpc.ts +2 -2
  43. package/packages/mailx-service/sync-queue.d.ts +3 -0
  44. package/packages/mailx-service/sync-queue.d.ts.map +1 -1
  45. package/packages/mailx-service/sync-queue.js +30 -11
  46. package/packages/mailx-service/sync-queue.js.map +1 -1
  47. package/packages/mailx-service/sync-queue.ts +34 -9
  48. package/packages/mailx-service/sync-worker.js +51 -0
  49. package/packages/mailx-service/sync-worker.js.map +1 -1
  50. package/packages/mailx-service/sync-worker.ts +45 -0
  51. package/packages/mailx-settings/package.json +1 -1
  52. package/packages/mailx-store/db.d.ts +10 -1
  53. package/packages/mailx-store/db.d.ts.map +1 -1
  54. package/packages/mailx-store/db.js +68 -7
  55. package/packages/mailx-store/db.js.map +1 -1
  56. package/packages/mailx-store/db.ts +70 -8
  57. package/packages/mailx-store/package.json +1 -1
  58. package/packages/mailx-store/store.d.ts +1 -1
  59. package/packages/mailx-store/store.d.ts.map +1 -1
  60. package/packages/mailx-store/store.js +2 -2
  61. package/packages/mailx-store/store.js.map +1 -1
  62. package/packages/mailx-store/store.ts +2 -2
  63. package/packages/mailx-store-web/imap-web-provider.d.ts +7 -0
  64. package/packages/mailx-store-web/imap-web-provider.d.ts.map +1 -1
  65. package/packages/mailx-store-web/imap-web-provider.js +12 -0
  66. package/packages/mailx-store-web/imap-web-provider.js.map +1 -1
  67. package/packages/mailx-store-web/imap-web-provider.ts +15 -0
  68. package/packages/mailx-store-web/package.json +1 -1
  69. package/packages/mailx-store-web/sync-manager.d.ts.map +1 -1
  70. package/packages/mailx-store-web/sync-manager.js +12 -0
  71. package/packages/mailx-store-web/sync-manager.js.map +1 -1
  72. package/packages/mailx-store-web/sync-manager.ts +12 -0
  73. package/packages/mailx-store-web/web-service.d.ts.map +1 -1
  74. package/packages/mailx-store-web/web-service.js +53 -19
  75. package/packages/mailx-store-web/web-service.js.map +1 -1
  76. package/packages/mailx-store-web/web-service.ts +60 -20
  77. package/packages/mailx-types/index.d.ts +5 -1
  78. package/packages/mailx-types/index.d.ts.map +1 -1
  79. package/packages/mailx-types/index.js.map +1 -1
  80. package/packages/mailx-types/index.ts +5 -1
  81. package/packages/mailx-types/package.json +1 -1
@@ -193,13 +193,13 @@ function getAccounts() {
193
193
  function getFolders(accountId) {
194
194
  return ipc().getFolders(accountId);
195
195
  }
196
- function getMessages(accountId, folderId, page = 1, pageSize = 50, flaggedOnly = false, sort, sortDir) {
196
+ function getMessages(accountId, folderId, page = 1, pageSize = 50, flaggedOnly = false, sort, sortDir, dateBasis2) {
197
197
  abortMessageListRequests();
198
- return ipc().getMessages(accountId, folderId, page, pageSize, sort, sortDir, void 0, flaggedOnly);
198
+ return ipc().getMessages(accountId, folderId, page, pageSize, sort, sortDir, void 0, flaggedOnly, dateBasis2);
199
199
  }
200
- function getUnifiedInbox(page = 1, pageSize = 50, flaggedOnly = false) {
200
+ function getUnifiedInbox(page = 1, pageSize = 50, flaggedOnly = false, dateBasis2) {
201
201
  abortMessageListRequests();
202
- return ipc().getUnifiedInbox(page, pageSize, flaggedOnly);
202
+ return ipc().getUnifiedInbox(page, pageSize, flaggedOnly, dateBasis2);
203
203
  }
204
204
  function searchMessages(query, page = 1, pageSize = 50, scope = "all", accountId = "", folderId = 0, includeTrashSpam = false) {
205
205
  return ipc().searchMessages(query, page, pageSize, scope, accountId, folderId, includeTrashSpam);
@@ -888,6 +888,213 @@ var init_mailx_types = __esm({
888
888
  }
889
889
  });
890
890
 
891
+ // client/components/address-book.js
892
+ var address_book_exports = {};
893
+ __export(address_book_exports, {
894
+ openAddressBook: () => openAddressBook
895
+ });
896
+ async function openAddressBook(prefillSearch) {
897
+ if (isOpen)
898
+ return;
899
+ isOpen = true;
900
+ const backdrop = document.createElement("div");
901
+ backdrop.className = "mailx-modal-backdrop";
902
+ const panel = document.createElement("div");
903
+ panel.className = "mailx-modal mailx-modal-wide";
904
+ panel.innerHTML = `
905
+ <div class="mailx-modal-title">
906
+ <span class="mailx-modal-title-text">Address Book</span>
907
+ <button type="button" class="mailx-modal-close" id="ab-close" title="Close (Esc)" aria-label="Close">&times;</button>
908
+ </div>
909
+ <div class="ab-toolbar">
910
+ <input type="search" id="ab-search" class="mailx-modal-input" placeholder="Search name or email\u2026" autocomplete="off">
911
+ <button type="button" class="mailx-modal-btn mailx-modal-btn-primary" id="ab-new">+ New contact</button>
912
+ </div>
913
+ <div class="ab-count" id="ab-count"></div>
914
+ <div class="ab-list" id="ab-list">Loading\u2026</div>
915
+ <div class="mailx-modal-buttons">
916
+ <span class="mailx-modal-spacer"></span>
917
+ <button type="button" class="mailx-modal-btn" data-action="close">Close</button>
918
+ </div>`;
919
+ backdrop.appendChild(panel);
920
+ document.body.appendChild(backdrop);
921
+ const searchInput2 = panel.querySelector("#ab-search");
922
+ const listEl = panel.querySelector("#ab-list");
923
+ const countEl = panel.querySelector("#ab-count");
924
+ let editingEmail = null;
925
+ const render2 = (items, total) => {
926
+ countEl.textContent = total === items.length ? `${total} contact${total === 1 ? "" : "s"}` : `Showing ${items.length} of ${total}`;
927
+ if (items.length === 0) {
928
+ listEl.innerHTML = `<div class="ab-empty">No contacts match.</div>`;
929
+ return;
930
+ }
931
+ const fmtDate = (ms) => {
932
+ if (!ms)
933
+ return "";
934
+ const d = new Date(ms);
935
+ const now = /* @__PURE__ */ new Date();
936
+ return d.getFullYear() === now.getFullYear() ? d.toLocaleDateString(void 0, { month: "short", day: "numeric" }) : d.toLocaleDateString();
937
+ };
938
+ listEl.innerHTML = `
939
+ <div class="ab-row ab-header">
940
+ <span class="ab-name">Name</span>
941
+ <span class="ab-email">Email</span>
942
+ <span class="ab-source">Source</span>
943
+ <span class="ab-count-cell" title="Times sent to">\xD7</span>
944
+ <span class="ab-last">Last</span>
945
+ <span class="ab-actions"></span>
946
+ </div>` + items.map((c) => `
947
+ <div class="ab-row" data-email="${escapeAttr(c.email)}">
948
+ <span class="ab-name">${escapeHtml(c.name || "")}</span>
949
+ <span class="ab-email">${escapeHtml(c.email)}</span>
950
+ <span class="ab-source">${escapeHtml(c.source)}</span>
951
+ <span class="ab-count-cell">${c.useCount || 0}</span>
952
+ <span class="ab-last">${fmtDate(c.lastUsed)}</span>
953
+ <span class="ab-actions">
954
+ <button type="button" class="ab-edit" title="Edit name">\u270E</button>
955
+ <button type="button" class="ab-del" title="Delete">\u{1F5D1}</button>
956
+ <button type="button" class="ab-mail" title="Compose to">\u2709</button>
957
+ </span>
958
+ </div>`).join("");
959
+ listEl.querySelectorAll(".ab-row[data-email]").forEach((row) => {
960
+ const email = row.dataset.email;
961
+ const c = items.find((x) => x.email === email);
962
+ row.querySelector(".ab-edit")?.addEventListener("click", () => beginEdit(row, c));
963
+ row.querySelector(".ab-del")?.addEventListener("click", () => doDelete(c));
964
+ row.querySelector(".ab-mail")?.addEventListener("click", () => composeTo(c));
965
+ });
966
+ };
967
+ const beginEdit = (row, c) => {
968
+ if (editingEmail === c.email)
969
+ return;
970
+ editingEmail = c.email;
971
+ const nameSpan = row.querySelector(".ab-name");
972
+ const original = c.name || "";
973
+ nameSpan.innerHTML = `<input type="text" value="${escapeAttr(original)}" class="ab-name-input">`;
974
+ const inp = nameSpan.querySelector("input");
975
+ inp.focus();
976
+ inp.select();
977
+ const commit = async () => {
978
+ const newName = inp.value.trim();
979
+ if (newName !== original) {
980
+ try {
981
+ await upsertContact(newName, c.email);
982
+ c.name = newName;
983
+ } catch (e) {
984
+ alert(`Update failed: ${e?.message || e}`);
985
+ }
986
+ }
987
+ nameSpan.textContent = c.name || "";
988
+ editingEmail = null;
989
+ };
990
+ inp.addEventListener("blur", commit);
991
+ inp.addEventListener("keydown", (e) => {
992
+ if (e.key === "Enter") {
993
+ e.preventDefault();
994
+ inp.blur();
995
+ } else if (e.key === "Escape") {
996
+ e.preventDefault();
997
+ inp.value = original;
998
+ inp.blur();
999
+ }
1000
+ });
1001
+ };
1002
+ const doDelete = async (c) => {
1003
+ if (!confirm(`Delete contact "${c.name || c.email}"?`))
1004
+ return;
1005
+ try {
1006
+ await deleteContact(c.email);
1007
+ await reload();
1008
+ } catch (e) {
1009
+ alert(`Delete failed: ${e?.message || e}`);
1010
+ }
1011
+ };
1012
+ const composeTo = (c) => {
1013
+ const init = {
1014
+ mode: "new",
1015
+ to: [{ name: c.name || "", address: c.email }],
1016
+ cc: [],
1017
+ bcc: [],
1018
+ subject: "",
1019
+ bodyHtml: "",
1020
+ inReplyTo: "",
1021
+ references: [],
1022
+ accounts: []
1023
+ };
1024
+ try {
1025
+ sessionStorage.setItem("composeInit", JSON.stringify(init));
1026
+ } catch {
1027
+ }
1028
+ document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "new" } }));
1029
+ close();
1030
+ };
1031
+ let reloadDebounce;
1032
+ const reload = async () => {
1033
+ try {
1034
+ const r = await listContacts(searchInput2.value, 1, 200);
1035
+ render2(r.items, r.total);
1036
+ } catch (e) {
1037
+ listEl.innerHTML = `<div class="ab-empty">Load failed: ${escapeHtml(e?.message || String(e))}</div>`;
1038
+ }
1039
+ };
1040
+ const scheduleReload = () => {
1041
+ if (reloadDebounce)
1042
+ window.clearTimeout(reloadDebounce);
1043
+ reloadDebounce = window.setTimeout(reload, 200);
1044
+ };
1045
+ panel.querySelector("#ab-new")?.addEventListener("click", async () => {
1046
+ const email = prompt("Email address:");
1047
+ if (!email)
1048
+ return;
1049
+ const name = prompt("Display name (optional):") || "";
1050
+ try {
1051
+ await upsertContact(name, email.trim());
1052
+ await reload();
1053
+ } catch (e) {
1054
+ alert(`Add failed: ${e?.message || e}`);
1055
+ }
1056
+ });
1057
+ searchInput2.addEventListener("input", scheduleReload);
1058
+ if (prefillSearch)
1059
+ searchInput2.value = prefillSearch;
1060
+ const close = () => {
1061
+ if (reloadDebounce)
1062
+ window.clearTimeout(reloadDebounce);
1063
+ backdrop.remove();
1064
+ document.removeEventListener("keydown", onKey, true);
1065
+ isOpen = false;
1066
+ };
1067
+ const onKey = (e) => {
1068
+ if (e.key === "Escape") {
1069
+ e.stopPropagation();
1070
+ e.preventDefault();
1071
+ close();
1072
+ }
1073
+ };
1074
+ document.addEventListener("keydown", onKey, true);
1075
+ panel.querySelector("#ab-close").addEventListener("click", close);
1076
+ panel.querySelector('[data-action="close"]').addEventListener("click", close);
1077
+ backdrop.addEventListener("mousedown", (e) => {
1078
+ if (e.target === backdrop)
1079
+ close();
1080
+ });
1081
+ await reload();
1082
+ }
1083
+ function escapeHtml(s) {
1084
+ return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
1085
+ }
1086
+ function escapeAttr(s) {
1087
+ return escapeHtml(s);
1088
+ }
1089
+ var isOpen;
1090
+ var init_address_book = __esm({
1091
+ "client/components/address-book.js"() {
1092
+ "use strict";
1093
+ init_api_client();
1094
+ isOpen = false;
1095
+ }
1096
+ });
1097
+
891
1098
  // client/components/message-viewer.js
892
1099
  function parsedCacheGet(accountId, folderId, uid) {
893
1100
  if (folderId == null)
@@ -1317,7 +1524,7 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
1317
1524
  }
1318
1525
  if (!cachedMsg) {
1319
1526
  const previewText = (cached.preview || "").trim();
1320
- bodyEl.innerHTML = previewText ? `<div class="mv-preview-placeholder">${escapeHtml(previewText)}</div>` : `<div class="mv-empty">Loading body\u2026</div>`;
1527
+ bodyEl.innerHTML = previewText ? `<div class="mv-preview-placeholder">${escapeHtml2(previewText)}</div>` : `<div class="mv-empty">Loading body\u2026</div>`;
1321
1528
  }
1322
1529
  } else if (!cachedMsg) {
1323
1530
  bodyEl.innerHTML = `<div class="mv-empty">Loading body\u2026</div>`;
@@ -1346,7 +1553,7 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
1346
1553
  const previewText = (msg.preview || cached?.preview || "").trim();
1347
1554
  const waitStart = Date.now();
1348
1555
  const indicatorHtml = `<span class="mv-wait-elapsed" data-start="${waitStart}">(0s)</span>`;
1349
- bodyEl.innerHTML = previewText ? `<div class="mv-preview-placeholder">${escapeHtml(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>`;
1556
+ 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>`;
1350
1557
  const captureGen = gen;
1351
1558
  const tick = setInterval(() => {
1352
1559
  if (captureGen !== showMessageGeneration) {
@@ -1450,6 +1657,13 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
1450
1657
  if (name) {
1451
1658
  items.push({ label: `Copy both: ${both}`, action: () => navigator.clipboard.writeText(both) });
1452
1659
  }
1660
+ items.push({
1661
+ label: `Open in contacts: ${addr.address}`,
1662
+ action: async () => {
1663
+ const { openAddressBook: openAddressBook2 } = await Promise.resolve().then(() => (init_address_book(), address_book_exports));
1664
+ await openAddressBook2(addr.address);
1665
+ }
1666
+ });
1453
1667
  items.push({
1454
1668
  label: `Add to contacts: ${addr.address}`,
1455
1669
  action: async () => {
@@ -1828,7 +2042,7 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
1828
2042
  bodyEl.innerHTML = `<div class="mv-system-message mv-system-error">
1829
2043
  <div class="mv-system-tag">mailx</div>
1830
2044
  <div class="mv-system-title">Body fetch failed</div>
1831
- <div class="mv-system-body">${escapeHtml(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>
2045
+ <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>
1832
2046
  </div>`;
1833
2047
  } else {
1834
2048
  const emlPath = msg.emlPath || "";
@@ -1837,8 +2051,8 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
1837
2051
  const headline = attCount > 0 ? "This message has attachments but no body text." : "This message has no body text \u2014 only a subject.";
1838
2052
  const crumbs = `mailx${appVer ? " " + appVer : ""} \xB7 ${accountId}/${uid}${emlPath ? " \xB7 " + emlPath : ""}`;
1839
2053
  bodyEl.innerHTML = `<div class="mv-system-message">
1840
- <div class="mv-system-body">${escapeHtml(headline)}</div>
1841
- <div class="mv-system-body" style="color:var(--color-text-muted);font-size:0.8em;margin-top:8px">${escapeHtml(crumbs)}</div>
2054
+ <div class="mv-system-body">${escapeHtml2(headline)}</div>
2055
+ <div class="mv-system-body" style="color:var(--color-text-muted);font-size:0.8em;margin-top:8px">${escapeHtml2(crumbs)}</div>
1842
2056
  </div>`;
1843
2057
  }
1844
2058
  }
@@ -2037,7 +2251,7 @@ function renderHeaderFromEnvelope(headerEl, env) {
2037
2251
  }
2038
2252
  }
2039
2253
  }
2040
- function escapeHtml(s) {
2254
+ function escapeHtml2(s) {
2041
2255
  return (s || "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
2042
2256
  }
2043
2257
  function linkifyText(text) {
@@ -2791,6 +3005,7 @@ __export(message_list_exports, {
2791
3005
  clearSearchMode: () => clearSearchMode,
2792
3006
  exitMultiSelect: () => exitMultiSelect,
2793
3007
  getCurrentFocused: () => getCurrentFocused,
3008
+ getDateBasis: () => getDateBasis,
2794
3009
  getSelectedMessages: () => getSelectedMessages,
2795
3010
  initMessageList: () => initMessageList,
2796
3011
  loadMessages: () => loadMessages,
@@ -2804,6 +3019,7 @@ __export(message_list_exports, {
2804
3019
  revealMessage: () => revealMessage,
2805
3020
  scrollFocusedIntoView: () => scrollFocusedIntoView,
2806
3021
  selectAllVisible: () => selectAllVisible,
3022
+ setDateBasis: () => setDateBasis,
2807
3023
  setLiveFilter: () => setLiveFilter,
2808
3024
  setRowFlagged: () => setRowFlagged,
2809
3025
  setRowSeen: () => setRowSeen,
@@ -2834,6 +3050,23 @@ function emptyListMessage() {
2834
3050
  }
2835
3051
  return "No messages";
2836
3052
  }
3053
+ function effectiveDate(msg) {
3054
+ return dateBasis === "received" ? msg.date : msg.sentDate ?? msg.date;
3055
+ }
3056
+ function getDateBasis() {
3057
+ return dateBasis;
3058
+ }
3059
+ function setDateBasis(basis) {
3060
+ if (basis === dateBasis)
3061
+ return;
3062
+ dateBasis = basis;
3063
+ try {
3064
+ localStorage.setItem("mailx-date-basis", basis);
3065
+ } catch {
3066
+ }
3067
+ listCache.clear();
3068
+ reloadCurrentFolder();
3069
+ }
2837
3070
  function cacheKey(mode, a, f, flagged, q) {
2838
3071
  if (mode === "folder")
2839
3072
  return `folder:${a}:${f}:${flagged ? "flag" : ""}`;
@@ -2864,12 +3097,14 @@ function withScrollAnchor(body, doRender) {
2864
3097
  const wasAtTop = scrollTop < 4;
2865
3098
  let anchorUuid = "";
2866
3099
  let anchorOffsetWithinViewport = 0;
3100
+ let anchoredOnSelected = false;
2867
3101
  const selected = body.querySelector(".ml-row.selected");
2868
3102
  if (selected) {
2869
3103
  const vt = selected.offsetTop - scrollTop;
2870
3104
  if (vt > -selected.offsetHeight && vt < viewportH) {
2871
3105
  anchorUuid = selected.dataset.uuid || "";
2872
3106
  anchorOffsetWithinViewport = vt;
3107
+ anchoredOnSelected = true;
2873
3108
  }
2874
3109
  }
2875
3110
  if (!anchorUuid) {
@@ -2892,7 +3127,7 @@ function withScrollAnchor(body, doRender) {
2892
3127
  return;
2893
3128
  const after = body.querySelector(`.ml-row[data-uuid="${CSS.escape(anchorUuid)}"]`);
2894
3129
  if (after) {
2895
- if (wasAtTop && after.offsetTop < viewportH - after.offsetHeight) {
3130
+ if (wasAtTop && !anchoredOnSelected && after.offsetTop < viewportH - after.offsetHeight) {
2896
3131
  body.scrollTop = 0;
2897
3132
  } else {
2898
3133
  body.scrollTop = after.offsetTop - anchorOffsetWithinViewport;
@@ -2915,7 +3150,7 @@ function rememberPosition() {
2915
3150
  positionMemory.set(key, { uid, uuid: sel.dataset.uuid || "", scroll: body.scrollTop });
2916
3151
  persistPositions();
2917
3152
  }
2918
- function pickRestoreUid(items, saved) {
3153
+ function pickRestoreUid(items, saved, allowNeighbor = true) {
2919
3154
  if (!items.length)
2920
3155
  return null;
2921
3156
  if (saved.uuid) {
@@ -2923,6 +3158,8 @@ function pickRestoreUid(items, saved) {
2923
3158
  if (exact?.uuid)
2924
3159
  return exact.uuid;
2925
3160
  }
3161
+ if (!allowNeighbor)
3162
+ return null;
2926
3163
  let best = null;
2927
3164
  for (const m of items) {
2928
3165
  if (typeof m.uid !== "number" || !m.uuid)
@@ -3265,6 +3502,24 @@ function exitMultiSelect() {
3265
3502
  updateBulkBar();
3266
3503
  }
3267
3504
  function updateBulkBar() {
3505
+ const panel = document.getElementById("message-list");
3506
+ if (!panel)
3507
+ return;
3508
+ const body = document.getElementById("ml-body");
3509
+ const count = body?.classList.contains("multi-select-on") ? body.querySelectorAll(".ml-row.selected").length : 0;
3510
+ let pill = document.getElementById("ml-select-count");
3511
+ if (count >= 2) {
3512
+ if (!pill) {
3513
+ pill = document.createElement("div");
3514
+ pill.id = "ml-select-count";
3515
+ pill.className = "ml-select-count";
3516
+ panel.appendChild(pill);
3517
+ }
3518
+ pill.textContent = `${count} selected`;
3519
+ pill.style.display = "";
3520
+ } else if (pill) {
3521
+ pill.style.display = "none";
3522
+ }
3268
3523
  }
3269
3524
  function selectAllVisible() {
3270
3525
  const body = document.getElementById("ml-body");
@@ -3325,22 +3580,7 @@ function initMessageList(handler) {
3325
3580
  }
3326
3581
  }, { passive: true });
3327
3582
  body.addEventListener("scroll", () => {
3328
- if (loading)
3329
- return;
3330
- const nearBottom = body.scrollHeight - body.scrollTop - body.clientHeight < 1e3;
3331
- if (nearBottom) {
3332
- if (currentPage * 50 < totalMessages) {
3333
- loadMoreMessages();
3334
- } else {
3335
- if (!body._mlScrollEndLogged) {
3336
- body._mlScrollEndLogged = true;
3337
- const rows = body.querySelectorAll(".ml-row").length;
3338
- console.log(` [ml-scroll] reached bottom \u2014 currentPage=${currentPage} pageSize=50 loadedRows=${rows} totalMessages=${totalMessages} searchMode=${searchMode} unifiedMode=${unifiedMode} flaggedOnly=${body.classList.contains("flagged-only")}`);
3339
- }
3340
- }
3341
- } else {
3342
- body._mlScrollEndLogged = false;
3343
- }
3583
+ maybeLoadMore(body);
3344
3584
  });
3345
3585
  }
3346
3586
  document.addEventListener("mailx-remove-stale", (e) => {
@@ -3484,7 +3724,7 @@ async function loadUnifiedInbox(autoSelect = true) {
3484
3724
  withScrollAnchor(body, renderCached);
3485
3725
  else
3486
3726
  renderCached();
3487
- const targetUuid = remembered ? pickRestoreUid(cached.items, remembered) : null;
3727
+ const targetUuid = remembered ? pickRestoreUid(cached.items, remembered, autoSelect) : null;
3488
3728
  if (targetUuid) {
3489
3729
  if (autoSelect)
3490
3730
  body.scrollTop = savedScroll;
@@ -3496,7 +3736,7 @@ async function loadUnifiedInbox(autoSelect = true) {
3496
3736
  body.innerHTML = `<div class="ml-empty">Loading...</div>`;
3497
3737
  }
3498
3738
  try {
3499
- const result = await getUnifiedInbox(1, 50, flaggedOnly);
3739
+ const result = await getUnifiedInbox(1, 50, flaggedOnly, dateBasis);
3500
3740
  if (myGen !== loadGen)
3501
3741
  return;
3502
3742
  totalMessages = result.total;
@@ -3510,7 +3750,7 @@ async function loadUnifiedInbox(autoSelect = true) {
3510
3750
  }
3511
3751
  setMessages(result.items);
3512
3752
  withScrollAnchor(body, () => renderMessages(body, "", result.items, { background: !autoSelect }));
3513
- const targetUuid = remembered ? pickRestoreUid(result.items, remembered) : null;
3753
+ const targetUuid = remembered ? pickRestoreUid(result.items, remembered, autoSelect) : null;
3514
3754
  if (targetUuid) {
3515
3755
  if (!cached)
3516
3756
  body.scrollTop = savedScroll;
@@ -3564,7 +3804,7 @@ async function loadSearchResults(query, scope = "all", accountId = "", folderId
3564
3804
  body.innerHTML = `<div class="ml-empty">Invalid regex</div>`;
3565
3805
  return;
3566
3806
  }
3567
- const source = scope === "current" && accountId ? await getMessages(accountId, folderId, 1, 1e4) : await getUnifiedInbox(1, 1e4);
3807
+ const source = scope === "current" && accountId ? await getMessages(accountId, folderId, 1, 1e4, false, currentSort, currentSortDir, dateBasis) : await getUnifiedInbox(1, 1e4, false, dateBasis);
3568
3808
  if (myGen !== loadGen)
3569
3809
  return;
3570
3810
  const matches = source.items.filter((m) => regex.test(m.subject || "") || regex.test(m.from?.name || "") || regex.test(m.from?.address || "") || regex.test(m.preview || ""));
@@ -3637,7 +3877,7 @@ async function loadMessages(accountId, folderId, page = 1, specialUse = "", auto
3637
3877
  withScrollAnchor(body, renderCached);
3638
3878
  else
3639
3879
  renderCached();
3640
- const targetUuid = remembered ? pickRestoreUid(cached.items, remembered) : null;
3880
+ const targetUuid = remembered ? pickRestoreUid(cached.items, remembered, autoSelect) : null;
3641
3881
  if (focusUid != null) {
3642
3882
  requestAnimationFrame(() => {
3643
3883
  if (!focusByIdentity(accountId, focusUid, { scroll: true }) && autoSelect)
@@ -3656,7 +3896,7 @@ async function loadMessages(accountId, folderId, page = 1, specialUse = "", auto
3656
3896
  body.innerHTML = `<div class="ml-empty">Loading...</div>`;
3657
3897
  }
3658
3898
  try {
3659
- const result = await getMessages(accountId, folderId, 1, 50, flaggedOnly, currentSort, currentSortDir);
3899
+ const result = await getMessages(accountId, folderId, 1, 50, flaggedOnly, currentSort, currentSortDir, dateBasis);
3660
3900
  if (myGen !== loadGen)
3661
3901
  return;
3662
3902
  totalMessages = result.total;
@@ -3680,7 +3920,7 @@ async function loadMessages(accountId, folderId, page = 1, specialUse = "", auto
3680
3920
  focusedRow = null;
3681
3921
  }
3682
3922
  }
3683
- const targetUuid = remembered ? pickRestoreUid(result.items, remembered) : null;
3923
+ const targetUuid = remembered ? pickRestoreUid(result.items, remembered, autoSelect) : null;
3684
3924
  if (focusUid != null) {
3685
3925
  requestAnimationFrame(() => {
3686
3926
  if (myGen !== loadGen)
@@ -3707,6 +3947,22 @@ async function loadMessages(accountId, folderId, page = 1, specialUse = "", auto
3707
3947
  body.innerHTML = `<div class="ml-empty">Error: ${e.message}</div>`;
3708
3948
  }
3709
3949
  }
3950
+ function maybeLoadMore(body) {
3951
+ if (loading)
3952
+ return;
3953
+ const nearBottom = body.scrollHeight - body.scrollTop - body.clientHeight < 1e3;
3954
+ if (!nearBottom) {
3955
+ body._mlScrollEndLogged = false;
3956
+ return;
3957
+ }
3958
+ if (currentPage * 50 < totalMessages) {
3959
+ loadMoreMessages();
3960
+ } else if (!body._mlScrollEndLogged) {
3961
+ body._mlScrollEndLogged = true;
3962
+ const rows = body.querySelectorAll(".ml-row").length;
3963
+ console.log(` [ml-scroll] reached bottom \u2014 currentPage=${currentPage} pageSize=50 loadedRows=${rows} totalMessages=${totalMessages} searchMode=${searchMode} unifiedMode=${unifiedMode} flaggedOnly=${body.classList.contains("flagged-only")}`);
3964
+ }
3965
+ }
3710
3966
  async function loadMoreMessages() {
3711
3967
  const body = document.getElementById("ml-body");
3712
3968
  if (!body)
@@ -3718,7 +3974,7 @@ async function loadMoreMessages() {
3718
3974
  const rowsBefore = body.querySelectorAll(".ml-row").length;
3719
3975
  try {
3720
3976
  const flaggedOnly = body.classList.contains("flagged-only");
3721
- const result = searchMode ? await searchMessages(currentSearchQuery, currentPage) : unifiedMode ? await getUnifiedInbox(currentPage, 50, flaggedOnly) : await getMessages(currentAccountId2, currentFolderId, currentPage, 50, flaggedOnly);
3977
+ const result = searchMode ? await searchMessages(currentSearchQuery, currentPage) : unifiedMode ? await getUnifiedInbox(currentPage, 50, flaggedOnly, dateBasis) : await getMessages(currentAccountId2, currentFolderId, currentPage, 50, flaggedOnly, currentSort, currentSortDir, dateBasis);
3722
3978
  const current = getMessages2();
3723
3979
  setMessages([...current, ...result.items]);
3724
3980
  appendMessages(body, unifiedMode ? "" : currentAccountId2, result.items);
@@ -3732,6 +3988,7 @@ async function loadMoreMessages() {
3732
3988
  } finally {
3733
3989
  loading = false;
3734
3990
  }
3991
+ requestAnimationFrame(() => maybeLoadMore(body));
3735
3992
  }
3736
3993
  function renderMessages(body, accountId, items, opts = {}) {
3737
3994
  const preserveSel = body.classList.contains("multi-select-on") ? new Set(Array.from(body.querySelectorAll(".ml-row.selected")).map((r) => r.dataset.uuid || "").filter(Boolean)) : null;
@@ -3745,6 +4002,7 @@ function renderMessages(body, accountId, items, opts = {}) {
3745
4002
  for (const uuid of preserveSel) {
3746
4003
  body.querySelector(`.ml-row[data-uuid="${CSS.escape(uuid)}"]`)?.classList.add("selected");
3747
4004
  }
4005
+ updateBulkBar();
3748
4006
  }
3749
4007
  try {
3750
4008
  maybeShowFilterEmptyState(body);
@@ -3778,7 +4036,8 @@ function restoreSelection(body, savedUuid) {
3778
4036
  const accountId = row.dataset.accountId;
3779
4037
  const uid = Number(row.dataset.uid);
3780
4038
  if (accountId && Number.isFinite(uid)) {
3781
- focusByIdentity(accountId, uid, { scroll: false });
4039
+ const switching = focusedRow?.msg?.uuid !== savedUuid;
4040
+ focusByIdentity(accountId, uid, { scroll: switching });
3782
4041
  }
3783
4042
  }
3784
4043
  async function showThreadPopup(pillEl, headMsg) {
@@ -3803,7 +4062,7 @@ async function showThreadPopup(pillEl, headMsg) {
3803
4062
  from.textContent = msg.from?.name || msg.from?.address || "?";
3804
4063
  const date = document.createElement("span");
3805
4064
  date.className = "ml-thread-popup-date";
3806
- date.textContent = formatDate(msg.date);
4065
+ date.textContent = formatDate(effectiveDate(msg));
3807
4066
  const subject = document.createElement("span");
3808
4067
  subject.className = "ml-thread-popup-subject";
3809
4068
  subject.textContent = msg.subject || "(no subject)";
@@ -3927,12 +4186,12 @@ function formatDate(epochMs) {
3927
4186
  return d.toLocaleString(void 0, dateFmtSameYear);
3928
4187
  return d.toLocaleString(void 0, dateFmt);
3929
4188
  }
3930
- function escapeHtml2(s) {
4189
+ function escapeHtml3(s) {
3931
4190
  const div = document.createElement("div");
3932
4191
  div.textContent = s;
3933
4192
  return div.innerHTML;
3934
4193
  }
3935
- var onMessageSelect, currentAccountId2, currentFolderId, currentSpecialUse, lastClickedRow, currentPage, totalMessages, loading, unifiedMode, searchMode, liveFilterText, currentSearchQuery, wasUnifiedBeforeSearch, showToInsteadOfFrom, touchWasScroll, lastPointerWasTouch, currentSort, currentSortDir, loadGen, listCache, CACHE_KEY_UNIFIED, positionMemory, POSITION_STORAGE_KEY, focusedRow, rowByKey, prioritySenders, priorityDomains, timeFmt, dateFmt, dateFmtSameYear, MessageRow;
4194
+ var onMessageSelect, currentAccountId2, currentFolderId, currentSpecialUse, lastClickedRow, currentPage, totalMessages, loading, unifiedMode, searchMode, liveFilterText, currentSearchQuery, wasUnifiedBeforeSearch, showToInsteadOfFrom, touchWasScroll, lastPointerWasTouch, currentSort, currentSortDir, dateBasis, loadGen, listCache, CACHE_KEY_UNIFIED, positionMemory, POSITION_STORAGE_KEY, focusedRow, rowByKey, prioritySenders, priorityDomains, timeFmt, dateFmt, dateFmtSameYear, MessageRow;
3936
4195
  var init_message_list = __esm({
3937
4196
  "client/components/message-list.js"() {
3938
4197
  "use strict";
@@ -3960,6 +4219,7 @@ var init_message_list = __esm({
3960
4219
  }
3961
4220
  currentSort = "date";
3962
4221
  currentSortDir = "desc";
4222
+ dateBasis = localStorage.getItem("mailx-date-basis") || "sent";
3963
4223
  loadGen = 0;
3964
4224
  listCache = /* @__PURE__ */ new Map();
3965
4225
  CACHE_KEY_UNIFIED = "unified";
@@ -4078,7 +4338,7 @@ var init_message_list = __esm({
4078
4338
  }
4079
4339
  const subject = document.createElement("span");
4080
4340
  subject.className = "ml-subject";
4081
- subject.innerHTML = escapeHtml2(msg.subject);
4341
+ subject.innerHTML = escapeHtml3(msg.subject);
4082
4342
  if (threadHead && threadCount > 1 && msg.threadId) {
4083
4343
  const threadPill = document.createElement("span");
4084
4344
  threadPill.className = "ml-thread-pill";
@@ -4104,7 +4364,8 @@ var init_message_list = __esm({
4104
4364
  renderStatusIcons(icons, msg);
4105
4365
  const dateText = document.createElement("span");
4106
4366
  dateText.className = "ml-date-text";
4107
- dateText.textContent = formatDate(msg.date);
4367
+ dateText.textContent = formatDate(effectiveDate(msg));
4368
+ dateText.title = dateBasis === "received" ? "Received date" : "Sent date";
4108
4369
  date.appendChild(icons);
4109
4370
  date.appendChild(dateText);
4110
4371
  row.appendChild(avatar);
@@ -4304,6 +4565,13 @@ var init_message_list = __esm({
4304
4565
  if (e.ctrlKey || e.metaKey) {
4305
4566
  this.setSelected(!this.isSelected);
4306
4567
  lastClickedRow = this.el;
4568
+ if (body) {
4569
+ const n = body.querySelectorAll(".ml-row.selected").length;
4570
+ if (n >= 1)
4571
+ body.classList.add("multi-select-on");
4572
+ else
4573
+ body.classList.remove("multi-select-on");
4574
+ }
4307
4575
  } else {
4308
4576
  clearSelection();
4309
4577
  focusRow(this);
@@ -4507,9 +4775,9 @@ __export(outbox_view_exports, {
4507
4775
  openOutboxView: () => openOutboxView
4508
4776
  });
4509
4777
  async function openOutboxView() {
4510
- if (isOpen)
4778
+ if (isOpen2)
4511
4779
  return;
4512
- isOpen = true;
4780
+ isOpen2 = true;
4513
4781
  const backdrop = document.createElement("div");
4514
4782
  backdrop.className = "mailx-modal-backdrop";
4515
4783
  const panel = document.createElement("div");
@@ -4550,19 +4818,19 @@ async function openOutboxView() {
4550
4818
  return `
4551
4819
  <div class="ob-row ob-pink" data-idx="${i}">
4552
4820
  <div class="ob-row-hdr">
4553
- <span class="ob-acct">${escapeHtml3(m.accountId)}</span>
4554
- <span class="ob-subject">${escapeHtml3(m.subject || "(no subject)")}</span>
4821
+ <span class="ob-acct">${escapeHtml4(m.accountId)}</span>
4822
+ <span class="ob-subject">${escapeHtml4(m.subject || "(no subject)")}</span>
4555
4823
  <span class="ob-created">${fmtDate(m.createdAt)}</span>
4556
4824
  ${claimBadge}
4557
4825
  ${m.attempts > 0 ? `<span class="ob-badge ob-retry" title="Retry attempts made so far">retry \xD7${m.attempts}</span>` : ""}
4558
4826
  </div>
4559
4827
  <div class="ob-row-meta">
4560
- <span class="ob-from">${escapeHtml3(m.from || "")}</span>
4561
- \u2192 <span class="ob-to">${escapeHtml3(m.to || "")}</span>
4562
- ${m.cc ? ` \xB7 Cc: ${escapeHtml3(m.cc)}` : ""}
4828
+ <span class="ob-from">${escapeHtml4(m.from || "")}</span>
4829
+ \u2192 <span class="ob-to">${escapeHtml4(m.to || "")}</span>
4830
+ ${m.cc ? ` \xB7 Cc: ${escapeHtml4(m.cc)}` : ""}
4563
4831
  <span class="ob-size">\xB7 ${(m.sizeBytes / 1024).toFixed(1)}kB</span>
4564
4832
  </div>
4565
- <div class="ob-row-path">${escapeHtml3(m.path)}</div>
4833
+ <div class="ob-row-path">${escapeHtml4(m.path)}</div>
4566
4834
  <div class="ob-row-actions">
4567
4835
  <button type="button" class="ob-cancel">Cancel</button>
4568
4836
  </div>
@@ -4598,13 +4866,13 @@ Subject: ${m.subject}`;
4598
4866
  const items = await listQueuedOutgoing();
4599
4867
  renderList(items || []);
4600
4868
  } catch (e) {
4601
- listEl.innerHTML = `<div class="ob-empty">Load failed: ${escapeHtml3(e?.message || String(e))}</div>`;
4869
+ listEl.innerHTML = `<div class="ob-empty">Load failed: ${escapeHtml4(e?.message || String(e))}</div>`;
4602
4870
  }
4603
4871
  };
4604
4872
  const close = () => {
4605
4873
  backdrop.remove();
4606
4874
  document.removeEventListener("keydown", onKey, true);
4607
- isOpen = false;
4875
+ isOpen2 = false;
4608
4876
  };
4609
4877
  const onKey = (e) => {
4610
4878
  if (e.key === "Escape") {
@@ -4623,15 +4891,15 @@ Subject: ${m.subject}`;
4623
4891
  });
4624
4892
  await reload();
4625
4893
  }
4626
- function escapeHtml3(s) {
4894
+ function escapeHtml4(s) {
4627
4895
  return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
4628
4896
  }
4629
- var isOpen;
4897
+ var isOpen2;
4630
4898
  var init_outbox_view = __esm({
4631
4899
  "client/components/outbox-view.js"() {
4632
4900
  "use strict";
4633
4901
  init_api_client();
4634
- isOpen = false;
4902
+ isOpen2 = false;
4635
4903
  }
4636
4904
  });
4637
4905
 
@@ -4662,7 +4930,7 @@ function calendarKind(id, primary) {
4662
4930
  }
4663
4931
  function calIconHtml(info) {
4664
4932
  const kind = calendarKind(info.id, info.primary);
4665
- const t = escapeHtml4(info.name);
4933
+ const t = escapeHtml5(info.name);
4666
4934
  if (kind === "personal")
4667
4935
  return `<span class="cal-ico cal-ico-dot" title="${t}"></span>`;
4668
4936
  if (kind === "usHoliday")
@@ -4673,9 +4941,9 @@ function calIconHtml(info) {
4673
4941
  return `<span class="cal-ico cal-ico-emoji" title="${t}">\u{1F382}</span>`;
4674
4942
  if (kind === "otherHoliday")
4675
4943
  return `<span class="cal-ico cal-ico-emoji" title="${t}">\u2726</span>`;
4676
- const letter = escapeHtml4((info.name.trim()[0] || "?").toUpperCase());
4944
+ const letter = escapeHtml5((info.name.trim()[0] || "?").toUpperCase());
4677
4945
  const color = info.color || "#7a7a7a";
4678
- return `<span class="cal-ico cal-ico-mono" style="background:${escapeHtml4(color)}" title="${t}">${letter}</span>`;
4946
+ return `<span class="cal-ico cal-ico-mono" style="background:${escapeHtml5(color)}" title="${t}">${letter}</span>`;
4679
4947
  }
4680
4948
  function calInfoFor(calendarId) {
4681
4949
  const id = calendarId || "primary";
@@ -4724,10 +4992,10 @@ async function renderCalendarList() {
4724
4992
  host.innerHTML = "";
4725
4993
  } else {
4726
4994
  const sorted = [...list].sort((a, b) => (a.primary ? 0 : 1) - (b.primary ? 0 : 1) || a.name.localeCompare(b.name));
4727
- host.innerHTML = sorted.map((c) => `<label class="cal-side-cal-row" title="${escapeHtml4(c.name)}">
4728
- <input type="checkbox" class="cal-side-cal-check" data-cal-id="${escapeHtml4(c.id)}" ${hiddenCalendars.has(c.id) ? "" : "checked"}>
4995
+ host.innerHTML = sorted.map((c) => `<label class="cal-side-cal-row" title="${escapeHtml5(c.name)}">
4996
+ <input type="checkbox" class="cal-side-cal-check" data-cal-id="${escapeHtml5(c.id)}" ${hiddenCalendars.has(c.id) ? "" : "checked"}>
4729
4997
  ${calIconHtml(c)}
4730
- <span class="cal-side-cal-name">${escapeHtml4(c.name)}</span>
4998
+ <span class="cal-side-cal-name">${escapeHtml5(c.name)}</span>
4731
4999
  </label>`).join("");
4732
5000
  host.querySelectorAll(".cal-side-cal-check").forEach((cb) => {
4733
5001
  cb.addEventListener("change", async () => {
@@ -4811,7 +5079,7 @@ function formatTime(e) {
4811
5079
  return "all day";
4812
5080
  return new Date(e.start).toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit", hour12: false });
4813
5081
  }
4814
- function escapeHtml4(s) {
5082
+ function escapeHtml5(s) {
4815
5083
  return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
4816
5084
  }
4817
5085
  function renderHead() {
@@ -4828,10 +5096,10 @@ function overdueTaskRowHtml(t) {
4828
5096
  const sameYear = d.getFullYear() === (/* @__PURE__ */ new Date()).getFullYear();
4829
5097
  dueLabel = sameYear ? `${d.getMonth() + 1}/${d.getDate()}` : d.toISOString().slice(0, 10);
4830
5098
  }
4831
- return `<div class="cal-side-task cal-side-task-overdue" data-uuid="${escapeHtml4(t.uuid)}">
5099
+ return `<div class="cal-side-task cal-side-task-overdue" data-uuid="${escapeHtml5(t.uuid)}">
4832
5100
  <input type="checkbox" class="cal-side-overdue-check" title="Mark done">
4833
- <span class="cal-side-task-title" title="${escapeHtml4(t.title)}">${escapeHtml4(t.title)}</span>
4834
- <span class="cal-side-task-due overdue">${escapeHtml4(dueLabel)}</span>
5101
+ <span class="cal-side-task-title" title="${escapeHtml5(t.title)}">${escapeHtml5(t.title)}</span>
5102
+ <span class="cal-side-task-due overdue">${escapeHtml5(dueLabel)}</span>
4835
5103
  </div>`;
4836
5104
  }
4837
5105
  function renderEvents(events) {
@@ -4888,10 +5156,10 @@ function renderEvents(events) {
4888
5156
  html += `<div class="cal-side-day cal-side-day-daily">Daily</div>`;
4889
5157
  for (const e of dailyHeads) {
4890
5158
  const link = e.htmlLink || "";
4891
- html += `<div class="cal-side-event" data-id="${e.id}" data-link="${escapeHtml4(link)}" ${link ? 'title="Click to open in Google Calendar"' : ""}>
5159
+ html += `<div class="cal-side-event" data-id="${e.id}" data-link="${escapeHtml5(link)}" ${link ? 'title="Click to open in Google Calendar"' : ""}>
4892
5160
  ${calIconHtml(calInfoFor(e.calendarId))}
4893
- <span class="cal-side-event-time">${escapeHtml4(formatTime(e))}</span>
4894
- <span class="cal-side-event-title" title="${escapeHtml4(e.title)}">${escapeHtml4(e.title)}<span class="cal-side-event-recur" title="Daily">\u21BB</span></span>
5161
+ <span class="cal-side-event-time">${escapeHtml5(formatTime(e))}</span>
5162
+ <span class="cal-side-event-title" title="${escapeHtml5(e.title)}">${escapeHtml5(e.title)}<span class="cal-side-event-recur" title="Daily">\u21BB</span></span>
4895
5163
  </div>`;
4896
5164
  }
4897
5165
  }
@@ -4909,7 +5177,7 @@ function renderEvents(events) {
4909
5177
  const d = new Date(e.start);
4910
5178
  const dayKey = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
4911
5179
  if (dayKey !== lastDayKey) {
4912
- html += `<div class="cal-side-day">${escapeHtml4(formatDayHeader(d, today, tomorrow))}</div>`;
5180
+ html += `<div class="cal-side-day">${escapeHtml5(formatDayHeader(d, today, tomorrow))}</div>`;
4913
5181
  lastDayKey = dayKey;
4914
5182
  }
4915
5183
  const recurMark = e.recurringEventId ? `<span class="cal-side-event-recur" title="Recurring event">\u21BB</span>` : "";
@@ -4917,14 +5185,14 @@ function renderEvents(events) {
4917
5185
  const recurAttr = e.recurringEventId ? ' data-recurring="1"' : "";
4918
5186
  if (isHolidayKind || kind === "birthday") {
4919
5187
  html += `<div class="cal-side-event" data-holiday="1" data-holiday-kind="${kind}" data-id="${e.id}">
4920
- <span class="cal-side-event-title cal-side-event-holiday-title" title="${escapeHtml4(e.title)}">${calIconHtml(info)} ${escapeHtml4(e.title)}</span>
5188
+ <span class="cal-side-event-title cal-side-event-holiday-title" title="${escapeHtml5(e.title)}">${calIconHtml(info)} ${escapeHtml5(e.title)}</span>
4921
5189
  </div>`;
4922
5190
  } else {
4923
5191
  const titleAttr = link ? 'title="Click to open in Google Calendar"' : "";
4924
- html += `<div class="cal-side-event" data-id="${e.id}"${recurAttr} data-link="${escapeHtml4(link)}" ${titleAttr}>
5192
+ html += `<div class="cal-side-event" data-id="${e.id}"${recurAttr} data-link="${escapeHtml5(link)}" ${titleAttr}>
4925
5193
  ${calIconHtml(info)}
4926
- <span class="cal-side-event-time">${escapeHtml4(formatTime(e))}</span>
4927
- <span class="cal-side-event-title" title="${escapeHtml4(e.title)}">${escapeHtml4(e.title)}${recurMark}</span>
5194
+ <span class="cal-side-event-time">${escapeHtml5(formatTime(e))}</span>
5195
+ <span class="cal-side-event-title" title="${escapeHtml5(e.title)}">${escapeHtml5(e.title)}${recurMark}</span>
4928
5196
  </div>`;
4929
5197
  }
4930
5198
  }
@@ -5009,7 +5277,7 @@ async function renderTasks(prefetched) {
5009
5277
  const sel = selectedTaskUuids.has(t.uuid) ? " selected" : "";
5010
5278
  html += `<div class="cal-side-task${sel}" data-uuid="${t.uuid}">
5011
5279
  <input type="checkbox" ${done ? "checked" : ""} class="cal-side-task-check">
5012
- <span class="cal-side-task-title${done ? " done" : ""}" title="${escapeHtml4(t.title)}">${escapeHtml4(t.title)}</span>
5280
+ <span class="cal-side-task-title${done ? " done" : ""}" title="${escapeHtml5(t.title)}">${escapeHtml5(t.title)}</span>
5013
5281
  ${dueHtml}
5014
5282
  <button class="cal-side-task-delete" title="Delete task" aria-label="Delete task">\xD7</button>
5015
5283
  </div>`;
@@ -5088,7 +5356,7 @@ async function refresh() {
5088
5356
  } catch (e) {
5089
5357
  const body = document.getElementById("cal-side-body");
5090
5358
  if (body)
5091
- body.innerHTML = `<div class="cal-side-empty cal-side-quota-error">Couldn't load calendar: ${escapeHtml4(e?.message || String(e))}</div>`;
5359
+ body.innerHTML = `<div class="cal-side-empty cal-side-quota-error">Couldn't load calendar: ${escapeHtml5(e?.message || String(e))}</div>`;
5092
5360
  }
5093
5361
  renderTasks(prefetchedTasks);
5094
5362
  }
@@ -5369,14 +5637,14 @@ function initCalendarSidebar() {
5369
5637
  const host = event.feature === "tasks" ? document.getElementById("cal-side-tasks") : document.getElementById("cal-side-body");
5370
5638
  if (host && !host.querySelector(".cal-side-quota-error")) {
5371
5639
  const msg = event.message || `Google ${event.feature} quota exceeded \u2014 try again later.`;
5372
- host.innerHTML = `<div class="cal-side-empty cal-side-quota-error">${escapeHtml4(msg)}</div>`;
5640
+ host.innerHTML = `<div class="cal-side-empty cal-side-quota-error">${escapeHtml5(msg)}</div>`;
5373
5641
  }
5374
5642
  } else if (event?.type === "authScopeError") {
5375
5643
  const host = event.feature === "tasks" ? document.getElementById("cal-side-tasks") : document.getElementById("cal-side-body");
5376
5644
  if (host && !host.querySelector(".cal-side-auth-error")) {
5377
5645
  const msg = event.message || "Google access needs re-consent.";
5378
5646
  host.innerHTML = `<div class="cal-side-empty cal-side-auth-error">
5379
- <div style="margin-bottom:0.6em">${escapeHtml4(msg)}</div>
5647
+ <div style="margin-bottom:0.6em">${escapeHtml5(msg)}</div>
5380
5648
  <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>
5381
5649
  </div>`;
5382
5650
  const btn = host.querySelector(".cal-side-reauth-btn");
@@ -5421,211 +5689,6 @@ var init_calendar_sidebar = __esm({
5421
5689
  }
5422
5690
  });
5423
5691
 
5424
- // client/components/address-book.js
5425
- var address_book_exports = {};
5426
- __export(address_book_exports, {
5427
- openAddressBook: () => openAddressBook
5428
- });
5429
- async function openAddressBook() {
5430
- if (isOpen2)
5431
- return;
5432
- isOpen2 = true;
5433
- const backdrop = document.createElement("div");
5434
- backdrop.className = "mailx-modal-backdrop";
5435
- const panel = document.createElement("div");
5436
- panel.className = "mailx-modal mailx-modal-wide";
5437
- panel.innerHTML = `
5438
- <div class="mailx-modal-title">
5439
- <span class="mailx-modal-title-text">Address Book</span>
5440
- <button type="button" class="mailx-modal-close" id="ab-close" title="Close (Esc)" aria-label="Close">&times;</button>
5441
- </div>
5442
- <div class="ab-toolbar">
5443
- <input type="search" id="ab-search" class="mailx-modal-input" placeholder="Search name or email\u2026" autocomplete="off">
5444
- <button type="button" class="mailx-modal-btn mailx-modal-btn-primary" id="ab-new">+ New contact</button>
5445
- </div>
5446
- <div class="ab-count" id="ab-count"></div>
5447
- <div class="ab-list" id="ab-list">Loading\u2026</div>
5448
- <div class="mailx-modal-buttons">
5449
- <span class="mailx-modal-spacer"></span>
5450
- <button type="button" class="mailx-modal-btn" data-action="close">Close</button>
5451
- </div>`;
5452
- backdrop.appendChild(panel);
5453
- document.body.appendChild(backdrop);
5454
- const searchInput2 = panel.querySelector("#ab-search");
5455
- const listEl = panel.querySelector("#ab-list");
5456
- const countEl = panel.querySelector("#ab-count");
5457
- let editingEmail = null;
5458
- const render2 = (items, total) => {
5459
- countEl.textContent = total === items.length ? `${total} contact${total === 1 ? "" : "s"}` : `Showing ${items.length} of ${total}`;
5460
- if (items.length === 0) {
5461
- listEl.innerHTML = `<div class="ab-empty">No contacts match.</div>`;
5462
- return;
5463
- }
5464
- const fmtDate = (ms) => {
5465
- if (!ms)
5466
- return "";
5467
- const d = new Date(ms);
5468
- const now = /* @__PURE__ */ new Date();
5469
- return d.getFullYear() === now.getFullYear() ? d.toLocaleDateString(void 0, { month: "short", day: "numeric" }) : d.toLocaleDateString();
5470
- };
5471
- listEl.innerHTML = `
5472
- <div class="ab-row ab-header">
5473
- <span class="ab-name">Name</span>
5474
- <span class="ab-email">Email</span>
5475
- <span class="ab-source">Source</span>
5476
- <span class="ab-count-cell" title="Times sent to">\xD7</span>
5477
- <span class="ab-last">Last</span>
5478
- <span class="ab-actions"></span>
5479
- </div>` + items.map((c) => `
5480
- <div class="ab-row" data-email="${escapeAttr(c.email)}">
5481
- <span class="ab-name">${escapeHtml5(c.name || "")}</span>
5482
- <span class="ab-email">${escapeHtml5(c.email)}</span>
5483
- <span class="ab-source">${escapeHtml5(c.source)}</span>
5484
- <span class="ab-count-cell">${c.useCount || 0}</span>
5485
- <span class="ab-last">${fmtDate(c.lastUsed)}</span>
5486
- <span class="ab-actions">
5487
- <button type="button" class="ab-edit" title="Edit name">\u270E</button>
5488
- <button type="button" class="ab-del" title="Delete">\u{1F5D1}</button>
5489
- <button type="button" class="ab-mail" title="Compose to">\u2709</button>
5490
- </span>
5491
- </div>`).join("");
5492
- listEl.querySelectorAll(".ab-row[data-email]").forEach((row) => {
5493
- const email = row.dataset.email;
5494
- const c = items.find((x) => x.email === email);
5495
- row.querySelector(".ab-edit")?.addEventListener("click", () => beginEdit(row, c));
5496
- row.querySelector(".ab-del")?.addEventListener("click", () => doDelete(c));
5497
- row.querySelector(".ab-mail")?.addEventListener("click", () => composeTo(c));
5498
- });
5499
- };
5500
- const beginEdit = (row, c) => {
5501
- if (editingEmail === c.email)
5502
- return;
5503
- editingEmail = c.email;
5504
- const nameSpan = row.querySelector(".ab-name");
5505
- const original = c.name || "";
5506
- nameSpan.innerHTML = `<input type="text" value="${escapeAttr(original)}" class="ab-name-input">`;
5507
- const inp = nameSpan.querySelector("input");
5508
- inp.focus();
5509
- inp.select();
5510
- const commit = async () => {
5511
- const newName = inp.value.trim();
5512
- if (newName !== original) {
5513
- try {
5514
- await upsertContact(newName, c.email);
5515
- c.name = newName;
5516
- } catch (e) {
5517
- alert(`Update failed: ${e?.message || e}`);
5518
- }
5519
- }
5520
- nameSpan.textContent = c.name || "";
5521
- editingEmail = null;
5522
- };
5523
- inp.addEventListener("blur", commit);
5524
- inp.addEventListener("keydown", (e) => {
5525
- if (e.key === "Enter") {
5526
- e.preventDefault();
5527
- inp.blur();
5528
- } else if (e.key === "Escape") {
5529
- e.preventDefault();
5530
- inp.value = original;
5531
- inp.blur();
5532
- }
5533
- });
5534
- };
5535
- const doDelete = async (c) => {
5536
- if (!confirm(`Delete contact "${c.name || c.email}"?`))
5537
- return;
5538
- try {
5539
- await deleteContact(c.email);
5540
- await reload();
5541
- } catch (e) {
5542
- alert(`Delete failed: ${e?.message || e}`);
5543
- }
5544
- };
5545
- const composeTo = (c) => {
5546
- const init = {
5547
- mode: "new",
5548
- to: [{ name: c.name || "", address: c.email }],
5549
- cc: [],
5550
- bcc: [],
5551
- subject: "",
5552
- bodyHtml: "",
5553
- inReplyTo: "",
5554
- references: [],
5555
- accounts: []
5556
- };
5557
- try {
5558
- sessionStorage.setItem("composeInit", JSON.stringify(init));
5559
- } catch {
5560
- }
5561
- document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "new" } }));
5562
- close();
5563
- };
5564
- let reloadDebounce;
5565
- const reload = async () => {
5566
- try {
5567
- const r = await listContacts(searchInput2.value, 1, 200);
5568
- render2(r.items, r.total);
5569
- } catch (e) {
5570
- listEl.innerHTML = `<div class="ab-empty">Load failed: ${escapeHtml5(e?.message || String(e))}</div>`;
5571
- }
5572
- };
5573
- const scheduleReload = () => {
5574
- if (reloadDebounce)
5575
- window.clearTimeout(reloadDebounce);
5576
- reloadDebounce = window.setTimeout(reload, 200);
5577
- };
5578
- panel.querySelector("#ab-new")?.addEventListener("click", async () => {
5579
- const email = prompt("Email address:");
5580
- if (!email)
5581
- return;
5582
- const name = prompt("Display name (optional):") || "";
5583
- try {
5584
- await upsertContact(name, email.trim());
5585
- await reload();
5586
- } catch (e) {
5587
- alert(`Add failed: ${e?.message || e}`);
5588
- }
5589
- });
5590
- searchInput2.addEventListener("input", scheduleReload);
5591
- const close = () => {
5592
- if (reloadDebounce)
5593
- window.clearTimeout(reloadDebounce);
5594
- backdrop.remove();
5595
- document.removeEventListener("keydown", onKey, true);
5596
- isOpen2 = false;
5597
- };
5598
- const onKey = (e) => {
5599
- if (e.key === "Escape") {
5600
- e.stopPropagation();
5601
- e.preventDefault();
5602
- close();
5603
- }
5604
- };
5605
- document.addEventListener("keydown", onKey, true);
5606
- panel.querySelector("#ab-close").addEventListener("click", close);
5607
- panel.querySelector('[data-action="close"]').addEventListener("click", close);
5608
- backdrop.addEventListener("mousedown", (e) => {
5609
- if (e.target === backdrop)
5610
- close();
5611
- });
5612
- await reload();
5613
- }
5614
- function escapeHtml5(s) {
5615
- return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
5616
- }
5617
- function escapeAttr(s) {
5618
- return escapeHtml5(s);
5619
- }
5620
- var isOpen2;
5621
- var init_address_book = __esm({
5622
- "client/components/address-book.js"() {
5623
- "use strict";
5624
- init_api_client();
5625
- isOpen2 = false;
5626
- }
5627
- });
5628
-
5629
5692
  // client/components/alarms.js
5630
5693
  var alarms_exports = {};
5631
5694
  __export(alarms_exports, {
@@ -9066,6 +9129,9 @@ document.addEventListener("mailx-share-intent", ((e) => {
9066
9129
  if (detail.subject) init.subject = detail.subject;
9067
9130
  if (detail.text) init.bodyHtml = `<p>${escapeHtml7(String(detail.text)).replace(/\n/g, "<br>")}</p>`;
9068
9131
  }
9132
+ if (Array.isArray(detail.attachments) && detail.attachments.length) {
9133
+ init.attachments = detail.attachments.filter((a) => a?.filename && a?.dataBase64);
9134
+ }
9069
9135
  sessionStorage.setItem("composeInit", JSON.stringify(init));
9070
9136
  showComposeOverlay(init.subject || "Compose");
9071
9137
  }));
@@ -11204,6 +11270,19 @@ optThreaded?.addEventListener("change", () => {
11204
11270
  localStorage.setItem("mailx-threaded", String(optThreaded.checked));
11205
11271
  reloadCurrentFolder();
11206
11272
  });
11273
+ var optDateSent = document.getElementById("opt-date-sent");
11274
+ var optDateReceived = document.getElementById("opt-date-received");
11275
+ (function initDateBasisRadios() {
11276
+ const basis = getDateBasis();
11277
+ if (optDateSent) optDateSent.checked = basis === "sent";
11278
+ if (optDateReceived) optDateReceived.checked = basis === "received";
11279
+ })();
11280
+ optDateSent?.addEventListener("change", () => {
11281
+ if (optDateSent.checked) setDateBasis("sent");
11282
+ });
11283
+ optDateReceived?.addEventListener("change", () => {
11284
+ if (optDateReceived.checked) setDateBasis("received");
11285
+ });
11207
11286
  function syncFilterFlaggedButton() {
11208
11287
  const btn = document.getElementById("btn-filter-flagged");
11209
11288
  if (!btn) return;