Почему не стоит валидировать email-адреса с помощью регулярных выражений
Почему regex для email подводит: он отклоняет валидные адреса, принимает недоставляемые и может вызвать ReDoS. Лучше использовать HTML5 email input или библиотеку.
Регулярное выражение не способно валидировать email-адрес — оно лишь проверяет грубое соответствие формату, и даже самый тщательно составленный паттерн будет одновременно отклонять легитимные адреса и принимать синтаксически правдоподобные, которые никогда не доставят письмо. Причина не в том, что вы ещё не нашли правильный паттерн — дело в том, что вопрос «является ли этот email валидным?» объединяет три разные задачи, а регулярное выражение способно решить лишь одну из них — и притом наименее полезную. В этой статье мы разберём каждую из трёх задач по отдельности, обратимся к тому, что на самом деле говорят спецификации (RFC 5321, RFC 5322, RFC 6531 и WHATWG HTML Living Standard), покажем, где именно популярные паттерны дают сбой в обоих направлениях, и предложим прагматичный JS/TS-код для практического применения.
Ключевые выводы
- Валидация email-адресов состоит из трёх уровней: UX-проверка на вменяемость, синтаксическая валидация согласно RFC 5321/5322 и верификация существования — и только подтверждающее письмо доказывает, что адрес реально принимает почту.
<input type="email">из HTML Living Standard использует регулярное выражение, которое сама спецификация называет «намеренным нарушением RFC 5322»; оно сознательно не является RFC-полным и представляет собой лучший вариант по умолчанию, чем любой паттерн, написанный вручную.- Популярные email-регулярные выражения дают сбой в обоих направлениях: они отклоняют реальные адреса (plus-адресация, новые gTLD, quoted local parts, интернационализированные адреса согласно RFC 6531) и принимают недоставляемые.
- Подверженное катастрофическому откату email-регулярное выражение, запущенное на стороне сервера в Node.js, может быть проэксплуатировано с помощью специально сформированного входного значения для блокировки event loop — это вектор атаки типа ReDoS (CWE-1333).
- Единственное ограничение по длине, которое стоит проверять до запуска любого паттерна, взято из RFC 5321 §4.5.3.1.3: сам адрес ограничен 254 символами.
Три уровня валидации email
Валидация email-адресов включает три уровня: UX-проверка на вменяемость (похоже ли это на email?), синтаксическая валидация (соответствует ли строка правилам RFC 5321/5322?) и верификация существования (действительно ли этот почтовый ящик принимает письма?). Только третий уровень доказывает, что адрес работает, и только подтверждающее письмо это обеспечивает. Источники, призывающие «перестать использовать regex», правы, но они смешивают эти уровни. Чёткое разграничение между ними позволяет понять, какой инструмент где применять.
- Уровень 1 — UX-проверка на вменяемость. Быстрая и дешёвая проверка на стороне клиента, которая ловит очевидные опечатки (
alicegmail.com, пробел в конце) и даёт мгновенную обратную связь. Это единственный уровень, где регулярное выражение уместно, и даже здесь следует использовать минимальный паттерн, который справляется с задачей. - Уровень 2 — синтаксическая валидация. Соответствует ли строка грамматике email-RFC? Это значительно сложнее, чем кажется, не поддаётся написанному вручную регулярному выражению и — что принципиально важно — ничего не доказывает относительно доставляемости. Идеально соответствующий RFC адрес может указывать на несуществующий домен.
- Уровень 3 — верификация существования. Принимает ли реальный почтовый ящик письма по этому адресу? Единственным доказательством того, что email-адрес работает, является успешно доставленное сообщение; подтверждающее письмо делает за один шаг то, что никакое регулярное выражение не способно сделать вообще.
Ошибка, которую допускает практически каждое «идеальное email-регулярное выражение», — попытка идеально решить задачу Уровня 2, тогда как Уровень 2 не отвечает на вопрос, который на самом деле важен. Важен вопрос Уровня 3, а до него не дотягивается ни один паттерн.
Что на самом деле означает «валидный»
Discover how at OpenReplay.com.
Валидный email-адрес допускает значительно больше вариантов, чем предполагает большинство регулярных выражений, поскольку грамматика в RFC 5321 (SMTP) и RFC 5322 (формат сообщения) разрешает конструкции, которые выглядят некорректными. Локальная часть — всё, что стоит перед @ — может содержать длинный список специальных символов и даже быть строкой в кавычках.
Локальная часть без кавычек строится из atext, определённого в RFC 5322 §3.2.3, который наряду с буквами и цифрами допускает следующие символы:
! # $ % & ' * + - / = ? ^ _ ` { | } ~
Это означает, что user+tag@example.com (plus-адресация) является валидным — + — это обычный atext согласно RFC 5321 §4.1.2. То, будет ли принимающий сервер обрабатывать +tag как субадрес, определяется реализацией (RFC 5233), но сам адрес является корректно сформированным. Локальная часть также может быть строкой в кавычках: "user name"@example.com является валидным согласно RFC 5321 §4.1.2 и RFC 5322 §3.2.4, включая пробелы. Домен может быть IP-адресом в квадратных скобках — user@[192.168.1.1] является валидным согласно RFC 5321 §4.1.3.
Есть одно ограничение, которое стоит проверять быстро и дёшево. RFC 5321 §4.5.3.1.3 ограничивает forward-path 256 октетами, включая угловые скобки, что оставляет 254 символа для самого адреса; локальная часть отдельно ограничена 64 октетами (§4.5.3.1.1), а домен — 255 октетами (§4.5.3.1.2). Проверка длины — это единственная валидация, которую корректно обрабатывает сравнение строк и для которой не нужно регулярное выражение.
Интернационализированные адреса (EAI)
Интернационализированные email-адреса, определённые в RFC 6531, — например, 用户@例子.广告 — являются валидными и всё более распространёнными; ни одно ASCII-only регулярное выражение их не обрабатывает, и это задача для библиотеки, а не для регулярного выражения. EAI (RFC 6531 §3.3) расширяет локальную часть, допуская UTF-8, а домен может быть не-ASCII Unicode. Это отличается от IDNA-доменов в punycode-кодировке (RFC 5891): EAI распространяется и на локальную часть. Любой паттерн, предполагающий [a-zA-Z0-9] для локальной части, некорректен для растущей доли пользователей по всему миру, и не существует единого регулярного выражения, которое корректно принимало бы как ASCII, так и Unicode-локальные части, не принимая при этом мусор.
Почему email-регулярные выражения дают сбой в обоих направлениях
Написанное вручную email-регулярное выражение даёт сбой и как фильтр, и как привратник: оно порождает ложноотрицательные результаты (отклоняет доставляемые адреса) и ложноположительные (принимает адреса, которые соответствуют грамматике, но никогда не получат письмо). Оба типа ошибок постоянно попадают в продакшн, потому что тестовые наборы используют test@example.com, который проходит любой паттерн.
Возьмём канонический copy-paste с Stack Overflow — паттерн, требующий TLD длиной от 2 до 4 символов:
// Распространённый copy-paste паттерн. Не используйте его.
const bad = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/;
Вот что он делает с реальными адресами:
| Адрес | Что проверяется | Этот regex | Правильный результат | Почему это неверно |
|---|---|---|---|---|
name+filter@gmail.com | plus-адресация | ✅ принимает | ✅ валидный | (здесь проходит, но более строгие паттерны отклоняют +) |
user@studio.photography | длинный gTLD | ❌ отклоняет | ✅ валидный | {2,4} отклоняет TLD длиннее 4 символов |
"user name"@example.com | quoted local part | ❌ отклоняет | ✅ валидный | строки в кавычках и пробелы допустимы |
用户@例子.广告 | EAI (RFC 6531) | ❌ отклоняет | ✅ валидный | ASCII-only классы символов |
someone@validformat.test | несуществующий домен | ✅ принимает | ❌ недоставляемый | синтаксис корректен, но домен не резолвится |
Регулярное выражение, отклоняющее name+filter@gmail.com (plus-адресация) или user@studio.photography (gTLD, делегированный в рамках программы New gTLD ICANN; .photography был добавлен в корневую зону в 2013 году), не является строгим — оно просто ошибочно. Оба адреса синтаксически корректны и используют допустимые возможности email. Одно лишь ограничение {2,4} для TLD ломает .photography, .accountants, .engineering и сотни других валидных делегирований.
Записи сессий нередко показывают, как пользователи сталкиваются с ошибками валидации, несколько раз исправляют ввод и в итоге бросают форму. Исследования удобства использования форм неизменно указывают на трение, вызванное валидацией, как на фактор, способствующий отказам и снижению конверсии.
Ложноположительные результаты — адреса, которые проходят регулярное выражение, но никогда не доставляют письма — столь же реальны. someone@validformat.test проходит приведённый выше паттерн и большинство других, однако .test — это зарезервированный TLD (RFC 2606), который никогда ничего не доставит. Синтаксическое соответствие и доставляемость — независимые свойства, а регулярное выражение видит только первое.
ReDoS: когда регулярное выражение само является уязвимостью
Подверженное катастрофическому откату email-регулярное выражение, запущенное на стороне сервера в Node.js, может быть проэксплуатировано с помощью специально сформированного входного значения для блокировки event loop — это вектор атаки типа «отказ в обслуживании» (CWE-1333: Inefficient Regular Expression Complexity, а также OWASP ReDoS), не имеющий отношения к email, но напрямую связанный с катастрофическим откатом. Паттерны с вложенными или смежными квантификаторами над перекрывающимися классами символов могут выполняться за экспоненциальное время на входных данных, которые почти совпадают.
Ниже приведена воспроизводимая демонстрация. Конструкция (...)+ в паттерне оборачивает группу, которая может совпадать с одним и тем же символом несколькими способами, поэтому длинная последовательность одного символа, за которой следует несовпадающий символ, вынуждает движок перебирать экспоненциальное количество разбиений перед тем, как зафиксировать несовпадение:
// Node.js v24. Запуск: node redos.js
// Намеренно уязвимый паттерн, подверженный катастрофическому откату.
const evil = /^([a-zA-Z0-9]+)*@example\.com$/;
// Специально сформированное «почти совпадение»: много символов 'a', затем символ, ломающий совпадение.
const attack = "a".repeat(40) + "!";
console.time("redos");
evil.test(attack); // блокирует event loop
console.timeEnd("redos");
На текущей сборке Node.js увеличение числа повторений приводит к взрывному росту времени выполнения — каждый добавленный символ примерно удваивает объём работы. Поскольку regex-движок Node выполняется синхронно в основном потоке, один запрос с таким входным значением блокирует event loop и задерживает все остальные запросы в обработке. Конструкция (x+)* — это явный признак уязвимости: любая группа, способная совпасть с одной и той же подстрокой более чем одним способом под внешним квантификатором, является кандидатом на катастрофический откат. Решение — не писать более умный паттерн, а вообще не создавать паттерны такого класса, что как раз и достигается при делегировании задачи платформе или поддерживаемой библиотеке.
Синтаксис — это не доставляемость
Даже идеально соответствующий RFC адрес ничего не говорит о том, будет ли доставлено письмо. Регулярное выражение не может проверить, существует ли домен, есть ли у него MX-записи, создан ли почтовый ящик и не является ли адрес одноразовым. Это вопросы сети и политики, а не грамматики. Адрес realuser@gmail.com и опечатанный realuser@gmial.com синтаксически валидны оба; только DNS-запрос позволяет их различить, и только реальная доставка отличает живой почтовый ящик от мёртвого.
Одноразовые и временные email-домены — это смежная, но отдельная проблема: адреса, которые синтаксически и операционно валидны, но существуют для обхода регистрации. Их обнаружение требует поддерживаемого блок-листа доменов провайдеров, а не паттерна — список доменов постоянно меняется, и любой жёстко закодированный список устаревает. Рассматривайте это как политический слой поверх валидации, а не её часть.
Что делать вместо этого
Используйте многоуровневый подход: минимальная проверка на вменяемость для UX, встроенная валидация платформы для синтаксиса, поддерживаемая библиотека только при необходимости большего, и подтверждающее письмо для единственного, что действительно важно. Вот порядок — от самого дешёвого к авторитетному.
1. Минимальная проверка на вменяемость
Для мгновенной обратной связи на стороне клиента наименьший полезный паттерн — это тот, что фигурирует в исходном аргументе «перестаньте валидировать с помощью regex»: требуется что-то, затем @, что-то, точка и что-то. Дополните его проверкой длины.
/**
* Проверка на вменяемость (Уровень 1): ловит очевидные опечатки и ничего более.
* Намеренно мягкая — НЕ является доказательством валидности.
* @param value - необработанная входная строка
* @returns true, если значение имеет грубую форму email и не превышает 254 символа
*/
export function looksLikeEmail(value: string): boolean {
if (value.length > 254) return false; // RFC 5321 §4.5.3.1.3
return /.+@.+\..+/.test(value);
}
Эта проверка отклоняет alicegmail.com и alice@localhost, принимает plus-адресацию и длинные gTLD и выполняется за константное время. Возвращаемое значение true нельзя трактовать как «валидный» — это лишь ловушка для опечаток.
2. Отдавайте предпочтение платформе: <input type="email">
Лучшим вариантом по умолчанию для синтаксической валидации является браузерный <input type="email">, и стоит точно понимать, что именно он делает. <input type="email"> из HTML Living Standard использует регулярное выражение, которое сама спецификация называет «намеренным нарушением RFC 5322» — оно сознательно не является RFC-полным, жертвуя точностью спецификации ради удобства использования, и это лучший вариант по умолчанию, чем любой паттерн, написанный вручную. Спецификация приводит точный паттерн:
^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$
Разберём по частям:
[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+— локальная часть, допускающая специальные символыatext. Намеренно не поддерживает quoted local parts ("user name"@…).@— ровно один разделитель.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?— метка домена: начинается и заканчивается буквенно-цифровым символом, дефисы допустимы внутри, ограничена 63 символами. Намеренно не поддерживает IP-literal домены (user@[192.168.1.1]).(?:\.<метка>)*— ноль или более дополнительных меток, разделённых точками, так что однометочные и многометочные домены оба проходят.
WHATWG открыто документирует компромисс: этот паттерн намеренно отклоняет некоторые технически валидные RFC 5322-адреса (строки в кавычках, IP-литералы), поскольку такие формы крайне редко встречаются в реальных регистрациях, а их поддержка порождает больше ошибок, чем предотвращает. Это правильный компромисс для поля формы, и именно поэтому <input type="email"> должен быть вашей базовой линией для Уровня 2 — у него нет патологий с откатом, и он соответствует тому, что браузеры уже обеспечивают.
3. Обращайтесь к поддерживаемой библиотеке только при необходимости большего
Если вам нужна серверная синтаксическая валидация, выходящая за рамки HTML5-паттерна, используйте поддерживаемую, хорошо протестированную библиотеку, а не пишите собственную. Пакет validator (npm validator, лицензия MIT) предоставляет функцию isEmail, которая поддерживает quoted local parts и предлагает опции для IP-literal доменов и отображаемых имён:
import isEmail from "validator/lib/isEmail";
/**
* Синтаксическая валидация (Уровень 2), серверная сторона.
* @param email - адрес-кандидат (уже прошедший проверку длины)
* @returns true, если синтаксически валиден согласно RFC-ориентированным правилам validator
*/
export function isSyntacticallyValid(email: string): boolean {
return isEmail(email, { allow_utf8_local_part: true });
}
Предпочтите его более старому пакету email-validator, который не публиковался с 2018 года. Библиотека даёт вам протестированную обработку граничных случаев и активного мейнтейнера, исправляющего ситуации, которые ваш написанный вручную паттерн никогда не учтёт — включая, при правильных опциях, EAI-адреса.
4. Настоящий ответ: отправьте подтверждающее письмо
Единственный шаг, доказывающий, что адрес работает, — это доставка. Отправьте подтверждающее сообщение с одноразовой ссылкой; считайте адрес верифицированным только после того, как пользователь перейдёт по ней. Это double opt-in, и он делает сложную предварительную валидацию излишней — некорректный или недоставляемый адрес просто никогда не подтвердится.
/**
* Набросок потока верификации. Хранилище и почтовый клиент зависят от приложения.
* @param email - строка, уже прошедшая проверки длины и синтаксиса
*/
async function startEmailVerification(email: string): Promise<void> {
const token = crypto.randomUUID();
await storePendingVerification(email, token); // истекает, например, через 24 часа
const link = `https://app.example.com/verify?token=${token}`;
await sendMail(email, "Подтвердите ваш email", `Нажмите для подтверждения: ${link}`);
// Помечаем аккаунт верифицированным только при обращении к /verify с валидным токеном.
}
Отправка подтверждающего письма — это решение, к которому в конечном счёте приходит источник за источником, по одной и той же причине: оно делает за один шаг то, что никакое регулярное выражение не способно сделать вообще. Как выразился Джейми Завински: «Некоторые люди, столкнувшись с проблемой, думают: “Знаю, я воспользуюсь регулярными выражениями”. Теперь у них две проблемы». В случае с email второй проблемой является то, что регулярное выражение всё равно не ответило на вопрос.
Заключение
Перестаньте пытаться валидировать адрес и начните верифицировать почтовый ящик. Используйте минимальный паттерн плюс ограничение в 254 символа для мгновенной UX-обратной связи, опирайтесь на <input type="email"> или поддерживаемую библиотеку вроде validator для синтаксиса, и ставьте каждый реальный аккаунт за шлагбаум подтверждающего письма — этот последний шаг единственный, который доказывает, что кто-то там есть. В следующий раз, когда форма регистрации потребует поле для email, обратитесь к платформе и потоку подтверждения, а не к паттерну со Stack Overflow.
Часто задаваемые вопросы
Какова максимальная допустимая длина email-адреса?
Email-адрес ограничен 254 символами. Это следует из RFC 5321, раздел 4.5.3.1.3, который ограничивает forward-path 256 октетами, включая окружающие угловые скобки, что оставляет 254 символа для самого адреса. Локальная часть отдельно ограничена 64 октетами, а домен — 255 октетами. Простое сравнение длины обеспечивает это корректно — это единственная валидация, которую стоит выполнять до запуска любого паттерна.
Валидирует ли HTML5 email input согласно полной грамматике RFC 5322?
Нет. HTML Living Standard явно описывает regex своего email input как «намеренное нарушение RFC 5322». Он намеренно отклоняет технически валидные формы, такие как quoted local parts ('user name'@example.com) и IP-literal домены (user@[192.168.1.1]), поскольку они крайне редко встречаются в реальных регистрациях. Компромисс отдаёт предпочтение удобству использования перед полнотой спецификации, что делает его более безопасным вариантом по умолчанию, чем написанный вручную паттерн, — однако это не полноценный RFC-валидатор.
Как регулярное выражение для валидации email может вызвать атаку типа «отказ в обслуживании»?
Регулярное выражение с вложенными или смежными квантификаторами над перекрывающимися классами символов, например конструкция ([a-zA-Z0-9]+)*, может выполняться за экспоненциальное время на входных данных, которые почти совпадают. Это катастрофический откат, классифицированный как CWE-1333. При выполнении на стороне сервера в Node.js, где regex-движок работает синхронно в основном потоке, один специально сформированный запрос может заблокировать event loop и задержать все остальные запросы в обработке. Решение — полностью избегать этого класса паттернов, а не писать более умный.
Может ли регулярное выражение проверить, существует ли email-адрес на самом деле?
Нет. Регулярное выражение инспектирует только форму строки; оно не может проверить, существует ли домен, есть ли у него MX-записи или создан ли почтовый ящик. Синтаксическое соответствие и доставляемость — независимые свойства. Адрес вроде realuser@gmial.com синтаксически валиден, но недоставляем из-за опечатки, а someone@validformat.test проходит большинство паттернов, однако использует зарезервированный TLD, который никогда ничего не доставит. Только успешно доставленное подтверждающее письмо доказывает, что адрес принимает почту.
Почему email-регулярные выражения отклоняют валидные адреса вроде name+filter@gmail.com?
Plus-адресация полностью валидна, поскольку знак плюса является обычным atext согласно RFC 5322, раздел 3.2.3, и RFC 5321, раздел 4.1.2. Паттерны, отклоняющие его, а также адреса на длинных gTLD вроде .photography или интернационализированные адреса, определённые в RFC 6531, не являются строгими — они просто ошибочны. Эти ложноотрицательные результаты попадают в продакшн, потому что тестовые наборы используют test@example.com, который проходит любой паттерн, и отклонение реальных адресов никогда не всплывает в тестах.