2007年,一位开发者在阅读了RFC规范后写下了一篇震惊的文章,标题是《直到我读了RFC,我才知道怎么验证电子邮件地址》。他发现几乎所有网上的正则表达式都太严格了——按照规范,这些地址全部合法:

Abc\@[email protected]
"Abc@def"@example.com
"Fred Bloggs"@example.com
customer/[email protected]
[email protected]
!def!xyz%[email protected]

这不是孤例。2021年,GitHub上有人报告了一个问题:某网站拒绝了 user@ai 这个地址——因为它的正则表达式要求域名必须有"点"。然而 ai 是安哥拉的顶级域名,而且它有MX记录。理论上,x@ai 是一个完全有效的电子邮件地址。

电子邮件验证的复杂性远超大多数开发者的想象。这不是一个可以用几十个字符的正则表达式解决的问题——它涉及四十年的协议演进、无数的历史包袱、以及理论与现实的巨大鸿沟。

一个地址的前世今生

要理解电子邮件地址为什么如此复杂,必须回到1982年。

那一年,RFC 822发布了。这份文档定义了ARPANET文本消息的格式,其中包含了电子邮件地址的语法。当时的互联网是一个完全不同的世界:UUCP仍然是主流的邮件传输方式,“bang path”(host1!host2!user)是一种常见的地址格式。

RFC 822的设计哲学是"宽松"。它允许地址中包含注释、空白、甚至换行符。例如,下面这个在RFC 822下是完全合法的:

Pete(A nice \) chap) <pete(his account)@silly.test(his host)>

括号中的内容是注释,会被系统忽略。这种设计在当时的学术环境中是合理的——邮件主要在研究者之间传递,可读性比严格性更重要。

2001年,RFC 2822取代了RFC 822。新规范收紧了许多规则,但为了向后兼容,仍然保留了"过时语法"(obsolete syntax)的支持。2008年,RFC 5322再次更新,成为当前的标准。

然而,电子邮件地址的规范不仅仅来自RFC 5322。RFC 5321定义了SMTP协议,它对地址有自己的限制。当你实际发送邮件时,SMTP服务器会对地址进行验证,而这个验证可能与RFC 5322的语法规则不完全一致。

这就是问题的根源:电子邮件地址没有一个单一、明确的定义。 它的有效性取决于你问的是哪个RFC、哪个邮件服务器、以及你处于协议栈的哪个层级。

大小写敏感:规范与现实的撕裂

RFC 5321的第2.4节有一段毫不含糊的陈述:

The local-part of a mailbox MUST BE treated as case sensitive.

邮箱的本地部分必须被视为大小写敏感的。

这意味着,根据规范,[email protected][email protected] 可能指向两个完全不同的邮箱。邮件服务器在处理本地部分时必须保留原始大小写。

然而,现实完全不同。几乎所有主要的邮件服务提供商都把本地部分当作大小写不敏感来处理。Gmail、Outlook、Yahoo Mail——它们都会把 JohnjohnJOHN 视为同一个用户。

这不是bug,而是务实的工程决策。想象一下,如果 johnJohn 是两个不同的用户,用户在输入邮箱地址时哪怕大小写打错一个字母,邮件就会发错人。这种用户体验是灾难性的。

但这也造成了一个两难困境:如果你的系统严格遵循RFC,你可能会把来自同一个人的两个地址当作不同的账户;如果你忽略大小写,你又违反了规范。大多数系统选择了后者,但开发者需要意识到这是一个有意识的选择,而非理所当然。

更复杂的是,域名部分确实是大小写不敏感的——这遵循DNS的规则。所以 [email protected][email protected] 在任何情况下都应该被视为相同。

引号字符串:一个被遗忘的角落

RFC 5322允许本地部分使用引号包围。在引号内部,几乎所有ASCII字符都是允许的,包括空格、@符号,甚至换行符。

以下全部是合法的电子邮件地址:

"hello world"@example.com
"quote@example"@example.com
"john..doe"@example.com
".john"@example.com
"john."@example.com

最后一个例子尤其值得注意:在非引号模式下,本地部分不能以点开头或结尾,也不能有两个连续的点。但在引号模式下,这些限制全部消失。

问题是,几乎没有任何现代邮件服务允许用户注册包含这些特殊字符的地址。Gmail不允许空格,不允许引号。Outlook也是。大多数企业邮件系统同样如此。

