PHPでさっくりできそうに思いますが、実はあまり簡単にいかない問題です。
順番に考えていきましょう。
- ローカルパート分割の問題
- ドメイン名分割の問題
メールアドレスの仕様
メールアドレスの仕様についてはRFC 5322 & 5321に沿ったメールアドレス(local-part)に使える文字まとめ - Qiitaが詳しかったので参照してください。又聞きの情報ではなくRFC 5322 - Internet Message Format 日本語訳を読み込んでみるのも良いでしょう。
メールアドレス検証のために使えるPHP組み込み機能はfilter_var()
とFILTER_VALIDATE_EMAIL
(PHP: 検証フィルタ - Manual)です。
$email_addr = 'a@b.com';
$filtered = filter_var($email_addr, FILTER_VALIDATE_EMAIL);
if (empty($filtered)) {
return null;
}
メールアドレスのローカルパートにUnicode(UTF-8)を許可するときは、filter_var()
の引数にフラグとしてFILTER_FLAG_EMAIL_UNICODE
を追加してください。
RFCの仕様にそのまま準拠すべきか、RFCに準拠したメールアドレスをすべて受け入れるべきかというのは多少悩ましい問題ですし、サービスごとの裁量でRFCよりも厳しい基準を設けるもの現実的な判断です。現に私が開発しているWebサービスもそうです。
たとえば、かつてdocomoで発行可能だった hogehoge..@example.com
のようなRFC違反のメールアドレスはよく知られています。また、quoted-string形式のメールアドレス表現を受け入れるべきかといった問題も悩ましいものがあります。abc@example.com
と"abc"@example.com
は同じメールアドレスを表しますが、メールアドレスでログインさせるよくある会員制のWebサイトでquoteのあるなしで別のアカウントを登録可能とあっては、ユーザーも、おそらく運営側としても混乱を招きます。また、残念なことにabc@example.com
を"abc"@example.com
と冗長にquoteすることで正常に受け入れてくれないMTAもあります1。
そのようなややこしい状況に関してメールアドレスをデータベースにどのように保持すべきかというのは大変に興味深い問題ではありますが、ここでは「quoted-stringも受け入れる」として話を進めましょう。
ローカルパートの分割
問題は、hoge@example.com
のようなメールアドレスを ['local' => 'hoge', 'domain' => 'example.com']
に分けるのは簡単か、ということです。
雑に思い浮かぶのは以下のようなコードでしょう。
[$local, $domain] = explode('@', $email_address, 2);
これはメールアドレスにquoted-stringを許可しない場合には期待通りに動作するでしょう。しかし、許可するならそうはいきません。
以下のメールアドレスは仕様としてすべて正当です。
""@example.com
"\ "@example.com
"@"@example.com
"(@_@)"@example.com
$email_addr = '"(@_@)"@example.com';
$filtered = filter_var($email_addr, FILTER_VALIDATE_EMAIL);
if (empty($filtered)) {
return null;
}
if (!preg_match('/\A(?<local>.+)@(?<domain>[^@]+)\z/u', $filtered, $matches)) {
return null;
// または例外を投げる
}
var_dump($matches['local'], $matches['domain']);
// string(7) ""(@_@)""
// string(11) "example.com"
めでたしめでたし(?)ですね。この正規表現はパートの分割のみを目的としているので、必ず正当なメールアドレスチェックとセットで使わないと脆弱性の原因になります。
ドメイン名の問題
ローカルパートの部分が解決したところでドメイン名の問題にとりかかりましょう。世の中にはドメイン名からサブドメインを取り除きたいという需要があるそうです。
ぱっと思いつくのは @mail.example.ac.jp
と @stu.example.ac.jp
と @example.ac.jp
のようなメールアドレスから大学のドメイン名 example.ac.jp
を取り出したい、といった感じでしょう。
どうしてこの処理が難しいのかという問題は、徳丸先生の記事に非常に詳しいです。
ざっくり言うと city.machida.kanagawa.jp
のサブドメインはcity
ですが、city.machida.tokyo.jp
にはサブドメインは存在せず、www.city.machida.tokyo.jp
のサブドメインはwww.city
ではなくwww
です。
実はこの構造はMozilla Firefoxを使っていればアドレスバーで判別できます。
細かすぎて伝わりにくいかと思いますが、サブドメインの抽出は簡易的な処理では困難であることをおわかりいただけたかと思います。
というわけで、このようなドメインのルールは https://publicsuffix.org/list/ で管理されており、PHPの良い実装もあるので、ライブラリを使ってください。
上記の説明通りに分解できてますね。やったあ。サブドメインを除いた部分は$result->registrableDomain()->toString()
で取れています。
まとめ
一見して初心者でも簡単に書けそうな処理に見えても意外にそんなことはない。
おまけ
この記事の内容と直接関係ないのですが、Public Suffix Listという仕組みの運用について興味深い問題が起こっているので、せっかくなので紹介のみしておきます。読んでください。
正規表現でメールアドレスを処理することの困難さは小飼さんの2009年の記事に詳しいです。
僕は正規表現の大好きなPHPerなので、みなさまもご安全に…
-
私の知る限り、まさにdocomoがそうでした。是正されていると嬉しいのですが… ↩