私自身驚いたのだが、'test@[127.0.0.1' . "\\\x1f]"はRFC2822に準拠している。

へぼへぼCTO日記 - 「danコガいはもう正規表現をblogに書くな」と言わせないでくれ
おかげで上記のコードも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

見ての通り、\x1Ftextであり、\\\x1Fquoted-pairであり、よってdcontentの正当な一部となり、domain-literalとして正当なのである。

少なくとも、RFC2822に従えばそういうことになる。私自身我が目を疑ったが、Email::Valid->rfc822()も同様の結果を返す。ただしEmail::Valid->address()の方は、domain-litはすべて無視する。

余談だが、quoted-pairが、RFC2822とRFC822では異なっている。

RFC 822 (rfc822) - STANDARD FOR THE FORMAT OF ARPA INTERNET TEXT MESSAGES
     quoted-pair =  "\" CHAR                     ; may quote any char

端的な違いは、CRとLFを含むか含まないかである。

話を元に戻す。以上をふまえると、私が元にしたperlfaq9も不正確だということになる。たまたま\x1Fは正しく解釈したものの、\\\Sは明らかに手抜きであり、正しくは\\[\x01-\x09\x0B-\x0c\x0e-\x7f]ということになる。この点を修正した正規表現を、以下に掲載する。

へぼへぼCTO日記 - 「danコガいはもう正規表現をblogに書くな」と言わせないでくれ
ところでこの正規表現には他にも問題が残っている。domain-literalで\\\Sにマッチするようになっているがこれはなんなのだろう。

実にするどい意見である。そもそもの問題は、domain-litを無批判に使っていたことにある。MTAなどはとにかく、Webフォームやメーラーの設定フィールドなど、およそ人間が入力する「メールアドレス」にdomain-litは不要である。RFC2822から、domain-litを抜いた正規表現は、以下のとおりとなる。

はRF2822非準拠(domain-literalは除く)

最後に、これらをデモする 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";
}