@bobfrankston/rmfmail 1.2.281 → 1.2.283
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/.commitmsg +17 -63
- package/client/android-bootstrap.bundle.js +72 -3
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/client/app.bundle.js +9 -0
- package/client/app.bundle.js.map +2 -2
- package/client/components/message-viewer.js +24 -0
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +26 -0
- package/client/styles/components.css +18 -0
- package/npmchanges.md +131 -0
- package/package.json +5 -5
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/package.json +1 -1
- package/packages/mailx-store/store.d.ts +4 -1
- package/packages/mailx-store/store.d.ts.map +1 -1
- package/packages/mailx-store/store.js +11 -3
- package/packages/mailx-store/store.js.map +1 -1
- package/packages/mailx-store/store.ts +14 -3
- package/packages/mailx-store-web/package.json +1 -1
- package/packages/mailx-types/index.d.ts +2 -2
- package/packages/mailx-types/index.d.ts.map +1 -1
- package/packages/mailx-types/index.js +1 -1
- package/packages/mailx-types/index.js.map +1 -1
- package/packages/mailx-types/index.ts +2 -2
- package/packages/mailx-types/package.json +1 -1
- package/packages/mailx-types/trust.d.ts +26 -1
- package/packages/mailx-types/trust.d.ts.map +1 -1
- package/packages/mailx-types/trust.js +131 -5
- package/packages/mailx-types/trust.js.map +1 -1
- package/packages/mailx-types/trust.ts +155 -5
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-65528 → node_modules.npmglobalize-stash-23452}/.package-lock.json +0 -0
package/.commitmsg
CHANGED
|
@@ -1,63 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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.
|
|
1
|
+
Show the spam score only when it means something
|
|
2
|
+
|
|
3
|
+
"Only show the spam score if it is high and likely." The first cut put a chip on
|
|
4
|
+
every message that carried a score, which is nearly all of them — a badge that
|
|
5
|
+
is always there carries no information and turns into furniture the eye stops
|
|
6
|
+
reading, which is the same failure mode the findings themselves are built to
|
|
7
|
+
avoid.
|
|
8
|
+
|
|
9
|
+
The chip now renders only at or above halfway to the server's own threshold.
|
|
10
|
+
That floor is a proportion, not a constant, so it follows if the threshold
|
|
11
|
+
changes. Halfway is where "did not quite trip the wire" stops meaning the same
|
|
12
|
+
thing as "clean": the sextortion mail scored 2.7 against 4.5 and was delivered
|
|
13
|
+
unflagged, so showing only what the server flagged would miss precisely the
|
|
14
|
+
case worth showing. Two levels remain — flagged, and past halfway — because
|
|
15
|
+
anything quieter no longer renders.
|
|
16
|
+
|
|
17
|
+
Findings are unaffected: they never depended on the score.
|
|
@@ -2248,13 +2248,25 @@ function selfSpoof(input) {
|
|
|
2248
2248
|
if (!own.has(from))
|
|
2249
2249
|
return null;
|
|
2250
2250
|
const auth = headerAll(input.headerLines, "authentication-results").join(" ; ");
|
|
2251
|
+
if (!auth)
|
|
2252
|
+
return null;
|
|
2251
2253
|
const failed = [
|
|
2252
2254
|
/\bspf=(fail|softfail)\b/i.test(auth) ? "SPF" : "",
|
|
2253
2255
|
/\bdkim=(fail|permerror)\b/i.test(auth) ? "DKIM" : "",
|
|
2254
2256
|
/\bdmarc=(fail|permerror)\b/i.test(auth) ? "DMARC" : ""
|
|
2255
2257
|
].filter(Boolean);
|
|
2256
|
-
if (!failed.length)
|
|
2257
|
-
|
|
2258
|
+
if (!failed.length) {
|
|
2259
|
+
if (/\b(spf|dkim|dmarc)=pass\b/i.test(auth))
|
|
2260
|
+
return null;
|
|
2261
|
+
const envelopeAddr = bare(header(input.headerLines, "return-path"));
|
|
2262
|
+
const note = envelopeAddr && envelopeAddr !== from ? `; envelope sender ${envelopeAddr}` : "";
|
|
2263
|
+
return {
|
|
2264
|
+
id: "self-spoof",
|
|
2265
|
+
severity: "danger",
|
|
2266
|
+
text: `This claims to be from your own address, ${from}, and nothing proves it came from your account.`,
|
|
2267
|
+
detail: `no SPF, DKIM or DMARC result to check against${note} \u2014 your domain publishes no SPF record, so forgery cannot be detected by the sending side`
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2258
2270
|
const envelope = bare(header(input.headerLines, "return-path"));
|
|
2259
2271
|
const envelopeNote = envelope && envelope !== from ? `; actually sent by ${envelope}` : "";
|
|
2260
2272
|
return {
|
|
@@ -2326,17 +2338,73 @@ function assessMessageTrust(input) {
|
|
|
2326
2338
|
const findings = [
|
|
2327
2339
|
serverSpamVerdict(input.headerLines),
|
|
2328
2340
|
selfSpoof(input),
|
|
2341
|
+
relayAuthMismatch(input),
|
|
2342
|
+
zeroWidthObfuscation(input.bodyText || ""),
|
|
2329
2343
|
hiddenLinkOverlay(input.bodyHtml || ""),
|
|
2330
2344
|
redirectorLink(input.bodyHtml || "")
|
|
2331
2345
|
].filter(Boolean);
|
|
2332
2346
|
const rank = { danger: 0, caution: 1 };
|
|
2333
2347
|
return findings.sort((a, b) => rank[a.severity] - rank[b.severity]);
|
|
2334
2348
|
}
|
|
2335
|
-
|
|
2349
|
+
function relayAuthMismatch(input) {
|
|
2350
|
+
const from = bare(input.fromAddress);
|
|
2351
|
+
const fromDomain = from.split("@")[1] || "";
|
|
2352
|
+
if (!fromDomain)
|
|
2353
|
+
return null;
|
|
2354
|
+
for (const name of RELAY_IDENTITY_HEADERS) {
|
|
2355
|
+
const stamped = bare(header(input.headerLines, name));
|
|
2356
|
+
const stampedDomain = stamped.split("@")[1] || "";
|
|
2357
|
+
if (!stampedDomain || stampedDomain === fromDomain)
|
|
2358
|
+
continue;
|
|
2359
|
+
if (stampedDomain.endsWith("." + fromDomain) || fromDomain.endsWith("." + stampedDomain))
|
|
2360
|
+
continue;
|
|
2361
|
+
return {
|
|
2362
|
+
id: "relay-auth-mismatch",
|
|
2363
|
+
severity: "danger",
|
|
2364
|
+
text: `The account that actually sent this belongs to someone else: ${stampedDomain}, not ${fromDomain}.`,
|
|
2365
|
+
detail: `the sending server recorded ${stamped} as the authenticated sender, while the message claims to be from ${from}`
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2368
|
+
return null;
|
|
2369
|
+
}
|
|
2370
|
+
function zeroWidthObfuscation(bodyText) {
|
|
2371
|
+
if (!bodyText)
|
|
2372
|
+
return null;
|
|
2373
|
+
const between = bodyText.match(/[A-Za-z][][A-Za-z]/g);
|
|
2374
|
+
const count = between ? between.length : 0;
|
|
2375
|
+
if (count < ZERO_WIDTH_MIN_OCCURRENCES)
|
|
2376
|
+
return null;
|
|
2377
|
+
return {
|
|
2378
|
+
id: "zero-width-obfuscation",
|
|
2379
|
+
severity: "danger",
|
|
2380
|
+
text: "The words in this message are stuffed with invisible characters to get past spam filters.",
|
|
2381
|
+
detail: `${count} zero-width characters hidden inside words \u2014 legitimate mail has no reason to do this`
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
2384
|
+
function spamScoreOf(headerLines) {
|
|
2385
|
+
const status = header(headerLines, "x-spam-status");
|
|
2386
|
+
if (!status)
|
|
2387
|
+
return null;
|
|
2388
|
+
const score = Number(status.match(/score=([-\d.]+)/)?.[1]);
|
|
2389
|
+
const threshold = Number(status.match(/required=([-\d.]+)/)?.[1]);
|
|
2390
|
+
if (!Number.isFinite(score) || !Number.isFinite(threshold))
|
|
2391
|
+
return null;
|
|
2392
|
+
return { score, threshold, flagged: /^yes/i.test(status) };
|
|
2393
|
+
}
|
|
2394
|
+
var OVERLAY_MIN_EDGE_PX, RELAY_IDENTITY_HEADERS, ZERO_WIDTH_MIN_OCCURRENCES;
|
|
2336
2395
|
var init_trust = __esm({
|
|
2337
2396
|
"packages/mailx-types/trust.js"() {
|
|
2338
2397
|
"use strict";
|
|
2339
2398
|
OVERLAY_MIN_EDGE_PX = 2e3;
|
|
2399
|
+
RELAY_IDENTITY_HEADERS = [
|
|
2400
|
+
"x-source-auth",
|
|
2401
|
+
// cPanel / Exim (Bluehost, HostGator, most shared hosts)
|
|
2402
|
+
"x-authenticated-sender",
|
|
2403
|
+
// Postfix / Communigate
|
|
2404
|
+
"x-auth-id",
|
|
2405
|
+
"x-authenticated-user"
|
|
2406
|
+
];
|
|
2407
|
+
ZERO_WIDTH_MIN_OCCURRENCES = 12;
|
|
2340
2408
|
}
|
|
2341
2409
|
});
|
|
2342
2410
|
|
|
@@ -2739,6 +2807,7 @@ __export(mailx_types_exports, {
|
|
|
2739
2807
|
setDraft: () => setDraft,
|
|
2740
2808
|
setFlagged: () => setFlagged,
|
|
2741
2809
|
setSeen: () => setSeen,
|
|
2810
|
+
spamScoreOf: () => spamScoreOf,
|
|
2742
2811
|
splitRecipients: () => splitRecipients,
|
|
2743
2812
|
toggleFlagged: () => toggleFlagged,
|
|
2744
2813
|
toggleSeen: () => toggleSeen
|