From d5bc230ab45cdaa7b19e029d19c663fd1eac80c2 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 22 Sep 2026 15:11:17 +0000 Subject: [PATCH] fix(spam): stop blocking signups whose address is initials then digits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A reporter could not register: checkEmail refused their long-established personal Gmail address with "Email matches spam pattern" and no appeal route. They sent a minimal reproduction — checkEmail("abc123456789@example.com") returns { spam: true } — and noted that PR #549 changed plus-tag handling but left this rule untouched. The rule was /^[a-z]{2,3}\d{6,}@/i in SPAM_EMAIL_PATTERNS, meant to catch ab123456@. Initials followed by digits is one of the most ordinary ways a real address is formed: a birth year, a phone fragment, digits of pi. The shape alone says nothing about who typed it, and unlike a username — which a blocked signup can simply pick differently — an email address is the one they have, so a false positive here is a closed door rather than an inconvenience. This is the same correction #549 made to the plus-tag length rule and #531 made to the username shape heuristics: what marks a generated address is randomness, not the presence of digits after letters. The remaining /^[a-z0-9]{20,}@/i still catches long random local parts, and the disposable-domain set and generated-tag test are untouched. The test that asserted ab123456@example.com is spam now asserts it is allowed, alongside the reported shape and three other real-world ones. Co-Authored-By: Claude Opus 5 (1M context) --- src/lib/spam-check.test.ts | 15 ++++++++++++++- src/lib/spam-check.ts | 18 +++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/lib/spam-check.test.ts b/src/lib/spam-check.test.ts index b7e8bed6..fbc73806 100644 --- a/src/lib/spam-check.test.ts +++ b/src/lib/spam-check.test.ts @@ -98,8 +98,21 @@ describe("checkEmail", () => { expect(checkEmail(email).spam).toBe(false); }); + // Initials followed by digits is one of the most ordinary shapes a real + // address takes, and it used to be a hard block with no appeal: a reporter's + // years-old personal address was refused as "Email matches spam pattern". + // An address is not something a blocked signup can just pick differently. + it.each([ + ["abc123456789@example.com", "the shape reported as blocked"], + ["ab123456@example.com", "two initials and a digit run"], + ["jsm19850612@example.com", "initials and a date of birth"], + ["kp4155550143@example.com", "initials and a phone fragment"], + ["xy31415926535@example.com", "initials and digits of pi"], + ])("allows email: %s (%s)", (email) => { + expect(checkEmail(email).spam).toBe(false); + }); + it.each([ - ["ab123456@example.com", "letters then a long digit run"], ["x7f2q9k1m4z8p3w6r5t0@example.com", "long random local part"], ["someone+x7f2q9k1m4z8@gmail.com", "generated-looking tag"], ["someone@mailinator.com", "disposable domain"], diff --git a/src/lib/spam-check.ts b/src/lib/spam-check.ts index 550bb7f3..e8271f8e 100644 --- a/src/lib/spam-check.ts +++ b/src/lib/spam-check.ts @@ -151,8 +151,24 @@ const DISPOSABLE_DOMAINS = new Set([ "guerrillamail.org", "harakirimail.com", "mailforspam.com", ]); +/** + * Local parts that are random rather than chosen. + * + * There used to be a second rule here, `/^[a-z]{2,3}\d{6,}@/i`, meant to catch + * `ab123456@`. It caught people instead. Initials followed by digits is one of + * the most ordinary ways a real address is formed — a birth year, a phone + * fragment, digits of pi — and the shape alone says nothing about who typed it. + * It blocked a reporter's long-established personal Gmail address with nothing + * but "Email matches spam pattern" and no route back in. + * + * Unlike a username, an email address is not something a blocked signup can + * simply pick differently, so a false positive here is a closed door rather than + * an inconvenience. The same reasoning retired the plus-tag length rule below, + * and corroboration replaced shape in checkSpam (#531): what marks a generated + * address is randomness, which the length rule already covers, not the presence + * of digits after letters. + */ const SPAM_EMAIL_PATTERNS = [ - /^[a-z]{2,3}\d{6,}@/i, // ab123456@... /^[a-z0-9]{20,}@/i, // long random local part ];