diff --git a/lib/outreach/cold.ts b/lib/outreach/cold.ts index 866c3ea1..5f628afc 100644 --- a/lib/outreach/cold.ts +++ b/lib/outreach/cold.ts @@ -22,8 +22,48 @@ const INTERNAL = /@(profullstack\.com|crawlproof\.com)$/i; * excludes all role accounts because it mails people who opted in personally; * this list is deliberately narrower. */ +/** + * Mailboxes that must never receive cold outreach. + * + * Not a deliverability heuristic — these are addresses where an unsolicited + * pitch is actively harmful. Writing to an opt-out inbox is the opposite of + * what it exists for; writing to a data-protection officer hands a complaint + * to the one person whose job is filing them; writing to an accessibility or + * accommodations queue takes time from people who need it. + * + * Every entry below was added because a real send reached it. `optout` and + * `dpo` were missed by the original list even though `unsubscribe`, `privacy` + * and `legal` were on it — near-synonyms are not covered by intent, only by + * enumeration, so the list errs toward listing variants. `accomodation` is + * the common misspelling and is deliberately included; the address that + * prompted it was spelled that way. + */ const NEVER_CONTACT_LOCALPART = - /^(noreply|no-reply|donotreply|do-not-reply|mailer-daemon|postmaster|abuse|dmca|security|privacy|legal|unsubscribe|bounce|bounces)$/i; + new RegExp( + "^(" + + [ + // Automated senders — nobody reads these. + "noreply", "no-reply", "donotreply", "do-not-reply", "mailer-daemon", + "postmaster", "bounce", "bounces", + // Opting out. Mailing these is backwards. + "unsubscribe", "optout", "opt-out", "remove", "removeme", "no-contact", + // Reporting and enforcement. + "abuse", "dmca", "security", "fraud", "phishing", "spam", "complaints", + "whistleblower", "ethics", + // Data protection. A cold pitch here is a complaint waiting to happen. + "privacy", "legal", "compliance", "dpo", "gdpr", + "dataprotection", "data-protection", + // Accessibility and accommodations — queues for people who need them. + "accessibility", "a11y", + "accommodation", "accommodations", "accomodation", "accomodations", + // Hiring queues. Wrong target for a pitch, and wrong target for a + // recruiting pitch too — you are writing to a rival's applicants. + "careers", "jobs", "recruiting", "recruitment", "hr", "talent", + "candidates", "applications", "admissions", + ].join("|") + + ")$", + "i", + ); /** Ranked best-first. A named human beats a shared inbox beats a department. */ const ROLE_PREFERENCE = [ @@ -81,7 +121,19 @@ export function domainOf(email: string): string { } export function isNeverContactMailbox(email: string): boolean { - return NEVER_CONTACT_LOCALPART.test(localPart(email)); + const local = localPart(email); + if (NEVER_CONTACT_LOCALPART.test(local)) return true; + // Compound localparts have to be checked token by token: an exact match + // alone lets `candidate-accomodations` through while blocking + // `accomodations`, which is how a real accessibility queue got mailed. + // + // Splitting only on separators is what keeps this from over-blocking — + // `privacyengineering` and `hrothgar` stay one token and stay contactable, + // where a substring match would have refused both. + return local + .split(/[-._+]/) + .filter(Boolean) + .some((token) => NEVER_CONTACT_LOCALPART.test(token)); } /** diff --git a/tests/never-contact.test.ts b/tests/never-contact.test.ts new file mode 100644 index 00000000..3738838a --- /dev/null +++ b/tests/never-contact.test.ts @@ -0,0 +1,97 @@ +import { describe, it, expect } from "vitest"; +import { isNeverContactMailbox, rankContacts, suppressionReason } from "@/lib/outreach/cold"; + +// Every address in this first list actually received a live cold email before +// the guard covered it. They are kept verbatim rather than paraphrased, +// because the failure was that near-synonyms of listed words were not listed: +// `unsubscribe`, `privacy` and `legal` were all present while `optout` and +// `dpo` were not, and intent does not close that gap — enumeration does. +describe("addresses that leaked through and must not again", () => { + const leaked = [ + "optout@dribbble.com", + "dpo@ecoconsult.it", + "candidate-accomodations@activisionblizzard.com", + ]; + + for (const email of leaked) { + it(`never contacts ${email}`, () => { + expect(isNeverContactMailbox(email)).toBe(true); + }); + } +}); + +describe("categories that must never receive cold outreach", () => { + const blocked = [ + // Opting out — mailing these is backwards. + "unsubscribe@example.com", + "opt-out@example.com", + "removeme@example.com", + // Data protection — a pitch here goes to whoever files the complaint. + "privacy@example.com", + "gdpr@example.com", + "data-protection@example.com", + "compliance@example.com", + // Reporting and enforcement. + "abuse@example.com", + "phishing@example.com", + "whistleblower@example.com", + // Accessibility queues, including the common misspelling. + "accessibility@example.com", + "accommodations@example.com", + "accomodations@example.com", + "a11y@example.com", + // Automated senders. + "noreply@example.com", + "mailer-daemon@example.com", + "bounces@example.com", + // Hiring queues. + "careers@example.com", + "recruiting@example.com", + "admissions@example.com", + ]; + + for (const email of blocked) { + it(`blocks ${email}`, () => { + expect(isNeverContactMailbox(email)).toBe(true); + }); + } + + it("reports it as a suppression reason, not a silent drop", () => { + expect( + suppressionReason({ email: "optout@example.com", suppressed: false }), + ).toBe("never-contact-mailbox"); + }); + + it("filters them out of ranked contacts entirely", () => { + const ranked = rankContacts([ + { email: "optout@example.com", source: "mailto", sameDomain: true }, + { email: "jane@example.com", source: "mailto", sameDomain: true }, + ]); + expect(ranked.map((c) => c.email)).toEqual(["jane@example.com"]); + }); +}); + +describe("addresses that are still fair game", () => { + // Over-blocking costs real prospects. A shared business inbox is often the + // only address a small company publishes, and is a legitimate B2B target — + // unlike anything in the list above. + const allowed = [ + "hello@example.com", + "info@example.com", + "contact@example.com", + "support@example.com", + "sales@example.com", + "jane.doe@example.com", + "j.smith@example.com", + // Contains a blocked word but is not that mailbox. + "privacyengineering@example.com", + "careerscoach@example.com", + "hrothgar@example.com", + ]; + + for (const email of allowed) { + it(`allows ${email}`, () => { + expect(isNeverContactMailbox(email)).toBe(false); + }); + } +});