@bobfrankston/rmfmail 1.2.261 → 1.2.268
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/TODO.md +1 -0
- package/client/android-bootstrap.bundle.js +32 -0
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/client/app.bundle.js +102 -126
- package/client/app.bundle.js.map +3 -3
- package/client/app.js +59 -116
- package/client/app.js.map +1 -1
- package/client/app.ts +64 -93
- package/client/components/message-list.js +18 -3
- package/client/components/message-list.js.map +1 -1
- package/client/components/message-list.ts +18 -3
- package/client/components/message-viewer.js +84 -24
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +90 -21
- package/client/compose/compose.bundle.js +16 -5
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/compose/compose.js +5 -6
- package/client/compose/compose.js.map +1 -1
- package/client/compose/compose.ts +5 -4
- package/client/lib/rmf-tiny.js +15 -1
- package/client/package.json +1 -1
- package/client/styles/components.css +54 -1
- package/docs/thundermail-jmap.md +291 -0
- package/package.json +9 -9
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +17 -1
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +17 -1
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-store-web/package.json +1 -1
- package/packages/mailx-types/index.d.ts +51 -0
- package/packages/mailx-types/index.d.ts.map +1 -1
- package/packages/mailx-types/index.js +72 -0
- package/packages/mailx-types/index.js.map +1 -1
- package/packages/mailx-types/index.ts +72 -0
- package/packages/mailx-types/package.json +1 -1
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-62112 → node_modules.npmglobalize-stash-83744}/.package-lock.json +0 -0
package/client/app.ts
CHANGED
|
@@ -1011,9 +1011,8 @@ async function openCompose(mode: ComposeMode, overrideMsg?: any, overrideAccount
|
|
|
1011
1011
|
// bootstraps (200-500 ms for TinyMCE, less for Quill); by then the IPC
|
|
1012
1012
|
// round-trip has resolved. Earlier code awaited getAccounts FIRST,
|
|
1013
1013
|
// adding the IPC latency to the perceived Ctrl+N → editor-visible time.
|
|
1014
|
-
//
|
|
1015
|
-
//
|
|
1016
|
-
// polling.
|
|
1014
|
+
// handOffComposeInit() below does the actual handoff (parent stash +
|
|
1015
|
+
// `compose-init` postMessage + sessionStorage + heartbeat).
|
|
1017
1016
|
const accountsP = getAccounts();
|
|
1018
1017
|
const msg = current?.message;
|
|
1019
1018
|
// Title bar text needs the subject from msg (no IPC dependency) — build
|
|
@@ -1254,6 +1253,32 @@ async function openCompose(mode: ComposeMode, overrideMsg?: any, overrideAccount
|
|
|
1254
1253
|
frame = showComposeOverlay(composeTitle);
|
|
1255
1254
|
}
|
|
1256
1255
|
|
|
1256
|
+
handOffComposeInit(frame, init);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/**
|
|
1260
|
+
* Hand a compose init payload to the compose iframe.
|
|
1261
|
+
*
|
|
1262
|
+
* THIS IS THE ONLY SUPPORTED WAY to seed a compose overlay — every entry
|
|
1263
|
+
* point (Ctrl+N, reply, mailto, Windows Share, Android share, edit-draft)
|
|
1264
|
+
* must go through it. It runs four handoffs at once because under WebView2's
|
|
1265
|
+
* custom-protocol host no single one is reliable:
|
|
1266
|
+
*
|
|
1267
|
+
* 1. `window.__mailxComposeInits[key]` — a parent-window stash the
|
|
1268
|
+
* same-origin iframe reads synchronously via window.parent. Most
|
|
1269
|
+
* reliable, and the only race-free one.
|
|
1270
|
+
* 2. `compose-init` postMessage carrying the payload.
|
|
1271
|
+
* 3. sessionStorage (the original mechanism).
|
|
1272
|
+
* 4. A 100 ms heartbeat for 3 s, plus a re-post on iframe `load`.
|
|
1273
|
+
*
|
|
1274
|
+
* The belt-and-braces is not paranoia: sessionStorage AND postMessage have
|
|
1275
|
+
* each failed in production (Bob 2026-05-24, replyAll opened with empty
|
|
1276
|
+
* fields), which is why the stash exists. The mailto / share / Android-share
|
|
1277
|
+
* / edit-draft paths were still on the old sessionStorage-only handoff and
|
|
1278
|
+
* so kept reproducing that same empty-compose bug — clicking a mailto link
|
|
1279
|
+
* in another app opened compose with nothing filled in (Bob 2026-08-19).
|
|
1280
|
+
*/
|
|
1281
|
+
function handOffComposeInit(frame: HTMLIFrameElement | null | undefined, init: any): void {
|
|
1257
1282
|
const initJson = JSON.stringify(init);
|
|
1258
1283
|
try { sessionStorage.setItem("composeInit", initJson); }
|
|
1259
1284
|
catch (e: any) { console.error("[compose] sessionStorage.setItem failed:", e?.message || e); }
|
|
@@ -2288,8 +2313,10 @@ document.getElementById("rail-inbox")?.addEventListener("click", () => {
|
|
|
2288
2313
|
inbox?.click();
|
|
2289
2314
|
});
|
|
2290
2315
|
document.getElementById("rail-unified")?.addEventListener("click", () => {
|
|
2291
|
-
|
|
2292
|
-
|
|
2316
|
+
// The || document.getElementById("ft-all-inboxes") fallback was dropped
|
|
2317
|
+
// 2026-08-19: no HTML declares that id and nothing creates it, so the
|
|
2318
|
+
// querySelector was always the only live path.
|
|
2319
|
+
const unified = document.querySelector('.folder-tree .all-inboxes') as HTMLElement | null;
|
|
2293
2320
|
unified?.click();
|
|
2294
2321
|
});
|
|
2295
2322
|
document.getElementById("rail-contacts")?.addEventListener("click", async () => {
|
|
@@ -2546,8 +2573,7 @@ document.addEventListener("mailx-share-intent", ((e: CustomEvent) => {
|
|
|
2546
2573
|
if (Array.isArray(detail.attachments) && detail.attachments.length) {
|
|
2547
2574
|
init.attachments = detail.attachments.filter((a: any) => a?.filename && a?.dataBase64);
|
|
2548
2575
|
}
|
|
2549
|
-
|
|
2550
|
-
showComposeOverlay(init.subject || "Compose");
|
|
2576
|
+
handOffComposeInit(showComposeOverlay(init.subject || "Compose"), init);
|
|
2551
2577
|
}) as EventListener);
|
|
2552
2578
|
|
|
2553
2579
|
// ── Search ──
|
|
@@ -2984,11 +3010,10 @@ window.addEventListener("message", (e) => {
|
|
|
2984
3010
|
}
|
|
2985
3011
|
return;
|
|
2986
3012
|
}
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
}
|
|
3013
|
+
// (removed 2026-08-19, dead-code audit) An "openLink" postMessage
|
|
3014
|
+
// handler lived here. Nothing has posted that type since the preview
|
|
3015
|
+
// iframe moved to "linkClick" — it was unreachable. Link opening is
|
|
3016
|
+
// handled by the "linkClick" branch below.
|
|
2992
3017
|
if (e.data?.type === "linkClick" && e.data.url) {
|
|
2993
3018
|
const url = e.data.url;
|
|
2994
3019
|
console.log(`[android-link] parent got linkClick → ${url}`);
|
|
@@ -3013,10 +3038,11 @@ window.addEventListener("message", (e) => {
|
|
|
3013
3038
|
} catch { /* diagnostic only */ }
|
|
3014
3039
|
return;
|
|
3015
3040
|
}
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3041
|
+
// (removed 2026-08-19, dead-code audit) A "previewToggleFullscreen"
|
|
3042
|
+
// postMessage handler lived here, fed by an iframe-level dblclick that
|
|
3043
|
+
// was itself removed for hijacking word-selection. Nothing has posted
|
|
3044
|
+
// the type since; the chrome dblclick handler is the only entry into
|
|
3045
|
+
// fullscreen-preview.
|
|
3020
3046
|
if (e.data?.type === "previewContextMenu") {
|
|
3021
3047
|
// Translate iframe-relative coords to viewport. Find the iframe whose
|
|
3022
3048
|
// contentWindow matches the source so we map correctly even when
|
|
@@ -3037,75 +3063,15 @@ window.addEventListener("message", (e) => {
|
|
|
3037
3063
|
);
|
|
3038
3064
|
return;
|
|
3039
3065
|
}
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
const url: string = e.data.url || "";
|
|
3050
|
-
// Find a sensible filename for the Save action.
|
|
3051
|
-
const guessName = (() => {
|
|
3052
|
-
try {
|
|
3053
|
-
const u = new URL(url);
|
|
3054
|
-
const last = u.pathname.split("/").pop() || "";
|
|
3055
|
-
return last && last.includes(".") ? last : "";
|
|
3056
|
-
} catch { return ""; }
|
|
3057
|
-
})();
|
|
3058
|
-
const items: { label: string; action: () => void }[] = [
|
|
3059
|
-
{ label: "Open in browser", action: () => {
|
|
3060
|
-
window.open(url, "_blank", "noopener,noreferrer");
|
|
3061
|
-
}},
|
|
3062
|
-
{ label: guessName ? `Save "${guessName}"…` : "Save link as…", action: () => {
|
|
3063
|
-
// Trigger a download via anchor with download attr.
|
|
3064
|
-
const a = document.createElement("a");
|
|
3065
|
-
a.href = url;
|
|
3066
|
-
if (guessName) a.download = guessName;
|
|
3067
|
-
else a.download = "";
|
|
3068
|
-
a.style.display = "none";
|
|
3069
|
-
document.body.appendChild(a);
|
|
3070
|
-
a.click();
|
|
3071
|
-
setTimeout(() => a.remove(), 1000);
|
|
3072
|
-
}},
|
|
3073
|
-
{ label: "Copy URL", action: async () => {
|
|
3074
|
-
try { await navigator.clipboard.writeText(url); }
|
|
3075
|
-
catch { prompt("URL:", url); }
|
|
3076
|
-
}},
|
|
3077
|
-
{ label: "Copy link text", action: async () => {
|
|
3078
|
-
try { await navigator.clipboard.writeText(e.data.text || url); }
|
|
3079
|
-
catch { prompt("Text:", e.data.text || url); }
|
|
3080
|
-
}},
|
|
3081
|
-
];
|
|
3082
|
-
// Build a tiny inline menu (showContextMenu would do but it's in components/).
|
|
3083
|
-
const menu = document.createElement("div");
|
|
3084
|
-
menu.style.cssText = `position:fixed;z-index:2400;background:var(--color-bg);border:1px solid var(--color-border);border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,0.2);padding:4px 0;font-size:13px;min-width:180px;`;
|
|
3085
|
-
menu.style.left = `${Math.min(x, window.innerWidth - 200)}px`;
|
|
3086
|
-
menu.style.top = `${Math.min(y, window.innerHeight - 200)}px`;
|
|
3087
|
-
// mousedown inside the menu must NOT reach the document-level
|
|
3088
|
-
// dismiss handler — otherwise the menu is removed before click
|
|
3089
|
-
// fires on the row and the action silently no-ops (user report
|
|
3090
|
-
// 2026-04-24). Stop propagation at the menu root covers every row.
|
|
3091
|
-
menu.addEventListener("mousedown", (ev) => ev.stopPropagation());
|
|
3092
|
-
for (const it of items) {
|
|
3093
|
-
const row = document.createElement("div");
|
|
3094
|
-
row.textContent = it.label;
|
|
3095
|
-
row.style.cssText = `padding:6px 12px;cursor:pointer;`;
|
|
3096
|
-
row.addEventListener("mouseenter", () => row.style.background = "var(--color-bg-hover)");
|
|
3097
|
-
row.addEventListener("mouseleave", () => row.style.background = "");
|
|
3098
|
-
row.addEventListener("click", () => { menu.remove(); it.action(); });
|
|
3099
|
-
menu.appendChild(row);
|
|
3100
|
-
}
|
|
3101
|
-
document.body.appendChild(menu);
|
|
3102
|
-
const dismiss = () => { menu.remove(); document.removeEventListener("mousedown", dismiss); document.removeEventListener("keydown", dismiss); };
|
|
3103
|
-
setTimeout(() => {
|
|
3104
|
-
document.addEventListener("mousedown", dismiss);
|
|
3105
|
-
document.addEventListener("keydown", dismiss);
|
|
3106
|
-
}, 0);
|
|
3107
|
-
return;
|
|
3108
|
-
}
|
|
3066
|
+
// (removed 2026-08-19) A second, dead link context menu used to live
|
|
3067
|
+
// here — a "linkContextMenu" postMessage handler labelled "Copy URL",
|
|
3068
|
+
// built when the preview iframe still posted that message type. Nothing
|
|
3069
|
+
// has posted it since the viewer moved to showBodyContextMenu(), so it
|
|
3070
|
+
// was unreachable code offering a different label ("Copy URL") for the
|
|
3071
|
+
// same action the live menu calls "Copy link URL" — the kind of drift
|
|
3072
|
+
// Bob spotted 2026-08-19 ("other menus have both copy link and copy
|
|
3073
|
+
// url"). The live implementation is in components/message-viewer.ts;
|
|
3074
|
+
// keep link-menu changes there and nowhere else.
|
|
3109
3075
|
if (e.data?.type === "mailx-send-error") {
|
|
3110
3076
|
// Send failed AFTER compose closed (fire-and-forget model). Surface in
|
|
3111
3077
|
// the status bar so the user sees something instead of the silence.
|
|
@@ -3781,8 +3747,7 @@ async function openComposeFromMailto(m: {
|
|
|
3781
3747
|
references: m.inReplyTo ? [m.inReplyTo] : [],
|
|
3782
3748
|
accounts: accounts.map((a: any) => ({ id: a.id, name: a.name, email: a.email, signature: a.signature, sig: a.sig })),
|
|
3783
3749
|
};
|
|
3784
|
-
|
|
3785
|
-
try { frame?.contentWindow?.postMessage({ type: "compose-init-ready" }, "*"); } catch { /* */ }
|
|
3750
|
+
handOffComposeInit(frame, init);
|
|
3786
3751
|
}
|
|
3787
3752
|
|
|
3788
3753
|
/** C46: open compose from a Windows Share sheet drop. Shared files become
|
|
@@ -3825,8 +3790,7 @@ async function openComposeFromShare(s: {
|
|
|
3825
3790
|
attachments: s.attachments,
|
|
3826
3791
|
accounts: accounts.map((a: any) => ({ id: a.id, name: a.name, email: a.email, signature: a.signature, sig: a.sig })),
|
|
3827
3792
|
};
|
|
3828
|
-
|
|
3829
|
-
try { frame?.contentWindow?.postMessage({ type: "compose-init-ready" }, "*"); } catch { /* */ }
|
|
3793
|
+
handOffComposeInit(frame, init);
|
|
3830
3794
|
}
|
|
3831
3795
|
|
|
3832
3796
|
// ── Keyboard shortcuts ──
|
|
@@ -4477,8 +4441,16 @@ document.addEventListener("keydown", (e) => {
|
|
|
4477
4441
|
}
|
|
4478
4442
|
const bd = document.getElementById("rail-menu-backdrop");
|
|
4479
4443
|
if (bd) { bd.remove(); closed = true; }
|
|
4480
|
-
|
|
4481
|
-
|
|
4444
|
+
// Was getElementById("theme-submenu") — an id nothing declares, so Escape
|
|
4445
|
+
// silently never closed the theme submenu (dead-code audit 2026-08-19).
|
|
4446
|
+
// The real element is #theme-submenu-popout; also reset the toggle's
|
|
4447
|
+
// aria-expanded so the button doesn't claim to be open.
|
|
4448
|
+
const themeSub = document.getElementById("theme-submenu-popout");
|
|
4449
|
+
if (themeSub && !themeSub.hidden) {
|
|
4450
|
+
themeSub.hidden = true;
|
|
4451
|
+
document.getElementById("theme-submenu-toggle")?.setAttribute("aria-expanded", "false");
|
|
4452
|
+
closed = true;
|
|
4453
|
+
}
|
|
4482
4454
|
if (closed) {
|
|
4483
4455
|
e.preventDefault();
|
|
4484
4456
|
refreshToolbarOverlayShield();
|
|
@@ -6113,8 +6085,7 @@ document.addEventListener("mailx-popout-message", (async (e: any) => {
|
|
|
6113
6085
|
}
|
|
6114
6086
|
if (loaded.length) (init as any).attachments = loaded;
|
|
6115
6087
|
}
|
|
6116
|
-
|
|
6117
|
-
showComposeOverlay(msg.subject ? `Edit: ${msg.subject}` : "Edit draft");
|
|
6088
|
+
handOffComposeInit(showComposeOverlay(msg.subject ? `Edit: ${msg.subject}` : "Edit draft"), init);
|
|
6118
6089
|
return;
|
|
6119
6090
|
}
|
|
6120
6091
|
// ONE read-only popout, not three. This handler used to build its own
|
|
@@ -8,7 +8,7 @@ import { showMessage as viewerShow, clearViewer as viewerClear } from "./message
|
|
|
8
8
|
import { showContextMenu } from "./context-menu.js";
|
|
9
9
|
import { pickFolder } from "./folder-picker.js";
|
|
10
10
|
import { foldText } from "../lib/fold-text.js";
|
|
11
|
-
import { seenOf, flaggedOf, draftOf, setSeen, setFlagged } from "@bobfrankston/mailx-types";
|
|
11
|
+
import { seenOf, flaggedOf, draftOf, setSeen, setFlagged, isJunkPreviewText, displayNameClaimsOtherAddress } from "@bobfrankston/mailx-types";
|
|
12
12
|
let onMessageSelect;
|
|
13
13
|
let currentAccountId;
|
|
14
14
|
let currentFolderId;
|
|
@@ -1923,7 +1923,18 @@ class MessageRow {
|
|
|
1923
1923
|
from.textContent = msg.to.map((a) => a.name || a.address).join(", ");
|
|
1924
1924
|
}
|
|
1925
1925
|
else {
|
|
1926
|
-
|
|
1926
|
+
// The row normally shows the display name alone — which is exactly
|
|
1927
|
+
// what a display-name spoof relies on ("security@paypal.com"
|
|
1928
|
+
// wired to anywhere). When the name asserts an address that
|
|
1929
|
+
// isn't the real one, show the real one too so the row can't
|
|
1930
|
+
// lie on its own (Bob 2026-08-19).
|
|
1931
|
+
const fromName = msg.from.name || msg.from.address;
|
|
1932
|
+
const fromClaim = displayNameClaimsOtherAddress(msg.from.name || "", msg.from.address || "");
|
|
1933
|
+
from.textContent = fromClaim ? `${fromName} — actually ${msg.from.address}` : fromName;
|
|
1934
|
+
if (fromClaim) {
|
|
1935
|
+
from.classList.add("ml-from-spoofed");
|
|
1936
|
+
from.title = `Display name claims ${fromClaim}, but the message is from ${msg.from.address}`;
|
|
1937
|
+
}
|
|
1927
1938
|
}
|
|
1928
1939
|
if (showAccountTag && accountId) {
|
|
1929
1940
|
const tag = document.createElement("span");
|
|
@@ -1961,7 +1972,11 @@ class MessageRow {
|
|
|
1961
1972
|
});
|
|
1962
1973
|
subject.prepend(threadPill);
|
|
1963
1974
|
}
|
|
1964
|
-
|
|
1975
|
+
// isJunkPreviewText: rows indexed before extractPreview learned to skip
|
|
1976
|
+
// a text/plain part that is literally "undefined" (Bob 2026-08-19).
|
|
1977
|
+
// Suppressing it here means those rows read correctly straight away
|
|
1978
|
+
// instead of waiting for a -repair re-sync.
|
|
1979
|
+
if (msg.preview && !isJunkPreviewText(msg.preview)) {
|
|
1965
1980
|
// Defensive cleanup for older rows whose preview was indexed
|
|
1966
1981
|
// before extractPreview() stripped data: URIs. Quill-pasted
|
|
1967
1982
|
// images can leave a `data:image/png;base64,…` blob in the
|