Валидация email в JavaScript: как работает под капотом (разбор кода)

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Как валидировать 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                

Блоги

Рекомендаций по книгам