Почта и доставляемость

Валидация email-адреса: что доказывает шаблон, а что нет

Четыре разные проверки продаются под одним словом, сложность у них растёт по порядку, и только последняя устанавливает то, что вам на самом деле хотелось узнать. Дальше: что способен доказать шаблон, что добавляет один DNS-запрос, почему спрашивать у почтового сервера стоит меньше, чем кажется, и какая из четырёх — единственный настоящий ответ.

  • Средний
  • 21 мин на чтение
Синий конверт парит рядом с серым планшетом с большой синей галочкой, а перед ними — наклонённая серая карточка поменьше с крестиком

Четыре вопроса на одно слово

«Это валидный email-адрес?» — не один вопрос, а четыре, и труднее они по порядку, а ответ на последний — единственный, который кому-либо действительно был нужен, — вот почему столько кода валидации так дотошно разбирает первый и молчит про остальные.

Вот они, в том порядке, в котором их встречает форма регистрации.

Похоже ли это на адрес по форме?
Проверка синтаксиса. Она выполняется в браузере, ничего не стоит и ловит случайно набранную запятую и пропущенный @. Это единственный слой, который честно может что-то отклонить сам по себе, — и даже он должен отклонять намного меньше, чем отклоняет большинство шаблонов.
Может ли этот домен вообще принимать почту?
Проверка DNS. Один запрос говорит, готово ли хоть что-нибудь, хоть где-нибудь принимать почту для части после @. Она ловит доменное имя с пропущенной буквой и домен, у которого в прошлом году истёк срок, — а про сам почтовый ящик не скажет вообще ничего.
Существует ли этот почтовый ящик?
Проверка по SMTP — и именно ей это руководство дольше всего будет объяснять, почему ей не стоит верить. Спросить можно. Ответом часто окажется вежливое «да» от сервера, принимающего любое имя, или намеренный временный отказ, или согласие, за которым через пару минут следует баунс.
Это адрес именно того человека?
С технической стороны на это не ответит ничто. Адрес может быть безупречным, доставляемым — и чужим: набранным с одной опечаткой в цифре, или введённым нарочно, чтобы пройти мимо вас. Закрывает вопрос только письмо, которое дошло и было использовано.
ШаблонРаботает в браузереФорма строкиНичего о миреничего не стоитDNS-запросMX, потом AДомен может приниматьНичего о почтовом ящикеодин запросПроверка сервераСпросил и повесил трубкуПочти ничегоCatch-all говорит «да» всеммедленно и рискованноПодтверждённое письмоСсылка, использованная разТам кто-то естьЕдинственный стоящий ответодно настоящее письмоКаждая следующая строка стоит дороже предыдущей — и оправдывает эту цену.
Четыре слоя, и что устанавливает каждый. Только нижняя строка даёт факт о человеке; три строки над ней лишь сужают поле, чтобы письмо стоило отправлять.

Первые три дёшевы и мало что доказывают. Четвёртая — единственная, что заслуживает своего названия, и единственная, что стоит письма. Всё, что ниже, — о том, как разумно потратить три дешёвые, чтобы не потратить впустую дорогую.

Слой первый: шаблон и четыре вещи, которых он не может знать

Официального регулярного выражения для email-адреса не существует, и существовать не может. RFC 5322 определяет грамматику, а не шаблон, и эта грамматика допускает комментарии в скобках, свёрнутые пробелы и строки в кавычках, содержащие почти любой символ, — ничего из этого не должна принимать ни одна настоящая форма регистрации, а верный стандарту шаблон обязан принимать всё это.

А то, что существует, — намеренно более узкое определение, которое уже реализовано в браузерах: то самое, что спецификация HTML задаёт для поля <input type="email">. Это осознанный компромисс, а не переписанный стандарт, именно ему ваша форма уже подчиняется ещё до того, как выполнится хоть одна строка вашего кода, и он достаточно короткий, чтобы прочитать его за один раз:

regex
/^[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])?)*$/

Используйте его, или тот, что уже встроен в вашу платформу. Все четыре варианта ниже — один и тот же слой, и выбор между ними значит меньше, чем то, что вы сделаете после:

