Perl
PHP
正規表現
IPv6
セキュリティ

PHPしか書けないザコがメールアドレス正規表現でガチ勢に挑んでみた

この記事の情報は古いので,最新の情報が欲しい方は 「PHPで各種バリデーション」 をお読みください。

訂正: IPv6のメールアドレスは IPv6: プレフィクスが必要です。PHP7.1時点でこの形式に対応していることを確認しました。

- 誤: a@[2001:0db8:bd05:01d2:288a:1fc0:0001:10ee]

- 正: a@[IPv6:2001:0db8:bd05:01d2:288a:1fc0:0001:10ee]


関数ラインナップ


私の関数


MyFunction

function validate_email($email, $strict = true) {

$dot_string = $strict ?
'(?:[A-Za-z0-9!#$%&*+=?^_`{|}~\'\\/-]|(?<!\\.|\\A)\\.(?!\\.|@))' :
'(?:[A-Za-z0-9!#$%&*+=?^_`{|}~\'\\/.-])'
;
$quoted_string = '(?:\\\\\\\\|\\\\"|\\\\?[A-Za-z0-9!#$%&*+=?^_`{|}~()<>[\\]:;@,. \'\\/-])';
$ipv4_part = '(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])';
$ipv6_part = '(?:[A-fa-f0-9]{1,4})';
$fqdn_part = '(?:[A-Za-z](?:[A-Za-z0-9-]{0,61}?[A-Za-z0-9])?)';
$ipv4 = "(?:(?:{$ipv4_part}\\.){3}{$ipv4_part})";
$ipv6 = '(?:' .
"(?:(?:{$ipv6_part}:){7}(?:{$ipv6_part}|:))" . '|' .
"(?:(?:{$ipv6_part}:){6}(?::{$ipv6_part}|:{$ipv4}|:))" . '|' .
"(?:(?:{$ipv6_part}:){5}(?:(?::{$ipv6_part}){1,2}|:{$ipv4}|:))" . '|' .
"(?:(?:{$ipv6_part}:){4}(?:(?::{$ipv6_part}){1,3}|(?::{$ipv6_part})?:{$ipv4}|:))" . '|' .
"(?:(?:{$ipv6_part}:){3}(?:(?::{$ipv6_part}){1,4}|(?::{$ipv6_part}){0,2}:{$ipv4}|:))" . '|' .
"(?:(?:{$ipv6_part}:){2}(?:(?::{$ipv6_part}){1,5}|(?::{$ipv6_part}){0,3}:{$ipv4}|:))" . '|' .
"(?:(?:{$ipv6_part}:){1}(?:(?::{$ipv6_part}){1,6}|(?::{$ipv6_part}){0,4}:{$ipv4}|:))" . '|' .
"(?::(?:(?::{$ipv6_part}){1,7}|(?::{$ipv6_part}){0,5}:{$ipv4}|:))" .
')';
$fqdn = "(?:(?:{$fqdn_part}\\.)+?{$fqdn_part})";
$local = "({$dot_string}++|(\"){$quoted_string}++\")";
$domain = "({$fqdn}|\\[{$ipv4}]|\\[{$ipv6}]|\\[{$fqdn}])";
$pattern = "/\\A{$local}@{$domain}\\z/";
return preg_match($pattern, $email, $matches) &&
(
!empty($matches[2]) && !isset($matches[1][66]) && !isset($matches[0][256]) ||
!isset($matches[1][64]) && !isset($matches[0][254])
)
;
}


  • 通常のローカル部

  • ローカル部のクオート方式

  • IPv4

  • 純正IPv6

  • IPv4混じりのIPv6

以上全てに対応しました。

strict = false にすると日本の古い携帯電話のアドレスも許容します。

今回は割愛。


PHPの filter_var 関数

詳 細 不 明


DevAchieve (和田さん)

http://wada811.blogspot.com/2013/03/best-email-format-check-regex-in-php.html


DevAchieve'sFunction

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;
}
}


RFC2822

404 Blog Not Found 管理人の小飼さんが紹介されている有名な記事です。

http://blog.livedoor.jp/dankogai/archives/51189905.html

(Perlよく分からないので生成後のコードで勘弁してください)


RFC2822'sFunction

