@bobfrankston/rmfmail 1.2.299 → 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 CHANGED
@@ -1,53 +1,41 @@
1
- Laundering needs a second party a redirector alone is not one
2
-
3
- Google's account-security notice, which is exactly the mail a reader must be
4
- able to believe, carried a caution banner: "A link hides where it goes: it
5
- passes through accounts.google.com and ends at myaccount.google.com." Both
6
- halves of that sentence name Google. Bob, on the message:
7
- `mailxstore/bobma/e1/e193f2bb….eml`, "this is valid."
8
-
9
- The check compared `target.hostname === url.hostname` and reported anything
10
- else. That catches the case it was written for — `castanet-sa.fr/redirect.php
11
- ?newurl=https://evil.example`, someone's abandoned redirect script spending a
12
- real business's reputation but the comparison is not what the finding
13
- claims. Laundering requires a SECOND PARTY, someone whose name is on the
14
- visible link without their consent. Two ways a link has none, both now checked,
15
- neither of them a threshold:
16
-
17
- 1. The redirector and the destination are the same organisation. Equality
18
- widened to the sub/parent-domain test this file already uses twice, now a
19
- shared `sameOrg()` deliberately not "the last two labels match", which
20
- would make paypal.co.uk and evil.co.uk relatives.
21
-
22
- 2. The redirector IS the sender, proved. accounts.google.com and
23
- myaccount.google.com are siblings, so rule 1 does not reach them; what
24
- clears the message is that the receiving server recorded `dmarc=pass
25
- header.from=accounts.google.com`. The host in the visible link belongs to
26
- the party that provably sent the mail. Whether that party is trustworthy
27
- is a different question than the one this check asks.
28
-
29
- The proof comes from `provedSenderDomain()`, which asks dmarcProof first — an
30
- Authentication-Results header alone is enough, so this works on Gmail-API mail
31
- with no SpamAssassin headers and falls back to the DKIM_VALID_AU /
32
- ALL_TRUSTED proofs SpamAssassin states in its own rule list. It reads the
33
- topmost Authentication-Results only, which the sender cannot write. No proof
34
- means the check stays on: an escape a forger could open by typing a domain
35
- into From would be worse than no check.
36
-
37
- The mirror rule is NOT safe and is deliberately absent. "The link ENDS at the
38
- proved sender" would clear `legit-bank.com/r?u=https://evil.com` sent from a
39
- DMARC-proved evil.com, which is the laundering case in its purest form the
40
- reader sees the bank's hostname. Only the visible host may be the sender's.
41
-
42
- Measured over the 800 most recent messages in the store: redirector-link fires
43
- 15 times before, 4 after. Cleared: Substack's own link wrapper on Substack
44
- mail (6), Google's alert, Microsoft Teams, dev.to. Still flagged, correctly by
45
- this rule's terms: Microsoft's Safe Links wrapper (nam.safelink.emails.azure.net
46
- -> admin.microsoft.com, 3) and a donation platform bouncing back to the charity
47
- that mailed. Those are third-party hostnames in front of the reader, and no
48
- hostname allowlist is going in to quiet them.
49
-
50
- Four tests: the sibling-subdomain case, the Google alert with its proof and
51
- again with the proof removed, and a forged accounts.google.com whose server
52
- recorded the DMARC failure — still flagged, as the escape is the server's
53
- verdict and never the From line's say-so.
1
+ The chip claimed every rule was a bulk-mail test without looking at them
2
+
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.
14
+
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".
21
+
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.
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.
36
+
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.
@@ -2318,6 +2318,7 @@ function analyzeServerSpam(input) {
2318
2318
  const unknown = rules.filter((r) => r.cls === "other" && scored(r));
2319
2319
  const unknownWeight = unknown.reduce((sum, r) => sum + r.score, 0);
2320
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");
2321
2322
  const kind = dmarc.authFailures.length || present("forgery") ? "forgery" : present("association") ? "association" : unknownCouldFlagAlone ? "unclassified" : rules.length ? "bulk" : "unclassified";
2322
2323
  const has = (name) => rules.some((r) => r.name === name);
2323
2324
  const provedBy = dmarc.pass ? "dmarc" : has("DKIM_VALID_AU") ? "dkim" : has("ALL_TRUSTED") ? "trusted" : "";
@@ -2334,6 +2335,7 @@ function analyzeServerSpam(input) {
2334
2335
  kind,
2335
2336
  proved: !!provedBy,
2336
2337
  provedBy,
2338
+ bulkOnly,
2337
2339
  provedDomain: dmarc.domain || fromDomain,
2338
2340
  provedPolicy: dmarc.policy,
2339
2341
  bayesHam: rules.some((r) => r.name === "BAYES_00" || r.name === "BAYES_01"),
@@ -2381,7 +2383,7 @@ function serverSpamVerdict(input) {
2381
2383
  return {
2382
2384
  id: "server-spam-verdict",
2383
2385
  severity: "info",
2384
- 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.",
2385
2387
  detail: `${numbers}${why}${bayes}`
2386
2388
  };
2387
2389
  const proof = a.proved ? `${provedNote(a)} ` : "";
@@ -2561,6 +2563,7 @@ function spamScoreOf(input) {
2561
2563
  kind: a.kind,
2562
2564
  proved: a.proved,
2563
2565
  provedBy: a.provedBy,
2566
+ bulkOnly: a.bulkOnly,
2564
2567
  trusted: !!input.senderTrusted && a.proved && a.kind !== "forgery" && !a.authFailures.length,
2565
2568
  reasons: a.reasons
2566
2569
  };