@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.
- package/client/app.bundle.js +55 -20
- package/client/app.bundle.js.map +2 -2
- package/client/components/message-viewer.js +89 -22
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +95 -19
- package/client/package.json +1 -1
- package/client/styles/components.css +45 -1
- package/package.json +3 -3
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-41916 → node_modules.npmglobalize-stash-88864}/.package-lock.json +0 -0
|
@@ -687,6 +687,14 @@ function installPreviewControls(iframe: HTMLIFrameElement): void {
|
|
|
687
687
|
const attach = () => {
|
|
688
688
|
const doc = iframe.contentDocument;
|
|
689
689
|
if (!doc) return;
|
|
690
|
+
// Idempotent per DOCUMENT. A freshly-appended srcdoc iframe first
|
|
691
|
+
// exposes a throwaway `about:blank` document whose readyState is
|
|
692
|
+
// already "complete" — so binding to it is binding to something that
|
|
693
|
+
// is discarded a frame later, and every path below has to run again
|
|
694
|
+
// against the real one. Marking the document (not the iframe) lets us
|
|
695
|
+
// safely attach from both the immediate and the load path.
|
|
696
|
+
if ((doc as any).__mvControlsBound) return;
|
|
697
|
+
(doc as any).__mvControlsBound = true;
|
|
690
698
|
|
|
691
699
|
applyZoom(doc);
|
|
692
700
|
// Paint search marks as soon as the text exists — `attach` also runs
|
|
@@ -747,8 +755,14 @@ function installPreviewControls(iframe: HTMLIFrameElement): void {
|
|
|
747
755
|
// host; the doc-level handler missed cases where WebView2's native
|
|
748
756
|
// menu fired before our parent listener got installed.
|
|
749
757
|
};
|
|
758
|
+
// Bind on BOTH paths, not one or the other: `load` is when the real
|
|
759
|
+
// srcdoc document exists, and the immediate call covers an iframe that
|
|
760
|
+
// is genuinely already loaded (re-render of a live preview). The
|
|
761
|
+
// per-document guard in attach() makes the overlap a no-op — where the
|
|
762
|
+
// old `complete ? attach() : onload` choice could bind the whole set to
|
|
763
|
+
// the discarded about:blank document and never run against the letter.
|
|
764
|
+
iframe.addEventListener("load", attach);
|
|
750
765
|
if (iframe.contentDocument?.readyState === "complete") attach();
|
|
751
|
-
else iframe.addEventListener("load", attach, { once: true });
|
|
752
766
|
// DOMContentLoaded is the "text is painted" mark (see the _ptick pair at
|
|
753
767
|
// the render site); highlight there too so marks appear with the words
|
|
754
768
|
// rather than after the last tracking pixel resolves.
|
|
@@ -1199,18 +1213,7 @@ export async function showMessage(accountId: string, uid: number, folderId?: num
|
|
|
1199
1213
|
const fromEl = headerEl.querySelector(".mv-from")!;
|
|
1200
1214
|
const toEl = headerEl.querySelector(".mv-to")!;
|
|
1201
1215
|
fromEl.textContent = formatAddr(msg.from);
|
|
1202
|
-
|
|
1203
|
-
if (msg.cc?.length) toLine += ` Cc: ${msg.cc.map(formatAddr).join(", ")}`;
|
|
1204
|
-
// Always-visible Delivered-To line — shown when present and not already
|
|
1205
|
-
// covered by the To/Cc list. Critical for accounts with multiple aliases
|
|
1206
|
-
// where you need to see which one received the message at a glance.
|
|
1207
|
-
const toAddrs = (msg.to || []).map((a: { address: string }) => a.address.toLowerCase());
|
|
1208
|
-
const ccAddrs = (msg.cc || []).map((a: { address: string }) => a.address.toLowerCase());
|
|
1209
|
-
const dt = (msg.deliveredTo || "").toLowerCase();
|
|
1210
|
-
if (msg.deliveredTo && !toAddrs.includes(dt) && !ccAddrs.includes(dt)) {
|
|
1211
|
-
toLine += ` Delivered-To: ${msg.deliveredTo}`;
|
|
1212
|
-
}
|
|
1213
|
-
toEl.textContent = toLine;
|
|
1216
|
+
setRecipientLine(toEl, msg.to, msg.cc, msg.deliveredTo);
|
|
1214
1217
|
headerEl.querySelector(".mv-subject")!.textContent = msg.subject;
|
|
1215
1218
|
document.dispatchEvent(new CustomEvent("mailx-message-shown", { detail: { accountId } }));
|
|
1216
1219
|
|
|
@@ -1221,7 +1224,12 @@ export async function showMessage(accountId: string, uid: number, folderId?: num
|
|
|
1221
1224
|
e.preventDefault();
|
|
1222
1225
|
const me = e as MouseEvent;
|
|
1223
1226
|
const items: MenuItem[] = [];
|
|
1224
|
-
const
|
|
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;
|
|
1225
1233
|
for (const addr of addrs) {
|
|
1226
1234
|
if (!addr?.address) continue;
|
|
1227
1235
|
const name = addr.name || "";
|
|
@@ -1304,6 +1312,13 @@ export async function showMessage(accountId: string, uid: number, folderId?: num
|
|
|
1304
1312
|
});
|
|
1305
1313
|
items.push({ label: "", action: () => {}, separator: true });
|
|
1306
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
|
+
}
|
|
1307
1322
|
items.push({ label: "Reply", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "reply" } })) });
|
|
1308
1323
|
items.push({ label: "Reply All", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "replyAll" } })) });
|
|
1309
1324
|
items.push({ label: "Forward", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "forward" } })) });
|
|
@@ -2129,6 +2144,71 @@ function formatAddr(addr: { name: string; address: string }): string {
|
|
|
2129
2144
|
return addr.address;
|
|
2130
2145
|
}
|
|
2131
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
|
+
|
|
2132
2212
|
/** Render the viewer header from a list-row envelope (instant — no body
|
|
2133
2213
|
* fetch awaited). Used to populate the header pane the moment a message
|
|
2134
2214
|
* is clicked so the user always sees something actionable; getMessage()
|
|
@@ -2142,11 +2222,7 @@ function renderHeaderFromEnvelope(headerEl: HTMLElement, env: any): void {
|
|
|
2142
2222
|
const subjEl = headerEl.querySelector(".mv-subject");
|
|
2143
2223
|
const dateEl = headerEl.querySelector(".mv-date");
|
|
2144
2224
|
if (fromEl) fromEl.textContent = formatAddr(env.from);
|
|
2145
|
-
if (toEl)
|
|
2146
|
-
let toLine = `To: ${(env.to || []).map(formatAddr).join(", ")}`;
|
|
2147
|
-
if (env.cc?.length) toLine += ` Cc: ${env.cc.map(formatAddr).join(", ")}`;
|
|
2148
|
-
toEl.textContent = toLine;
|
|
2149
|
-
}
|
|
2225
|
+
if (toEl) setRecipientLine(toEl, env.to, env.cc);
|
|
2150
2226
|
if (subjEl) subjEl.textContent = env.subject || "";
|
|
2151
2227
|
if (dateEl) {
|
|
2152
2228
|
try { dateEl.textContent = new Date(env.date).toLocaleString(); }
|
package/client/package.json
CHANGED
|
@@ -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 {
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|