@bobfrankston/rmfmail 1.2.288 → 1.2.290

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,29 +1,39 @@
1
- Trust: a message that never crossed a trust boundary proves itself
1
+ Spam banner: an unknown rule must be able to condemn on its own
2
2
 
3
- Bob 2026-08-27, on his own reply to John Levine about spam handling: "why is
4
- my own mail spam". It carried no Authentication-Results at all — his own
5
- authenticated submission never left his systems, so nothing added one and
6
- `proved` therefore came back false, dropping it to a caution banner that told
7
- him not to open the links in prose he had just written. What flagged it was
8
- `URIBL_DBL_SPAM` 8.0 on blueorchestra.org: the domain he was QUOTING, in a
9
- discussion about spam. Guilt by association at its purest.
3
+ Bob 2026-08-28: "we have a problem since, of course, things like conference
4
+ announcements use bulk mailers." An IEEE/ICECER call for papers, DMARC-proved,
5
+ BAYES_00, scored exactly 4.5 of 4.5 DCC_CHECK 3.0 + KAM_MARKETINGBL 1.0 +
6
+ **1.5 GB_AWSTRACK_REDIR, Amazon SES's click-tracker** and the one rule this
7
+ file had never heard of pushed a proved announcement into "Check this message
8
+ before acting on it / Links and attachments here should not be opened".
10
9
 
11
- SpamAssassin states two proofs in its own rule list that such mail carries
12
- instead of an AR header:
10
+ The "unknown means caution" policy was right; testing it by PRESENCE was not.
11
+ An unknown rule now keeps a proved sender at caution only when it could have
12
+ flagged the message BY ITSELF — its own scores summed against the server's own
13
+ threshold. That adds no constant to tune: "enough to condemn" already has a
14
+ number and it is the one the server used. KAM_BENEFICIARY, the case the policy
15
+ exists for, is worth 10.0 alone and still cautions; a 1.5 click-tracker cannot.
16
+ Unscored rules (no X-Spam-Report) leave the sum unknown, and unknown stays
17
+ cautious.
13
18
 
14
- ALL_TRUSTED Passed through trusted hosts only via SMTP
15
- DKIM_VALID_AU Message has a valid DKIM or DK signature from author's domain
19
+ Two classification fixes found by sampling what still cautioned a PROVED
20
+ sender across the store:
16
21
 
17
- DKIM_VALID_AU is the same evidence DMARC alignment rests on — the author
18
- domain signed it and the signature verified so it proves the From line on
19
- its own. ALL_TRUSTED says nothing untrusted handled the message in transit.
20
- `proved` now comes from any of the three, and `provedBy` names which, so the
21
- banner says WHICH proof rather than asserting DMARC that isn't there. His
22
- message is now a grey note: "…but bob.ma proved it sent this. Valid signature
23
- from bob.ma, the sending domain itself (DKIM_VALID_AU)."
22
+ - Branded ESP click-trackers (GB_AWSTRACK*, SendGrid, Mailchimp) are bulk-mail
23
+ machinery. Open redirectors (URI_PHP_REDIR and friends) stay unclassified
24
+ a tracking domain the sending platform owns is not an abused redirect.
25
+ - KAM_*URIBL_* ("Body contains URI listed in PCCC WILD RBL", 9.0) and
26
+ SH_HBL_EMAILS ("Email address listed in email blocklist", 8.0) are the same
27
+ evidence as URIBL_SBL somebody else's list about a host the message
28
+ mentions and were the bulk of the remaining noise: a Business Insider
29
+ newsletter, an MIT cancer-center invitation, a campaign mailing.
30
+ KAM_INFOUSMEBIZ judges the TLD a domain lives in, which is guilt by
31
+ neighbourhood.
24
32
 
