@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.
Files changed (51) hide show
  1. package/TODO.md +1 -0
  2. package/client/android-bootstrap.bundle.js +34 -1
  3. package/client/android-bootstrap.bundle.js.map +2 -2
  4. package/client/app.bundle.js +54 -108
  5. package/client/app.bundle.js.map +3 -3
  6. package/client/app.js +59 -116
  7. package/client/app.js.map +1 -1
  8. package/client/app.ts +64 -93
  9. package/client/components/message-list.js +18 -3
  10. package/client/components/message-list.js.map +1 -1
  11. package/client/components/message-list.ts +18 -3
  12. package/client/components/message-viewer.js +14 -4
  13. package/client/components/message-viewer.js.map +1 -1
  14. package/client/components/message-viewer.ts +13 -3
  15. package/client/compose/compose.bundle.js +17 -6
  16. package/client/compose/compose.bundle.js.map +2 -2
  17. package/client/compose/compose.js +13 -7
  18. package/client/compose/compose.js.map +1 -1
  19. package/client/compose/compose.ts +13 -5
  20. package/client/lib/rmf-tiny.js +15 -1
  21. package/client/package.json +1 -1
  22. package/client/styles/components.css +9 -0
  23. package/docs/thundermail-jmap.md +291 -0
  24. package/package.json +11 -11
  25. package/packages/mailx-imap/index.d.ts.map +1 -1
  26. package/packages/mailx-imap/index.js +17 -1
  27. package/packages/mailx-imap/index.js.map +1 -1
  28. package/packages/mailx-imap/index.ts +17 -1
  29. package/packages/mailx-imap/package-lock.json +2 -2
  30. package/packages/mailx-imap/package.json +1 -1
  31. package/packages/mailx-settings/docs/thundermail-jmap.md +291 -0
  32. package/packages/mailx-settings/index.d.ts +1 -0
  33. package/packages/mailx-settings/index.d.ts.map +1 -1
  34. package/packages/mailx-settings/index.js +1 -0
  35. package/packages/mailx-settings/index.js.map +1 -1
  36. package/packages/mailx-settings/index.ts +1 -0
  37. package/packages/mailx-settings/package.json +1 -1
  38. package/packages/mailx-store/package.json +1 -1
  39. package/packages/mailx-store-web/package.json +1 -1
  40. package/packages/mailx-store-web/web-settings.d.ts +1 -0
  41. package/packages/mailx-store-web/web-settings.d.ts.map +1 -1
  42. package/packages/mailx-store-web/web-settings.js +1 -0
  43. package/packages/mailx-store-web/web-settings.js.map +1 -1
  44. package/packages/mailx-store-web/web-settings.ts +1 -0
  45. package/packages/mailx-types/index.d.ts +59 -1
  46. package/packages/mailx-types/index.d.ts.map +1 -1
  47. package/packages/mailx-types/index.js +72 -0
  48. package/packages/mailx-types/index.js.map +1 -1
  49. package/packages/mailx-types/index.ts +80 -1
  50. package/packages/mailx-types/package.json +1 -1
  51. /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
- // We post `compose-init-ready` to the iframe once init is in
1064
- // sessionStorage so compose.ts's IIFE can read synchronously without
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
- const unified = document.querySelector('.folder-tree .all-inboxes')
2364
- || document.getElementById("ft-all-inboxes");
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
- sessionStorage.setItem("composeInit", JSON.stringify(init));
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
- if (e.data?.type === "openLink" && e.data.url) {
3115
- const api = window.mailxapi;
3116
- if (api?.openExternal)
3117
- api.openExternal(e.data.url);
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
- if (e.data?.type === "previewToggleFullscreen") {
3149
- toggleFullscreenPreview();
3150
- return;
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
- if (e.data?.type === "linkContextMenu") {
3169
- // C29: right-click in body iframe Open / Save / Copy URL menu.
3170
- // Iframe's clientX/Y is relative to the iframe; translate to viewport.
3171
- let iframeRect = null;
3172
- for (const f of Array.from(document.querySelectorAll("iframe"))) {
3173
- if (f.contentWindow === e.source) {
3174
- iframeRect = f.getBoundingClientRect();
3175
- break;
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
- sessionStorage.setItem("composeInit", JSON.stringify(init));
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
- sessionStorage.setItem("composeInit", JSON.stringify(init));
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
- const themeSub = document.getElementById("theme-submenu");
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
- sessionStorage.setItem("composeInit", JSON.stringify(init));
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