Где вы находитесьПроверка, которая уже естьЧто она делает такого, чего вы могли не ожидать
В браузере, вообще без кода<input type="email" required>Применяет шаблон выше ещё до того, как поле вообще увидит ваш JavaScript, и показывает сообщение на родном языке посетителя.
В PHP, без единой установкиfilter_var($a, FILTER_VALIDATE_EMAIL)Строже, чем HTML-вариант: требует точку в домене, поэтому отказывает адресу, который работал бы только внутри одной машины.
В Python, с одним маленьким пакетомemail_validator.validate_email(a)Делает синтаксическую проверку и, если разрешить, ещё и DNS-проверку домена из второго слоя, — два дешёвых слоя за одним вызовом.
В Java или Kotlin, через Bean Validation@EmailНамеренно очень нестрогая. Это аннотация, задуманная для того, чтобы ловить явно неверное, и она без вопросов примет домен, которого никогда не существовало.

Какой бы вы ни использовали, важно то, о чём он умалчивает. Шаблон, вернувший true, сообщил вам одну вещь — и четыре не-вещи.

Не то, что домен существует
Адрес на домене, который никто никогда не регистрировал, пройдёт любой шаблон из этого раздела. Ничего не проверялось, ни к кому не обращались — ни одно регулярное выражение ни разу не делало сетевой запрос.
Не то, что ящик существует
Даже на настоящем домене шаблону нечего сказать о части до @. Эта часть принадлежит только принимающему серверу, и это как раз то, чего DNS не скажет никогда.
Не то, что письмо доставляемо
У домена может быть безупречная запись и почтовый сервер, выключенный уже месяц. Синтаксис — свойство строки; доставляемость — свойство мира в момент, когда вы нажимаете «отправить».
Не то, что адрес принадлежит тому, кто его вводит
Самый частый плохой адрес в любой базе — настоящий, доставляемый, и принадлежит тому, кто никогда не регистрировался, — потому что в одной букве ошиблись, или потому что форму заполнили правдоподобной ложью.

Слой второй: один запрос и что он устанавливает

Часть после @ — это домен, и у домена либо есть куда положить почту, либо нет. Один DNS-запрос отвечает на это за несколько миллисекунд, и это самая ценная проверка во всём этом руководстве, — потому что ошибка, которую она ловит, чуть неверно набранное доменное имя, с большим отрывом самая частая настоящая проблема из всех.

shell
$ dig +short MX example.com

Ответ означает, что хост назван. Отсутствие ответа не значит отсутствие почты: домен без MX-записи, но с записью A, всё равно её принимает, потому что отправители переходят на неё как на запасной вариант. Есть четыре исхода, и отказом из них являются только два.

Что возвращает запросМожет принимать?Что должна делать форма
Одна или несколько MX-записейДаПримите его. Это подавляющее большинство адресов, и до отправки больше ничего делать не стоит.
Нет MX, но есть запись A или AAAAДа, через запасной вариантПримите его. Это необычно, но полностью законно, и отправители туда доставят. Отказ превращает рабочий адрес в потерянного клиента.
Единственная запись, всё значение которой — 0 .Нет, и намеренноОткажите и объясните почему. Это нулевая MX-запись: владелец домена опубликовал — единственным доступным для этого способом, — что здесь почту никто не принимает.
Домен вообще не резолвитсяНетОткажите и предложите ближайший похожий вариант. NXDOMAIN на имени, которое человек набрал тридцать секунд назад, почти всегда означает одну неверную букву.

Когда эта проверка ломается, дело никогда не в самом запросе. Дело в том, куда его поместили — на критический путь, на каждое нажатие клавиши, с отказом, который блокирует отправку формы. Есть шесть правил, которые делают её полезной:

  1. Запускайте её, когда поле заполнено, а не пока его печатают. Один запрос при потере фокуса или один при отправке формы. Запрос на каждое нажатие клавиши — это запрос на каждое нажатие клавиши.
  2. Сначала приведите домен к нижнему регистру. DNS всё равно, а вашему кэшу — нет: два написания одного домена — это один запрос, а не два.
  3. Запрашивайте MX, а запасным вариантом берите A. Два запроса, из которых условный только второй. Библиотека, проверяющая только MX, будет отклонять рабочие домены.
  4. Кэшируйте ответ на несколько минут. Горстка доменов покрывает большинство регистраций где угодно, так что большинство запросов превращаются в отсутствие запроса вовсе.
  5. При сбое — принимайте. Если резолвер не ответил вовремя, примите адрес. Плохая минута у вашего DNS-провайдера не должна превращаться в форму, отказывающую всем подряд.
  6. Предлагайте исправление, но никогда не применяйте его сами. Когда домен отличается на одну букву от распространённого, предложите исправление как что-то, на что можно нажать. Молча переписать то, что кто-то ввёл, — вот так ссылка для подтверждения уходит незнакомцу.