25
- Forgery still outranks proof across the store the danger count is unchanged
26
- at 294 of 1,131 flagged; caution 599, quiet 238.
27
-
28
- Also: the Insert menu gains Blockquote and Quoted text (paste) — rmf-tiny
29
- 0.1.53, see its .commitmsg.
33
+ Over the same 20,308 messages, of 1,131 flagged: 294 danger (unchanged proof
34
+ never overrides forgery), 278 caution, 559 quiet. What still cautions a proved
35
+ sender is now almost entirely content-scam rules: ADVANCE_FEE, DEAR_BENEFICIARY,
36
+ KAM_CRIM, UNDISC_MONEY, BITCOIN_DIRECT. The two false positives left in that
37
+ set — a Substack news digest about displacement tripping KAM_CRIM, a Qatar
38
+ Airways statement tripping BITCOIN_DIRECT — are exactly what sender trust is
39
+ for, and are deliberately NOT being fixed by inventing more rule exceptions.
package/TODO.md CHANGED
@@ -230,7 +230,7 @@ Items blocked on a design decision. Short-form question here; full context in th
230
230
  | **C129** | OPEN | **Android prod/dev split via filename/URL channel** — User 2026-05-09 (revised): no separate MAUI project. Same source tree builds; output filenames distinguish channels: Phase 1 (start now) writes `rmfmaildev.apk` alongside today's `mailx-maui.apk`. Phase 2 (when a release is declared) switches to `rmfmail-<version>.apk` versioned + `rmfmaildev.apk` for latest, with promotion = re-pointing `rmfmail.apk` at a chosen version. Single APK contents per build; channel is runtime setting in AppUpdater (Settings toggle for which URL to poll). Side-by-side install on one device NOT supported by this design (single Android package id) — that's a separate need. See `docs/prod-android.md` for full plan. Phase 1 ~30 min; full flow ~2.5 hours when activated. |
231
231
  | **C130** | OPEN | **Reset prefetch error budget after fresh sync** — Android: `errors`/`failedThisSession` accumulate within a prefetch session. Already resets on next syncAll cycle (since each `prefetchBodies(account)` call is fresh). Verify this works in practice via logit; if a long sync session burns through budget and stays stuck, add an explicit reset on `bodyAvailable` events that fire while prefetch is still running. |
232
232
  | **C164** | RESEARCHED | **Thundermail / JMAP provider** — full protocol survey at [`docs/thundermail-jmap.md`](thundermail-jmap.md), probed live 2026-08-17. Thundermail is Stalwart, so there is no proprietary API: IMAP 993 / SMTP 465 work **today with zero code** (an `accounts.jsonc` entry and nothing else), CardDAV `/dav/card` + CalDAV `/dav/cal` are http-basic, and JMAP is live at `https://mail.thundermail.com/jmap/` advertising core / mail / submission / calendars / contacts / filenode / sieve / quota plus **WebSocket push** (`supportsPush: true`). JMAP is the attractive bet: it kills the IDLE lane, the 5-lane connection semaphore and the Dovecot 20-conn ceiling, needs no transport injection (one path for Android and desktop, cf. C125), and `Email/changes` is native set-difference sync. **From address: resolved — good.** The 15-alias cap is `@thundermail.com`/`@tb.pro` only; custom domains get unlimited aliases plus a catch-all (empty-name alias), and with catch-all on you can send from any prefix unregistered — their own example is the per-vendor pattern already used on `bob.ma`. mailx needs ~nothing here (per-message From identity shipped v1.2.201); only Stalwart-side `MAIL FROM`/DKIM validation for an unregistered prefix is untested. **Still blocking/staged:** JMAP-calendars and `filenode` are unratified drafts; OAuth `scopes_supported` covers mail only (no calendars/contacts scope); invite-only beta so there is nothing to test against; and **no Drive equivalent** — Thunderbird Send is one-shot file *sharing*, not a listable revisable store, so mailx's shared-config dependency (`accounts.jsonc` / `contacts.jsonc` / preferences / allowlist on GDrive) has no home. **C161 is therefore the prerequisite, not JMAP** — the `_rmfmail` hidden-IMAP-folder state channel removes the Drive dependency for every account type at once and keeps working unchanged if JMAP lands later. Until C161 ships, adopting Thundermail is a half-migration that trades a Google dependency for a Thunderbird one. |
