メールアドレスチェックの正規表現:実用的なパターンとその限界
アドレスが実際に使えるかどうかは、どんな正規表現にもわかりません。短いパターンで入力ミスを拾い、残りは確認メールで確かめます。
「メールアドレス 正規表現」で検索すると、8文字ほどのものから数千文字に及ぶものまで、ありとあらゆるパターンが見つかります。長いものは規格に厳密に従おうとしていますが、それでもメールボックスが実在するかどうかは判定できません。短いものは読みやすく、正しい用途に使えば十分に役立ちます。その用途とは、@の欠落や空白の混入といった明らかな入力ミスを、本当の確認を担う確認メールを送る前に拾うことです。
RFC 5322の完全な文法を目標にすべきでない理由
RFC 5322はメールメッセージに含まれるアドレスの構文を定めていますが、会員登録フォームが求めるよりはるかに多くの形を許しています。次のアドレスはどれも構文上は有効です。
"john doe"@example.com:空白を含む、引用符で囲まれたローカル部。john(comment)@example.com:アドレスの中のコメント。user@[192.168.0.1]:ドメイン名の代わりに書かれたIPアドレス。admin@localhost:ドットを含まないドメイン。
コメントや折り返しの空白まで含めて文法全体を実装した正規表現は巨大で、レビューが難しく、多くのメールシステムが拒否するアドレスまで受け入れてしまいます。しかもjane@gmial.comは素通りさせます。SMTP(RFC 5321)はさらに長さの上限を定めており、@より前の部分は64オクテット、ドメインは255オクテットまでです。パスの構文と合わせると、実用上の最大長は254文字になります。長さのチェックは、正規表現で書くよりコードで書くほうが簡単です。
実用的なパターン
多くのアプリケーションが実際に必要としているのは、次のパターンです。空白と@以外の文字の並び、@が1つ、そしてドットを少なくとも1つ含むドメイン、という形を求めます。
^[^\s@]+@[^\s@]+\.[^\s@]+$
このパターンには直しておきたい弱点が1つあります。[^\s@]はドットにもマッチするため、user@example..comのような空のドメインラベルを受け入れてしまうのです。各ラベルからドットを除外すると、この穴がふさがります。
^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$
次の表は、Node 24でテスト用のアドレスに各パターンを適用した結果です。次の節で説明するHTML仕様のパターンも並べています。
| アドレス | 短いパターン | ラベル版パターン | HTML type=email |
|---|---|---|---|
first.last+news@example.co.uk | 通過 | 通過 | 通過 |
o'brien@example.ie | 通過 | 通過 | 通過 |
user@bücher.de | 通過 | 通過 | 拒否 |
δοκιμή@παράδειγμα.δοκιμή | 通過 | 通過 | 拒否 |
"john doe"@example.com | 拒否 | 拒否 | 拒否 |
admin@localhost | 拒否 | 拒否 | 通過 |
user @example.com | 拒否 | 拒否 | 拒否 |
user@@example.com | 拒否 | 拒否 | 拒否 |
user@example..com | 通過 | 拒否 | 拒否 |
user@example.com. | 通過 | 拒否 | 拒否 |
us..er@example.com | 通過 | 通過 | 通過 |
user@-example.com | 通過 | 通過 | 拒否 |
user@example.c | 通過 | 通過 | 通過 |
3つのうち完璧なものはありませんし、完璧である必要もありません。ローカル部の連続したドットや1文字のトップレベルドメインはどれも通過してしまいますが、それは確認メールで捕まえられます。大切なのは、実在してよく使われているアドレスを1つも拒否しないことです。
ラベル版を選ぶべき理由はパフォーマンスにもあります。末尾が空白で終わる40,000文字の細工された入力に対し、短いパターンはバックトラッキングのためNodeで約2.9秒かかりましたが、ラベル版は1ミリ秒未満で終わりました。パターンを実行する前に254文字の上限をチェックしておけば、どちらのパターンでもこの問題は起きません。
HTML5のinput type=emailのパターン
<input type="email">は、WHATWGのHTML Standardにある定義に照らしてブラウザーが検証します。仕様はこの定義をRFC 5322への「意図的な違反」(willful violation)と呼び、実装例として次のJavaScriptの正規表現を示しています。
/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
このパターンは@の前に一般的な記号を許し、ドメインラベルには英数字とハイフンで63文字以内、先頭と末尾はハイフン以外という条件を課します。一方でドットは必須ではないため、admin@localhostは通過します。ASCII以外の文字はすべて拒否します。スクリプトからinput.checkValidity()やinput.validity.typeMismatchでブラウザーのチェックを再利用することもできますが、ブラウザーで行うチェックは回避できるので、サーバー側でも必ず検証し直してください。
国際化メールアドレス
ドメインにはASCII以外の文字を含められます。bücher.deはDNS上ではxn--bcher-kva.deとしてやり取りされ、JavaScriptのnew URL('http://bücher.de').hostname、PHPのidn_to_ascii()、Pythonの'bücher.de'.encode('idna')はいずれもこの形を返します。@より前の部分にもASCII以外の文字を使えますが、そうしたアドレスに配送できるのはSMTPUTF8(RFC 6531)に対応したメールサーバーだけです。
受け入れるかどうかは、正規表現ではなく利用しているメール配信の仕組みに合わせて決めてください。[^\s@]をもとにしたパターンは国際化アドレスを受け入れ、HTMLのパターンは拒否します。PHPのfilter_var($email, FILTER_VALIDATE_EMAIL)も拒否する側で、私たちのテストではuser@bücher.deとδοκιμή@example.comの両方を拒否しました。FILTER_FLAG_EMAIL_UNICODEフラグを付けると後者は受け入れましたが、Unicodeのドメインは依然として拒否しました。
JavaScript・Python・PHPで同じチェックを書く
どの関数も前後の空白を取り除き、長さの上限を確認し、文字列全体がマッチすることを求めます。3つの関数は、上の表のアドレスに加えていくつかの境界ケース、つまり前後の空白、値の途中の改行、ローカル部のノーブレークスペース、256文字のアドレスでも、まったく同じ結果を返しました。
// JavaScript
function isValidEmail(value) {
const email = value.trim();
return email.length <= 254 && /^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/.test(email);
}
# Python
import re
EMAIL_RE = re.compile(r"[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+")
def is_valid_email(value: str) -> bool:
email = value.strip()
return len(email) <= 254 and EMAIL_RE.fullmatch(email) is not None
// PHP
function isValidEmail(string $value): bool
{
$email = trim($value);
return strlen($email) <= 254
&& preg_match('/^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/Du', $email) === 1;
}
これらのコードには、実際のバグを防ぐ細部が2つあります。Pythonでは、$を使ったre.match()が"user@example.com\n"を受け入れました。$は末尾の改行の直前にもマッチするからです。re.fullmatch()はこれを拒否しました。PHPもD修飾子を付けないかぎり同じ挙動になります。PHPではu修飾子も重要で、これがないと\sがノーブレークスペースにマッチせず、user\u{00A0}x@example.comが受け入れられてしまいました。JavaScriptでは、test()を呼ぶだけならgフラグは付けないでください。
本当の検証は確認メールで行う
パターンにわかるのは、アドレスがそれらしく見えるかどうかだけです。メールボックスが存在するか、メールを受信できるか、入力した本人のものかは判定できません。MXレコードを調べればドメインがメールを受け取る設定になっていることはわかりますが、特定のメールボックスが存在することまではわかりません。確実なテストは、実際にメッセージを送ることだけです。
- アドレスを未確認の状態で保存します。
- ランダムで1回しか使えず、たとえば24時間で期限切れになるトークンを含むリンクをメールで送ります。
- リンクが開かれたときに初めて、アドレスを確認済みにします。
- それまでは、そのアドレスにほかのメールを送らないようにします。
正規化は控えめにとどめます。前後の空白を削除し、ドメインを小文字にする程度で十分です。多くのプロバイダーは大文字と小文字を区別しませんが、RFC 5321は@より前の部分を大文字と小文字を区別するものとして扱うので、入力されたとおりに保存してください。
手元のリストでパターンを試す
いずれかのパターンを前後のスラッシュを付けずに正規表現テスターに貼り付け、フラグをgmにして、サンプルのアドレスを1行に1つずつ入力します。mフラグがあると^と$が各行に適用されるので、通過したアドレスが1行ずつ強調表示されます。上の3つのパターンすべてで、この行ごとの表示が各アドレスを個別にテストした結果と一致することを確認しています。ただしアプリケーションのコードでは、mを付けずに1件ずつテストしてください。そうしないと、複数行の値のうち1行がマッチしただけで通過してしまいます。