@bobfrankston/rmfmail 1.2.280 → 1.2.281

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.
Files changed (35) hide show
  1. package/.commitmsg +63 -19
  2. package/client/android-bootstrap.bundle.js +145 -0
  3. package/client/android-bootstrap.bundle.js.map +3 -3
  4. package/client/app.bundle.js +16 -0
  5. package/client/app.bundle.js.map +3 -3
  6. package/client/components/message-viewer.js +27 -1
  7. package/client/components/message-viewer.js.map +1 -1
  8. package/client/components/message-viewer.ts +29 -1
  9. package/client/styles/components.css +46 -0
  10. package/npmchanges.md +22 -0
  11. package/package.json +1 -1
  12. package/packages/mailx-imap/package-lock.json +2 -2
  13. package/packages/mailx-imap/package.json +1 -1
  14. package/packages/mailx-settings/docs/editor.md +29 -3
  15. package/packages/mailx-settings/docs/preferences.md +2 -1
  16. package/packages/mailx-settings/package.json +1 -1
  17. package/packages/mailx-store/package.json +1 -1
  18. package/packages/mailx-store/store.d.ts +4 -0
  19. package/packages/mailx-store/store.d.ts.map +1 -1
  20. package/packages/mailx-store/store.js +18 -1
  21. package/packages/mailx-store/store.js.map +1 -1
  22. package/packages/mailx-store/store.ts +22 -1
  23. package/packages/mailx-store-web/package.json +1 -1
  24. package/packages/mailx-types/index.d.ts +2 -0
  25. package/packages/mailx-types/index.d.ts.map +1 -1
  26. package/packages/mailx-types/index.js +18 -0
  27. package/packages/mailx-types/index.js.map +1 -1
  28. package/packages/mailx-types/index.ts +20 -0
  29. package/packages/mailx-types/package.json +1 -1
  30. package/packages/mailx-types/trust.d.ts +66 -0
  31. package/packages/mailx-types/trust.d.ts.map +1 -0
  32. package/packages/mailx-types/trust.js +228 -0
  33. package/packages/mailx-types/trust.js.map +1 -0
  34. package/packages/mailx-types/trust.ts +239 -0
  35. /package/packages/mailx-imap/{node_modules.npmglobalize-stash-18028 → node_modules.npmglobalize-stash-65528}/.package-lock.json +0 -0
