私自身驚いたのだが、'test@[127.0.0.1' . "\\\x1f]"はRFC2822に準拠している。
おかげで上記のコードもvalidだ。なんてこった
なぜそうなのか、というのは、RFC2822のdomain-literalの仕様による。
domain-literal = [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS]
「[]で囲まれたdcontent」っていったいなんだ?
dcontent = dtext / quoted-pair
「dtextまたはquoted-pair」?
dtext = NO-WS-CTL / ; Non white space controls
%d33-90 / ; The rest of the US-ASCII
%d94-126 ; characters not including "[",
; "]", or "\"
quoted-pair = ("\" text) / obs-qp
で、textとは何かにたどり着く。
text = %d1-9 / ; Characters excluding CR and LF
%d11 /
%d12 /
%d14-127 /
obs-text
見ての通り、\x1Fはtextであり、\\\x1Fはquoted-pairであり、よってdcontentの正当な一部となり、domain-literalとして正当なのである。
少なくとも、RFC2822に従えばそういうことになる。私自身我が目を疑ったが、Email::Valid->rfc822()も同様の結果を返す。ただしEmail::Valid->address()の方は、domain-litはすべて無視する。
余談だが、quoted-pairが、RFC2822とRFC822では異なっている。
quoted-pair = "\" CHAR ; may quote any char
端的な違いは、CRとLFを含むか含まないかである。
話を元に戻す。以上をふまえると、私が元にしたperlfaq9も不正確だということになる。たまたま\x1Fは正しく解釈したものの、\\\Sは明らかに手抜きであり、正しくは\\[\x01-\x09\x0B-\x0c\x0e-\x7f]ということになる。この点を修正した正規表現を、以下に掲載する。
ところでこの正規表現には他にも問題が残っている。domain-literalで\\\Sにマッチするようになっているがこれはなんなのだろう。
実にするどい意見である。そもそもの問題は、domain-litを無批判に使っていたことにある。MTAなどはとにかく、Webフォームやメーラーの設定フィールドなど、およそ人間が入力する「メールアドレス」にdomain-litは不要である。RFC2822から、domain-litを抜いた正規表現は、以下のとおりとなる。
最後に、これらをデモする perl script をEntryの最後につけておく。
正規表現を正規にやろうとするとこれほど難しいものだとは。
Dan the Regexp Monger
use strict;
use warnings;
use Email::Valid;
my $rfc2822 = qr<(?:(?:(?:(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+)
(?:\.(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+))*)|(?:"(?:\\[^\r\n]|
[^\\"])*")))\@(?:(?:(?:(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+)
(?:\.(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+))*)|
(?:\[(?:\\[\x01-\x09\x0B-\x0c\x0e-\x7f]|[\x21-\x5a\x5e-\x7e])*\])))>x;
my $rfc2822_ndl = qr<(?:(?:(?:(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+)
(?:\.(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+))*)|(?:"(?:\\[^\r\n]|
[^\\"])*")))\@(?:(?:(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+)
(?:\.(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+))*))>x;
my $perlfaq9 = qr<(?:(?:(?:(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+)
(?:\.(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+))*)|(?:"(?:\\[^\r\n]|
[^\\"])*")))\@(?:(?:(?:(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+)
(?:\.(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+))*)|(?:\[(?:\\\S|
[\x21-\x5a\x5e-\x7e])*\])))>x;
my $regexp_info = qr<(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.
[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"
(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|
\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*
[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|
2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?
[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:
(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|
\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])>x;
sub match { $_[0] =~ /\A$_[1]\z/ }
for my $addr (
'da.me..@docomo.ne.jp', '"da.me.."@docomo.ne.jp',
'dankogai+regexp@gmail.com', 'test@[192.168.0.1]',
'test@[127.0.0.1' . "\\\x1f]", 'test@[127.0.0.1' . "\\\x0a]",
'test@[127.0.0.1' . "\\\x20]",
)
{
my $eaddr = $addr;
$eaddr =~ s/([\x00-\x1f])/sprintf("\\x%02x",ord $1)/eg;
print "$eaddr", "\n";
print " rfc2822: ", 0 + !!match( $addr, $rfc2822 ), "\n";
print " rfc2822_ndl: ", 0 + !!match( $addr, $rfc2822_ndl ), "\n";
print " perlfaq9: ", 0 + !!match( $addr, $perlfaq9 ), "\n";
print " regexp_info: ", 0 + !!match( $addr, $regexp_info ), "\n";
print " E::V->rfc822: ", 0 + !!Email::Valid->rfc822($addr), "\n";
print " E::V->address:", 0 + !!Email::Valid->address($addr), "\n";
}
同じレベルの間違いですね。
正規表現を書き慣れていれば、メールアドレスを表す正規表現に
"\S" が使われることはありえないと直感的に気付くはずです。
もし "\S" と同じような意味で使うなら "(?!\s)" を使うべきなので "\S" を使う機会はありません。
ちなみにまだ指摘するべき点が2点あります。
このまま使うと恥をかきますよ >読者のみなさん