然而,如果你的正则表达式拒绝这些地址,你实际上是在拒绝RFC合规的地址。这可能导致某些边缘用户无法使用你的服务——比如,某个遗留系统的用户可能确实有一个包含空格的邮件地址。

加号和子地址:一种约定,而非标准

很多开发者知道Gmail有一个"加号技巧":[email protected] 会把邮件投递到 [email protected]。这个功能被称为"子地址"或"加号寻址",RFC 5233对其进行了标准化。

但这里有两个误区:

首先,加号本身只是一个普通字符。RFC 5321没有给加号任何特殊含义。是否实现子地址功能完全取决于邮件服务器的实现。Gmail实现了,Outlook实现了,但很多企业邮件系统没有。

其次,不同的服务对子地址的处理方式不同。Gmail忽略加号及其后的内容。Outlook也类似。但有些系统可能会把 john+labeljohn 视为完全不同的账户。

这意味着,如果你在用户注册时把 [email protected] 转换为 [email protected],你可能帮用户避免了创建重复账户,但也可能阻止用户使用他们想要的特定子地址。

长度限制:数字游戏

RFC 5321规定了以下长度限制:

  • 本地部分:最多64个八位字节
  • 域名部分:最多255个八位字节
  • 整个地址(在SMTP的MAIL FROM/RCPT TO命令中):最多254个字符

等等——64 + 1(@)+ 255 = 320,为什么总限制是254?

这是因为SMTP命令中的地址需要被尖括号包围(<user@domain>),加上空格和命令本身,总长度限制在512字符以内。经过计算,地址本身最多254字符。

但这里还有更多复杂性:RFC 5322的语法没有对总长度做出明确限制,只是分别限制了本地部分和域名部分。而且,64和255指的是"八位字节"而非"字符"——在UTF-8编码下,一个Unicode字符可能占用多个字节。

实际上,很少有邮件地址会接近这些限制。但如果你在设计数据库字段,建议使用 VARCHAR(255) 而非更小的值——总有一天,一个用户会带着他们那来自某个学术机构的超长邮件地址出现。

IP地址域名:技术正确,现实荒谬

RFC 5321允许在域名部分直接使用IP地址,称为"地址字面量"(address literal):

user@[192.168.1.1]
user@[IPv6:2001:db8::1]

这在技术上是完全合法的。邮件服务器应该能够处理这样的地址。

但在实际中,你几乎永远不会看到这样的地址被使用。它们主要用于测试和调试目的。大多数邮件系统即使接受这样的地址,也会给予极高的垃圾邮件评分。

然而,如果你的正则表达式拒绝这样的地址,你实际上是在拒绝RFC合规的地址。这在理论上可能导致互操作性问题。

国际化地址:Unicode的挑战

2012年,RFC 6531和6532引入了国际化电子邮件地址(EAI)的支持。这使得邮件地址可以包含Unicode字符:

"josé@example.com"@例子.测试
用户@例子.测试
"🍺"@example.com

这需要邮件服务器支持SMTPUTF8扩展。近年来,主流邮件服务已经开始支持国际化地址。Gmail在2014年宣布支持非拉丁字符的邮件地址。

然而,国际化地址带来了一系列新问题:

  1. 同形字攻击:西里尔字母的 а(U+0430)和拉丁字母的 a(U+0061)看起来完全相同,但实际上是不同的字符。攻击者可以注册一个看起来像合法地址的邮件账户。

  2. 显示问题:某些邮件客户端可能无法正确显示国际化地址,特别是当用户的系统缺少相应字体时。

  3. 数据库兼容性:如果你的数据库使用旧版本的字符集(如Latin-1),国际化地址可能导致存储问题。

顶级域名邮箱:你确定域名必须有点?

大多数人认为电子邮件地址的域名部分必须有至少一个点,例如 [email protected]。但RFC并没有这个要求。

2013年,研究者检查了当时的根区文件,发现有25个顶级域名有MX记录。这意味着像 ai@aim@ttsanta.cl@ws 这样的地址在技术上是有效的。

实际上,安哥拉的 .ai 顶级域名确实有MX记录,而且确实有人使用 n@ai 这样的地址。Ian Goldberg曾经维护这个地址,声称它是"最短的电子邮件地址"。

