@bobfrankston/mailx-types 0.1.96 → 0.1.100
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/mailx-api.d.ts +1 -0
- package/package.json +1 -1
- package/trust.d.ts +22 -0
- package/trust.js +59 -5
package/mailx-api.d.ts
CHANGED
package/package.json
CHANGED
package/trust.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ export declare const TRUST_LIST_KEYS: {
|
|
|
39
39
|
readonly domain: "trustedDomains";
|
|
40
40
|
readonly intermediary: "trustedIntermediaries";
|
|
41
41
|
readonly list: "trustedLists";
|
|
42
|
+
readonly via: "trustedSendersVia";
|
|
42
43
|
};
|
|
43
44
|
export type TrustListType = keyof typeof TRUST_LIST_KEYS;
|
|
44
45
|
/** One piece of evidence that a message is not what it claims to be. */
|
|
@@ -63,6 +64,15 @@ export interface TrustFinding {
|
|
|
63
64
|
* when the list is not yet in allowlist.jsonc `trustedLists`, so the
|
|
64
65
|
* viewer can offer "Trust the ‹list› list". (2026-09-09) */
|
|
65
66
|
list?: string;
|
|
67
|
+
/** "‹from domain› via ‹signer›": the reader trusts this sender, nothing
|
|
68
|
+
* proves the From line, but a relay DID sign the message — so the
|
|
69
|
+
* viewer can offer to trust the sender when that relay signs for it
|
|
70
|
+
* (allowlist.jsonc `trustedSendersVia`). (2026-09-10) */
|
|
71
|
+
senderVia?: string;
|
|
72
|
+
/** The proved sender domain whose trust would clear this finding — set
|
|
73
|
+
* on a redirector warning when the sender is proved but not yet on the
|
|
74
|
+
* reader's list, so the viewer can offer "Stop warning about". (2026-09-11) */
|
|
75
|
+
sender?: string;
|
|
66
76
|
}
|
|
67
77
|
export interface TrustInput {
|
|
68
78
|
/** Every header line, in order, as mailparser's `headerLines`. Repeated
|
|
@@ -113,6 +123,18 @@ export interface TrustInput {
|
|
|
113
123
|
* spf=pass for the list's bounce address, or dkim=pass for its domain.
|
|
114
124
|
* An entry counts only on such a message. See listProof. */
|
|
115
125
|
trustedLists?: string[];
|
|
126
|
+
/** "‹from domain› via ‹signer domain›" entries from allowlist.jsonc
|
|
127
|
+
* `trustedSendersVia`. Bob 2026-09-10, Ground News: From
|
|
128
|
+
* groundnews.email, sent through Amazon SES, signed only as
|
|
129
|
+
* amazonses.com, no DMARC record — the sender is on his list and can
|
|
130
|
+
* never be proved, so the list was ignored (correctly) and nothing could
|
|
131
|
+
* be approved. The evidence he does have is the pair: this sender always
|
|
132
|
+
* arrives signed by this relay, and a relay like SES lets a customer send
|
|
133
|
+
* only From identities it has verified. An entry counts only on a
|
|
134
|
+
* message the named relay DKIM-signed, for the named From domain; a
|
|
135
|
+
* forger with their own SES account is not groundnews.email's, and one
|
|
136
|
+
* without a relay signature matches nothing. See provedSenderVia. */
|
|
137
|
+
trustedSendersVia?: string[];
|
|
116
138
|
}
|
|
117
139
|
/**
|
|
118
140
|
* Everything findable about this message, worst first.
|
package/trust.js
CHANGED
|
@@ -39,6 +39,7 @@ export const TRUST_LIST_KEYS = {
|
|
|
39
39
|
domain: "trustedDomains",
|
|
40
40
|
intermediary: "trustedIntermediaries",
|
|
41
41
|
list: "trustedLists",
|
|
42
|
+
via: "trustedSendersVia",
|
|
42
43
|
};
|
|
43
44
|
/** First value of a header, case-insensitive. */
|
|
44
45
|
function header(lines, name) {
|
|
@@ -256,6 +257,27 @@ function provedIntermediary(input) {
|
|
|
256
257
|
return entry;
|
|
257
258
|
return "";
|
|
258
259
|
}
|
|
260
|
+
/** Parse a `trustedSendersVia` entry — "groundnews.email via amazonses.com". */
|
|
261
|
+
function parseSenderVia(entry) {
|
|
262
|
+
const m = (entry || "").trim().toLowerCase().match(/^(\S+)\s+via\s+(\S+)$/);
|
|
263
|
+
return m ? { domain: m[1], signer: m[2] } : null;
|
|
264
|
+
}
|
|
265
|
+
/** The `trustedSendersVia` entry this message satisfies, or "": the From
|
|
266
|
+
* domain matches the entry's, and the entry's relay is among the domains
|
|
267
|
+
* whose signature the receiving server verified. (2026-09-10) */
|
|
268
|
+
function provedSenderVia(input) {
|
|
269
|
+
const entries = (input.trustedSendersVia || []).map(parseSenderVia).filter(Boolean);
|
|
270
|
+
if (!entries.length)
|
|
271
|
+
return "";
|
|
272
|
+
const fromDomain = (bare(input.fromAddress).split("@")[1] || "").toLowerCase();
|
|
273
|
+
if (!fromDomain)
|
|
274
|
+
return "";
|
|
275
|
+
const signers = dkimSigners(input);
|
|
276
|
+
for (const e of entries)
|
|
277
|
+
if (sameOrg(fromDomain, e.domain) && signers.some(s => sameOrg(s, e.signer)))
|
|
278
|
+
return `${e.domain} via ${e.signer}`;
|
|
279
|
+
return "";
|
|
280
|
+
}
|
|
259
281
|
/** The mailing list this message came through, and whether the receiving
|
|
260
282
|
* server proved the list sent it. (2026-09-09)
|
|
261
283
|
*
|
|
@@ -484,6 +506,7 @@ function analyzeServerSpam(input) {
|
|
|
484
506
|
intermediary: provedIntermediary(input),
|
|
485
507
|
signers: dkimSigners(input),
|
|
486
508
|
list,
|
|
509
|
+
senderVia: provedSenderVia(input),
|
|
487
510
|
};
|
|
488
511
|
}
|
|
489
512
|
/**
|
|
@@ -541,7 +564,10 @@ function serverSpamVerdict(input) {
|
|
|
541
564
|
// A trusted mailing LIST answers it for every post the list provably
|
|
542
565
|
// delivered, whoever wrote it — the From line is the list's word, not
|
|
543
566
|
// the author's, and the reader subscribed to the list (2026-09-09).
|
|
544
|
-
|
|
567
|
+
// A sender trusted "via" a relay that signed this message answers it
|
|
568
|
+
// too — the pair is the reader's evidence for a sender that never
|
|
569
|
+
// proves itself (2026-09-10).
|
|
570
|
+
const trustedAndProved = (input.senderTrusted && a.proved) || !!a.intermediary || a.list.trusted || !!a.senderVia;
|
|
545
571
|
if (trustedAndProved && a.kind !== "forgery" && !a.authFailures.length)
|
|
546
572
|
return null;
|
|
547
573
|
// The party that sent this on the author's behalf, when it signed: the
|
|
@@ -604,14 +630,26 @@ function serverSpamVerdict(input) {
|
|
|
604
630
|
detail: `${numbers}${why}${bayes}`,
|
|
605
631
|
};
|
|
606
632
|
const proof = a.proved ? `${provedNote(a)} ` : "";
|
|
607
|
-
if (input.senderTrusted && !a.proved)
|
|
633
|
+
if (input.senderTrusted && !a.proved) {
|
|
634
|
+
// The one thing that did verify: a relay's signature. Name it and
|
|
635
|
+
// the action — a trusted sender that can never be proved otherwise
|
|
636
|
+
// leaves the reader with a banner and no way forward (Bob
|
|
637
|
+
// 2026-09-10, Ground News via Amazon SES: "another valid site. How
|
|
638
|
+
// do I approve it?").
|
|
639
|
+
const fromDomain = (bare(input.fromAddress).split("@")[1] || "").toLowerCase();
|
|
640
|
+
const relaySigner = a.signers.find(s => !sameOrg(s, fromDomain)) || "";
|
|
641
|
+
const senderViaNote = relaySigner
|
|
642
|
+
? `. ${relaySigner} signed this message on the sender's behalf; "Trust ${fromDomain} via ${relaySigner}" stops this note for mail from ${fromDomain} that ${relaySigner} signs.`
|
|
643
|
+
: "";
|
|
608
644
|
return {
|
|
609
645
|
id: "server-spam-verdict",
|
|
610
646
|
severity: "caution",
|
|
611
647
|
text: "This claims to be a sender you trust, but nothing proves it is.",
|
|
612
|
-
detail: `No DMARC pass, no valid signature from the sending domain, and it passed through hosts your server does not trust. ${numbers}${why}${listNote}`,
|
|
648
|
+
detail: `No DMARC pass, no valid signature from the sending domain, and it passed through hosts your server does not trust. ${numbers}${why}${listNote}${senderViaNote}`,
|
|
613
649
|
...viaList,
|
|
650
|
+
...(relaySigner ? { senderVia: `${fromDomain} via ${relaySigner}` } : {}),
|
|
614
651
|
};
|
|
652
|
+
}
|
|
615
653
|
return {
|
|
616
654
|
id: "server-spam-verdict",
|
|
617
655
|
severity: "caution",
|
|
@@ -809,6 +847,19 @@ function provedSenderDomain(input) {
|
|
|
809
847
|
function redirectorLink(input) {
|
|
810
848
|
const bodyHtml = input.bodyHtml || "";
|
|
811
849
|
const senderDomain = provedSenderDomain(input);
|
|
850
|
+
// 5. (2026-09-11) The proved sender is one the reader trusts. The
|
|
851
|
+
// finding's own words were "only the sender could vouch for the
|
|
852
|
+
// link" — and the reader has vouched for the sender. A Toast receipt
|
|
853
|
+
// (dmarc=pass toasttab.com, toasttab.com on Bob's list) links through
|
|
854
|
+
// toasttakeout.page.link, Firebase's per-app redirector, to
|
|
855
|
+
// toasttakeout.com: two organisations by hostname, nobody signed for
|
|
856
|
+
// page.link, the sender is neither — rules 1 to 4 all miss, and
|
|
857
|
+
// Bob: "I trust toasttakeout.com. There should be a trust button."
|
|
858
|
+
// Trust counts only with proof, as everywhere else: without the
|
|
859
|
+
// server's pass the list would be a switch a forger flips by typing a
|
|
860
|
+
// trusted domain into From.
|
|
861
|
+
if (senderDomain && input.senderTrusted)
|
|
862
|
+
return null;
|
|
812
863
|
// 3. (2026-09-08) The redirector belongs to a reader-listed intermediary
|
|
813
864
|
// that provably handled the message. Google Calendar wraps every link
|
|
814
865
|
// in an invitation as google.com/url?q=…, signs the mail d=google.com,
|
|
@@ -853,13 +904,16 @@ function redirectorLink(input) {
|
|
|
853
904
|
// Which party, and how — or why there is no party (Bob 2026-09-08).
|
|
854
905
|
const how = signer
|
|
855
906
|
? ` ${signer} signed this message; "Trust ${signer} as an intermediary" stops this warning for links through ${signer} in mail it signs.`
|
|
856
|
-
:
|
|
907
|
+
: senderDomain
|
|
908
|
+
? ` Nobody signed this message for ${url.hostname}, so there is no intermediary to trust; ${senderDomain} proved it sent this, and "Stop warning about *@${senderDomain}" lets it vouch for its own links.`
|
|
909
|
+
: ` Nobody signed this message for ${url.hostname}, so there is no intermediary to trust; only the sender could vouch for the link, and nothing proves who the sender is.`;
|
|
857
910
|
return {
|
|
858
911
|
id: "redirector-link",
|
|
859
912
|
severity: "caution",
|
|
860
913
|
text: `A link hides where it goes: it passes through ${url.hostname} and ends at ${target.hostname}.`,
|
|
861
914
|
detail: `${url.hostname} -> ${target.hostname}.${how}`,
|
|
862
915
|
...(signer ? { via: signer } : {}),
|
|
916
|
+
...(senderDomain && !signer ? { sender: senderDomain } : {}),
|
|
863
917
|
};
|
|
864
918
|
}
|
|
865
919
|
}
|
|
@@ -982,7 +1036,7 @@ export function spamScoreOf(input) {
|
|
|
982
1036
|
proved: a.proved,
|
|
983
1037
|
provedBy: a.provedBy,
|
|
984
1038
|
bulkOnly: a.bulkOnly,
|
|
985
|
-
trusted: ((!!input.senderTrusted && a.proved) || !!a.intermediary || a.list.trusted) && a.kind !== "forgery" && !a.authFailures.length,
|
|
1039
|
+
trusted: ((!!input.senderTrusted && a.proved) || !!a.intermediary || a.list.trusted || !!a.senderVia) && a.kind !== "forgery" && !a.authFailures.length,
|
|
986
1040
|
reasons: a.reasons,
|
|
987
1041
|
listId: a.list.id,
|
|
988
1042
|
listProved: a.list.proved,
|