結論
簡易的には、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 に準拠する完璧なバリデーションを実装しようとすることは、無駄なコストを発生させてしまう可能性があります。サービスの目的に対して何が最善であるか、着手前に開発チームで議論をすると良いと思います。
環境
- MAMP 6.8
- Apache 2.4.54
- PHP 8.2.0
解説
メールアドレスの形式判定
初めに、メールアドレスの技術仕様を理解する必要があります。
RFC
技術仕様は RFC(Request for Comments)に情報があります。
RFC とは、IETF(Internet Engineering Task Force)が公開しているインターネットの技術仕様を纏めた文書群のことです。
今回は IETF が信頼している RFC Editor を利用して技術仕様を参照します。
本件に関わる文書は下記になります。
構成
RFC 5321. 2.3.11. Mailbox and Address より、メールアドレスの構成は「local-part@domain」と定義されています。
サイズ
- RFC 5321. 4.5.3.1.1. Local-part
- RFC 5321. 4.5.3.1.2. Domain
- RFC 5321. 4.5.3.1.3. Path
上記の文書より、最大長は下記になります。
- local-part : 64 octet
- domain : 255 octet(句読点「.」と区切り文字「@」の 2 octet を含む)
- local-part@domain : 256 octet(句読点「.」と区切り文字「@」の 2 octet を含む)
1 octet は 8 bit です。
つまり、8 bit を半角 1 文字とすると下記のようになります。
- local-part : 最大 64 文字
- domain : 最大 255 文字(句読点「.」と区切り文字「@」の 2 文字を含む)
- local-part@domain : 最大 256 文字(句読点「.」と区切り文字「@」の 2 文字を含む)
制限
- RFC 3696. 2. Restrictions on domain (DNS) names
- RFC 3696. 3. Restrictions on email addresses
- RFC 5322. 3.2.3. Atom
- RFC 5322. 3.4.1. Addr-Spec Specification
上記の文書より、local-part には下記のような制限があります。
- ラテン文字の大文字(A ~ Z)と小文字(a ~ z)が使用できる。
- 数字(0 ~ 9)が使用できる。
!
#
$
%
&
'
*
+
-
/
=
?
^
_
`
{
|
}
~
が使用できる。- 先頭と末尾に
.
は使用できない。 - 2 つ以上の連続した
.
は使用できない。 "
で囲むと 2 つ以上の連続した.
を使用できる。"
で囲むと(
)
,
:
;
<
>
@
[
]
が使用できる。"
で囲むと"
と\
を除く ASCII 印刷可能文字 93 文字(33(10), 35(10) – 91(10), 93(10) – 126(10))が使用できる。"
内で\
の後に ASCII 印刷可能文字 93 文字(33(10), 35(10) – 91(10), 93(10) – 126(10))が使用できる。"
内で\
を前に付けると半角スペース、水平タブ(Horizontal Tabulation)、"
\
が使用できる。
domain には下記のような制限があります。
- ラテン文字の大文字(A ~ Z)と小文字(a ~ z)が使用できる。
- 数字(0 ~ 9)が使用できる。
-
(先頭と末尾以外)が使用できる。- TLD(Top Level Domain)を全て数字にして使用できない。
[
]
で囲むと IP アドレス が使用できる。- 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 点は非対応です。
- コメント
- FWS
- 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 点にご注意下さい。
- IDN(Internationalized Domain Name)を含む有効なメールアドレスにマッチしない。
- domain-part が Punycode にエンコードされたアドレスにはマッチする。
以上です。