@bobfrankston/rmfmail 1.2.248 → 1.2.251
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/bin/check-bridge-contract.mjs +9 -1
- package/client/app.bundle.js +222 -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 +3 -2
- package/client/components/message-list.js.map +1 -1
- package/client/components/message-list.ts +3 -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/lib/mailxapi.js +12 -0
- package/client/styles/components.css +8 -3
- package/package.json +5 -5
- 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-63500 → node_modules.npmglobalize-stash-83100}/.package-lock.json +0 -0
|
@@ -9,6 +9,7 @@ import type { ListMessage } from "../lib/message-state.js";
|
|
|
9
9
|
import { showMessage as viewerShow, clearViewer as viewerClear } from "./message-viewer.js";
|
|
10
10
|
import { showContextMenu, type MenuItem } from "./context-menu.js";
|
|
11
11
|
import { pickFolder } from "./folder-picker.js";
|
|
12
|
+
import { foldText } from "../lib/fold-text.js";
|
|
12
13
|
import { seenOf, flaggedOf, draftOf, setSeen, setFlagged } from "@bobfrankston/mailx-types";
|
|
13
14
|
|
|
14
15
|
type MessageSelectHandler = (accountId: string, uid: number, folderId: number) => void;
|
|
@@ -43,7 +44,7 @@ let liveFilterText = "";
|
|
|
43
44
|
* it (background reloads resume). Lower-cased once here so the per-row test
|
|
44
45
|
* is a plain substring check. */
|
|
45
46
|
export function setLiveFilter(query: string): void {
|
|
46
|
-
liveFilterText = (query || "").trim()
|
|
47
|
+
liveFilterText = foldText((query || "").trim());
|
|
47
48
|
const body = document.getElementById("ml-body");
|
|
48
49
|
if (body) applyLiveFilter(body);
|
|
49
50
|
}
|
|
@@ -57,7 +58,7 @@ function applyLiveFilter(body: HTMLElement): void {
|
|
|
57
58
|
return;
|
|
58
59
|
}
|
|
59
60
|
for (const row of body.querySelectorAll(".ml-row")) {
|
|
60
|
-
const text = row.textContent
|
|
61
|
+
const text = foldText(row.textContent);
|
|
61
62
|
(row as HTMLElement).classList.toggle("filter-hidden", !text.includes(liveFilterText));
|
|
62
63
|
}
|
|
63
64
|
}
|
|
@@ -13,6 +13,46 @@ import { showContextMenu } from "./context-menu.js";
|
|
|
13
13
|
import { updateMessageFlags as stateUpdateFlags } from "../lib/message-state.js";
|
|
14
14
|
import { setRowSeen } from "./message-list.js";
|
|
15
15
|
import { setSeen, seenOf } from "@bobfrankston/mailx-types";
|
|
16
|
+
// ── "Copy image" bridge ──
|
|
17
|
+
// The preview iframe encodes the image and posts the bytes back; these track
|
|
18
|
+
// the outstanding request. Keyed by id because a user can hit Copy image on a
|
|
19
|
+
// second picture before the first encode finishes.
|
|
20
|
+
const pendingImageCopies = new Map();
|
|
21
|
+
let imageCopySeq = 0;
|
|
22
|
+
const IMAGE_COPY_TIMEOUT_MS = 8000;
|
|
23
|
+
window.addEventListener("message", (e) => {
|
|
24
|
+
const d = e.data;
|
|
25
|
+
if (!d || d.type !== "previewImageBytes")
|
|
26
|
+
return;
|
|
27
|
+
const resolve = pendingImageCopies.get(d.reqId);
|
|
28
|
+
if (!resolve)
|
|
29
|
+
return; // timed out already
|
|
30
|
+
pendingImageCopies.delete(d.reqId);
|
|
31
|
+
resolve({ blob: d.blob, error: d.error });
|
|
32
|
+
});
|
|
33
|
+
/** Ask the preview iframe to encode the image the user right-clicked. Resolves
|
|
34
|
+
* with a PNG blob, or an error string when the iframe can't read it (a remote
|
|
35
|
+
* image taints the canvas — caller falls back to the daemon). */
|
|
36
|
+
function requestImageFromPreview(sourceWindow, src) {
|
|
37
|
+
if (!sourceWindow)
|
|
38
|
+
return Promise.resolve({ error: "preview window is gone" });
|
|
39
|
+
const reqId = ++imageCopySeq;
|
|
40
|
+
return new Promise(resolve => {
|
|
41
|
+
const done = (r) => { clearTimeout(timer); resolve(r); };
|
|
42
|
+
const timer = setTimeout(() => {
|
|
43
|
+
pendingImageCopies.delete(reqId);
|
|
44
|
+
resolve({ error: "the preview did not answer in time" });
|
|
45
|
+
}, IMAGE_COPY_TIMEOUT_MS);
|
|
46
|
+
pendingImageCopies.set(reqId, done);
|
|
47
|
+
try {
|
|
48
|
+
sourceWindow.postMessage({ type: "previewCommand", cmd: "copyImage", reqId, src }, "*");
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
pendingImageCopies.delete(reqId);
|
|
52
|
+
done({ error: err?.message || String(err) });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
16
56
|
/** Currently displayed message (for reply/forward) */
|
|
17
57
|
let currentMessage = null;
|
|
18
58
|
let currentAccountId = "";
|
|
@@ -189,25 +229,77 @@ export function showPreviewBodyMenu(absX, absY, selectedText, sourceWindow, link
|
|
|
189
229
|
}
|
|
190
230
|
})();
|
|
191
231
|
items.push({ label: "Copy image", action: async () => {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
232
|
+
// Three ways in, because no single one covers every image a
|
|
233
|
+
// letter can carry (Bob 2026-08-11: right-click → copy just
|
|
234
|
+
// failed). In order of what works most often:
|
|
235
|
+
// 1. the preview iframe encodes its own already-decoded
|
|
236
|
+
// <img> — the only path that works for inline cid:/data:
|
|
237
|
+
// images, and it costs no network at all;
|
|
238
|
+
// 2. the parent fetches the src — fine for data: and for
|
|
239
|
+
// hosts that send CORS headers;
|
|
240
|
+
// 3. the daemon fetches it — remote images are cross-origin
|
|
241
|
+
// with no CORS headers, so the WebView can never read
|
|
242
|
+
// their bytes, but Node has no such restriction.
|
|
243
|
+
const attempts = [];
|
|
244
|
+
const writePng = async (png) => {
|
|
245
|
+
await navigator.clipboard.write([new ClipboardItem({ "image/png": png })]);
|
|
246
|
+
};
|
|
247
|
+
// Transcode to PNG via canvas — WebView2's clipboard image
|
|
248
|
+
// write is reliable for PNG; arbitrary source types (webp,
|
|
249
|
+
// gif, jpeg) may not round-trip directly.
|
|
250
|
+
const toPng = async (blob) => {
|
|
251
|
+
if (blob.type === "image/png")
|
|
252
|
+
return blob;
|
|
198
253
|
const bmp = await createImageBitmap(blob);
|
|
199
254
|
const canvas = document.createElement("canvas");
|
|
200
255
|
canvas.width = bmp.width;
|
|
201
256
|
canvas.height = bmp.height;
|
|
202
257
|
canvas.getContext("2d").drawImage(bmp, 0, 0);
|
|
203
|
-
|
|
204
|
-
|
|
258
|
+
return await new Promise((res, rej) => canvas.toBlob(b => b ? res(b) : rej(new Error("toBlob failed")), "image/png"));
|
|
259
|
+
};
|
|
260
|
+
// 1. Ask the preview to encode it.
|
|
261
|
+
const fromPreview = await requestImageFromPreview(sourceWindow, imgSrc);
|
|
262
|
+
if (fromPreview.blob) {
|
|
263
|
+
try {
|
|
264
|
+
await writePng(fromPreview.blob);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
catch (e) {
|
|
268
|
+
attempts.push(`clipboard write: ${e?.message || e}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
attempts.push(`preview: ${fromPreview.error}`);
|
|
273
|
+
}
|
|
274
|
+
// 2. Fetch it from here.
|
|
275
|
+
try {
|
|
276
|
+
const resp = await fetch(imgSrc);
|
|
277
|
+
if (!resp.ok)
|
|
278
|
+
throw new Error(`HTTP ${resp.status}`);
|
|
279
|
+
await writePng(await toPng(await resp.blob()));
|
|
280
|
+
return;
|
|
205
281
|
}
|
|
206
282
|
catch (e) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
283
|
+
attempts.push(`direct fetch: ${e?.message || e}`);
|
|
284
|
+
}
|
|
285
|
+
// 3. Let the daemon fetch it (remote images, no CORS).
|
|
286
|
+
if (/^https?:/i.test(imgSrc)) {
|
|
287
|
+
try {
|
|
288
|
+
const { fetchRemoteImage } = await import("../lib/api-client.js");
|
|
289
|
+
const r = await fetchRemoteImage(imgSrc);
|
|
290
|
+
if (!r?.base64)
|
|
291
|
+
throw new Error(r?.error || "no data returned");
|
|
292
|
+
const bytes = Uint8Array.from(atob(r.base64), c => c.charCodeAt(0));
|
|
293
|
+
await writePng(await toPng(new Blob([bytes], { type: r.contentType || "image/png" })));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
catch (e) {
|
|
297
|
+
attempts.push(`via rmfmail: ${e?.message || e}`);
|
|
298
|
+
}
|
|
210
299
|
}
|
|
300
|
+
// Name what was tried — a bare "couldn't copy" leaves nothing
|
|
301
|
+
// to act on, and these three failure modes need different fixes.
|
|
302
|
+
alert(`Couldn't copy that image.\n\n${attempts.map(a => `• ${a}`).join("\n")}`);
|
|
211
303
|
} }, { label: `Save image as "${guessImgName}"…`, action: () => {
|
|
212
304
|
const a = document.createElement("a");
|
|
213
305
|
a.href = imgSrc;
|
|
@@ -2372,6 +2464,9 @@ ${csp}
|
|
|
2372
2464
|
// dismissers it sees. The parent's debounced 500 ms show-timer still
|
|
2373
2465
|
// suppresses flicker.
|
|
2374
2466
|
var lastHoveredHref = "";
|
|
2467
|
+
// The <img> under the most recent right-click, kept so a later
|
|
2468
|
+
// "copyImage" command can encode that exact element (see below).
|
|
2469
|
+
var lastContextImg = null;
|
|
2375
2470
|
function postLinkHover(href, rect, mouse) {
|
|
2376
2471
|
// Only post on transitions to avoid spamming the parent on every
|
|
2377
2472
|
// mousemove inside a link.
|
|
@@ -2447,6 +2542,45 @@ ${csp}
|
|
|
2447
2542
|
s.addRange(r);
|
|
2448
2543
|
}
|
|
2449
2544
|
} catch (_) {}
|
|
2545
|
+
} else if (d.cmd === "copyImage") {
|
|
2546
|
+
// Encode the right-clicked image HERE and hand the parent the
|
|
2547
|
+
// bytes. The parent can't fetch it itself: an inline image is a
|
|
2548
|
+
// multi-megabyte data: URI it would have to re-download through
|
|
2549
|
+
// the message channel, and a remote one is cross-origin with no
|
|
2550
|
+
// CORS headers, so a parent-side fetch rejects. This img is
|
|
2551
|
+
// already decoded here, so a canvas draw needs no network at all.
|
|
2552
|
+
// (A remote image DOES taint the canvas — toBlob then throws and
|
|
2553
|
+
// the parent falls back to fetching it through the daemon.)
|
|
2554
|
+
var target = lastContextImg;
|
|
2555
|
+
if (!target && d.src) {
|
|
2556
|
+
var all = document.getElementsByTagName("img");
|
|
2557
|
+
for (var i = 0; i < all.length; i++) {
|
|
2558
|
+
if (all[i].currentSrc === d.src || all[i].src === d.src) { target = all[i]; break; }
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
if (!target) {
|
|
2562
|
+
window.parent.postMessage({ type: "previewImageBytes", reqId: d.reqId, error: "image element not found" }, "*");
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
try {
|
|
2566
|
+
var cv = document.createElement("canvas");
|
|
2567
|
+
cv.width = target.naturalWidth || target.width;
|
|
2568
|
+
cv.height = target.naturalHeight || target.height;
|
|
2569
|
+
if (!cv.width || !cv.height) throw new Error("image has no decoded size yet");
|
|
2570
|
+
cv.getContext("2d").drawImage(target, 0, 0);
|
|
2571
|
+
cv.toBlob(function (b) {
|
|
2572
|
+
if (!b) {
|
|
2573
|
+
window.parent.postMessage({ type: "previewImageBytes", reqId: d.reqId, error: "encode failed" }, "*");
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
window.parent.postMessage({ type: "previewImageBytes", reqId: d.reqId, blob: b }, "*");
|
|
2577
|
+
}, "image/png");
|
|
2578
|
+
} catch (err) {
|
|
2579
|
+
window.parent.postMessage({
|
|
2580
|
+
type: "previewImageBytes", reqId: d.reqId,
|
|
2581
|
+
error: (err && err.message) ? err.message : String(err)
|
|
2582
|
+
}, "*");
|
|
2583
|
+
}
|
|
2450
2584
|
}
|
|
2451
2585
|
});
|
|
2452
2586
|
|
|
@@ -2465,6 +2599,10 @@ ${csp}
|
|
|
2465
2599
|
try { rect = e.target.getBoundingClientRect ? e.target.getBoundingClientRect() : rect; } catch (_) {}
|
|
2466
2600
|
var a = e.target && e.target.closest ? e.target.closest("a[href]") : null;
|
|
2467
2601
|
var img = e.target && e.target.closest ? e.target.closest("img") : null;
|
|
2602
|
+
// Remember the element itself, not just its src — "Copy image" encodes
|
|
2603
|
+
// this exact <img>, and matching by src would pick the wrong one when
|
|
2604
|
+
// a letter repeats the same image (spacers, logos, tracking pixels).
|
|
2605
|
+
lastContextImg = img;
|
|
2468
2606
|
var sel = (window.getSelection && window.getSelection()) ? window.getSelection().toString() : "";
|
|
2469
2607
|
window.parent.postMessage({
|
|
2470
2608
|
type: "previewContextMenu",
|