У запроса есть одна цена, о которой стоит знать: это первый момент, когда ваша форма рассказывает внешнему миру о том, что набрал посетитель. Там, где это важно, альтернатива — полностью пропустить этот слой и оставить всю проверку одному письму.

Слой третий: спросить у сервера, и почему это не ответ

Есть способ спросить у почтового сервера, примет ли он конкретный адрес, ничего при этом не отправляя. Откройте сеанс, назовите отправителя, назовите получателя, прочитайте ответ на эту одну команду и повесьте трубку до самого письма:

smtp
220 mx1.example.com ESMTP ready
EHLO checker.example.net
250 mx1.example.com
MAIL FROM:<probe@example.net>
250 2.1.0 Ok
RCPT TO:<someone@example.com>
250 2.1.5 Ok
QUIT
221 2.0.0 Bye

250 после RCPT TO — вот что в конечном счёте продаёт любой сервис верификации адресов в мире. Стоит точно понимать, насколько мало это стоит.

Catch-all домен отвечает «да» на всё
Домен, настроенный принимать любое имя, — а именно так работает этот сервис, и точно так же работает множество корпоративных доменов, — отвечает 250 на адрес, которым никто никогда не пользовался. Ответ правдив, и это не информация.
Осторожный сервер намеренно отвечает 450
Грейлистинг отказывает первой попытке незнакомого отправителя и просит зайти чуть позже. Настоящий отправитель зайдёт; проверяющий скрипт — никогда. Этот временный отказ — намеренное отсутствие ответа, и прочитать его как «такого ящика не существует» — именно та ошибка, ради которой он и задуман.
Некоторые серверы сперва принимают, а отказывают потом
Крупные провайдеры регулярно принимают письмо прямо во время сеанса, а решение выносят позже, — и тогда отказ превращается в баунс, приходящий через пару минут после того, как ваша проверка вернула чистый результат.
Некоторые серверы отказывают всем, кого не узнают
Сервер, решивший, что ваш адрес ему незнаком, может отказать получателю по причинам, вообще не связанным с получателем. Вы измерили собственную репутацию, а не их почтовый ящик.
И это тратит ту самую репутацию, которую вы пытались защитить
Соединение, которое называет получателей и никогда ничего не отправляет, выглядит в точности как сбор адресов из каталога, потому что это он и есть. Делать это массово со своего собственного адреса — самый быстрый путь в чёрный список, а у отправителя из чёрного списка перестают доходить даже настоящие письма.

Есть один узкий случай, где такая проверка по-настоящему полезна: один адрес, проверенный вручную, на домене, которым вы управляете или который вам разрешили трогать. А как шаг внутри формы регистрации она медленная, часто ошибается и порой вредит именно тому, для защиты чего её туда добавили.

Ролевые адреса, одноразовые домены и список, который вы вот-вот купите

Между проверкой домена и настоящим письмом сидит целое семейство проверок, которые вообще не о законности. Они о том, хотите ли вы этот адрес, — а это бизнес-решение в техническом костюме, и уже хотя бы поэтому его стоит отделять от трёх слоёв вокруг.

Ролевые адреса
Имена вроде info@, support@ и admin@. Они настоящие, и обычно это общий ящик, а не человек, — что делает их плохим домом для всего, за чем стоит пароль. Отметить стоит. Отказывать — почти никогда.
Одноразовые домены
Адреса вроде тех, что раздаёт этот сайт, — на доменах, которые существуют для того, чтобы их выбросили. Сверяться с публичным списком таких доменов — вполне разумно, если честно признавать, что любой такой список неполон и слегка устарел уже в день, когда вы его скачали.
Адреса бесплатных провайдеров
Некоторые корпоративные формы отказывают всему, что не является доменом компании. Это политика, иногда правильная, и она заслуживает того, чтобы быть записанной как политика — фраза, которую посетитель может прочитать, — а не спрятанной внутри того, что называют валидацией.
Опечатки в популярных доменах
Имя известного провайдера с одной неверной буквой. Этот пункт не похож на три предыдущих: это не политика, он ловит настоящую ошибку, и человек всегда этому рад. Это единственный пункт здесь, который стоит реализовать.

Первые три объединяет свойство, из-за которого их трудно оценивать: вы можете измерить, что они пропускают, и не можете измерить, чего они стоят.

У нас здесь очевидный интерес, так что вот честная версия. Если за вашей формой стоит бесплатный пробный период с чем-то дорогим на другом конце, отказ одноразовым доменам сэкономит вам деньги, и стоит его сделать. Если это рассылка, загрузка или платный аккаунт, вы в основном облагаете налогом осторожных — а осторожные это как раз те, кто читает то, что вы присылаете.

