电子邮件地址是许多应用程序中识别用户的重要手段,确保这些电子邮件地址的有效性和可交付性至关重要。Python 的email-validator库是一个强大的电子邮件地址验证库,专为 Python 3.8 及以上版本设计。本文将详细介绍如何使用这个库来验证电子邮件地址的语法和可交付性,并提供相关示例代码。

email-validator 的主要特性

python-email-validator库提供了一些强大的功能,使其成为验证电子邮件地址的理想选择。以下是一些关键特性:

  1. 语法验证:该库能够准确地验证电子邮件地址的语法,确保其符合 RFC 标准。包括对本地部分和域名部分的详细验证。
  2. 域名验证:除了语法验证,还可以验证域名的有效性,确保域名存在且可以解析。这包括检查 DNS 记录以确认域名的存在。
  3. 标准化:标准化电子邮件地址以确保一致性。例如,将域名部分转换为小写,将本地部分中的不必要字符去掉。
  4. 国际化支持:支持国际化域名(IDN)和非 ASCII 字符的本地部分。能够处理多种语言和字符集的电子邮件地址。
  5. 详细的错误报告:提供详细的错误报告,帮助开发者了解具体的验证失败原因,并可以采取相应的措施。
  6. 可定制性:提供多种可选参数,允许开发者根据具体需求调整验证逻辑。例如,可以指定是否允许域名文字(domain literals)等。
  7. 返回详细信息:返回详细的电子邮件地址信息,包括标准化后的地址、本地部分、域名部分、ASCII 化的地址等,方便进一步处理。
  8. 安全性(Security):通过严格的验证和标准化步骤,减少安全风险,如避免常见的注入攻击。

安装 email-validator

要安装email-validator库,可以通过 PyPI:

pip install email-validator

根据你的环境,可能需要使用pip3

email-validator 快速入门

在创建用户账户时验证用户的电子邮件地址,可以这样做:

from email_validator import validate_email, EmailNotValidError

email = "my+address@example.org"

try:
    # 检查电子邮件地址是否有效。在账户创建页面启用check_deliverability(但在登录页面不需要)。
    emailinfo = validate_email(email, check_deliverability=False)
    print(emailinfo.__dict__)

    # 之后,只使用标准化形式的电子邮件地址,特别是在进行数据库查询之前。
    email = emailinfo.normalized

except EmailNotValidError as e:
    # 异常消息是人类可读的,解释了为什么这是无效(或不可交付)的电子邮件地址。
    print(str(e))

运行输出:

{
    'original': 'my+address@example.org',
    'display_name': None,
    'local_part': 'my+address',
    'ascii_local_part': 'my+address',
    'smtputf8': False,
    'domain': 'example.org',
    'ascii_domain': 'example.org',
    'normalized': 'my+address@example.org',
    'ascii_email': 'my+address@example.org'
}

此代码验证地址并返回其 normalized 标准化形式。你应该将标准化形式存入数据库,并在检查地址是否在数据库中时始终进行标准化。在登录表单中,将check_deliverability设置为False以避免不必要的 DNS 查询。

email-validator 使用方法

概述

validate_email(email_address)函数接收一个电子邮件地址并执行以下操作:

  • 如果地址无效,则抛出带有人类可读错误信息的EmailNotValidError异常,解释为什么地址无效。
  • 如果地址有效,则返回一个对象,包含地址的标准化形式(应该使用此形式)以及其他信息。

当电子邮件地址无效时,validate_email函数会抛出EmailSyntaxError(如果地址格式无效)或EmailUndeliverableError(如果域名未通过 DNS 检查)。这两个异常类都是EmailNotValidError的子类,而EmailNotValidErrorValueError的子类。

当电子邮件地址有效时,会返回一个包含地址标准化形式和其他信息的对象。

默认情况下,验证器不允许使用已废弃的电子邮件地址形式,即使它们仍然有效且可交付,因为这些形式可能在登录时给你带来麻烦。

验证器还可以选择性地检查电子邮件地址中的域名是否具有 DNS MX 记录,表明它可以接收电子邮件。由于 DNS 查询速度慢,有时不可用或不可靠,因此根据你的用例,决定是否需要这些检查并在不需要时关闭它们。

选项

