Laravelのemailバリデーション

Laravelデフォルトのバリデーションルール「email」を使用したWEBサービスを作成したところ、バリデーション不足を指摘された。

環境
Laravel　5.8.38


#問題点

ユーザー登録画面にて、`test@example.comあいうえお`といった、@以降にひらがなを含むメールアドレスを登録できてしまう。


```php:RegisterController.php
protected function validator(array $data)
{
    return Validator::make($data, [
        'name' => ['required', 'string', 'max:100'],
        'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
        'password' => ['required', new AlphaNumHalf, 'min:8', 'confirmed'],
    ]);
}
```


![スクリーンショット 2020-11-11 5.43.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680199/3527dc0f-6296-f839-f964-59609961ab0a.png)


実際に登録されたメールアドレス
`test@example.xn--com-m63bkmoq`

まじか・・・


###結論

~~`'email'`~~
　↓
`'email:strict,dns,spoof'`
とりあえずこれ。dnsによりドメインが存在するアドレスのみバリデーションを通るので、意味不明な日本語を入れたらまず通らないようにはできると思う。
が、本当にこれが正しいのか分からないので、色々調べてみる。


#何が起きているのか
Laravelの公式ドキュメントによれば、

>email
フィールドがメールアドレスとして正しいことをバリデートします。内部でこのバリデーションルールはメールアドレスの検証にegulias/email-validatorパッケージを使用しています。デフォルトではRFCValidationバリデータが適用されますが、他のバリデーションスタイルも適用可能です。
適用可能なバリデーションスタイルは、次の通りです。
・rfc: RFCValidation
・strict: NoRFCWarningsValidation
・dns: DNSCheckValidation
・spoof: SpoofCheckValidation
・filter: FilterEmailValidation

