Как валидировать email в JavaScript: от простого regex до полного разбора под капотом
Каждый разработчик хотя бы раз в жизни сталкивался с вопросом: «How can I validate an email address in JavaScript?». Этот вопрос на Stack Overflow собрал тысячи просмотров и сотни ответов, потому что тема — настоящий клубок нюансов. Сегодня мы не просто дадим готовый код, а заглянем под капот: разберем, почему простая проверка может пропустить a@b или заблокировать "test@[IPv6:2001:db8::1]". Поехали!
Почему «просто regex» — это плохая идея?
Самое популярное решение в интернете — скопировать регулярное выражение. Но давайте честно: email-адреса — это хаос. Спецификация RFC 5321 и RFC 5322 позволяют использовать символы +, -, ., % и даже комментарии в скобках. А еще есть домены вроде museum или @[192.168.1.1]. Попробуйте уместить все это в одну строку regex — и вы либо пропустите валидный адрес, либо заблокируете реального пользователя.
Вот пример, который часто встречается на Stack Overflow:
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
Этот код работает для 90% случаев, но:
- Пропускает
test@localhost(валидный для локальной сети) - Блокирует
"test@[192.168.1.1]"(валидный по RFC) - Не проверяет длину домена или наличие точек
Мораль: не пытайтесь написать идеальный regex. Лучше использовать комбинацию простой проверки + серверную валидацию.
Анатомия email-адреса: что нужно знать перед написанием кода
Прежде чем писать валидатор, давайте разберем структуру email по спецификации:
- Локальная часть (до @): может содержать буквы, цифры, точки, плюсы, дефисы, но не может начинаться или заканчиваться точкой. Допустимы кавычки для специальных символов.
- Домен (после @): состоит из меток, разделенных точками. Каждая метка — от 1 до 63 символов. Домен может быть IP-адресом в квадратных скобках.
- Общая длина: не более 254 символов.
Зная это, мы можем написать более осмысленную проверку.
Шаг 1: Простая, но рабочая валидация (для 99% случаев)
Начнем с кода, который используют в боевых проектах (например, в Angular или React). Он не идеален, но достаточен для большинства форм:
function isEmailSimple(email) {
// Убираем пробелы
const trimmed = email.trim();
// Проверка на пустую строку
if (!trimmed) return false;
// Базовая структура: что-то @ что-то . что-то
const atIndex = trimmed.lastIndexOf('@');
if (atIndex < 1 || atIndex === trimmed.length - 1) return false;
const localPart = trimmed.slice(0, atIndex);
const domain = trimmed.slice(atIndex + 1);
// Домен должен содержать хотя бы одну точку
const dotIndex = domain.lastIndexOf('.');
if (dotIndex < 1 || dotIndex === domain.length - 1) return false;
// Проверка длины
if (trimmed.length > 254) return false;
if (localPart.length > 64) return false;
return true;
}
// Примеры:
console.log(isEmailSimple('user@example.com')); // true
console.log(isEmailSimple('a@b.c')); // true (формально валидно)
console.log(isEmailSimple('user@localhost')); // false (нет точки)
console.log(isEmailSimple('@example.com')); // false
Этот код уже лучше, чем слепой regex. Он проверяет структуру, длину и наличие @ с точкой. Но он пропускает user@[192.168.1.1] — для большинства веб-форм это не критично.
Шаг 2: Продвинутая валидация с учетом RFC (почти как в браузере)
Современные браузеры используют встроенную валидацию через атрибут type="email" в HTML. Но как они это делают под капотом? Они используют алгоритм, описанный в спецификации HTML5. Давайте воспроизведем его:
function isEmailRFC(email) {
// Шаг 1: Убираем ведущие/замыкающие пробелы
const trimmed = email.trim();
if (!trimmed) return false;
// Шаг 2: Длина не более 254 символов
if (trimmed.length > 254) return false;
// Шаг 3: Ищем последний @
const atIndex = trimmed.lastIndexOf('@');
if (atIndex === -1 || atIndex === 0 || atIndex === trimmed.length - 1) return false;
const localPart = trimmed.slice(0, atIndex);
const domain = trimmed.slice(atIndex + 1);
// Шаг 4: Локальная часть не длиннее 64 символов
if (localPart.length > 64) return false;
// Шаг 5: Проверка локальной части на недопустимые символы
// RFC разрешает: буквы, цифры, !#$%&'*+/=?^_`{|}~ и .
// Но точка не может быть первой, последней или двойной
const allowedLocal = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+$/;
if (!allowedLocal.test(localPart)) return false;
// Проверка точек в локальной части
if (localPart.startsWith('.') || localPart.endsWith('.')) return false;
if (localPart.includes('..')) return false;
// Шаг 6: Проверка домена
// Домен может быть обычным или в квадратных скобках (IP)
if (domain.startsWith('[') && domain.endsWith(']')) {
// IP-адрес в скобках
const ip = domain.slice(1, -1);
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (!ipv4Regex.test(ip)) return false;
// Дополнительно проверяем, что числа от 0 до 255
const parts = ip.split('.');
if (parts.some(p => parseInt(p) > 255)) return false;
} else {
// Обычный домен
// Должен содержать хотя бы одну точку
const dotIndex = domain.lastIndexOf('.');
if (dotIndex < 1 || dotIndex === domain.length - 1) return false;
// Каждая метка домена: буквы, цифры, дефисы (не в начале/конце)
const labels = domain.split('.');
for (let label of labels) {
if (label.length > 63) return false;
if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(label)) return false;
}
}
return true;
}
// Тесты
console.log(isEmailRFC('user@example.com')); // true
console.log(isEmailRFC