validate_email函数还接受以下关键字参数(默认值如下):

  • check_deliverability=True:如果为 True,则通过 DNS 查询检查电子邮件地址中的域名是否可以接收邮件,如上所述。在执行登录页面验证时建议传递False,因为通过查询 DNS 在每次登录时重新验证先前已验证的域名可能是不必要的。你也可以设置email_validator.CHECK_DELIVERABILITYFalse,以默认关闭所有调用的此选项。
  • dns_resolver=None:传递一个dns.resolver.Resolver实例来控制 DNS 解析器,包括设置超时和缓存。caching_resolver函数是一个辅助函数,用于构造一个带有LRUCache的 dns.resolver.Resolver。跨validate_email调用重用相同的解析器实例以利用缓存。
  • test_environment=False:如果为 True,则禁用基于 DNS 的可交付性检查,并允许test**.test域名。你也可以设置email_validator.TEST_ENVIRONMENTTrue,以默认开启所有调用的此选项。
  • allow_smtputf8=True:设置为False以禁止需要SMTPUTF8扩展的国际化地址。你也可以设置email_validator.ALLOW_SMTPUTF8False,以默认关闭所有调用的此选项。
  • allow_quoted_local=False:设置为True以允许在@符号之前包含空格、@符号或其他意外字符的电子邮件地址。这些字符在本地部分周围用引号括起来(所谓的引用字符串本地部分)。在validate_email返回的对象中,标准化的本地部分会删除任何不必要的反斜杠转义,并在地址在没有引号的情况下有效时删除周围的引号。你也可以设置email_validator.ALLOW_QUOTED_LOCALTrue,以默认开启所有调用的此选项。
  • allow_domain_literal=False:设置为True以允许在电子邮件地址的域部分中使用括号括起来的 IPv4 和带有"IPv6:“前缀的 IPv6 地址。对这些地址不执行可交付性检查。在validate_email返回的对象中,标准化域将使用压缩的 IPv6 格式(如果适用)。对象的domain_address属性将保存解析的ipaddress.IPv4Addressipaddress.IPv6Address对象(如果适用)。你也可以设置email_validator.ALLOW_DOMAIN_LITERALTrue,以默认开启所有调用的此选项。
  • allow_display_name=False:设置为True以允许输入字符串中包含显示名称和括号括起来的地址,如My Name <me@example.org>。它在 RFC 5322 第 3.4 节的精神上实现,因此可能比你期望的更严格或更宽松。如果存在显示名称,它会在返回对象的display_name字段中提供,并且会被解引号和解转义。你也可以设置email_validator.ALLOW_DISPLAY_NAMETrue,以默认开启所有调用的此选项。
  • allow_empty_local=False:设置为True以允许本地部分为空(即@example.com),例如用于验证 Postfix 别名。

DNS 超时和缓存

在验证许多电子邮件地址或控制超时时,创建一个缓存的dns.resolver.Resolver以在每次调用中重用。caching_resolver函数可以方便地返回一个实例:

from email_validator import validate_email, caching_resolver

resolver = caching_resolver(timeout=10)

while True:
    validate_email(email, dns_resolver=resolver)

测试地址

该库通过抛出EmailSyntaxError来拒绝使用特殊用途域名invalidlocalhosttest等地址。这是为了保护你的系统免受滥用:你可能不希望用户能够导致发送邮件到localhost。然而,在非生产测试环境中,你可能希望使用@test@myname.test电子邮件地址。可以通过以下三种方式允许此操作:

  1. 在调用validate_email时添加test_environment=True
  2. 全局设置email_validator.TEST_ENVIRONMENTTrue
  3. email_validator.SPECIAL_USE_DOMAIN_NAMES中删除要使用的特殊用途域名,例如:
import email_validator
email_validator.SPECIAL_USE_DOMAIN_NAMES.remove("test")

在测试中使用@example.com/net/org是有诱惑力的。它们在该库的SPECIAL_USE_DOMAIN_NAMES列表中,因此可以使用,但不应该使用。这些域名保留给 IANA 用于文档用途,因此没有意外发邮件给这些域名的风险。但是请注意,如果未禁用基于 DNS 的可交付性检查,该库仍将拒绝这些域名,因为这些域名无法解析为接收邮件的域名。在测试中,请考虑使用你自己的域名或@test@myname.test

国际化电子邮件地址

电子邮件协议 SMTP 和域名系统 DNS 历史上只允许电子邮件地址和域名中使用英文(ASCII)字符。它们各自以不同的方式适应了国际化,创造了两种不同的电子邮件地址国际化方面。

(如果你的邮件提交库完全不支持 Unicode,那么在邮件提交前必须用 ASCII 化形式替换电子邮件地址。此库在返回的对象中提供 ASCII 化形式的电子邮件地址。)