233
- | **C165** | PARTIAL | **Spam-banner false positives — trust the sender, do not tune the threshold** — full diagnosis and a "What shipped" section at [`docs/spam-false-positives.md`](spam-false-positives.md). Bob 2026-08-27: the red "This message is not what it says it is" banner fired on Constant Contact / Adobe MAX / Mokin newsletters he subscribed to, because `serverSpamVerdict()` consumed SpamAssassin’s **bottom line** (a delivery decision) to answer a different question (is this forged) and never parsed the `tests=` list in the same header. **Shipped v1.2.286 (mailx-types 0.1.64, mailx-store 0.1.96):** `tests=` is classified into forgery / association / bulk / neutral / other; `X-Spam-Report` supplies each rule’s score and description so findings name their evidence; the **topmost** `Authentication-Results` is read for a DMARC-aligned pass with `header.from=` checked against the real From. Severity follows the rules — danger only on a forgery rule or a server-recorded SPF/DKIM/DMARC failure (phishing/malware URL listings count); a new neutral `info` severity (no headline, no "do not open" footer, grey chip) when the sender is proved and every scoring rule is classifiable, or the score is all bulk; caution otherwise, **including a proved sender whose rules are unclassified** — unknown is never a reason to go quiet (326 messages in the store). `spamScoreOf()` and the banner now come from one analysis so they cannot disagree. Measured over 20,291 messages: of 1,131 flagged, 294 stay red, 672 caution, 165 quiet; the 2026-08-25 phishes stay red. Tests: all four samples verbatim + both ways the DMARC proof could be gamed. **Still owed:** sender trust — `allowlist.jsonc` already is the approved-sender store (`getAllowlist()` at `mailx-service/index.ts:1980`) and the viewer already has “Trust all senders at ‹domain›”, but nothing consults it when computing `trust`/`spamScore` (`store.ts:586`) and the banner has no inline trust action. Open Qs unchanged: trust key granularity (shared ESP domain vs Reply-To org vs Delivered-To tag), whether trust suppresses the whole banner or only the bulk/association half (leaning: never suppress forgery evidence), and auto-trust on reply/move-out-of-Junk. |
233
+ | **C165** | PARTIAL | **Spam-banner false positives — trust the sender, do not tune the threshold** — full diagnosis and a "What shipped" section at [`docs/spam-false-positives.md`](spam-false-positives.md). Bob 2026-08-27: the red "This message is not what it says it is" banner fired on Constant Contact / Adobe MAX / Mokin newsletters he subscribed to, because `serverSpamVerdict()` consumed SpamAssassin’s **bottom line** (a delivery decision) to answer a different question (is this forged) and never parsed the `tests=` list in the same header. **Shipped v1.2.286–289:** `tests=` is classified into forgery / association / bulk / neutral / other; `X-Spam-Report` supplies each rule’s score and description so findings name their evidence; the **topmost** `Authentication-Results` is read for a DMARC-aligned pass with `header.from=` checked against the real From. Severity follows the rules — danger only on a forgery rule or a server-recorded SPF/DKIM/DMARC failure (phishing/malware URL listings count); a new neutral `info` severity (no headline, no "do not open" footer, grey chip) when the sender is proved and every scoring rule is classifiable, or the score is all bulk; caution otherwise, **including a proved sender whose rules are unclassified** — unknown is never a reason to go quiet (326 messages in the store). `spamScoreOf()` and the banner now come from one analysis so they cannot disagree. Measured over 20,291 messages: of 1,131 flagged, 294 stay red, 672 caution, 165 quiet; the 2026-08-25 phishes stay red. Tests: all four samples verbatim + both ways the DMARC proof could be gamed. Follow-up v1.2.288: proof of the From line accepts `DKIM_VALID_AU` and `ALL_TRUSTED` as well as DMARC (`provedBy` names which, in banner and chip tooltip) — Bob's OWN mail carries no `Authentication-Results` at all and was getting a caution banner over a URL it quoted. **John Levine 2026-08-27 confirms the direction:** DCC means bulk, "you have to whitelist the senders … I haven't tried to do a global one because everyone's different" — bulk is a fact about the mail, wanted is a fact about the reader. Server-side whitelisting is out for Bob (he whitelists `@bob.ma` and "that gets abused"), so the client list must key on evidence (DKIM-verified domain, Delivered-To tag), not the From line. **Still owed:** sender trust — `allowlist.jsonc` already is the approved-sender store (`getAllowlist()` at `mailx-service/index.ts:1980`) and the viewer already has “Trust all senders at ‹domain›”, but nothing consults it when computing `trust`/`spamScore` (`store.ts:586`) and the banner has no inline trust action. Open Qs unchanged: trust key granularity (shared ESP domain vs Reply-To org vs Delivered-To tag), whether trust suppresses the whole banner or only the bulk/association half (leaning: never suppress forgery evidence), and auto-trust on reply/move-out-of-Junk. |
234
234
 
