HTML 仕様のメールアドレス正規表現に最小限の修正を加える

## TL;DR

以下の正規表現は、HTML 仕様のメールアドレス正規表現の末尾に、 TLD 必須のルールを加えたものです。

```
/^[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])?)*\.[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$/
```

`<input type="email">` の `pattern` 属性に指定する場合は、以下のように短く書けます。

```html
<input type="email" pattern=".+\.[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]">
```

以下、詳細です。

## HTML 仕様のメールアドレス正規表現について

HTML 仕様には、 `<input type="email">` のバリデーションルールと同等の正規表現が含まれています。

 * [HTML Living Standard - valid email address](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address)
 * [`<input type="email">` - MDN ](https://developer.mozilla.org/ja/docs/Web/HTML/Element/input/email)

以下がその正規表現です。

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

ブラウザが標準で提供する機能なので、ぜひこのルールを活用したいところです。

## 課題

しかしながら。。

この `type="email"` は、**TLD が無いメールアドレスを許可します**。`a@a` は、valid なアドレスだと判定されます。

上の正規表現の遷移図は以下のようになっています。

> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2596/17994a5e-a7aa-f6f8-1f12-5fdc907da32e.png)
([Regexper](https://regexper.com/#%5E%5Ba-zA-Z0-9.!%23%24%25%26'*%2B%5C%2F%3D%3F%5E_%60%7B%7C%7D~-%5D%2B%40%5Ba-zA-Z0-9%5D%28%3F%3A%5Ba-zA-Z0-9-%5D%7B0%2C61%7D%5Ba-zA-Z0-9%5D%29%3F%28%3F%3A%5C.%5Ba-zA-Z0-9%5D%28%3F%3A%5Ba-zA-Z0-9-%5D%7B0%2C61%7D%5Ba-zA-Z0-9%5D%29%3F%29*%24))

このような緩いルールになっている理由は、`pattern` 属性を使って更に条件を狭めることができるからです。デフォルトでメールアドレスとして最も緩い条件を適用し、次に `pattern` で必要に応じた制限を加えます。

> If you need the entered e-mail address to be restricted further than just "any string that looks like an e-mail address," you can use the pattern attribute to specify a regular expression the value must match for it to be valid.
([MDN](https://developer.mozilla.org/ja/docs/Web/HTML/Element/input/email#Pattern_validation))

デフォルトの正規表現と `pattern` の正規表現は、AND 条件です。両方が検証されます。

> However, the browser runs both the standard e-mail address filter and our custom pattern against the specified text.
([MDN](https://developer.mozilla.org/ja/docs/Web/HTML/Element/input/email#Pattern_validation))

2つのルールは AND で検証されるので、`pattern` 属性で、例えば `@` が含まれるかどうかをチェックする必要はありません。

`pattern` 属性を指定しない `type="email"` は、現実には使い物になりません。
組織内で TLD 無しのアドレスだけを受け付けるというならともかく、TLD があっても無くてもどちらでも構わないようなメールアドレスを `input` で入力させるのは、相当に特殊な場合だけでしょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2596/bca9179b-068e-d203-c26f-41f59d69437d.png)
JPNIC の「[ドットなしドメイン名 (Dotless Domain Names) について](https://www.nic.ad.jp/ja/in-policy/policy-report-201309.pdf)」の記事も参考になります。

## 正規表現に TLD の制限を加える

そこで、HTML 仕様を極力変えずに TLD の制限を加えてみます。

```
(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*
```

がドメインの2番め以降のラベルで、0回以上の繰り返しなので、この後に、以下を付け加えます。

```
\.[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]
```

修正を加えた正規表現は以下の通り。

```
/^[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])?)*\.[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$/
```

遷移図は以下のようになります。

> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2596/dab01853-d31f-7760-cd44-5895fe40982f.png)
([Regexper](https://regexper.com/#%5E%5Ba-zA-Z0-9.!%23%24%25%26'*%2B%5C%2F%3D%3F%5E_%60%7B%7C%7D~-%5D%2B%40%5Ba-zA-Z0-9%5D%28%3F%3A%5Ba-zA-Z0-9-%5D%7B0%2C61%7D%5Ba-zA-Z0-9%5D%29%3F%28%3F%3A%5C.%5Ba-zA-Z0-9%5D%28%3F%3A%5Ba-zA-Z0-9-%5D%7B0%2C61%7D%5Ba-zA-Z0-9%5D%29%3F%29*%5C.%5Ba-zA-Z0-9%5D%5Ba-zA-Z0-9-%5D%7B0%2C61%7D%5Ba-zA-Z0-9%5D%24))

## 1文字の TLD は禁止にする

1文字の TLD を禁止する仕様は[存在しない](https://stackoverflow.com/a/21872376)ようですが、[今の TLD に1文字のものはない](https://data.iana.org/TLD/tlds-alpha-by-domain.txt)ので、`(?: )?` を外して「TLD 2 文字以上」の制限を加えています。上限 63 文字は、他のラベルと同様です。

1文字の TLD を許容するなら、最後の `*` を `+` に変えるだけで済みますが、それでは最低限の制約にならないと考えました。コピペミスや、打ち間違いで `.jp` の最後の1文字が抜けるような、よくあるパターンに対応できません。

参考: [Deep Research -「ICANN は 1 文字の TLD を許容していますか？」](https://chatgpt.com/share/67bfcf84-ec38-8003-b4b8-f6f13243a217)

## pattern に指定する場合は追加のエスケープが必要

`pattern=""` に指定する場合、文字クラスの中のハイフンはエスケープが必要です。
([コメント](https://qiita.com/koseki/items/ec2658cea7a900c74365#comment-692423a8a6f1f6bd437e)で教えていただきました)

```
<input type="email" pattern=".+\.[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]">
                                                      ↑このエスケープが必要！
```

2023年に `pattern` 属性の正規表現に付くフラグが `u` から `v` に[変更されました](https://zenn.dev/pixiv/articles/54e5d29c54e7f5#html-standard-%E3%81%A8%E3%81%AE%E9%96%A2%E4%BF%82)。

`v` フラグを使う場合、文字クラスに含まれる `\[]{}()/-|` のエスケープが必要だということです。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Regular_expressions/Character_class
> `]` と `\` に加え、`(`、`)`、`[`、`{`、`}`、`/`、`-`、`|` の文字は、文字クラス内で文字通りに表わす場合、エスケープする必要があります。

## TLD 部分だけを pattern に指定できるか

通常の正規表現であれば、部分一致で、末尾の TLD の正規表現だけを指定できそうなものです。

```
pattern="\.[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]$"
```

しかし、これはうまくいきません。HTML の `pattern` 属性は、`^(?: )$` でラップされるからです。


> 5 . Let `anchoredPattern` be the string `"^(?:"`, followed by `pattern`, followed by `")$"`.
([HTML Living Standard](https://html.spec.whatwg.org/multipage/input.html#the-pattern-attribute))

なので、`pattern` にはメールアドレス全体にマッチする正規表現を指定する必要があります。

`.+` を先頭につけて、

```
pattern=".+\.[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]"
```

と書けます。デフォルトのルールと `pattern` は両方が検証されるので、前半部分は `.+` と省略してしまっても問題ありません。

## ローカルパートに記号を許すか

デフォルトの正規表現では、以下の記号をローカルパート (`@` より前) に許可しています。

```
.!#$%&'*+/=?^_`{|}~-
```

この内、絶対に必要なのは以下でしょう。

```
.+_-
```

残りの記号は、場合によっては削ってもよいかもしれません。

```
!#$%&'*/=?^`{|}~
```

特にユーザ登録で、他のサービスと連携する可能性がある場合、最初から絞っておかないと後で困る可能性がありそうです。こちらで許可した記号が、連携先では許可されていないかもしれません。

## メジャーなサービスのバリデーションはどうなっているか

いくつかのサイトで、ユーザ登録画面の挙動を見てみました。(実際には登録せず、クライアントサイドのバリデーションの挙動だけを見ています)

 * Facebook は、1 文字の TLD は許可。ローカルパートの記号も許可しているようでした
 * Twitter は、1文字の TLD は禁止。記号は基本の文字に加えて `&` を許可していました
 * Apple ID は、1文字の TLD は禁止。基本の記号だけを許可しているようでした


## デモ

[以下](https://codepen.io/kkoseki/pen/rNLWaEb)で `type="email"` の `input` 要素に `pattern` を指定したときの挙動を試せます。

<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="html,result" data-user="kkoseki" data-slug-hash="rNLWaEb" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="HTML email validation">
  <span>See the Pen <a href="https://codepen.io/kkoseki/pen/rNLWaEb">
  HTML email validation</a> by koseki (<a href="https://codepen.io/kkoseki">@kkoseki</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

また、様々なメールアドレスに正規表現を適用するテストページを[こちら](https://codepen.io/kkoseki/full/gOMLjRZ)に作成しました。

* https://codepen.io/kkoseki/full/gOMLjRZ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2596/e45a7c6b-dd96-108b-224d-3a22961492a2.png)

Microsoft の以下のページに、いい感じのテストデータがあります。

 * [Email Address test cases](https://docs.microsoft.com/ja-jp/archive/blogs/testing123/email-address-test-cases)