Валидация email — задача, где стремление к строгости приносит больше вреда, чем пользы. Каждая дополнительная проверка отсекает какое-то количество мусора и какое-то количество настоящих пользователей, и второе обычно дороже.
Разберём способы проверки и, главное, где остановиться.
Зачем валидировать email
Проверка email-адреса — обязательный этап при работе с формами. Некорректный адрес приводит к:
- Потерянным письмам и уведомлениям
- Засорению базы данных невалидными записями
- Проблемам с отправкой рассылок (падение репутации домена)
Способ 1: filter_var() — рекомендуемый
Встроенная функция PHP с фильтром FILTER_VALIDATE_EMAIL:
<?php
function isValidEmail(string $email): bool
{
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
// Примеры
$tests = [
'user@example.com', // ✅ true
'user.name@example.co.uk', // ✅ true
'user+tag@example.com', // ✅ true
'user@localhost', // ✅ true (технически валидный)
'user@', // ❌ false
'@example.com', // ❌ false
'user example.com', // ❌ false
'', // ❌ false
];
foreach ($tests as $email) {
$result = isValidEmail($email) ? '✅' : '❌';
echo "{$result} {$email}\n";
}Преимущества
- Встроен в PHP, не требует зависимостей
- Быстрый и оптимизированный
- Соответствует RFC 822/5322
Расширенная проверка
<?php
class EmailValidator
{
/**
* Базовая валидация формата
*/
public static function isValid(string $email): bool
{
return filter_var(trim($email), FILTER_VALIDATE_EMAIL) !== false;
}
/**
* Проверка с очисткой
*/
public static function sanitize(string $email): ?string
{
$clean = filter_var(trim($email), FILTER_SANITIZE_EMAIL);
if (self::isValid($clean)) {
return strtolower($clean);
}
return null;
}
/**
* Проверка существования домена (MX-запись)
*/
public static function hasMxRecord(string $email): bool
{
if (!self::isValid($email)) {
return false;
}
$domain = substr(strrchr($email, '@'), 1);
return checkdnsrr($domain, 'MX') || checkdnsrr($domain, 'A');
}
/**
* Полная валидация: формат + DNS
*/
public static function isDeliverable(string $email): bool
{
return self::isValid($email) && self::hasMxRecord($email);
}
}
// Использование
$email = 'user@gmail.com';
if (EmailValidator::isDeliverable($email)) {
echo "Email валиден и домен существует";
}
// Очистка и нормализация
$dirty = ' User@EXAMPLE.COM ';
$clean = EmailValidator::sanitize($dirty);
echo $clean; // user@example.comСовет: Проверка MX-записи добавляет сетевой запрос. Используйте её только при регистрации, не при каждом логине.
Не пишите свою регулярку для email. Формат адреса описан в RFC 5322, и он гораздо сложнее, чем что-то@что-то.что-то: допустимы плюсы, кавычки, точки в неожиданных местах, доменные имена в национальных алфавитах, новые доменные зоны длиннее четырёх символов.
Любая самописная регулярка отсекает часть валидных адресов. Классические жертвы: имя+тег@gmail.com (частый приём для фильтрации почты), адреса в зонах вида .museum и .travel, адреса на кириллических доменах.
filter_var($email, FILTER_VALIDATE_EMAIL) реализует проверку по стандарту и работает предсказуемо. Раздел ниже полезен для понимания, как устроен формат, но в рабочем коде брать оттуда регулярку не нужно.
Способ 2: Регулярные выражения
Альтернативный подход с preg_match():
<?php
function isValidEmailRegex(string $email): bool
{
// RFC 5322 упрощённый паттерн
$pattern = '/^[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])?)*$/';
return (bool) preg_match($pattern, $email);
}
// Более строгий паттерн (только стандартные домены)
function isValidEmailStrict(string $email): bool
{
$pattern = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/';
return (bool) preg_match($pattern, $email);
}Когда использовать regex
- Нужна кастомная логика (например, запрет определённых доменов)
- Требуется извлечение частей адреса
- Работа в среде без
filter_var()
<?php
class EmailPatternValidator
{
/**
* Проверка с блоком временных почт
*/
public static function isNotDisposable(string $email): bool
{
$disposable = [
'tempmail.com',
'10minutemail.com',
'guerrillamail.com',
'throwaway.email',
'mailinator.com',
];
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return false;
}
$domain = strtolower(substr(strrchr($email, '@'), 1));
return !in_array($domain, $disposable);
}
/**
* Извлечение частей адреса
*/
public static function parse(string $email): ?array
{
$pattern = '/^([^@]+)@(.+)$/';
if (preg_match($pattern, $email, $matches)) {
return [
'local' => $matches[1],
'domain' => $matches[2],
];
}
return null;
}
/**
* Только корпоративные адреса
*/
public static function isCorporate(string $email): bool
{
$freeProviders = [
'gmail.com',
'yahoo.com',
'hotmail.com',
'outlook.com',
'mail.ru',
'yandex.ru',
];
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return false;
}
$domain = strtolower(substr(strrchr($email, '@'), 1));
return !in_array($domain, $freeProviders);
}
}
// Примеры
$email = 'user@company.com';
var_dump(EmailPatternValidator::isNotDisposable($email)); // true
var_dump(EmailPatternValidator::isCorporate($email)); // true
print_r(EmailPatternValidator::parse($email));
// ['local' => 'user', 'domain' => 'company.com']Способ 3: PHPMailer
Библиотека PHPMailer включает собственный валидатор:
composer require phpmailer/phpmailer<?php
use PHPMailer\PHPMailer\PHPMailer;
require 'vendor/autoload.php';
$mail = new PHPMailer();
$tests = [
'user@example.com',
'invalid-email',
'user@localhost',
];
foreach ($tests as $email) {
$valid = $mail->validateAddress($email);
$result = $valid ? '✅' : '❌';
echo "{$result} {$email}\n";
}Когда использовать PHPMailer
- Уже используете библиотеку для отправки писем
- Нужна проверка с учётом IDN (интернационализированные домены)
Сравнение методов
| Метод | Скорость | Зависимости | RFC-совместимость | Рекомендация |
|---|---|---|---|---|
filter_var() | ⚡ Быстрый | Нет | Полная | ✅ Основной |
| Regex | ⚡ Быстрый | Нет | Зависит от паттерна | Для кастомных правил |
| PHPMailer | 🐢 Средний | Composer | Полная + IDN | Если уже используется |
Полный класс валидатора
<?php
namespace App\Validation;
class EmailValidator
{
private array $disposableDomains = [];
private array $allowedDomains = [];
public function __construct(array $options = [])
{
$this->disposableDomains = $options['disposable'] ?? [];
$this->allowedDomains = $options['allowed'] ?? [];
}
/**
* Комплексная валидация
*/
public function validate(string $email, array $rules = []): ValidationResult
{
$email = trim(strtolower($email));
$errors = [];
// Базовая проверка формата
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return new ValidationResult(false, ['Некорректный формат email']);
}
$domain = substr(strrchr($email, '@'), 1);
// Проверка временных почт
if (in_array('no_disposable', $rules)) {
if (in_array($domain, $this->disposableDomains)) {
$errors[] = 'Временные почтовые ящики не разрешены';
}
}
// Проверка белого списка доменов
if (in_array('allowed_only', $rules) && !empty($this->allowedDomains)) {
if (!in_array($domain, $this->allowedDomains)) {
$errors[] = 'Домен не в списке разрешённых';
}
}
// Проверка MX-записи
if (in_array('check_mx', $rules)) {
if (!checkdnsrr($domain, 'MX') && !checkdnsrr($domain, 'A')) {
$errors[] = 'Домен не существует или не принимает почту';
}
}
return new ValidationResult(empty($errors), $errors, $email);
}
}
class ValidationResult
{
public function __construct(
public readonly bool $isValid,
public readonly array $errors = [],
public readonly ?string $normalizedEmail = null
) {}
}
// Использование
$validator = new EmailValidator([
'disposable' => ['tempmail.com', 'mailinator.com'],
'allowed' => [], // Пусто = все разрешены
]);
$result = $validator->validate('user@gmail.com', ['no_disposable', 'check_mx']);
if ($result->isValid) {
echo "Email валиден: {$result->normalizedEmail}";
} else {
echo "Ошибки: " . implode(', ', $result->errors);
}Где остановиться
Проверки можно наращивать бесконечно, поэтому полезно понимать, что каждая из них даёт и чего стоит.
Проверка формата (filter_var) — обязательна, стоит ничего, ложных срабатываний почти не даёт. Берите.
Проверка MX-записи домена — отсекает опечатки в домене (gmali.com), но добавляет сетевой запрос в форму регистрации. Это задержка при отправке и зависимость от DNS: при проблемах с резолвингом вы перестанете регистрировать пользователей вообще. Если делаете — с коротким таймаутом и с обработкой ошибки в сторону «пропустить», а не «отклонить».
Списки одноразовых доменов — работают на известных сервисах и бесполезны против новых, которые появляются постоянно. Плюс регулярно отсекают легитимных пользователей: часть корпоративных доменов попадает в такие списки по ошибке. Применять стоит осознанно и только там, где одноразовые адреса действительно вредят бизнесу.
Белые списки доменов — оправданы в узких случаях (корпоративный сервис для сотрудников одной компании) и вредны во всех остальных.
Ни одна из этих проверок не отвечает на главный вопрос: существует ли адрес и читает ли его человек. Ответ на него даёт только письмо со ссылкой подтверждения — и именно оно является настоящей валидацией.
Отсюда практический вывод, экономящий много времени: не усложняйте проверку формы, если у вас есть подтверждение по ссылке. Опечатка в адресе обнаружится сама — письмо не придёт, пользователь вернётся и исправит. Дополнительные проверки в этом сценарии защищают вас разве что от лишних отправок.
И обратное: если подтверждения нет, никакая валидация формата его не заменит.
Итоги
Минимум, который нужен почти всем: filter_var для формата, нормализация (обрезка пробелов, приведение к нижнему регистру), подтверждение по ссылке.
Нормализация важнее, чем кажется. Пробел, скопированный вместе с адресом, и заглавные буквы — самые частые причины «письмо не приходит». Обрезать и привести к нижнему регистру нужно и при регистрации, и при входе, иначе пользователь не сможет войти под тем же адресом, под которым зарегистрировался.
Сообщение об ошибке должно быть понятным. «Проверьте адрес: возможно, опечатка в домене» полезнее, чем «Некорректный email», особенно если вы уже знаете, что именно не так.
Комментарии
Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.