
Поначалу некоторые из этих «вымыслов» могут показаться очевидными или неважными. Откровенно говоря, это не так уж далеко от истины. Однако позвольте мне нарисовать подробную забавную картину, демонстрирующую, что даже скучная электронная почта может неожиданным образом противоречить нашим ожиданиям.
Мы рассмотрим множество пограничных случаев, споткнёмся об маленькие препятствия и обнаружим, что некоторые технически корректные детали не всегда поддерживаются даже в больших системах наподобие Gmail (и, честно говоря, на то есть веские причины).
Не каждый пример будет полезным пограничным случаем, который вам обязательно нужно правильно обрабатывать. Но в конечном итоге я подведу вас к одному основному выводу: адреса электронной почты погрязли в легаси, а определения валидных и невалидных компонентов системы постепенно незаметно меняются.
Мы можем легко принять, казалось бы, здравое решение, которое неожиданно вызовет проблемы. Кроме того, многоопытные разработчики (и старые системы) могут иметь ожидания, ранее бывшие корректными, но больше не работающие. Итак, без лишних предисловий, перейдём к вымыслам…
«Адреса электронной почты можно валидировать при помощи регулярных выражений»
Давайте сначала разоблачим самое простое заблуждение. Я далеко не первый человек, говорящий это онлайн, и определённо не буду последним. Но мне кажется, что повторять это полезно, потому что даже после тысяч постов, твитов и комментариев на Reddit этот антипаттерн упорно отказывается умирать даже в 2026 году.
Решение на основе регулярных выражений может вызвать инфаркт у вас, вашего бизнеса и клиентов тремя основными способами:
-
Его достаточно затратно выполнять, при этом оно не обеспечивает практически никакой выгоды
Честно говоря, этот пункт раньше был болезненнее, но теперь мы приучились кидать простые запросы в огромные кластеры прожорливых GPU.
-
Regex сложны, их магией владеют немногие, а реализации движков регулярных выражений несогласованы. Очень легко совершить ошибку, не осознавая этого.
В этом посте мы рассмотрим пару примеров, потенциально приводящих к ошибкам.
-
Даже если вы скопипастили одно из «хороших» regex, мир продолжает эволюционировать. Валидное сегодня завтра превратится в легаси
Интернет замусорен советами, актуальными двадцать лет назад. А ещё он замусорен регулярными выражениями, которые были хороши двадцать лет назад, потому что легаси вечно.
Предупреждение о личном мнении: при валидации ввода важнее помогать пользователю, чтобы ему сложнее было совершать простые ошибки. Она должна быть достаточно строгой, чтобы упростить жизнь пользователя, но только ненамного. Не пользуйтесь валидацией ввода для защиты от своих пользователей; применяйте её для защиты пользователей от самих себя. В этом отношении есть вполне разумный аргумент об использовании regex-тестов для улучшения UX, о котором стоит поразмыслить. UX важен и я не отрицаю этого. Однако, попробую стать на мгновение адвокатом дьявола: возможно, риски перевешивают выгоду. В году 2026-м от Рождества Христова можно разумно ожидать, что пользователи умеют вводить собственный почтовый адрес или, что ещё лучше, автоматически вводят его из операционной системы, браузера, клавиатурного приложения или менеджера паролей.
Возможно даже, что плохо реализованная валидация форм отфильтровала больше людей, чем их собственная необходимость в поддержке. Следовательно:
-
Не валидируйте адреса электронной почты. Если же это необходимо, то используйте простое клиентское regex, чтобы помочь пользователям избежать распространённых ошибок и опечаток.
Пытайтесь обеспечить как можно большую свободу. Используйте что-то типа
^[^@]+@[^@\s]+$, просто проверяющее, что пользователь ввёл «something@something»
-
Если валидация есть в API или обработчике форм, то для согласованности с фронтендом используйте то же самое регулярное выражение.
Это возвращает нас к пункту «не используйте валидацию ввода для защиты себя от пользователей». По умолчанию защищайте себя санацией ввода, а не отказом от него.
-
Верифицируйте адрес, не беспокойтесь о его валидации.
Отправьте письмо, пусть пользователь нажмёт на подтверждающую ссылку или введёт код верификации.
Вот и всё. Ничего усложнять не нужно. Не надо проверять запись MX домена; ваш сервис электронной почты делает это в процессе отправки письма (спойлер: ниже будет немного забавной информации о записях MX). И вам уж точно не нужно большое регулярное выражение. Вы, наверно, всё равно отправляете письмо для подтверждения! Если так, то это может стать оправданием для удаления кода, а подобное всем программистам нравится больше всего!
Итак, с самым простым разобрались, а теперь давайте перейдём к специфическим пунктам, из-за которых обработка почты становится забавной и сбивает с толку!
«Адрес электронной почты должен быть валидным, а сервисы электронной почты поддерживают любой валидный адрес»
Возможно, вас это шокирует, но Интернет состоит из ПО. И не существует какого-то единого стандартного ПО; на самом деле, оно очень часто расходится с нашими ожиданиями.
У ПО серверов электронной почты, от крупных сервисов наподобие Gmail до опенсорсных проектов наподобие Postfix, уровни поддержки официальных правил форматов электронной почты варьируются. Поддержка SMTPUTF8 появилась в Postfix ещё примерно в 2015 году, но её включили по умолчанию только спустя несколько лет. С другой стороны, Dovecot по-прежнему не поддерживает его даже в 2026 году. И это касается не только опенсорса: Gmail ограничивает список символов при создании адреса, однако, похоже, поддерживает отправку в UTF8.
Ниже мы подробнее рассмотрим SMTPUTF8 и RFC 6531. Но давайте взглянем на другой пример, в котором мы можем напрямую противоречить ограничениям всех соответствующих RFC. Посмотрим на RFC 5321 Section 4.5.3, где определяются предельные значения длин.
4.5.3.1.1. Локальная часть Максимальная общая длина имени пользователя или другой локальной части равна 64 октетам.
Это достаточно простое ограничение и его вполне легко понять! Но отдельные его части могут быть незнакомы для некоторых читателей:
-
«локальная часть» (local-part)
Если не усложнять, то локальная часть — это всё, что идёт перед символом
@.(В этом примере рассмотрим простой случай, но ниже снова вернёмся к локальной части)
-
октет
Октет — это стандартный 8-битный байт (Википедия)
Следовательно, адрес электронной почты entirelytoomanycharactersinthisemailwhatisevenhappeningblahblahdonttrythisathome@gitpush–force.com, имеющий 80-байтную локальную часть, должен стопроцентно считаться невалидным. Так что, естественно, работать не должен.
Но помните, я говорил, что поведение ПО расходится с ожиданиями? На самом деле, вы сможете отправить мне письмо на этот адрес. Ваш провайдер разрешит его (скорее всего) без возражений, а мой провайдер без проблем доставит его в мои входящие.
«Адрес электронной почты может состоять только из символов ASCII»
Наверно, это заблуждение сильнее распространено в англоязычном мире, но мне любопытно: если вы живёте не в англосфере, то ожидаете ли, что в адресах обязательно должна использоваться латиница ASCII?
В 2012 году (это и совсем недавно, и очень давно по меркам мира технологий), возникла интернализация электронной почты, реализованная в наборе RFC, соответствующих различным частям стека почты. В частности, RFC 6531 определяет расширение SMTPUTF8, позволяющее использовать в локальной части адреса электронной почты с символами вне ASCII. Довольно удивительно то, что ещё всего 14 лет назад большинство населения Земли не могло указать в адресе почты собственное имя на собственном языке.
Международные символы теоретически работали и допускались даже до 2012 года, например, в рамках Punycode и RFC 3490 — хака кодирования, позволявшего представлять символы Unicode в ASCII. Но в те времена это возможно было только для имени домена, а локальная часть всё ещё была ограничена ASCII.
«Адрес электронной почты должен быть человекочитаемым»
Затронув латиницу, можно углубиться дальше. При интернализации локальная часть определяется в виде потока октетов. По сути, мы помещаем байты, не соответствующие валидному символу стандартного Unicode. Можно добавить в адрес почты �, и он останется валидным. Но этот символ нечитаем ни в одном человеческом языке.
«У адресов электронной почты всегда есть домен второго уровня (SLD) и домен верхнего уровня (TLD)»
Вспомните знакомые нам адреса, с которыми мы работаем повседневно. Остров @icloud.com в море @gmail.com, почта провайдеров, @employer.com и @school.edu. Все они соответствуют привычному паттерну: нечто-точка-нечто.
Существует три типа валидных адресов, не соответствующих ему; приведу их в порядке убывания важности.
-
Адреса, имеющие поддомены, а значит, и несколько точек в домене.
-
Адреса без точки
В реальном мире этот пункт важен для адресов, например, интранета, где каждая машина сети имеет имя хоста. Этот случай важно учитывать ПО для работы с почтой, но, вероятно, для большинства из нас он не актуален.
Строго говоря, кто-нибудь в ICANN, Verisign и так далее может зарегистрировать адрес наподобие admin@net, но давайте не вдаваться в такие фантазии.
-
Адреса, в которых вместо имени домена используется IP-адрес. В RFC 5321 Section 4.1.3 напрямую указывается поддержка такого случая, он называется «литералы адреса».
Этот случай встречается очень редко, но вполне допустим в реальном мире.
-
Кстати, вы можете написать мне и на ben@[50.169.39.178], если такой формат поддерживает ваш почтовый клиент.
К сожалению, у веб-клиента Gmail, похоже, есть с этим проблемы. Но я думаю, что если вы будете использовать клиент наподобие Thunderbird, то это всё же сработает через Gmail.
Веб-клиент Gmail отправляет письма, но при моём тестировании он, похоже, вырезает квадратные скобки
[], определённые в разделе «Address Literals» RFC, и сообщения отправляются на безымянный хост.
«У электронных адресов всегда есть "нормальный" TLD»
Мой личный адрес почты находится не в .com или .net, а в домене .email. И это очень сильно мне нравится, ведь я дурачок, любящий всякие приколы и странности просто из-за их прикольности и странности.
Однако мне пришлось создать аналог на домене .net, потому что оказалось, что множество компаний выполняет логику валидации, которая, похоже, не допускает домены, не входящие в привычный список: .com, .net, .org, .edu и так далее.
«В адресе электронной почты может быть только один символ @»
Примечание: с развенчанием этого заблуждения у меня возникли трудности; возможно, я неправильно понял RFC. Я добавил этот раздел на случай, если кто-нибудь подскажет, что же я упускаю! Как я уже говорил, со мной можно связаться по адресу ben@[50.169.39.178], если его поддерживает ваш клиент, и добавил, что у Gmail как будто есть с ним проблемы. Но в данном случае Gmail просто отказывается отправлять письмо.
Так что, если Gmail не поддерживает его в явном виде, то, вероятно, и вам тоже не стоит. Но теоретически, согласно RFC 5322 Section 3.2.4, это допустимо:
Строки символов, содержащие символы, не входящие в допускаемые в атомах, можно представить в формате закавыченной строки, где символы окружены символами кавычек (DQUOTE, ASCII-значение 34). qtext = %d33 / ; Печатные символы %d35-91 / ; US-ASCII, исключая %d93-126 / ; "\" и символ кавычки obs-qtext qcontent = qtext / quoted-pair quoted-string = [CFWS] DQUOTE *([FWS] qcontent) [FWS] DQUOTE [CFWS]
Чтение RFC иногда бывает скучным занятием; по сути, здесь говорится, что любой символ ASCII от 33 до 126, за исключением 34 (самого символа кавычки) и 92 (символа обратной косой черты), может быть закавыченной строкой в локальной части адреса электронной почты.
То есть, строго говоря, ben"@“lolwat@gitpush--force.com должен быть валидным адресом, но я не смог найти клиента, позволяющего указать его, а писать скрипт для отправки по SMTP мне было слишком лениво.
«Точки в имени пользователя/локальной части опциональны»
Когда я был Очень Крутым Подростком™, я зарегистрировал почту X.Darth.Monkey.X@gmail.com, потому что именно так в те времена поступали Очень Крутые Подростки™, имеющие приглашение в бету Gmail. Со временем мне стало лениво и я перестал использовать точки при вводе своего адреса. xdarthmonkeyx@gmail.com работал точно так же. И он выглядел чуть менее кринжово, когда вышла из моды эстетика стиля phpbb ранних 2000-х.
Постепенно люди стали считать, что это обычное, ожидаемое поведение. Однако RFC 5321 и другие почтовые RFC оставляют кучу свободы реализации локальной части на сервере. В том числе это касается и точек. Оказалось, что разрешение опускать точки ни в коем случае не общепринятое!
«Есть не так много уникальных имён доменов электронной почты»
Есть ли у вас доступ к базе данных в продакшене с адресами электронной почты пользователей? Если да, то высока вероятность того, что ваш работодатель обязан заблокировать вам доступ к базе данных. Но пока он этого не сделал, попробуйте следующий запрос:
SELECT COUNT(DISTINCT SPLIT_PART(email, '@', 2)) FROM users;
Какой результат вы ожидаете получить своим очень подозрительным запросом, который оставит след в логах Postgres и заставит администратора базы данных позадавать вам кучу вопросов? Очевидно, не пару доменов. Наверняка там будут gmail, outlook, icloud, proton, yahoo и так далее. Может быть, несколько чудиков наподобие этого ben@gitpush–force.com.
Ну, значит, пару десятков поставщиков услуг электронной почты, максимум сотню? Нет. Если у вас большая база пользователей, то вы, вероятно, получите ТЫСЯЧИ уникальных имён хостов.
Если откровенно, многие из них — это просто ребрендинг крупных поставщиков наподобие Gmail или Outlook. Например, я учился в Балтиморском университете и преподавал в Общественном колледже округа Балтимор. Поэтому у меня есть несколько адресов .edu! Но даже эти два очень престижных и хорошо финансируемых учебных образования не имеют собственной почтовой инфраструктуры.
> dig mx ubalt.edu +noall +answer ubalt.edu. 350 IN MX 10 ubalt-edu.mail.protection.outlook.com. > dig mx ccbc.edu +noall +answer ccbc.edu. 3398 IN MX 20 nospam.ccbc.edu. ccbc.edu. 3398 IN MX 0 ccbc-edu.mail.protection.outlook.com.
На самом деле, они оба пользуются услугами outlook.com.
Наверно, это будет просто любопытным фактом: аналогично тому, что мы узнали о точках в Gmail, локальная часть адреса может реализовываться по-разному в зависимости сервера или провайдера. Я был свидетелем того, как команды пытались встроить в свою логику допущения о провайдере. Но это неравный бой: количество комбинаций почтовых серверов, провайдеров, локалей, конфигураций и версий бесконечно. А ваше время — нет.
«Адрес почты не может заканчиваться точкой»
Конкретно это больше особенность DNS, нежели почты. В DNS . обозначает корневую зону. Если вы работали с DNS или маршрутизацией трафика, то можете уже знать об этом. А если даже не знаете, то, скорее всего, уже сталкивались с этим, не осознавая. В dig корневая зона «точка» по умолчанию включена в вывод! Посмотрите сами: это видно, когда я прошу у dig запись A для gitpush--force.com; он показывает в ответах gitpush--force.com. (с точкой в конце).
dig gitpush--force.com +noall +answe gitpush--force.com. 273 IN A 104.21.60.65 gitpush--force.com. 273 IN A 172.67.192.184
Обычно отправка письма с точкой в конце адреса работает без проблем. Однако стоит отметить, что ben@gitpush–force.com и ben@gitpush–force.com. будут одним и тем же почтовым ящиком. Также стоит отметить, что это ещё один пример нарушения RFC5322, которое просто работает; Section 3.2.3 запрещает точки в конце.
«Почтовые адреса (не)чувствительны к регистру»
Имя хоста адрес электронной почты никогда не чувствительно к регистру. Однако для локальной части адреса всё немного запутанней. Прежде, чем я приступлю к подробностям, приведу ответ Mattie B на StackOverflow, в котором применён принцип Постела:
По-прежнему неверно писать ПО, предполагающее, что локальные части почтовых адресов нечувствительны к регистру, но учитывая то, что существует много неправильно ведущего себя ПО, нельзя уверенно требовать чувствительности к регистру, если вы находитесь на принимающей стороне.
Давайте вернёмся к техническим определениям: в RFC 5321 есть конкретное утверждение:
Локальная часть = Dot-string / Quoted-string ; МОЖЕТ быть чувствительной к регистру
То есть конкретно в случае адреса почты по определению допускается его чувствительность к регистру, поэтому следует всегда обращаться с ним соответствующе? Кажется, что тут всё ясно и можно просто использовать это допущение. К сожалению, в теории всё гораздо понятнее, чем на практике. Мне удалось найти только один почтовый сервер, в котором чувствительность к регистру локальной части имеет какой-то смысл. В Exim можно сделать локальную часть чувствительной к регистру, но по умолчанию она к нему нечувствительна.
Все почтовые серверы, конфигурации и провайдеры, имеющие вес в реальном мире, обрабатывают локальную часть, как нечувствительную к регистру, и на то у них есть очень веские причины (прошу прощения у всех пользователей Exim, по какой-то причине установивших caseful_local_part): ben@gitpush–force.com и BEN@gitpush–force.com в реальном мире просто должны указывать на один и тот же ящик; нет никаких причин поступать как-то иначе.
Значит, если «все важные» ящики нечувствительны к регистру, то нам об этом не стоит волноваться? Ну, не совсем. Конкретно это допущение становится причиной одной из самых распространённых ошибок, которые мне встречались в обработке адресов электронной почты. Чтобы разобраться, давайте ненадолго перейдём к практическому примеру.
Допустим, вы создаёте веб-сайт, на котором пользователи могут создать аккаунт; достаточно часто используется правило, не позволяющее дублировать адреса почты между аккаунтами; иными словами, у двух аккаунтов не может быть общего почтового адреса. Давайте для примера создадим таблицу с крайне упрощённой схемой.
create table users ( id serial primary key, email varchar(254) unique not null );
Но хотя ben@gitpush–force.com и BEN@gitpush–force.com представляют одного пользователя, этот уникальный не мешает мне случайно создать два аккаунта!
-- Это не вызовет никаких ошибок уникальных индексов insert into users (email) values ('ben@gitpush--force.com'); insert into users (email) values ('BEN@gitpush--force.com');
Как этому препятствовать? Можно просто сказать «принудительно поднимаем адрес в верхний регистр» или даже поднять регистр самого индекса,
create unique index on users(upper(email));
Такой подход крайне распространён. Настолько, что, на мой взгляд, примерно >50% систем, обрабатывающих адреса ящиков, используют ту или иную его версию. К сожалению, это ловушка. Откровенно говоря, он работает в 99% случаев, но не защищён от дурака, а учитывая насколько важна надёжная коммуникация и насколько тесно мы сегодня связываем адреса электронной почты с идентификацией, это может поставить нас в шаткое положение.
Позвольте мне познакомить вас с чудесным миром выравнивания регистра Unicode. Если не усложнять, то принудительное приведение адреса почты к общему регистру может быть в некоторых языках разрушительной и необратимой операцией. Если бы меня звали Ben Weiß и у меня был бы адрес benweiß@gitpush–force.com, то если бы вы попытались преобразовать его в верхний, возникли бы проблемы, потому что не входящий в ASCII символ ß выравнивается не просто в другую букву, а в ДВЕ буквы, два символа ASCII: SS.
Однако принудительный перевод в нижний регистр тоже не решает проблемы. При опускании в нижний регистр турецкая İ выравнивается в обычный символ ASCII i. И должен вам сказать, что в мире МНОГО людей с именем İbrahim.
Что ещё забавнее, различные реализации toLower()/toUpper() выполняют выравнивание по-разному, поэтому вы будете получать разные баги в зависимости от языка программирования, версии, базы данных, системной локали и так далее (всё это будет очень весело и совершенно несложно отлаживать!).
Как же поступить правильно? У меня для вас плохие новости: идеального решения нет. Впрочем, существует общее «достаточно хорошее» решение: использовать citext в Postgres или COLLATE utf8mb4_general_ci в MySQL.
Почему же оно неидеально? Вернёмся к турецкому: допустим, есть два человека по имени İbrahim и по какой-то безумной причине их провайдер разрешил им зарегистрировать İbrahim@turknetmail.com и ibrahim@turknetmail.com. Строго говоря, это разрешается соответствующими RFC, однако маловероятно в реальном мире. Но оба они не пройдут проверку на уникальность и в Postgres, и в MySQL, потому что İ создаёт коллизию с i. К счастью, маловероятно, что это когда-то произойдёт, но раздражает то, что технически это возможно.
Допущения о субадресации с плюсом
Как можно понять из статьи, я человек, любящий изучать мелкие особенности электронной почты, поэтому при регистрации в новом сервисе почти всегда использую субадреса с меткой плюса. Для логина на веб-сайте какой-нибудь компании я использую почту ben+thatcompany@{myemailhost}.email. Я убеждаю себя, что делаю это ради конфиденциальности, чтобы в случае взлома базы данных почты этой компании или продажи моего адреса маркетологам я бы понял, где произошла утечка.
Но истинная причина в том, что мне просто нравится раздражаться, когда это ломает UX из-за программных ошибок или рассогласованностей. Когда я несколько лет назад создавал аккаунт United Airlines, при вводе «ben+united@{myemailhost}.email» я увидел сбой regex «Введите валидный почтовый адрес». Следует сказать, что символ + абсолютно валиден и допустим в локальной части, а сам этот адрес валиден и работает. В нём нет ничего странного или нового: впервые я начал делать так в 2011 году с появлением Gmail!
Давайте найдём информацию в RFC5321 Section 4.1.2, RFC5322 Section 3.4.1 и RFC5322 Section 3.2.3:
В RFC5321 определяется следующий синтаксис команд…
Локальная часть = Dot-string / Quoted-string ; МОЖЕТ быть чувствительной к регистру Dot-string = Atom *("." Atom) Atom = 1*atext
По сути, это означает «локальная часть может быть Dot-string или Quoted-string». Мы уже рассмотрели некоторые особенности Quoted-string (закавыченных строк), поэтому давайте перейдём к Dot-string. Она определяется в виде Atom *("." Atom). Это означает, что она допускает любое количество «Atom», разделённых одинарными точками. А Atom определяется, как 1*atext, то есть Atom может быть одним или более «atext».
Но что такое «atext»? RFC 5321 унаследовал это определение от RFC5322, поэтому давайте перейдём к нему, где и найдём ещё одно похожее определение локальной части…
Локальная часть = dot-atom / quoted-string / obs-local-part
А вот определение «atext», где в явном виде разрешается символ +!
atext = ALPHA / DIGIT / ; Печатные символы "!" / "#" / ; US-ASCII, исключая "$" / "%" / ; специальные. Используются для атомов. "&" / "'" / "*" / "+" / "-" / "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~"
United стала первой в длинном пополняющемся списке компаний, официально запретивших мой валидный и работающий адрес электронной почты. Последней пока в него попала Motorola, когда позволила мне сделать заказ с адресом ben+motorola@{myemailhost}.email, но на странице состояния заказа произошёл сбой с сообщением «Недопустимый адрес электронной почты»… при нажатии на ссылку, которую Motorola успешно доставила на мой адрес! (Кстати, мне нравится мой новенький блестящий Razr Fold).
Такая субадресация у моего поставщика услуг почты, а также в Gmail доставляет ben+whatever@gitpush–force.com в те же входящие, что и ben@gitpush–force.com. Но люди, знающие о субадресации, почему-то на удивление часто предполагают, что она универсальна и что можно сделать вид, что ben+whateer и ben всегда будут одними и теми же входящими. Некоторые хитрые рекламщики и маркетологи даже пытаются объединять адрес, исходя из этого допущения. Но если говорить в целом, по большей мере это просто особенность реализации Gmail.
В заключение…
Я уже выдохся, поэтому подведу краткий итог: мы исследовали некоторые пограничные случаи и неожиданное поведение электронной почты, которые, если честно, не нужно запоминать всем. Хотя каждое из этих «заблуждений» может показаться очевидным, бессмысленным или тривиально устранимым, всё их множество в целом создаёт сложную паутину ловушек. Из-за интернациональных символов, чувствительности к регистру, точек, специфичных для серверов фич и ограничений, неожиданных символов, странностей DNS и других тонкостей становится сложно делать допущения обо всём, что связано с электронной почтой.
И это возвращает нас к главному: не нужно слишком уж заморачиваться. Для уникального индекса используйте citext в Postgres или COLLATE utf8mb4_general_ci в MySQL. Для верификации отправляйте письмо с кодом/ссылкой подтверждения.
Ничего более сложного и не требуется.