2013/03/12

[PHP]実用的なメールアドレスの正規表現

メールアドレスは仕様がヤバイ

正規表現辞典正規表現辞典を読んでループ展開も覚えた私に怖いものなどない!と
メールアドレスの正規表現を書いてみた。…ものは漏れがあった。
メールアドレスの正規表現なんて決定版があるんじゃないの?と調べてみたら
404 Blog Not Found:「PHP使いはもう正規表現をblogに書くな」と言わせないでくれ
こぶたのラッパ » 「PHP使いはもう正規表現をblogに書くな」のメールアドレスチェック用正規表現をPHP用に書きなおす
「danコガいはもう正規表現をblogに書くな」と言わせないでくれ | へぼい日記
404 Blog Not Found:regexp - 'test@[127.0.0.1' . "¥¥¥x1f]" はRFC2822準拠
などの記事が見つかった。正規表現がヤバイと思ったらまず仕様がヤバイらしい。
Perlメモ
Jeffrey E. F. Friedl氏原著による 「詳説 正規表現」にはメールアドレスはネストしたコメントを持つことができるので正規表現で表わすのは不可能であると書いてあります。
メールアドレス - Wikipedia
Wikipedia 見るとわかるが実は色々と記号を使えたり、" " でくくればなんでもありという凄い仕様なのである。

そこで一番理解できそうな以下の記事の正規表現を使うことにした。
メールアドレス(addr-spec)の正規表現 | へぼい日記
<?php
$wsp           = '[\x20\x09]'; // 半角空白と水平タブ
$vchar         = '[\x21-\x7e]'; // ASCIIコードの ! から ~ まで
$quoted_pair   = "\\\\(?:$vchar|$wsp)"; // \ を前につけた quoted-pair 形式なら \ と " が使用できる
$qtext         = '[\x21\x23-\x5b\x5d-\x7e]'; // $vchar から \ と " を抜いたもの。\x22 は " , \x5c は \
$qcontent      = "(?:$qtext|$quoted_pair)"; // quoted-string 形式の条件分岐
$quoted_string = "\"$qcontent*\""; // " で 囲まれた quoted-string 形式。量指定子が*ということは空文字を許容している
$atext         = '[a-zA-Z0-9!#$%&\'*+\-\/\=?^_`{|}~]'; // 通常、メールアドレスに使用出来る文字
$dot_atom_text = "$atext+(?:[.]$atext+)*"; // ドットが連続しない RFC 準拠形式をループ展開で記述
$dot_atom      = $dot_atom_text; // RFC5322 では dot-atom と呼ぶ
$local_part    = "(?:$dot_atom|$quoted_string)"; // local-part は dot-atom 形式 または quoted-string 形式のどちらか
$domain        = $dot_atom; // ドメインは英数字と記号 を ドット区切りにした文字
$addr_spec     = "${local_part}[@]$domain"; // 変数展開で配列として処理されるため可変変数として書いている

// 昔の携帯電話メールアドレス用
$dot_atom_loose   = "$atext+(?:[.]|$atext)*"; // 連続したドットと @ の直前のドットを許容する
$local_part_loose = "(?:$dot_atom_loose|$quoted_string)"; // 昔の携帯電話メールアドレス用。昔の携帯電話は quoted-string 形式のメールアドレスを取得できたのか?
$addr_spec_loose  = "${local_part_loose}[@]$domain"; // 変数展開で配列として処理されるため可変変数として書いている

$input_addr_spec = 'foo@example.com';

if ( preg_match("/\A$addr_spec\z/", $input_addr_spec) ) {
    print "valid addr-spec\n";
}
?>
理解せずにコピペするのは嫌だったのでコメントをつけて理解できるようにした。
リンク先の方針は引用していないけど読んでおいてください。
RFC 5322 準拠で昔の携帯電話のメールアドレスで取得できた
「連続したドット」と「 @ の直前のドット」を持つメールアドレスも許容する正規表現を書いてくれている。
\ が \\\\ と表記されている理由は正規表現事典のP.115を見るべし。
簡単にいえば文字列のエスケープと正規表現のエスケープでそれぞれ倍増するから。

あと 多分バグで quoted-string 形式の正規表現の量指定子が * になっていて ""@domain を通してしまう。

^-^@-.- はRFC準拠のメールアドレスです

メールアドレス(addr-spec)の正規表現 | へぼい日記ではローカルパートのテストは十分行われているけど
ドメイン部分のテストは不十分に感じる。その証拠に ^-^@-.- なども通してしまう。
流石にこんな顔文字みたいなメールアドレスを DB で見かけたら目を疑ってしまう。
その理由はRFC 5322 - Internet Message Formatにほとんどドメインについて書かれていないからである。
ドットとハイフンは連続しないと思われるが連続を許す正規表現だし、記号も色々使えるのでおかしい気がする。
そう思って調べてみると Wikipedia にRFC 5321 - Simple Mail Transfer Protocolにも
メールアドレスについて書かれている、と書いてあった。
軽くキーワードだけ抽出して読んでみた感じでは
ドメインはドット区切りのサブドメインの繰り返しからなり、
サブドメインはハイフン区切りの英数字の繰り返しからなる。
読む人は RFC で使われる ABNF の仕様(RFC 5234 - Augmented BNF for Syntax Specifications: ABNF)が
わからないと読みにくいかも。まぁなんとなく予想はつくだろうけど。

