@bobfrankston/rmfmail 1.2.237 → 1.2.239

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 CHANGED
@@ -693,6 +693,12 @@ This is not tidiness. The RFC 2047 header fix on 2026-08-08 was hand-applied to
693
693
  - ~~**Context menus** — three clipboard implementations (rich editor, compose recipient field, nothing at all elsewhere)~~ — **FIXED v1.2.230**, see DONE.md.
694
694
  - **`escapeHtml`** — 15 separate definitions across client + packages. [S]
695
695
 
696
+ <a id="ext163"></a>
697
+
698
+ ### C163 — Edit-in-Word documents open in Compatibility Mode [↑ summary](#sum)
699
+
700
+ Word's title bar reads "<file> — **Compatibility Mode** — Word" for every Edit-in-Word hand-off: `html-to-docx` emits a document Word treats as an older format. Measured 2026-08-09 while chasing the round-trip bug; it does NOT disable proofing (Word reported `NoProofing=0`, `LanguageID=1033` and flagged 3/3 planted misspellings through COM), so it is not the cause of "it didn't do a spell check" — but it does disable newer Word features and is a plausible source of round-trip fidelity loss through mammoth. Worth either configuring html-to-docx to emit a current-format docx or checking whether a `w:compat` block / missing `docProps/app.xml` is what trips it. [S/M]
701
+
696
702
  ## Priority 1 — Get basics working [↑ top](#top)
697
703
 
698
704
  ### External content security
@@ -2701,13 +2701,6 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
2701
2701
  _ptick("iframe srcdoc set");
2702
2702
  installPreviewControls(iframe);