package/.commitmsg CHANGED
@@ -1,19 +1,63 @@
1
- About: show the bundled editor version
2
-
3
- CORRECTION to 1.2.279. That release added the resolved tinymce / tinymce-i18n
4
- versions to build-tinymce.js's console output, on the claim that this put the
5
- answer in the build transcript every publish archives. It does not:
6
- npmglobalize swallows a sub-build's stdout and reports only "✓ Build
7
- succeeded", so the line never reaches c:\temp\rebuild\. The echo is still
8
- correct when the script is run directly, but the build log is not where this
9
- question gets answered.
10
-
11
- About… now carries an "Editor bundle" row — "TinyMCE 8.8.2 · 61 languages"
12
- read at display time from client/lib/tinymce/package.json and langs/index.json,
13
- the same tree the editor loads from, so it cannot drift from what is actually
14
- running. That answers it on the machine showing the symptom, which is where it
15
- matters, rather than requiring a checkout of the matching tag.
16
-
17
- Why this keeps mattering: both specs are caret ranges, so the resolved version
18
- moves whenever npm re-resolves the tree — tinymce sat at 8.6.0 for weeks under
19
- a ^8.5.0 spec with nothing to notice.
1
+ Say when a message is forged
2
+
3
+ Two phishes arrived on 2026-08-25 and mailx rendered both without a word of
4
+ warning ("this is an attack could you do a better job of identifying it as
5
+ bogus?"). They carried, in the message itself:
6
+
7
+ X-Spam-Flag: YES <- Bob's own server, score 6.7/4.5
8
+ Authentication-Results: ... spf=fail
9
+ From: bobip@bobf.frankston.com <- his own alias, forged
10
+ Return-Path: <3EDTkWNvbY@visionsurveysqld.com.au>
11
+ <area coords="0,0,10000,10000" href="http://.../redirect.php?newurl=...">
12
+
13
+ Not one of those headers was read anywhere in the codebase no X-Spam-*, no
14
+ Authentication-Results, nothing. The one impersonation check that existed
15
+ (displayNameClaimsOtherAddress) needs a display name, and these had none. Worse,
16
+ the card was a cid: image, so hasRemoteContent was false and even the
17
+ remote-content banner stayed away: the message displayed clean.
18
+
19
+ The whole visible card was one hidden link. sanitizeHtml stripped their forms
20
+ and inputs and left <area> untouched, so the trap was live in the reader —
21
+ every pixel of the picture pointed at an open redirector on a real French
22
+ company's site, which forwarded to a throwaway host with the victim's own
23
+ address base64'd into the fragment so the landing page could prefill it.
24
+
25
+ packages/mailx-types/trust.ts — assessMessageTrust, four checks, all evidence
26
+ rather than scoring:
27
+
28
+ server-spam-verdict The receiving server already decided. Free, and it was
29
+ being thrown away. Only a POSITIVE verdict is trusted:
30
+ anyone can inject X-Spam-Flag: NO into mail they send,
31
+ and honouring that would hand attackers an off switch.
32
+ A forged YES only flags the forger.
33
+ self-spoof From is one of your own addresses AND authentication
34
+ explicitly failed. Either half alone is ordinary —
35
+ people mail themselves notes, forwarded mail fails SPF.
36
+ Together they are not. Own addresses come from the
37
+ recipient allowlist plus Delivered-To, so an alias
38
+ never added to the allowlist is still covered.
39
+ hidden-link-overlay An <area> hotspot bigger than any real image. Runs on
40
+ the RAW html, before the sanitizer disarms it.
41
+ redirector-link A link whose query carries another absolute URL. Names
42
+ the ENDPOINT — a reader can't URL-decode a query string.
43
+
44
+ Deliberately absent: bare SPF failure on mail not claiming to be you.
45
+ Forwarded mail fails SPF constantly, and flagging it would be the cry-wolf
46
+ banner this is trying not to be. Ordinary mail returns nothing at all — no
47
+ "looks fine" reassurance, which would be a claim this cannot support.
48
+
49
+ sanitizeHtml now also disarms image maps outright (<map> and usemap=).
50
+ Unconditional rather than size-gated: image maps have no legitimate use in
51
+ mail, so there is no threshold to tune and nothing to slip past by choosing a
52
+ smaller rectangle. The image still renders; only its hidden clickability goes,
53
+ and the banner explains why it stopped responding.
54
+
55
+ The banner is its own element above the body, not part of the remote-content
56
+ one — the two are independent, as the cid: case proved — and it never
57
+ collapses. A reader who must expand a dropdown to learn a message is forged has
58
+ already been asked to trust it. Each finding shows its evidence so it can be
59
+ checked rather than believed.
60
+
61
+ tests/trust.test.ts pins both phishes and, as importantly, the negative cases:
62
+ a self-sent note with no auth headers, SPF failure on mail not claiming to be
63
+ you, a modest image map, a same-site return-path parameter, and ordinary mail.
@@ -2199,6 +2199,147 @@ var init_contact_rules = __esm({
2199
2199
  }
2200
2200
  });
2201
2201
 
