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

Pythonでのメールアドレス検証は、簡単な部分は簡単ですが、最も重要な部分は意外なほど難しいものです。このガイドでは、ローカルで実行できる構文チェックとDNSルックアップを紹介し、素のSMTPプローブが実際にはうまくいかない理由を詳しく説明したうえで、REST APIでメールボックスレベルの結果を得る方法を示します。

要点

Pythonは不正な形式の入力の除外と、必要に応じたDNS/MXチェックのキャッシュに使いましょう。メールボックスレベルの結果には検証APIを使います。素のsmtplibによるプローブはスロットリングされやすく、曖昧な応答を返すことが多いためです。

メールの「検証」とは何か

メール検証とは、何かを送信する前に、アドレスが実在しメールを受信できることを確認するプロセスです。これは段階的なチェックに分かれ、後になるほど難しくなります。アドレスの形式は正しいか、ドメインはそもそもメールを受け付けるか、そしてそのメールボックスは存在するか。Pythonでは最初の2つは簡単にうまく実装できます。3つ目こそ、自前の実装の多くが気づかないうちに失敗する部分であり、その理由を理解することがこの記事で最も役立つポイントです。

以下のプランは多層構造です。低コストで高速なチェックをローカルで実行して明らかに壊れた入力を除外し、そのうえで、アドレスが使えるかどうかのメールボックスレベルの答えを検証APIに問い合わせます。これにより、API呼び出しなしで不要な入力を即座に弾き、残ったものすべてについてより明確な結果を得られます。

構文チェック、DNSのMXルックアップ、API検証、アプリのポリシーを使った多層的なPythonメール検証チェック
信頼できるPython実装では、ローカルチェックを高速かつ低コストに保ち、SMTP、キャッチオール、使い捨て、ロールベースの判定はマネージドな検証APIに任せます。

ステップ1:構文の検証

最初のチェックでは、アドレスが正しい形式かを確認します。正規表現を書きたくなりますが、メールの構文は見た目よりはるかに複雑で、自作の正規表現はほぼ必ず厳しすぎるか緩すぎるかのどちらかになります。この用途の定番ライブラリはemail-validatorで、関連する標準を正しく実装しており、本番環境でも広く使われています。

pipでインストールします:

pip install email-validator

次にアドレスを検証します。このライブラリは、不正な形式のものにはEmailNotValidErrorを送出し、有効な入力には正規化されたオブジェクトを返します:

from email_validator import validate_email, EmailNotValidError

def check_syntax(address):
    try:
        result = validate_email(address, check_deliverability=False)
        return True, result.normalized
    except EmailNotValidError as error:
        return False, str(error)

ok, value = check_syntax("jane.doe@example.com")
print(ok, value)  # True jane.doe@example.com

ok, value = check_syntax("jane.doe@@example")
print(ok, value)  # False, with a description of the problem

オプションcheck_deliverability=Falseを設定すると、このステップはネットワーク呼び出しのない純粋な構文チェックになり、キー入力やフォーム送信のたびに実行できるほど高速です。生のユーザー入力ではなく、ライブラリが返すnormalizedの値を常に保存してください。アドレスが正規化され、一貫した形で保存できます。

ステップ2:ドメインとMXのチェック

形式が正しいアドレスでも、ドメインがメールを受信できなければ意味がありません。次のチェックでは、ドメインのDNSレコードを照会し、そのドメインを担当するメールサーバーを示すMXレコードを探します。MXレコードのないドメインはメールを受け付けられないため、そのドメインのアドレスはすぐに除外できます。

一方、email-validatorライブラリにcheck_deliverability=Trueを渡せば、検証の一環としてMXルックアップを実行してくれます:

from email_validator import validate_email, EmailNotValidError

def check_domain(address):
    try:
        result = validate_email(address, check_deliverability=True)
        return True, result.domain
    except EmailNotValidError as error:
        return False, str(error)

同じドメインの多数のアドレスで結果をキャッシュしたいなど、DNSクエリを自分で実行したい場合は、dnspythonライブラリを直接使います:

pip install dnspython
import dns.exception
import dns.resolver

def has_mx_record(domain):
    try:
        records = dns.resolver.resolve(domain, "MX")
        return len(records) > 0
    except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN):
        return False
    except dns.exception.DNSException:
        return False  # Treat resolver errors as inconclusive.

print(has_mx_record("gmail.com"))  # True
print(has_mx_record("no-such-domain-xyz.invalid"))  # False

これは確かに有用です。失効したドメイン、存在しないドメイン、メールを扱っていないドメインのアドレスを除外できます。しかし、まだわからないことがあります。それはsomeone@gmail.comが実在するメールボックスかどうかです。GmailにはMXレコードがあるため、すべてのGmailアドレスは有効かどうかに関係なくこのチェックを通過します。