emailバリデーションにも種類があるらしい。今更知った。デフォルトではRFCValidationが適用されるとのこと。
それぞれの詳細は下記サイトが参考になった。
[【Laravel 5.8.33】メールアドレスの新バリデーション・実例全５件！](https://blog.capilano-fw.com/?p=4475)

では、他のバリデーションスタイルで`test@example.comあいうえお`を登録しようとするとどうなるか。結果はこちら。

| バリデーションスタイル | 結果 |
|:-:|:-:|
| rfc  | `test@example.xn--com-m63bkmoq`  |
| strict  | `test@example.xn--com-m63bkmoq`  |
| dns  | 登録できない  |
| spoof  | `test@example.xn--com-m63bkmoq`  |
| filter  | `test@example.xn--com-m63bkmoq`  |

それぞれのバリデーションの詳細をみるとそりゃそうだよな、という感じ。


###xn--com-m63bkmoqとはなんなのか

調べてみると、Punycodeという符号化方式で、`comあいうえお`の部分がアルファベット、数字、ハイフンのみの文字列`xn--com-m63bkmoq`に変換されているようだ。


>ドメイン名として Punycode を使用する際は、ピリオド（.）で区切られたドメイン名の階層レベルごとにプレフィックスとして「xn--」を使用し、エンコードされた文字列を続ける。大文字と小文字は区別されない。

>| 可読なドメイン名 | Punycodeでのドメイン名 |
|:-:|:-:|
| ドメイン名例.jp  | xn--eckwd4c7cu47r2wf.jp  |
| ウィキペディア.ドメイン名例.jp  | xn--cckbak0byl6e.xn--eckwd4c7cu47r2wf.jp  |
| 可愛いね.そうでもないよ  | xn--n8j5d625jn9k.xn--n8jd2ewbp7lub  |
	
>引用：[Punycode](https://ja.wikipedia.org/wiki/Punycode)	

可愛いね.そうでもないよは笑う。

また、試しに[日本語JPドメイン名のPunycode変換・逆変換](https://punycode.jp/)にて`comあいうえお`と入力してみると、やはり`xn--com-m63bkmoq`に変換された。

![スクリーンショット 2020-11-11 13.02.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680199/add130b9-3733-5e96-b52c-df4182b4f416.png)


###カスタムバリデーションで正規表現を使ってみる

正規表現を使ったカスタムバリデーションを試そうともしたが、Punycodeでアルファベット、数字、ハイフンのみの文字列に変換されてしまうのならば、RFCValidation等と同じ結果になってしまうのではないだろうか？

念のため試してみる。
emailの正規表現は難しいらしく、どれを使えば良いのか・・・という感じだが、こちらの記事を参考にさせていただいた。
[メールアドレスを表す現実的な正規表現](https://qiita.com/sakuro/items/1eaa307609ceaaf51123)

自作のバリデーションルール

```php:MyEmailRegex.php
public function passes($attribute, $value)
{
    return preg_match("/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/", $value);
}
```

```php:RegisterController.php
use App\Rules\AlphaNumHalf;

protected function validator(array $data)
{
    return Validator::make($data, [
        'name' => ['required', 'string', 'max:100'],
        'email' => ['required', 'string', new MyEmailRegex, 'max:255', 'unique:users'],
        'password' => ['required', new AlphaNumHalf, 'min:8', 'confirmed'],
    ]);
}

```

この正規表現の意味は、「何かしらの文字＠英数字か"-".英数字か"-"」となる。
全角文字がPunycodeでアルファベット、数字、ハイフンのみの文字列に変換されてからバリデーションを通っていそうだが、そうであればやっぱり正規表現で引っ掛けるのは難しいのではないか。

`test@example.comあいうえお`を登録してみると、やっぱりバリデーションに引っかからず`test@example.xn--com-m63bkmoq`と登録された。

このようなバリデーションは無難であると先の記事には書いてあるが、それなら僕の受けた指摘は一体なんなのだと釈然としない。


###strict,dns,spoofを併用する
Laravelが用意してくれているバリデーション

- strict: NoRFCWarningsValidation
- dns: DNSCheckValidation
- spoof: SpoofCheckValidation

は、併用が可能。

```php:RegisterController.php
protected function validator(array $data)
{
    return Validator::make($data, [
        'name' => ['required', 'string', 'max:100'],
        'email' => ['required', 'string', 'email:strict,dns,spoof', 'max:255', 'unique:users'],
        'password' => ['required', new AlphaNumHalf, 'min:8', 'confirmed'],
    ]);
}

```

なぜ併用するかというと、
`strict`だけだと先に述べたように`test@example.comあいうえお`が通ってしまう。
`dns`だけだと`.test..@gmail.com`等が通ってしまう。
※RFCでは@の前に.をつけること等が禁止されている。

`strict`でRFCに違反するアドレスがはじかれ、
`dns`でドメインが有効でないアドレスがはじかれる。
ついでに`spoof`でなりすましメールもはじかれる。

ドメイン部分に日本語を入力し、Punycodeで変換されたものが実際に有効なドメインでなければ(そんなドメインがあるのか分からないが)、日本語を入力してもしっかりバリデーションしてくれるようになった。




#結論
Laravelのemailのバリデーションについて、デフォルトのまま`email(email:rfc)`では不十分な場合がある。その場合は、`email:dns`等、他のバリデーションも併用すると良さそう。

ただ、バリデーションの種類云々と同じくらい大事なのが、**「どの程度のバリデーションでサービスの依頼主は満足するか？」を、質問なり打ち合わせでしっかり確認しておくこと**だと感じた。（僕は何も確認せずに納品してしまったので）

記事の内容等に間違いがありましたら、申し訳ございません。


#おまけ
```php:vendor/laravel/framework/src/Illuminate/Validation/Concerns/ValidatesAttributes.php
public function validateEmail($attribute, $value, $parameters)
{
    if (! is_string($value) && ! (is_object($value) && method_exists($value, '__toString'))) {
        return false;
    }

    $validations = collect($parameters)
        ->unique()
        ->map(function ($validation) {
            if ($validation === 'rfc') {
                return new RFCValidation();
            } elseif ($validation === 'strict') {
                return new NoRFCWarningsValidation();
            } elseif ($validation === 'dns') {
                return new DNSCheckValidation();
            } elseif ($validation === 'spoof') {
                return new SpoofCheckValidation();
            } elseif ($validation === 'filter') {
                return new FilterEmailValidation();
            }
        })
        ->values()
        ->all() ?: [new RFCValidation()];

    return (new EmailValidator)->isValid($value, new MultipleValidationWithAnd($validations));
}
```

```php:vendor/laravel/framework/src/Illuminate/Validation/Concerns/FilterEmailValidation.php
namespace Illuminate\Validation\Concerns;

use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\Warning\Warning;
use Egulias\EmailValidator\Exception\InvalidEmail;
use Egulias\EmailValidator\Validation\EmailValidation;

class FilterEmailValidation implements EmailValidation
{
    /**
     * Returns true if the given email is valid.
     *
     * @param  string  $email
     * @param  EmailLexer
     * @return bool
     */
    public function isValid($email, EmailLexer $emailLexer)
    {
        return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
    }

    /**
     * Returns the validation error.
     *
     * @return InvalidEmail|null
     */
    public function getError()
    {
        //
    }

    /**
     * Returns the validation warnings.
     *
     * @return Warning[]
     */
    public function getWarnings()
    {
        return [];
    }
}
```





#参考サイト

・emailのバリデーションについて
[PHPで各種バリデーション](https://qiita.com/mpyw/items/346f1789ad0e1b969ebc#2015811-%E8%BF%BD%E8%A8%98-1)
[Gmailが日本語など非アルファベット文字を含むメールアドレスとの送受信に対応](https://gigazine.net/news/20140806-gmail-admit-non-alphabet/)
[RFC 5322 & 5321に沿ったメールアドレス（local-part）に使える文字まとめ](https://qiita.com/yoshitake_1201/items/40268332cd23f67c504c)
[メールアドレスを表す現実的な正規表現](https://qiita.com/sakuro/items/1eaa307609ceaaf51123)
[バリデーションで使用する正規表現の読み方](https://qiita.com/190131start/items/20d68c4f16edb8c3731c)

・ドメインについて
[ドメイン名のしくみ](https://www.nic.ad.jp/ja/dom/system.html)

・Laravelバリデーションについて
[Laravel 5.8 バリデーション](https://readouble.com/laravel/5.8/ja/validation.html?header=%25E3%2583%2595%25E3%2582%25A9%25E3%2583%25BC%25E3%2583%25A0%25E3%2583%25AA%25E3%2582%25AF%25E3%2582%25A8%25E3%2582%25B9%25E3%2583%2588%25E4%25BD%259C%25E6%2588%2590#rule-email)
[【Laravel 5.8.33】メールアドレスの新バリデーション・実例全５件！](https://blog.capilano-fw.com/?p=4475)
[バリデーションエラー/POST送信時のLaravelの挙動を追う](https://qiita.com/tatsukoni/items/256fa38dcf7ad96550ca)

・Punycodeについて
[Punycode](https://ja.wikipedia.org/wiki/Punycode)
[日本語JPドメイン名のPunycode変換・逆変換](https://punycode.jp/)