@bobfrankston/rmfmail 1.2.261 → 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.
@@ -1213,18 +1213,7 @@ export async function showMessage(accountId: string, uid: number, folderId?: num
1213
1213
  const fromEl = headerEl.querySelector(".mv-from")!;
1214
1214
  const toEl = headerEl.querySelector(".mv-to")!;
1215
1215
  fromEl.textContent = formatAddr(msg.from);
1216
- let toLine = `To: ${msg.to.map(formatAddr).join(", ")}`;
1217
- if (msg.cc?.length) toLine += ` Cc: ${msg.cc.map(formatAddr).join(", ")}`;
1218
- // Always-visible Delivered-To line — shown when present and not already
1219
- // covered by the To/Cc list. Critical for accounts with multiple aliases
1220
- // where you need to see which one received the message at a glance.
1221
- const toAddrs = (msg.to || []).map((a: { address: string }) => a.address.toLowerCase());
1222
- const ccAddrs = (msg.cc || []).map((a: { address: string }) => a.address.toLowerCase());
1223
- const dt = (msg.deliveredTo || "").toLowerCase();
1224
- if (msg.deliveredTo && !toAddrs.includes(dt) && !ccAddrs.includes(dt)) {
1225
- toLine += ` Delivered-To: ${msg.deliveredTo}`;
1226
- }
1227
- toEl.textContent = toLine;
1216
+ setRecipientLine(toEl, msg.to, msg.cc, msg.deliveredTo);
1228
1217
  headerEl.querySelector(".mv-subject")!.textContent = msg.subject;
1229
1218
  document.dispatchEvent(new CustomEvent("mailx-message-shown", { detail: { accountId } }));
1230
1219
 
@@ -1235,7 +1224,12 @@ export async function showMessage(accountId: string, uid: number, folderId?: num
1235
1224
  e.preventDefault();
1236
1225
  const me = e as MouseEvent;
1237
1226
  const items: MenuItem[] = [];
1238
- const addrs = el === fromEl ? [msg.from] : [...(msg.to || []), ...(msg.cc || [])];
1227
+ const allAddrs = el === fromEl ? [msg.from] : [...(msg.to || []), ...(msg.cc || [])];
1228
+ // Bulk mail can address hundreds of people; enumerating every
1229
+ // one builds a menu thousands of items tall. Show the first
1230
+ // few and say how many were left out.
1231
+ const addrs = allAddrs.slice(0, CONTEXT_MENU_ADDR_CAP);
1232
+ const omitted = allAddrs.length - addrs.length;
1239
1233
  for (const addr of addrs) {
1240
1234
  if (!addr?.address) continue;
1241
1235
  const name = addr.name || "";
@@ -1318,6 +1312,13 @@ export async function showMessage(accountId: string, uid: number, folderId?: num
1318
1312
  });
1319
1313
  items.push({ label: "", action: () => {}, separator: true });
1320
1314
  }
1315
+ if (omitted > 0) {
1316
+ items.push({
1317
+ label: `… ${omitted} more recipients — copy the whole list`,
1318
+ action: () => navigator.clipboard.writeText(allAddrs.map(formatAddr).join(", ")),
1319
+ });
1320
+ items.push({ label: "", action: () => {}, separator: true });
1321
+ }
1321
1322
  items.push({ label: "Reply", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "reply" } })) });
1322
1323
  items.push({ label: "Reply All", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "replyAll" } })) });
1323
1324
  items.push({ label: "Forward", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "forward" } })) });
@@ -2143,6 +2144,71 @@ function formatAddr(addr: { name: string; address: string }): string {
2143
2144
  return addr.address;
2144
2145
  }
2145
2146
 
