概要
メールアドレスのバリデーションは、一見すると正規表現ひとつで片付きそうに見えます。しかし実務で扱うと、RFC 5321 の仕様が想像以上に広く、コメント付きアドレスや引用符で囲まれたローカル部など「形式としては正しいが実運用では使われない」パターンが大量に存在します。InternetAddress.validate() は Jakarta Mail 依存であり、しかもコメント付きアドレスを許容するため、そのまま使うとユーザー入力のバリデーションとしては緩すぎるケースがあります。逆に、正規表現を厳密にしすぎると「+」付きアドレスや新しい TLD を弾いてしまい、正当なユーザーの登録を妨げるという問題も起こります。この記事では、形式チェック、ドメイン部の構造検証、MX レコードの存在確認という3段階のアプローチで、実務に適したバリデーションを Pure Java で組み立てます。
使いどころ
ユーザー登録フォームでメールアドレスの形式と到達可能性を事前チェックする
CSV インポート時にメールアドレス列のデータクレンジングと不正値の検出を行う
メール配信システムで送信前にバウンスリスクの高いアドレスをフィルタリングする
コード例
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class EmailValidator {
// バリデーション結果
record ValidationResult(boolean valid, List<String> errors) {
static ValidationResult ok() {
return new ValidationResult(true, List.of());
}
static ValidationResult of(List<String> errors) {
return new ValidationResult(
errors.isEmpty(), List.copyOf(errors));
}
}
// RFC 5321 の実用的な範囲をカバーする正規表現
// 「+」付きエイリアス、ハイフン付きドメインを許容
private static final Pattern EMAIL_PATTERN =
Pattern.compile(
"^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9]"
+ "([a-zA-Z0-9\\-]*[a-zA-Z0-9])?"
+ "(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]*"
+ "[a-zA-Z0-9])?)*\\.[a-zA-Z]{2,}$");
private static final int MAX_LOCAL_LENGTH = 64;
private static final int MAX_DOMAIN_LENGTH = 253;
private static final int MAX_TOTAL_LENGTH = 254;
/** 第1段階: 形式チェック */
public static List<String> checkFormat(String email) {
var errors = new ArrayList<String>();
if (email == null || email.isBlank()) {
errors.add("メールアドレスは必須です");
return errors;
}
if (email.length() > MAX_TOTAL_LENGTH) {
errors.add("メールアドレスは"
+ MAX_TOTAL_LENGTH + "文字以内です");
}
var atIndex = email.indexOf('@');
if (atIndex < 0) {
errors.add("@ が含まれていません");
return errors;
}
var local = email.substring(0, atIndex);
var domain = email.substring(atIndex + 1);
if (local.length() > MAX_LOCAL_LENGTH) {
errors.add("ローカル部は"
+ MAX_LOCAL_LENGTH + "文字以内です");
}
if (domain.length() > MAX_DOMAIN_LENGTH) {
errors.add("ドメイン部は"
+ MAX_DOMAIN_LENGTH + "文字以内です");
}
if (!EMAIL_PATTERN.matcher(email).matches()) {
errors.add("メールアドレスの形式が不正です");
}
return errors;
}
/** 第2段階: ドメインの DNS 解決チェック */
public static List<String> checkDomain(String email) {
var errors = checkFormat(email);
if (!errors.isEmpty()) {
return errors;
}
var domain = email.substring(
email.indexOf('@') + 1);
try {
InetAddress.getByName(domain);
} catch (UnknownHostException e) {
errors.add("ドメイン " + domain
+ " が解決できません");
}
return errors;
}
/** 第3段階: MX レコードの存在確認 */
public static List<String> checkMxRecord(
String email) {
var errors = checkDomain(email);
if (!errors.isEmpty()) {
return errors;
}
var domain = email.substring(
email.indexOf('@') + 1);
try {
// JNDI で MX レコードを照会
var env = new java.util.Hashtable<
String, String>();
env.put("java.naming.factory.initial",
"com.sun.jndi.dns.DnsContextFactory");
var ctx = new javax.naming.directory
.InitialDirContext(env);
var attrs = ctx.getAttributes(
domain, new String[]{"MX"});
var mx = attrs.get("MX");
if (mx == null || mx.size() == 0) {
errors.add("ドメイン " + domain
+ " に MX レコードがありません");
}
ctx.close();
} catch (javax.naming.NamingException e) {
errors.add("MX レコードの照会に失敗: "
+ e.getMessage());
}
return errors;
}
/** 全段階を実行して結果を返す */
public static ValidationResult validate(
String email) {
return ValidationResult.of(checkMxRecord(email));
}
public static void main(String[] args) {
// 形式チェックのみ
System.out.println("=== 形式チェック ===");
var r1 = checkFormat("[email protected]");
System.out.println("[email protected] -> "
+ (r1.isEmpty() ? "OK" : r1));
var r2 = checkFormat("invalid@@example");
System.out.println("invalid@@example -> " + r2);
// 全段階
System.out.println("\n=== 全段階バリデーション ===");
var result = validate("[email protected]");
if (result.valid()) {
System.out.println("バリデーション OK");
} else {
result.errors().forEach(e ->
System.out.println(" - " + e));
}
}
}Version Coverage
var による型推論で記述量が減る。record でバリデーション結果を構造化でき、正規表現パターンの可読性も向上する。
// Java 17: var + record で結果を構造化
record EmailCheckResult(boolean valid, String reason) {}
var pattern = Pattern.compile(
"^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$");
var result = pattern.matcher(email).matches()
? new EmailCheckResult(true, "")
: new EmailCheckResult(false, "形式が不正です");Library Comparison
注意点
InternetAddress.validate() はコメント付きアドレス(例: user(comment)@example.com)を許容するため、ユーザー入力のバリデーションには不向きな場合がある。strict モードでも RFC 822 準拠の範囲で通過する。
正規表現で「+」記号を弾くと、Gmail のエイリアス機能([email protected])を使っているユーザーが登録できなくなる。意図的に除外する場合は仕様として明記すること。
MX レコードの DNS ルックアップはネットワーク I/O を伴うため、大量のアドレスを一括検証する場合はタイムアウトとスロットリングを設ける必要がある。
国際化ドメイン名(IDN)は Punycode に変換してから検証する必要がある。java.net.IDN.toASCII() で変換できるが、変換失敗時の例外処理を忘れないこと。
FAQ
Gmail のエイリアス機能で広く使われているため、通常は許可すべきです。正規表現のローカル部に「+」を含めておけば対応できます。
java.net.IDN.toASCII() で Punycode に変換してから通常のドメイン検証を行います。変換に失敗した場合はドメインとして不正と判断できます。
形式チェックは「明らかに不正なものを弾く」程度にとどめ、到達確認は実際にメールを送って確認するのが確実です。厳密すぎると正当なアドレスを弾くリスクがあります。