大多数验证正则表达式会拒绝这样的地址,因为它们要求域名包含至少一个点。这在99.9%的情况下是正确的决定——但那0.1%的用户呢?

特殊字符的完整列表

RFC 5322定义了 atext,即本地部分在不使用引号时允许的字符:

atext = ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" /
        "-" / "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~"

这意味着除了字母和数字,以下字符都是允许的:

! # $ % & ' * + - / = ? ^ _ { | } ~`

注意,点号(.)也是允许的,但它只能出现在两个 atext 之间——不能出现在开头、结尾,也不能连续出现。

以下是合法的地址示例:

[email protected]
[email protected]
[email protected]
user%[email protected]
# $ % & ' * + - / = ? ^ _ ` { | } [email protected]

最后一个例子虽然合法,但几乎没有邮件服务会允许注册这样的地址。

为什么正则表达式永远不够

阅读了以上内容后,答案应该很明显了。一个真正RFC合规的正则表达式会极其复杂——Stack Overflow上有人写了一个超过6000个字符的正则表达式来验证RFC 5322,但它仍然无法处理嵌套注释(因为正则表达式不能处理无限嵌套)。

更重要的是,即使一个地址在语法上完全正确,也不能保证:

  1. 这个邮箱实际存在
  2. 这个邮箱属于输入它的人
  3. 这个邮箱可以接收邮件

你唯一能确定的是:这个字符串符合RFC定义的电子邮件地址语法。

正确的做法

那么,应该怎么验证电子邮件地址?

格式验证:宽松,而非严格

前端验证应该宽松。检查是否包含 @ 符号,检查 @ 前后是否有内容,这就够了。HTML5的 <input type="email"> 就是这样一个实现——它只检查非常基本的格式。

<input type="email" name="email" required>

浏览器内置的验证足够应对大多数情况,而且它会随着规范的更新而更新。

发送验证邮件

这是唯一真正有效的方法。发送一封包含唯一链接的邮件到用户提供的地址。如果用户能够点击链接,就证明:

  1. 地址格式正确(否则邮件服务器会拒绝)
  2. 邮箱存在且可接收邮件
  3. 用户能够访问这个邮箱

这比任何正则表达式都可靠。它不仅验证了地址的语法,还验证了地址的实际可用性。

存储时的考虑

在存储邮件地址时:

  • 使用足够长的字段(VARCHAR(255) 是一个好的选择)
  • 不要自动转换为小写——虽然大多数邮件服务会这样做,但你可能遇到例外
  • 如果需要去重,考虑存储两个版本:原始版本(用于显示)和规范化版本(用于比较)

显示时注意安全性

邮件地址可能包含恶意内容。在显示时确保进行适当的转义。特别是,不要把邮件地址直接插入到HTML中而不转义——地址可能包含 <> 字符(在引号模式下),这可能导致XSS攻击。

四十年的遗产

电子邮件地址的复杂性不是设计缺陷,而是历史的必然。当RFC 822在1982年发布时,互联网是一个小得多的地方,参与者是研究者和学术机构,他们需要灵活的寻址方式来连接不同的邮件系统。

四十年过去了,当初的设计选择成为了今天的包袱。我们无法改变过去,但我们可以理解它,并在当前的约束下做出最务实的选择。

下次当你需要在用户注册表单上添加电子邮件验证时,请记住:一个简单的 @ 检查,加上一封验证邮件,比你写的任何正则表达式都更可靠。

这不仅是工程上的最佳实践,也是对用户的尊重。与其告诉用户"你的邮箱格式不对",不如让邮件服务器来告诉我们真相。


参考资料

  • RFC 5321 - Simple Mail Transfer Protocol (2008)
  • RFC 5322 - Internet Message Format (2008)
  • RFC 6531 - SMTP Extension for Internationalized Email (2012)
  • RFC 6532 - Internationalized Email Headers (2012)
  • RFC 822 - Standard for the Format of ARPA Internet Text Messages (1982)
  • Haacked, “I Knew How To Validate An Email Address Until I Read The RFC” (2007)
  • Jan Schaumann, “Your E-Mail Validation Logic is Wrong” (2021)
  • Stack Overflow, “How can I validate an email address using a regular expression?” (2008)
  • kdeldycke/awesome-falsehood GitHub Repository
  • Wikipedia, “Email address”