uCheckeruChecker
Блог/Верификация
12 мин чтения

Регулярные выражения для проверки 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");          // false

Python

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")       # False

Go

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.compasspasspass
tag+test@gmail.compasspasspass
user@sub.domain.co.ukpasspasspass
"john doe"@example.compassfailpass
user@[192.168.1.1]passfailpass
missing-at.comfailfailfail
user@domainpassfailfail
user @exam ple.compassfailfail

Наивный паттерн слишком мягок. 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 бесплатных проверок для старта.

regex emailрегулярное выражение emailemail regex patternвалидация email regexRFC 5322 emailпроверка email JavaScriptemail validation Pythonвалидация форм