Слой четвёртый: письмо, которое и есть проверка

Всё, что было выше, лишь сужает поле. Ничто из этого не устанавливает единственный важный факт — что этот адрес доходит до человека перед вами, — и есть ровно одна вещь, которая это делает: отправить туда что-то и увидеть, что это использовали.

Вот цикл, который уже есть почти у каждой регистрации, и который половина из них воспринимает как формальность:

  1. Примите адрес по одной нестрогой проверке. Шаблон в браузере — и больше ничего между посетителем и кнопкой.
  2. Создайте аккаунт неподтверждённым. Не задержку формы, не экран «в ожидании»: человек уже внутри, и единственное, чего он пока не может, — это горстка вещей, для которых адрес действительно нужен.
  3. Отправьте одно письмо с одноразовой ссылкой или кодом. Одно, со сроком действия, привязанное именно к этому аккаунту и этому адресу — и ни к какому другому.
  4. Пусть доказательством будет ссылка. Клик, или введённый обратно код, — это и есть вся верификация целиком. Ничто другое в этом руководстве не даёт ответа хотя бы отдалённо такой же надёжности.
  5. Дайте им путь назад. Заметную кнопку «отправить ещё раз» и способ изменить адрес, не теряя аккаунт, — потому что самая частая причина, по которой на ссылку так и не кликнули, это опечатка, которую человек теперь наконец может увидеть.
  6. Сжигайте те, что никто не подтвердил. Тихая зачистка через фиксированный срок не даёт опечаткам и одноразовым адресам копиться в список, которому никто не доверяет.

Из этого возникает практическая проблема, и именно ради неё существует этот сайт. Этот цикл теперь самый важный путь в продукте, а протестировать его — значит получать настоящие письма на адрес, который вы контролируете, — снова и снова, внутри пайплайна, без человека, открывающего почтовый ящик.

Адрес на одном из публичных доменов не требует ни регистрации, ни ключа, и всё, что на него приходит, через секунду можно прочитать по HTTP:

shell
$ curl -sG https://grabmail.io/api/v1/mailbox \
  --data-urlencode "address=signup-42@grabmail.io"

Отсюда весь цикл верификации можно провести от начала до конца тестовым набором, либо сюда же можно направить домен, которым вы владеете, чтобы каждый прогон получал адрес, которого раньше не существовало. Письма хранятся 5 дней, а затем удаляются, — это правильный срок жизни для тестовой фикстуры и неправильный для почтового ящика.

Что на самом деле класть в форму

Сжато, в том порядке, в котором выполняется код:

  1. Обрежьте пробелы — и только их. Пробелы по краям — артефакт вставки из буфера и никогда не задумывались нарочно. Больше ничего в этой строке не ваше, чтобы менять.
  2. Сверьте с одним нестрогим шаблоном. Браузерным или платформенным. Отклоняйте пропущенный @ и набранную запятую — и больше ничего.
  3. Ограничьте длину 254 символами. Одно число, в столбце и в поле, — и целый класс ввода, о котором можно больше не думать.
  4. Проверяйте домен вне критического пути, отказывая в пользу приёма. Сначала MX, потом A, с кэшем, — и никогда не повод заблокировать отправку, которая иначе бы сработала.
  5. Предлагайте исправление — не вносите его сами. Домен, отличающийся на одну букву от распространённого, — это вопрос, который стоит задать, а не находка, по которой стоит действовать.
  6. Отправьте письмо. Вот это и есть валидация. Всё, что было выше, — лишь сортировка.
  7. Скажите словами, что не так. Именно эта часть решает, дойдёт ли кто-нибудь до конца:
Что произошлоЧто обычно говорят формыЧто сказать вместо этого
В строке нет @«Введите правильный email-адрес»«В email-адресе должен быть @ — вы имели в виду name@example.com?»
Домен не резолвится«Введите правильный email-адрес»«Мы не можем найти этот домен. Написание верное?»
Домен отличается на одну букву от распространённогоВообще ничего; форма отправляется«Вы имели в виду…?» — с исправленным адресом в виде кнопки, на которую можно нажать
Он в чёрном списке, который вы решили использовать«Введите правильный email-адрес»«Для этого нам нужен адрес, который вы всё ещё сможете читать через месяц.»
Письмо ушло, но так и не было подтвержденоВообще ничего; аккаунт просто существует«Мы отправили ссылку на этот адрес. Не пришла? Отправьте ещё раз или измените адрес.»