ステップ3:メールボックスのチェックと、素のSMTPでは不十分な理由

メールボックスそのものを確認するには、受信側のメールサーバーとやり取りする必要があります。原理的には、SMTPで接続し、自身を名乗り、対象アドレスを指定してRCPT TOコマンドを発行し、応答コードを読み取ります。肯定的なコードならメールボックスが存在し、拒否なら存在しないと考えられます。Python標準のsmtplibでこれを実行でき、コードは一見とてもシンプルです:

import smtplib
import dns.resolver

def naive_smtp_check(address):
    domain = address.split("@")[1]
    mx_host = str(dns.resolver.resolve(domain, "MX")[0].exchange)

    server = smtplib.SMTP(timeout=10)
    server.connect(mx_host)
    server.helo("example.com")
    server.mail("probe@example.com")
    code, message = server.rcpt(address)
    server.quit()

    # 250 and 251 generally mean the mailbox was accepted.
    return code in (250, 251)

これはチュートリアルでは動きますが、本番環境ではうまくいきません。その理由を詳しく知っておく価値があります。本格的な検証ツールがこうしたスクリプトに頼らない理由がわかるからです。

IPの評価とスロットリング

メールボックスプロバイダーは検証プローブを積極的に検知します。1台のサーバーからRCPT TOチェックを数回実行するだけで、GmailやOutlookなどのプロバイダーはすぐにそのIPをスロットリングまたはブロックします。スロットリングされると、メールボックスが存在するかどうかに関係なく曖昧な応答が返るため、結果は意味をなさなくなります。本格的な検証が、適切にウォームアップされ十分に管理された大規模なIPアドレスプールから行われるのは、まさにこのためです。

グレーリスティング

多くのサーバーはグレーリスティングを採用しており、未知の送信者から初めて接続があると、意図的に一時的な4xxエラーを返します。正規のメールサーバーは後で再試行しますが、スパマーは通常しません。再試行しない単純なスクリプトは、この一時的な失敗を拒否と読み取り、まったく問題のないメールボックスを無効と判定してしまいます。これは単に待てなかったことによる偽陰性です。

キャッチオールドメイン

キャッチオールドメインはあらゆるアドレス宛てのメールを受け付けるため、RCPT TOはrealperson@domain.comとrandom-string-9f3a@domain.comのどちらに対しても肯定的なコードを返します。こうしたドメインでは、素のSMTPチェックで実在するメールボックスと存在しないメールボックスを区別できません。キャッチオールを検出するには、確実に存在しないアドレスに2回目のプローブを送り、それも受け付けられるかを確認する必要があります。これは単純なイエス・ノーではなく、キャッチオール検出です。

大手プロバイダーからの曖昧な応答

一部の大手プロバイダーは、RCPT TOの挙動を意図的に曖昧にしています。ハンドシェイクの段階ではすべてのアドレスを受け付け、後からバウンスさせるプロバイダーもあり、その場合SMTPの応答には求めている情報がそもそも含まれていません。

そのため、email-validatorのメンテナーはSMTPチェックを意図的にライブラリから除外しています。ドキュメントには、サーバーはアドレスが配信可能かどうかを明かさないようにできており、メールを受け付けたように見えるアドレスでも後からバウンスすることがあるため、自分でSMTPサーバーに接続しても得るものはない、と明記されています。率直な結論として、構文チェックとMXチェックは自前のコードで十分にできますが、信頼できるメールボックスチェックはできません。

ステップ4:VeriMails APIで検証する

実用的な解決策は、そのために構築されたインフラを持つ検証サービスにメールボックスチェックを任せ、Pythonから1回のHTTPリクエストでそのサービスを呼び出すことです。VeriMailsが提供する REST API は、構文、MX、DNS、ライブSMTPハンドシェイク、キャッチオール検出、使い捨てアドレス検出、ロールベース検出までの全工程を実行し、1つの構造化された結果を返します。

標準のrequestsライブラリを使った検証の呼び出しは次のとおりです。エンドポイントの詳細は VeriMailsのAPIドキュメント:

import requests

def verify_email(address, api_key):
    response = requests.get(
        "https://api.verimails.com/v1/verify",
        params={"email": address},
        headers={"Authorization": f"Bearer {api_key}"},
        timeout=10,
    )
    response.raise_for_status()
    return response.json()

result = verify_email("jane.doe@example.com", api_key="YOUR_API_KEY")
print(result["status"])      # e.g. "deliverable", "undeliverable", "catch_all"
print(result["disposable"])  # True if a throwaway address
print(result["role_based"])  # True for addresses such as info@ or support@

あとは結果に応じて対応できます。たとえば登録フォームなら、deliverableのアドレスを受け付け、undeliverableとdisposableのアドレスを拒否し、独自のポリシーで扱う結果は catch_all:

