Merge pull request #202 from Coffey-Labs/feat/sender-link-warnings

Warn about outside senders, large sends and links that mislead
This commit is contained in:
Coffey Labs
2026-09-01 22:48:12 -07:00
committed by GitHub
9 changed files with 665 additions and 11 deletions
+155
View File
@@ -0,0 +1,155 @@
import { describe, expect, it } from "vitest";
import {
crossesRecipientThreshold,
domainCovered,
externalRecipients,
internalDomains,
isExternalSender,
linkVerdict,
shownDomain,
} from "@/lib/warnings";
const addr = (email: string, name: string | null = null) => ({ name, email });
describe("internalDomains", () => {
it("always counts your own identities, without them being configured", () => {
// An account signed in as [email protected] warning that example.com is
// external would be absurd, and is what an empty list would do.
const d = internalDomains(["[email protected]", "[email protected]"], []);
expect([...d].sort()).toEqual(["example.com", "example.org"]);
});
it("adds configured domains, tolerating a leading @ and stray case", () => {
const d = internalDomains([], ["@Partner.com", " sister.org "]);
expect([...d].sort()).toEqual(["partner.com", "sister.org"]);
});
it("ignores empty entries rather than adding an empty domain", () => {
expect(internalDomains(["notanemail"], ["", " ", "@"]).size).toBe(0);
});
});
describe("domainCovered", () => {
const internal = internalDomains([], ["example.com"]);
it("covers the domain itself and its subdomains", () => {
expect(domainCovered("example.com", internal)).toBe(true);
expect(domainCovered("mail.example.com", internal)).toBe(true);
expect(domainCovered("a.b.example.com", internal)).toBe(true);
});
it("does not cover a domain that merely ends with the same letters", () => {
// The whole point of matching on a dot boundary: this is the shape an
// attacker registers.
expect(domainCovered("notexample.com", internal)).toBe(false);
expect(domainCovered("example.com.evil.net", internal)).toBe(false);
});
it("is case-insensitive and says no to nothing", () => {
expect(domainCovered("MAIL.EXAMPLE.COM", internal)).toBe(true);
expect(domainCovered("", internal)).toBe(false);
});
});
describe("externalRecipients", () => {
const internal = internalDomains(["[email protected]"], []);
it("returns only those outside, in the order addressed", () => {
const out = externalRecipients(
[addr("[email protected]"), addr("[email protected]"), addr("[email protected]"), addr("[email protected]")],
internal,
);
expect(out.map((a) => a.email)).toEqual(["[email protected]", "[email protected]"]);
});
it("is empty when everyone is inside", () => {
expect(externalRecipients([addr("[email protected]")], internal)).toEqual([]);
});
});
describe("isExternalSender", () => {
const internal = internalDomains(["[email protected]"], []);
it("reads the first From address", () => {
expect(isExternalSender([addr("[email protected]")], internal)).toBe(true);
expect(isExternalSender([addr("[email protected]")], internal)).toBe(false);
});
it("claims nothing about a message with no sender", () => {
expect(isExternalSender(null, internal)).toBe(false);
expect(isExternalSender([], internal)).toBe(false);
});
});
describe("crossesRecipientThreshold", () => {
it("is off at zero, whatever the count", () => {
expect(crossesRecipientThreshold(500, 0)).toBe(false);
});
it("fires at the threshold and above, not below", () => {
expect(crossesRecipientThreshold(9, 10)).toBe(false);
expect(crossesRecipientThreshold(10, 10)).toBe(true);
expect(crossesRecipientThreshold(11, 10)).toBe(true);
});
});
describe("shownDomain", () => {
it("reads a domain out of link text that is a URL or a bare host", () => {
expect(shownDomain("https://example.com/x")).toBe("example.com");
expect(shownDomain("example.com")).toBe("example.com");
expect(shownDomain(" WWW.Example.COM ")).toBe("www.example.com");
});
it("reads nothing out of text that is prose", () => {
// "click" and "here" are not claims about a destination.
expect(shownDomain("click here")).toBeNull();
expect(shownDomain("here")).toBeNull();
expect(shownDomain("")).toBeNull();
expect(shownDomain(null)).toBeNull();
});
});
describe("linkVerdict", () => {
const trusted = ["example.com"];
it("says nothing about a trusted destination", () => {
expect(linkVerdict("https://example.com/a", "example.com", trusted)).toEqual({ warn: false });
expect(linkVerdict("https://mail.example.com/a", null, trusted)).toEqual({ warn: false });
});
it("warns about an untrusted destination", () => {
expect(linkVerdict("https://unknown.net/a", null, trusted)).toEqual({
warn: true,
reason: "untrusted",
domain: "unknown.net",
});
});
it("warns about a mismatch even when the destination is trusted", () => {
// Trusted is not the same as being the place the text claimed.
expect(linkVerdict("https://example.com/login", "yourbank.com", trusted)).toEqual({
warn: true,
reason: "mismatch",
domain: "example.com",
shownDomain: "yourbank.com",
});
});
it("treats a subdomain of the claimed domain as no mismatch", () => {
expect(linkVerdict("https://login.yourbank.com/", "yourbank.com", ["yourbank.com"])).toEqual({ warn: false });
});
it("leaves alone anything that is not http or https", () => {
// mailto opens the composer; an anchor goes nowhere. Warning about these
// is noise, and noise is how a warning stops being read.
expect(linkVerdict("mailto:[email protected]", null, [])).toEqual({ warn: false });
expect(linkVerdict("#section", null, [])).toEqual({ warn: false });
expect(linkVerdict("javascript:alert(1)", null, [])).toEqual({ warn: false });
expect(linkVerdict("not a url at all", null, [])).toEqual({ warn: false });
});
it("warns about everything when nothing is trusted yet", () => {
const v = linkVerdict("https://example.com/a", null, []);
expect(v).toEqual({ warn: true, reason: "untrusted", domain: "example.com" });
});
});