235
235
  <a id="concerns"></a>
236
236
 
@@ -2308,7 +2308,10 @@ function analyzeServerSpam(input) {
2308
2308
  });
2309
2309
  const scored = (r) => !(r.score <= 0);
2310
2310
  const present = (cls) => rules.some((r) => r.cls === cls && scored(r));
2311
- const kind = dmarc.authFailures.length || present("forgery") ? "forgery" : present("association") ? "association" : present("other") ? "unclassified" : rules.length ? "bulk" : "unclassified";
2311
+ const unknown = rules.filter((r) => r.cls === "other" && scored(r));
2312
+ const unknownWeight = unknown.reduce((sum, r) => sum + r.score, 0);
2313
+ const unknownCouldFlagAlone = !unknown.length ? false : !Number.isFinite(unknownWeight) || !Number.isFinite(threshold) ? true : unknownWeight >= threshold;
2314
+ const kind = dmarc.authFailures.length || present("forgery") ? "forgery" : present("association") ? "association" : unknownCouldFlagAlone ? "unclassified" : rules.length ? "bulk" : "unclassified";
2312
2315
  const has = (name) => rules.some((r) => r.name === name);
2313
2316
  const provedBy = dmarc.pass ? "dmarc" : has("DKIM_VALID_AU") ? "dkim" : has("ALL_TRUSTED") ? "trusted" : "";
2314
2317
  const fromDomain = (bare(input.fromAddress).split("@")[1] || "").toLowerCase();
@@ -2530,6 +2533,7 @@ function spamScoreOf(input) {
2530
2533
  flagged: a.flagged,
2531
2534
  kind: a.kind,
2532
2535
  proved: a.proved,
2536
+ provedBy: a.provedBy,
2533
2537
  reasons: a.reasons
2534
2538
  };
2535
2539
  }
@@ -2553,7 +2557,18 @@ var init_trust = __esm({
2553
2557
  /^SH_DBL/,
2554
2558
  /^SEM_URIBL/,
2555
2559
  /^SPAMHAUS_/,
2556
- /^RCVD_IN_(SBL|XBL|PBL|BL_SPAMCOP|SORBS|BRBL|VALIDITY|MSPIKE_[LZ])/
2560
+ /^RCVD_IN_(SBL|XBL|PBL|BL_SPAMCOP|SORBS|BRBL|VALIDITY|MSPIKE_[LZ])/,
2561
+ // Blocklist rules that happen to live under other prefixes. KAM_*URIBL_*
2562
+ // ("Body contains URI listed in PCCC WILD RBL", 9.0) and SH_HBL_EMAILS
2563
+ // ("Email address listed in email blocklist", 8.0) are the same kind of
2564
+ // evidence as URIBL_SBL — somebody else's list, about a host or address
2565
+ // the message mentions — and they were the bulk of what still cautioned a
2566
+ // proved sender: a Business Insider newsletter, an MIT cancer-center
2567
+ // invitation, a campaign mailing. KAM_INFOUSMEBIZ judges the TLD a domain
2568
+ // lives in, which is guilt by neighbourhood.
2569
+ /^KAM_(BODY_|FROM_)?URIBL/,
2570
+ /^SH_HBL/,
2571
+ /^KAM_INFOUSMEBIZ$/
2557
2572
  ];
2558
2573
  BULK_RULES = [
2559
2574
  /^DCC_/,
@@ -2581,7 +2596,14 @@ var init_trust = __esm({
2581
2596
  /^MPART_ALT_DIFF/,
2582
2597
  /^TVD_/,
2583
2598
  /^T_REMOTE_IMAGE$/,
2584
- /^UNPARSEABLE_RELAY$/
2599
+ /^UNPARSEABLE_RELAY$/,
2600
+ // Branded ESP click-trackers. Not open redirectors (URI_PHP_REDIR and
2601
+ // friends stay unclassified): these are the sending platform's own
2602
+ // tracking domain, which is mass-mail machinery by definition.
2603
+ /^GB_AWSTRACK/,
2604
+ /AWSTRACK/,
2605
+ /^GB_SENDGRID/,
2606
+ /^GB_MAILCHIMP/
2585
2607
  ];
2586
2608
  NEUTRAL_RULES = [
2587
2609
  /^BAYES_/,