Почему валидации email регуляркой недостаточно. Проверка MX-записей с примерами на PHP и Ruby

    Уж сколько раз твердили Миру… Существует давний и, вероятно, нескончаемый спор о том, какой именно регуляркой правильно и нужно проверять поле email пользователя.

    Да, проверять регуляркой действительно нужно. Но ведь наши продукты работают в Сети. Так почему бы не использовать её настоящую мощь?

    К тому-же нередко бывают ситуации, когда пользователи реально ошибаются при вводе адреса email (в том числе и в домене). Ну или, в поле email вводят любую возможную «Хабракадабру», что легко пролетит через regexp, но никак не может быть почтой, потому что даже домена такого не существует в природе :)

    Кстати, на этом вот нюансе мы буквально только что подзалетели: суть в том что на сайте, поднятом на одной, довольно популярной CMS-ке у нас почему-то прекратили идти email-уводемления.

    Причиной, как выяснилось, стало попадание адреса рассыльщика в спам.

    Причин было несколько:

    1. CMS довольно популярная, а, стало быть, и регистрирующихся ботом-спамеров по неё немало. И что интереснее — в настройках можно (и многие так, к слову, и делают) — отключают проверку email. В этом случае сюда можно (и так большинство ботов и делает) вводить любую белиберду
    2. Тексты писем не были переписаны со стандартных.

    Итого: спамеры массово лезли регистрироваться, кидали скрипту левые email-ы, куда мы пытались отправлять письма. Фильтр же спама видел что с нашего email-а идёт ряд писем, с текстами, что он уже видел много раз с других email-адресов, и при этом немалое их количество валится на несуществующие email-адреса.

    В общем почтовый адрес периодически подпадал под спам.

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

    Собственно суть проверки довольно проста: при регистрации, на стадии валидации данных пользователя мы отщепляем домен от email-а, и смотрим что там есть по MX-ам.

    Сложно? На самом деле нет. Но позволяет существенно снизить нагрузку на почтовые службы. И, кстати, гораздо реже попадать в спам-листы (ведь отсылка большого числа писем на несуществующие почтовые адреса — один из признаков спама).

    На PHP, как ни странно, сделать это довольно просто:

    $email ="11@sdlkfjsdl.co.uk";
    $domain = substr(strrchr($email, "@"), 1);
    $res = getmxrr($domain, $mx_records, $mx_weight);
    if (false == $res || 0 == count($mx_records) || (1 == count($mx_records) && ($mx_records[0] == null  || $mx_records[0] == "0.0.0.0" ) ) ){
    //Проверка не пройдена - нормальные mx-записи не обнаружены
    	echo "No MX for domain: $domain";
    }else{
    //Проверка пройдена, живая MX-запись на домене есть, и почта на нём работает
    	echo "It seems that we have qualify MX-records for domain: $domain";
    }
    

    Поясню по довольно «монструозному» if-у. Дело в том, что в документации к функции getmxrr были комментарии с упоминаниями про не совсем корректное его поведение. И хотя на php7.1 мне их обнаружить не удалось — лишняя проверка — не лишняя :)

    На ruby это делается схожим образом:

    domain = invite.email.split('@').last.mb_chars.downcase.to_s.force_encoding("UTF-8")
    #На случай, если домен русскоязычный. Точнее уже не совсем помню зачем преобразовывал в UTF-8, но видимо нечто вылетало
    
    mail_servers = Resolv::DNS.open.getresources(domain, Resolv::DNS::Resource::IN::MX)
    if mail_servers.empty?
       #Нет MX-серверов. Нечего и пытаться сюда слать письма
       false
    else
       true
    end

    При этом уточню, что подобная проверка поля email может не только довольно серьёзно сказаться на качестве информации в базе данных вашего проекта (и снизить риск попадания рассыльщиков уведомлений в спам), но и повлечь за собой снижение нагрузок. Ведь отправка писем из скрипта — довольно не быстрый на практике процесс.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 27

      +2
      Да, проверять регуляркой действительно нужно.

      Странно, я постоянно читаю, что проверять регуляркой НЕ нужно https://habr.com/post/175375/

        0
        Ну, как-минимум наличие домена у адреса электронной почты проверять то уж точно нужно :)
        Без него как-бы никуда. Например, видел ошибки когда точку путают с запятой.
          0

          А смысл? Если юзер указал некорректный домен, то отправленное письмо не придёт никуда — оно вообще не отправится дальше вашего собственного SMTP, который вас банить не должен. Подозреваю, что проблему вызвала попытка отправлять почту через SMTP gmail или подобного сервиса… ну, тут скорее ССЗБ и проблема решается использованием собственного SMTP, а не валидацией MX.

            +1

            чтобы сказать о проблеме юзеру сразу, естественно.

              +1

              Это незначительное улучшение UX (которое легко может стать ухудшением, если ожидание результатов MX-запроса будет блокировать отправку формы юзером). К реальной валидации email данная фича имеет очень слабое отношение.

              0

              Есть проблема, что Google быстро начнет банить ваш собственный SMTP за рассылку похожих писем. Собственно я сам с таким столкнулся.

                0
                Аналогично. Я вообще подумывал попробовать устанавливать соединение на 25-м порту, либо — пытаться сделать одинаковые письма «разными» :)
                А скорее всего — по хорошему нужно делать и то, и другое…
                  0

                  Google обычно банит не сам, а используя информацию сторонних сервисов вроде spamhaus.org. При корректной настройке своего SMTP (включая reverse DNS, SPF, etc.) и рассылке легальных писем а не реального спама обычно это проблема решаемая.

              0
              Ну, в качестве решения в той статье предлагается просто послать пользователю письмо. Здесь же замечается, что это, вообще-то, и вызвало проблемы (слать письма безо всяких проверок):
              Фильтр же спама видел что с нашего email-а идёт ряд писемю… и при этом немалое их количество валится на несуществующие email-адреса.
              В общем почтовый адрес периодически подпадал под спам.

              +1
              Тема обсасывалась уже не единожды. Единственно верный способ проверить валидность email-а — это отправить на него письмо и не получить отлуп от почтового сервера. Остальные способы — лишь некоторая вероятность того, что email валидный.
                +5
                и не получить отлуп от почтового сервера
                И получить от пользователя желаемую реакцию, например, переход по url-у.
                А отсутствие отлупа не означает отсутствия ящика. Например, корпоративные сервера нередко настраивают так, что они принимают всю входящую почту подряд, потом сортируют по ящикам, а лишнее удаляют или отправляют в дефолтный ящик.
                +5
                В Symfony, кстати, валидацию email по проверке MX и A-записей объявили устаревшим, по причине того, что это не является надежным способом проверки, поскольку зависит от состояния сети и некоторые действительные сервера отказываются отвечать на эти запросы.
                  +4
                  Стоит так же упомянуть что страница мануала для getmxrr четко говорит: Эта функция не должна использоваться для проверки адреса., что как бы намекает.
                    0
                    Мы проверяем не то, что существует конкрентный email, а то, что домен, у которого данный email был прописан — он в принципе имеет mail сервер для обработки писем…
                    +2
                    Отвечают не сами сервера, а DNS-ы. И если вы не отрезолвисились по DNS на сервер MX — стало быть никакого письма вы отправить не сможете точно.
                    0

                    Единственный надёжный способ проверить существование адреса электронной почты это отправка туда письма с кодом подтверждения. Наличие MX у домена (по хорошему надо проверять ещё и его разрешение в IP или, что лучше, наличие слушающего по этому адресу 25 порта) не гарантирует существование / функционирование ящика.
                    Всё остальное компромисс.

                      0
                      domain = invite.email.split('@').last.mb_chars.downcase.to_s.force_encoding("UTF-8")
                      #На случай, если домен русскоязычный. Точнее уже не совсем помню зачем преобразовывал в UTF-8, но видимо нечто вылетало
                      


                      Без mb_chars не будет downcase для русских символов работать. Скорее всего после этого преобразование в обычную рубишную строку обратно.
                        0
                        Это ясно. Я просто не помню зачем мне была нужна кодировка utf-8. Правда так хуже точно не будет :)
                          +1
                          на самом деле наличие force_encoding скорее всего говорит о том что где то что то не так и лучше избавиться от проблем с кодировкой на архитектурном уровне.
                        0
                        А что только домены проверяются? Можно проверять и сам ящик без отправки письма на него?
                          0
                          Неплохая идея. Но тогда вам нужно будет устанавливать полноценное соединение через c почтовым серверов, обменяться приветствиями, потом заявить что «я хочу отправить письмо такому-то», и если получаете ответ «да, это возможно» — обрываете соединение, отправляете письмо стандартными средствами. Но это довольно небыстро по времени :)
                          +2
                          Этот способ также некорректный.

                          Случай первый:
                          RFC 974 определяет порядок маршрутизации почты:

                          There is one other special case. If the response contains an answer
                          which is a CNAME RR, it indicates that REMOTE is actually an alias
                          for some other domain name. The query should be repeated with the
                          canonical domain name.

                          В случае, если домен является алиасом, ваш код посчитает, что это невалидный домен.

                          Случай второй:
                          Раздел 5 RFC5321 определяет корректное поведение отправителя почты:
                          The lookup first attempts to locate an MX record associated with the
                          name. If a CNAME record is found, the resulting name is processed as
                          if it were the initial name. If a non-existent domain error is
                          returned, this situation MUST be reported as an error. If a
                          temporary error is returned, the message MUST be queued and retried
                          later (see Section 4.5.4.1). If an empty list of MXs is returned,
                          the address is treated as if it was associated with an implicit MX
                          RR, with a preference of 0, pointing to that host.
                          If MX records are
                          present, but none of them are usable, or the implicit MX is unusable,
                          this situation MUST be reported as an error.

                          То есть в случае отсутствия MX-записи доставка нормальным образом по стандарту происходит по A-записи домена.

                          Кроме того, почта может быть доставлена по SRV-записям, но предложенный Вами код этого конечно же не учитывает.

                          Вообще говоря, домены электронной почты не обязаны быть доступны постоянно. Стандарт предлагает такую стратегию доставки:
                          The sender MUST delay retrying a particular destination after one
                          attempt has failed. In general, the retry interval SHOULD be at
                          least 30 minutes; however, more sophisticated and variable strategies
                          will be beneficial when the SMTP client can determine the reason for
                          non-delivery.

                          Retries continue until the message is transmitted or the sender gives
                          up; the give-up time generally needs to be at least 4-5 days. It MAY
                          be appropriate to set a shorter maximum number of retries for non-
                          delivery notifications and equivalent error messages than for
                          standard messages. The parameters to the retry algorithm MUST be
                          configurable.


                          То есть чтобы убедиться, что домен мёртвый, его нужно наблюдать хотя бы 4-5 дней.

                          В общем, такой способ проверки по большому счёту — мусор. Сложившийся стандарт в индустрии по этому вопросу — отправить письмо с подтверждением адреса и до подтверждения не включать в списки рассылок.
                            0
                            Ну на самом деле, по крайней мере в современной практике получается так, что если не получилось отправить письмо прямо сейчас — значит почтовый сервис де-факто не работает.
                            Но вы натолкнули меня на мысль: отсутствие MX-записи по факту ещё не означает полный отлуп (хоть в немалом количестве случае это, скорее всего и будет), но является поводом призадуматься.
                            Поэтому да, логичнее не увидев MX-записи, посмотреть в A и в CNAME — если не будет и их — тогда это гарантированный отлуп.
                            Если есть CNAME — берём адрес или домен из него. и Повторяем цикл.
                            Как только по циклу дошли до ip-адреса — смотрим наличие 25-го порта. Если он открыт и представляется — тогда ок, почта есть.
                            Если нет — отлуп.
                            Хотя да, думаю что вы правы — это довольно геморройный уже получается с точки зрения написания способ проверки.
                            Но по крайней мере можно сказать одно: нет mx, cname и a-записей — это однозначный отлуп и почты наверняка нет.
                              0
                              А что это даёт?

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

                              И по итогу всё решит подтвердил ли юзер по ссылке свой адрес или нет.



                              Есть ещё вот какой неприятный момент: как вызывать эту функцию получения значений записей из DNS. Эта функция в PHP не имеет настраиваемых ограничений по таймауту и сколько она проработает в итоге зависит от платформы и конфигурации системного резолвера. Может полминуты запросто, если нэймсервер домена просто молчит. Возникает ситуация, что можно случайно или умышленно создать ситуацию, когда все воркеры PHP будут застопорены ожиданием какого-нибудь кривого или заблокированного РКН домена.

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

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

                            Что касается регулярок — помню был у клиента адрес типа ..xx|zz..@ — и он оказался работающим (попал в базу до того как email стал проверяться на фронтэнде и прошел валидацию через отправку кода).

                            Так что, как уже говорили раньше, единственный способ убедится в реальности адреса — это получить от пользователя подтверждение после отправки ему кода валидации или URL (причём последний должен быть с явным подтверждением после нажатия, или его могут случайно «подтвердить» антивирусы и/или антиспамы).

                            Единственное что можно улучшить — это интегрировать валидацию в процесс регистрации (или смены адреса), т.е. делать её немедленно после подтверждения email, и говорить пользователю если адрес получил отлуп. Но этот путь тернист, ибо ведет к потенциальной DoS, а также может дать сбой если сервер временно недоступен.
                              +1
                              Интересно, что к функции getmxrr() есть примечание в документации:

                              This function should not be used for the purposes of address verification. Only the mailexchangers found in DNS are returned, however, according to » RFC 2821 when no mail exchangers are listed, hostname itself should be used as the only mail exchanger with a priority of 0.
                                0
                                У меня возникает мысль, что для рассылок нужно использовать соответствующие системы. SendGrip, MailChimp или любой другой подобный. Они, конечно, стоят денег, но при этом вы, как пользователь, не несёте риски, связанные с верификацией адреса клиента и попаданием своего пула адресов и доменов в черные списки. Интересно обсудить насколько это верный (идеологически и экономически) подход. Или всё-таки для проектов среднего масштаба придется строить свои велосипеды…

                                Only users with full accounts can post comments. Log in, please.