@bobfrankston/mailx-types 0.1.88 → 0.1.92
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/index.d.ts +2 -2
- package/index.js +1 -1
- package/package.json +1 -1
- package/trust.d.ts +35 -0
- package/trust.js +160 -8
package/index.d.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* This is the contract between client and server.
|
|
5
5
|
*/
|
|
6
6
|
export { CONTACT_RULES } from "./contact-rules.js";
|
|
7
|
-
export { assessMessageTrust, spamScoreOf } from "./trust.js";
|
|
7
|
+
export { assessMessageTrust, spamScoreOf, TRUST_LIST_KEYS } from "./trust.js";
|
|
8
8
|
export { logIfChanged, clearLogState } from "./log-changes.js";
|
|
9
|
-
export type { TrustFinding, TrustInput, SpamScore } from "./trust.js";
|
|
9
|
+
export type { TrustFinding, TrustInput, SpamScore, TrustListType } from "./trust.js";
|
|
10
10
|
export type { MailxApi } from "./mailx-api.js";
|
|
11
11
|
export { addContactsDenylistEntry, addContactsPreferredEntry, type PreferredContactEntry, type CloudReadFn, type CloudWriteFn, } from "./contacts-config.js";
|
|
12
12
|
export { expandRecipients, splitRecipients, isAddressToken, extractAddress, } from "./groups.js";
|
package/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// this barrel so a single source-of-truth (contact-rules.jsonc) drives
|
|
8
8
|
// junk-contact filtering on every platform.
|
|
9
9
|
export { CONTACT_RULES } from "./contact-rules.js";
|
|
10
|
-
export { assessMessageTrust, spamScoreOf } from "./trust.js";
|
|
10
|
+
export { assessMessageTrust, spamScoreOf, TRUST_LIST_KEYS } from "./trust.js";
|
|
11
11
|
export { logIfChanged, clearLogState } from "./log-changes.js";
|
|
12
12
|
// Shared contacts.jsonc mutations — one implementation for desktop + Android.
|
|
13
13
|
export { addContactsDenylistEntry, addContactsPreferredEntry, } from "./contacts-config.js";
|
package/package.json
CHANGED
package/trust.d.ts
CHANGED
|
@@ -28,6 +28,19 @@
|
|
|
28
28
|
* be you. Forwarded mail fails SPF constantly, and flagging that would be
|
|
29
29
|
* exactly the cry-wolf banner this is trying not to be.
|
|
30
30
|
*/
|
|
31
|
+
/** What the reader can vouch for, and the allowlist.jsonc key each one is
|
|
32
|
+
* written to. One table, shared by the desktop service and the Android
|
|
33
|
+
* web-service, so "trust X" cannot land in different keys on the two
|
|
34
|
+
* platforms — the merge is by key. (Claude Code 2026-09-09; the two
|
|
35
|
+
* services each carried their own ternary before, and adding `list` to one
|
|
36
|
+
* and not the other is exactly the drift this prevents.) */
|
|
37
|
+
export declare const TRUST_LIST_KEYS: {
|
|
38
|
+
readonly sender: "trustedSenders";
|
|
39
|
+
readonly domain: "trustedDomains";
|
|
40
|
+
readonly intermediary: "trustedIntermediaries";
|
|
41
|
+
readonly list: "trustedLists";
|
|
42
|
+
};
|
|
43
|
+
export type TrustListType = keyof typeof TRUST_LIST_KEYS;
|
|
31
44
|
/** One piece of evidence that a message is not what it claims to be. */
|
|
32
45
|
export interface TrustFinding {
|
|
33
46
|
/** Stable identifier — for tests, logs, and any future suppression UI. */
|
|
@@ -46,6 +59,10 @@ export interface TrustFinding {
|
|
|
46
59
|
* the author's behalf. Set only when such a party exists and is not yet
|
|
47
60
|
* trusted, so the viewer can offer the action by name. (2026-09-08) */
|
|
48
61
|
via?: string;
|
|
62
|
+
/** The mailing list that provably delivered this message (its List-Id),
|
|
63
|
+
* when the list is not yet in allowlist.jsonc `trustedLists`, so the
|
|
64
|
+
* viewer can offer "Trust the ‹list› list". (2026-09-09) */
|
|
65
|
+
list?: string;
|
|
49
66
|
}
|
|
50
67
|
export interface TrustInput {
|
|
51
68
|
/** Every header line, in order, as mailparser's `headerLines`. Repeated
|
|
@@ -85,6 +102,17 @@ export interface TrustInput {
|
|
|
85
102
|
* list that cleared it on sight would be a switch any forger could
|
|
86
103
|
* flip by pasting the link. See provedIntermediary. */
|
|
87
104
|
trustedIntermediaries?: string[];
|
|
105
|
+
/** Mailing lists the reader subscribes to, by List-Id ("ieee-cs.mit.edu"),
|
|
106
|
+
* from allowlist.jsonc `trustedLists`. Bob 2026-09-09: "how do I say
|
|
107
|
+
* that this is a valid mailing list message". A list that does not
|
|
108
|
+
* rewrite From (Mailman only does so when the author's domain publishes
|
|
109
|
+
* a DMARC policy; gmail.com does not) delivers the poster's own address,
|
|
110
|
+
* and its subject tag and footer break every signature on the way — so
|
|
111
|
+
* the author can never be proved, and per-sender trust has nothing to
|
|
112
|
+
* hold on to. The LIST can be proved: the receiving server records
|
|
113
|
+
* spf=pass for the list's bounce address, or dkim=pass for its domain.
|
|
114
|
+
* An entry counts only on such a message. See listProof. */
|
|
115
|
+
trustedLists?: string[];
|
|
88
116
|
}
|
|
89
117
|
/**
|
|
90
118
|
* Everything findable about this message, worst first.
|
|
@@ -137,6 +165,13 @@ export interface SpamScore {
|
|
|
137
165
|
/** The reader approved this sender AND the sender is proved: the score is
|
|
138
166
|
* still true, and no longer worth showing. The viewer hides the chip. */
|
|
139
167
|
trusted: boolean;
|
|
168
|
+
/** The mailing list that delivered this (List-Id), or "". With
|
|
169
|
+
* `listProved` — the receiving server recorded the list's host sending
|
|
170
|
+
* it — the viewer can offer "Trust the ‹list› list"; `listTrusted` says
|
|
171
|
+
* the reader already has. (2026-09-09) */
|
|
172
|
+
listId: string;
|
|
173
|
+
listProved: boolean;
|
|
174
|
+
listTrusted: boolean;
|
|
140
175
|
}
|
|
141
176
|
export declare function spamScoreOf(input: TrustInput): SpamScore;
|
|
142
177
|
//# sourceMappingURL=trust.d.ts.map
|
package/trust.js
CHANGED
|
@@ -28,6 +28,18 @@
|
|
|
28
28
|
* be you. Forwarded mail fails SPF constantly, and flagging that would be
|
|
29
29
|
* exactly the cry-wolf banner this is trying not to be.
|
|
30
30
|
*/
|
|
31
|
+
/** What the reader can vouch for, and the allowlist.jsonc key each one is
|
|
32
|
+
* written to. One table, shared by the desktop service and the Android
|
|
33
|
+
* web-service, so "trust X" cannot land in different keys on the two
|
|
34
|
+
* platforms — the merge is by key. (Claude Code 2026-09-09; the two
|
|
35
|
+
* services each carried their own ternary before, and adding `list` to one
|
|
36
|
+
* and not the other is exactly the drift this prevents.) */
|
|
37
|
+
export const TRUST_LIST_KEYS = {
|
|
38
|
+
sender: "trustedSenders",
|
|
39
|
+
domain: "trustedDomains",
|
|
40
|
+
intermediary: "trustedIntermediaries",
|
|
41
|
+
list: "trustedLists",
|
|
42
|
+
};
|
|
31
43
|
/** First value of a header, case-insensitive. */
|
|
32
44
|
function header(lines, name) {
|
|
33
45
|
const want = name.toLowerCase();
|
|
@@ -135,7 +147,28 @@ const NEUTRAL_RULES = [
|
|
|
135
147
|
/^DKIMWL_/, /^RCVD_IN_(DNSWL|MSPIKE_H|IADB)/,
|
|
136
148
|
/^SHORTCIRCUIT$/, /^AWL$/, /^NO_RELAYS$/, /^ALL_TRUSTED$/,
|
|
137
149
|
];
|
|
138
|
-
|
|
150
|
+
/** Rules that test whether the From domain matches the path the message took
|
|
151
|
+
* — which a mailing list breaks BY DESIGN. Mailman re-sends the poster's
|
|
152
|
+
* message from its own host with its own bounce address, so a Gmail From
|
|
153
|
+
* arrives without a Google hop or a Google envelope, and the author's
|
|
154
|
+
* signature no longer verifies over the tagged subject and appended footer.
|
|
155
|
+
* Every one of these fired on an ordinary IEEE-CS meeting announcement
|
|
156
|
+
* (Bob 2026-09-09) and together they painted it as a phish. They are
|
|
157
|
+
* evidence of forgery only when nothing else explains the mismatch; on a
|
|
158
|
+
* message a list PROVABLY delivered (see listProof) the list is the
|
|
159
|
+
* explanation, and they are set to neutral. URL and phishing listings are
|
|
160
|
+
* not in this set: a list member can post a bad link like anyone else. */
|
|
161
|
+
const LIST_RELAY_RULES = [
|
|
162
|
+
/^POSSIBLE_GMAIL_PHISHER$/, // From gmail.com, not sent through Google
|
|
163
|
+
/^FREEMAIL_FORGED_FROMDOMAIN$/, // From and envelope are different freemail domains
|
|
164
|
+
/^(NML_|DKIM_)ADSP_/, // author domain did not sign what arrived
|
|
165
|
+
/^FORGED_[A-Z]+_RCVD$/, // From ‹provider› without ‹provider›'s Received
|
|
166
|
+
];
|
|
167
|
+
function classifyRule(name, dmarcFailed, listRelayed = false) {
|
|
168
|
+
if (listRelayed)
|
|
169
|
+
for (const re of LIST_RELAY_RULES)
|
|
170
|
+
if (re.test(name))
|
|
171
|
+
return "neutral";
|
|
139
172
|
if (dmarcFailed && name === "DKIM_INVALID")
|
|
140
173
|
return "forgery";
|
|
141
174
|
for (const re of FORGERY_RULES)
|
|
@@ -183,6 +216,30 @@ function dkimSigners(input) {
|
|
|
183
216
|
}
|
|
184
217
|
return out;
|
|
185
218
|
}
|
|
219
|
+
/** Did the From domain's OWN organisation pass authentication at the
|
|
220
|
+
* receiving server — a verified signature for the From domain, or an SPF
|
|
221
|
+
* pass for an envelope at the From domain? This is DMARC's relaxed
|
|
222
|
+
* alignment computed locally, for domains that publish no DMARC policy and
|
|
223
|
+
* so never get a dmarc= verdict. Only the From domain's own servers can pass
|
|
224
|
+
* SPF as that domain or sign as it, so a forger's pass for their own domain
|
|
225
|
+
* does not count. Returns which proof, or "". (2026-09-09) */
|
|
226
|
+
function alignedPass(input) {
|
|
227
|
+
const fromDomain = (bare(input.fromAddress).split("@")[1] || "").toLowerCase();
|
|
228
|
+
if (!fromDomain)
|
|
229
|
+
return "";
|
|
230
|
+
if (dkimSigners(input).some(d => sameOrg(d, fromDomain)))
|
|
231
|
+
return "dkim";
|
|
232
|
+
const auth = headerAll(input.headerLines, "authentication-results")[0] || "";
|
|
233
|
+
for (const part of auth.split(";")) {
|
|
234
|
+
if (!/\bspf=pass\b/i.test(part))
|
|
235
|
+
continue;
|
|
236
|
+
const mailfrom = (part.match(/(?:spf|smtp)\.mailfrom=([^\s;,]+)/i)?.[1] || "").toLowerCase();
|
|
237
|
+
const domain = mailfrom.includes("@") ? mailfrom.split("@")[1] : mailfrom;
|
|
238
|
+
if (domain && sameOrg(domain, fromDomain))
|
|
239
|
+
return "spf";
|
|
240
|
+
}
|
|
241
|
+
return "";
|
|
242
|
+
}
|
|
186
243
|
/** The entry in `trustedIntermediaries` that provably handled this message,
|
|
187
244
|
* or "" — proof being a verified signature from its domain, or a DMARC pass
|
|
188
245
|
* when it is the author. The list alone proves nothing. (2026-09-08) */
|
|
@@ -199,6 +256,49 @@ function provedIntermediary(input) {
|
|
|
199
256
|
return entry;
|
|
200
257
|
return "";
|
|
201
258
|
}
|
|
259
|
+
/** The mailing list this message came through, and whether the receiving
|
|
260
|
+
* server proved the list sent it. (2026-09-09)
|
|
261
|
+
*
|
|
262
|
+
* Identity is the List-Id (RFC 2919), the one header that is the same on
|
|
263
|
+
* every post to the list whoever wrote it — `<ieee-cs.mit.edu>` — so it is
|
|
264
|
+
* what `trustedLists` is keyed on. The list's HOST is the Sender: domain
|
|
265
|
+
* (Mailman sets `Sender: list-bounces@host`) or the List-Id's own domain.
|
|
266
|
+
*
|
|
267
|
+
* Proof is the topmost Authentication-Results, which the sender cannot
|
|
268
|
+
* write: `spf=pass` for a bounce address at the list's host — SPF is checked
|
|
269
|
+
* against the envelope, and a list's envelope IS the list — or `dkim=pass`
|
|
270
|
+
* for the list's domain, which Mailman 3 and Sympa add. A List-Id alone is
|
|
271
|
+
* a header anyone can type; without the server's pass it proves nothing,
|
|
272
|
+
* and `trustedLists` never counts on such a message. */
|
|
273
|
+
function listProof(input) {
|
|
274
|
+
const none = { id: "", proved: false, host: "" };
|
|
275
|
+
const raw = header(input.headerLines, "list-id");
|
|
276
|
+
if (!raw)
|
|
277
|
+
return none;
|
|
278
|
+
// `Meeting Announcements <ieee-cs.mit.edu>` or bare `<ieee-cs.mit.edu>`;
|
|
279
|
+
// the angle-bracketed token is the identifier, the rest is a comment.
|
|
280
|
+
const id = (raw.match(/<([^>]+)>/)?.[1] || raw).trim().toLowerCase();
|
|
281
|
+
if (!id)
|
|
282
|
+
return none;
|
|
283
|
+
const senderDomain = (bare(header(input.headerLines, "sender")).split("@")[1] || "").toLowerCase();
|
|
284
|
+
const hosts = [senderDomain, id].filter(Boolean);
|
|
285
|
+
const auth = headerAll(input.headerLines, "authentication-results")[0] || "";
|
|
286
|
+
for (const part of auth.split(";")) {
|
|
287
|
+
if (/\bspf=pass\b/i.test(part)) {
|
|
288
|
+
// iecc writes `spf.mailfrom=user@host`, Microsoft `smtp.mailfrom=host`.
|
|
289
|
+
const mailfrom = (part.match(/(?:spf|smtp)\.mailfrom=([^\s;,]+)/i)?.[1] || "").toLowerCase();
|
|
290
|
+
const domain = mailfrom.includes("@") ? mailfrom.split("@")[1] : mailfrom;
|
|
291
|
+
if (domain && hosts.some(h => sameOrg(domain, h)))
|
|
292
|
+
return { id, proved: true, host: domain };
|
|
293
|
+
}
|
|
294
|
+
if (/\bdkim=pass\b/i.test(part)) {
|
|
295
|
+
const d = (part.match(/header\.d=([^\s;,]+)/i)?.[1] || "").toLowerCase();
|
|
296
|
+
if (d && hosts.some(h => sameOrg(d, h)))
|
|
297
|
+
return { id, proved: true, host: d };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return { id, proved: false, host: "" };
|
|
301
|
+
}
|
|
202
302
|
function dmarcProof(input) {
|
|
203
303
|
const auth = headerAll(input.headerLines, "authentication-results")[0] || "";
|
|
204
304
|
if (!auth)
|
|
@@ -275,6 +375,20 @@ function analyzeServerSpam(input) {
|
|
|
275
375
|
const threshold = Number(status.match(/required=(-?[\d.]+)/)?.[1]);
|
|
276
376
|
const flagged = /^yes/i.test(status) || /^yes/i.test(flag);
|
|
277
377
|
const dmarc = dmarcProof(input);
|
|
378
|
+
// A list the server proved delivered this message explains two things
|
|
379
|
+
// that otherwise read as forgery: the author's signature no longer
|
|
380
|
+
// verifying (the list tagged the subject and appended a footer), and the
|
|
381
|
+
// From domain not matching the path (the list re-sent it from its own
|
|
382
|
+
// host). Both are removed from the evidence on a PROVED list whether or
|
|
383
|
+
// not the reader trusts it — the explanation is a fact about the
|
|
384
|
+
// headers, and the trust is a separate question answered below. SPF and
|
|
385
|
+
// DMARC failures stay: SPF is checked against the list's own envelope,
|
|
386
|
+
// and a list that lets a DMARC failure through is not one to be quiet
|
|
387
|
+
// about. (2026-09-09)
|
|
388
|
+
const listFound = listProof(input);
|
|
389
|
+
const trustedLists = (input.trustedLists || []).map(l => (l || "").trim().toLowerCase());
|
|
390
|
+
const list = { ...listFound, trusted: listFound.proved && trustedLists.includes(listFound.id) };
|
|
391
|
+
const authFailures = list.proved && !dmarc.failed ? dmarc.authFailures.filter(f => f !== "DKIM") : dmarc.authFailures;
|
|
278
392
|
const report = parseSpamReport(header(input.headerLines, "x-spam-report"));
|
|
279
393
|
// Everything up to the first lowercase key=value — SpamAssassin's trailing
|
|
280
394
|
// shortcircuit=/autolearn=/version= fields. Cutting on "not a rule name"
|
|
@@ -288,7 +402,7 @@ function analyzeServerSpam(input) {
|
|
|
288
402
|
name,
|
|
289
403
|
score: found ? found.score : NaN,
|
|
290
404
|
description: found ? found.description : "",
|
|
291
|
-
cls: classifyRule(name, dmarc.failed),
|
|
405
|
+
cls: classifyRule(name, dmarc.failed, list.proved),
|
|
292
406
|
};
|
|
293
407
|
});
|
|
294
408
|
// A rule that scored zero or negative did not put this message over any
|
|
@@ -325,7 +439,7 @@ function analyzeServerSpam(input) {
|
|
|
325
439
|
// blocklist rules" — with 3.2 of its 8.3 coming from KAM_ACCOUNTPHISH,
|
|
326
440
|
// which is neither, listed directly underneath.
|
|
327
441
|
const bulkOnly = rules.every(r => !scored(r) || r.cls === "bulk" || r.cls === "neutral");
|
|
328
|
-
const kind = (
|
|
442
|
+
const kind = (authFailures.length || present("forgery")) ? "forgery"
|
|
329
443
|
: present("association") ? "association"
|
|
330
444
|
: unknownCouldFlagAlone ? "unclassified"
|
|
331
445
|
: rules.length ? "bulk"
|
|
@@ -365,10 +479,11 @@ function analyzeServerSpam(input) {
|
|
|
365
479
|
proved: !!provedBy, provedBy, bulkOnly,
|
|
366
480
|
provedDomain: dmarc.domain || fromDomain, provedPolicy: dmarc.policy,
|
|
367
481
|
bayesHam: rules.some(r => r.name === "BAYES_00" || r.name === "BAYES_01"),
|
|
368
|
-
authFailures
|
|
482
|
+
authFailures,
|
|
369
483
|
reasons,
|
|
370
484
|
intermediary: provedIntermediary(input),
|
|
371
485
|
signers: dkimSigners(input),
|
|
486
|
+
list,
|
|
372
487
|
};
|
|
373
488
|
}
|
|
374
489
|
/**
|
|
@@ -423,7 +538,10 @@ function serverSpamVerdict(input) {
|
|
|
423
538
|
// A trusted INTERMEDIARY that provably handled the message answers the
|
|
424
539
|
// same question the same way — Google Calendar's invitation on Aaron's
|
|
425
540
|
// behalf is wanted because the reader said google.com is (2026-09-08).
|
|
426
|
-
|
|
541
|
+
// A trusted mailing LIST answers it for every post the list provably
|
|
542
|
+
// delivered, whoever wrote it — the From line is the list's word, not
|
|
543
|
+
// the author's, and the reader subscribed to the list (2026-09-09).
|
|
544
|
+
const trustedAndProved = (input.senderTrusted && a.proved) || !!a.intermediary || a.list.trusted;
|
|
427
545
|
if (trustedAndProved && a.kind !== "forgery" && !a.authFailures.length)
|
|
428
546
|
return null;
|
|
429
547
|
// The party that sent this on the author's behalf, when it signed: the
|
|
@@ -439,6 +557,14 @@ function serverSpamVerdict(input) {
|
|
|
439
557
|
const relayNote = relay && !a.proved
|
|
440
558
|
? `. Sent on the author's behalf by ${relay}, which signed it; "Trust ${relay} as an intermediary" stops this note for mail ${relay} signs.`
|
|
441
559
|
: "";
|
|
560
|
+
// The list that provably delivered this, named the same way, with the
|
|
561
|
+
// action that writes `trustedLists`. Offered only while the author is
|
|
562
|
+
// unproved and the list is not yet trusted: a proved author needs no
|
|
563
|
+
// list, and a trusted list returned above.
|
|
564
|
+
const listNote = a.list.proved && !a.list.trusted && !a.proved
|
|
565
|
+
? `. Delivered by the ${a.list.id} mailing list — ${a.list.host} proved it sent this — which re-sent the author's message from its own host, so nothing proves the From line; "Trust the ${a.list.id} list" stops this note for mail the list delivers.`
|
|
566
|
+
: "";
|
|
567
|
+
const viaList = listNote ? { list: a.list.id } : {};
|
|
442
568
|
const numbers = Number.isFinite(a.score) && Number.isFinite(a.threshold)
|
|
443
569
|
? `SpamAssassin score ${a.score} of ${a.threshold}`
|
|
444
570
|
: "flagged by SpamAssassin";
|
|
@@ -483,14 +609,16 @@ function serverSpamVerdict(input) {
|
|
|
483
609
|
id: "server-spam-verdict",
|
|
484
610
|
severity: "caution",
|
|
485
611
|
text: "This claims to be a sender you trust, but nothing proves it is.",
|
|
486
|
-
detail: `No DMARC pass, no valid signature from the sending domain, and it passed through hosts your server does not trust. ${numbers}${why}`,
|
|
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}`,
|
|
613
|
+
...viaList,
|
|
487
614
|
};
|
|
488
615
|
return {
|
|
489
616
|
id: "server-spam-verdict",
|
|
490
617
|
severity: "caution",
|
|
491
618
|
text: "Your mail server classified this as spam before delivering it.",
|
|
492
|
-
detail: `${proof}${numbers}${why}${bayes}${relayNote}`,
|
|
619
|
+
detail: `${proof}${numbers}${why}${bayes}${relayNote}${listNote}`,
|
|
493
620
|
...via,
|
|
621
|
+
...viaList,
|
|
494
622
|
};
|
|
495
623
|
}
|
|
496
624
|
/**
|
|
@@ -529,6 +657,27 @@ function selfSpoof(input) {
|
|
|
529
657
|
const auth = headerAll(input.headerLines, "authentication-results").join(" ; ");
|
|
530
658
|
if (!auth)
|
|
531
659
|
return null;
|
|
660
|
+
// The From domain passed DMARC, aligned, at the receiving server: the
|
|
661
|
+
// message provably came from that domain, which is the opposite of a
|
|
662
|
+
// spoof. Checked BEFORE the failure scan because the scan reads every
|
|
663
|
+
// Authentication-Results and every signature — an ISOC list post
|
|
664
|
+
// (2026-09-09) carried dkim=pass d=elists.isoc.org, dmarc=pass
|
|
665
|
+
// header.from=elists.isoc.org, AND dkim=fail for the original author's
|
|
666
|
+
// standardstrack.com signature the list had broken, and that one fail
|
|
667
|
+
// painted the whole message red as "did not come from your account"
|
|
668
|
+
// (the list address was the Delivered-To, so it counted as Bob's own).
|
|
669
|
+
// One failed signature on a message with an aligned pass is not
|
|
670
|
+
// evidence about the From line; the pass is.
|
|
671
|
+
//
|
|
672
|
+
// The same proof without the dmarc= verdict: a From domain that publishes
|
|
673
|
+
// no DMARC policy gets no dmarc=pass, but its own SPF pass or its own
|
|
674
|
+
// verified signature is the very evidence DMARC would have aligned on.
|
|
675
|
+
// The AMW list (berglist.com, 2026-09-09): spf=pass for
|
|
676
|
+
// amw-bounces@berglist.com, dkim=pass d=berglist.com, and dkim=fail for
|
|
677
|
+
// the gmail.com author signature the list broke — flagged as a spoof of
|
|
678
|
+
// the list's own address.
|
|
679
|
+
if (dmarcProof(input).pass || alignedPass(input))
|
|
680
|
+
return null;
|
|
532
681
|
const failed = [
|
|
533
682
|
/\bspf=(fail|softfail)\b/i.test(auth) ? "SPF" : "",
|
|
534
683
|
/\bdkim=(fail|permerror)\b/i.test(auth) ? "DKIM" : "",
|
|
@@ -833,8 +982,11 @@ export function spamScoreOf(input) {
|
|
|
833
982
|
proved: a.proved,
|
|
834
983
|
provedBy: a.provedBy,
|
|
835
984
|
bulkOnly: a.bulkOnly,
|
|
836
|
-
trusted: ((!!input.senderTrusted && a.proved) || !!a.intermediary) && a.kind !== "forgery" && !a.authFailures.length,
|
|
985
|
+
trusted: ((!!input.senderTrusted && a.proved) || !!a.intermediary || a.list.trusted) && a.kind !== "forgery" && !a.authFailures.length,
|
|
837
986
|
reasons: a.reasons,
|
|
987
|
+
listId: a.list.id,
|
|
988
|
+
listProved: a.list.proved,
|
|
989
|
+
listTrusted: a.list.trusted,
|
|
838
990
|
};
|
|
839
991
|
}
|
|
840
992
|
//# sourceMappingURL=trust.js.map
|