2147
+ /** Above this many To+Cc addresses the recipient line is offered collapsed
2148
+ * with a "▸ N recipients" expander. Anything under it reads fine inline. */
2149
+ const MANY_RECIPIENTS = 6;
2150
+
2151
+ /** Ceiling on how many addresses the header's right-click menu enumerates.
2152
+ * Each address contributes ~7 entries (copy name / copy address / contacts /
2153
+ * preferred / denylist / priority), so a 444-recipient bulk mail would build
2154
+ * a 3000-item menu taller than the screen and slow to open. */
2155
+ const CONTEXT_MENU_ADDR_CAP = 12;
2156
+
2157
+ /**
2158
+ * Fill the viewer's To/Cc line.
2159
+ *
2160
+ * Written as elements rather than a bare `textContent` because bulk mail can
2161
+ * carry hundreds of recipients: the full text stays in the DOM (so find,
2162
+ * select and copy still see every address) but `.mv-to-text` is line-clamped
2163
+ * to two rows, with a toggle to expand. Unclamped, that one line grew the
2164
+ * header past the whole viewer height and starved `.mv-body` to zero — see
2165
+ * the `.mv-header` comment in components.css.
2166
+ */
2167
+ function setRecipientLine(
2168
+ toEl: Element,
2169
+ to: { name: string; address: string }[] | undefined,
2170
+ cc: { name: string; address: string }[] | undefined,
2171
+ deliveredTo?: string,
2172
+ ): void {
2173
+ const toList = to || [];
2174
+ const ccList = cc || [];
2175
+ let line = `To: ${toList.map(formatAddr).join(", ")}`;
2176
+ if (ccList.length) line += ` Cc: ${ccList.map(formatAddr).join(", ")}`;
2177
+ // Always-visible Delivered-To — shown when present and not already covered
2178
+ // by the To/Cc list. Critical for accounts with multiple aliases where you
2179
+ // need to see which one received the message at a glance.
2180
+ if (deliveredTo) {
2181
+ const dt = deliveredTo.toLowerCase();
2182
+ const covered = [...toList, ...ccList].some(a => a.address.toLowerCase() === dt);
2183
+ if (!covered) line += ` Delivered-To: ${deliveredTo}`;
2184
+ }
2185
+
2186
+ toEl.textContent = "";
2187
+ toEl.classList.remove("mv-to-expanded");
2188
+ const text = document.createElement("span");
2189
+ text.className = "mv-to-text";
2190
+ text.textContent = line;
2191
+ toEl.append(text);
2192
+
2193
+ const count = toList.length + ccList.length;
2194
+ // Offer the expander when the list is long by count, or when the clamp
2195
+ // actually cut something off (a handful of very long display names).
2196
+ const clamped = text.scrollHeight > text.clientHeight + 1;
2197
+ if (count <= MANY_RECIPIENTS && !clamped) return;
2198
+
2199
+ const toggle = document.createElement("button");
2200
+ toggle.className = "mv-to-toggle";
2201
+ toggle.type = "button";
2202
+ const label = count === 1 ? "1 recipient" : `${count} recipients`;
2203
+ const paint = (open: boolean) => {
2204
+ toggle.textContent = `${open ? "▾" : "▸"} ${label}`;
2205
+ toggle.title = open ? "Collapse the recipient list" : "Show the full recipient list";
2206
+ };
2207
+ paint(false);
2208
+ toggle.addEventListener("click", () => paint(toEl.classList.toggle("mv-to-expanded")));
2209
+ toEl.append(toggle);
2210
+ }
2211
+
2146
2212
  /** Render the viewer header from a list-row envelope (instant — no body
2147
2213
  * fetch awaited). Used to populate the header pane the moment a message
2148
2214
  * is clicked so the user always sees something actionable; getMessage()
@@ -2156,11 +2222,7 @@ function renderHeaderFromEnvelope(headerEl: HTMLElement, env: any): void {
2156
2222
  const subjEl = headerEl.querySelector(".mv-subject");
2157
2223
  const dateEl = headerEl.querySelector(".mv-date");
2158
2224
  if (fromEl) fromEl.textContent = formatAddr(env.from);
2159
- if (toEl) {
2160
- let toLine = `To: ${(env.to || []).map(formatAddr).join(", ")}`;
2161
- if (env.cc?.length) toLine += ` Cc: ${env.cc.map(formatAddr).join(", ")}`;
2162
- toEl.textContent = toLine;
2163
- }
2225
+ if (toEl) setRecipientLine(toEl, env.to, env.cc);
2164
2226
  if (subjEl) subjEl.textContent = env.subject || "";
2165
2227
  if (dateEl) {
2166
2228
  try { dateEl.textContent = new Date(env.date).toLocaleString(); }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-client",
3
- "version": "1.0.36",
3
+ "version": "1.0.37",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1976,6 +1976,16 @@ body.calendar-sidebar-on .calendar-sidebar { display: flex; }
1976
1976
  font-size: var(--font-size-sm);
1977
1977
  line-height: 1.5;
1978
1978
 
1979
+ /* The header is a min-content-sized item in .message-viewer's flex column,
1980
+ so anything unbounded inside it (a 400-recipient bulk-mail To: list, an
1981
+ expanded Details panel with a full Received chain) squeezes .mv-body
1982
+ down to zero height — and since .message-viewer is overflow:hidden, the
1983
+ pane then has nothing that scrolls at all. Cap the header and let IT
1984
+ scroll; .mv-body always keeps at least half the pane. */
1985
+ flex: 0 0 auto;
1986
+ max-height: 50%;
1987
+ overflow-y: auto;
1988
+
1979
1989
  .mv-toolbar {
1980
1990
  display: flex;
1981
1991
  align-items: center;
@@ -1984,6 +1994,13 @@ body.calendar-sidebar-on .calendar-sidebar { display: flex; }
1984
1994
  border-bottom: 1px solid var(--color-border);
1985
1995
  margin-bottom: var(--gap-xs);
1986
1996
 
1997
+ /* Stays put when the header itself scrolls (long recipient list,
1998
+ Details open) — the actions must never scroll out of reach. */
1999
+ position: sticky;
2000
+ top: 0;
2001
+ background: var(--color-bg-surface);
2002
+ z-index: 1;
2003
+
1987
2004
  /* Larger, easier-to-hit action glyphs — reply / reply-all /
1988
2005
  * forward / delete / flag / mark-unread are single-glyph buttons. */
1989
2006
  .tb-btn {
@@ -1997,7 +2014,34 @@ body.calendar-sidebar-on .calendar-sidebar { display: flex; }
1997
2014
  .mv-header-info { }
1998
2015
  .mv-header-actions { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; flex-shrink: 0; }
1999
2016
  .mv-from { font-weight: 600; }
2000
- .mv-to { color: var(--color-text-muted); }
2017
+ .mv-to {
2018
+ color: var(--color-text-muted);
2019
+
2020
+ /* Bulk mail routinely carries hundreds of To: addresses (the Geek
2021
+ Squad scam Bob hit on 2026-08-17 had 444). Clamp to two rows; the
2022
+ "▸ N recipients" button below expands the full list in place. */
2023
+ .mv-to-text {
2024
+ display: -webkit-box;
2025
+ -webkit-box-orient: vertical;
2026
+ -webkit-line-clamp: 2;
2027
+ line-clamp: 2;
2028
+ overflow: hidden;
2029
+ }
2030
+ &.mv-to-expanded .mv-to-text {
2031
+ display: block;
2032
+ -webkit-line-clamp: none;
2033
+ line-clamp: none;
2034
+ }
2035
+ .mv-to-toggle {
2036
+ background: none;
2037
+ border: none;
2038
+ padding: 0;
2039
+ font: inherit;
2040
+ color: var(--color-accent);
2041
+ cursor: pointer;
2042
+ text-decoration: underline;
2043
+ }
2044
+ }
2001
2045
  .mv-subject { font-size: var(--font-size-lg); font-weight: 600; margin-top: var(--gap-xs); }
2002
2046
  /* Search matches in the subject. Same treatment as the body (which paints
2003
2047
  its own copy of this rule inside the preview iframe — the iframe can't
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/rmfmail",
3
- "version": "1.2.261",
3
+ "version": "1.2.263",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -38,7 +38,7 @@
38
38
  "@bobfrankston/mailx-store-web": "^0.1.80",
39
39
  "@bobfrankston/mailx-sync": "^0.1.29",
40
40
  "@bobfrankston/miscinfo": "^1.0.19",
41
- "@bobfrankston/msger": "^0.1.425",
41
+ "@bobfrankston/msger": "^0.1.427",
42
42
  "@bobfrankston/node-tcp-transport": "^0.1.10",
43
43
  "@bobfrankston/oauthsupport": "^1.0.34",
44
44
  "@bobfrankston/rmf-tiny": "^0.1.49",
@@ -118,7 +118,7 @@
118
118
  "@bobfrankston/mailx-store-web": "^0.1.80",
119
119
  "@bobfrankston/mailx-sync": "^0.1.29",
120
120
  "@bobfrankston/miscinfo": "^1.0.19",
121
- "@bobfrankston/msger": "^0.1.425",
121
+ "@bobfrankston/msger": "^0.1.427",
122
122
  "@bobfrankston/node-tcp-transport": "^0.1.10",
123
123
  "@bobfrankston/oauthsupport": "^1.0.34",
124
124
  "@bobfrankston/rmf-tiny": "^0.1.49",