@bobfrankston/rmfmail 1.2.299 → 1.2.301

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,29 @@
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
+ Placing the caret in the body is the same act as taking focus
2
+
3
+ Ctrl+N opens compose with the cursor in the message body instead of the To
4
+ field (Bob 2026-09-03: "^n (and forward?) still start me in the body rather
5
+ than the to field" — and yes, forward too).
6
+
7
+ applyInit made the decision twice. First it seeded the body and called
8
+ `editor.setCursor(0)`; twenty lines later it picked the first empty field and
9
+ focused it. Reading top to bottom the second one wins, and it does not:
10
+ `setCursor` is not a caret-placement call, it is a focus call. rmf-tiny's
11
+ implementation ends with `editor.focus()` and then re-runs the whole placement
12
+ inside `requestAnimationFrame`deliberately, to survive the nested-iframe
13
+ focus race that produced the old "have to click in and try again" symptom — so
14
+ it lands after anything applyInit does synchronously afterwards. Quill's
15
+ `setSelection` focuses as well. Whatever order the two lines were written in,
16
+ the editor was always going to win.
17
+
18
+ So the two are now one decision, made once and up front: `focusTarget` is the
19
+ first field the user still has to fill, To → Subject → body, and `setCursor` is
20
+ asked for only when the answer is the body. On a reply that is still the body —
21
+ To and Subject are filled, and the call also scrolls the quote out of view so
22
+ the empty line is what's on screen, which is the behaviour it exists for. On a
23
+ forward and on new mail it is the To field, untouched.
24
+
25
+ Forward has been doing this since it grew a quoted body. Ctrl+N joined it in
26
+ v1.2.298, three hours ago: before the account signature reached compose,
27
+ `bodyToRender` was empty for new mail and the branch holding `setCursor` never
28
+ ran at all. The first message anyone composed with a signature was also the
29
+ first one that opened with the cursor in the wrong place.
@@ -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
  };