国际化域名(IDN)

第一个是国际化域名(RFC 5891),也称为 IDNA 2008。DNS 系统尚未更新以支持 Unicode。相反,国际化域名会被转换为以 xn-- 开头的特殊 IDNA ASCII “Punycode” 形式。在电子邮件地址的域名部分包含非 ASCII 字符时,域名部分会在邮件传输过程中被替换为其 IDNA ASCII 等效形式。您的邮件提交库可能会透明地为您完成这项操作(尽管网络上的合规性并不总是很好)。该库通过 Kim Davies 的 idna 模块遵循 IDNA 2008 标准。

国际化本地部分

第二种国际化是电子邮件地址的本地部分(@ 符号之前)的国际化。在非国际化的电子邮件地址中,仅允许使用英文字母、数字和一些标点符号(._!#$%&'^``*+-=~/?{|})。在国际化的电子邮件地址本地部分中,允许使用更广泛的 Unicode 字符。

这些包含非 ASCII 字符的电子邮件地址要求您的邮件提交库和所有沿途的邮件服务器,包括您自己的出站邮件服务器,都支持 SMTPUTF8(RFC 6531)扩展。对 SMTPUTF8 的支持各不相同。如果您提前知道 SMTPUTF8 不被您的邮件提交堆栈支持,那么必须使用 allow_smtputf8=False 关键字参数过滤掉需要 SMTPUTF8 的地址(见上文)。这将导致验证函数在需要 SMTPUTF8 时引发 EmailSyntaxError。如果不设置 allow_smtputf8=False,您还可以检查返回对象中的 smtputf8 字段值。

不安全的 Unicode 字符被拒绝

许多 Unicode 字符在显示时不安全,尤其是当电子邮件地址与其他文本连接时。因此,该库通过不允许保留字符、非私人使用字符、格式字符(可用于更改字符显示顺序)、空白字符、控制字符以及组合字符作为本地部分和域名的首字符(以防它们与电子邮件地址字符串外部的内容或 @ 符号结合)来保护您。请参阅 https://qntm.org/safehttps://trojansource.codes/ 了解相关的先前工作。(除了空白字符,这些检查是您在安全敏感上下文中应应用于几乎所有用户输入的检查。)这并不能防止许多 Unicode 字符外观相似的已知问题,这些问题可以用来欺骗阅读显示文本的人。

标准化电子邮件地址

Unicode 标准化

电子邮件地址中使用 Unicode 引入了标准化问题。不同的 Unicode 字符串可能看起来相同,并且对用户具有相同的语义含义。成功验证时返回的标准化字段提供了给定电子邮件地址的正确标准化形式。

例如,CJK 全角拉丁字母在域名中被认为与其 ASCII 对应字符在语义上等效。该库将其标准化为 ASCII 对应字符(根据 IDNA 的要求):

emailinfo = validate_email("me@Domain.com")
print(emailinfo.normalized)
print(emailinfo.ascii_email)
# 输出 "me@domain.com" 两次

由于最终用户可能在不同时间以不同的(但等效的)未标准化形式输入他们的电子邮件地址,因此您应该在将其存入数据库(账户创建期间)、查询数据库(登录期间)或发送外发邮件之前,立即将其替换为标准化形式。

标准化包括将电子邮件地址的域名部分小写(域名是不区分大小写的)、对整个地址进行 Unicode “NFC” 标准化(将字符加组合字符转化为预合成字符,替换域名部分的全角和半角字符,可能还有其他 UTS46 映射),以及将 Punycode 转换为 Unicode 字符。

标准化可能会更改电子邮件地址中的字符和电子邮件地址的长度,因此一个字符串可能在标准化之前是有效地址,但在标准化之后无效,反之亦然。该库只允许在标准化前后均有效的地址。

其他标准化

如果您通过 allow_quoted_localallow_domain_literal 选项允许了引号字符串本地部分和域名文字 IPv6 地址,标准化也会应用于这些部分。在引号字符串本地部分中,会移除不必要的反斜杠转义,甚至在不必要的情况下移除周围的引号。对于 IPv6 域名文字,IPv6 地址会标准化为压缩形式。RFC 2142 还要求对一些特定的邮箱名称(如 postmaster@)进行小写标准化。

示例

对于电子邮件地址 test@axiaoxin.com,返回的对象是:

ValidatedEmail(
  normalized='test@axiaoxin.com',
  local_part='test',
  domain='axiaoxin.com',
  ascii_email='test@axiaoxin.com',
  ascii_local_part='test',
  ascii_domain='axiaoxin.com',
  smtputf8=False)

对于虚构但有效的地址 example@ツ.ⓁⒾⒻⒺ,它具有国际化的域名但 ASCII 的本地部分,返回的对象是:

ValidatedEmail(
  normalized='example@ツ.life',
  local_part='example',
  domain='ツ.life',
  ascii_email='example@xn--bdk.life',
  ascii_local_part='example',
  ascii_domain='xn--bdk.life',
  smtputf8=False)

请注意,标准化和其他字段提供了电子邮件地址、域名和(在其他情况下)本地部分的标准化形式(参见之前关于标准化的讨论),您应该在数据库中使用这些形式。

调用 validate_email 并使用上述电子邮件地址的 ASCII 形式 example@xn--bdk.life,返回完全相同的信息(即,标准化字段将始终包含 Unicode 字符,而不是 Punycode)。

对于虚构的地址 ツ-test@axiaoxin.com,它具有国际化的本地部分,返回的对象是:

ValidatedEmail(
  normalized='ツ-test@axiaoxin.com',
  local_part='ツ-test',
  domain='axiaoxin.com',
  ascii_email=None,
  ascii_local_part=None,
  ascii_domain='axiaoxin.com',
  smtputf8=True)

现在 smtputf8True,并且 ascii_emailNone,因为地址的本地部分是国际化的。local_partnormalized 字段返回地址的标准化形式。

返回值

当电子邮件地址通过验证时,返回对象中的字段包括:

字段
normalized电子邮件地址的标准化形式,您应该将其放入数据库中。这结合了 local_partdomain 字段(见下文)。
ascii_email如果设置,则为标准化电子邮件地址的 ASCII 形式,通过将域名部分替换为 IDNA Punycode。该字段会在存在 ASCII 形式的电子邮件地址时出现(包括如果电子邮件地址本身已经是 ASCII)。如果电子邮件地址的本地部分包含国际化字符,ascii_email 将为 None。如果设置,它仅仅结合了 ascii_local_partascii_domain
local_part给定电子邮件地址的标准化本地部分(@ 符号之前)。标准化包括 Unicode NFC 标准化和移除不必要的引号和反斜杠。如果 allow_quoted_localTrue 并且周围的引号是必要的,这些引号会出现在此字段中。
ascii_local_part如果设置,本地部分仅由 ASCII 字符组成。
domain电子邮件地址的域名部分的规范国际化 Unicode 形式。如果返回的字符串包含非 ASCII 字符,则需要使用 SMTPUTF8 功能来传输消息,或者电子邮件地址的域名部分必须首先转换为 IDNA ASCII 形式:使用 ascii_domain 字段。
ascii_domain给定电子邮件地址的域名部分的 IDNA Punycode 编码形式,按网络上传输的形式。
domain_address如果允许域名文字,并且电子邮件地址包含一个,则为 ipaddress.IPv4Addressipaddress.IPv6Address 对象。
display_name如果没有显示名称且角括号未围绕地址,则为 None;否则,将设置为显示名称,如果有角括号但没有显示名称,则为空字符串。如果显示名称被引号引用,将去掉引号和转义。
smtputf8一个布尔值,指示由于地址的本地部分包含非 ASCII 字符,您的邮件中继的 SMTPUTF8 功能将被要求来传输到此地址(本地部分不能进行 IDNA 编码)。如果作为参数传递了 allow_smtputf8=False,则此标志将始终为 False,因为如果它本来会是 True,则会引发异常。
mx域的 MX 记录在 DNS 中指定的 (优先级, 域名) 元组的列表(见 RFC 5321 第 5 节)。如果由于暂时问题如超时,无法完成可传递性检查,则可能为 None
mx_fallback_type如果找到 MX 记录则为 None。如果 DNS 中没有实际指定 MX 记录,而是通过过时的机制从 A 或 AAAA 记录推断出来,则值为所用的 DNS 记录类型(A 或 AAAA)。如果由于暂时问题如超时,无法完成可传递性检查,则可能为 None
spf检查可传递性时发现的任何 SPF 记录。仅在查询了 SPF 记录时设置。

结论

email-validator库提供了一种强大且灵活的方式来验证电子邮件地址。无论是验证语法还是检查可交付性,这个库都能帮助你确保收集到的电子邮件地址是有效且可靠的。希望本文能帮助你更好地理解和使用email-validator库,让你的应用程序更健壮。


也可以看看