@bobfrankston/rmfmail 1.2.261 → 1.2.268
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 +1 -0
- package/client/android-bootstrap.bundle.js +32 -0
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/client/app.bundle.js +102 -126
- package/client/app.bundle.js.map +3 -3
- package/client/app.js +59 -116
- package/client/app.js.map +1 -1
- package/client/app.ts +64 -93
- package/client/components/message-list.js +18 -3
- package/client/components/message-list.js.map +1 -1
- package/client/components/message-list.ts +18 -3
- package/client/components/message-viewer.js +84 -24
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +90 -21
- package/client/compose/compose.bundle.js +16 -5
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/compose/compose.js +5 -6
- package/client/compose/compose.js.map +1 -1
- package/client/compose/compose.ts +5 -4
- package/client/lib/rmf-tiny.js +15 -1
- package/client/package.json +1 -1
- package/client/styles/components.css +54 -1
- package/docs/thundermail-jmap.md +291 -0
- package/package.json +9 -9
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +17 -1
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +17 -1
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-store-web/package.json +1 -1
- package/packages/mailx-types/index.d.ts +51 -0
- package/packages/mailx-types/index.d.ts.map +1 -1
- package/packages/mailx-types/index.js +72 -0
- package/packages/mailx-types/index.js.map +1 -1
- package/packages/mailx-types/index.ts +72 -0
- package/packages/mailx-types/package.json +1 -1
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-62112 → node_modules.npmglobalize-stash-83744}/.package-lock.json +0 -0
|
@@ -15,7 +15,7 @@ import type { MenuItem } from "./context-menu.js";
|
|
|
15
15
|
import type { ListMessage } from "../lib/message-state.js";
|
|
16
16
|
import { updateMessageFlags as stateUpdateFlags } from "../lib/message-state.js";
|
|
17
17
|
import { setRowSeen } from "./message-list.js";
|
|
18
|
-
import { setSeen, seenOf } from "@bobfrankston/mailx-types";
|
|
18
|
+
import { setSeen, seenOf, formatSender } from "@bobfrankston/mailx-types";
|
|
19
19
|
|
|
20
20
|
// ── "Copy image" bridge ──
|
|
21
21
|
// The preview iframe encodes the image and posts the bytes back; these track
|
|
@@ -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
|
-
|
|
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
|
|
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" } })) });
|
|
@@ -2139,8 +2140,80 @@ export async function showMessage(accountId: string, uid: number, folderId?: num
|
|
|
2139
2140
|
}
|
|
2140
2141
|
|
|
2141
2142
|
function formatAddr(addr: { name: string; address: string }): string {
|
|
2142
|
-
|
|
2143
|
-
|
|
2143
|
+
// formatSender collapses the redundant `"x@y.com" <x@y.com>` form (Microsoft
|
|
2144
|
+
// 365 quarantine notices and plenty of bulk senders emit it; showing the
|
|
2145
|
+
// same address twice reads as if two parties were involved — Bob
|
|
2146
|
+
// 2026-08-19) and reports when a display name asserts a DIFFERENT address
|
|
2147
|
+
// than the envelope's, which is the display-name spoof.
|
|
2148
|
+
const { text, claimed } = formatSender(addr);
|
|
2149
|
+
if (!claimed) return text;
|
|
2150
|
+
// Never let the claim stand unqualified: name the real address explicitly.
|
|
2151
|
+
return `${text} — display name claims ${claimed}`;
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
/** Above this many To+Cc addresses the recipient line is offered collapsed
|
|
2155
|
+
* with a "▸ N recipients" expander. Anything under it reads fine inline. */
|
|
2156
|
+
const MANY_RECIPIENTS = 6;
|
|
2157
|
+
|
|
2158
|
+
/** Ceiling on how many addresses the header's right-click menu enumerates.
|
|
2159
|
+
* Each address contributes ~7 entries (copy name / copy address / contacts /
|
|
2160
|
+
* preferred / denylist / priority), so a 444-recipient bulk mail would build
|
|
2161
|
+
* a 3000-item menu taller than the screen and slow to open. */
|
|
2162
|
+
const CONTEXT_MENU_ADDR_CAP = 12;
|
|
2163
|
+
|
|
2164
|
+
/**
|
|
2165
|
+
* Fill the viewer's To/Cc line.
|
|
2166
|
+
*
|
|
2167
|
+
* Written as elements rather than a bare `textContent` because bulk mail can
|
|
2168
|
+
* carry hundreds of recipients: the full text stays in the DOM (so find,
|
|
2169
|
+
* select and copy still see every address) but `.mv-to-text` is line-clamped
|
|
2170
|
+
* to two rows, with a toggle to expand. Unclamped, that one line grew the
|
|
2171
|
+
* header past the whole viewer height and starved `.mv-body` to zero — see
|
|
2172
|
+
* the `.mv-header` comment in components.css.
|
|
2173
|
+
*/
|
|
2174
|
+
function setRecipientLine(
|
|
2175
|
+
toEl: Element,
|
|
2176
|
+
to: { name: string; address: string }[] | undefined,
|
|
2177
|
+
cc: { name: string; address: string }[] | undefined,
|
|
2178
|
+
deliveredTo?: string,
|
|
2179
|
+
): void {
|
|
2180
|
+
const toList = to || [];
|
|
2181
|
+
const ccList = cc || [];
|
|
2182
|
+
let line = `To: ${toList.map(formatAddr).join(", ")}`;
|
|
2183
|
+
if (ccList.length) line += ` Cc: ${ccList.map(formatAddr).join(", ")}`;
|
|
2184
|
+
// Always-visible Delivered-To — shown when present and not already covered
|
|
2185
|
+
// by the To/Cc list. Critical for accounts with multiple aliases where you
|
|
2186
|
+
// need to see which one received the message at a glance.
|
|
2187
|
+
if (deliveredTo) {
|
|
2188
|
+
const dt = deliveredTo.toLowerCase();
|
|
2189
|
+
const covered = [...toList, ...ccList].some(a => a.address.toLowerCase() === dt);
|
|
2190
|
+
if (!covered) line += ` Delivered-To: ${deliveredTo}`;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
toEl.textContent = "";
|
|
2194
|
+
toEl.classList.remove("mv-to-expanded");
|
|
2195
|
+
const text = document.createElement("span");
|
|
2196
|
+
text.className = "mv-to-text";
|
|
2197
|
+
text.textContent = line;
|
|
2198
|
+
toEl.append(text);
|
|
2199
|
+
|
|
2200
|
+
const count = toList.length + ccList.length;
|
|
2201
|
+
// Offer the expander when the list is long by count, or when the clamp
|
|
2202
|
+
// actually cut something off (a handful of very long display names).
|
|
2203
|
+
const clamped = text.scrollHeight > text.clientHeight + 1;
|
|
2204
|
+
if (count <= MANY_RECIPIENTS && !clamped) return;
|
|
2205
|
+
|
|
2206
|
+
const toggle = document.createElement("button");
|
|
2207
|
+
toggle.className = "mv-to-toggle";
|
|
2208
|
+
toggle.type = "button";
|
|
2209
|
+
const label = count === 1 ? "1 recipient" : `${count} recipients`;
|
|
2210
|
+
const paint = (open: boolean) => {
|
|
2211
|
+
toggle.textContent = `${open ? "▾" : "▸"} ${label}`;
|
|
2212
|
+
toggle.title = open ? "Collapse the recipient list" : "Show the full recipient list";
|
|
2213
|
+
};
|
|
2214
|
+
paint(false);
|
|
2215
|
+
toggle.addEventListener("click", () => paint(toEl.classList.toggle("mv-to-expanded")));
|
|
2216
|
+
toEl.append(toggle);
|
|
2144
2217
|
}
|
|
2145
2218
|
|
|
2146
2219
|
/** Render the viewer header from a list-row envelope (instant — no body
|
|
@@ -2156,11 +2229,7 @@ function renderHeaderFromEnvelope(headerEl: HTMLElement, env: any): void {
|
|
|
2156
2229
|
const subjEl = headerEl.querySelector(".mv-subject");
|
|
2157
2230
|
const dateEl = headerEl.querySelector(".mv-date");
|
|
2158
2231
|
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
|
-
}
|
|
2232
|
+
if (toEl) setRecipientLine(toEl, env.to, env.cc);
|
|
2164
2233
|
if (subjEl) subjEl.textContent = env.subject || "";
|
|
2165
2234
|
if (dateEl) {
|
|
2166
2235
|
try { dateEl.textContent = new Date(env.date).toLocaleString(); }
|
|
@@ -2832,7 +2832,21 @@ async function createTinyMceEditor(container2, opts = {}) {
|
|
|
2832
2832
|
// an undefined/oversized line-height plus per-line font-size
|
|
2833
2833
|
// changes threw it off. A definite, tight line-height gives the
|
|
2834
2834
|
// marker a stable baseline to sit under.
|
|
2835
|
-
|
|
2835
|
+
// The editable body MUST fill the iframe. TinyMCE sizes the
|
|
2836
|
+
// body to its content, so on a short/empty draft the body was
|
|
2837
|
+
// ~18px inside a ~430px iframe and every point below the first
|
|
2838
|
+
// line hit <html> instead — not editable. Consequences, both
|
|
2839
|
+
// reported by Bob 2026-08-19 with a screenshot: WebView2 showed
|
|
2840
|
+
// its PAGE context menu (Back / Refresh / Save as / Print) with
|
|
2841
|
+
// no Cut/Copy/Paste, and msger's `ContextMenuTarget.IsEditable()`
|
|
2842
|
+
// gate suppressed the Bold/Italic/…/View HTML source items — so
|
|
2843
|
+
// right-clicking anywhere in an empty compose gave a menu with
|
|
2844
|
+
// nothing useful in it. Filling the body makes the whole editor
|
|
2845
|
+
// area editable, which is also what makes click-below-text put
|
|
2846
|
+
// the caret at the end. Verified in a hit-test harness: body
|
|
2847
|
+
// 18px → 429px, and points at 30/60/90% go html→body.
|
|
2848
|
+
"html { height: 100%; }",
|
|
2849
|
+
`body { font-family: system-ui, sans-serif; font-size: ${opts.fontSizePx ?? 14}px; line-height: 1.3; caret-color: #0a2647; min-height: 100%; box-sizing: border-box; }`,
|
|
2836
2850
|
"blockquote { border-left: 3px solid #c0c8d0; margin: 0 0 0 4px; padding: 2px 0 2px 10px; color: #555; }",
|
|
2837
2851
|
// Drop the 0.5em margin-gap above the reply block and the per-line
|
|
2838
2852
|
// font-size shrink on the attribution line — both contributed to
|
|
@@ -6202,10 +6216,7 @@ var _parentInitListeners = [];
|
|
|
6202
6216
|
var _msgEventCount = 0;
|
|
6203
6217
|
window.addEventListener("message", (e) => {
|
|
6204
6218
|
_msgEventCount++;
|
|
6205
|
-
if (e.data?.type === "compose-init
|
|
6206
|
-
_parentInitReady = true;
|
|
6207
|
-
for (const fn of _parentInitListeners.splice(0)) fn();
|
|
6208
|
-
} else if (e.data?.type === "compose-init" && e.data.init) {
|
|
6219
|
+
if (e.data?.type === "compose-init" && e.data.init) {
|
|
6209
6220
|
if (!_postedInit) {
|
|
6210
6221
|
try {
|
|
6211
6222
|
logClientEvent("compose-init-received", { src: "postMessage" });
|