メールアドレスからサブドメインを抽出するには

PHPでさっくりできそうに思いますが、実はあまり簡単にいかない問題です。

順番に考えていきましょう。

 1. ローカルパート分割の問題
 2. ドメイン名分割の問題

## メールアドレスの仕様

メールアドレスの仕様については[RFC 5322 & 5321に沿ったメールアドレス（local-part）に使える文字まとめ - Qiita](https://qiita.com/yoshitake_1201/items/40268332cd23f67c504c)が詳しかったので参照してください。又聞きの情報ではなく[RFC 5322 - Internet Message Format 日本語訳](https://tex2e.github.io/rfc-translater/html/rfc5322.html)を読み込んでみるのも良いでしょう。

https://qiita.com/yoshitake_1201/items/40268332cd23f67c504c

メールアドレス検証のために使えるPHP組み込み機能は[`filter_var()`](https://www.php.net/manual/ja/function.filter-var.php)と`FILTER_VALIDATE_EMAIL`([PHP: 検証フィルタ - Manual](https://www.php.net/manual/ja/filter.filters.validate.php))です。

https://www.php.net/manual/ja/function.filter-var.php

```php
$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]。

[^1]: 私の知る限り、まさにdocomoがそうでした。是正されていると嬉しいのですが…

そのようなややこしい状況に関してメールアドレスをデータベースにどのように保持すべきかというのは大変に興味深い問題ではありますが、ここでは「**quoted-stringも受け入れる**」として話を進めましょう。

### ローカルパートの分割

問題は、`hoge@example.com`のようなメールアドレスを `['local' => 'hoge', 'domain' => 'example.com']` に分けるのは簡単か、ということです。

雑に思い浮かぶのは以下のようなコードでしょう。

```php
[$local, $domain] = explode('@', $email_address, 2);
```

これはメールアドレスにquoted-stringを許可しない場合には期待通りに動作するでしょう。しかし、許可するならそうはいきません。

以下のメールアドレスは仕様としてすべて正当です。

 * `""@example.com`
 * `"\ "@example.com`
 * `"@"@example.com`
 * `"(@_@)"@example.com`

```php
$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` を取り出したい、といった感じでしょう。

どうしてこの処理が難しいのかという問題は、徳丸先生の記事に非常に詳しいです。

https://blog.tokumaru.org/2013/08/machidakanagawajp.html

ざっくり言うと `city.machida.kanagawa.jp`のサブドメインは`city`ですが、`city.machida.tokyo.jp`にはサブドメインは存在せず、`www.city.machida.tokyo.jp`のサブドメインは`www.city`ではなく`www`です。

実はこの構造はMozilla Firefoxを使っていればアドレスバーで判別できます。

<ul><li><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4121/00ab487e-be33-9199-e61e-64edf005c3b0.png"></li><li><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4121/85a4a0ca-9871-e3f3-a16d-44766a317e1f.png"></li><li><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4121/f72d4258-0999-e635-c790-e22b5322eede.png"></li></ul>

細かすぎて伝わりにくいかと思いますが、サブドメインの抽出は簡易的な処理では困難であることをおわかりいただけたかと思います。

というわけで、このようなドメインのルールは https://publicsuffix.org/list/ で管理されており、PHPの良い実装もあるので、ライブラリを使ってください。

https://github.com/jeremykendall/php-domain-parser

![スクリーンショット 2021-09-08 3.22.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4121/9fe4a37d-0ba2-0c03-859b-194370960c7d.png)

上記の説明通りに分解できてますね。やったあ。サブドメインを除いた部分は`$result->registrableDomain()->toString()`で取れています。

## まとめ

一見して初心者でも簡単に書けそうな処理に見えても意外にそんなことはない。

## おまけ

この記事の内容と直接関係ないのですが、Public Suffix Listという仕組みの運用について興味深い問題が起こっているので、せっかくなので紹介のみしておきます。読んでください。

https://blog.jxck.io/entries/2021-04-21/public-suffix-list.html

正規表現でメールアドレスを処理することの困難さは小飼さんの2009年の記事に詳しいです。

http://blog.livedoor.jp/dankogai/archives/51189905.html

僕は正規表現の大好きなPHPerなので、みなさまもご安全に…