如何用 Python 验证邮箱地址

用 Python 验证邮箱地址,简单的部分很简单,而最关键的部分却出奇地难。本指南介绍可在本地运行的语法检查和 DNS 查询,解释原始 SMTP 探测在实践中为何会失败,并演示如何通过 REST API 获取邮箱级别的结果。

要点速览

用 Python 拒绝格式错误的输入,并可选择缓存 DNS/MX 检查结果。邮箱级别的结果请交给验证 API,因为原始smtplib探测很容易被限流,返回的结果也常常模棱两可。

邮箱“验证”究竟意味着什么

邮箱验证,是指在发送任何内容之前,确认一个地址真实存在且能够收信的过程。它可以拆分为一系列检查,难度逐级递增:地址格式是否正确,域名是否能收信,以及具体邮箱是否存在。在 Python 中,前两项很容易做好。第三项则是大多数自研方案悄然失效的地方,而理解其原因正是本文最有价值的部分。

下面的方案是分层的。先在本地运行低成本、快速的检查,剔除明显有问题的输入,再调用验证 API 获取邮箱级别的结论,判断地址能否使用。这样,垃圾输入无需调用 API 即可立即拒绝,而通过初筛的地址都能得到更清晰的结果。

Python 分层邮箱验证检查:语法校验、DNS MX 查询、API 验证和应用策略
可靠的 Python 实现会让本地检查保持快速、低成本,再由托管的验证 API 给出 SMTP、Catch-all、一次性邮箱和角色邮箱的判定。

第 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 信誉与限流

邮箱服务商会主动识别验证探测。从同一台服务器运行少量RCPT TO检查,Gmail、Outlook 等服务商在几次尝试内就会对该 IP 限流或封禁。一旦被限流,无论邮箱是否存在,服务器都会返回模棱两可的响应,您的结果也就失去了意义。正因如此,真正的验证是从大规模、经过妥善预热和良好管理的 IP 池中进行的。

灰名单

许多服务器使用灰名单机制,在陌生发件人首次连接时故意返回一个临时性的4xx失败。正规邮件服务器会稍后重试;垃圾邮件发送者通常不会。一个不懂重试的简单脚本会把这次临时失败当作拒收,把一个完全正常的邮箱标记为无效。这纯粹是因为缺乏耐心而导致的误判。

Catch-all 域名

Catch-all 域名会接收发往任意地址的邮件,因此RCPT TO对以下地址都会返回肯定的响应码:realperson@domain.com和random-string-9f3a@domain.com。在这类域名上,原始 SMTP 检查无法区分真实邮箱和不存在的邮箱。要识别 Catch-all,需要再向一个肯定不存在的地址发起探测,看它是否同样被接受,这属于 Catch-all 检测,而不是简单的是或否。

主要服务商的模糊响应

一些大型服务商刻意让它们的RCPT TO行为变得模糊。有些服务商在握手阶段接受所有地址,之后才退信,因此 SMTP 响应根本不包含您想要的信息。

这正是email-validator的维护者刻意不在库中加入 SMTP 检查的原因。其文档明确指出,自行连接 SMTP 服务器毫无益处,因为服务器很善于隐藏地址是否可送达,而一个看似接收邮件的地址事后仍可能退信。坦白说:语法和 MX 检查可以在自己的代码里做好,但可靠的邮箱检查做不到。

第 4 步:使用 VeriMails API 进行验证

切实可行的方案是,让验证服务在专为此打造的基础设施上完成邮箱检查,而您只需在 Python 中发出一个 HTTP 请求来调用它。VeriMails 提供一个 REST API ,可运行完整流程:语法、MX、DNS、实时 SMTP 握手、 Catch-all 检测、一次性邮箱检测和角色邮箱检测,并返回一个结构化结果。

使用标准的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 验证更为合适,因为上传一个文件远比循环调用单地址端点成千上万次高效。

Python 应用的推荐模式是结合两个层面:保留本地的email-validator语法检查,在消耗积分之前立即拒绝垃圾输入,再对所有通过的地址调用 API,获取权威的送达性结果。明显的错误能得到快速反馈,真实地址也能得到可信的判定。

应用处理规则表

结果注册环节规则导入或 CRM 规则
deliverable接受并存储规范化后的地址保留在活跃分组中
undeliverable拒绝并给出清晰的更正提示任何活动前予以屏蔽
catch_all根据账户风险接受或人工审核单独分组并保守发送
disposable对大多数产品予以拒绝从外联列表中屏蔽
role_based仅在可接受共享收件箱时允许陌生客户外联前人工审核

API 调用与列表任务

在需要即时决策的场景使用 API:注册、结账、线索表单、邀请和账户变更。对于 CSV 导出、CRM 清理或需要对成千上万个地址做同样审查的活动受众,则使用批量验证。两种工作流应接入同一套屏蔽规则,这样在一条路径中被拒绝的地址,之后就不会从另一个工具中被发信。

整合起来

一个干净的实现包含三层。第一层,使用email-validator的语法检查,并设置check_deliverability=False,在本地即时运行,拒绝格式错误的输入。第二层,如果想在调用 API 之前过滤失效域名,可以选择使用dnspython进行 MX 检查,不过 API 同样涵盖这一步。第三层,调用 VeriMails API 获取邮箱级别的结果,包括您自己难以可靠复现的 Catch-all、一次性邮箱和角色邮箱检测。

需要记住的是分工。语法和 DNS 完全可以由您在 Python 中处理。实时邮箱检查则取决于 IP 信誉、灰名单行为和 Catch-all 检测,这些都不是单个脚本能应付的,这也是标准验证库刻意不做 SMTP 探测的原因。用本地检查保证速度,用 API 完成邮箱级别的验证,再通过价格页面选择适合预期用量的套餐。

常见问题

您可以用 email-validator 库检查语法,用 dnspython 确认域名有 MX 记录,两者都完全在您的代码中运行。但仅靠自己无法可靠确认邮箱是否存在。这需要从可信 IP 地址进行实时 SMTP 握手,而从单台服务器发起的原始 smtplib 探测通常会被邮箱服务商封禁或限流。
主要服务商会识别验证探测,并在几次尝试内对来源 IP 限流,之后无论邮箱是否存在都返回模糊响应。灰名单机制会对陌生发件人返回临时失败,简单脚本会将其误判为无效。Catch-all 域名则接受所有地址。正因如此,官方的 email-validator 库刻意省略了 SMTP 检查。
采用分层方法:先在本地运行快速的语法检查,拒绝明显有问题的输入,再调用 VeriMails 等验证 API 获取邮箱级别的结果。API 会在妥善管理的基础设施上执行 SMTP 握手,并在一次响应中返回语法、MX、DNS、SMTP、Catch-all、一次性邮箱和角色邮箱的结果。
VeriMails API 会为每个地址返回明确的结果,速度足以在表单提交过程中调用。新账户可获得 100 免费积分,无需信用卡,因此付费之前就能先测试集成。

从干净的数据开始

注册即送 100 个免费积分,无需信用卡。今天就把这些建议付诸实践。

免费开始
无需信用卡。积分永不过期。