@bobfrankston/rmfmail 1.2.248 → 1.2.250

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 (53) hide show
  1. package/client/app.bundle.js +222 -50
  2. package/client/app.bundle.js.map +4 -4
  3. package/client/app.js +23 -36
  4. package/client/app.js.map +1 -1
  5. package/client/app.ts +17 -36
  6. package/client/components/folder-picker.js +3 -2
  7. package/client/components/folder-picker.js.map +1 -1
  8. package/client/components/folder-picker.ts +3 -2
  9. package/client/components/folder-tree.js +90 -12
  10. package/client/components/folder-tree.js.map +1 -1
  11. package/client/components/folder-tree.ts +89 -12
  12. package/client/components/message-list.js +3 -2
  13. package/client/components/message-list.js.map +1 -1
  14. package/client/components/message-list.ts +3 -2
  15. package/client/components/message-viewer.js +149 -11
  16. package/client/components/message-viewer.js.map +1 -1
  17. package/client/components/message-viewer.ts +139 -11
  18. package/client/compose/compose.bundle.js +11 -0
  19. package/client/compose/compose.bundle.js.map +2 -2
  20. package/client/help/search-help.js +6 -1
  21. package/client/help/search-help.js.map +1 -1
  22. package/client/help/search-help.ts +6 -1
  23. package/client/lib/api-client.js +14 -0
  24. package/client/lib/api-client.js.map +1 -1
  25. package/client/lib/api-client.ts +9 -0
  26. package/client/lib/fold-text.js +18 -0
  27. package/client/lib/fold-text.js.map +1 -0
  28. package/client/lib/fold-text.ts +17 -0
  29. package/client/styles/components.css +8 -3
  30. package/package.json +1 -1
  31. package/packages/mailx-imap/package-lock.json +2 -2
  32. package/packages/mailx-imap/package.json +1 -1
  33. package/packages/mailx-service/index.d.ts +16 -0
  34. package/packages/mailx-service/index.d.ts.map +1 -1
  35. package/packages/mailx-service/index.js +49 -1
  36. package/packages/mailx-service/index.js.map +1 -1
  37. package/packages/mailx-service/index.ts +48 -1
  38. package/packages/mailx-service/jsonrpc.js +2 -0
  39. package/packages/mailx-service/jsonrpc.js.map +1 -1
  40. package/packages/mailx-service/jsonrpc.ts +2 -0
  41. package/packages/mailx-service/package.json +1 -1
  42. package/packages/mailx-settings/package.json +1 -1
  43. package/packages/mailx-store/db.d.ts.map +1 -1
  44. package/packages/mailx-store/db.js +5 -2
  45. package/packages/mailx-store/db.js.map +1 -1
  46. package/packages/mailx-store/db.ts +5 -2
  47. package/packages/mailx-store/package.json +1 -1
  48. package/packages/mailx-store-web/package.json +1 -1
  49. package/packages/mailx-types/mailx-api.d.ts +7 -0
  50. package/packages/mailx-types/mailx-api.d.ts.map +1 -1
  51. package/packages/mailx-types/mailx-api.ts +4 -0
  52. package/packages/mailx-types/package.json +1 -1
  53. /package/packages/mailx-imap/{node_modules.npmglobalize-stash-63500 → node_modules.npmglobalize-stash-52956}/.package-lock.json +0 -0
