メールアドレスのバリデーション
正規表現による簡単なメールアドレスバリデータ
いろいろ話題になったメールアドレスのバリデータですが、勉強がてら、一から作ってみようかと思い立ちました。
資料集め
メールアドレスの書式に関しての最新のドキュメントはIETFの管理するRFC5322です。また、ABNFの書式に関してはRFC5234を参照します。どちらもPlain版をダウンロードしておくと便利です。
RFCはABNFで一通りの仕様が表記されていますので、英語が分からなくても、ABNFの仕様だけを読めば一通りの仕様は理解できます。
ABNFの簡単な説明
ABNFの仕様を簡単に説明します。
仕様を考える
なるべく簡単で、広く使えそうなものを考えるということで、フルスペックから以下を制限することにしました。
- domain-literal
([192.168.*.*]のようなもの)
は捨て - CFWS/FWS(コメントとスペースの類)は捨て
- quoted-pair
(\を使ったエスケープ表現)
は捨て - コントロールコード
(\x00-\x1f,\x7f)
は全部捨て
それでは、addr-specから順番に見ていきます。
addr-spec
フルスペックは以下の通りです。
addr-spec = local-part "@" domain local-part = dot-atom / quoted-string / obs-local-part domain = dot-atom / domain-literal / obs-domain
domain-literalを捨てます。
addr-spec = local-part "@" domain local-part = dot-atom / quoted-string / obs-local-part domain = dot-atom / obs-domain
とりあえずこれで、次は定義されていないdot-atom、quoted-string、obs-local-part、obs-domainを見ていきます。
dot-atom
フルスペックは以下の通りです。
dot-atom = [CFWS] dot-atom-text [CFWS] dot-atom-text = 1*atext *("." 1*atext)
CFWSを捨てて整理した結果、以下の通りになります。
dot-atom = 1*atext *("." 1*atext)
obs-local-part
フルスペックは以下の通りです。
obs-local-part = word *("." word) word = atom / quoted-string atom = [CFWS] 1*atext [CFWS]
CFWSを捨てて整理した結果、以下の通りになります。
obs-local-part = (1*atext / quoted-string) *("." (1*atext / quoted-string))
obs-domain
フルスペックは以下の通りです。
obs-domain = atom *("." atom) atom = [CFWS] 1*atext [CFWS]
CFWSを捨てて整理した結果、以下の通りになります。
obs-domain = 1*atext *("." 1*atext)
dot-atomと等しくなりました。
addr-spec 再び
addr-spec = local-part "@" domain local-part = dot-atom / quoted-string / obs-local-part domain = dot-atom / obs-domain
整理していきます。local-partのdot-atomはobs-local-partに含まれる表現であり、quoted-stringはobs-local-partに含まれる表現なので、全てobs-local-partに統一します。
また、domainのobs-domainとdot-atomが同一なので、obs-domainに統一します。
addr-spec = local-part "@" domain local-part = obs-local-part domain = obs-domain
整理すると以下の通りになります。
addr-spec = obs-local-part "@" obs-domain
さっぱりしました。これを起点にして、obs-local-partとobs-domainの定義をしていきます。
obs-local-part/obs-domain 再び
obs-local-part = (1*atext / quoted-string) *("." (1*atext / quoted-string)) obs-domain = 1*atext *("." 1*atext)
atextとquoted-stringが定義されていないので、定義していきます。
atext
フルスペックは以下の通りです。
atext = ALPHA / DIGIT / ; Printable US-ASCII "!" / "#" / ; characters not including "$" / "%" / ; specials. Used for atoms. "&" / "'" / "*" / "+" / "-" / "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~" ALPHA = %x41-5A / %x61-7A ; A-Z / a-z DIGIT = %x30-39 ; 0-9
整理すると以下の通りになります。
atext = %x21 / %x23-27 / %x2a-2b / %x2d / %x2f-39 / %x3d / %x3f / %x41-5a / %x5e-7e
quoted-string
フルスペックは以下の通りです。
quoted-string = [CFWS] DQUOTE *([FWS] qcontent) [FWS] DQUOTE [CFWS] DQUOTE = %x22 ; " (Double Quote) qcontent = qtext / quoted-pair
CFWSとFWSとquoted-pairを捨てて整理すると、以下の通りになります。
quoted-string = %x22 *(qtext) %x22
最後にqtextを定義していきます。
qtext
フルスペックは以下の通りです。
qtext = %d33 / ; Printable US-ASCII %d35-91 / ; characters not including %d93-126 / ; "\" or the quote character obs-qtext obs-qtext = obs-NO-WS-CTL obs-NO-WS-CTL = %d1-8 / ; US-ASCII control %d11 / ; characters that do not %d12 / ; include the carriage %d14-31 / ; return, line feed, and %d127 ; white space characters
コントロールコードを捨てて整理すると、以下の通りになります。
qtext = %x21 / %x23-5b / %x5d-7e
仕様のまとめ
qtext = %x21 / %x23-5b / %x5d-7e quoted-string = %x22 *(qtext) %x22 atext = %x21 / %x23-27 / %x2a-2b / %x2d / %x2f-39 / %x3d / %x3f / %x41-5a / %x5e-7e obs-local-part = (1*atext / quoted-string) *("." (1*atext / quoted-string)) obs-domain = 1*atext *("." 1*atext) addr-spec = obs-local-part "@" obs-domain
これでABNFでの仕様が完成です。次は、これを正規表現へ変換します。
正規表現に変換
ABNF表記を正規表現にするのは簡単です。ルールは以下の通り。
A / B
は(A|B)
に置き換え(AとBがそれぞれ一文字の場合は[AB]
に置き換え)*A
はA*
に置き換え1*A
はA+
に置き換え
Perl(および互換)の正規表現の場合、()
の代わりに(?:)
を使うことで、パターンの記憶を制限して、時間やメモリなどを節約できます。
my $qtext = "[\x21\x23-\\\x5b\\\x5d-\x7e]"; my $quoted_string = "\x22$qtext*\x22"; my $atext = "[\x21\x23-\x27\x2a-\x2b\x2d\x2f-\x39\x3d\x3f\x41-\x5a\x5e-\x7e]"; my $obs_local_part = "(?:$atext+|$quoted_string)(?:\\.(?:$atext+|$quoted_string))*"; my $obs_domain = "$atext+(?:\\.$atext+)*"; my $addr_spec = "$obs_local_part\@$obs_domain"; m/^$addr_spec\z/;
テストケースを考える
仕様に沿っているかどうか、テストしてみたいのでテストケースを考えます。幸いにも、それぞれのパートの内容が被ることがない(例えば、local-partのatextに"@"が含まれないのでlocal-partとdomainを個別に考えていい)ので、テストケースもパートごとに分けて単純に考えることができます。
状態遷移図を描いて、全てのパターンをたどるテストケースを作ります。
超適当な状態遷移図
addr_spec のテストケース
obs-local-partをA、obs-domainをBとして、終端に#を置いて、書き直して考えてみます。
A @ B #
テストケースは下の通りになります。
成功するケース A @ B # 失敗するケース # A # A @ #
obs-local-part のテストケース
atext+をA、quoted-stringをBとして、終端に#を置いて、書き直して考えてみます。
( A | B ) ( . ( A | B ) )* #
テストケースは下の通りになります。
成功するケース A # B # A . A # A . B # B . A # B . B # 失敗するケース # A . # B . #
obs-domain のテストケース
atext+をAとして、終端に#を置いて、書き直して考えてみます。
A ( . A )* #
テストケースは下の通りになります。
成功するケース A # A . A # 失敗するケース # A . #
quoted-string のテストケース
qtextをAとして、終端に#を置いて、書き直して考えてみます。
" A* " #
テストケースは下の通りになります。
成功するケース "" # " A " # 失敗するケース # " # " A #
atextとqtext のテストケース
単純にatextとqtestに含まれる文字が成功するケースで、含まない文字が失敗するケースになります。
テストケース
our @valid_case = ( 'foo@example.com', '"foo"@example.com', 'foo.bar@example.com', 'foo."bar"@example.com', '"foo".bar@example.com', '"foo"."bar"@example.com', 'foo@localhost', '""@example.com', '09AZaz!#$%&\'*+-/=?^_`{|}@example.com', '"09AZaz!#$%&\'()*+,-./:;<=>?@[]^_`{|}"@example.com', '{^_^}/~@example.com', '~/a.out@example.com', '"cellular.."@example.com', ); our @invalid_case = ( '', 'foo', 'foo@', '@example.com', 'foo.@example.com', '"foo".@example.com', 'foo@', 'foo@example.', '"@example.com', '"foo@example.com', "\x08".'@example.com', ':@example.com', "\"\x0d\x0a\"".'@example.com', 'foo@example.com'."\n", 'cellular..@example.com', );