@bobfrankston/rmfmail 1.2.263 → 1.2.270
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 +34 -1
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/client/app.bundle.js +54 -108
- 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 +14 -4
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +13 -3
- package/client/compose/compose.bundle.js +17 -6
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/compose/compose.js +13 -7
- package/client/compose/compose.js.map +1 -1
- package/client/compose/compose.ts +13 -5
- package/client/lib/rmf-tiny.js +15 -1
- package/client/package.json +1 -1
- package/client/styles/components.css +9 -0
- package/docs/thundermail-jmap.md +291 -0
- package/package.json +11 -11
- 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-settings/docs/thundermail-jmap.md +291 -0
- package/packages/mailx-settings/index.d.ts +1 -0
- package/packages/mailx-settings/index.d.ts.map +1 -1
- package/packages/mailx-settings/index.js +1 -0
- package/packages/mailx-settings/index.js.map +1 -1
- package/packages/mailx-settings/index.ts +1 -0
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/package.json +1 -1
- package/packages/mailx-store-web/package.json +1 -1
- package/packages/mailx-store-web/web-settings.d.ts +1 -0
- package/packages/mailx-store-web/web-settings.d.ts.map +1 -1
- package/packages/mailx-store-web/web-settings.js +1 -0
- package/packages/mailx-store-web/web-settings.js.map +1 -1
- package/packages/mailx-store-web/web-settings.ts +1 -0
- package/packages/mailx-types/index.d.ts +59 -1
- 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 +80 -1
- package/packages/mailx-types/package.json +1 -1
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-88864 → node_modules.npmglobalize-stash-18100}/.package-lock.json +0 -0
package/client/app.js
CHANGED
|
@@ -1060,9 +1060,8 @@ async function openCompose(mode, overrideMsg, overrideAccountId) {
|
|
|
1060
1060
|
// bootstraps (200-500 ms for TinyMCE, less for Quill); by then the IPC
|
|
1061
1061
|
// round-trip has resolved. Earlier code awaited getAccounts FIRST,
|
|
1062
1062
|
// adding the IPC latency to the perceived Ctrl+N → editor-visible time.
|
|
1063
|
-
//
|
|
1064
|
-
//
|
|
1065
|
-
// polling.
|
|
1063
|
+
// handOffComposeInit() below does the actual handoff (parent stash +
|
|
1064
|
+
// `compose-init` postMessage + sessionStorage + heartbeat).
|
|
1066
1065
|
const accountsP = getAccounts();
|
|
1067
1066
|
const msg = current?.message;
|
|
1068
1067
|
// Title bar text needs the subject from msg (no IPC dependency) — build
|
|
@@ -1308,6 +1307,31 @@ async function openCompose(mode, overrideMsg, overrideAccountId) {
|
|
|
1308
1307
|
}
|
|
1309
1308
|
frame = showComposeOverlay(composeTitle);
|
|
1310
1309
|
}
|
|
1310
|
+
handOffComposeInit(frame, init);
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Hand a compose init payload to the compose iframe.
|
|
1314
|
+
*
|
|
1315
|
+
* THIS IS THE ONLY SUPPORTED WAY to seed a compose overlay — every entry
|
|
1316
|
+
* point (Ctrl+N, reply, mailto, Windows Share, Android share, edit-draft)
|
|
1317
|
+
* must go through it. It runs four handoffs at once because under WebView2's
|
|
1318
|
+
* custom-protocol host no single one is reliable:
|
|
1319
|
+
*
|
|
1320
|
+
* 1. `window.__mailxComposeInits[key]` — a parent-window stash the
|
|
1321
|
+
* same-origin iframe reads synchronously via window.parent. Most
|
|
1322
|
+
* reliable, and the only race-free one.
|
|
1323
|
+
* 2. `compose-init` postMessage carrying the payload.
|
|
1324
|
+
* 3. sessionStorage (the original mechanism).
|
|
1325
|
+
* 4. A 100 ms heartbeat for 3 s, plus a re-post on iframe `load`.
|
|
1326
|
+
*
|
|
1327
|
+
* The belt-and-braces is not paranoia: sessionStorage AND postMessage have
|
|
1328
|
+
* each failed in production (Bob 2026-05-24, replyAll opened with empty
|
|
1329
|
+
* fields), which is why the stash exists. The mailto / share / Android-share
|
|
1330
|
+
* / edit-draft paths were still on the old sessionStorage-only handoff and
|
|
1331
|
+
* so kept reproducing that same empty-compose bug — clicking a mailto link
|
|
1332
|
+
* in another app opened compose with nothing filled in (Bob 2026-08-19).
|
|
1333
|
+
*/
|
|
1334
|
+
function handOffComposeInit(frame, init) {
|
|
1311
1335
|
const initJson = JSON.stringify(init);
|
|
1312
1336
|
try {
|
|
1313
1337
|
sessionStorage.setItem("composeInit", initJson);
|
|
@@ -2360,8 +2384,10 @@ document.getElementById("rail-inbox")?.addEventListener("click", () => {
|
|
|
2360
2384
|
inbox?.click();
|
|
2361
2385
|
});
|
|
2362
2386
|
document.getElementById("rail-unified")?.addEventListener("click", () => {
|
|
2363
|
-
|
|
2364
|
-
|
|
2387
|
+
// The || document.getElementById("ft-all-inboxes") fallback was dropped
|
|
2388
|
+
// 2026-08-19: no HTML declares that id and nothing creates it, so the
|
|
2389
|
+
// querySelector was always the only live path.
|
|
2390
|
+
const unified = document.querySelector('.folder-tree .all-inboxes');
|
|
2365
2391
|
unified?.click();
|
|
2366
2392
|
});
|
|
2367
2393
|
document.getElementById("rail-contacts")?.addEventListener("click", async () => {
|
|
@@ -2655,8 +2681,7 @@ document.addEventListener("mailx-share-intent", ((e) => {
|
|
|
2655
2681
|
if (Array.isArray(detail.attachments) && detail.attachments.length) {
|
|
2656
2682
|
init.attachments = detail.attachments.filter((a) => a?.filename && a?.dataBase64);
|
|
2657
2683
|
}
|
|
2658
|
-
|
|
2659
|
-
showComposeOverlay(init.subject || "Compose");
|
|
2684
|
+
handOffComposeInit(showComposeOverlay(init.subject || "Compose"), init);
|
|
2660
2685
|
}));
|
|
2661
2686
|
// ── Search ──
|
|
2662
2687
|
let searchTimeout;
|
|
@@ -3111,13 +3136,10 @@ window.addEventListener("message", (e) => {
|
|
|
3111
3136
|
}
|
|
3112
3137
|
return;
|
|
3113
3138
|
}
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
else
|
|
3119
|
-
window.open(e.data.url, "_blank", "noopener,noreferrer");
|
|
3120
|
-
}
|
|
3139
|
+
// (removed 2026-08-19, dead-code audit) An "openLink" postMessage
|
|
3140
|
+
// handler lived here. Nothing has posted that type since the preview
|
|
3141
|
+
// iframe moved to "linkClick" — it was unreachable. Link opening is
|
|
3142
|
+
// handled by the "linkClick" branch below.
|
|
3121
3143
|
if (e.data?.type === "linkClick" && e.data.url) {
|
|
3122
3144
|
const url = e.data.url;
|
|
3123
3145
|
console.log(`[android-link] parent got linkClick → ${url}`);
|
|
@@ -3145,10 +3167,11 @@ window.addEventListener("message", (e) => {
|
|
|
3145
3167
|
catch { /* diagnostic only */ }
|
|
3146
3168
|
return;
|
|
3147
3169
|
}
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3170
|
+
// (removed 2026-08-19, dead-code audit) A "previewToggleFullscreen"
|
|
3171
|
+
// postMessage handler lived here, fed by an iframe-level dblclick that
|
|
3172
|
+
// was itself removed for hijacking word-selection. Nothing has posted
|
|
3173
|
+
// the type since; the chrome dblclick handler is the only entry into
|
|
3174
|
+
// fullscreen-preview.
|
|
3152
3175
|
if (e.data?.type === "previewContextMenu") {
|
|
3153
3176
|
// Translate iframe-relative coords to viewport. Find the iframe whose
|
|
3154
3177
|
// contentWindow matches the source so we map correctly even when
|
|
@@ -3165,91 +3188,15 @@ window.addEventListener("message", (e) => {
|
|
|
3165
3188
|
showPreviewBodyMenu(x, y, String(e.data.selectedText || ""), e.source, String(e.data.linkUrl || "") || undefined, String(e.data.linkText || "") || undefined, String(e.data.imgSrc || "") || undefined);
|
|
3166
3189
|
return;
|
|
3167
3190
|
}
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
}
|
|
3178
|
-
const x = (iframeRect?.left || 0) + (e.data.x || 0);
|
|
3179
|
-
const y = (iframeRect?.top || 0) + (e.data.y || 0);
|
|
3180
|
-
const url = e.data.url || "";
|
|
3181
|
-
// Find a sensible filename for the Save action.
|
|
3182
|
-
const guessName = (() => {
|
|
3183
|
-
try {
|
|
3184
|
-
const u = new URL(url);
|
|
3185
|
-
const last = u.pathname.split("/").pop() || "";
|
|
3186
|
-
return last && last.includes(".") ? last : "";
|
|
3187
|
-
}
|
|
3188
|
-
catch {
|
|
3189
|
-
return "";
|
|
3190
|
-
}
|
|
3191
|
-
})();
|
|
3192
|
-
const items = [
|
|
3193
|
-
{ label: "Open in browser", action: () => {
|
|
3194
|
-
window.open(url, "_blank", "noopener,noreferrer");
|
|
3195
|
-
} },
|
|
3196
|
-
{ label: guessName ? `Save "${guessName}"…` : "Save link as…", action: () => {
|
|
3197
|
-
// Trigger a download via anchor with download attr.
|
|
3198
|
-
const a = document.createElement("a");
|
|
3199
|
-
a.href = url;
|
|
3200
|
-
if (guessName)
|
|
3201
|
-
a.download = guessName;
|
|
3202
|
-
else
|
|
3203
|
-
a.download = "";
|
|
3204
|
-
a.style.display = "none";
|
|
3205
|
-
document.body.appendChild(a);
|
|
3206
|
-
a.click();
|
|
3207
|
-
setTimeout(() => a.remove(), 1000);
|
|
3208
|
-
} },
|
|
3209
|
-
{ label: "Copy URL", action: async () => {
|
|
3210
|
-
try {
|
|
3211
|
-
await navigator.clipboard.writeText(url);
|
|
3212
|
-
}
|
|
3213
|
-
catch {
|
|
3214
|
-
prompt("URL:", url);
|
|
3215
|
-
}
|
|
3216
|
-
} },
|
|
3217
|
-
{ label: "Copy link text", action: async () => {
|
|
3218
|
-
try {
|
|
3219
|
-
await navigator.clipboard.writeText(e.data.text || url);
|
|
3220
|
-
}
|
|
3221
|
-
catch {
|
|
3222
|
-
prompt("Text:", e.data.text || url);
|
|
3223
|
-
}
|
|
3224
|
-
} },
|
|
3225
|
-
];
|
|
3226
|
-
// Build a tiny inline menu (showContextMenu would do but it's in components/).
|
|
3227
|
-
const menu = document.createElement("div");
|
|
3228
|
-
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;`;
|
|
3229
|
-
menu.style.left = `${Math.min(x, window.innerWidth - 200)}px`;
|
|
3230
|
-
menu.style.top = `${Math.min(y, window.innerHeight - 200)}px`;
|
|
3231
|
-
// mousedown inside the menu must NOT reach the document-level
|
|
3232
|
-
// dismiss handler — otherwise the menu is removed before click
|
|
3233
|
-
// fires on the row and the action silently no-ops (user report
|
|
3234
|
-
// 2026-04-24). Stop propagation at the menu root covers every row.
|
|
3235
|
-
menu.addEventListener("mousedown", (ev) => ev.stopPropagation());
|
|
3236
|
-
for (const it of items) {
|
|
3237
|
-
const row = document.createElement("div");
|
|
3238
|
-
row.textContent = it.label;
|
|
3239
|
-
row.style.cssText = `padding:6px 12px;cursor:pointer;`;
|
|
3240
|
-
row.addEventListener("mouseenter", () => row.style.background = "var(--color-bg-hover)");
|
|
3241
|
-
row.addEventListener("mouseleave", () => row.style.background = "");
|
|
3242
|
-
row.addEventListener("click", () => { menu.remove(); it.action(); });
|
|
3243
|
-
menu.appendChild(row);
|
|
3244
|
-
}
|
|
3245
|
-
document.body.appendChild(menu);
|
|
3246
|
-
const dismiss = () => { menu.remove(); document.removeEventListener("mousedown", dismiss); document.removeEventListener("keydown", dismiss); };
|
|
3247
|
-
setTimeout(() => {
|
|
3248
|
-
document.addEventListener("mousedown", dismiss);
|
|
3249
|
-
document.addEventListener("keydown", dismiss);
|
|
3250
|
-
}, 0);
|
|
3251
|
-
return;
|
|
3252
|
-
}
|
|
3191
|
+
// (removed 2026-08-19) A second, dead link context menu used to live
|
|
3192
|
+
// here — a "linkContextMenu" postMessage handler labelled "Copy URL",
|
|
3193
|
+
// built when the preview iframe still posted that message type. Nothing
|
|
3194
|
+
// has posted it since the viewer moved to showBodyContextMenu(), so it
|
|
3195
|
+
// was unreachable code offering a different label ("Copy URL") for the
|
|
3196
|
+
// same action the live menu calls "Copy link URL" — the kind of drift
|
|
3197
|
+
// Bob spotted 2026-08-19 ("other menus have both copy link and copy
|
|
3198
|
+
// url"). The live implementation is in components/message-viewer.ts;
|
|
3199
|
+
// keep link-menu changes there and nowhere else.
|
|
3253
3200
|
if (e.data?.type === "mailx-send-error") {
|
|
3254
3201
|
// Send failed AFTER compose closed (fire-and-forget model). Surface in
|
|
3255
3202
|
// the status bar so the user sees something instead of the silence.
|
|
@@ -3975,11 +3922,7 @@ async function openComposeFromMailto(m) {
|
|
|
3975
3922
|
references: m.inReplyTo ? [m.inReplyTo] : [],
|
|
3976
3923
|
accounts: accounts.map((a) => ({ id: a.id, name: a.name, email: a.email, signature: a.signature, sig: a.sig })),
|
|
3977
3924
|
};
|
|
3978
|
-
|
|
3979
|
-
try {
|
|
3980
|
-
frame?.contentWindow?.postMessage({ type: "compose-init-ready" }, "*");
|
|
3981
|
-
}
|
|
3982
|
-
catch { /* */ }
|
|
3925
|
+
handOffComposeInit(frame, init);
|
|
3983
3926
|
}
|
|
3984
3927
|
/** C46: open compose from a Windows Share sheet drop. Shared files become
|
|
3985
3928
|
* attachments; shared text/links become the body; a shared page title (or
|
|
@@ -4016,11 +3959,7 @@ async function openComposeFromShare(s) {
|
|
|
4016
3959
|
attachments: s.attachments,
|
|
4017
3960
|
accounts: accounts.map((a) => ({ id: a.id, name: a.name, email: a.email, signature: a.signature, sig: a.sig })),
|
|
4018
3961
|
};
|
|
4019
|
-
|
|
4020
|
-
try {
|
|
4021
|
-
frame?.contentWindow?.postMessage({ type: "compose-init-ready" }, "*");
|
|
4022
|
-
}
|
|
4023
|
-
catch { /* */ }
|
|
3962
|
+
handOffComposeInit(frame, init);
|
|
4024
3963
|
}
|
|
4025
3964
|
// ── Keyboard shortcuts ──
|
|
4026
3965
|
// Capture-phase pre-handler: intercept WebView accelerator keys that would
|
|
@@ -4734,9 +4673,14 @@ document.addEventListener("keydown", (e) => {
|
|
|
4734
4673
|
bd.remove();
|
|
4735
4674
|
closed = true;
|
|
4736
4675
|
}
|
|
4737
|
-
|
|
4676
|
+
// Was getElementById("theme-submenu") — an id nothing declares, so Escape
|
|
4677
|
+
// silently never closed the theme submenu (dead-code audit 2026-08-19).
|
|
4678
|
+
// The real element is #theme-submenu-popout; also reset the toggle's
|
|
4679
|
+
// aria-expanded so the button doesn't claim to be open.
|
|
4680
|
+
const themeSub = document.getElementById("theme-submenu-popout");
|
|
4738
4681
|
if (themeSub && !themeSub.hidden) {
|
|
4739
4682
|
themeSub.hidden = true;
|
|
4683
|
+
document.getElementById("theme-submenu-toggle")?.setAttribute("aria-expanded", "false");
|
|
4740
4684
|
closed = true;
|
|
4741
4685
|
}
|
|
4742
4686
|
if (closed) {
|
|
@@ -6560,8 +6504,7 @@ document.addEventListener("mailx-popout-message", (async (e) => {
|
|
|
6560
6504
|
if (loaded.length)
|
|
6561
6505
|
init.attachments = loaded;
|
|
6562
6506
|
}
|
|
6563
|
-
|
|
6564
|
-
showComposeOverlay(msg.subject ? `Edit: ${msg.subject}` : "Edit draft");
|
|
6507
|
+
handOffComposeInit(showComposeOverlay(msg.subject ? `Edit: ${msg.subject}` : "Edit draft"), init);
|
|
6565
6508
|
return;
|
|
6566
6509
|
}
|
|
6567
6510
|
// ONE read-only popout, not three. This handler used to build its own
|