PHP でメールアドレスのバリデーションを作る

目次

結論

簡易的には、Filter 関数filter_varネットワーク関数checkdnsrr を利用してバリデーションを実装します。

function checkEmail(string $email): bool {
  $array = explode('@', $email);
  $domain_part = array_pop($array);

  return filter_var($email, FILTER_VALIDATE_EMAIL) !== false &&
    (checkdnsrr($domain_part) || checkdnsrr($domain_part, 'A') || checkdnsrr($domain_part, 'AAAA'));
}

RFC 違反メールアドレスを通す必要がある場合は、正規表現でバリデーションを実装するなどの工夫が必要になります。

注意点として、正規表現で RFC に準拠する完璧なバリデーションを実装しようとすることは、無駄なコストを発生させてしまう可能性があります。サービスの目的に対して何が最善であるか、着手前に開発チームで議論をすると良いと思います。

環境

  1. MAMP 6.8
  2. Apache 2.4.54
  3. PHP 8.2.0

解説

メールアドレスの形式判定

初めに、メールアドレスの技術仕様を理解する必要があります。

RFC

技術仕様は RFC(Request for Comments)に情報があります。

RFC とは、IETF(Internet Engineering Task Force)が公開しているインターネットの技術仕様を纏めた文書群のことです。

今回は IETF が信頼している RFC Editor を利用して技術仕様を参照します。

本件に関わる文書は下記になります。

  1. RFC 822
  2. RFC 3696
  3. RFC 5321
  4. RFC 5322
  5. RFC 6854
  6. RFC 7504

構成

RFC 5321. 2.3.11. Mailbox and Address より、メールアドレスの構成は「local-part@domain」と定義されています。

サイズ

  1. RFC 5321. 4.5.3.1.1. Local-part
  2. RFC 5321. 4.5.3.1.2. Domain
  3. RFC 5321. 4.5.3.1.3. Path

上記の文書より、最大長は下記になります。

  1. local-part : 64 octet
  2. domain : 255 octet(句読点「.」と区切り文字「@」の 2 octet を含む)
  3. local-part@domain : 256 octet(句読点「.」と区切り文字「@」の 2 octet を含む)

1 octet は 8 bit です。

つまり、8 bit を半角 1 文字とすると下記のようになります。

  1. local-part : 最大 64 文字
  2. domain : 最大 255 文字(句読点「.」と区切り文字「@」の 2 文字を含む)
  3. local-part@domain : 最大 256 文字(句読点「.」と区切り文字「@」の 2 文字を含む)

制限

  1. RFC 3696. 2. Restrictions on domain (DNS) names
  2. RFC 3696. 3. Restrictions on email addresses
  3. RFC 5322. 3.2.3. Atom
  4. RFC 5322. 3.4.1. Addr-Spec Specification

上記の文書より、local-part には下記のような制限があります。

  1. ラテン文字の大文字(A ~ Z)と小文字(a ~ z)が使用できる。
  2. 数字(0 ~ 9)が使用できる。
  3. !#$%&'*+-/=?^_`{|}~が使用できる。
  4. 先頭と末尾に . は使用できない。
  5. 2 つ以上の連続した . は使用できない。
  6. " で囲むと 2 つ以上の連続した . を使用できる。
  7. " で囲むと(),:;<>@[]が使用できる。
  8. " で囲むと"\を除く ASCII 印刷可能文字 93 文字(33(10), 35(10) – 91(10), 93(10) – 126(10))が使用できる。
  9. " 内で\の後に ASCII 印刷可能文字 93 文字(33(10), 35(10) – 91(10), 93(10) – 126(10))が使用できる。
  10. " 内で\を前に付けると半角スペース、水平タブ(Horizontal Tabulation)、" \が使用できる。

domain には下記のような制限があります。

  1. ラテン文字の大文字(A ~ Z)と小文字(a ~ z)が使用できる。
  2. 数字(0 ~ 9)が使用できる。
  3. -(先頭と末尾以外)が使用できる。
  4. TLD(Top Level Domain)を全て数字にして使用できない。
  5. [ ] で囲むと IP アドレス が使用できる。
  6. DNS レコードに名前解決される FQDN が使用できる。

