From 9eeace6bc2ebd97698d3de6f41b70a87a0ca9a31 Mon Sep 17 00:00:00 2001 From: overthelex Date: Thu, 2 Jul 2026 15:23:13 +0300 Subject: [PATCH] feat(email): block RU/SU TLDs + disposable domains in validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit validateEmail now rejects addresses on RU/SU TLDs, common Russian webmail (yandex.com, ya.ru, …) and disposable providers (mailinator, yopmail, …) with reason "blocked", before the MX check. These domains have valid MX so MX-only validation would let them through; we don't mail them. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/utils/email-validate.ts | 50 ++++++++++++++++++++++++++++++++++-- tests/email-validate.test.ts | 22 +++++++++++++++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/utils/email-validate.ts b/src/utils/email-validate.ts index 33ad1e0..d4840cd 100644 --- a/src/utils/email-validate.ts +++ b/src/utils/email-validate.ts @@ -8,6 +8,48 @@ export interface EmailValidation { // Reasonable syntax check (not full RFC 5322 — deliberately conservative). const SYNTAX_RE = /^[^\s@]+@[^\s@.]+(\.[^\s@.]+)+$/; +// TLDs we never mail (RU/SU — not a target audience, hurts sending reputation). +const BLOCKED_TLDS = [".ru", ".su"]; + +// Exact registrable domains we block: Russian webmail hosted on other TLDs + +// common disposable/throwaway providers. Matches the domain or any subdomain. +const BLOCKED_DOMAINS = new Set([ + // Russian webmail (mail.ru/list.ru/bk.ru/inbox.ru are already .ru) + "yandex.com", + "yandex.by", + "yandex.kz", + "ya.ru", + // Disposable / throwaway (curated subset of the most common) + "mailinator.com", + "guerrillamail.com", + "guerrillamailblock.com", + "sharklasers.com", + "10minutemail.com", + "temp-mail.org", + "tempmail.com", + "throwawaymail.com", + "yopmail.com", + "getnada.com", + "dispostable.com", + "maildrop.cc", + "fakeinbox.com", + "trashmail.com", + "mailnesia.com", + "mohmal.com", + "emailondeck.com", + "spam4.me", +]); + +function isBlockedDomain(domain: string): boolean { + if (BLOCKED_TLDS.some((tld) => domain.endsWith(tld))) return true; + if (BLOCKED_DOMAINS.has(domain)) return true; + // Subdomain of a blocked domain (e.g. foo.mailinator.com) + for (const d of BLOCKED_DOMAINS) { + if (domain.endsWith("." + d)) return true; + } + return false; +} + // Per-domain MX cache — registry data repeats domains heavily, so one lookup // per domain saves tens of thousands of DNS queries on a 60k import. const mxCache = new Map(); @@ -30,12 +72,16 @@ async function domainHasMx(domain: string): Promise { return ok; } -// Full check: syntax + the domain actually has mail exchangers. -// This is what cuts the bounce rate on stale registry addresses. +export { isBlockedDomain }; + +// Full check: syntax → domain blocklist (RU/SU + disposable) → MX records. +// This is what cuts the bounce rate on stale registry addresses and keeps us +// off domains we should never mail. export async function validateEmail(email: string): Promise { const addr = email.trim().toLowerCase(); if (!validateSyntax(addr)) return { valid: false, reason: "syntax" }; const domain = addr.slice(addr.lastIndexOf("@") + 1); + if (isBlockedDomain(domain)) return { valid: false, reason: "blocked" }; if (!(await domainHasMx(domain))) return { valid: false, reason: "no_mx" }; return { valid: true }; } diff --git a/tests/email-validate.test.ts b/tests/email-validate.test.ts index 9990c0c..094188a 100644 --- a/tests/email-validate.test.ts +++ b/tests/email-validate.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { validateSyntax } from "../src/utils/email-validate.js"; +import { validateSyntax, isBlockedDomain } from "../src/utils/email-validate.js"; describe("email syntax validation", () => { it("accepts normal addresses", () => { @@ -13,3 +13,23 @@ describe("email syntax validation", () => { } }); }); + +describe("domain blocklist", () => { + it("blocks RU/SU TLDs", () => { + for (const d of ["mail.ru", "list.ru", "bk.ru", "example.su", "foo.mail.ru"]) { + expect(isBlockedDomain(d)).toBe(true); + } + }); + + it("blocks RU webmail on other TLDs and disposable providers", () => { + for (const d of ["yandex.com", "ya.ru", "mailinator.com", "yopmail.com", "sub.mailinator.com"]) { + expect(isBlockedDomain(d)).toBe(true); + } + }); + + it("allows normal domains", () => { + for (const d of ["gmail.com", "ukr.net", "i.ua", "meta.ua", "law.com.ua"]) { + expect(isBlockedDomain(d)).toBe(false); + } + }); +});