@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 CHANGED
@@ -172,6 +172,7 @@ export interface MailxApi {
172
172
  color: string;
173
173
  primary: boolean;
174
174
  accessRole?: string;
175
+ selected?: boolean;
175
176
  }>>;
176
177
  createCalendarEvent(ev: any): Promise<{
177
178
  uuid: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-types",
3
- "version": "0.1.96",
3
+ "version": "0.1.100",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
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
- const trustedAndProved = (input.senderTrusted && a.proved) || !!a.intermediary || a.list.trusted;
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
- : ` Nobody signed this message for ${url.hostname}, so there is no intermediary to trust; only the sender could vouch for the link.`;
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,