@bobfrankston/rmfmail 1.2.289 → 1.2.291
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 +31 -12
- package/TODO.md +1 -1
- package/client/android-bootstrap.bundle.js +35 -3
- 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 +5 -1
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +6 -2
- package/docs/spam-false-positives.md +34 -0
- package/npmchanges.md +57 -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/docs/spam-false-positives.md +34 -0
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/package.json +1 -1
- package/packages/mailx-store/store.d.ts.map +1 -1
- package/packages/mailx-store/store.js +15 -5
- package/packages/mailx-store/store.js.map +1 -1
- package/packages/mailx-store/store.ts +15 -5
- package/packages/mailx-store-web/package.json +1 -1
- package/packages/mailx-types/package.json +1 -1
- package/packages/mailx-types/trust.d.ts +11 -0
- package/packages/mailx-types/trust.d.ts.map +1 -1
- package/packages/mailx-types/trust.js +50 -1
- package/packages/mailx-types/trust.js.map +1 -1
- package/packages/mailx-types/trust.ts +62 -1
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-53712 → node_modules.npmglobalize-stash-27960}/.package-lock.json +0 -0
package/.commitmsg
CHANGED
|
@@ -1,12 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
`
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
An approved sender, proved, says nothing at all
|
|
2
|
+
|
|
3
|
+
Bob 2026-08-28, having just clicked "Always: *@icecer.com" on a conference call
|
|
4
|
+
for papers that still showed a bulk-mail note: "I accepted the server so
|
|
5
|
+
shouldn't this be considered safe?" Yes — and this is the half of C165 that was
|
|
6
|
+
still open. `allowlist.jsonc` has been the approved-sender store all along, and
|
|
7
|
+
nothing consulted it when computing trust.
|
|
8
|
+
|
|
9
|
+
`assessMessageTrust` and `spamScoreOf` now take `senderTrusted`, and store.ts
|
|
10
|
+
fills it from the sender and domain lists the reader maintains. When the sender
|
|
11
|
+
is approved AND proved AND nothing accuses it of forgery, the server-spam
|
|
12
|
+
finding is not rendered at all and the score chip goes with it: the score
|
|
13
|
+
answers "is this bulk", the reader has already answered "is it wanted", and
|
|
14
|
+
repeating the first after the second is how a banner becomes furniture.
|
|
15
|
+
|
|
16
|
+
Three boundaries, deliberate:
|
|
17
|
+
|
|
18
|
+
- **Trust never silences forgery evidence.** SPF_FAIL, a DMARC failure, a
|
|
19
|
+
FORGED_* rule, self-spoof, relay-auth mismatch — all still render, at full
|
|
20
|
+
severity, on a trusted sender.
|
|
21
|
+
- **Trust counts only where the sender is PROVED** (DMARC, DKIM_VALID_AU, or
|
|
22
|
+
ALL_TRUSTED). Otherwise the approved-sender list would be a switch a forger
|
|
23
|
+
flips by typing a trusted address into From. A trusted address that cannot be
|
|
24
|
+
proved now gets a SHARPER warning than an untrusted one: "This claims to be a
|
|
25
|
+
sender you trust, but nothing proves it is."
|
|
26
|
+
- **senderTrusted is narrower than allowRemote.** The recipient-alias list says
|
|
27
|
+
mail reached an address of the reader's, which permits images; it is not a
|
|
28
|
+
statement about who sent it, and the trust banner asks about the sender.
|
|
29
|
+
|
|
30
|
+
The measurement is unchanged — spamScoreOf still reports the score, kind and
|
|
31
|
+
reasons, with `trusted: true` alongside. Only the display goes.
|
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
|
|
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
|
|
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();
|
|
@@ -2347,6 +2350,9 @@ function serverSpamVerdict(input) {
|
|
|
2347
2350
|
const a = analyzeServerSpam(input);
|
|
2348
2351
|
if (!a || !a.flagged)
|
|
2349
2352
|
return null;
|
|
2353
|
+
const trustedAndProved = input.senderTrusted && a.proved;
|
|
2354
|
+
if (trustedAndProved && a.kind !== "forgery" && !a.authFailures.length)
|
|
2355
|
+
return null;
|
|
2350
2356
|
const numbers = Number.isFinite(a.score) && Number.isFinite(a.threshold) ? `SpamAssassin score ${a.score} of ${a.threshold}` : "flagged by SpamAssassin";
|
|
2351
2357
|
const why = a.reasons.length ? ` \u2014 ${a.reasons.join("; ")}` : "";
|
|
2352
2358
|
const bayes = a.bayesHam ? "; your own trained filter puts it at 0-1% spam (BAYES_00)" : "";
|
|
@@ -2372,6 +2378,13 @@ function serverSpamVerdict(input) {
|
|
|
2372
2378
|
detail: `${numbers}${why}${bayes}`
|
|
2373
2379
|
};
|
|
2374
2380
|
const proof = a.proved ? `${provedNote(a)} ` : "";
|
|
2381
|
+
if (input.senderTrusted && !a.proved)
|
|
2382
|
+
return {
|
|
2383
|
+
id: "server-spam-verdict",
|
|
2384
|
+
severity: "caution",
|
|
2385
|
+
text: "This claims to be a sender you trust, but nothing proves it is.",
|
|
2386
|
+
detail: `No DMARC pass, no valid signature from the sending domain, and it passed through hosts your server does not trust. ${numbers}${why}`
|
|
2387
|
+
};
|
|
2375
2388
|
return {
|
|
2376
2389
|
id: "server-spam-verdict",
|
|
2377
2390
|
severity: "caution",
|
|
@@ -2531,6 +2544,7 @@ function spamScoreOf(input) {
|
|
|
2531
2544
|
kind: a.kind,
|
|
2532
2545
|
proved: a.proved,
|
|
2533
2546
|
provedBy: a.provedBy,
|
|
2547
|
+
trusted: !!input.senderTrusted && a.proved && a.kind !== "forgery" && !a.authFailures.length,
|
|
2534
2548
|
reasons: a.reasons
|
|
2535
2549
|
};
|
|
2536
2550
|
}
|
|
@@ -2554,7 +2568,18 @@ var init_trust = __esm({
|
|
|
2554
2568
|
/^SH_DBL/,
|
|
2555
2569
|
/^SEM_URIBL/,
|
|
2556
2570
|
/^SPAMHAUS_/,
|
|
2557
|
-
/^RCVD_IN_(SBL|XBL|PBL|BL_SPAMCOP|SORBS|BRBL|VALIDITY|MSPIKE_[LZ])
|
|
2571
|
+
/^RCVD_IN_(SBL|XBL|PBL|BL_SPAMCOP|SORBS|BRBL|VALIDITY|MSPIKE_[LZ])/,
|
|
2572
|
+
// Blocklist rules that happen to live under other prefixes. KAM_*URIBL_*
|
|
2573
|
+
// ("Body contains URI listed in PCCC WILD RBL", 9.0) and SH_HBL_EMAILS
|
|
2574
|
+
// ("Email address listed in email blocklist", 8.0) are the same kind of
|
|
2575
|
+
// evidence as URIBL_SBL — somebody else's list, about a host or address
|
|
2576
|
+
// the message mentions — and they were the bulk of what still cautioned a
|
|
2577
|
+
// proved sender: a Business Insider newsletter, an MIT cancer-center
|
|
2578
|
+
// invitation, a campaign mailing. KAM_INFOUSMEBIZ judges the TLD a domain
|
|
2579
|
+
// lives in, which is guilt by neighbourhood.
|
|
2580
|
+
/^KAM_(BODY_|FROM_)?URIBL/,
|
|
2581
|
+
/^SH_HBL/,
|
|
2582
|
+
/^KAM_INFOUSMEBIZ$/
|
|
2558
2583
|
];
|
|
2559
2584
|
BULK_RULES = [
|
|
2560
2585
|
/^DCC_/,
|
|
@@ -2582,7 +2607,14 @@ var init_trust = __esm({
|
|
|
2582
2607
|
/^MPART_ALT_DIFF/,
|
|
2583
2608
|
/^TVD_/,
|
|
2584
2609
|
/^T_REMOTE_IMAGE$/,
|
|
2585
|
-
/^UNPARSEABLE_RELAY
|
|
2610
|
+
/^UNPARSEABLE_RELAY$/,
|
|
2611
|
+
// Branded ESP click-trackers. Not open redirectors (URI_PHP_REDIR and
|
|
2612
|
+
// friends stay unclassified): these are the sending platform's own
|
|
2613
|
+
// tracking domain, which is mass-mail machinery by definition.
|
|
2614
|
+
/^GB_AWSTRACK/,
|
|
2615
|
+
/AWSTRACK/,
|
|
2616
|
+
/^GB_SENDGRID/,
|
|
2617
|
+
/^GB_MAILCHIMP/
|
|
2586
2618
|
];
|
|
2587
2619
|
NEUTRAL_RULES = [
|
|
2588
2620
|
/^BAYES_/,
|