2202
+ // packages/mailx-types/trust.js
2203
+ function header(lines, name) {
2204
+ const want = name.toLowerCase();
2205
+ for (const h of lines) {
2206
+ if ((h.key || "").toLowerCase() !== want)
2207
+ continue;
2208
+ const colon = h.line.indexOf(":");
2209
+ return colon < 0 ? "" : h.line.slice(colon + 1).trim();
2210
+ }
2211
+ return "";
2212
+ }
2213
+ function headerAll(lines, name) {
2214
+ const want = name.toLowerCase();
2215
+ const out = [];
2216
+ for (const h of lines) {
2217
+ if ((h.key || "").toLowerCase() !== want)
2218
+ continue;
2219
+ const colon = h.line.indexOf(":");
2220
+ if (colon >= 0)
2221
+ out.push(h.line.slice(colon + 1).trim());
2222
+ }
2223
+ return out;
2224
+ }
2225
+ function bare(addr) {
2226
+ const m = (addr || "").match(/[^\s<>,;]+@[^\s<>,;]+/);
2227
+ return (m ? m[0] : addr || "").toLowerCase().replace(/[.,;]+$/, "");
2228
+ }
2229
+ function serverSpamVerdict(lines) {
2230
+ const flag = header(lines, "x-spam-flag");
2231
+ const status = header(lines, "x-spam-status");
2232
+ if (!/^yes/i.test(flag) && !/^yes/i.test(status))
2233
+ return null;
2234
+ const score = status.match(/score=([-\d.]+)/)?.[1];
2235
+ const required = status.match(/required=([-\d.]+)/)?.[1];
2236
+ return {
2237
+ id: "server-spam-verdict",
2238
+ severity: "danger",
2239
+ text: "Your mail server classified this as spam before delivering it.",
2240
+ detail: score && required ? `SpamAssassin score ${score}, threshold ${required}` : status || flag || "X-Spam-Flag: YES"
2241
+ };
2242
+ }
2243
+ function selfSpoof(input) {
2244
+ const from = bare(input.fromAddress);
2245
+ if (!from)
2246
+ return null;
2247
+ const own = new Set(input.ownAddresses.map(bare).filter(Boolean));
2248
+ if (!own.has(from))
2249
+ return null;
2250
+ const auth = headerAll(input.headerLines, "authentication-results").join(" ; ");
2251
+ const failed = [
2252
+ /\bspf=(fail|softfail)\b/i.test(auth) ? "SPF" : "",
2253
+ /\bdkim=(fail|permerror)\b/i.test(auth) ? "DKIM" : "",
2254
+ /\bdmarc=(fail|permerror)\b/i.test(auth) ? "DMARC" : ""
2255
+ ].filter(Boolean);
2256
+ if (!failed.length)
2257
+ return null;
2258
+ const envelope = bare(header(input.headerLines, "return-path"));
2259
+ const envelopeNote = envelope && envelope !== from ? `; actually sent by ${envelope}` : "";
2260
+ return {
2261
+ id: "self-spoof",
2262
+ severity: "danger",
2263
+ text: `This claims to be from your own address, ${from}, but it did not come from your account.`,
2264
+ detail: `${failed.join(" and ")} authentication failed${envelopeNote}`
2265
+ };
2266
+ }
2267
+ function hiddenLinkOverlay(bodyHtml) {
2268
+ for (const m of bodyHtml.matchAll(/<area\b[^>]*>/gi)) {
2269
+ const tag = m[0];
2270
+ const href = tag.match(/href\s*=\s*["']([^"']+)["']/i)?.[1];
2271
+ if (!href)
2272
+ continue;
2273
+ const coords = (tag.match(/coords\s*=\s*["']([^"']+)["']/i)?.[1] || "").split(/[,\s]+/).map(Number).filter((n) => Number.isFinite(n));
2274
+ if (coords.length < 4)
2275
+ continue;
2276
+ const width = Math.abs(coords[2] - coords[0]);
2277
+ const height = Math.abs(coords[3] - coords[1]);
2278
+ if (width < OVERLAY_MIN_EDGE_PX || height < OVERLAY_MIN_EDGE_PX)
2279
+ continue;
2280
+ let host = href;
2281
+ try {
2282
+ host = new URL(href).hostname;
2283
+ } catch {
2284
+ }
2285
+ return {
2286
+ id: "hidden-link-overlay",
2287
+ severity: "danger",
2288
+ text: "The picture in this message is one large hidden link \u2014 clicking anywhere on it goes to the same place.",
2289
+ detail: `${width}x${height} clickable area over the image, pointing at ${host}`
2290
+ };
2291
+ }
2292
+ return null;
2293
+ }
2294
+ function redirectorLink(bodyHtml) {
2295
+ for (const m of bodyHtml.matchAll(/href\s*=\s*["']([^"']+)["']/gi)) {
2296
+ let url;
2297
+ try {
2298
+ url = new URL(m[1]);
2299
+ } catch {
2300
+ continue;
2301
+ }
2302
+ if (!/^https?:$/.test(url.protocol))
2303
+ continue;
2304
+ for (const [, value] of url.searchParams) {
2305
+ let target;
2306
+ try {
2307
+ target = new URL(value);
2308
+ } catch {
2309
+ continue;
2310
+ }
2311
+ if (!/^https?:$/.test(target.protocol))
2312
+ continue;
2313
+ if (target.hostname === url.hostname)
2314
+ continue;
2315
+ return {
2316
+ id: "redirector-link",
2317
+ severity: "caution",
2318
+ text: `A link hides where it goes: it passes through ${url.hostname} and ends at ${target.hostname}.`,
2319
+ detail: `${url.hostname} -> ${target.hostname}`
2320
+ };
2321
+ }
2322
+ }
2323
+ return null;
2324
+ }
2325
+ function assessMessageTrust(input) {
2326
+ const findings = [
2327
+ serverSpamVerdict(input.headerLines),
2328
+ selfSpoof(input),
2329
+ hiddenLinkOverlay(input.bodyHtml || ""),
2330
+ redirectorLink(input.bodyHtml || "")
2331
+ ].filter(Boolean);
2332
+ const rank = { danger: 0, caution: 1 };
2333
+ return findings.sort((a, b) => rank[a.severity] - rank[b.severity]);
2334
+ }
2335
+ var OVERLAY_MIN_EDGE_PX;
2336
+ var init_trust = __esm({
2337
+ "packages/mailx-types/trust.js"() {
2338
+ "use strict";
2339
+ OVERLAY_MIN_EDGE_PX = 2e3;
2340
+ }
2341
+ });
2342
+
2202
2343
  // packages/mailx-types/contacts-config.js