@@ -52,6 +52,7 @@ __export(api_client_exports, {
52
52
  drainStoreSync: () => drainStoreSync,
53
53
  emptyFolder: () => emptyFolder,
54
54
  fetchImageAsDataUri: () => fetchImageAsDataUri,
55
+ fetchRemoteImage: () => fetchRemoteImage,
55
56
  flagSenderOrDomain: () => flagSenderOrDomain,
56
57
  formatJsonc: () => formatJsonc,
57
58
  getAccounts: () => getAccounts,
@@ -614,6 +615,16 @@ function readConfigHelp(name) {
614
615
  function unsubscribeOneClick(url) {
615
616
  return ipc().unsubscribeOneClick?.(url);
616
617
  }
618
+ async function fetchRemoteImage(url) {
619
+ const fn = ipc().fetchRemoteImage;
620
+ if (!fn)
621
+ return { error: "this host can't fetch remote images" };
622
+ try {
623
+ return await fn(url);
624
+ } catch (e) {
625
+ return { error: e?.message || String(e) };
626
+ }
627
+ }
617
628
  function openInWord(editId, html, popoutId = "") {
618
629
  return ipc().openInWord?.(editId, html, popoutId) ?? Promise.resolve({ ok: false, path: "", opener: "none" });
619
630
  }
@@ -1163,6 +1174,16 @@ var init_context_menu = __esm({
1163
1174
  }
1164
1175
  });
1165
1176
 
1177
+ // client/lib/fold-text.js
1178
+ function foldText(s) {
1179
+ return (s || "").normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase();
1180
+ }
1181
+ var init_fold_text = __esm({
1182
+ "client/lib/fold-text.js"() {
1183
+ "use strict";
1184
+ }
1185
+ });
1186
+
1166
1187
  // client/lib/message-state.js
1167
1188
  var message_state_exports = {};
1168
1189
  __export(message_state_exports, {
@@ -1639,6 +1660,28 @@ __export(message_viewer_exports, {
1639
1660
  toggleFullscreenPreview: () => toggleFullscreenPreview,
1640
1661
  wrapHtmlBody: () => wrapHtmlBody
1641
1662
  });
1663
+ function requestImageFromPreview(sourceWindow, src) {
1664
+ if (!sourceWindow)
1665
+ return Promise.resolve({ error: "preview window is gone" });
1666
+ const reqId = ++imageCopySeq;
1667
+ return new Promise((resolve) => {
1668
+ const done = (r) => {
1669
+ clearTimeout(timer);
1670
+ resolve(r);
1671
+ };
1672
+ const timer = setTimeout(() => {
1673
+ pendingImageCopies.delete(reqId);
1674
+ resolve({ error: "the preview did not answer in time" });
1675
+ }, IMAGE_COPY_TIMEOUT_MS);
1676
+ pendingImageCopies.set(reqId, done);
1677
+ try {
1678
+ sourceWindow.postMessage({ type: "previewCommand", cmd: "copyImage", reqId, src }, "*");
1679
+ } catch (err) {
1680
+ pendingImageCopies.delete(reqId);
1681
+ done({ error: err?.message || String(err) });
1682
+ }
1683
+ });
1684
+ }
1642
1685
  function parsedCacheGet(accountId, folderId, uid) {
1643
1686
  if (folderId == null)
1644
1687
  return null;
@@ -1760,19 +1803,56 @@ function showPreviewBodyMenu(absX, absY, selectedText, sourceWindow, linkUrl, li
1760
1803
  }
1761
1804
  })();
1762
1805
  items.push({ label: "Copy image", action: async () => {
1763
- try {
1764
- const resp = await fetch(imgSrc);
1765
- const blob = await resp.blob();
1806
+ const attempts = [];
1807
+ const writePng = async (png) => {
1808
+ await navigator.clipboard.write([new ClipboardItem({ "image/png": png })]);
1809
+ };
1810
+ const toPng = async (blob) => {
1811
+ if (blob.type === "image/png")
1812
+ return blob;
1766
1813
  const bmp = await createImageBitmap(blob);
1767
1814
  const canvas = document.createElement("canvas");
1768
1815
  canvas.width = bmp.width;
1769
1816
  canvas.height = bmp.height;
1770
1817
  canvas.getContext("2d").drawImage(bmp, 0, 0);
1771
- const png = await new Promise((res, rej) => canvas.toBlob((b) => b ? res(b) : rej(new Error("toBlob failed")), "image/png"));
1772
- await navigator.clipboard.write([new ClipboardItem({ "image/png": png })]);
1818
+ return await new Promise((res, rej) => canvas.toBlob((b) => b ? res(b) : rej(new Error("toBlob failed")), "image/png"));
1819
+ };
1820
+ const fromPreview = await requestImageFromPreview(sourceWindow, imgSrc);
1821
+ if (fromPreview.blob) {
1822
+ try {
1823
+ await writePng(fromPreview.blob);
1824
+ return;
1825
+ } catch (e) {
1826
+ attempts.push(`clipboard write: ${e?.message || e}`);
1827
+ }
1828
+ } else {
1829
+ attempts.push(`preview: ${fromPreview.error}`);
1830
+ }
1831
+ try {
1832
+ const resp = await fetch(imgSrc);
1833
+ if (!resp.ok)
1834
+ throw new Error(`HTTP ${resp.status}`);
1835
+ await writePng(await toPng(await resp.blob()));
1836
+ return;
1773
1837
  } catch (e) {
1774
- alert(`Couldn't copy image: ${e?.message || e}${imgSrc.startsWith("cid:") ? " (inline cid: image \u2014 try Save instead, or right-click \u2192 save in the message)" : ""}`);
1838
+ attempts.push(`direct fetch: ${e?.message || e}`);
1839
+ }
1840
+ if (/^https?:/i.test(imgSrc)) {
1841
+ try {
1842
+ const { fetchRemoteImage: fetchRemoteImage2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
1843
+ const r = await fetchRemoteImage2(imgSrc);
1844
+ if (!r?.base64)
1845
+ throw new Error(r?.error || "no data returned");
1846
+ const bytes = Uint8Array.from(atob(r.base64), (c) => c.charCodeAt(0));
1847
+ await writePng(await toPng(new Blob([bytes], { type: r.contentType || "image/png" })));
1848
+ return;
1849
+ } catch (e) {
1850
+ attempts.push(`via rmfmail: ${e?.message || e}`);
1851
+ }
1775
1852
  }
1853
+ alert(`Couldn't copy that image.
1854
+
1855
+ ${attempts.map((a) => `\u2022 ${a}`).join("\n")}`);
1776
1856
  } }, { label: `Save image as "${guessImgName}"\u2026`, action: () => {
1777
1857
  const a = document.createElement("a");
1778
1858
  a.href = imgSrc;
@@ -3305,6 +3385,9 @@ ${csp}
3305
3385
  // dismissers it sees. The parent's debounced 500 ms show-timer still
3306
3386
  // suppresses flicker.
3307
3387
  var lastHoveredHref = "";
3388
+ // The <img> under the most recent right-click, kept so a later
3389
+ // "copyImage" command can encode that exact element (see below).
3390
+ var lastContextImg = null;
3308
3391
  function postLinkHover(href, rect, mouse) {
3309
3392
  // Only post on transitions to avoid spamming the parent on every
3310
3393
  // mousemove inside a link.
@@ -3380,6 +3463,45 @@ ${csp}
3380
3463
  s.addRange(r);
3381
3464
  }
3382
3465
  } catch (_) {}
3466
+ } else if (d.cmd === "copyImage") {
3467
+ // Encode the right-clicked image HERE and hand the parent the
3468
+ // bytes. The parent can't fetch it itself: an inline image is a
3469
+ // multi-megabyte data: URI it would have to re-download through
3470
+ // the message channel, and a remote one is cross-origin with no
3471
+ // CORS headers, so a parent-side fetch rejects. This img is
3472
+ // already decoded here, so a canvas draw needs no network at all.
3473
+ // (A remote image DOES taint the canvas \u2014 toBlob then throws and
3474
+ // the parent falls back to fetching it through the daemon.)
3475
+ var target = lastContextImg;
3476
+ if (!target && d.src) {
3477
+ var all = document.getElementsByTagName("img");
3478
+ for (var i = 0; i < all.length; i++) {
3479
+ if (all[i].currentSrc === d.src || all[i].src === d.src) { target = all[i]; break; }
3480
+ }
3481
+ }
3482
+ if (!target) {
3483
+ window.parent.postMessage({ type: "previewImageBytes", reqId: d.reqId, error: "image element not found" }, "*");
3484
+ return;
3485
+ }
3486
+ try {
3487
+ var cv = document.createElement("canvas");
3488
+ cv.width = target.naturalWidth || target.width;
3489
+ cv.height = target.naturalHeight || target.height;
3490
+ if (!cv.width || !cv.height) throw new Error("image has no decoded size yet");
3491
+ cv.getContext("2d").drawImage(target, 0, 0);
3492
+ cv.toBlob(function (b) {
3493
+ if (!b) {
3494
+ window.parent.postMessage({ type: "previewImageBytes", reqId: d.reqId, error: "encode failed" }, "*");
3495
+ return;
3496
+ }
3497
+ window.parent.postMessage({ type: "previewImageBytes", reqId: d.reqId, blob: b }, "*");
3498
+ }, "image/png");
3499
+ } catch (err) {
3500
+ window.parent.postMessage({
3501
+ type: "previewImageBytes", reqId: d.reqId,
3502
+ error: (err && err.message) ? err.message : String(err)
3503
+ }, "*");
3504
+ }
3383
3505
  }
3384
3506
  });
3385
3507
 
@@ -3398,6 +3520,10 @@ ${csp}
3398
3520
  try { rect = e.target.getBoundingClientRect ? e.target.getBoundingClientRect() : rect; } catch (_) {}
3399
3521
  var a = e.target && e.target.closest ? e.target.closest("a[href]") : null;
3400
3522
  var img = e.target && e.target.closest ? e.target.closest("img") : null;
3523
+ // Remember the element itself, not just its src \u2014 "Copy image" encodes
3524
+ // this exact <img>, and matching by src would pick the wrong one when
3525
+ // a letter repeats the same image (spacers, logos, tracking pixels).
3526
+ lastContextImg = img;
3401
3527
  var sel = (window.getSelection && window.getSelection()) ? window.getSelection().toString() : "";
3402
3528
  window.parent.postMessage({
3403
3529
  type: "previewContextMenu",
@@ -3723,7 +3849,7 @@ function spawnDesktopPopout(msg, accountId) {
3723
3849
  function escapeHtmlLocal(s) {
3724
3850
  return (s || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3725
3851
  }
3726
- var currentMessage, currentAccountId, dragOutUrls, showMessageGeneration, retryCount, lastEnvelope, PARSED_CACHE_LIMIT, parsedCache, sessionAllowedRemote, recentFetchErrors, WAIT_GIVE_UP_SECS, 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;
3852
+ var pendingImageCopies, imageCopySeq, IMAGE_COPY_TIMEOUT_MS, currentMessage, currentAccountId, dragOutUrls, showMessageGeneration, retryCount, lastEnvelope, PARSED_CACHE_LIMIT, parsedCache, sessionAllowedRemote, recentFetchErrors, WAIT_GIVE_UP_SECS, 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;
3727
3853
  var init_message_viewer = __esm({
3728
3854
  "client/components/message-viewer.js"() {
3729
3855
  "use strict";
@@ -3732,6 +3858,19 @@ var init_message_viewer = __esm({
3732
3858
  init_message_state();
3733
3859
  init_message_list();
3734
3860
  init_mailx_types();
3861
+ pendingImageCopies = /* @__PURE__ */ new Map();
3862
+ imageCopySeq = 0;
3863
+ IMAGE_COPY_TIMEOUT_MS = 8e3;
3864
+ window.addEventListener("message", (e) => {
3865
+ const d = e.data;
3866
+ if (!d || d.type !== "previewImageBytes")
3867
+ return;
3868
+ const resolve = pendingImageCopies.get(d.reqId);
3869
+ if (!resolve)
3870
+ return;
3871
+ pendingImageCopies.delete(d.reqId);
3872
+ resolve({ blob: d.blob, error: d.error });
3873
+ });
3735
3874
  currentMessage = null;
3736
3875
  currentAccountId = "";
3737
3876
  dragOutUrls = [];
@@ -3909,7 +4048,7 @@ function pickFolder(accountId, opts) {
3909
4048
  const targets = folders.filter((f) => !excluded.has(f.id)).filter((f) => f.specialUse !== "outbox").sort((a, b) => a.path.localeCompare(b.path));
3910
4049
  function render2(filter) {
3911
4050
  listEl.innerHTML = "";
3912
- const lc = filter.toLowerCase().trim();
4051
+ const lc = foldText(filter.trim());
3913
4052
  let firstMatchSet = false;
3914
4053
  for (const f of targets) {
3915
4054
  const row = document.createElement("div");
@@ -3922,7 +4061,7 @@ function pickFolder(accountId, opts) {
3922
4061
  tag.textContent = f.specialUse || "";
3923
4062
  row.appendChild(name);
3924
4063
  row.appendChild(tag);
3925
- const matches = !lc || f.path.toLowerCase().includes(lc);
4064
+ const matches = !lc || foldText(f.path).includes(lc) || foldText(f.name).includes(lc);
3926
4065
  if (matches) {
3927
4066
  row.classList.add("match");
3928
4067
  if (!firstMatchSet) {
@@ -3949,6 +4088,7 @@ var init_folder_picker = __esm({
3949
4088
  "client/components/folder-picker.js"() {
3950
4089
  "use strict";
3951
4090
  init_api_client();
4091
+ init_fold_text();
3952
4092
  }
3953
4093
  });
3954
4094
 
@@ -3980,7 +4120,7 @@ __export(message_list_exports, {
3980
4120
  showThreadPopup: () => showThreadPopup
3981
4121
  });
3982
4122
  function setLiveFilter(query) {
3983
- liveFilterText = (query || "").trim().toLowerCase();
4123
+ liveFilterText = foldText((query || "").trim());
3984
4124
  const body = document.getElementById("ml-body");
3985
4125
  if (body)
3986
4126
  applyLiveFilter(body);
@@ -3993,7 +4133,7 @@ function applyLiveFilter(body) {
3993
4133
  return;
3994
4134
  }
3995
4135
  for (const row of body.querySelectorAll(".ml-row")) {
3996
- const text = row.textContent?.toLowerCase() || "";
4136
+ const text = foldText(row.textContent);
3997
4137
  row.classList.toggle("filter-hidden", !text.includes(liveFilterText));
3998
4138
  }
3999
4139
  }
@@ -5130,6 +5270,7 @@ var init_message_list = __esm({
5130
5270
  init_message_viewer();
5131
5271
  init_context_menu();
5132
5272
  init_folder_picker();
5273
+ init_fold_text();
5133
5274
  init_mailx_types();
5134
5275
  currentSpecialUse = "";
5135
5276
  lastClickedRow = null;
@@ -7287,7 +7428,7 @@ decides which operators work.</p>
7287
7428
  <tr><td><code>after:1w</code> / <code>before:1m</code></td><td>Newer / older than \u2014 <code>d</code>, <code>w</code>, <code>m</code>, <code>y</code>, a date, <code>today</code>, <code>yesterday</code></td></tr>
7288
7429
  <tr><td><code>has:attachment</code></td><td>Only messages that carry a file attachment</td></tr>
7289
7430
  <tr><td><code>is:unread</code></td><td>Also <code>is:flagged</code>, <code>is:read</code>, <code>is:answered</code>, <code>is:draft</code></td></tr>
7290
- <tr><td><code>folder:sent</code></td><td>Restrict to folders whose name contains the term</td></tr>
7431
+ <tr><td><code>folder:sent</code></td><td>Restrict to folders whose <em>name or path</em> contains the term \u2014 so <code>folder:rmf</code> and <code>folder:lists/rmf</code> both work</td></tr>
7291
7432
  <tr><td><code>/regex/</code></td><td>Client-side regex over the currently-visible rows. Local only \u2014 never sent to the server.</td></tr>
7292
7433
  </table>
7293
7434
  <p>Any remaining unqualified text is the free-text term.</p>
@@ -7327,6 +7468,11 @@ results only.</p>
7327
7468
  <p>All three modes \u2014 local FTS5, IMAP server SEARCH, and <code>/regex/</code> \u2014
7328
7469
  are <strong>case-insensitive</strong>. <code>Lunch</code>, <code>lunch</code>, and
7329
7470
  <code>LUNCH</code> match the same messages. There is no case-sensitive mode.</p>
7471
+ <p>The sidebar <strong>Find folder</strong> box and the <strong>Move to folder</strong>
7472
+ picker go further: they also ignore accents, so <code>archiv</code> finds
7473
+ <code>\xC4rchiv</code>. Find folder searches every account's whole folder list \u2014
7474
+ collapsed folders and collapsed accounts included \u2014 and opens the tree to
7475
+ whatever it finds.</p>
7330
7476
 
7331
7477
  <h3>Limitations</h3>
7332
7478
  <ul>
@@ -7475,6 +7621,7 @@ function setActiveView(view, title) {
7475
7621
  }
7476
7622
 
7477
7623
  // client/components/folder-tree.js
7624
+ init_fold_text();
7478
7625
  var onFolderSelect;
7479
7626
  var onUnifiedInbox = null;
7480
7627
  var selectedElement;
@@ -7494,6 +7641,29 @@ function collapseDragExpanded() {
7494
7641
  dragAutoExpanded.clear();
7495
7642
  return true;
7496
7643
  }
7644
+ var folderFilter = "";
7645
+ function setFolderFilter(query) {
7646
+ const next = foldText(query.trim());
7647
+ if (next === folderFilter)
7648
+ return;
7649
+ folderFilter = next;
7650
+ const container = document.getElementById("folder-tree");
7651
+ if (container)
7652
+ loadFolderTree(container);
7653
+ }
7654
+ function pruneToMatches(nodes, q, forceExpand) {
7655
+ const kept = [];
7656
+ for (const n of nodes) {
7657
+ const selfMatch = foldText(n.name).includes(q) || foldText(n.path).includes(q);
7658
+ const keptChildren = pruneToMatches(n.children, q, forceExpand);
7659
+ if (!selfMatch && keptChildren.length === 0)
7660
+ continue;
7661
+ kept.push({ ...n, children: selfMatch ? n.children : keptChildren });
7662
+ if (keptChildren.length > 0)
7663
+ forceExpand.add(`${n.accountId}:${n.path}`);
7664
+ }
7665
+ return kept;
7666
+ }
7497
7667
  var expandState = JSON.parse(localStorage.getItem("mailx-folders-expanded") || "{}");
7498
7668
  function saveExpandState() {
7499
7669
  localStorage.setItem("mailx-folders-expanded", JSON.stringify(expandState));
@@ -7630,10 +7800,10 @@ function sortFolders(nodes) {
7630
7800
  sortFolders(n.children);
7631
7801
  }
7632
7802
  }
7633
- function renderNode(node, container, depth) {
7803
+ function renderNode(node, container, depth, forceExpand) {
7634
7804
  const hasChildren = node.children.length > 0;
7635
7805
  const expandKey = `${node.accountId}:${node.path}`;
7636
- const isExpanded = expandState[expandKey] === true;
7806
+ const isExpanded = forceExpand?.has(expandKey) ? expandState[expandKey] !== false : expandState[expandKey] === true;
7637
7807
  const folderEl = document.createElement("div");
7638
7808
  folderEl.className = "ft-folder";
7639
7809
  folderEl.dataset.accountId = node.accountId;
@@ -7987,7 +8157,7 @@ function renderNode(node, container, depth) {
7987
8157
  container.appendChild(folderEl);
7988
8158
  if (hasChildren && isExpanded) {
7989
8159
  for (const child of node.children) {
7990
- renderNode(child, container, depth + 1);
8160
+ renderNode(child, container, depth + 1, forceExpand);
7991
8161
  }
7992
8162
  }
7993
8163
  }
@@ -8317,12 +8487,12 @@ async function loadFolderTree(container) {
8317
8487
  const accountFolderData = await Promise.all(accounts.map(async (account) => {
8318
8488
  const accountKey = `account:${account.id}`;
8319
8489
  const accountExpanded = expandState[accountKey] !== false;
8320
- const folders = accountExpanded ? await getFolders(account.id) : [];
8490
+ const folders = accountExpanded || folderFilter ? await getFolders(account.id) : [];
8321
8491
  return { account, folders };
8322
8492
  }));
8323
8493
  const savedScroll = container.scrollTop;
8324
8494
  const fragment = document.createDocumentFragment();
8325
- if (accounts.length >= 1) {
8495
+ if (accounts.length >= 1 && (!folderFilter || foldText("All Inboxes").includes(folderFilter))) {
8326
8496
  const unifiedEl = document.createElement("div");
8327
8497
  unifiedEl.className = "ft-folder ft-unified";
8328
8498
  unifiedEl.title = accounts.length > 1 ? "Merged inbox view of all accounts \u2014 click to see messages from every account's INBOX sorted by date" : "Inbox view across all your accounts";
@@ -8339,7 +8509,7 @@ async function loadFolderTree(container) {
8339
8509
  });
8340
8510
  fragment.appendChild(unifiedEl);
8341
8511
  }
8342
- if (lastOutboxTotal > 0) {
8512
+ if (lastOutboxTotal > 0 && (!folderFilter || foldText("Send-pending").includes(folderFilter))) {
8343
8513
  const pendingEl = document.createElement("div");
8344
8514
  pendingEl.className = "ft-folder ft-unified ft-send-pending";
8345
8515
  pendingEl.id = "ft-send-pending";
@@ -8364,7 +8534,7 @@ async function loadFolderTree(container) {
8364
8534
  const accountEl = document.createElement("div");
8365
8535
  accountEl.className = "ft-account";
8366
8536
  const accountKey = `account:${account.id}`;
8367
- const accountExpanded = expandState[accountKey] !== false;
8537
+ const accountExpanded = folderFilter ? true : expandState[accountKey] !== false;
8368
8538
  const header = document.createElement("div");
8369
8539
  header.className = "ft-account-header";
8370
8540
  const baseLabel = labelOf(account);
@@ -8372,7 +8542,7 @@ async function loadFolderTree(container) {
8372
8542
  const disambiguator = isDup ? ` (${account.email || account.id})` : "";
8373
8543
  header.textContent = `${accountExpanded ? "\u25BE" : "\u25B8"} ${baseLabel}${disambiguator}`;
8374
8544
  header.addEventListener("click", () => {
8375
- expandState[accountKey] = !accountExpanded;
8545
+ expandState[accountKey] = expandState[accountKey] === false;
8376
8546
  saveExpandState();
8377
8547
  const treeContainer = document.getElementById("folder-tree");
8378
8548
  if (treeContainer)
@@ -8429,10 +8599,17 @@ async function loadFolderTree(container) {
8429
8599
  showContextMenu(e.clientX, e.clientY, items, { editTarget: e.target });
8430
8600
  });
8431
8601
  accountEl.appendChild(header);
8602
+ let filteredOut = false;
8432
8603
  if (accountExpanded && folders.length > 0) {
8433
8604
  const delimiter = folders[0]?.delimiter || ".";
8434
- const tree = buildTree(folders, delimiter, account.id);
8605
+ let tree = buildTree(folders, delimiter, account.id);
8435
8606
  sortFolders(tree);
8607
+ const forceExpand = /* @__PURE__ */ new Set();
8608
+ if (folderFilter && !foldText(`${baseLabel} ${account.email || ""} ${account.id}`).includes(folderFilter)) {
8609
+ tree = pruneToMatches(tree, folderFilter, forceExpand);
8610
+ if (tree.length === 0)
8611
+ filteredOut = true;
8612
+ }
8436
8613
  const lowerCounts = /* @__PURE__ */ new Map();
8437
8614
  for (const f of folders) {
8438
8615
  const key = (f.path || "").toLowerCase();
@@ -8443,7 +8620,7 @@ async function loadFolderTree(container) {
8443
8620
  if (c > 1)
8444
8621
  duplicatePaths.add(k);
8445
8622
  for (const node of tree) {
8446
- renderNode(node, accountEl, 1);
8623
+ renderNode(node, accountEl, 1, forceExpand);
8447
8624
  }
8448
8625
  if (duplicatePaths.size > 0) {
8449
8626
  accountEl.querySelectorAll(".ft-folder").forEach((el) => {
@@ -8454,8 +8631,17 @@ async function loadFolderTree(container) {
8454
8631
  }
8455
8632
  });
8456
8633
  }
8634
+ } else if (folderFilter) {
8635
+ filteredOut = true;
8457
8636
  }
8458
- fragment.appendChild(accountEl);
8637
+ if (!filteredOut)
8638
+ fragment.appendChild(accountEl);
8639
+ }
8640
+ if (folderFilter && !fragment.querySelector(".ft-folder")) {
8641
+ const empty = document.createElement("div");
8642
+ empty.className = "ft-filter-empty";
8643
+ empty.textContent = `No folder matches "${document.getElementById("ft-filter-input")?.value || folderFilter}"`;
8644
+ fragment.appendChild(empty);
8459
8645
  }
8460
8646
  container.replaceChildren(fragment);
8461
8647
  container.scrollTop = savedScroll;
@@ -10617,36 +10803,22 @@ document.getElementById("search-server-too")?.addEventListener("change", rerunAc
10617
10803
  document.getElementById("search-include-trash")?.addEventListener("change", rerunActiveSearch);
10618
10804
  var ftFilterInput = document.getElementById("ft-filter-input");
10619
10805
  if (ftFilterInput) {
10620
- ftFilterInput.addEventListener("input", () => {
10621
- const query = ftFilterInput.value.toLowerCase();
10622
- const tree = document.getElementById("folder-tree");
10623
- if (!tree) return;
10624
- if (!query) {
10625
- tree.querySelectorAll(".ft-filter-hidden").forEach((el) => el.classList.remove("ft-filter-hidden"));
10626
- return;
10627
- }
10628
- const folders = tree.querySelectorAll(".ft-folder");
10629
- const accounts = tree.querySelectorAll(".ft-account");
10630
- for (const acct of accounts) acct.classList.add("ft-filter-hidden");
10631
- for (const f of folders) f.classList.add("ft-filter-hidden");
10632
- for (const f of folders) {
10633
- const name = f.querySelector(".ft-folder-name")?.textContent?.toLowerCase() || "";
10634
- if (name.includes(query)) {
10635
- f.classList.remove("ft-filter-hidden");
10636
- const acct = f.closest(".ft-account");
10637
- if (acct) acct.classList.remove("ft-filter-hidden");
10638
- }
10639
- }
10640
- const unified = tree.querySelector(".ft-unified");
10641
- if (unified) {
10642
- const text = unified.textContent?.toLowerCase() || "";
10643
- unified.classList.toggle("ft-filter-hidden", !text.includes(query));
10644
- }
10645
- });
10806
+ let ftFilterTimer = null;
10807
+ const FT_FILTER_DEBOUNCE_MS = 120;
10808
+ const applyFolderFilter = (immediate) => {
10809
+ if (ftFilterTimer) {
10810
+ clearTimeout(ftFilterTimer);
10811
+ ftFilterTimer = null;
10812
+ }
10813
+ const run = () => setFolderFilter(ftFilterInput.value);
10814
+ if (immediate) run();
10815
+ else ftFilterTimer = setTimeout(run, FT_FILTER_DEBOUNCE_MS);
10816
+ };
10817
+ ftFilterInput.addEventListener("input", () => applyFolderFilter(false));
10646
10818
  ftFilterInput.addEventListener("keydown", (e) => {
10647
10819
  if (e.key === "Escape") {
10648
10820
  ftFilterInput.value = "";
10649
- ftFilterInput.dispatchEvent(new Event("input"));
10821
+ applyFolderFilter(true);
10650
10822
  }
10651
10823
  });
10652
10824
  }