実用的なメールアドレスの形式チェック関数

<?php
function isValidEmailFormat($email, $supportPeculiarFormat = true){
    $wsp              = '[\x20\x09]'; // 半角空白と水平タブ
    $vchar            = '[\x21-\x7e]'; // ASCIIコードの ! から ~ まで
    $quoted_pair      = "\\\\(?:{$vchar}|{$wsp})"; // \ を前につけた quoted-pair 形式なら \ と " が使用できる
    $qtext            = '[\x21\x23-\x5b\x5d-\x7e]'; // $vchar から \ と " を抜いたもの。\x22 は " , \x5c は \
    $qcontent         = "(?:{$qtext}|{$quoted_pair})"; // quoted-string 形式の条件分岐
    $quoted_string    = "\"{$qcontent}+\""; // " で 囲まれた quoted-string 形式。
    $atext            = '[a-zA-Z0-9!#$%&\'*+\-\/\=?^_`{|}~]'; // 通常、メールアドレスに使用出来る文字
    $dot_atom         = "{$atext}+(?:[.]{$atext}+)*"; // ドットが連続しない RFC 準拠形式をループ展開で構築
    $local_part       = "(?:{$dot_atom}|{$quoted_string})"; // local-part は dot-atom 形式 または quoted-string 形式のどちらか
    // ドメイン部分の判定強化
    $alnum            = '[a-zA-Z0-9]'; // domain は先頭英数字
    $sub_domain       = "{$alnum}+(?:-{$alnum}+)*"; // hyphenated alnum をループ展開で構築
    $domain           = "(?:{$sub_domain})+(?:[.](?:{$sub_domain})+)+"; // ハイフンとドットが連続しないように $sub_domain をループ展開
    $addr_spec        = "{$local_part}[@]{$domain}"; // 合成
    // 昔の携帯電話メールアドレス用
    $dot_atom_loose   = "{$atext}+(?:[.]|{$atext})*"; // 連続したドットと @ の直前のドットを許容する
    $local_part_loose = $dot_atom_loose; // 昔の携帯電話メールアドレスで quoted-string 形式なんてあるわけない。たぶん。
    $addr_spec_loose  = "{$local_part_loose}[@]{$domain}"; // 合成
    // 昔の携帯電話メールアドレスの形式をサポートするかで使う正規表現を変える
    if($supportPeculiarFormat){
        $regexp = $addr_spec_loose;
    }else{
        $regexp = $addr_spec;
    }
    // \A は常に文字列の先頭にマッチする。\z は常に文字列の末尾にマッチする。
    if(preg_match("/\A{$regexp}\z/", $email)){
        return true;
    }else{
        return false;
    }
}
俺は変数展開は{}でくくる派なんだよお!
あとRFC 5321 - Simple Mail Transfer Protocolではサブドメインは1回でも良いので hoge@fuga を
許容するのだけど感覚として最低でもドットは1回は含まれるだろ?ってことで15行目の量指定子を + にした。

引数で昔の携帯電話のメールアドレスの形式をサポートするか選べるようにした。
なので以下のように書ける。普通こんな事しないだろうけど。
<?php
if(isValidEmailFormat($email)){
    if(isValidEmailFormat($email, false)){
        // RFC準拠の正しいメールアドレスです
    }else{
        // キャリアの独自形式なのでメールが届かない可能性があるよ的な注意書きが出せる
    }
}else{
    // 不正な形式なのでエラーです
}
ちなみに昔の携帯電話のメールアドレスの形式をサポートしないなら filter_var を使うこともできる。
if(filter_var($email, FILTER_VALIDATE_EMAIL)){
    // 正しい(?)形式です
}
(?)をつけたのはドメイン部分のハイフンとドット周りで変なのを許容するから。
処理時間は isValidEmailFormat より 6倍遅い上に、filter_var のメールアドレスチェックは微妙なので
isValidEmailFormat 関数を使って行きましょう!

追記: 2013/08/11

Perl - PHPしか書けないザコがメールアドレス正規表現でガチ勢に挑んでみた - Qiita [キータ]
色々なパターンをテストしてもらいました。
基本的にWebサービスでの利用を想定していたので
はてブコメントにあるようなドメインパートが localhost のメールアドレスや
IPv4, IPv6 のメールアドレスは通らなくても良いかな?
a@a.0, a@0.0, a@0.a が通ってるのが微妙感あるけどどうなんだろ?
他は大体想定通りなので良かった。
文字数チェックは行なっていないので別途チェックしましょう。

タグ(RSS)