@bobfrankston/rmfmail 1.2.247 → 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.
- package/client/app.bundle.js +239 -50
- package/client/app.bundle.js.map +4 -4
- package/client/app.js +23 -36
- package/client/app.js.map +1 -1
- package/client/app.ts +17 -36
- package/client/components/folder-picker.js +3 -2
- package/client/components/folder-picker.js.map +1 -1
- package/client/components/folder-picker.ts +3 -2
- package/client/components/folder-tree.js +90 -12
- package/client/components/folder-tree.js.map +1 -1
- package/client/components/folder-tree.ts +89 -12
- package/client/components/message-list.js +35 -2
- package/client/components/message-list.js.map +1 -1
- package/client/components/message-list.ts +32 -2
- package/client/components/message-viewer.js +149 -11
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +139 -11
- package/client/compose/compose.bundle.js +11 -0
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/help/search-help.js +6 -1
- package/client/help/search-help.js.map +1 -1
- package/client/help/search-help.ts +6 -1
- package/client/lib/api-client.js +14 -0
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +9 -0
- package/client/lib/fold-text.js +18 -0
- package/client/lib/fold-text.js.map +1 -0
- package/client/lib/fold-text.ts +17 -0
- package/client/styles/components.css +23 -3
- package/package.json +1 -1
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-service/index.d.ts +16 -0
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +49 -1
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +48 -1
- package/packages/mailx-service/jsonrpc.js +2 -0
- package/packages/mailx-service/jsonrpc.js.map +1 -1
- package/packages/mailx-service/jsonrpc.ts +2 -0
- package/packages/mailx-service/package.json +1 -1
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/db.d.ts.map +1 -1
- package/packages/mailx-store/db.js +5 -2
- package/packages/mailx-store/db.js.map +1 -1
- package/packages/mailx-store/db.ts +5 -2
- package/packages/mailx-store/package.json +1 -1
- package/packages/mailx-store-web/package.json +1 -1
- package/packages/mailx-types/mailx-api.d.ts +7 -0
- package/packages/mailx-types/mailx-api.d.ts.map +1 -1
- package/packages/mailx-types/mailx-api.ts +4 -0
- package/packages/mailx-types/package.json +1 -1
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-16772 → node_modules.npmglobalize-stash-52956}/.package-lock.json +0 -0
package/client/app.bundle.js
CHANGED
|
@@ -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
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
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
|
-
|
|
1772
|
-
|
|
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
|
-
|
|
1838
|
+
attempts.push(`direct fetch: ${e?.message || e}`);
|
|
1775
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
|
+
}
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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.
|
|
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.
|
|
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()
|
|
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
|
|
4136
|
+
const text = foldText(row.textContent);
|
|
3997
4137
|
row.classList.toggle("filter-hidden", !text.includes(liveFilterText));
|
|
3998
4138
|
}
|
|
3999
4139
|
}
|
|
@@ -5094,6 +5234,17 @@ function appendMessages(body, accountId, items) {
|
|
|
5094
5234
|
row.attach(body);
|
|
5095
5235
|
}
|
|
5096
5236
|
}
|
|
5237
|
+
function formatSize2(bytes) {
|
|
5238
|
+
if (!bytes || bytes <= 0)
|
|
5239
|
+
return "";
|
|
5240
|
+
if (bytes < 1024)
|
|
5241
|
+
return "<1K";
|
|
5242
|
+
const kb = bytes / 1024;
|
|
5243
|
+
if (kb < 1024)
|
|
5244
|
+
return `${Math.round(kb)}K`;
|
|
5245
|
+
const mb = kb / 1024;
|
|
5246
|
+
return mb < 10 ? `${mb.toFixed(1)}M` : `${Math.round(mb)}M`;
|
|
5247
|
+
}
|
|
5097
5248
|
function formatDate(epochMs) {
|
|
5098
5249
|
const d = new Date(epochMs);
|
|
5099
5250
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -5119,6 +5270,7 @@ var init_message_list = __esm({
|
|
|
5119
5270
|
init_message_viewer();
|
|
5120
5271
|
init_context_menu();
|
|
5121
5272
|
init_folder_picker();
|
|
5273
|
+
init_fold_text();
|
|
5122
5274
|
init_mailx_types();
|
|
5123
5275
|
currentSpecialUse = "";
|
|
5124
5276
|
lastClickedRow = null;
|
|
@@ -5282,11 +5434,17 @@ var init_message_list = __esm({
|
|
|
5282
5434
|
const icons = document.createElement("span");
|
|
5283
5435
|
icons.className = "ml-status-icons";
|
|
5284
5436
|
renderStatusIcons(icons, msg);
|
|
5437
|
+
const size = document.createElement("span");
|
|
5438
|
+
size.className = "ml-size";
|
|
5439
|
+
size.textContent = formatSize2(msg.size);
|
|
5440
|
+
if (msg.size > 0)
|
|
5441
|
+
size.title = `${msg.size.toLocaleString()} bytes`;
|
|
5285
5442
|
const dateText = document.createElement("span");
|
|
5286
5443
|
dateText.className = "ml-date-text";
|
|
5287
5444
|
dateText.textContent = formatDate(effectiveDate(msg));
|
|
5288
5445
|
dateText.title = dateBasis === "received" ? "Received date" : "Sent date";
|
|
5289
5446
|
date.appendChild(icons);
|
|
5447
|
+
date.appendChild(size);
|
|
5290
5448
|
date.appendChild(dateText);
|
|
5291
5449
|
row.appendChild(avatar);
|
|
5292
5450
|
row.appendChild(flag);
|
|
@@ -7270,7 +7428,7 @@ decides which operators work.</p>
|
|
|
7270
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>
|
|
7271
7429
|
<tr><td><code>has:attachment</code></td><td>Only messages that carry a file attachment</td></tr>
|
|
7272
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>
|
|
7273
|
-
<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>
|
|
7274
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>
|
|
7275
7433
|
</table>
|
|
7276
7434
|
<p>Any remaining unqualified text is the free-text term.</p>
|
|
@@ -7310,6 +7468,11 @@ results only.</p>
|
|
|
7310
7468
|
<p>All three modes \u2014 local FTS5, IMAP server SEARCH, and <code>/regex/</code> \u2014
|
|
7311
7469
|
are <strong>case-insensitive</strong>. <code>Lunch</code>, <code>lunch</code>, and
|
|
7312
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>
|
|
7313
7476
|
|
|
7314
7477
|
<h3>Limitations</h3>
|
|
7315
7478
|
<ul>
|
|
@@ -7458,6 +7621,7 @@ function setActiveView(view, title) {
|
|
|
7458
7621
|
}
|
|
7459
7622
|
|
|
7460
7623
|
// client/components/folder-tree.js
|
|
7624
|
+
init_fold_text();
|
|
7461
7625
|
var onFolderSelect;
|
|
7462
7626
|
var onUnifiedInbox = null;
|
|
7463
7627
|
var selectedElement;
|
|
@@ -7477,6 +7641,29 @@ function collapseDragExpanded() {
|
|
|
7477
7641
|
dragAutoExpanded.clear();
|
|
7478
7642
|
return true;
|
|
7479
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
|
+
}
|
|
7480
7667
|
var expandState = JSON.parse(localStorage.getItem("mailx-folders-expanded") || "{}");
|
|
7481
7668
|
function saveExpandState() {
|
|
7482
7669
|
localStorage.setItem("mailx-folders-expanded", JSON.stringify(expandState));
|
|
@@ -7613,10 +7800,10 @@ function sortFolders(nodes) {
|
|
|
7613
7800
|
sortFolders(n.children);
|
|
7614
7801
|
}
|
|
7615
7802
|
}
|
|
7616
|
-
function renderNode(node, container, depth) {
|
|
7803
|
+
function renderNode(node, container, depth, forceExpand) {
|
|
7617
7804
|
const hasChildren = node.children.length > 0;
|
|
7618
7805
|
const expandKey = `${node.accountId}:${node.path}`;
|
|
7619
|
-
const isExpanded = expandState[expandKey] === true;
|
|
7806
|
+
const isExpanded = forceExpand?.has(expandKey) ? expandState[expandKey] !== false : expandState[expandKey] === true;
|
|
7620
7807
|
const folderEl = document.createElement("div");
|
|
7621
7808
|
folderEl.className = "ft-folder";
|
|
7622
7809
|
folderEl.dataset.accountId = node.accountId;
|
|
@@ -7970,7 +8157,7 @@ function renderNode(node, container, depth) {
|
|
|
7970
8157
|
container.appendChild(folderEl);
|
|
7971
8158
|
if (hasChildren && isExpanded) {
|
|
7972
8159
|
for (const child of node.children) {
|
|
7973
|
-
renderNode(child, container, depth + 1);
|
|
8160
|
+
renderNode(child, container, depth + 1, forceExpand);
|
|
7974
8161
|
}
|
|
7975
8162
|
}
|
|
7976
8163
|
}
|
|
@@ -8300,12 +8487,12 @@ async function loadFolderTree(container) {
|
|
|
8300
8487
|
const accountFolderData = await Promise.all(accounts.map(async (account) => {
|
|
8301
8488
|
const accountKey = `account:${account.id}`;
|
|
8302
8489
|
const accountExpanded = expandState[accountKey] !== false;
|
|
8303
|
-
const folders = accountExpanded ? await getFolders(account.id) : [];
|
|
8490
|
+
const folders = accountExpanded || folderFilter ? await getFolders(account.id) : [];
|
|
8304
8491
|
return { account, folders };
|
|
8305
8492
|
}));
|
|
8306
8493
|
const savedScroll = container.scrollTop;
|
|
8307
8494
|
const fragment = document.createDocumentFragment();
|
|
8308
|
-
if (accounts.length >= 1) {
|
|
8495
|
+
if (accounts.length >= 1 && (!folderFilter || foldText("All Inboxes").includes(folderFilter))) {
|
|
8309
8496
|
const unifiedEl = document.createElement("div");
|
|
8310
8497
|
unifiedEl.className = "ft-folder ft-unified";
|
|
8311
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";
|
|
@@ -8322,7 +8509,7 @@ async function loadFolderTree(container) {
|
|
|
8322
8509
|
});
|
|
8323
8510
|
fragment.appendChild(unifiedEl);
|
|
8324
8511
|
}
|
|
8325
|
-
if (lastOutboxTotal > 0) {
|
|
8512
|
+
if (lastOutboxTotal > 0 && (!folderFilter || foldText("Send-pending").includes(folderFilter))) {
|
|
8326
8513
|
const pendingEl = document.createElement("div");
|
|
8327
8514
|
pendingEl.className = "ft-folder ft-unified ft-send-pending";
|
|
8328
8515
|
pendingEl.id = "ft-send-pending";
|
|
@@ -8347,7 +8534,7 @@ async function loadFolderTree(container) {
|
|
|
8347
8534
|
const accountEl = document.createElement("div");
|
|
8348
8535
|
accountEl.className = "ft-account";
|
|
8349
8536
|
const accountKey = `account:${account.id}`;
|
|
8350
|
-
const accountExpanded = expandState[accountKey] !== false;
|
|
8537
|
+
const accountExpanded = folderFilter ? true : expandState[accountKey] !== false;
|
|
8351
8538
|
const header = document.createElement("div");
|
|
8352
8539
|
header.className = "ft-account-header";
|
|
8353
8540
|
const baseLabel = labelOf(account);
|
|
@@ -8355,7 +8542,7 @@ async function loadFolderTree(container) {
|
|
|
8355
8542
|
const disambiguator = isDup ? ` (${account.email || account.id})` : "";
|
|
8356
8543
|
header.textContent = `${accountExpanded ? "\u25BE" : "\u25B8"} ${baseLabel}${disambiguator}`;
|
|
8357
8544
|
header.addEventListener("click", () => {
|
|
8358
|
-
expandState[accountKey] =
|
|
8545
|
+
expandState[accountKey] = expandState[accountKey] === false;
|
|
8359
8546
|
saveExpandState();
|
|
8360
8547
|
const treeContainer = document.getElementById("folder-tree");
|
|
8361
8548
|
if (treeContainer)
|
|
@@ -8412,10 +8599,17 @@ async function loadFolderTree(container) {
|
|
|
8412
8599
|
showContextMenu(e.clientX, e.clientY, items, { editTarget: e.target });
|
|
8413
8600
|
});
|
|
8414
8601
|
accountEl.appendChild(header);
|
|
8602
|
+
let filteredOut = false;
|
|
8415
8603
|
if (accountExpanded && folders.length > 0) {
|
|
8416
8604
|
const delimiter = folders[0]?.delimiter || ".";
|
|
8417
|
-
|
|
8605
|
+
let tree = buildTree(folders, delimiter, account.id);
|
|
8418
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
|
+
}
|
|
8419
8613
|
const lowerCounts = /* @__PURE__ */ new Map();
|
|
8420
8614
|
for (const f of folders) {
|
|
8421
8615
|
const key = (f.path || "").toLowerCase();
|
|
@@ -8426,7 +8620,7 @@ async function loadFolderTree(container) {
|
|
|
8426
8620
|
if (c > 1)
|
|
8427
8621
|
duplicatePaths.add(k);
|
|
8428
8622
|
for (const node of tree) {
|
|
8429
|
-
renderNode(node, accountEl, 1);
|
|
8623
|
+
renderNode(node, accountEl, 1, forceExpand);
|
|
8430
8624
|
}
|
|
8431
8625
|
if (duplicatePaths.size > 0) {
|
|
8432
8626
|
accountEl.querySelectorAll(".ft-folder").forEach((el) => {
|
|
@@ -8437,8 +8631,17 @@ async function loadFolderTree(container) {
|
|
|
8437
8631
|
}
|
|
8438
8632
|
});
|
|
8439
8633
|
}
|
|
8634
|
+
} else if (folderFilter) {
|
|
8635
|
+
filteredOut = true;
|
|
8440
8636
|
}
|
|
8441
|
-
|
|
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);
|
|
8442
8645
|
}
|
|
8443
8646
|
container.replaceChildren(fragment);
|
|
8444
8647
|
container.scrollTop = savedScroll;
|
|
@@ -10600,36 +10803,22 @@ document.getElementById("search-server-too")?.addEventListener("change", rerunAc
|
|
|
10600
10803
|
document.getElementById("search-include-trash")?.addEventListener("change", rerunActiveSearch);
|
|
10601
10804
|
var ftFilterInput = document.getElementById("ft-filter-input");
|
|
10602
10805
|
if (ftFilterInput) {
|
|
10603
|
-
|
|
10604
|
-
|
|
10605
|
-
|
|
10606
|
-
if (
|
|
10607
|
-
|
|
10608
|
-
|
|
10609
|
-
|
|
10610
|
-
|
|
10611
|
-
|
|
10612
|
-
|
|
10613
|
-
|
|
10614
|
-
|
|
10615
|
-
for (const f of folders) {
|
|
10616
|
-
const name = f.querySelector(".ft-folder-name")?.textContent?.toLowerCase() || "";
|
|
10617
|
-
if (name.includes(query)) {
|
|
10618
|
-
f.classList.remove("ft-filter-hidden");
|
|
10619
|
-
const acct = f.closest(".ft-account");
|
|
10620
|
-
if (acct) acct.classList.remove("ft-filter-hidden");
|
|
10621
|
-
}
|
|
10622
|
-
}
|
|
10623
|
-
const unified = tree.querySelector(".ft-unified");
|
|
10624
|
-
if (unified) {
|
|
10625
|
-
const text = unified.textContent?.toLowerCase() || "";
|
|
10626
|
-
unified.classList.toggle("ft-filter-hidden", !text.includes(query));
|
|
10627
|
-
}
|
|
10628
|
-
});
|
|
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));
|
|
10629
10818
|
ftFilterInput.addEventListener("keydown", (e) => {
|
|
10630
10819
|
if (e.key === "Escape") {
|
|
10631
10820
|
ftFilterInput.value = "";
|
|
10632
|
-
|
|
10821
|
+
applyFolderFilter(true);
|
|
10633
10822
|
}
|
|
10634
10823
|
});
|
|
10635
10824
|
}
|