Обратите внимание, сколько строк говорят одно и то же неверное. «Введите правильный email-адрес» стоит по умолчанию везде, потому что это верно для каждого случая и бесполезно ни в одном из них: в трёх строках из пяти адрес был правильным, а у человека, который его ввёл, нет способа понять, в какой он строке.

Коротко

  1. Обрежьте пробелы по краям строки. Больше ничего в ней не меняйте.
  2. Сверьте с одним нестрогим шаблоном — браузерным или платформенным — и на этом остановитесь. Не пишите свой.
  3. Отклоняйте всё длиннее 254 символов и храните столбец именно такого размера.
  4. Запрашивайте MX домена, запасным вариантом берите A, кэшируйте результат, делайте это вне критического пути и принимайте адрес, если запрос не удался.
  5. Отказывайте домену, который не резолвится, и домену, публикующему нулевую MX-запись. Всё остальное, что даёт DNS, принимайте.
  6. Предлагайте исправление написания, когда домен почти совпадает с известным. Никогда не применяйте его сами.
  7. Отдельно и письменно решите, блокируете ли вы одноразовые или ролевые адреса, — и почему.
  8. Отправляйте письмо с одноразовой ссылкой, считайте клик верификацией, и не усложняйте повторную отправку и исправление адреса.
  9. Тестируйте этот цикл на настоящем почтовом ящике, включая пути, где он ломается, и удаляйте неподтверждённые аккаунты по расписанию.

Девять строк, и только последние две хоть что-то устанавливают. Остальные семь существуют, чтобы письмо в конце этого пути стоило отправлять.

Вопросы

Существует ли официальное регулярное выражение для email-адресов?

Нет, и не может существовать. RFC 5322 задаёт грамматику, а не шаблон, а шаблон, верный ей, принимал бы пробелы в кавычках и комментарии в скобках, которых не выдаёт ни один провайдер. Используйте тот, что определяет спецификация HTML для поля email, или ту проверку, что уже встроена в вашу платформу, — а сэкономленные усилия потратьте на письмо-подтверждение.

Можно ли проверить, существует ли email-адрес, ничего не отправляя?

Спросить можно, узнать — нельзя. Catch-all домен принимает любое имя, грейлистинг отвечает намеренным временным отказом, крупные провайдеры принимают письмо во время сеанса, а потом присылают баунс, а сервер, который вас не узнаёт, может отказать по причинам, связанным с вами, а не с получателем. Массовая проверка вдобавок заносит ваш адрес отправителя в чёрные списки.

name+tag@example.com — это законный адрес?

Да. Знак плюса — обычный разрешённый символ в локальной части, и у большинства крупных провайдеров он ещё и направляет письмо в ящик перед ним, — именно это делает плюс-адресацию полезной. Форма, которая его отвергает, отказывает законным адресам и заодно сообщает кое-что о себе самой.

Email-адреса чувствительны к регистру?

Домен — никогда. Часть перед @, согласно стандарту, оставлена на усмотрение принимающего сервера — а на практике каждый крупный провайдер регистр игнорирует. Храните копию в нижнем регистре, чтобы находить дубликаты, а отправляйте на строку в точности такой, какой её набрали.

Какой длины может быть email-адрес?

64 символа до @, 255 для домена и 254 для всего адреса целиком, как его несёт сервер. Из этих трёх чисел используйте последнее: поместите его в столбец базы данных и в поле — и целый класс ввода перестанет быть вашей проблемой.

Стоит ли блокировать одноразовые email-адреса?

Если за бесплатным пробным периодом стоит что-то дорогое — да, и смиритесь с тем, что любой используемый список неполон. Для рассылки, загрузки или платного аккаунта те, кого вы отталкиваете, — в основном осторожные люди, и вы никогда не увидите их в логе. Полный разбор компромисса стоит прочитать прежде, чем ставить пакет, который примет это решение за вас.

Как протестировать цикл верификации при регистрации без настоящего почтового ящика?

Отправьте на адрес публичного одноразового домена и прочитайте письмо обратно через API — без регистрации, без ключа, и со свежим адресом на каждый прогон. Для набора тестов, которому нужны сотни адресов, направьте домен, которым вы владеете, на catch-all-ящик и придумывайте адрес под каждый тест. Полный разбор — в руководстве «Тестируем цикл верификации email от начала до конца».

Читать дальше

Попробуйте, пока свежо

Адрес — это один клик, без аккаунта и карты. Всё из этого руководства сразу заработает на нём.

С возвращением

Ваши ящики и ваши домены в одном месте.