Pythonで メールアドレスを 検証する 方法
Pythonでのメールアドレス検証は、簡単な部分は簡単ですが、最も重要な部分は意外なほど難しいものです。このガイドでは、ローカルで実行できる構文チェックとDNSルックアップを紹介し、素のSMTPプローブが実際にはうまくいかない理由を詳しく説明したうえで、REST APIでメールボックスレベルの結果を得る方法を示します。
Pythonは不正な形式の入力の除外と、必要に応じたDNS/MXチェックのキャッシュに使いましょう。メールボックスレベルの結果には検証APIを使います。素のsmtplibによるプローブはスロットリングされやすく、曖昧な応答を返すことが多いためです。
メールの 「検証」とは 何か
メール検証とは、何かを送信する前に、アドレスが実在しメールを受信できることを確認するプロセスです。これは段階的なチェックに分かれ、後になるほど難しくなります。アドレスの形式は正しいか、ドメインはそもそもメールを受け付けるか、そしてそのメールボックスは存在するか。Pythonでは最初の2つは簡単にうまく実装できます。3つ目こそ、自前の実装の多くが気づかないうちに失敗する部分であり、その理由を理解することがこの記事で最も役立つポイントです。
以下のプランは多層構造です。低コストで高速なチェックをローカルで実行して明らかに壊れた入力を除外し、そのうえで、アドレスが使えるかどうかのメールボックスレベルの答えを検証APIに問い合わせます。これにより、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を使い、料金ページで想定ボリュームに合ったプランを選びましょう。