@bobfrankston/rmfmail 1.2.96 → 1.2.97
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 +110 -19
- package/client/app.bundle.js.map +3 -3
- package/client/components/message-viewer.js +132 -25
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +116 -23
- package/client/compose/compose.bundle.js +4 -0
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/lib/api-client.js +4 -0
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +5 -0
- package/client/lib/mailxapi.js +3 -0
- package/package.json +1 -1
- package/packages/mailx-service/index.d.ts +7 -0
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +40 -0
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +31 -0
- 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
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* drift impossible: there is exactly one path into this pane.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { getMessage, updateFlags, allowRemoteContent, flagSenderOrDomain, getAttachment, openAttachment, addContact, listContacts, upsertContact, unsubscribeOneClick, addPreferredContact, onEvent, subscribeStore } from "../lib/api-client.js";
|
|
12
|
+
import { getMessage, updateFlags, allowRemoteContent, flagSenderOrDomain, getAttachment, openAttachment, getMessageSource, addContact, listContacts, upsertContact, unsubscribeOneClick, addPreferredContact, onEvent, subscribeStore } from "../lib/api-client.js";
|
|
13
13
|
import { showContextMenu } from "./context-menu.js";
|
|
14
14
|
import type { MenuItem } from "./context-menu.js";
|
|
15
15
|
import type { ListMessage } from "../lib/message-state.js";
|
|
@@ -20,6 +20,9 @@ import { setSeen, seenOf } from "@bobfrankston/mailx-types";
|
|
|
20
20
|
/** Currently displayed message (for reply/forward) */
|
|
21
21
|
let currentMessage: any = null;
|
|
22
22
|
let currentAccountId: string = "";
|
|
23
|
+
// Blob URLs created for attachment drag-out (revoked when the next message's
|
|
24
|
+
// chips render, so they don't accumulate across the session).
|
|
25
|
+
const dragOutUrls: string[] = [];
|
|
23
26
|
let showMessageGeneration = 0; // Cancel stale fetches
|
|
24
27
|
let retryCount = 0;
|
|
25
28
|
/** Last envelope handed in by the list — used for envelope-first paint
|
|
@@ -220,6 +223,8 @@ export function showPreviewBodyMenu(absX: number, absY: number, selectedText: st
|
|
|
220
223
|
{ label: "Zoom out (−)", action: () => { for (const f of Array.from(document.querySelectorAll("iframe"))) { const d = (f as HTMLIFrameElement).contentDocument; if (d) { setZoom(previewZoom - ZOOM_STEP, d); break; } } } },
|
|
221
224
|
{ label: "", action: () => {}, separator: true },
|
|
222
225
|
{ label: "Print… (Ctrl+P)", action: () => printCurrentMessage() },
|
|
226
|
+
{ label: "Save message (.eml)…", action: () => { void saveCurrentMessageAsEml(); } },
|
|
227
|
+
{ label: "Save message as HTML…", action: () => { void saveCurrentMessageAsHtml(); } },
|
|
223
228
|
);
|
|
224
229
|
showContextMenu(absX, absY, items);
|
|
225
230
|
}
|
|
@@ -1260,6 +1265,30 @@ export async function showMessage(accountId: string, uid: number, folderId?: num
|
|
|
1260
1265
|
attEl.hidden = true;
|
|
1261
1266
|
if (msg.attachments?.length) {
|
|
1262
1267
|
attEl.hidden = false;
|
|
1268
|
+
// ── Drag-out prefetch cache ──
|
|
1269
|
+
// dataTransfer.setData ONLY works synchronously inside dragstart —
|
|
1270
|
+
// the drag data store closes at the first `await` — so the old
|
|
1271
|
+
// handler that fetched the attachment inside dragstart silently
|
|
1272
|
+
// set nothing, and drag-to-Explorer never worked (Bob 2026-07-02).
|
|
1273
|
+
// Prefetch every attachment's bytes in the background right after
|
|
1274
|
+
// the chips render; dragstart reads this cache synchronously.
|
|
1275
|
+
// Blob URLs from the previous message are revoked first.
|
|
1276
|
+
for (const u of dragOutUrls) { try { URL.revokeObjectURL(u); } catch { /* */ } }
|
|
1277
|
+
dragOutUrls.length = 0;
|
|
1278
|
+
const dragCache = new Map<number, { url: string; contentType: string }>();
|
|
1279
|
+
void (async () => {
|
|
1280
|
+
for (const a1 of msg.attachments) {
|
|
1281
|
+
if ((a1.size || 0) > 50 * 1024 * 1024) continue; // don't buffer >50MB for a maybe-drag
|
|
1282
|
+
try {
|
|
1283
|
+
const data = await getAttachment(accountId, uid, a1.id, msg.folderId);
|
|
1284
|
+
const bytes = Uint8Array.from(atob(data.content), c => c.charCodeAt(0));
|
|
1285
|
+
const blob = new Blob([bytes], { type: data.contentType || "application/octet-stream" });
|
|
1286
|
+
const url = URL.createObjectURL(blob);
|
|
1287
|
+
dragOutUrls.push(url);
|
|
1288
|
+
dragCache.set(a1.id, { url, contentType: data.contentType || "application/octet-stream" });
|
|
1289
|
+
} catch { /* drag-out just won't arm for this attachment */ }
|
|
1290
|
+
}
|
|
1291
|
+
})();
|
|
1263
1292
|
// Save (download to disk) one attachment by index — distinct from
|
|
1264
1293
|
// the click handler's "open with the OS app". Desktop (WebView2 =
|
|
1265
1294
|
// Chromium) gets a real Save-As dialog via showSaveFilePicker;
|
|
@@ -1378,26 +1407,27 @@ export async function showMessage(accountId: string, uid: number, folderId?: num
|
|
|
1378
1407
|
restoreChip();
|
|
1379
1408
|
}
|
|
1380
1409
|
});
|
|
1381
|
-
// Drag the chip to an external target (Explorer / Finder
|
|
1382
|
-
//
|
|
1383
|
-
// format: "mime:filename:blob-url".
|
|
1384
|
-
//
|
|
1410
|
+
// Drag the chip to an external target (Explorer / Finder) to
|
|
1411
|
+
// drop the file there. Chromium `DownloadURL` dataTransfer
|
|
1412
|
+
// format: "mime:filename:blob-url". MUST be synchronous — the
|
|
1413
|
+
// content comes from the prefetch cache above; Chromium stamps
|
|
1414
|
+
// the dropped file with the Mark-of-the-Web like any download.
|
|
1385
1415
|
chip.draggable = true;
|
|
1386
|
-
chip.addEventListener("dragstart",
|
|
1416
|
+
chip.addEventListener("dragstart", (e) => {
|
|
1387
1417
|
if (!e.dataTransfer) return;
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
e.dataTransfer.setData("DownloadURL", downloadUrl);
|
|
1397
|
-
e.dataTransfer.effectAllowed = "copy";
|
|
1398
|
-
} catch (err: any) {
|
|
1399
|
-
console.error(`Attachment drag-out failed: ${err.message || err}`);
|
|
1418
|
+
const cached = dragCache.get(att.id);
|
|
1419
|
+
if (!cached) {
|
|
1420
|
+
// Prefetch still in flight (or >50MB skip). Cancel the
|
|
1421
|
+
// drag rather than start one that can't carry data.
|
|
1422
|
+
e.preventDefault();
|
|
1423
|
+
const statusEl = document.getElementById("status-sync");
|
|
1424
|
+
if (statusEl) statusEl.textContent = "Attachment still loading — try dragging again in a moment.";
|
|
1425
|
+
return;
|
|
1400
1426
|
}
|
|
1427
|
+
// Sanitize filename: no path separators, no newlines.
|
|
1428
|
+
const safeName = (att.filename || "attachment").replace(/[\r\n"\/\\]/g, "_");
|
|
1429
|
+
e.dataTransfer.setData("DownloadURL", `${cached.contentType}:${safeName}:${cached.url}`);
|
|
1430
|
+
e.dataTransfer.effectAllowed = "copy";
|
|
1401
1431
|
});
|
|
1402
1432
|
// Right-click → Open / Save / Save all. Click still opens; this
|
|
1403
1433
|
// adds explicit save (download to disk) which the click path
|
|
@@ -1971,9 +2001,11 @@ export function popOutToWindow(): void {
|
|
|
1971
2001
|
* document — a header block (From/To/Cc/Subject/Date) plus the message body
|
|
1972
2002
|
* — renders it in an off-screen iframe, and fires the print dialog. There
|
|
1973
2003
|
* was no way to print a letter before (Bob 2026-05-21). */
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2004
|
+
/** Full standalone HTML document for a message: printed-letter header block
|
|
2005
|
+
* (From/To/Cc/Subject/Date) + body. Shared by Print and Save-as-HTML so the
|
|
2006
|
+
* saved file reads exactly like the printout. */
|
|
2007
|
+
function buildPrintableDoc(m: any): string | null {
|
|
2008
|
+
if (!m) return null;
|
|
1977
2009
|
const fmt = (a: { name?: string; address?: string }): string =>
|
|
1978
2010
|
a?.name ? `${a.name} <${a.address || ""}>` : (a?.address || "");
|
|
1979
2011
|
const esc = escapeHtmlLocal;
|
|
@@ -1991,11 +2023,16 @@ export function printCurrentMessage(): void {
|
|
|
1991
2023
|
const bodyHtml = m.bodyHtml
|
|
1992
2024
|
? m.bodyHtml
|
|
1993
2025
|
: `<pre style="white-space:pre-wrap;word-break:break-word;font-family:system-ui,sans-serif;">${esc(m.bodyText || "")}</pre>`;
|
|
1994
|
-
|
|
1995
|
-
`<!DOCTYPE html><html><head><meta charset="utf-8">` +
|
|
2026
|
+
return `<!DOCTYPE html><html><head><meta charset="utf-8">` +
|
|
1996
2027
|
`<title>${esc(m.subject || "Message")}</title>` +
|
|
1997
2028
|
`<style>@media print{body{margin:0;}}body{margin:16px;}img{max-width:100%;}</style>` +
|
|
1998
2029
|
`</head><body>${headerHtml}${bodyHtml}</body></html>`;
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
export function printCurrentMessage(): void {
|
|
2033
|
+
if (!currentMessage) return;
|
|
2034
|
+
const doc = buildPrintableDoc(currentMessage);
|
|
2035
|
+
if (!doc) return;
|
|
1999
2036
|
|
|
2000
2037
|
const iframe = document.createElement("iframe");
|
|
2001
2038
|
iframe.style.cssText = "position:fixed;left:-9999px;width:1px;height:1px;border:0;";
|
|
@@ -2016,6 +2053,62 @@ export function printCurrentMessage(): void {
|
|
|
2016
2053
|
document.body.appendChild(iframe);
|
|
2017
2054
|
}
|
|
2018
2055
|
|
|
2056
|
+
/** Save a Blob to disk. Native Save-As dialog where the File System Access
|
|
2057
|
+
* API exists (WebView2/Chromium — which also stamps the file with the
|
|
2058
|
+
* Mark-of-the-Web, so Office/SmartScreen treat it as untrusted); falls back
|
|
2059
|
+
* to a programmatic <a download> elsewhere. */
|
|
2060
|
+
async function saveBlobAs(suggestedName: string, blob: Blob): Promise<void> {
|
|
2061
|
+
const picker = (window as any).showSaveFilePicker;
|
|
2062
|
+
if (typeof picker === "function") {
|
|
2063
|
+
let handle: any;
|
|
2064
|
+
try {
|
|
2065
|
+
handle = await picker({ suggestedName });
|
|
2066
|
+
} catch (e: any) {
|
|
2067
|
+
if (e?.name === "AbortError") return; // user cancelled
|
|
2068
|
+
throw e;
|
|
2069
|
+
}
|
|
2070
|
+
const writable = await handle.createWritable();
|
|
2071
|
+
await writable.write(blob);
|
|
2072
|
+
await writable.close();
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
const url = URL.createObjectURL(blob);
|
|
2076
|
+
const a = document.createElement("a");
|
|
2077
|
+
a.href = url; a.download = suggestedName; a.style.display = "none";
|
|
2078
|
+
document.body.appendChild(a); a.click();
|
|
2079
|
+
setTimeout(() => { a.remove(); URL.revokeObjectURL(url); }, 5000);
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
/** Save the open message's raw RFC 822 source as a .eml file — the portable
|
|
2083
|
+
* mail format Outlook / Thunderbird / other clients open directly (an .eml
|
|
2084
|
+
* with multipart/related content is what .mht wraps; .eml additionally keeps
|
|
2085
|
+
* the real headers and attachments). */
|
|
2086
|
+
export async function saveCurrentMessageAsEml(): Promise<void> {
|
|
2087
|
+
if (!currentMessage) return;
|
|
2088
|
+
try {
|
|
2089
|
+
const src = await getMessageSource(currentAccountId, currentMessage.uid, currentMessage.folderId);
|
|
2090
|
+
const bytes = Uint8Array.from(atob(src.dataBase64), c => c.charCodeAt(0));
|
|
2091
|
+
await saveBlobAs(src.filename || "message.eml", new Blob([bytes], { type: "message/rfc822" }));
|
|
2092
|
+
} catch (e: any) {
|
|
2093
|
+
window.dispatchEvent(new CustomEvent("mailx-alert", { detail: { message: `Couldn't save message: ${e?.message || e}`, key: "message-save" } }));
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
/** Save the open message as a standalone HTML file — the same header block +
|
|
2098
|
+
* body document the Print path renders, so the saved file reads like the
|
|
2099
|
+
* printed letter. */
|
|
2100
|
+
export async function saveCurrentMessageAsHtml(): Promise<void> {
|
|
2101
|
+
if (!currentMessage) return;
|
|
2102
|
+
const doc = buildPrintableDoc(currentMessage);
|
|
2103
|
+
if (!doc) return;
|
|
2104
|
+
const base = (currentMessage.subject || "message").replace(/[<>:"/\\|?*\r\n]/g, "_").slice(0, 80).trim() || "message";
|
|
2105
|
+
try {
|
|
2106
|
+
await saveBlobAs(`${base}.html`, new Blob([doc], { type: "text/html" }));
|
|
2107
|
+
} catch (e: any) {
|
|
2108
|
+
window.dispatchEvent(new CustomEvent("mailx-alert", { detail: { message: `Couldn't save message: ${e?.message || e}`, key: "message-save" } }));
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2019
2112
|
/** Build a floating overlay carrying a snapshot of the message: header
|
|
2020
2113
|
* (subject, from, to, date) + sandboxed body iframe + attachment chips.
|
|
2021
2114
|
* Reuses the compose-overlay drag/resize/close pattern. Independent of the
|
|
@@ -1052,6 +1052,7 @@ __export(api_client_exports, {
|
|
|
1052
1052
|
getDiagnostics: () => getDiagnostics,
|
|
1053
1053
|
getFolders: () => getFolders,
|
|
1054
1054
|
getMessage: () => getMessage,
|
|
1055
|
+
getMessageSource: () => getMessageSource,
|
|
1055
1056
|
getMessages: () => getMessages,
|
|
1056
1057
|
getOutboxStatus: () => getOutboxStatus,
|
|
1057
1058
|
getPrimaryAccount: () => getPrimaryAccount,
|
|
@@ -1614,6 +1615,9 @@ async function openAttachment(accountId, uid, attachmentId, folderId, filename)
|
|
|
1614
1615
|
const fn = ipc().openAttachment;
|
|
1615
1616
|
return fn ? fn(accountId, uid, attachmentId, folderId, filename) : void 0;
|
|
1616
1617
|
}
|
|
1618
|
+
async function getMessageSource(accountId, uid, folderId) {
|
|
1619
|
+
return ipc().getMessageSource(accountId, uid, folderId);
|
|
1620
|
+
}
|
|
1617
1621
|
async function getDeviceAccounts() {
|
|
1618
1622
|
return ipc().getDeviceAccounts?.() ?? [];
|
|
1619
1623
|
}
|