Регулярные выражения для проверки email: полный разбор паттернов
Каждый разработчик хотя бы раз писал regex для валидации email. И почти каждый делал это неправильно. Одни паттерны пропускают половину легитимных адресов, другие принимают строки, которые не имеют отношения к email. В этой статье разберём конкретные regex-паттерны от простых до приближённых к RFC 5322, покажем код на трёх языках и объясним, почему одного regex для настоящей валидации всё равно недостаточно.
Зачем вообще проверять email регулярным выражением
У regex в контексте email одна задача: быстро отсечь очевидный мусор на стороне клиента, прежде чем данные уйдут на сервер. Форма регистрации, подписка на рассылку, checkout. Пользователь вводит «asdf» или забывает символ @. Regex ловит это за микросекунды, без сетевого запроса.
Но тут важно понимать границу применимости. Regex проверяет синтаксис. Он не знает, существует ли домен, отвечает ли MX-сервер, не является ли ящик одноразовым. Это как проверка паспорта по формату номера: можно убедиться, что цифр десять, но нельзя узнать, настоящий ли документ.
Разработчики, которые полагаются только на regex, регулярно получают базы с 20-30% невалидных адресов. Синтаксис верный, но адрес не существует. Поэтому regex стоит рассматривать как первый фильтр, а не как решение.
Уровень 1: наивный паттерн
Самый простой вариант, который встречается в туториалах для начинающих:
.+@.+\..+Что он делает: требует хотя бы один символ до @, хотя бы один после, точку и ещё что-то после точки. Всё.
Проблемы очевидны. Этот паттерн пропустит «hello@.com», «@domain.com» (если точка стоит в нужном месте) и даже строки с пробелами. Он примет «user name@dom ain.c om». Для формы обратной связи на лендинге это допустимо: лучше принять сомнительный адрес, чем потерять лид. Для регистрации аккаунта или платёжной формы такой уровень проверки создаёт проблемы.
Уровень 2: практичный паттерн
Паттерн, который покрывает 95% реальных случаев и при этом остаётся читаемым:
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$Разберём по частям:
^ # начало строки
[a-zA-Z0-9._%+-]+ # local part: буквы, цифры, точка, _, %, +, -
@ # разделитель
[a-zA-Z0-9.-]+ # domain: буквы, цифры, точка, дефис
\. # точка перед TLD
[a-zA-Z]{2,} # TLD: минимум 2 буквы
$ # конец строкиЭтот паттерн корректно обработает подавляющее большинство адресов: user@example.com, firstname.lastname@company.co.uk, tag+filter@gmail.com. Он отсеет строки без @, адреса с пробелами, домены без TLD.
Ограничения тоже есть. Паттерн не принимает кириллические домены (user@почта.рф), не поддерживает quoted local part ("john doe"@example.com) и не проверяет длину частей. Но на практике 99% email-адресов в коммерческих базах попадают в этот формат.
Вот как это выглядит в коде на трёх языках:
JavaScript
function isValidEmail(email) {
const re = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return re.test(email);
}
// Примеры
isValidEmail("user@example.com"); // true
isValidEmail("tag+test@gmail.com"); // true
isValidEmail("user@.com"); // false
isValidEmail("user@domain"); // falsePython
import re
def is_valid_email(email: str) -> bool:
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, email))
# Примеры
is_valid_email("user@example.com") # True
is_valid_email("name@sub.domain.org") # True
is_valid_email("missing-at.com") # FalseGo
package main
import (
"fmt"
"regexp"
)
var emailRe = regexp.MustCompile(
`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`,
)
func isValidEmail(email string) bool {
return emailRe.MatchString(email)
}
func main() {
fmt.Println(isValidEmail("dev@company.io")) // true
fmt.Println(isValidEmail("no-domain@")) // false
}Уровень 3: приближённый к RFC 5322
RFC 5322 определяет формат email-адреса. Полное соответствие стандарту требует рекурсивного разбора, который regex не в состоянии выполнить. Но можно подобраться близко.
Паттерн ниже часто используется в production-системах. Он обрабатывает quoted strings в local part, вложенные поддомены, числовые TLD и другие краевые случаи:
^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*
|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]
|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")
@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?
|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}
(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?
|[a-z0-9-]*[a-z0-9]:
(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]
|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$Выглядит пугающе. На практике этот паттерн используется в библиотеках валидации, а не пишется вручную. Если вы видите его в кодовой базе проекта без комментариев и ссылки на источник, это повод для code review.
Что покрывает этот паттерн, чего не покрывает практичный вариант:
- Quoted local part: "john doe"@example.com
- IP-адрес вместо домена: user@[192.168.1.1]
- Спецсимволы в local part: !#$%&'*+/=?^_{|}~
- Экранированные символы внутри кавычек
Стоит ли использовать его вместо практичного? Зависит от контекста. Для формы регистрации SaaS-продукта практичный паттерн предпочтительнее. Он проще, его можно прочитать и отладить. Никто не регистрируется с адресом "john doe"@[192.168.1.1]. Для почтового сервера или SMTP-библиотеки RFC-паттерн обоснован.
HTML5 input type="email" и встроенная валидация
Прежде чем писать свой regex, стоит вспомнить, что браузеры уже делают базовую проверку. Элемент input с type="email" использует собственный паттерн, описанный в спецификации WHATWG:
<input type="email" required />
<!-- Браузерный regex (WHATWG spec): -->
<!-- ^[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])?)*$ -->Этот паттерн строже наивного, но мягче RFC 5322. Он не поддерживает quoted local part и IP-литералы. Для большинства веб-форм этого достаточно.
Комбинация input type="email" на фронтенде и серверного regex даёт два слоя защиты без лишнего кода. Браузер покажет нативную ошибку, сервер перехватит запросы от ботов и кастомных клиентов.
Частые ошибки в regex для email
За годы code review мы видели одни и те же проблемы. Вот пять самых распространённых.
1. Ограничение TLD тремя символами
# Плохо: отрежет .info, .museum, .company
\.[a-zA-Z]{2,3}$
# Хорошо: принимает любую длину TLD
\.[a-zA-Z]{2,}$С 2014 года ICANN выдала сотни новых gTLD. Адреса вроде user@startup.technology или contact@my.company вполне легитимны. Паттерн с {2,3} их отбросит.
2. Запрет символа + в local part
# Плохо: не пропустит tag+filter@gmail.com
^[a-zA-Z0-9._-]+@
# Хорошо: плюс включён
^[a-zA-Z0-9._%+-]+@Plus-адресация (subaddressing) поддерживается Gmail, Outlook, Fastmail и другими провайдерами. Технически грамотные пользователи активно ей пользуются. Блокировка + отсечёт часть аудитории.
3. Отсутствие якорей ^ и $
# Плохо: найдёт email внутри любой строки
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/
# Хорошо: проверяет строку целиком
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/Без якорей regex найдёт подстроку, похожую на email, внутри произвольного текста. Строка «купи виагру user@spam.com прямо сейчас» пройдёт валидацию.
4. Case-sensitive проверка домена
Доменная часть email регистронезависима по стандарту. User@GMAIL.COM и user@gmail.com ведут в один ящик. Если ваш regex не включает флаг /i или не указывает A-Z в классе символов, вы потеряете адреса с заглавными буквами в домене.
5. Попытка проверить всё одним regex
Разработчик хочет в одном выражении проверить синтаксис, длину local part (до 64 символов), длину домена (до 253 символов), запрет двух точек подряд, запрет точки в начале и конце local part. Результат: regex на 300 символов, который никто не может прочитать, протестировать или исправить. Лучше разбить проверку на этапы.
function validateEmail(email) {
// Этап 1: базовая структура
if (!email || !email.includes("@")) return false;
const [local, domain] = email.split("@");
// Этап 2: длина частей (RFC 5321)
if (local.length > 64) return false;
if (domain.length > 253) return false;
// Этап 3: запрет точки в начале/конце local
if (local.startsWith(".") || local.endsWith(".")) return false;
// Этап 4: запрет двух точек подряд
if (local.includes("..")) return false;
// Этап 5: regex для допустимых символов
const re = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return re.test(email);
}Каждый этап можно протестировать отдельно. Каждый возвращает понятную ошибку. Regex остаётся простым и занимается только тем, для чего он хорош: проверкой допустимых символов.
Сравнение паттернов
Что пройдёт и что не пройдёт через каждый уровень:
| Адрес | Наивный | Практичный | RFC 5322 |
|---|---|---|---|
| user@example.com | pass | pass | pass |
| tag+test@gmail.com | pass | pass | pass |
| user@sub.domain.co.uk | pass | pass | pass |
| "john doe"@example.com | pass | fail | pass |
| user@[192.168.1.1] | pass | fail | pass |
| missing-at.com | fail | fail | fail |
| user@domain | pass | fail | fail |
| user @exam ple.com | pass | fail | fail |
Наивный паттерн слишком мягок. RFC-паттерн слишком сложен для типовых задач. Практичный паттерн попадает в правильный баланс для большинства веб-приложений.
Библиотеки вместо самописного regex
Для production-кода имеет смысл использовать готовые решения, которые обновляются и покрыты тестами:
# JavaScript / TypeScript
npm install zod
# или
npm install validator
# Python
pip install email-validator
# Go
go get github.com/badoux/checkmailПример с Zod, который стал де-факто стандартом валидации в TypeScript-проектах:
import { z } from "zod";
const schema = z.object({
email: z.string().email("Некорректный email"),
});
// Валидация
const result = schema.safeParse({ email: "user@example.com" });
if (!result.success) {
console.log(result.error.issues);
}Библиотека email-validator для Python идёт дальше синтаксиса: проверяет DNS-записи домена и определяет нормализованную форму адреса.
from email_validator import validate_email, EmailNotValidError
try:
info = validate_email("user@example.com", check_deliverability=True)
normalized = info.normalized
except EmailNotValidError as e:
print(str(e))Библиотеки решают задачу синтаксической проверки. Но даже с DNS-проверкой они не знают, активен ли ящик, не является ли он спам-ловушкой и будет ли принимать почту через неделю.
Почему одного regex недостаточно для реальной валидации
Адрес test@example.com пройдёт любой regex. Синтаксически он безупречен. Но example.com зарезервирован IANA для документации и не принимает почту. Regex этого не знает.
Полноценная валидация email включает несколько уровней, которые regex физически не может выполнить:
- Проверка MX-записей домена. Есть ли у домена почтовый сервер? Для этого нужен DNS-запрос.
- SMTP handshake. Отвечает ли сервер? Принимает ли конкретный адрес? Для этого нужно установить соединение.
- Catch-all домены. Некоторые серверы принимают любой адрес на своём домене. user123456@company.com пройдёт SMTP-проверку, но это не значит, что ящик реально читают.
- Disposable-домены. Адреса на mailinator.com, guerrillamail.com и сотнях подобных сервисов синтаксически валидны, но бесполезны для рассылок.
- Спам-ловушки. Адреса, специально созданные для отлова спамеров. Попадание в такой ящик портит репутацию домена отправителя.
Для каждого из этих уровней нужна серверная логика, сетевые запросы, актуальные базы данных. Regex этого не умеет по определению. Он работает с текстом, а не с инфраструктурой.
Regex для email - это проверка орфографии. Нужна, но не заменяет проверку фактов. Синтаксически корректный адрес может быть мёртвым, одноразовым или ловушкой.
Рабочая стратегия: regex + сервис валидации
Оптимальный pipeline валидации email выглядит так:
Пользователь вводит email
│
▼
[1] HTML5 input type="email" ← браузер, бесплатно
│
▼
[2] Клиентский regex ← мгновенный фидбек
│
▼
[3] Серверный regex + длина ← защита от ботов
│
▼
[4] Сервис валидации (API) ← MX, SMTP, disposable, ловушки
│
▼
Адрес добавлен в базуПервые три уровня отсекают 70-80% мусора и работают мгновенно. Четвёртый уровень проверяет оставшееся на стороне сервера. Вместе они дают валидацию, которая действительно защищает базу.
Пример интеграции клиентского regex с серверной валидацией через API:
// Фронтенд: быстрая проверка перед отправкой
function onSubmit(email) {
const re = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!re.test(email)) {
showError("Проверьте формат email");
return;
}
// Бэкенд: полная валидация
fetch("/api/validate-email", {
method: "POST",
body: JSON.stringify({ email }),
})
.then((res) => res.json())
.then((data) => {
if (data.status === "valid") {
proceedWithRegistration(email);
} else {
showError(data.reason);
}
});
}На стороне бэкенда /api/validate-email вызывает сервис валидации, который проверяет MX, SMTP, catch-all, disposable-статус и возвращает результат с уровнем риска. Regex отработал свою роль на фронтенде. Всё остальное делает инфраструктура.
Regex проверяет формат. Для проверки реальности адреса нужен сервис валидации. uChecker проверяет MX, SMTP, catch-all, disposable-домены и спам-ловушки. 30 бесплатных проверок для старта.