function rfc2822_func($input) {

$pattern =
'/^(?:(?:(?:(?:[a-zA-Z0-9_!#\$\%&\'*+\\/=?\^`{}~|\-]+)(?:\.(?:[a-zA-Z0-9_!'.
'#\$\%&\'*+\\/=?\^`{}~|\-]+))*)|(?:"(?:\\[^\r\n]|[^\\"])*")))\@(?:(?:(?:(?:'.
'[a-zA-Z0-9_!#\$\%&\'*+\\/=?\^`{}~|\-]+)(?:\.(?:[a-zA-Z0-9_!#\$\%&\'*+\\/=?\^`'.
"{}~|\-]+))*)|(?:\[(?:\\\S|[\x21-\x5a\x5e-\x7e])*\])))$/"
;
return (bool)preg_match($pattern, $input);
}


phpspot

http://phpspot.net/php/pg%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BE%EF%BC%9A%E3%83%A1%E3%83%BC%E3%83%AB%E3%82%A2%E3%83%89%E3%83%AC%E3%82%B9%E3%81%8B%E3%81%A9%E3%81%86%E3%81%8B%E8%AA%BF%E3%81%B9%E3%82%8B.html


phpspot'sFunction

function phpspot_func($input) {

$pattern = '/^([a-zA-Z0-9])+([a-zA-Z0-9\._-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\._-]+)+$/';
return (bool)preg_match($pattern, $input);
}


検証


テストコード(ついでにベンチマークも取る)

<?php

$emails = <<<'EOD'
Abc@example.com
Abc.123@example.com
user+mailbox/department=shipping@example.com
!#$%&'*+-/=?^_`.{|}~@example.com
"Abc@def"@example.com
"Fred\ Bloggs"@example.com
"Joe.\\Blow"@example.com
".dot_kara_hazimaru"@example.com
"I.likeyou."@example.com
"I..love...you"@example.com
Abc.@example.com
Abc..123@example.com
.dot_kara_hazimaru@example.com
I.like.you.@example.com
I..love...you@example.com
a@[0.0.0.0]
a@[255.255.255.255]
a@[255.255.255.256]
a@[001.002.003.004]
a@[2001:0db8:bd05:01d2:288a:1fc0:0001:10ee]
a@[2001:0db8:bd05:01d2:288a::1fc0:0001:10ee]
a@[2001:0db8:bd05:01d2:288a:1fc0:0001:10ee:11fe]
a@[2001:db8:20:3:1000:100:20:3]
a@[2001:db8::1234:0:0:9abc]
a@[2001:db8::9abc]
a@[::]
a@[0::0]
a@[::1]
a@[1::]
a@[1:2:3:4:5:6:7::]
a@[::255.255.255.255]
a@[::ffff:255.255.255.255]
a@[::ffff:0:255.255.255.255]
a@[2001:db8:3:4::192.0.2.33]
a@[64:ff9b::192.0.2.33]
a@[example.com]
a@[example.com:hoge]
a@0
a@a
a@0.a
a@0.0
a@a.0
a@.a
a@a-.a
a@-a.a
a@a-a.com
a@0-a.com
a@a-0.com
a@a-a.a-a
a@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.com
a@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678901.com
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/@example.com
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/a@example.com
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/"@example.com
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/a"@example.com
abcdefhghijklmnopqrstuvwxyzABC@aaaaaaaa01.aaaaaaaa02.aaaaaaaa03.aaaaaaaa04.aaaaaaaa05.aaaaaaaa06.aaaaaaaa07.aaaaaaaa08.aaaaaaaa09.aaaaaaaa10.aaaaaaaa11.aaaaaaaa12.aaaaaaaa13.aaaaaaaa14.aaaaaaaa15.aaaaaaaa16.aaaaaaaa17.aaaaaaaa18.aaaaaaaa19.aaaaaaaa20.com
abcdefhghijklmnopqrstuvwxyzABCD@aaaaaaaa01.aaaaaaaa02.aaaaaaaa03.aaaaaaaa04.aaaaaaaa05.aaaaaaaa06.aaaaaaaa07.aaaaaaaa08.aaaaaaaa09.aaaaaaaa10.aaaaaaaa11.aaaaaaaa12.aaaaaaaa13.aaaaaaaa14.aaaaaaaa15.aaaaaaaa16.aaaaaaaa17.aaaaaaaa18.aaaaaaaa19.aaaaaaaa20.com
"abcdefhghijklmnopqrstuvwxyzABC"@aaaaaaaa01.aaaaaaaa02.aaaaaaaa03.aaaaaaaa04.aaaaaaaa05.aaaaaaaa06.aaaaaaaa07.aaaaaaaa08.aaaaaaaa09.aaaaaaaa10.aaaaaaaa11.aaaaaaaa12.aaaaaaaa13.aaaaaaaa14.aaaaaaaa15.aaaaaaaa16.aaaaaaaa17.aaaaaaaa18.aaaaaaaa19.aaaaaaaa20.com
"abcdefhghijklmnopqrstuvwxyzABCD"@aaaaaaaa01.aaaaaaaa02.aaaaaaaa03.aaaaaaaa04.aaaaaaaa05.aaaaaaaa06.aaaaaaaa07.aaaaaaaa08.aaaaaaaa09.aaaaaaaa10.aaaaaaaa11.aaaaaaaa12.aaaaaaaa13.aaaaaaaa14.aaaaaaaa15.aaaaaaaa16.aaaaaaaa17.aaaaaaaa18.aaaaaaaa19.aaaaaaaa20.com
EOD;

