「お客さんにメールが届かないのでどうかして?」というCSからの質問。見てみるとexample@yahoo.comであるところ、example@yahpo.comになっていたりします。たった1文字違いですが、会員登録してもこのためにメールが届かないばかりか、次回からは多分ログインもできません。もちろんお客さんの間違いですが、登録時にこれが通ってしまうというのは問題です。これをどうかしたいです。

緩いメールアドレスのバリデーション

Laravelのデフォルトのメールアドレスのバリデーションemailは、結構緩いです。どれだけ緩いが実例見てみましょう。以下は、tinkerを使って、test@gmailという不正なメールアドレスチェックします。

>>> $v = validator(['email' => 'test@gmail'], ['email' => 'required|email'])
=> Illuminate\Validation\Validator {#3534
     +customMessages: [],
     +fallbackMessages: [],
     +customAttributes: [],
     +customValues: [],
     +excludeUnvalidatedArrayKeys: null,
     +extensions: [],
     +replacers: [],
   }
>>> $v->passes()
=> true

あっさり、通ってしまいます。

また、example.comとかのドメイン名は実在しないのに、これも通ります。

>>> $v = validator(['email' => 'test@example.com'], ['email' => 'required|email'])
...
>>> $v->passes()
=> true

email:dnsの登場

メールアドレスの@マークの後ろの部分、例えば、test@example.comなら、example.comの部分、はドメイン名と言ってインターネットでは非常に重要なデータなのですが、このドメインが実在するかどうか、そしてメールを受け取るかどうかがわかれば、少なくともメールアドレスのタイポは登録時に警告できますね。

それを利用したのが、email:dnsのバリデーションルールです。早速、tinkerで試してみましょう。

>>> $v = validator(['email' => 'test@gmail'], ['email' => 'required|email:dns'])
...
>>> $v->passes()
=> false

>>> $v = validator(['email' => 'test@example.com'], ['email' => 'required|email:dns'])
...
>>> $v->passes()
=> false

>>> $v = validator(['email' => 'test@yahpo.com'], ['email' => 'required|email:dns'])
...
>>> $v->passes()
=> false

素晴らしいです、みな通りません!

email:dnsの使用において注意が必要なのは、それを利用するには、以下の国際化関数、intlのライブラリが必要です。
https://www.php.net/manual/ja/book.intl.php

dns_get_record()

ちょっとemail:dnsの仕組みを探ってみましょう。Laravelのマニュアルによると、egulias/email-validatorのパッケージを使用しているそうな。そこから関心のコードの一部を掲載します。

...
class DNSCheckValidation implements EmailValidation
{
    /**
     * @var int
     */
    protected const DNS_RECORD_TYPES_TO_CHECK = DNS_MX + DNS_A + DNS_AAAA;

    /**
     * Reserved Top Level DNS Names (https://tools.ietf.org/html/rfc2606#section-2),
     * mDNS and private DNS Namespaces (https://tools.ietf.org/html/rfc6762#appendix-G)
     */
    const RESERVED_DNS_TOP_LEVEL_NAMES = [ //これらのドメイン名はみなエラーとなります
        // Reserved Top Level DNS Names
        'test',
        'example',
        'invalid',
        'localhost',

        // mDNS
        'local',

        // Private DNS Namespaces
        'intranet',
        'internal',
        'private',
        'corp',
        'home',
        'lan',
    ];
   ...

 private function validateDnsRecords($host) : bool
    {
        // A workaround to fix https://bugs.php.net/bug.php?id=73149
        /** @psalm-suppress InvalidArgument */
        set_error_handler(
            static function (int $errorLevel, string $errorMessage): ?bool {
                throw new \RuntimeException("Unable to get DNS record for the host: $errorMessage");
            }
        );

        try {
            // Get all MX, A and AAAA DNS records for host
            $dnsRecords = dns_get_record($host, static::DNS_RECORD_TYPES_TO_CHECK);
        } catch (\RuntimeException $exception) {
            $this->error = new InvalidEmail(new UnableToGetDNSRecord(), '');

            return false;
        } finally {
            restore_error_handler();
        }
...

上の45行目見てください。dns_get_record()なるphp関数があったのですね。
https://www.php.net/manual/ja/function.dns-get-record.php

上の説明によると、こんなしてドメイン情報取得できるようです。

>>> dns_get_record("yahpo.com", DNS_MX);
=> [
     [
       "host" => "yahpo.com",
       "class" => "IN",
       "ttl" => 0,
       "type" => "MX",
       "pri" => 0,
       "target" => "",
     ],
   ]
>>> dns_get_record("yahoo.com", DNS_MX);
=> [
     [
       "host" => "yahoo.com",
       "class" => "IN",
       "ttl" => 0,
       "type" => "MX",
       "pri" => 1,
       "target" => "mta5.am0.yahoodns.net",
     ],
     [
       "host" => "yahoo.com",
       "class" => "IN",
       "ttl" => 0,
       "type" => "MX",
       "pri" => 1,
       "target" => "mta6.am0.yahoodns.net",
     ],
     [
       "host" => "yahoo.com",
       "class" => "IN",
       "ttl" => 0,
       "type" => "MX",
       "pri" => 1,
       "target" => "mta7.am0.yahoodns.net",
     ],
   ]

最初のタイポのドメイン名のtargetは存在しないけれど、正式なドメインは3つtargetを返しています。Laravelのemail:dnsはこれを利用しているのですね。

最後に

メールのバリデーションには、email:dnsの他にもrfcとかstrictとかspoofとかあります。
emailとemail:rfcは同じです。また、email:rfc,dnsとように合わせての使用も可能です。

あと大事なのは、バリデーションでは必ず弾かれてしまうが、test.@docomo.ne.jpのようにドットが不正な場所にあるにもかかわらず実在するメールアドレスの対応です。docomo.ne.jpやezweb.ne.jpではその不正を許す必要あります。まだ使っている人いるんですよ。

By khino