@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.
- 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/styles/components.css +8 -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-63500 → node_modules.npmglobalize-stash-52956}/.package-lock.json +0 -0
|
@@ -17,6 +17,41 @@ import { updateMessageFlags as stateUpdateFlags } from "../lib/message-state.js"
|
|
|
17
17
|
import { setRowSeen } from "./message-list.js";
|
|
18
18
|
import { setSeen, seenOf } from "@bobfrankston/mailx-types";
|
|
19
19
|
|
|
20
|
+
// ── "Copy image" bridge ──
|
|
21
|
+
// The preview iframe encodes the image and posts the bytes back; these track
|
|
22
|
+
// the outstanding request. Keyed by id because a user can hit Copy image on a
|
|
23
|
+
// second picture before the first encode finishes.
|
|
24
|
+
const pendingImageCopies = new Map<number, (r: { blob?: Blob; error?: string }) => void>();
|
|
25
|
+
let imageCopySeq = 0;
|
|
26
|
+
const IMAGE_COPY_TIMEOUT_MS = 8000;
|
|
27
|
+
|
|
28
|
+
window.addEventListener("message", (e) => {
|
|
29
|
+
const d: any = e.data;
|
|
30
|
+
if (!d || d.type !== "previewImageBytes") return;
|
|
31
|
+
const resolve = pendingImageCopies.get(d.reqId);
|
|
32
|
+
if (!resolve) return; // timed out already
|
|
33
|
+
pendingImageCopies.delete(d.reqId);
|
|
34
|
+
resolve({ blob: d.blob, error: d.error });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/** Ask the preview iframe to encode the image the user right-clicked. Resolves
|
|
38
|
+
* with a PNG blob, or an error string when the iframe can't read it (a remote
|
|
39
|
+
* image taints the canvas — caller falls back to the daemon). */
|
|
40
|
+
function requestImageFromPreview(sourceWindow: Window | null, src: string): Promise<{ blob?: Blob; error?: string }> {
|
|
41
|
+
if (!sourceWindow) return Promise.resolve({ error: "preview window is gone" });
|
|
42
|
+
const reqId = ++imageCopySeq;
|
|
43
|
+
return new Promise(resolve => {
|
|
44
|
+
const done = (r: { blob?: Blob; error?: string }): void => { clearTimeout(timer); resolve(r); };
|
|
45
|
+
const timer = setTimeout(() => {
|
|
46
|
+
pendingImageCopies.delete(reqId);
|
|
47
|
+
resolve({ error: "the preview did not answer in time" });
|
|
48
|
+
}, IMAGE_COPY_TIMEOUT_MS);
|
|
49
|
+
pendingImageCopies.set(reqId, done);
|
|
50
|
+
try { sourceWindow.postMessage({ type: "previewCommand", cmd: "copyImage", reqId, src }, "*"); }
|
|
51
|
+
catch (err: any) { pendingImageCopies.delete(reqId); done({ error: err?.message || String(err) }); }
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
20
55
|
/** Currently displayed message (for reply/forward) */
|
|
21
56
|
let currentMessage: any = null;
|
|
22
57
|
let currentAccountId: string = "";
|
|
@@ -165,24 +200,71 @@ export function showPreviewBodyMenu(absX: number, absY: number, selectedText: st
|
|
|
165
200
|
})();
|
|
166
201
|
items.push(
|
|
167
202
|
{ label: "Copy image", action: async () => {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
203
|
+
// Three ways in, because no single one covers every image a
|
|
204
|
+
// letter can carry (Bob 2026-08-11: right-click → copy just
|
|
205
|
+
// failed). In order of what works most often:
|
|
206
|
+
// 1. the preview iframe encodes its own already-decoded
|
|
207
|
+
// <img> — the only path that works for inline cid:/data:
|
|
208
|
+
// images, and it costs no network at all;
|
|
209
|
+
// 2. the parent fetches the src — fine for data: and for
|
|
210
|
+
// hosts that send CORS headers;
|
|
211
|
+
// 3. the daemon fetches it — remote images are cross-origin
|
|
212
|
+
// with no CORS headers, so the WebView can never read
|
|
213
|
+
// their bytes, but Node has no such restriction.
|
|
214
|
+
const attempts: string[] = [];
|
|
215
|
+
|
|
216
|
+
const writePng = async (png: Blob): Promise<void> => {
|
|
217
|
+
await navigator.clipboard.write([new ClipboardItem({ "image/png": png })]);
|
|
218
|
+
};
|
|
219
|
+
// Transcode to PNG via canvas — WebView2's clipboard image
|
|
220
|
+
// write is reliable for PNG; arbitrary source types (webp,
|
|
221
|
+
// gif, jpeg) may not round-trip directly.
|
|
222
|
+
const toPng = async (blob: Blob): Promise<Blob> => {
|
|
223
|
+
if (blob.type === "image/png") return blob;
|
|
174
224
|
const bmp = await createImageBitmap(blob);
|
|
175
225
|
const canvas = document.createElement("canvas");
|
|
176
226
|
canvas.width = bmp.width; canvas.height = bmp.height;
|
|
177
227
|
canvas.getContext("2d")!.drawImage(bmp, 0, 0);
|
|
178
|
-
|
|
228
|
+
return await new Promise((res, rej) =>
|
|
179
229
|
canvas.toBlob(b => b ? res(b) : rej(new Error("toBlob failed")), "image/png"));
|
|
180
|
-
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// 1. Ask the preview to encode it.
|
|
233
|
+
const fromPreview = await requestImageFromPreview(sourceWindow, imgSrc);
|
|
234
|
+
if (fromPreview.blob) {
|
|
235
|
+
try { await writePng(fromPreview.blob); return; }
|
|
236
|
+
catch (e: any) { attempts.push(`clipboard write: ${e?.message || e}`); }
|
|
237
|
+
} else {
|
|
238
|
+
attempts.push(`preview: ${fromPreview.error}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 2. Fetch it from here.
|
|
242
|
+
try {
|
|
243
|
+
const resp = await fetch(imgSrc);
|
|
244
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
245
|
+
await writePng(await toPng(await resp.blob()));
|
|
246
|
+
return;
|
|
181
247
|
} catch (e: any) {
|
|
182
|
-
|
|
183
|
-
// context — surface rather than fail silently.
|
|
184
|
-
alert(`Couldn't copy image: ${e?.message || e}${imgSrc.startsWith("cid:") ? " (inline cid: image — try Save instead, or right-click → save in the message)" : ""}`);
|
|
248
|
+
attempts.push(`direct fetch: ${e?.message || e}`);
|
|
185
249
|
}
|
|
250
|
+
|
|
251
|
+
// 3. Let the daemon fetch it (remote images, no CORS).
|
|
252
|
+
if (/^https?:/i.test(imgSrc)) {
|
|
253
|
+
try {
|
|
254
|
+
const { fetchRemoteImage } = await import("../lib/api-client.js");
|
|
255
|
+
const r = await fetchRemoteImage(imgSrc);
|
|
256
|
+
if (!r?.base64) throw new Error(r?.error || "no data returned");
|
|
257
|
+
const bytes = Uint8Array.from(atob(r.base64), c => c.charCodeAt(0));
|
|
258
|
+
await writePng(await toPng(new Blob([bytes], { type: r.contentType || "image/png" })));
|
|
259
|
+
return;
|
|
260
|
+
} catch (e: any) {
|
|
261
|
+
attempts.push(`via rmfmail: ${e?.message || e}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Name what was tried — a bare "couldn't copy" leaves nothing
|
|
266
|
+
// to act on, and these three failure modes need different fixes.
|
|
267
|
+
alert(`Couldn't copy that image.\n\n${attempts.map(a => `• ${a}`).join("\n")}`);
|
|
186
268
|
} },
|
|
187
269
|
{ label: `Save image as "${guessImgName}"…`, action: () => {
|
|
188
270
|
const a = document.createElement("a"); a.href = imgSrc;
|
|
@@ -2219,6 +2301,9 @@ ${csp}
|
|
|
2219
2301
|
// dismissers it sees. The parent's debounced 500 ms show-timer still
|
|
2220
2302
|
// suppresses flicker.
|
|
2221
2303
|
var lastHoveredHref = "";
|
|
2304
|
+
// The <img> under the most recent right-click, kept so a later
|
|
2305
|
+
// "copyImage" command can encode that exact element (see below).
|
|
2306
|
+
var lastContextImg = null;
|
|
2222
2307
|
function postLinkHover(href, rect, mouse) {
|
|
2223
2308
|
// Only post on transitions to avoid spamming the parent on every
|
|
2224
2309
|
// mousemove inside a link.
|
|
@@ -2294,6 +2379,45 @@ ${csp}
|
|
|
2294
2379
|
s.addRange(r);
|
|
2295
2380
|
}
|
|
2296
2381
|
} catch (_) {}
|
|
2382
|
+
} else if (d.cmd === "copyImage") {
|
|
2383
|
+
// Encode the right-clicked image HERE and hand the parent the
|
|
2384
|
+
// bytes. The parent can't fetch it itself: an inline image is a
|
|
2385
|
+
// multi-megabyte data: URI it would have to re-download through
|
|
2386
|
+
// the message channel, and a remote one is cross-origin with no
|
|
2387
|
+
// CORS headers, so a parent-side fetch rejects. This img is
|
|
2388
|
+
// already decoded here, so a canvas draw needs no network at all.
|
|
2389
|
+
// (A remote image DOES taint the canvas — toBlob then throws and
|
|
2390
|
+
// the parent falls back to fetching it through the daemon.)
|
|
2391
|
+
var target = lastContextImg;
|
|
2392
|
+
if (!target && d.src) {
|
|
2393
|
+
var all = document.getElementsByTagName("img");
|
|
2394
|
+
for (var i = 0; i < all.length; i++) {
|
|
2395
|
+
if (all[i].currentSrc === d.src || all[i].src === d.src) { target = all[i]; break; }
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
if (!target) {
|
|
2399
|
+
window.parent.postMessage({ type: "previewImageBytes", reqId: d.reqId, error: "image element not found" }, "*");
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
try {
|
|
2403
|
+
var cv = document.createElement("canvas");
|
|
2404
|
+
cv.width = target.naturalWidth || target.width;
|
|
2405
|
+
cv.height = target.naturalHeight || target.height;
|
|
2406
|
+
if (!cv.width || !cv.height) throw new Error("image has no decoded size yet");
|
|
2407
|
+
cv.getContext("2d").drawImage(target, 0, 0);
|
|
2408
|
+
cv.toBlob(function (b) {
|
|
2409
|
+
if (!b) {
|
|
2410
|
+
window.parent.postMessage({ type: "previewImageBytes", reqId: d.reqId, error: "encode failed" }, "*");
|
|
2411
|
+
return;
|
|
2412
|
+
}
|
|
2413
|
+
window.parent.postMessage({ type: "previewImageBytes", reqId: d.reqId, blob: b }, "*");
|
|
2414
|
+
}, "image/png");
|
|
2415
|
+
} catch (err) {
|
|
2416
|
+
window.parent.postMessage({
|
|
2417
|
+
type: "previewImageBytes", reqId: d.reqId,
|
|
2418
|
+
error: (err && err.message) ? err.message : String(err)
|
|
2419
|
+
}, "*");
|
|
2420
|
+
}
|
|
2297
2421
|
}
|
|
2298
2422
|
});
|
|
2299
2423
|
|
|
@@ -2312,6 +2436,10 @@ ${csp}
|
|
|
2312
2436
|
try { rect = e.target.getBoundingClientRect ? e.target.getBoundingClientRect() : rect; } catch (_) {}
|
|
2313
2437
|
var a = e.target && e.target.closest ? e.target.closest("a[href]") : null;
|
|
2314
2438
|
var img = e.target && e.target.closest ? e.target.closest("img") : null;
|
|
2439
|
+
// Remember the element itself, not just its src — "Copy image" encodes
|
|
2440
|
+
// this exact <img>, and matching by src would pick the wrong one when
|
|
2441
|
+
// a letter repeats the same image (spacers, logos, tracking pixels).
|
|
2442
|
+
lastContextImg = img;
|
|
2315
2443
|
var sel = (window.getSelection && window.getSelection()) ? window.getSelection().toString() : "";
|
|
2316
2444
|
window.parent.postMessage({
|
|
2317
2445
|
type: "previewContextMenu",
|
|
@@ -1049,6 +1049,7 @@ __export(api_client_exports, {
|
|
|
1049
1049
|
drainStoreSync: () => drainStoreSync,
|
|
1050
1050
|
emptyFolder: () => emptyFolder,
|
|
1051
1051
|
fetchImageAsDataUri: () => fetchImageAsDataUri,
|
|
1052
|
+
fetchRemoteImage: () => fetchRemoteImage,
|
|
1052
1053
|
flagSenderOrDomain: () => flagSenderOrDomain,
|
|
1053
1054
|
formatJsonc: () => formatJsonc,
|
|
1054
1055
|
getAccounts: () => getAccounts,
|
|
@@ -1611,6 +1612,16 @@ function readConfigHelp(name) {
|
|
|
1611
1612
|
function unsubscribeOneClick(url) {
|
|
1612
1613
|
return ipc().unsubscribeOneClick?.(url);
|
|
1613
1614
|
}
|
|
1615
|
+
async function fetchRemoteImage(url) {
|
|
1616
|
+
const fn = ipc().fetchRemoteImage;
|
|
1617
|
+
if (!fn)
|
|
1618
|
+
return { error: "this host can't fetch remote images" };
|
|
1619
|
+
try {
|
|
1620
|
+
return await fn(url);
|
|
1621
|
+
} catch (e) {
|
|
1622
|
+
return { error: e?.message || String(e) };
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1614
1625
|
function openInWord(editId, html, popoutId = "") {
|
|
1615
1626
|
return ipc().openInWord?.(editId, html, popoutId) ?? Promise.resolve({ ok: false, path: "", opener: "none" });
|
|
1616
1627
|
}
|