def is_acceptable_for_signup(address, api_key):
    result = verify_email(address, api_key)

    if result["disposable"]:
        return False, "Disposable addresses are not allowed."
    if result["status"] == "undeliverable":
        return False, "That address does not appear to exist."

    return True, "Address verified."

呼び出しごとに明確な結果が返るため、ユーザーを待たせることなくフォーム送信の中で実行できるほど高速です。データベースにすでにある既存のアドレスリストには、一括CSV検証が適しています。単一アドレス用のエンドポイントを何千回もループするより、ファイルを1つアップロードするほうがはるかに効率的だからです。

Pythonアプリケーションで推奨されるパターンは、両方のレイヤーを組み合わせることです。クレジットを消費する前に、ローカルのemail-validatorによる構文チェックで不正な入力を即座に弾き、通過したものすべてについてAPIを呼び出して信頼できる到達性の結果を得ます。明らかなミスには素早いフィードバックを、実在するアドレスには信頼できる判定を得られます。

アプリケーションのポリシー表

結果登録時のポリシーインポートまたはCRMのポリシー
deliverable受け付けて、正規化したアドレスを保存アクティブなセグメントに残す
undeliverable修正を促す明確なメッセージとともに拒否キャンペーン前に除外
catch_allアカウントのリスクに応じて受け付けまたは確認別のセグメントに分けて慎重に送信
disposableほとんどのプロダクトでは拒否アウトリーチリストから除外
role_based共有受信トレイが許容される場合のみ許可コールドアウトリーチの前に確認

API呼び出しとリストジョブの使い分け

登録、チェックアウト、リードフォーム、招待、アカウント変更など、即時の判断が重要な場面ではAPIを使います。CSVエクスポート、CRMのクリーンアップ、キャンペーンのオーディエンスなど、何千ものアドレスを同じ基準で確認する必要がある場合は一括検証を使います。どちらのワークフローも同じ配信停止ルールにつなげることで、一方で拒否されたアドレスが後で別のツールから送信されることを防げます。

まとめ

クリーンな実装は3つのレイヤーで構成されます。1つ目は、email-validatorの構文チェックをcheck_deliverability=Falseで行い、ローカルで即座に実行して不正な入力を弾くこと。2つ目は、任意で行うdnspythonによるMXチェックで、API呼び出しの前に無効なドメインを除外したい場合に使います(APIでもカバーされます)。3つ目は、VeriMails APIの呼び出しで、自前では確実に再現できないキャッチオール、使い捨て、ロールベースの検出を含むメールボックスレベルの結果を得ることです。

覚えておきたいのは役割分担です。構文とDNSは、Pythonで十分に自分で扱えます。ライブのメールボックスチェックは、IPの評価、グレーリスティングの挙動、キャッチオール検出に左右され、単一のスクリプトでは対応できません。だからこそ標準の検証ライブラリはSMTPプローブを意図的に除外しているのです。スピードにはローカルチェックを、メールボックスレベルの検証にはAPIを使い、料金ページで想定ボリュームに合ったプランを選びましょう。

よくある質問

email-validatorライブラリで構文をチェックし、dnspythonでドメインにMXレコードがあるかを確認できます。どちらも完全に自分のコード内で実行できます。単独で確実にできないのは、メールボックスが存在するかの確認です。それには信頼されたIPアドレスからのライブSMTPハンドシェイクが必要で、単一サーバーからの素のsmtplibプローブは、通常メールボックスプロバイダーにブロックまたはスロットリングされます。
大手プロバイダーは検証プローブを検知し、数回のうちに送信元IPをスロットリングし、その後はメールボックスの有無に関係なく曖昧な応答を返します。グレーリスティングは未知の送信者に一時的な失敗を返し、単純なスクリプトはこれを無効と誤読します。キャッチオールドメインはすべてのアドレスを受け付けます。公式のemail-validatorライブラリがSMTPを意図的に省いているのは、こうした理由からです。
多層的なアプローチを取りましょう。ローカルで高速な構文チェックを実行して明らかに壊れた入力を弾き、そのうえでVeriMailsのような検証APIを呼び出してメールボックスレベルの結果を得ます。APIは適切に管理されたインフラからSMTPハンドシェイクを行い、構文、MX、DNS、SMTP、キャッチオール、使い捨て、ロールベースの結果を1つのレスポンスで返します。
VeriMails APIはアドレスごとに明確な結果を返し、フォーム送信の中で呼び出せるほど高速です。新規アカウントにはクレジットカード不要で100クレジットが無料で付与されるため、支払い前に連携をテストできます。

クリーンなデータから始めましょう

登録で100クレジット無料。クレジットカード不要。今日からアドバイスを実践しましょう。

無料で始める
クレジットカード不要。クレジットに有効期限はありません。