@bobfrankston/rmfmail 1.2.260 → 1.2.263

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.
@@ -746,6 +746,15 @@ function installPreviewControls(iframe) {
746
746
  const doc = iframe.contentDocument;
747
747
  if (!doc)
748
748
  return;
749
+ // Idempotent per DOCUMENT. A freshly-appended srcdoc iframe first
750
+ // exposes a throwaway `about:blank` document whose readyState is
751
+ // already "complete" — so binding to it is binding to something that
752
+ // is discarded a frame later, and every path below has to run again
753
+ // against the real one. Marking the document (not the iframe) lets us
754
+ // safely attach from both the immediate and the load path.
755
+ if (doc.__mvControlsBound)
756
+ return;
757
+ doc.__mvControlsBound = true;
749
758
  applyZoom(doc);
750
759
  // Paint search marks as soon as the text exists — `attach` also runs
751
760
  // on `load`, which on a remote-image newsletter is many seconds after
@@ -816,10 +825,15 @@ function installPreviewControls(iframe) {
816
825
  // host; the doc-level handler missed cases where WebView2's native
817
826
  // menu fired before our parent listener got installed.
818
827
  };
828
+ // Bind on BOTH paths, not one or the other: `load` is when the real
829
+ // srcdoc document exists, and the immediate call covers an iframe that
830
+ // is genuinely already loaded (re-render of a live preview). The
831
+ // per-document guard in attach() makes the overlap a no-op — where the
832
+ // old `complete ? attach() : onload` choice could bind the whole set to
833
+ // the discarded about:blank document and never run against the letter.
834
+ iframe.addEventListener("load", attach);
819
835
  if (iframe.contentDocument?.readyState === "complete")
820
836
  attach();
821
- else
822
- iframe.addEventListener("load", attach, { once: true });
823
837
  // DOMContentLoaded is the "text is painted" mark (see the _ptick pair at
824
838
  // the render site); highlight there too so marks appear with the words
825
839
  // rather than after the last tracking pixel resolves.
@@ -1325,19 +1339,7 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
1325
1339
  const fromEl = headerEl.querySelector(".mv-from");
1326
1340
  const toEl = headerEl.querySelector(".mv-to");
1327
1341
  fromEl.textContent = formatAddr(msg.from);
1328
- let toLine = `To: ${msg.to.map(formatAddr).join(", ")}`;
1329
- if (msg.cc?.length)
1330
- toLine += ` Cc: ${msg.cc.map(formatAddr).join(", ")}`;
1331
- // Always-visible Delivered-To line — shown when present and not already
1332
- // covered by the To/Cc list. Critical for accounts with multiple aliases
1333
- // where you need to see which one received the message at a glance.
1334
- const toAddrs = (msg.to || []).map((a) => a.address.toLowerCase());
1335
- const ccAddrs = (msg.cc || []).map((a) => a.address.toLowerCase());
1336
- const dt = (msg.deliveredTo || "").toLowerCase();
1337
- if (msg.deliveredTo && !toAddrs.includes(dt) && !ccAddrs.includes(dt)) {
1338
- toLine += ` Delivered-To: ${msg.deliveredTo}`;
1339
- }
1340
- toEl.textContent = toLine;
1342
+ setRecipientLine(toEl, msg.to, msg.cc, msg.deliveredTo);
1341
1343
  headerEl.querySelector(".mv-subject").textContent = msg.subject;
1342
1344
  document.dispatchEvent(new CustomEvent("mailx-message-shown", { detail: { accountId } }));
1343
1345
  // Right-click on email addresses in header: copy name, copy address,
@@ -1347,7 +1349,12 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
1347
1349
  e.preventDefault();
1348
1350
  const me = e;
1349
1351
  const items = [];
1350
- const addrs = el === fromEl ? [msg.from] : [...(msg.to || []), ...(msg.cc || [])];
1352
+ const allAddrs = el === fromEl ? [msg.from] : [...(msg.to || []), ...(msg.cc || [])];
1353
+ // Bulk mail can address hundreds of people; enumerating every
1354
+ // one builds a menu thousands of items tall. Show the first
1355
+ // few and say how many were left out.
1356
+ const addrs = allAddrs.slice(0, CONTEXT_MENU_ADDR_CAP);
1357
+ const omitted = allAddrs.length - addrs.length;
1351
1358
  for (const addr of addrs) {
1352
1359
  if (!addr?.address)
1353
1360
  continue;
@@ -1438,6 +1445,13 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
1438
1445
  });
1439
1446
  items.push({ label: "", action: () => { }, separator: true });
1440
1447
  }
1448
+ if (omitted > 0) {
1449
+ items.push({
1450
+ label: `… ${omitted} more recipients — copy the whole list`,
1451
+ action: () => navigator.clipboard.writeText(allAddrs.map(formatAddr).join(", ")),
1452
+ });
1453
+ items.push({ label: "", action: () => { }, separator: true });
1454
+ }
1441
1455
  items.push({ label: "Reply", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "reply" } })) });
