メールアドレスのバリデーションはなぜ難しいのか

メールアドレスの正規表現バリデーションは、一見簡単そうに見えて実は非常に奥が深いテーマです。RFC 5321/5322で定義されたメールアドレスの仕様は複雑で、完全準拠の正規表現は数千文字にもなります。

メールアドレスの構造

local-part @ domain
部分説明
ローカルパート@ の前の部分user, first.last, user+tag
@区切り文字@
ドメインパート@ の後の部分example.com, sub.domain.co.jp

意外と知らない有効なメールアドレス

RFC 5321/5322によると、以下のメールアドレスはすべて有効です。

アドレス有効/無効備考
[email protected]有効一般的な形式
[email protected]有効ドット入り
[email protected]有効プラスタグ(Gmail等で使用)
[email protected]有効ハイフン入り
[email protected]有効アンダースコア始まり
[email protected]有効サブドメイン
"user name"@example.com有効クォート内なら空白OK
[email protected]有効IPアドレス
user@[IPv6:2001:db8::1]有効IPv6アドレス

実用的な正規表現パターン

レベル1: 最もシンプル(最低限)

.+@.+\..+

マッチ例: [email protected], [email protected]

メリットデメリット
シンプル不正なアドレスも多数通過
false negativeが少ない@.. なども通過

レベル2: 実用的(おすすめ)

^[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])?)*$

これはHTML5の<input type="email">で使われているパターンに近い正規表現です。

メリットデメリット
ほとんどの不正アドレスを検出クォート内の特殊文字は非対応
実用上十分な精度IPアドレス形式のドメインは非対応

レベル3: より厳密

^(?:[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])+)\])$

RFC 5322に準拠したパターンですが、非常に複雑でメンテナンスが困難です。

おすすめ: レベル2 + サーバーサイド検証

実務ではレベル2の正規表現でフロントバリデーション + サーバーサイドで確認メール送信が最もバランスの良いアプローチです。

各プログラミング言語での実装

JavaScript

function isValidEmail(email) {
  const pattern = /^[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])?)*$/;
  return pattern.test(email);
}

// テスト
console.log(isValidEmail("[email protected]"));   // true
console.log(isValidEmail("invalid@"));            // false
console.log(isValidEmail("[email protected]"));  // true

HTML5のネイティブバリデーション

<input type="email" required>

ブラウザ内蔵のバリデーションが適用されます。JavaScriptの正規表現と組み合わせるのが効果的です。

Python

import re

def is_valid_email(email):
    pattern = r'^[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])?)*$'
    return re.match(pattern, email) is not None

# テスト
print(is_valid_email("[email protected]"))    # True
print(is_valid_email("invalid"))             # False

PHP

// filter_var を使う方法(推奨)
$email = "[email protected]";
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
    echo "有効なメールアドレス";
} else {
    echo "無効なメールアドレス";
}

PHPではfilter_var()を使うのがベストプラクティスです。自前の正規表現よりも正確で安全です。

Go

import "net/mail"

func isValidEmail(email string) bool {
    _, err := mail.ParseAddress(email)
    return err == nil
}

Goでは標準ライブラリのnet/mailパッケージを使うのが推奨です。

バリデーションのベストプラクティス

1. 正規表現だけに頼らない

正規表現はフロントエンドでの簡易チェックに使い、確実な検証は確認メールの送信で行いましょう。

2. ゆるくチェックする

厳密すぎる正規表現は、有効なメールアドレスを弾いてしまうリスクがあります。

# 悪い例: TLDを限定
user@example\.(?:com|net|org)

# 良い例: TLDを限定しない
user@example\.[a-zA-Z]{2,}

3. 大文字小文字を区別しない

メールアドレスのドメイン部分は大文字小文字を区別しません。ローカルパートも多くのメールサーバーで区別しません。

// 比較時は小文字に正規化
const normalizedEmail = email.toLowerCase().trim();

4. 空白をトリムする

ユーザーがコピペした際に前後に空白が入ることがあるため、必ずトリムしましょう。

5. 使い捨てメールアドレスの対策

使い捨てメールサービス(Guerrilla Mail、Tempmail等)のドメインをブロックリストで管理する方法もあります。ただし、常に新しいドメインが追加されるため、完全なブロックは困難です。

よくある間違い

間違い1: ドットの連続を許可しない

# 誤: ドットの連続を禁止(一部のメールアドレスを弾く)
^[^.][a-zA-Z0-9.]+[^.]@

RFC上、ローカルパートの先頭・末尾のドットは無効ですが、一部のメールサーバー(古いシステム)では許容されています。

間違い2: TLDの長さを制限する

# 誤: TLDを2-4文字に制限
@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$

# 正: TLDの長さを制限しない(.museum、.technology等がある)
@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$

間違い3: プラス記号を弾く

[email protected] のようなプラスタグは有効なメールアドレスです。Gmailやその他多くのメールサービスがサポートしています。

セキュリティ上の注意

ReDoS(正規表現サービス拒否攻撃)

複雑な正規表現は、特定の入力に対して処理時間が指数関数的に増大する「バックトラッキング爆発」を引き起こす可能性があります。

# ReDoS脆弱性のある正規表現の例
^([a-zA-Z0-9]+)*@example.com$

対策:

  • 入力の長さ制限を設ける(メールアドレスは254文字以下)
  • 可能であれば非バックトラッキング型のエンジン(RE2等)を使う
  • 正規表現を使わずライブラリでバリデーションする

よくある質問

Q: 正規表現だけでメールの存在を確認できる?

いいえ。正規表現はフォーマットの確認のみです。メールアドレスが実際に存在するかは、確認メール送信(double opt-in)で検証する必要があります。

Q: 国際化メールアドレス(IDN)は?

user@例え.jp のような国際化ドメイン名(IDN)や、用户@example.com のようなUTF-8ローカルパートはRFC 6531で定義されていますが、対応はまだ限定的です。

Q: HTMLの type=“email” だけで十分?

フロントエンドのバリデーションは簡単にバイパスできるため、サーバーサイドでも必ずバリデーションを行ってください。

まとめ

メールアドレスバリデーションの推奨アプローチ:

  1. フロントエンド: HTML5 type="email" + レベル2の正規表現
  2. サーバーサイド: 言語標準のバリデーション関数(PHP filter_var 等)
  3. 最終確認: 確認メール送信(double opt-in)

正規表現だけで完璧なバリデーションを目指すのではなく、多層的なアプローチで確実に検証しましょう。


正規表現のテスト・デバッグには Assistyの正規表現テストツール をご活用ください。パターンの入力とテスト文字列をリアルタイムで検証できます。