@bobfrankston/rmfmail 1.2.298 → 1.2.300
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 +36 -28
- package/client/android-bootstrap.bundle.js +27 -7
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/client/app.bundle.js +1 -1
- package/client/app.bundle.js.map +2 -2
- package/client/components/message-viewer.js +14 -2
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +16 -3
- package/npmchanges.md +92 -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-web/package.json +1 -1
- package/packages/mailx-types/package.json +1 -1
- package/packages/mailx-types/trust.d.ts +4 -0
- package/packages/mailx-types/trust.d.ts.map +1 -1
- package/packages/mailx-types/trust.js +81 -9
- package/packages/mailx-types/trust.js.map +1 -1
- package/packages/mailx-types/trust.ts +88 -7
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-39180 → node_modules.npmglobalize-stash-63828}/.package-lock.json +0 -0
package/.commitmsg
CHANGED
|
@@ -1,33 +1,41 @@
|
|
|
1
|
-
The
|
|
1
|
+
The chip claimed every rule was a bulk-mail test without looking at them
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
`
|
|
11
|
-
|
|
3
|
+
Altis Hotels' welcome mail (Bob 2026-09-03: "marked as spam but is valid",
|
|
4
|
+
`mailxstore/bobma/90/90fd2404….eml`). The banner on it is already right —
|
|
5
|
+
`info`, no headline, "Your mail server scored this as spam, but altishotels.com
|
|
6
|
+
proved it sent this", and the "Stop warning about marketing@altishotels.com"
|
|
7
|
+
button underneath. What was wrong sat one element above: the score chip's
|
|
8
|
+
tooltip read "Scored by bulk-mail or blocklist rules; the sending domain passed
|
|
9
|
+
DMARC, so the sender is proved" — and then listed, on the next line of the same
|
|
10
|
+
tooltip, `3.2 KAM_ACCOUNTPHISH (Spam that tries to get account information)`,
|
|
11
|
+
which is 3.2 of the message's 8.3 and is neither a bulk-mail nor a blocklist
|
|
12
|
+
rule. A reader who reads both lines has been told two different things about
|
|
13
|
+
the same message by the same tooltip, and the app is the one that is wrong.
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
The claim was never checked. `benign = proved || kind === "bulk"` picked the
|
|
16
|
+
neutral chip — correctly, that part is about the sender — and the tooltip then
|
|
17
|
+
described the RULES from that same flag. `proved` is a fact about the headers
|
|
18
|
+
and says nothing about any rule; `kind` is the WORST class present and says
|
|
19
|
+
nothing about the rest. Neither one can support a sentence beginning "every
|
|
20
|
+
rule that scored".
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
`analyzeServerSpam` now computes `bulkOnly` — every rule that scored is `bulk`
|
|
23
|
+
or `neutral` — and `spamScoreOf` carries it to the chip. The bulk-only sentence
|
|
24
|
+
is said only when it is true. Otherwise the tooltip says what IS established
|
|
25
|
+
and nothing more: the proof, by name, plus "not every rule that scored is a
|
|
26
|
+
bulk-mail test — they are listed below", or for an unproved sender "no forgery
|
|
27
|
+
rule scored", which `kind === "bulk"` does establish. The `info` banner for a
|
|
28
|
+
bulk verdict loses its "— nothing that scored tests who sent it" clause on the
|
|
29
|
+
same condition, since it was making the same unchecked claim.
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
Measured over the 800 newest messages in the store: 138 raise the neutral chip,
|
|
32
|
+
and 132 of those were being told the score was bulk-mail rules only. Most are
|
|
33
|
+
unflagged mail scoring above half the threshold, which carries no
|
|
34
|
+
X-Spam-Report at all — there the individual scores are unknown, which is
|
|
35
|
+
precisely when a claim about all of them cannot be made.
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
Not changed, on purpose: KAM_ACCOUNTPHISH stays unclassified. Its description
|
|
38
|
+
says it tests what the message asks for, not who sent it, but "other" is the
|
|
39
|
+
honest answer for a rule this file has no basis to classify, and the cautious
|
|
40
|
+
default is what "other" is for. The fix is to stop asserting past it, not to
|
|
41
|
+
guess at it.
|
|
@@ -2226,6 +2226,13 @@ function bare(addr) {
|
|
|
2226
2226
|
const m = (addr || "").match(/[^\s<>,;]+@[^\s<>,;]+/);
|
|
2227
2227
|
return (m ? m[0] : addr || "").toLowerCase().replace(/[.,;]+$/, "");
|
|
2228
2228
|
}
|
|
2229
|
+
function sameOrg(a, b) {
|
|
2230
|
+
const x = (a || "").toLowerCase();
|
|
2231
|
+
const y = (b || "").toLowerCase();
|
|
2232
|
+
if (!x || !y)
|
|
2233
|
+
return false;
|
|
2234
|
+
return x === y || x.endsWith("." + y) || y.endsWith("." + x);
|
|
2235
|
+
}
|
|
2229
2236
|
function classifyRule(name, dmarcFailed) {
|
|
2230
2237
|
if (dmarcFailed && name === "DKIM_INVALID")
|
|
2231
2238
|
return "forgery";
|
|
@@ -2311,6 +2318,7 @@ function analyzeServerSpam(input) {
|
|
|
2311
2318
|
const unknown = rules.filter((r) => r.cls === "other" && scored(r));
|
|
2312
2319
|
const unknownWeight = unknown.reduce((sum, r) => sum + r.score, 0);
|
|
2313
2320
|
const unknownCouldFlagAlone = !unknown.length ? false : !Number.isFinite(unknownWeight) || !Number.isFinite(threshold) ? true : unknownWeight >= threshold;
|
|
2321
|
+
const bulkOnly = rules.every((r) => !scored(r) || r.cls === "bulk" || r.cls === "neutral");
|
|
2314
2322
|
const kind = dmarc.authFailures.length || present("forgery") ? "forgery" : present("association") ? "association" : unknownCouldFlagAlone ? "unclassified" : rules.length ? "bulk" : "unclassified";
|
|
2315
2323
|
const has = (name) => rules.some((r) => r.name === name);
|
|
2316
2324
|
const provedBy = dmarc.pass ? "dmarc" : has("DKIM_VALID_AU") ? "dkim" : has("ALL_TRUSTED") ? "trusted" : "";
|
|
@@ -2327,6 +2335,7 @@ function analyzeServerSpam(input) {
|
|
|
2327
2335
|
kind,
|
|
2328
2336
|
proved: !!provedBy,
|
|
2329
2337
|
provedBy,
|
|
2338
|
+
bulkOnly,
|
|
2330
2339
|
provedDomain: dmarc.domain || fromDomain,
|
|
2331
2340
|
provedPolicy: dmarc.policy,
|
|
2332
2341
|
bayesHam: rules.some((r) => r.name === "BAYES_00" || r.name === "BAYES_01"),
|
|
@@ -2374,7 +2383,7 @@ function serverSpamVerdict(input) {
|
|
|
2374
2383
|
return {
|
|
2375
2384
|
id: "server-spam-verdict",
|
|
2376
2385
|
severity: "info",
|
|
2377
|
-
text: "Your mail server rates this bulk mail \u2014 nothing that scored tests who sent it.",
|
|
2386
|
+
text: a.bulkOnly ? "Your mail server rates this bulk mail \u2014 nothing that scored tests who sent it." : "Your mail server rates this bulk mail.",
|
|
2378
2387
|
detail: `${numbers}${why}${bayes}`
|
|
2379
2388
|
};
|
|
2380
2389
|
const proof = a.proved ? `${provedNote(a)} ` : "";
|
|
@@ -2455,7 +2464,17 @@ function hiddenLinkOverlay(bodyHtml) {
|
|
|
2455
2464
|
}
|
|
2456
2465
|
return null;
|
|
2457
2466
|
}
|
|
2458
|
-
function
|
|
2467
|
+
function provedSenderDomain(input) {
|
|
2468
|
+
const fromDomain = (bare(input.fromAddress).split("@")[1] || "").toLowerCase();
|
|
2469
|
+
if (!fromDomain)
|
|
2470
|
+
return "";
|
|
2471
|
+
if (dmarcProof(input).pass)
|
|
2472
|
+
return fromDomain;
|
|
2473
|
+
return analyzeServerSpam(input)?.proved ? fromDomain : "";
|
|
2474
|
+
}
|
|
2475
|
+
function redirectorLink(input) {
|
|
2476
|
+
const bodyHtml = input.bodyHtml || "";
|
|
2477
|
+
const senderDomain = provedSenderDomain(input);
|
|
2459
2478
|
for (const m of bodyHtml.matchAll(/href\s*=\s*["']([^"']+)["']/gi)) {
|
|
2460
2479
|
let url;
|
|
2461
2480
|
try {
|
|
@@ -2465,6 +2484,8 @@ function redirectorLink(bodyHtml) {
|
|
|
2465
2484
|
}
|
|
2466
2485
|
if (!/^https?:$/.test(url.protocol))
|
|
2467
2486
|
continue;
|
|
2487
|
+
if (senderDomain && sameOrg(url.hostname, senderDomain))
|
|
2488
|
+
continue;
|
|
2468
2489
|
for (const [, value] of url.searchParams) {
|
|
2469
2490
|
let target;
|
|
2470
2491
|
try {
|
|
@@ -2474,7 +2495,7 @@ function redirectorLink(bodyHtml) {
|
|
|
2474
2495
|
}
|
|
2475
2496
|
if (!/^https?:$/.test(target.protocol))
|
|
2476
2497
|
continue;
|
|
2477
|
-
if (target.hostname
|
|
2498
|
+
if (sameOrg(target.hostname, url.hostname))
|
|
2478
2499
|
continue;
|
|
2479
2500
|
return {
|
|
2480
2501
|
id: "redirector-link",
|
|
@@ -2493,7 +2514,7 @@ function assessMessageTrust(input) {
|
|
|
2493
2514
|
relayAuthMismatch(input),
|
|
2494
2515
|
zeroWidthObfuscation(input.bodyText || ""),
|
|
2495
2516
|
hiddenLinkOverlay(input.bodyHtml || ""),
|
|
2496
|
-
redirectorLink(input
|
|
2517
|
+
redirectorLink(input)
|
|
2497
2518
|
].filter(Boolean);
|
|
2498
2519
|
const rank = { danger: 0, caution: 1, info: 2 };
|
|
2499
2520
|
return findings.sort((a, b) => rank[a.severity] - rank[b.severity]);
|
|
@@ -2506,9 +2527,7 @@ function relayAuthMismatch(input) {
|
|
|
2506
2527
|
for (const name of RELAY_IDENTITY_HEADERS) {
|
|
2507
2528
|
const stamped = bare(header(input.headerLines, name));
|
|
2508
2529
|
const stampedDomain = stamped.split("@")[1] || "";
|
|
2509
|
-
if (!stampedDomain || stampedDomain
|
|
2510
|
-
continue;
|
|
2511
|
-
if (stampedDomain.endsWith("." + fromDomain) || fromDomain.endsWith("." + stampedDomain))
|
|
2530
|
+
if (!stampedDomain || sameOrg(stampedDomain, fromDomain))
|
|
2512
2531
|
continue;
|
|
2513
2532
|
return {
|
|
2514
2533
|
id: "relay-auth-mismatch",
|
|
@@ -2544,6 +2563,7 @@ function spamScoreOf(input) {
|
|
|
2544
2563
|
kind: a.kind,
|
|
2545
2564
|
proved: a.proved,
|
|
2546
2565
|
provedBy: a.provedBy,
|
|
2566
|
+
bulkOnly: a.bulkOnly,
|
|
2547
2567
|
trusted: !!input.senderTrusted && a.proved && a.kind !== "forgery" && !a.authFailures.length,
|
|
2548
2568
|
reasons: a.reasons
|
|
2549
2569
|
};
|