Skip to content
無料の100クレジットで検証を始める
Qualisend
すべての記事
エンジニアリング / 2026年6月11日

Djangoでメールアドレスを検証する方法

約10分で読めます

Qualisend team
Djangoでメールアドレスを検証する方法

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リファレンスをご覧ください。

送信者の信頼を、守る。

最初のリストを数分でクリーニング。100クレジット無料、クレジットカード登録は不要です。

今すぐ始める