1 ~ 3 は、所謂 LDH(Letters, Digits, Hyphen)rule です。

実装

Filter 関数filter_var を利用します。

第二引数の 検証フィルタ には FILTER_VALIDATE_EMAIL を指定します。

FILTER_VALIDATE_EMAIL は RFC 822 に準拠しているメールアドレスか否かを検証します。

function checkEmail(string $email): bool {
  return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}

下記 3 点は非対応です。

  1. コメント
  2. FWS
  3. Dotless Domain Names

FWS については RFC 5322. 2.2.3. Long Header Fields に情報があります。

PCRE 正規表現構文 による実装方法の解説に関しましては、「場合による」ので割愛させて頂きます。

DNS レコードの有無判定

DNS レコードとは、権威 DNS サーバー(Authoritative Name Server)が持っている Zone file の各行のことです。

実装

ネットワーク関数checkdnsrr を利用します。

第二引数の DNS レコードの種類には、MX, A, AAAA を指定します。又、第二引数の既定値は MX です。

これで、メール送信の可不可を確認することができます。

function checkEmail(string $email): bool {
  $array = explode('@', $email);
  $domain_part = array_pop($array);

  return filter_var($email, FILTER_VALIDATE_EMAIL) !== false &&
    (checkdnsrr($domain_part) || checkdnsrr($domain_part, 'A') || checkdnsrr($domain_part, 'AAAA'));
}

因みに、A レコードと AAAA レコードを指定している理由は、RFC 5321 5.1. Locating the Target Host の記述より、MX レコードが存在しない場合、A レコードと AAAA レコードに問い合わせる仕様になっているからです。

上記の仕様に対して、RFC 7505 A “Null MX” No Service Resource Record for Domains That Accept No Mail が発行されておりますが、Null MX の解説については本題から逸れてしまうので割愛させて頂きます。

検証

検証用の簡単なプログラムを書きました。

<?php
$email = $_POST['email'] ?? '';

if ($email) {

  if (checkEmail($email)) echo '<p>メールアドレスの入力チェック結果 : OK</p>';
  else echo '<p>メールアドレスの入力チェック結果 : NG</p>';
}

function checkEmail(string $email): bool {
  $array = explode('@', $email);
  $domain_part = array_pop($array);

  return filter_var($email, FILTER_VALIDATE_EMAIL) !== false &&
    (checkdnsrr($domain_part) || checkdnsrr($domain_part, 'A') || checkdnsrr($domain_part, 'AAAA'));
}

?>

<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
</head>
<body>

<hr>
<form method="post">
  <p>
    メールアドレス:<input type="text" name="email">
  </p>
  <input type="submit" value="送信する">
</form>

</body>
</html>

下記の文字列とマッチします。

Localpart@example.com
Localpart.123@example.com
local+part/Local=part@example.com
Localpart@www.example.com
{Localpart}@example.com
!#$%&'*+-/=?^_`.{|}~@example.com
"Local@part"@example.com
"Local\ Part"@example.com
"Local.\\Part"@example.com
":Localpart"@example.com
123@example.com
Localpart@xn--wgv71a119e.com

下記の文字列とマッチしません。

Localpart.@example.com
Localpart..123@example.com
localpart@com
(Localpart)@example.com
"lo cal p@rt!"@example.com
"lo.cal part"@example.com
"()<>[]:,;@\\"\\\\!#$%&\'*+-/=?^_`{}| ~.a"@example.com
" "@example.com
Localpart@日本語.com
Localpart@🚑.com

又、下記 2 点にご注意下さい。

  1. IDN(Internationalized Domain Name)を含む有効なメールアドレスにマッチしない。
  2. domain-part が Punycode にエンコードされたアドレスにはマッチする。

以上です。

参考

  1. PHP マニュアル
  2. RFC Editor
目次