(2017.7.1) 新規作成。そんなに新規でもないが。
(2017.7.2) IPv4/IPv6アドレスに対応。
"メールアドレス 正規表現"ぐらいで検索すると、上位に、微妙だったり不適切な内容のページが並ぶ。つらい。
あまつさえ、現実に, いまだに '+' が通らないWeb上の商業サービスもあって、妥当な処理が行なわれていない。
とても古い話題のはずが、まともな解説だけが時代の彼方に去って、訳の分からない記事が跋扈しているのか?
とはいえ、メイルアドレスは仕様が複雑すぎて、意外と難しい。
WHATWG HTML Living Standard には, input[type="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])?)*$/
'@' の後ろ, ドメインについて IPアドレスでの表現を禁止するのは、名前が引けないメイルサーバは公開サーバとしてありえないので、考え方として分かる。HTML (フォーム) の利用場面だし。
一方, '@' の前, local-part のほうは, quoted-string を除外したい気持ちは分からないでもないが、代替的な書き方がないので、やり過ぎではないか。
RFC 5322 Internet Message Format (2008年10月) が権威あることになっている。前の RFC 2822 (April 2001) から考えると, すでに15年以上経過している。
が、この page: メールアドレスの正規表現 の解説のように, 厳密に RFC の addr-spec に従うと, コメント, FWS (空白・タブ・改行), 制御文字 (後退など) すら許容することになる。
RFC 5322 は, メイルのヘッダというばらつきが非常に大きいテキストデータのなかから、メイルアドレスらしいものを抽出するためのルールで、何でもかんでもマッチするようになっている。
どこまで RFC 5322 から離れるか、どこで一線を引くかの見極めが大事。世間で RFC に従っていると主張しているものも、本当に準拠しているものは存在しない。(何も考えない勝手定義のは論外だが.)
ここまでは異論は少ないと思う。次は判断のしどころ。適用業務による。HTML LS は両方とも除外を選んでいる。
後のセクションでは、両方とも含める形で正規表現を作ってみる。
RFC 5322 での '@' の後ろ, domain は次のようになっている;
dot-atom の許容範囲が広すぎて、およそホスト名としてありえないものまでマッチしてしまう。
これまでの話をひっくり返すようだが、RFC 5322 を出発点とするのが, そもそもどうかしている。
RFC 5321 Simple Mail Transfer Protocol のほうを参照しなければならない。
RFC 5321 では, 次のようになっている;
メイルアドレスと言われてイメージするような、まっとうな定義になっている。(用途が違うから。)
ホスト名は, 数字で始まってよい (RFC 1123 section 2.1; October 1989). しかし、すべてが数字だけだと IPアドレスと区別できない。上記に加えて, Javaで参照されている RFC 2609 の制限を使う。
See Is it valid for a hostname to start with a digit? - Server Fault
という訳で, 上記の方針に沿って正規表現を作ってみる。Perlメモのものを参照しつつ, Ruby に書き直した。RFC 5321 を出発点として, ホスト名の部分を変更した。
できた正規表現。先頭の ^
と末尾の $
は付けていない。
(?-mix:((?:(?:[A-Za-z0-9!#$%&'*+\-\/=?^_`{|}~]{1,64}(?:\.[A-Za-z0-9!#$%&'*+\-\/=?^_`{|}~]{1,62}){0,31})|"(?:[\x21\x23-\x5B\x5D-\x7E]|\\[\x20-~]){0,62}"))@((?:(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.){0,127}[A-Za-z](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)|(?:\[(?:(?:[0-9]{1,3}(?:\.[0-9]{1,3}){3})|(?:(?:[0-9A-Fa-f]{1,4})?(?:(?:::|:)(?:[0-9A-Fa-f]{1,4})){1,7}(?:(?:::|:)(?:[0-9]{1,3}(?:\.[0-9]{1,3}){3}))?))\])))
(2020.3.28) IPv4射影アドレス (IPv4-Mapped IPv6 address) は, ::ffff
固定なので, ipv6_address のなかで通常のIPv6アドレスと並列に書いたほうがよい。See RFC 5952.
妥当なメイルアドレス、不正なのを、いろいろ集めてみた。
結果。SHOULDが 'Y' のものが通すべきもの。部分一致したものは、どこで一致したかを表示している。
弾くべきだが通ってしまったものがいくつかある。しかし、まぁ、上出来だと思う。
テストパタン | SHOULD | 結果 | コメント |
---|---|---|---|
email@domain.com | Y | MATCHED | |
firstname.lastname@domain.com | Y | MATCHED | |
email@subdomain.domain.com | Y | MATCHED | |
firstname+lastname@domain.com | Y | MATCHED | '+'を弾くアホがある |
1234567890@domain.com | Y | MATCHED | |
_______@domain.com | Y | MATCHED | |
firstname-lastname@domain.com | Y | MATCHED | |
-email@domain | Y | MATCHED | 先頭が'-'も可. |
abcABC123.defDEF456@ghiGHI789.comCOM012 | Y | MATCHED | |
abc.#%&'/=~`*+?{}^$-|@ghi.com | Y | MATCHED | |
Abc@example.com | Y | MATCHED | |
Abc.123@example.com | Y | MATCHED | |
user+mailbox/department=shipping@example.com | Y | MATCHED | |
customer/department=shipping@example.com | Y | MATCHED | |
!#$%&'*+-/=?^_`.{|}~@example.com | Y | MATCHED | |
!def!xyz%abc@example.com | Y | MATCHED | |
Abc.@example.com | N | , $1 = , $2 = | |
Abc..123@example.com | N | 123@example.com, $1 = 123, $2 = example.com | |
.dot_kara_hazimaru@example.com | N | dot_kara_hazimaru@example.com, $1 = dot_kara_hazimaru, $2 = example.com | |
I.like.you.@example.com | N | , $1 = , $2 = | |
I..love...you@example.com | N | you@example.com, $1 = you, $2 = example.com | |
abc.def@#%&'/=~`*+?{}^$-|.com | N | , $1 = , $2 = | |
ab<c.def@ghi.com | N | c.def@ghi.com, $1 = c.def, $2 = ghi.com | |
abc.de<f@ghi.com | N | f@ghi.com, $1 = f, $2 = ghi.com | |
.email@domain.com | N | email@domain.com, $1 = email, $2 = domain.com | |
email.@domain.com | N | , $1 = , $2 = | |
email..email@domain.com | N | email@domain.com, $1 = email, $2 = domain.com | |
あいうえお@domain.com | N | , $1 = , $2 = | |
"e#$$%&@>mail"@domain.com | Y | MATCHED | '@'や'>'を含められる |
"em,ail"@localhost | Y | MATCHED | カンマも可 |
"Abc@def"@example.com | Y | MATCHED | |
"Fred\ Bloggs"@example.com | Y | MATCHED | '\'に続ければ空白すら可 |
"Joe.\\Blow"@example.com | Y | MATCHED | '\'自身 |
"Joe.\"Blow"@example.com | Y | MATCHED | '"'も含められる. |
".dot_kara_hazimaru"@example.com | Y | MATCHED | |
"I.likeyou."@example.com | Y | MATCHED | |
"I..love...you"@example.com | Y | MATCHED | |
email@domain-one.com | Y | MATCHED | |
email@domain.name | N | MATCHED | (制限) トップレベルの実在性までは確認できない |
email@domain.co.jp | Y | MATCHED | |
email@localhost | Y | MATCHED | ドメインにピリオドがなくても可 |
a@a | Y | MATCHED | |
a@0.a | Y | MATCHED | |
a@a-a.com | Y | MATCHED | |
a@0-a.com | Y | MATCHED | |
a@a-0.com | Y | MATCHED | |
a@a-a.a-a | Y | MATCHED | |
email@-domain.com | N | , $1 = , $2 = | RFC 5322 だと通ってしまう. RFC 5321 で制約. |
email@-.-.-.- | N | , $1 = , $2 = | RFC 5322 だと通ってしまう. RFC 5321 で制約. |
email@123.123.123.123 | N | , $1 = , $2 = | RFC 2609 で制約. |
abc.def@ghi.#%&'/=~`*+?{}^$-| | N | abc.def@ghi, $1 = abc.def, $2 = ghi | |
abc.def@gh<i.com | N | abc.def@gh, $1 = abc.def, $2 = gh | |
abc.def@ghi.co<m | N | abc.def@ghi.co, $1 = abc.def, $2 = ghi.co | |
a@0 | N | , $1 = , $2 = | |
a@0.0 | N | , $1 = , $2 = | |
a@a.0 | N | a@a, $1 = a, $2 = a | |
a@.a | N | , $1 = , $2 = | |
a@a-.a | N | a@a, $1 = a, $2 = a | |
a@-a.a | N | , $1 = , $2 = | |
email@domain..com | N | email@domain, $1 = email, $2 = domain | |
email@[123.123.123.123] | Y | MATCHED | |
a@[255.255.255.255] | Y | MATCHED | |
a@[001.002.003.004] | Y | MATCHED | |
a@[2001:0db8:bd05:01d2:288a:1fc0:0001:10ee] | Y | MATCHED | |
a@[2001:db8:20:3:1000:100:20:3] | Y | MATCHED | |
a@[2001:db8::1234:0:0:9abc] | Y | MATCHED | |
a@[2001:db8::9abc] | Y | MATCHED | |
a@[::1] | Y | MATCHED | localhost |
ea@[::ffff:255.255.255.255] | Y | MATCHED | IPv4射影アドレス |
a@[::ffff:0:255.255.255.255] | N | MATCHED | IPv4射影アドレスとして正しくない. |
email@[111.222.333.44444] | N | , $1 = , $2 = | 不正なIPv4アドレス |
a@[0.0.0.0] | N | MATCHED | (制限) 正しくないが, 構文では弾けない |
a@[255.255.255.256] | N | MATCHED | (制限) 正しくないが, 構文では弾けない |
a@[example.com] | N | , $1 = , $2 = | |
a@[example.com:hoge] | N | , $1 = , $2 = | |
a@[fuga:xxxxxxx] | N | , $1 = , $2 = | (制限) 正しくないが、構文にはマッチ |
a@[2001:0db8:bd05:01d2:288a::1fc0:0001:10ee] | N | MATCHED | 1ヶ多い |
a@[2001:0db8:bd05:01d2:288a:1fc0:0001:10ee:11fe] | N | , $1 = , $2 = | 1ヶ多い |
a@[::] | N | , $1 = , $2 = | |
a@[0::0] | N | MATCHED | |
a@[1::] | N | , $1 = , $2 = | |
a@[1:2:3:4:5:6:7::] | N | , $1 = , $2 = | 数が合わない |
a@[::255.255.255.255] | N | , $1 = , $2 = | IPv4互換アドレス。廃止 |
a@[2001:db8:3:4::192.0.2.33] | N | MATCHED | IPv4射影アドレスとして不正。構文では弾けない |
a@[64:ff9b::192.0.2.33] | N | MATCHED | IPv4射影アドレスとして不正。構文では弾けない |
a@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.com | Y | MATCHED | 63バイトOK |
a@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678901.com | N | a@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890, $1 = a, $2 = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890 | 64バイトNG |
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/@example.com | Y | MATCHED | 64バイトOK |
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/a@example.com | N | bcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/a@example.com, $1 = bcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/a, $2 = example.com | 65バイトNG |
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567+/"@example.com | Y | MATCHED | 制限は local-part に掛かっている |
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567+/a"@example.com | N | , $1 = , $2 = | 65バイトNG |
abcdefhghijklmnopqrstuvwxyzABC@aaaaaaaa01.aaaaaaaa02.aaaaaaaa03.aaaaaaaa04.aaaaaaaa05.aaaaaaaa06.aaaaaaaa07.aaaaaaaa08.aaaaaaaa09.aaaaaaaa10.aaaaaaaa11.aaaaaaaa12.aaaaaaaa13.aaaaaaaa14.aaaaaaaa15.aaaaaaaa16.aaaaaaaa17.aaaaaaaa18.aaaaaaaa19.aaaaaaaa20.aaaaaaaa21.aaaaaaaa22.aaaaaaaa23 | Y | MATCHED | domain = 252オクテット |
abcdefhghijklmnopqrstuvwxyzABCD@aaaaaaaa01.aaaaaaaa02.aaaaaaaa03.aaaaaaaa04.aaaaaaaa05.aaaaaaaa06.aaaaaaaa07.aaaaaaaa08.aaaaaaaa09.aaaaaaaa10.aaaaaaaa11.aaaaaaaa12.aaaaaaaa13.aaaaaaaa14.aaaaaaaa15.aaaaaaaa16.aaaaaaaa17.aaaaaaaa18.aaaaaaaa19.aaaaaaaa20.aaaaaaaa21.aaaaaaaa22.aaaaaaaa23.zz | Y | MATCHED | domain = 255 |
abcdefhghijklmnopqrstuvwxyzABCD@aaaaaaaa01.aaaaaaaa02.aaaaaaaa03.aaaaaaaa04.aaaaaaaa05.aaaaaaaa06.aaaaaaaa07.aaaaaaaa08.aaaaaaaa09.aaaaaaaa10.aaaaaaaa11.aaaaaaaa12.aaaaaaaa13.aaaaaaaa14.aaaaaaaa15.aaaaaaaa16.aaaaaaaa17.aaaaaaaa18.aaaaaaaa19.aaaaaaaa20.aaaaaaaa21.aaaaaaaa22.aaaaaaaa23.zzz | N | MATCHED | domain = 256 |
"abcdefhghijklmnopqrstuvwxyzABC"@aaaaaaaa01.aaaaaaaa02.aaaaaaaa03.aaaaaaaa04.aaaaaaaa05.aaaaaaaa06.aaaaaaaa07.aaaaaaaa08.aaaaaaaa09.aaaaaaaa10.aaaaaaaa11.aaaaaaaa12.aaaaaaaa13.aaaaaaaa14.aaaaaaaa15.aaaaaaaa16.aaaaaaaa17.aaaaaaaa18.aaaaaaaa19.aaaaaaaa20.com | Y | MATCHED | |
"abcdefhghijklmnopqrstuvwxyzABCD"@aaaaaaaa01.aaaaaaaa02.aaaaaaaa03.aaaaaaaa04.aaaaaaaa05.aaaaaaaa06.aaaaaaaa07.aaaaaaaa08.aaaaaaaa09.aaaaaaaa10.aaaaaaaa11.aaaaaaaa12.aaaaaaaa13.aaaaaaaa14.aaaaaaaa15.aaaaaaaa16.aaaaaaaa17.aaaaaaaa18.aaaaaaaa19.aaaaaaaa20.com | Y | MATCHED | |
plainaddress | N | , $1 = , $2 = | |
@domain.com | N | , $1 = , $2 = | |
Joe Smith <email@domain.com> | N | email@domain.com, $1 = email, $2 = domain.com | |
email.domain.com | N | , $1 = , $2 = | |
email@domain@domain.com | N | email@domain, $1 = email, $2 = domain | |
email@domain.com (Joe Smith) | N | email@domain.com, $1 = email, $2 = domain.com | |
email@ example | N | , $1 = , $2 = | 空白を含む. CFWSはエラーにすべき. |
"foo"."bar"@example.com | N | "bar"@example.com, $1 = "bar", $2 = example.com | 旧形式 |
まともそうなページだけ。
ことから考えると、最悪 /[@]/
でいいじゃないか、という話もありえます。
本当に、これ。もっとも, メイルアドレスで quoted-string を認めようとすると, もう少ししっかりした正規表現が必要。
a@a
とかは通していい。テストケースを流しているのに、その結果を見ていない。
localhost
のようにドメイン (ホスト名) にドットが1回もないのも正当だが、わざわざマッチしないようにしてしまっている。