1442
1456
  items.push({ label: "Reply All", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "replyAll" } })) });
1443
1457
  items.push({ label: "Forward", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "forward" } })) });
@@ -2297,6 +2311,63 @@ function formatAddr(addr) {
2297
2311
  return `${addr.name} <${addr.address}>`;
2298
2312
  return addr.address;
2299
2313
  }
2314
+ /** Above this many To+Cc addresses the recipient line is offered collapsed
2315
+ * with a "▸ N recipients" expander. Anything under it reads fine inline. */
2316
+ const MANY_RECIPIENTS = 6;
2317
+ /** Ceiling on how many addresses the header's right-click menu enumerates.
2318
+ * Each address contributes ~7 entries (copy name / copy address / contacts /
2319
+ * preferred / denylist / priority), so a 444-recipient bulk mail would build
2320
+ * a 3000-item menu taller than the screen and slow to open. */
2321
+ const CONTEXT_MENU_ADDR_CAP = 12;
2322
+ /**
2323
+ * Fill the viewer's To/Cc line.
2324
+ *
2325
+ * Written as elements rather than a bare `textContent` because bulk mail can
2326
+ * carry hundreds of recipients: the full text stays in the DOM (so find,
2327
+ * select and copy still see every address) but `.mv-to-text` is line-clamped
2328
+ * to two rows, with a toggle to expand. Unclamped, that one line grew the
2329
+ * header past the whole viewer height and starved `.mv-body` to zero — see
2330
+ * the `.mv-header` comment in components.css.
2331
+ */
2332
+ function setRecipientLine(toEl, to, cc, deliveredTo) {
2333
+ const toList = to || [];
2334
+ const ccList = cc || [];
2335
+ let line = `To: ${toList.map(formatAddr).join(", ")}`;
2336
+ if (ccList.length)
2337
+ line += ` Cc: ${ccList.map(formatAddr).join(", ")}`;
2338
+ // Always-visible Delivered-To — shown when present and not already covered
2339
+ // by the To/Cc list. Critical for accounts with multiple aliases where you
2340
+ // need to see which one received the message at a glance.
2341
+ if (deliveredTo) {
2342
+ const dt = deliveredTo.toLowerCase();
2343
+ const covered = [...toList, ...ccList].some(a => a.address.toLowerCase() === dt);
2344
+ if (!covered)
2345
+ line += ` Delivered-To: ${deliveredTo}`;
2346
+ }
2347
+ toEl.textContent = "";
2348
+ toEl.classList.remove("mv-to-expanded");
2349
+ const text = document.createElement("span");
2350
+ text.className = "mv-to-text";
2351
+ text.textContent = line;
2352
+ toEl.append(text);
2353
+ const count = toList.length + ccList.length;
2354
+ // Offer the expander when the list is long by count, or when the clamp
2355
+ // actually cut something off (a handful of very long display names).
2356
+ const clamped = text.scrollHeight > text.clientHeight + 1;
2357
+ if (count <= MANY_RECIPIENTS && !clamped)
2358
+ return;
2359
+ const toggle = document.createElement("button");
2360
+ toggle.className = "mv-to-toggle";
2361
+ toggle.type = "button";
2362
+ const label = count === 1 ? "1 recipient" : `${count} recipients`;
2363
+ const paint = (open) => {
2364
+ toggle.textContent = `${open ? "▾" : "▸"} ${label}`;
2365
+ toggle.title = open ? "Collapse the recipient list" : "Show the full recipient list";
2366
+ };
2367
+ paint(false);
2368
+ toggle.addEventListener("click", () => paint(toEl.classList.toggle("mv-to-expanded")));
2369
+ toEl.append(toggle);
2370
+ }
2300
2371
  /** Render the viewer header from a list-row envelope (instant — no body
2301
2372
  * fetch awaited). Used to populate the header pane the moment a message
2302
2373
  * is clicked so the user always sees something actionable; getMessage()
@@ -2311,12 +2382,8 @@ function renderHeaderFromEnvelope(headerEl, env) {
2311
2382
  const dateEl = headerEl.querySelector(".mv-date");
2312
2383
  if (fromEl)
2313
2384
  fromEl.textContent = formatAddr(env.from);
2314
- if (toEl) {
2315
- let toLine = `To: ${(env.to || []).map(formatAddr).join(", ")}`;
2316
- if (env.cc?.length)
2317
- toLine += ` Cc: ${env.cc.map(formatAddr).join(", ")}`;
2318
- toEl.textContent = toLine;
2319
- }
2385
+ if (toEl)
2386
+ setRecipientLine(toEl, env.to, env.cc);
2320
2387
  if (subjEl)
2321
2388
  subjEl.textContent = env.subject || "";
2322
2389
  if (dateEl) {