@bobfrankston/rmfmail 1.2.312 → 1.2.313
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 +19 -36
- package/.llm/trusted-lists.md +5 -2
- package/TODO.md +2 -2
- package/client/app.bundle.js +34 -7
- package/client/app.bundle.js.map +2 -2
- package/client/components/message-viewer.js +55 -9
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +53 -9
- package/client/styles/components.css +3 -0
- package/docs/allowlist.md +7 -5
- package/docs/spam-false-positives.md +1 -1
- package/npmchanges.md +42 -0
- package/package.json +5 -5
- package/packages/mailx-settings/docs/allowlist.md +7 -5
package/.commitmsg
CHANGED
|
@@ -1,39 +1,22 @@
|
|
|
1
|
-
Trusted
|
|
1
|
+
Trusted lists: the action beside the warning, and a way to undo it
|
|
2
2
|
|
|
3
|
-
Bob 2026-09-09,
|
|
4
|
-
|
|
5
|
-
so Mailman does not rewrite it), re-sends from its own host with its own
|
|
6
|
-
bounce address, and tags the subject and appends a footer — which breaks
|
|
7
|
-
the gmail.com and mitprod signatures. iecc.com recorded both as dkim=fail,
|
|
8
|
-
SpamAssassin fired POSSIBLE_GMAIL_PHISHER / FREEMAIL_FORGED_FROMDOMAIN /
|
|
9
|
-
DKIM_ADSP_CUSTOM_MED, and the chip went red on a talk announcement. The
|
|
10
|
-
author can never be proved on list mail, so per-sender trust had nothing
|
|
11
|
-
to hold on to and the banner (correctly) offered no button.
|
|
3
|
+
Bob 2026-09-09, after v1.2.312: "it would be nice to have an [approve
|
|
4
|
+
this list] on the red warning banner" and "how would I undo an approval".
|
|
12
5
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
hides the chip, exactly as a trusted intermediary does. SPF and DMARC
|
|
22
|
-
failures and URL/phishing listings are never silenced — a list member can
|
|
23
|
-
post a bad link like anyone else.
|
|
6
|
+
An unflagged list post has no banner — the spam-score chip IS the warning
|
|
7
|
+
— so "Trust the ‹List-Id› list" now sits beside the chip as a button, the
|
|
8
|
+
same write as the banner button and the From-menu item. The red (danger)
|
|
9
|
+
banner still carries no list button on purpose: over the last 120 days
|
|
10
|
+
every proved-list post that stayed red (123 of 1,386 flagged list posts)
|
|
11
|
+
was a phishing or malware URL listing on a throwaway domain that signs
|
|
12
|
+
its own mail, and trusting a list never clears those, so a button there
|
|
13
|
+
would be one that does nothing.
|
|
24
14
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
the
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Tests: IEEE-CS headers verbatim (chip kind no longer forgery; trusted hides
|
|
34
|
-
it; flagged variant cautions naming the list and the action, then goes
|
|
35
|
-
quiet), ISOC (DKIM-proved list), an unproved List-Id (just a header), a
|
|
36
|
-
phish listing and an SPF failure on a trusted list (never silenced).
|
|
37
|
-
mailx-types 0.1.89, mailx-settings 0.1.82, mailx-store 0.1.119,
|
|
38
|
-
mailx-service 0.1.35, mailx-store-web 0.1.111. docs/allowlist.md and
|
|
39
|
-
docs/spam-false-positives.md updated; TODO C165.
|
|
15
|
+
Undo: once a list is trusted the chip and banner are gone, so the From
|
|
16
|
+
address's right-click item is now offered on every proved post and reads
|
|
17
|
+
"Stop trusting the ‹List-Id› mailing list" when the list is trusted — the
|
|
18
|
+
toggle the write always was, finally reachable. The action checks that
|
|
19
|
+
the answer went the way it asked and reports otherwise. docs/allowlist.md
|
|
20
|
+
header reworded: the "do not edit" notice is about the .md itself;
|
|
21
|
+
hand-editing allowlist.jsonc is fine and is how a sender or intermediary
|
|
22
|
+
approval is undone until those get an undo of their own.
|
package/.llm/trusted-lists.md
CHANGED
|
@@ -37,10 +37,13 @@ web-service.ts + web-settings.ts DEFAULT_ALLOWLIST (+bump), client/lib/api-clien
|
|
|
37
37
|
client/components/message-viewer.ts, docs/allowlist.md (+ mailx-settings/docs copy),
|
|
38
38
|
tests/trust.test.ts, .commitmsg, TODO.md C165, root version 1.2.311. Then rebuild.cmd.
|
|
39
39
|
|
|
40
|
+
## Follow-up 2026-09-09 evening
|
|
41
|
+
- Button beside the chip ("Trust the ‹List-Id› list"), From-menu item now toggles (Stop trusting…), docs reworded re editing allowlist.jsonc. Danger banners keep no list button: 123/123 proved-list danger cases in 120 days were URL phish/malware listings.
|
|
42
|
+
|
|
40
43
|
## Status
|
|
41
44
|
- [x] trust.ts
|
|
42
45
|
- [x] settings/store/service/web
|
|
43
46
|
- [x] client (banner button, From-menu item, chip tooltip)
|
|
44
47
|
- [x] tests pass (tests/trust.test.ts 40/40)
|
|
45
|
-
- [x] docs/TODO/.commitmsg/versions (v1.2.
|
|
46
|
-
- [
|
|
48
|
+
- [x] docs/TODO/.commitmsg/versions (v1.2.312)
|
|
49
|
+
- [x] rebuild.cmd — published v1.2.312 + APK built (exit 0). App must be restarted to pick up the new bundle.
|
package/TODO.md
CHANGED
|
@@ -144,7 +144,7 @@ None of these are truly showstoppers — mailx is usable — but they're the nex
|
|
|
144
144
|
|
|
145
145
|
| # | Status | Item |
|
|
146
146
|
|---|---|---|
|
|
147
|
-
| **C165** | PARTIAL | Spam banner fires on subscribed newsletters (Constant Contact / Adobe / Mokin). **Rule classification + DMARC veto shipped v1.2.286** — severity now comes from what the `tests=` rules test, not from the score; red is reserved for forgery. **Still owed: sender trust** via `allowlist.jsonc` + an inline trust action on the banner. **Do not tune the `threshold/2` chip floor.** **Trusted mailing lists shipped v1.2.
|
|
147
|
+
| **C165** | PARTIAL | Spam banner fires on subscribed newsletters (Constant Contact / Adobe / Mokin). **Rule classification + DMARC veto shipped v1.2.286** — severity now comes from what the `tests=` rules test, not from the score; red is reserved for forgery. **Still owed: sender trust** via `allowlist.jsonc` + an inline trust action on the banner. **Do not tune the `threshold/2` chip floor.** **Trusted mailing lists shipped v1.2.312** (`trustedLists` by List-Id, proof = server SPF/DKIM pass for the list host). [`docs/spam-false-positives.md`](spam-false-positives.md). [S] |
|
|
148
148
|
| [**S56**](#ext56) | OPEN | Row-objects own the preview pane — the last open slice of the old "S1 local-first refactor" umbrella. All S1 sub-shipments (tombstones, opaque UUIDs, stable row UUID, Message-ID move-detection, pink rows, viewer-reacts-to-list-replace, unified-inbox pink) are in Done under their version tags; this is what remains. ✓ (can start the `focus()`/`unfocus()` seam; full migration is more than one session) |
|
|
149
149
|
| [**S9**](#ext9) | PARTIAL | Predownloading gaps — 60s prefetch landed v1.0.321; per-account prefetch guard already in mailx-imap (`prefetchingAccounts` Set). Remaining: batch body prefetch (C24) for 10–50× speedup. |
|
|
150
150
|
| [**S51**](#ext51) ❓ | PARTIAL | Calendar Thunderbird-style sidebar. Sidebar UI + visible-by-default + 4-column grid reflow shipped v1.0.375/376. **Still owed**: Google Calendar live fetch via service-side proxy. Open Q: keep the full-screen calendar modal, or retire in favor of sidebar-only? |
|
|
@@ -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–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. **v1.2.
|
|
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. **v1.2.312 — trusted mailing lists.** Bob 2026-09-09, on an IEEE-CS announcement (MIT Mailman, poster's own gmail.com From kept, both signatures broken by the subject tag + footer, `POSSIBLE_GMAIL_PHISHER` + `dkim=fail` → red chip, author unprovable so no trust button by design): "how do I say that this is a valid mailing list message". New `allowlist.jsonc` `trustedLists` keyed on **List-Id**; counts only when the topmost `Authentication-Results` proves the list host (`spf=pass` for the Sender:/bounce domain, or `dkim=pass` for the list domain). On a proved list the author's DKIM failure and the From-vs-path rules (`LIST_RELAY_RULES` in trust.ts) are set aside as the list's doing; trusted + proved silences the verdict and the chip; SPF/DMARC failures and URL/phish listings never. Buttons: "Trust the ‹List-Id› list" on the banner, "Trust the ‹List-Id› mailing list" in the From address menu (chip tooltip points there). `TRUST_LIST_KEYS` in mailx-types is now the one table mapping trust type → allowlist key for desktop and Android. Tests: IEEE-CS headers verbatim + ISOC (DKIM-proved list) + unproved List-Id + phish listing + SPF fail. |
|
|
234
234
|
|
|
235
235
|
<a id="concerns"></a>
|
|
236
236
|
|
package/client/app.bundle.js
CHANGED
|
@@ -2931,21 +2931,25 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
|
|
|
2931
2931
|
}, separator: true });
|
|
2932
2932
|
}
|
|
2933
2933
|
const listSpam = msg.spamScore;
|
|
2934
|
-
if (listSpam?.listProved && listSpam.listId
|
|
2934
|
+
if (listSpam?.listProved && listSpam.listId) {
|
|
2935
2935
|
const listId = listSpam.listId;
|
|
2936
|
+
const untrust = !!listSpam.listTrusted;
|
|
2936
2937
|
items.push({
|
|
2937
|
-
label: `\u2713 Trust the ${listId} mailing list`,
|
|
2938
|
+
label: untrust ? `\u2717 Stop trusting the ${listId} mailing list` : `\u2713 Trust the ${listId} mailing list`,
|
|
2938
2939
|
action: async () => {
|
|
2939
2940
|
const st = document.getElementById("status-sync");
|
|
2940
2941
|
try {
|
|
2941
2942
|
const res = await trustSenderOrDomain("list", listId);
|
|
2942
|
-
if (!res
|
|
2943
|
+
if (!res || !!res.trusted === untrust)
|
|
2943
2944
|
throw new Error("the list did not save");
|
|
2944
|
-
listSpam.listTrusted =
|
|
2945
|
-
|
|
2946
|
-
|
|
2945
|
+
listSpam.listTrusted = res.trusted;
|
|
2946
|
+
if (res.trusted) {
|
|
2947
|
+
document.querySelector(".mv-spamscore")?.remove();
|
|
2948
|
+
document.querySelector(".mv-spamscore-action")?.remove();
|
|
2949
|
+
document.querySelector(".mv-trust")?.remove();
|
|
2950
|
+
}
|
|
2947
2951
|
if (st)
|
|
2948
|
-
st.textContent = `Trusting the ${listId} mailing list`;
|
|
2952
|
+
st.textContent = res.trusted ? `Trusting the ${listId} mailing list` : `No longer trusting the ${listId} mailing list \u2014 reopen the message to see its score again`;
|
|
2949
2953
|
} catch (e2) {
|
|
2950
2954
|
if (st) {
|
|
2951
2955
|
st.textContent = `Couldn't trust the ${listId} list \u2014 ${e2?.message || e2}`;
|
|
@@ -3192,6 +3196,29 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
|
|
|
3192
3196
|
Delivered by the ${spam.listId} mailing list, which your server proved sent it. Right-click the From address to trust the list.` : "");
|
|
3193
3197
|
chip.textContent = `spam score ${spam.score} of ${spam.threshold}`;
|
|
3194
3198
|
bodyEl.appendChild(chip);
|
|
3199
|
+
if (spam.listProved && spam.listId && !spam.listTrusted) {
|
|
3200
|
+
const listId = spam.listId;
|
|
3201
|
+
const b = document.createElement("button");
|
|
3202
|
+
b.className = "mv-trust-action mv-spamscore-action";
|
|
3203
|
+
b.textContent = `Trust the ${listId} list`;
|
|
3204
|
+
b.title = `Delivered by the ${listId} mailing list, which your server proved sent it. Trusting the list hides this score on every post the list delivers.`;
|
|
3205
|
+
b.addEventListener("click", async () => {
|
|
3206
|
+
b.disabled = true;
|
|
3207
|
+
try {
|
|
3208
|
+
const res = await trustSenderOrDomain("list", listId);
|
|
3209
|
+
if (!res?.trusted)
|
|
3210
|
+
throw new Error("the list did not save");
|
|
3211
|
+
spam.listTrusted = true;
|
|
3212
|
+
chip.remove();
|
|
3213
|
+
b.remove();
|
|
3214
|
+
document.querySelector(".mv-trust")?.remove();
|
|
3215
|
+
} catch (e) {
|
|
3216
|
+
b.disabled = false;
|
|
3217
|
+
b.textContent = `Could not save: ${e?.message || e}`;
|
|
3218
|
+
}
|
|
3219
|
+
});
|
|
3220
|
+
bodyEl.appendChild(b);
|
|
3221
|
+
}
|
|
3195
3222
|
}
|
|
3196
3223
|
const trust = msg.trust || [];
|
|
3197
3224
|
if (trust.length) {
|