$emails = explode("\n", $emails);

$time1_sum = 0.0;
$time2_sum = 0.0;
$time3_sum = 0.0;
$time4_sum = 0.0;
$time5_sum = 0.0;

echo "<h1>Results</h1>\n";
echo "<table border=\"1\">\n";
echo "<tr>\n";
echo "<th>E-mail</th><th>My Function</th><th>PHP Function</th><th>DevAchieve's Function</th><th>RFC2822's Function</th><th>phpspot's Function</th>\n";
foreach ($emails as $email) {
$ok = '<span style="color:green;">OK</span>';
$ng = '<span style="color:red;font-weight:bold;">NG</span>';
for ($i = 0; $i < 800; $i++) {
$t1 = microtime(true);
$r1 = validate_email($email) ? $ok : $ng;
$t2 = microtime(true);
$r2 = filter_var($email, FILTER_VALIDATE_EMAIL) !== false ? $ok : $ng;
$t3 = microtime(true);
$r3 = isValidEmailFormat($email, false) ? $ok : $ng;
$t4 = microtime(true);
$r4 = rfc2822_func($email) ? $ok : $ng;
$t5 = microtime(true);
$r5 = phpspot_func($email) ? $ok : $ng;
$t6 = microtime(true);
$time1_sum += $t2 - $t1;
$time2_sum += $t3 - $t2;
$time3_sum += $t4 - $t3;
$time4_sum += $t5 - $t4;
$time5_sum += $t6 - $t5;
}
$email = wordwrap($email, 40, "<br>\n", true);
echo "<tr>\n";
echo "<td style=\"font-family:Consolas, 'Courier New', Courier, Monaco, monospace;\">{$email}</td><td>{$r1}</td><td>{$r2}</td><td>{$r3}</td><td>{$r4}</td><td>{$r5}</td>\n";
echo "</tr>\n";
}
echo "</table>\n";
echo "<h1>Benchmarks (Repeated 800 times)</h1>\n";
echo "<p>\n";
printf("My Function: %f sec<br>\n", $time1_sum);
printf("PHP Function: %f sec<br>\n", $time2_sum);
printf("DevArchive's Function: %f sec<br>\n", $time3_sum);
printf("RFC2822's Function: %f sec<br>\n", $time4_sum);
printf("phpspot's Function: %f sec<br>\n", $time5_sum);
echo "</p>\n";


results.jpg


  • 画像このままでは文字小さいので拡大してみてください(笑)

  • いろいろ情報収集しましたが、私の関数の結果が一番正しいと思います。

  • なんかIPv6対応させたら重くなっちゃったけどまぁ許容範囲!

  • Windows版のApacheだと(だけ?)よくfilter_var関数実行中に落ちたりしました。
    いろいろ値変えてやってみたけど原因不明でした。

そろそろ僕も正規表現ガチ勢名乗っていいですか。