メールアドレスの検証にRegexを使うべきでない理由
メールの正規表現が失敗する理由を解説。正しいアドレスを拒否し、配信不能なものを通し、ReDoSも招きます。HTML5のemail入力かライブラリを使いましょう。
Regexはメールアドレスを検証できません。おおまかな形式を確認できるに過ぎず、どれほど精巧なパターンを作っても、正当なアドレスを弾いたり、構文的には正しく見えても実際には届かないアドレスを通過させたりしてしまいます。問題は「まだ正しいパターンを見つけていない」ことではありません。「このメールアドレスは有効か?」という問いが3つの異なる問題を混同しているからです。Regexが対処できるのはそのうちの1つ、しかも最も役に立たないものだけです。本記事ではこの3つの問題を切り分け、仕様書(RFC 5321、RFC 5322、RFC 6531、WHATWG HTML Living Standard)が実際に何を述べているかを引用し、よく使われるパターンが双方向でどのように破綻するかを具体的に示した上で、代わりに使うべき実践的なJS/TSコードを紹介します。
重要なポイント
- メールアドレスの検証には3つの異なる層があります。UXの健全性チェック、RFC 5321/5322に基づく構文検証、そして存在確認です。アドレスが実際にメールを受信できることを証明できるのは、確認メールを送信することだけです。
- HTML Living Standardの
<input type="email">が使用するRegexは、仕様書自身が「RFC 5322への意図的な違反」と明記しています。意図的にRFC完全準拠ではなく、手書きのパターンよりも優れたデフォルトです。 - よく使われるメールRegexは双方向で失敗します。実在するアドレス(プラスアドレッシング、新しいgTLD、クォートされたローカルパート、RFC 6531に基づく国際化アドレス)を弾き、届かないアドレスを通過させます。
- バックトラッキングが起きやすいメールRegexをNode.jsのサーバーサイドで実行すると、短い細工された入力によってイベントループをブロックされる可能性があります。これはReDoSサービス拒否攻撃のベクターです(CWE-1333)。
- パターンを実行する前に確認する価値のある唯一の長さ制約は、RFC 5321 §4.5.3.1.3に由来します。アドレス自体は254文字に制限されています。
メールアドレス検証の3つの層
メールアドレスの検証には3つの異なる層があります。UXの健全性チェック(メールアドレスのように見えるか)、構文検証(RFC 5321/5322のルールに準拠しているか)、そして存在確認(このメールボックスは実際にメールを受信するか)です。第3の層だけが実際にアドレスが機能することを証明でき、それを実現するのは確認メールの送信のみです。「Regexの使用をやめろ」という主張は正しいのですが、これらの層を混同しがちです。層を分けて考えることで、どのツールをどこで使うべきかが明確になります。
- 第1層 — UXの健全性チェック。 明らかなタイプミス(
alicegmail.comや末尾のスペースなど)を検出し、即座にフィードバックを返す、安価で高速なクライアントサイドのチェックです。Regexが適切なのはこの層だけであり、ここでも最小限のパターンを使うべきです。 - 第2層 — 構文検証。 文字列がメールRFCの文法に準拠しているかを確認します。これは見た目よりはるかに難しく、手書きのRegexでは対処しきれません。そして決定的なことに、配信可能性については何も証明しません。RFC完全準拠のアドレスが、存在しないドメインを指していることもあります。
- 第3層 — 存在確認。 実際のメールボックスがこのアドレスでメールを受信するかを確認します。メールアドレスが機能することを証明できる唯一の方法は、メッセージが正常に配信されることです。確認メールはRegexでは到底できないことを一度に実現します。
「究極のメールRegex」を目指すほぼすべての試みが犯す間違いは、第2層を完璧にしようとすることです。しかし第2層は、誰もが本当に知りたい問いに答えてくれません。問いは第3層にあり、どんなパターンもそこには届かないのです。
「有効」とは実際に何を意味するか
Discover how at OpenReplay.com.
有効なメールアドレスは、ほとんどのRegexが想定するよりもはるかに許容範囲が広いです。なぜなら、RFC 5321(SMTP)とRFC 5322(メッセージフォーマット)の文法は、一見壊れているように見える構造を許可しているからです。@より前のすべてであるローカルパートには、多数の特殊文字を含めることができ、クォートされた文字列にすることさえ可能です。
クォートなしのローカルパートはatextから構成されます。RFC 5322 §3.2.3で定義されており、文字と数字に加えて以下の文字が許可されています。
! # $ % & ' * + - / = ? ^ _ ` { | } ~
つまり、user+tag@example.com(プラスアドレッシング)は有効です。RFC 5321 §4.1.2によれば、+は通常のatextです。受信サーバーが+tagをサブアドレスとして扱うかどうかは実装依存(RFC 5233)ですが、アドレス自体は正しい形式です。ローカルパートはクォートされた文字列にすることもできます。"user name"@example.comはRFC 5321 §4.1.2およびRFC 5322 §3.2.4によれば、スペースを含めて有効です。ドメインはブラケットで囲まれたIPアドレスリテラルにすることもできます。user@[192.168.1.1]はRFC 5321 §4.1.3によれば有効です。
安価に確認できる価値のある制約が1つあります。RFC 5321 §4.5.3.1.3では、アングルブラケットを含むフォワードパスを256オクテットに制限しており、アドレス自体には254文字が残ります。ローカルパートは64オクテット(§4.5.3.1.1)、ドメインは255オクテット(§4.5.3.1.2)にそれぞれ制限されています。長さのチェックは文字列比較で正確に処理できる唯一の検証であり、Regexを必要としません。
国際化アドレス(EAI)
RFC 6531で定義された国際化メールアドレス(例:用户@例子.广告)は有効であり、ますます普及しています。ASCIIのみのRegexではこれらを処理できません。これはライブラリの問題であり、Regexの問題ではありません。EAI(RFC 6531 §3.3)はローカルパートをUTF-8に対応させるよう拡張しており、ドメインも非ASCIIのUnicodeにできます。これはIDNAのpunycodeエンコードされたドメイン(RFC 5891)とは異なります。EAIはローカルパートもカバーしています。ローカルパートに[a-zA-Z0-9]を仮定するパターンは、世界のユーザーの増加する一部にとって誤りであり、ASCIIとUnicodeの両方のローカルパートを正しく受け入れつつ不正な入力を弾く単一のRegexは存在しません。
メールアドレス検証のRegexが双方向で失敗する理由
手書きのメールRegexは、ゲートキーパーとしてもフィルターとしても失敗します。偽陰性(配信可能なアドレスを弾く)と偽陽性(文法には準拠しているが実際には届かないアドレスを通過させる)の両方を生み出します。テストスイートがtest@example.comを使用しており、これはすべてのパターンを通過するため、両方の失敗モードが常に本番環境に出荷されます。
Stack Overflowでよくコピペされる、TLDを2〜4文字に制限するパターンを見てみましょう。
// よくコピペされるパターン。使用しないこと。
const bad = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/;
実際のアドレスに対する動作を示します。
| アドレス | テスト内容 | このRegex | 正しい結果 | 問題の理由 |
|---|---|---|---|---|
name+filter@gmail.com | プラスアドレッシング | ✅ 通過 | ✅ 有効 | (ここでは通過するが、より厳格なパターンでは+を弾く) |
user@studio.photography | 長いgTLD | ❌ 拒否 | ✅ 有効 | {2,4}は4文字超のTLDを弾く |
"user name"@example.com | クォートされたローカルパート | ❌ 拒否 | ✅ 有効 | クォートされた文字列とスペースは有効 |
用户@例子.广告 | EAI(RFC 6531) | ❌ 拒否 | ✅ 有効 | ASCIIのみの文字クラス |
someone@validformat.test | 存在しないドメイン | ✅ 通過 | ❌ 配信不可 | 構文は正しいがドメインが解決しない |
name+filter@gmail.com(プラスアドレッシング)やuser@studio.photography(ICANNの新gTLDプログラムで委任されたgTLDで、.photographyは2013年にルートに追加)を弾くRegexは厳格なのではなく、間違っています。どちらも有効なメール機能を使用した構文的に正しいアドレスです。{2,4}というTLD制約だけで、.photography、.accountants、.engineering、その他数百の有効な委任を破壊します。
セッションリプレイでは、ユーザーが検証エラーに遭遇し、何度も入力を修正した末にフォームを離脱する様子が頻繁に確認されています。フォームのユーザビリティに関する研究では、検証によるフリクションがフォーム離脱率の増加とコンバージョン率の低下の一因であることが一貫して示されています。
偽陽性——Regexを通過しても実際には届かないアドレス——も同様に現実の問題です。someone@validformat.testは上記のパターンとほとんどのパターンを通過しますが、.testは予約済みTLD(RFC 2606)であり、決して配信されません。構文的な準拠と配信可能性は独立した性質であり、Regexが見られるのは前者だけです。
ReDoS:Regexが脆弱性になるとき
バックトラッキングが起きやすいメールRegexをNode.jsのサーバーサイドで実行すると、細工された入力によってイベントループをブロックされる可能性があります。これはサービス拒否攻撃のベクター(CWE-1333: 非効率な正規表現の複雑さ、およびOWASP ReDoSリファレンス)であり、メールとは無関係で、破滅的なバックトラッキングに関するものです。重複する文字クラスに対してネストまたは隣接した量指定子を持つパターンは、ほぼマッチする入力に対して指数関数的な時間を要することがあります。
以下は再現可能なデモンストレーションです。このパターンの(...)+は、同じ文字を複数の方法でマッチできるグループをラップしているため、1文字の長い連続の後にマッチしない文字が来ると、エンジンは失敗する前に指数関数的な数の分割を試みることを強いられます。
// Node.js v24。実行方法: node redos.js
// 意図的に脆弱な、バックトラッキングが起きやすいパターン。
const evil = /^([a-zA-Z0-9]+)*@example\.com$/;
// 細工された近似マッチ: 多数の'a'の後にマッチを壊す文字。
const attack = "a".repeat(40) + "!";
console.time("redos");
evil.test(attack); // イベントループをブロックする
console.timeEnd("redos");
現在のNode.jsビルドでは、繰り返し回数を増やすとマッチ時間が爆発的に増加します。文字を1つ追加するたびに処理量がほぼ2倍になります。NodeのRegexエンジンはメインスレッドで同期的に実行されるため、この入力を含む単一のリクエストがイベントループを停止させ、処理中の他のすべてのリクエストをブロックします。(x+)*の形が危険のサインです。外側の量指定子の下で、同じ部分文字列を複数の方法でマッチできるグループは、破滅的なバックトラッキングの候補となります。解決策はより賢いパターンを作ることではなく、このクラスのパターンを一切作らないことです。プラットフォームやメンテナンスされたライブラリに委ねることで、まさにそれが実現できます。
構文は配信可能性ではない
RFC完全準拠のアドレスでさえ、メールが届くかどうかについては何も教えてくれません。Regexはドメインが存在するか、MXレコードがあるか、メールボックスがプロビジョニングされているか、使い捨てアドレスでないかを確認できません。これらはネットワークとポリシーの問題であり、文法の問題ではありません。realuser@gmail.comというアドレスとタイポしたrealuser@gmial.comはどちらも構文的に有効ですが、DNSルックアップだけが両者を区別でき、実際の配信だけが生きているメールボックスと死んでいるメールボックスを区別できます。
使い捨てや一時的なメールドメインは関連する別の問題です。構文的にも運用上も有効ですが、サインアップを回避するために存在するアドレスです。これらを検出するには、プロバイダードメインのメンテナンスされたブロックリストが必要であり、パターンでは対処できません。ドメインリストは常に変化しており、ハードコードしたリストはすぐに陳腐化します。これは検証の一部ではなく、検証の上に乗るポリシー層として扱うべきです。
代わりに何をすべきか
レイヤードアプローチを使用してください。UX用の最小限の健全性チェック、構文用のプラットフォーム組み込みの検証、より多くが必要な場合のみメンテナンスされたライブラリ、そして本当に重要な唯一のことのための確認メールです。最も安価なものから権威あるものへの順序を示します。
1. 最小限の健全性チェック
クライアントサイドでの即座のフィードバックには、「Regexの使用をやめろ」という主張の元となった最小限の有用なパターンを使用します。何か、@、何か、ドット、何かを要求します。長さチェックと組み合わせます。
/**
* 第1層の健全性チェック: 明らかなタイプミスを検出するだけです。
* 意図的に許容範囲が広い — 有効性の証明ではありません。
* @param value - 生の入力文字列
* @returns 値がメールのおおまかな形式を持ち、254文字以下の場合はtrue
*/
export function looksLikeEmail(value: string): boolean {
if (value.length > 254) return false; // RFC 5321 §4.5.3.1.3
return /.+@.+\..+/.test(value);
}
この健全性チェックはalicegmail.comとalice@localhostを弾き、プラスアドレッシングと長いgTLDを通過させ、定数時間で実行されます。trueを「有効」として扱うのは安全ではありません。これはタイポキャッチャーです。
2. プラットフォームを優先する:<input type="email">
構文検証のための最良のデフォルトはブラウザ自身の<input type="email">であり、それが何をするかを正確に理解する価値があります。HTML Living Standardの<input type="email">が使用するRegexは、仕様書自身が「RFC 5322への意図的な違反」と呼んでいます。意図的にRFC完全準拠ではなく、仕様の正確さよりもユーザビリティを優先しており、自分で書くどんなパターンよりも優れたデフォルトです。仕様書には正確なパターンが引用されています。
^[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])?)*$
節ごとに読み解くと:
[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+— ローカルパート。atextの特殊文字を許可。クォートされたローカルパート("user name"@…)は意図的にサポートしない。@— 正確に1つのセパレーター。[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?— ドメインラベル:英数字で始まり終わり、内部にハイフンを許可、63文字に制限。IPリテラルドメイン(user@[192.168.1.1])は意図的にサポートしない。(?:\.<label>)*— ドットで区切られた追加ラベルを0個以上許可。単一ラベルと複数ラベルの両方のドメインが通過する。
WHATWGはトレードオフを明示しています。このパターンは技術的に有効なRFC 5322アドレス(クォートされたパート、IPリテラル)の一部を意図的に弾きます。それらの形式は実際のサインアップでは極めてまれであり、サポートすることで防ぐよりも多くのバグを招くからです。これはフォームフィールドとして正しいトレードオフであり、<input type="email">を第2層のベースラインにすべき理由です。バックトラッキングの問題がなく、ブラウザがすでに適用しているものと一致しています。
3. より多くが必要な場合のみメンテナンスされたライブラリを使用する
HTML5パターンを超えたサーバーサイドの構文検証が必要な場合は、自分でロールするのではなく、メンテナンスされた十分にテストされたライブラリを使用してください。validatorパッケージ(npm validator、MITライセンス)は、クォートされたローカルパートをサポートし、IPリテラルドメインや表示名のオプションを提供するisEmail関数を公開しています。
import isEmail from "validator/lib/isEmail";
/**
* 第2層の構文検証、サーバーサイド。
* @param email - 候補アドレス(長さチェック済み)
* @returns validatorのRFC準拠ルールに基づいて構文的に有効な場合はtrue
*/
export function isSyntacticallyValid(email: string): boolean {
return isEmail(email, { allow_utf8_local_part: true });
}
2018年以降更新されていない古いemail-validatorパッケージよりもこちらを優先してください。ライブラリを使用することで、テスト済みのエッジケース処理と、手書きのパターンでは決して対処できないケースを修正するアクティブなメンテナーが得られます。適切なオプションを使用すれば、EAIアドレスも含まれます。
4. 本当の答え:確認メールを送信する
アドレスが機能することを証明できる唯一のステップは配信です。ワンタイムリンクを含む確認メッセージを送信し、ユーザーがクリックした後にのみアドレスを確認済みとして扱います。これはダブルオプトインであり、上流の複雑な検証を不要にします。不正な形式や配信不可のアドレスは単純に確認されません。
/**
* 検証フローのスケッチ。ストレージとメーラーはアプリ固有。
* @param email - 長さ+構文チェックを通過した文字列
*/
async function startEmailVerification(email: string): Promise<void> {
const token = crypto.randomUUID();
await storePendingVerification(email, token); // 例えば24時間後に期限切れ
const link = `https://app.example.com/verify?token=${token}`;
await sendMail(email, "Confirm your email", `Click to confirm: ${link}`);
// 有効なトークンで/verifyにアクセスされた場合のみアカウントを確認済みにする。
}
確認メールの送信は、多くの情報源が最終的に行き着く構造です。理由は同じです。Regexでは到底できないことを一度に実現するからです。Jamie Zawinskiが言ったように、「問題に直面したとき、『そうだ、正規表現を使おう』と考える人がいる。今や彼らには2つの問題がある。」メールに関しては、2つ目の問題は、Regexがそもそも問いに答えられなかったということです。
まとめ
アドレスを検証しようとするのをやめ、メールボックスを確認することに集中してください。即座のUXフィードバックには最小限のパターンと254文字の上限を使用し、構文には<input type="email">やvalidatorなどのメンテナンスされたライブラリを活用し、すべての実際のアカウントを確認メールの後ろに置いてください。最後のステップだけが誰かが実際にいることを証明します。次回サインアップフォームにメールフィールドが必要になったとき、Stack Overflowのパターンではなく、プラットフォームと確認フローを使用してください。
FAQ
メールアドレスの最大有効長は何文字ですか?
メールアドレスは254文字に制限されています。これはRFC 5321のセクション4.5.3.1.3に由来し、アングルブラケットを含むフォワードパスを256オクテットに制限しており、アドレス自体には254文字が残ります。ローカルパートは64オクテット、ドメインは255オクテットにそれぞれ制限されています。シンプルな長さ比較でこれを正確に適用できます。これはパターンを実行する前に行う価値のある唯一の検証です。
HTML5のメール入力はRFC 5322の完全な文法に対して検証しますか?
いいえ。HTML Living Standardは、そのメール入力Regexを明示的に「RFC 5322への意図的な違反」と説明しています。クォートされたローカルパート('user name'@example.com)やIPリテラルドメイン(user@[192.168.1.1])などの技術的に有効な形式を意図的に弾きます。これらは実際のサインアップでは極めてまれだからです。このトレードオフは仕様の完全性よりもユーザビリティを優先しており、手書きのパターンよりも安全なデフォルトですが、完全なRFCバリデーターではありません。
メールアドレス検証のRegexはどのようにサービス拒否攻撃を引き起こす可能性がありますか?
重複する文字クラスに対してネストまたは隣接した量指定子を持つRegex(例:([a-zA-Z0-9]+)*の形)は、ほぼマッチする入力に対して指数関数的な時間を要することがあります。これは破滅的なバックトラッキングであり、CWE-1333として分類されています。Node.jsのサーバーサイドで実行すると、RegexエンジンがメインスレッドでI/O処理と同期的に実行されるため、細工された単一のリクエストがイベントループを停止させ、処理中の他のすべてのリクエストをブロックします。解決策はこのパターンクラスを完全に避けることであり、より賢いパターンを書くことではありません。
Regexはメールアドレスが実際に存在するかどうかを確認できますか?
いいえ。Regexは文字列の形式のみを検査します。ドメインが存在するか、MXレコードがあるか、メールボックスがプロビジョニングされているかを確認することはできません。構文的な準拠と配信可能性は独立した性質です。realuser@gmial.comというアドレスは構文的に有効ですが、タイポのため配信不可であり、someone@validformat.testはほとんどのパターンを通過しますが、決して配信されない予約済みTLDを使用しています。アドレスがメールを受信することを証明できるのは、正常に配信された確認メールだけです。
なぜメールRegexはname+filter@gmail.comのような有効なアドレスを弾くのですか?
プラスアドレッシングは完全に有効です。プラス記号はRFC 5322のセクション3.2.3とRFC 5321のセクション4.1.2によれば通常のatextだからです。これを弾くパターンは、.photographyのような長いgTLDのアドレスやRFC 6531で定義された国際化アドレスと同様に、厳格なのではなく間違っています。これらの偽陰性が本番環境に出荷されるのは、テストスイートがtest@example.comを使用しており、これはすべてのパターンを通過するため、実際のアドレスの拒否がテストで表面化しないからです。