2703
2703
  } else if (msg.bodyText) {
2704
- const pre = document.createElement("pre");
2705
- pre.style.cssText = "padding: 1rem; white-space: pre-wrap; word-break: break-word; font-family: system-ui, sans-serif; font-size: 17.5px; line-height: 1.5; color: #1a1a2e; background: #fff; margin: 0; height: 100%; overflow: auto;";
2706
- if (msg.bodyText.length > PROGRESSIVE_TEXT_THRESHOLD) {
2707
- renderTextProgressively(pre, msg.bodyText, gen);
2708
- } else {
2709
- pre.innerHTML = linkifyText(msg.bodyText);
2710
- }
2711
2704
  const trunc = msg.bodyTruncated;
2712
2705
  if (trunc) {
2713
2706
  const mb = (n) => `${(n / 1048576).toFixed(1)} MB`;
@@ -2721,7 +2714,21 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
2721
2714
  <code style="user-select:all;font-size:0.9em">${escapeHtml3(trunc.emlPath || "(path unavailable)")}</code></div>`;
2722
2715
  bodyEl.appendChild(note);
2723
2716
  }
2724
- bodyEl.appendChild(pre);
2717
+ const iframe = document.createElement("iframe");
2718
+ iframe.sandbox.add("allow-same-origin");
2719
+ iframe.sandbox.add("allow-popups");
2720
+ iframe.sandbox.add("allow-popups-to-escape-sandbox");
2721
+ iframe.sandbox.add("allow-top-navigation-by-user-activation");
2722
+ iframe.sandbox.add("allow-scripts");
2723
+ const head = msg.bodyText.length > PROGRESSIVE_TEXT_THRESHOLD ? msg.bodyText.slice(0, firstChunkEnd(msg.bodyText)) : msg.bodyText;
2724
+ iframe.srcdoc = wrapHtmlBody(`<pre class="rmf-plain" id="rmf-plain">${linkifyText(head)}</pre>`, false);
2725
+ iframe.addEventListener("load", () => _ptick("iframe load (all resources)"), { once: true });
2726
+ bodyEl.appendChild(iframe);
2727
+ _ptick("iframe srcdoc set (plain text)");
2728
+ installPreviewControls(iframe);
2729
+ if (msg.bodyText.length > head.length) {
2730
+ appendTextProgressively(iframe, msg.bodyText.slice(head.length), gen);
2731
+ }
2725
2732
  } else {
2726
2733
  const fetchErr = recentFetchErrors.get(`${accountId}:${uid}`);
2727
2734
  if (fetchErr) {
@@ -2973,31 +2980,54 @@ function renderHeaderFromEnvelope(headerEl, env) {
2973
2980
  function escapeHtml3(s) {
2974
2981
  return (s || "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
2975
2982
  }
2976
- function renderTextProgressively(pre, text, gen) {
2983
+ function firstChunkEnd(text) {
2984
+ const end = Math.min(TEXT_CHUNK_BYTES, text.length);
2985
+ if (end >= text.length)
2986
+ return text.length;
2987
+ const nl = text.lastIndexOf("\n", end);
2988
+ return nl > 0 ? nl + 1 : end;
2989
+ }
2990
+ function appendTextProgressively(iframe, rest, gen) {
2977
2991
  const chunks = [];
2978
2992
  let at = 0;
2979
- while (at < text.length) {
2980
- let end = Math.min(at + TEXT_CHUNK_BYTES, text.length);
2981
- if (end < text.length) {
2982
- const nl = text.lastIndexOf("\n", end);
2993
+ while (at < rest.length) {
2994
+ let end = Math.min(at + TEXT_CHUNK_BYTES, rest.length);
2995
+ if (end < rest.length) {
2996
+ const nl = rest.lastIndexOf("\n", end);
2983
2997
  if (nl > at)
2984
2998
  end = nl + 1;
2985
2999
  }
2986
- chunks.push(text.slice(at, end));
3000
+ chunks.push(rest.slice(at, end));
2987
3001
  at = end;
2988
3002
  }
2989
3003
  let i = 0;
2990
3004
  const step = () => {
2991
3005
  if (gen !== showMessageGeneration)
2992
3006
  return;
3007
+ const doc = iframe.contentDocument;
3008
+ const host = doc?.getElementById("rmf-plain");
3009
+ if (!doc || !host)
3010
+ return;
2993
3011
  if (i >= chunks.length)
2994
3012
  return;
2995
- const span = document.createElement("span");
3013
+ const span = doc.createElement("span");
2996
3014
  span.innerHTML = linkifyText(chunks[i++]);
2997
- pre.appendChild(span);
3015
+ host.appendChild(span);
2998
3016
  requestAnimationFrame(step);
2999
3017
  };
3000
- step();
3018
+ let waits = 0;
3019
+ const begin = () => {
3020
+ if (gen !== showMessageGeneration)
3021
+ return;
3022
+ if (iframe.contentDocument?.getElementById("rmf-plain")) {
3023
+ step();
3024
+ return;
3025
+ }
3026
+ if (waits++ > 120)
3027
+ return;
3028
+ requestAnimationFrame(begin);
3029
+ };
3030
+ requestAnimationFrame(begin);
3001
3031
  }
3002
3032
  function linkifyText(text) {
3003
3033
  const escaped = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -3130,6 +3160,19 @@ ${csp}
3130
3160
  img { max-width: 100%; }
3131
3161
  a { color: #1a6dd4; }
3132
3162
  pre, code { white-space: pre-wrap; }
3163
+ /* Plain-text bodies render as this <pre>. Sans-serif at the same size and
3164
+ line-height as an HTML body, so two messages in one thread don't look
3165
+ like they came from different apps (Bob 2026-05-12: "why the font
3166
+ change"). */
3167
+ .rmf-plain {
3168
+ margin: 0;
3169
+ padding: 0;
3170
+ font-family: system-ui, sans-serif;
3171
+ font-size: 17.5px;
3172
+ line-height: 1.5;
3173
+ white-space: pre-wrap;
3174
+ word-break: break-word;
3175
+ }
3133
3176
  blockquote { border-left: 3px solid #ccc; padding-left: 1rem; margin-left: 0; color: #666; }
3134
3177
  @media (prefers-color-scheme: dark) {
3135
3178
  body { color: #cdd6f4; background: #282840; }