@bobfrankston/rmfmail 1.2.282 → 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.
@@ -1652,26 +1652,27 @@ export async function showMessage(accountId: string, uid: number, folderId?: num
1652
1652
  // it can be checked rather than believed. (Claude Code 2026-08-25)
1653
1653
  bodyEl.innerHTML = "";
1654
1654
 
1655
- // The server's spam score, shown whether or not it crossed the line
1656
- // (Bob 2026-08-25: "should we have a flag ... with the spam assassin
1657
- // number and a color code?"). The sextortion mail that prompted this
1658
- // scored 2.7 against a 4.5 threshold under the wire, so nothing was
1659
- // flagged, but "did not quite trip it" is not the same as clean and
1660
- // the reader deserves the number rather than a silent pass.
1655
+ // The server's spam score shown ONLY when it is high enough to mean
1656
+ // something (Bob 2026-08-25: "only show the spam score if it is high
1657
+ // and likely"). The first cut put a chip on every message that carried
1658
+ // a score, which is nearly all of them: a badge that is always present
1659
+ // carries no information and becomes furniture the eye stops reading.
1661
1660
  //
1662
- // The colour is a PROPORTION of the server's own threshold, not a
1663
- // constant: red once the server flagged it, amber past halfway to its
1664
- // line, quiet below. If the server's threshold changes, this follows.
1661
+ // The floor is halfway to the SERVER'S OWN threshold, so it is a
1662
+ // proportion rather than a constant and follows if that threshold ever
1663
+ // changes. Halfway is where "did not quite trip the wire" starts being
1664
+ // a different statement from "clean" — the sextortion mail that
1665
+ // prompted all of this scored 2.7 against 4.5 and was delivered
1666
+ // unflagged, so showing only the flagged ones would miss exactly the
1667
+ // case worth showing.
1665
1668
  const spam = (msg as any).spamScore as { score: number; threshold: number; flagged: boolean };
1666
- if (spam && Number.isFinite(spam.score)) {
1667
- const level = spam.flagged ? "high" : (spam.score >= spam.threshold / 2 ? "mid" : "low");
1669
+ if (spam && Number.isFinite(spam.score) && (spam.flagged || spam.score >= spam.threshold / 2)) {
1670
+ const level = spam.flagged ? "high" : "mid";
1668
1671
  const chip = document.createElement("div");
1669
1672
  chip.className = `mv-spamscore mv-spamscore-${level}`;
1670
1673
  chip.title = level === "high"
1671
1674
  ? "Your mail server scored this at or above its spam threshold and flagged it."
1672
- : level === "mid"
1673
- ? "Below your server's spam threshold, but more than halfway to it."
1674
- : "Well below your mail server's spam threshold.";
1675
+ : "Below your server's spam threshold, but more than halfway to it.";
1675
1676
  chip.textContent = `spam score ${spam.score} of ${spam.threshold}`;
1676
1677
  bodyEl.appendChild(chip);
1677
1678
  }
@@ -2977,10 +2977,12 @@ body.calendar-sidebar-on .calendar-sidebar { display: flex; }
2977
2977
  opacity: 0.9;
2978
2978
  }
2979
2979
 
2980
- /* Receiving server's spam score always shown when the server left one, so a
2981
- below-threshold score is visible instead of silently passing. Colour is a
2982
- proportion of the server's own threshold (see message-viewer), which is why
2983
- there is no number hardcoded here. (Claude Code 2026-08-25) */
2980
+ /* Receiving server's spam score. Rendered only when the score is at least
2981
+ halfway to the server's own threshold a chip on every message is furniture
2982
+ the eye stops reading (Bob 2026-08-25: "only show the spam score if it is
2983
+ high and likely"). Two levels only, because anything quieter never renders
2984
+ at all. The cutoff is a proportion, computed in message-viewer, which is why
2985
+ no number appears here. (Claude Code 2026-08-25) */
2984
2986
  .mv-spamscore {
2985
2987
  display: inline-block;
2986
2988
  margin: 0 0 var(--gap-sm) 0;
@@ -2990,6 +2992,5 @@ body.calendar-sidebar-on .calendar-sidebar { display: flex; }
2990
2992
  font-family: var(--font-mono, ui-monospace, monospace);
2991
2993
  letter-spacing: 0.01em;
2992
2994
  }
2993
- .mv-spamscore-low { background: var(--color-bg-alt, #eee); color: var(--color-text-muted, #666); }
2994
2995
  .mv-spamscore-mid { background: oklch(0.55 0.18 50); color: white; font-weight: 600; }
2995
2996
  .mv-spamscore-high { background: oklch(0.42 0.20 25); color: white; font-weight: 700; }
package/npmchanges.md CHANGED
@@ -703,3 +703,68 @@ tests/trust.test.ts pins both phishes and, as importantly, the negative cases:
703
703
  a self-sent note with no auth headers, SPF failure on mail not claiming to be
704
704
  you, a modest image map, a same-site return-path parameter, and ordinary mail.
705
705
 
706
+ ## v1.2.282 — 2026-08-25
707
+
708
+ Catch the spam that scores under the threshold
709
+
710
+ A sextortion mail arrived that none of yesterday's checks caught ("yet this
711
+ spam got through"). SpamAssassin scored it 2.7 against a 4.5 threshold and
712
+ delivered it, so server-spam-verdict correctly stayed silent — the server said
713
+ no. Three independent proofs of forgery were sitting in the message anyway.
714
+
715
+ spf=none now counts as "not authenticated" for self-claimed mail, not just
716
+ fail. bobf.frankston.com publishes no SPF record, so nothing sent as that
717
+ domain can ever FAIL — only come back none. Treating none as neutral left the
718
+ least-protected domain the least defended, which is backwards. Checked against
719
+ the real store before changing it, 50,000 messages scanned:
720
+
721
+ From at one of Bob's domains 10,338
722
+ no Authentication-Results 10,034 <- his own mail, still ignored
723
+ spf=pass 243 <- still ignored
724
+ spf=fail / softfail 33
725
+ spf=none 18 <- nearly all spam
726
+
727
+ The 18 were "Settle your debt", "AAA Final notice", "McAfee Protection Plan
728
+ Ended" and similar. Legitimate self-mail sits in the no-header group: it never
729
+ crossed a trust boundary, so there is no Authentication-Results to read, and
730
+ that case is untouched. An explicit spf/dkim/dmarc=pass still silences the
731
+ check.
732
+
733
+ relay-auth-mismatch — Bob's own catch: "note that the origin is different than
734
+ the from domain in antiabuse and in source-auth".
735
+
736
+ From: universe@bobf.frankston.com
737
+ X-Source-Auth: admin@calebjross.com
738
+ X-Source-Cap: (base64) calebjro;calebjro;box2202.bluehost.com
739
+ Message-ID: <...@calebjross.com>
740
+
741
+ A Bluehost cPanel account belonging to an unrelated domain authenticated and
742
+ sent mail wearing his address. That stamp is written by the RELAY, downstream
743
+ of whoever submitted the message, so a forger cannot remove or edit it — which
744
+ makes the disagreement about as close to proof as mail headers get. Compared at
745
+ the registrable-domain level, since a host legitimately sending for a domain
746
+ authenticates as some account AT that domain and the local part varies for
747
+ ordinary reasons. Covers the cPanel/Exim, Postfix and Communigate spellings.
748
+
749
+ zero-width-obfuscation — the body had ~1380 zero-width characters stuffed
750
+ between letters, so "i regret to inform you" reached every word-matching filter
751
+ as gibberish while rendering as clean prose. SpamAssassin saw it
752
+ (UNICODE_OBFU_ZW_MANY) and still totalled only 2.7. Counted only BETWEEN TWO
753
+ LATIN LETTERS, which is what obfuscation looks like and what the legitimate
754
+ uses are not: an emoji ZWJ joins pictographs, Arabic and Indic ZWNJ sit between
755
+ their own scripts' letters. The occurrence count guards against a stray
756
+ character surviving a copy-paste; it is a sanity bound, not a sensitivity dial.
757
+
758
+ The score itself is now always visible — a chip above the body reading "spam
759
+ score 2.7 of 4.5" ("should we have a flag ... with the spam assassin number and
760
+ a color code?"). Separate from the findings on purpose: a finding is an
761
+ accusation and fires only on evidence, this is a measurement. Colour is a
762
+ PROPORTION of the server's own threshold — red once flagged, amber past halfway
763
+ to its line, quiet below — so it follows if the server's threshold changes
764
+ rather than hardcoding a number. Viewer only for now; the message-list row
765
+ would need the score stored at sync time.
766
+
767
+ tests/trust.test.ts covers all three, and the negative cases with them: spf=pass
768
+ and no-header both silent, a relay identity at the same organisation, an emoji
769
+ ZWJ family sequence, and a stray zero-width character from a paste.
770
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/rmfmail",
3
- "version": "1.2.282",
3
+ "version": "1.2.283",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -34,8 +34,8 @@
34
34
  "dependencies": {
35
35
  "@bobfrankston/iflow-direct": "^0.1.65",
36
36
  "@bobfrankston/mailx-host": "^0.1.15",
37
- "@bobfrankston/mailx-imap": "^0.1.159",
38
- "@bobfrankston/mailx-store-web": "^0.1.87",
37
+ "@bobfrankston/mailx-imap": "^0.1.160",
38
+ "@bobfrankston/mailx-store-web": "^0.1.88",
39
39
  "@bobfrankston/mailx-sync": "^0.1.29",
40
40
  "@bobfrankston/miscinfo": "^1.0.20",
41
41
  "@bobfrankston/msger": "^0.1.429",
@@ -116,8 +116,8 @@
116
116
  "dependencies": {
117
117
  "@bobfrankston/iflow-direct": "^0.1.65",
118
118
  "@bobfrankston/mailx-host": "^0.1.15",
119
- "@bobfrankston/mailx-imap": "^0.1.159",
120
- "@bobfrankston/mailx-store-web": "^0.1.87",
119
+ "@bobfrankston/mailx-imap": "^0.1.160",
120
+ "@bobfrankston/mailx-store-web": "^0.1.88",
121
121
  "@bobfrankston/mailx-sync": "^0.1.29",
122
122
  "@bobfrankston/miscinfo": "^1.0.20",
123
123
  "@bobfrankston/msger": "^0.1.429",