2203
2344
  function parseContactsConfig(raw) {
2204
2345
  if (!raw)
@@ -2570,6 +2711,7 @@ __export(mailx_types_exports, {
2570
2711
  addContactsDenylistEntry: () => addContactsDenylistEntry,
2571
2712
  addContactsPreferredEntry: () => addContactsPreferredEntry,
2572
2713
  answeredOf: () => answeredOf,
2714
+ assessMessageTrust: () => assessMessageTrust,
2573
2715
  buildMimeMessage: () => buildMimeMessage,
2574
2716
  deletedOf: () => deletedOf,
2575
2717
  displayNameClaimsOtherAddress: () => displayNameClaimsOtherAddress,
@@ -2636,6 +2778,8 @@ function sanitizeHtml(html) {
2636
2778
  clean = clean.replace(/<select\b[^>]*>[\s\S]*?<\/select>/gi, "[blocked: select]");
2637
2779
  clean = clean.replace(/<button\b[^>]*>([\s\S]*?)<\/button>/gi, "$1");
2638
2780
  clean = clean.replace(/<iframe\b[^>]*>[\s\S]*?<\/iframe>/gi, "");
2781
+ clean = clean.replace(/<map\b[^>]*>[\s\S]*?<\/map>/gi, "");
2782
+ clean = clean.replace(/\s+usemap\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "");
2639
2783
  return { html: clean, hasRemoteContent };
2640
2784
  }
2641
2785
  function encodeHeaderWord(text) {
@@ -3061,6 +3205,7 @@ var init_mailx_types = __esm({
3061
3205
  "packages/mailx-types/index.js"() {
3062
3206
  "use strict";
3063
3207
  init_contact_rules();
3208
+ init_trust();
3064
3209
  init_contacts_config();
3065
3210
  init_groups();
3066
3211
  init_reminder_state();