Djangoでメールアドレスを検証するには、1つではなく3つのチェックが必要ですが、フレームワークが提供するのは最初の1つだけです。Djangoのバリデーターが確認するのはアドレスの形式が正しいかどうかで、そのドメインがメールを受信できるか、メールボックスが存在するかは調べません。本格的な検証は、構文チェック、DNSルックアップ、SMTPによるメールボックスのプローブという順に重ねていくものです。このガイドでは、各レイヤーを、フォームやDjango REST Frameworkのシリアライザーに組み込めるDjangoバリデーターとして実装し、検証APIに処理を引き継ぐ箇所も示します。内容はPythonガイドの続きにあたるため、そちらで説明したDNSとSMTPの考え方はそのまま当てはまります。
結論#
構文チェックにはvalidate_email(または任意のEmailField)、MXルックアップにはdnspython、SMTPによるメールボックスの確認には検証APIを使います。これを3つのバリデーターとして組み込み、コストの低いものから順に実行して、いずれかで結論が出た時点で打ち切ります。DjangoのプロセスからSMTP接続を開いてメールボックスを調べるのは避けてください。ほとんどのホスティング環境でアウトバウンドのポート25がブロックされており、結果は送信元IPのレピュテーションやグレーリスティングに左右されるため、自前で再現するのは割に合いません。各レイヤーは、後のレイヤーより低いコストでアドレスを除外できます。アドレスを有効と判断できるのはAPIだけです。この流れに馴染みがない場合は、先にメールアドレス検証とはで用語を確認してください。
レイヤー1:Djangoのバリデーターによる形式チェック#
レイヤー1は、すでにDjangoが備えています。django.core.validators.validate_emailはEmailValidatorのインスタンスで、不正な形式のアドレスにはValidationErrorを送出し、正しいアドレスにはNoneを返します。
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
def is_valid_syntax(email: str) -> bool:
try:
validate_email(email)
return True
except ValidationError:
return False
手動で呼び出すことはほとんどありません。forms.EmailField、models.EmailField、DRFのserializers.EmailFieldには、いずれもEmailValidatorが自動で付与されるため、フィールドを宣言するだけで構文チェックが手に入ります。
from django import forms
class SignupForm(forms.Form):
email = forms.EmailField() # EmailValidator runs during clean()
これで何が得られるのかを正確に把握しておきましょう。EmailValidatorはRFC由来の文法に照らして形式を確認するだけです。DNSを解決せず、ソケットも開かず、example.comが存在するかどうかも知りません。definitely-fake@gmail.comは通過します。info@company-that-folded.comも通過します。typo@gmial.comも通過します。3つとも配信できないアドレスですが、文字列を読むだけのバリデーターには判別できません。これは正規表現によるメールアドレス検証が失敗するのと同じ理由で、構文と到達性は別の問いだからです。前者は文字列についての事実、後者はインターネットについての事実です。
レイヤー2:ドメインはメールを受け付けるか#
Djangoが用意していない最初のレイヤーですが、追加するのは簡単です。MXレコードのないドメインは、誰宛てのメールも受け付けられません。そのため、DNSルックアップを1回行うだけで、存在しないドメイン、会社名のつづり間違い、架空のTLDを除外できます。DjangoにはMXを解決する標準機能がないので、dnspythonを使い、単純な関数として包みます。
import dns.resolver # pip install dnspython
def has_mail_route(domain: str) -> bool:
try:
return len(dns.resolver.resolve(domain, "MX")) > 0
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers):
return False
Djangoのバリデーターは、不正な入力に対してValidationErrorを送出する呼び出し可能オブジェクトにすぎません。このチェックをバリデーターに格上げすれば、任意のフィールドのvalidatorsリストにそのまま差し込めます。
from django.core.exceptions import ValidationError
def validate_deliverable_domain(email: str) -> None:
domain = email.rsplit("@", 1)[-1]
if not has_mail_route(domain):
raise ValidationError("This domain can't receive email.")
validate_deliverable_domain("jane@gmail.com") # passes
validate_deliverable_domain("jane@company-that-folded.com") # raises ValidationError
MXがなくAレコードでメールを受け付けるドメインにも対応したい場合は、MXの結果が空のときにA/AAAAの解決にフォールバックしてください。ただし、実在するアドレスの大多数はMXチェックでカバーできます。
レイヤー3:メールボックスは本当に存在するか#
レイヤー1と2でできるのは、アドレスの除外だけです。ドメインに正しいMXレコードが公開されていても、手元のアドレスにメールボックスがあるとは限りません。noreply-9f2x@gmail.comは、メール経路のあるドメイン上の有効な構文ですが、作成されたことのないメールボックスです。特定のメールボックスを確認するには、SMTPの配信のやり取りが必要です。メールホストに接続してRCPT TOを発行し、応答を読み取り、何も送信せずに切断します。
原則として、smtplibを使えばDjangoからもスクリプトで実装できます。しかし実際には、アプリケーションサーバーから実行すべきではありません。ほとんどのクラウドホストはアウトバウンドのポート25をブロックしており、結果は接続元のIPのレピュテーションに左右され、受信サーバーは見慣れない送信者をグレーリスティングやレート制限の対象にします。そのため、ローカルのテストで動いたプローブが、本番では黙って失敗するか、ブラックリスト入りを招きます。メールアドレス検証の仕組みでは、キャッチオールドメインを含む一連の流れを詳しく解説しています。このレイヤーは、外部に任せる価値があります。
Qualisendの検証エンドポイントは、構文チェック、DNS、SMTPによるメールボックスのプローブという一連の処理を、レピュテーションを管理したインフラから実行し、判定結果を返します。バリデーターからrequestsで呼び出し、キーは環境変数から読み取ります。
import os
import requests # pip install requests
QUALISEND_API_URL = "https://app.qualisend.com/api/v1"
def verify(email: str) -> dict | None:
try:
res = requests.post(
f"{QUALISEND_API_URL}/verify",
headers={"Authorization": f"Bearer {os.environ['QUALISEND_API_KEY']}"},
json={"email": email},
timeout=10,
)
except requests.RequestException:
return None # timeout or network error: fail open
if res.status_code in (402, 429) or res.status_code >= 500:
return None # out of credits, rate-limited or down: fail open
res.raise_for_status()
return res.json() # {"job_id", "result": {"status", "score", "smtp", "sub_flags", ...}}
レスポンスは{ "job_id": ..., "result": { ... } }というエンベロープです。result["status"](deliverable、risky、undeliverable、unknownのいずれか)を読み取ります。あわせて、score、reason、そして役割アドレスや使い捨てアドレスといった特性を示すsub_flagsも返されます。正確なフィールド構造はAPIリファレンスをご確認ください。
この最初のstatusは即座に返ります。result["smtp"]が"pending"の場合、SMTPによるメールボックスのプローブはまだ実行中で、最終的な判定はGET /jobs/{job_id}に届きます。"cached"または"skipped"なら、結果はすでに確定しています。そのため、即時のundeliverableはフォームでエラーとして送出し、保留中のチェックはバックグラウンドタスクに渡します。
def validate_mailbox(email: str) -> None:
data = verify(email)
if data is None:
return # API unavailable: let the signup through, re-verify later
if data["result"]["status"] == "undeliverable":
raise ValidationError("We couldn't confirm a mailbox at this address.")
if data["result"]["smtp"] == "pending":
recheck_mailbox.delay(data["job_id"]) # final verdict, off the request path
このタスクは、リクエスト処理とは別に、プローブが完了するまでジョブをポーリングします。Celeryを使った例は次のとおりです(RQやDjango-Qでも同じ要領です)。
from celery import shared_task # pip install celery
@shared_task(bind=True, max_retries=180) # every 5 s, for up to 15 minutes
def recheck_mailbox(self, job_id: str) -> None:
res = requests.get(
f"{QUALISEND_API_URL}/jobs/{job_id}",
headers={"Authorization": f"Bearer {os.environ['QUALISEND_API_KEY']}"},
params={"include": "results"},
timeout=10,
)
res.raise_for_status()
job = res.json()
if job["status"] not in ("completed", "canceled", "failed"):
raise self.retry(countdown=5) # the SMTP probe is still running
for row in job.get("results", []):
if row["status"] == "undeliverable":
flag_undeliverable_signup(row["email"]) # your code: mark the account, hold sends
riskyでもエラーとして送出する、あるいはscoreを保存して通過させる、という選択もありえます。どちらにするかは運用方針の判断です。undeliverableを拒否するのが安全なデフォルトで、収集の時点でどこまで厳しくするかはサーバーレスのサインアップガイドで解説しています。
3つのレイヤーを組み合わせてDjangoでメールアドレスを検証する#
フォームで3つのレイヤーを組み合わせる場所としては、clean_<field>メソッドが定石です。Djangoがclean_emailを呼び出すのは、フィールド自身のEmailValidatorを通過した後だけなので、構文チェックはすでに済んでいます。そこへドメインとメールボックスのチェックを順番に加え、それぞれ早い段階で例外を送出します。
from django import forms
class SignupForm(forms.Form):
email = forms.EmailField() # layer 1 for free
def clean_email(self):
email = self.cleaned_data["email"] # syntax already passed
validate_deliverable_domain(email) # layer 2 — cheap, local
validate_mailbox(email) # layer 3 — the API round-trip
return email
この順序こそが肝です。フィールドのEmailValidatorが、コードの実行前に不正な入力を打ち切ります。ローカルのDNSチェックは、存在しないドメインを無料で除外します。APIが呼ばれるのは、その両方を通過したアドレスだけです。順序を誤ったり、コストの低いレイヤーを飛ばしたりすると、入力ミスのたびにクレジットを1つ消費することになります。
最初に決めておくべきことが1つあります。API呼び出し自体が失敗したときの扱いです。clean_emailの中でネットワークのタイムアウトやrequestsの例外が起きたとき、本物の顧客に、自分では直せない検証エラーを返してはいけません。リクエストの失敗はValidationErrorとは別にキャッチし、到達できないサービスはundeliverableではなくunknownとして扱ってください。一時的な障害で登録を止めるのではなく、サインアップは通して、アドレスは後で再検証します。構文とMXのレイヤーはすでにローカルで実行済みなので、緩和するのはネットワークに依存するレイヤーだけです。先ほどのverifyヘルパーはすでにこの処理を備えています。タイムアウト、ネットワークエラー、402(クレジット切れ)、429(レート制限)、5xxのいずれでもNoneを返し、validate_mailboxはそのアドレスを通過させます。
2つのカスタムバリデーターはどちらもDjangoのValidationErrorを送出するので、まったく同じ2つの関数をDjango REST Frameworkのシリアライザーにそのまま組み込めます。DRFのvalidate_<field>フックがこの例外をキャッチし、きれいな400レスポンスに変換します。
from rest_framework import serializers
class SignupSerializer(serializers.Serializer):
email = serializers.EmailField() # layer 1 for free
def validate_email(self, value):
validate_deliverable_domain(value) # layer 2
validate_mailbox(value) # layer 3
return value
これは、Node.js版やPython版のガイドと同じ3層構成です。検証を機能させるのはフレームワークではなく、レイヤーの重ね方です。レイヤー3を支える検証サービスを選ぶ際は、APIの比較で選択肢を並べて確認できます。
よくある質問
01DjangoのEmailValidatorだけでメールアドレスを検証できますか?
構文チェックであれば十分です。validate_email(およびそれを使うすべてのEmailField)はレイヤー1の適切なチェックで、自前の正規表現より優れています。ただし検証できるのは形式であり、到達性ではありません。DNSを解決せず、メールサーバーにも接続しないため、通過しても「メールアドレスの形をしている」という意味で、「配信できる」とは限りません。アドレスを信頼する前に、MXルックアップとSMTPによるメールボックスの確認を組み合わせてください。
02Djangoでカスタムのメールバリデーターを書くにはどうすればよいですか?
バリデーターは、値を受け取り、失敗時にdjango.core.exceptions.ValidationErrorを送出する呼び出し可能オブジェクトであれば何でも構いません。validate_deliverable_domain(email)のような関数を定義し、問題があれば例外を送出し、なければNoneを返すようにします。それをフィールドのvalidators=[...]リストに渡すか、フォームのclean_emailメソッドから呼び出します。DRFもDjangoのValidationErrorをキャッチするため、同じ関数をDRFシリアライザーのvalidate_emailフックでもそのまま使えます。
03APIを使わずにDjangoからメールボックスの存在を確認できますか?
部分的にしかできません。dnspythonを使えばドメインがメールを受け付けるかを確認でき、小さな依存パッケージ1つで、存在しないドメインを無料で除外できます。メールボックスを確認するにはSMTPのやり取りが必要で、smtplibで試すことはできますが、アプリサーバーから実行すべきではありません。ポート25は広くブロックされており、結果はIPのレピュテーションにも左右されるからです。この部分を引き受けるのが検証サービスです。
04これらのチェックはサインアップ時とリストのクリーニング時のどちらで行うべきですか?
どちらでも行いますが、深さは変えます。構文とMXのレイヤーはclean_emailの中で同期的に実行します。リクエストをブロックしても問題ない速さで、すぐにフィードバックを返せるからです。APIの判定結果にも、その場で対応してください。より負荷の高いバッチ検証は、遅延が問題にならず、アドレスをまとめて処理できるリストのクリーニングや管理業務に取っておきます。
SMTPレイヤーを追加する準備はできましたか?構文が完璧なアドレスを無料メールチェッカーに入力すれば、EmailFieldが通した文字列がundeliverableで返ってくる様子を確認できます。同じ判定をフォームに組み込む方法は、コピーして使えるサンプル付きのAPIリファレンスをご覧ください。



