为什么不应该用正则表达式验证电子邮件地址
解析邮箱正则为何失败:它会拒绝有效地址、接受不可投递地址,还可能引发 ReDoS。更好的做法是用 HTML5 email 输入或库。
正则表达式无法验证电子邮件地址——它只能检查大致的格式,即便是精心设计的模式,也会同时拒绝合法地址,并接受那些语法上看似合理却永远无法投递的地址。问题的根源不在于你还没找到正确的模式,而在于”这个邮件地址是否有效?“这个问题本身混淆了三个不同的问题,而正则表达式只能触及其中一个——而且还是最没用的那个。本文将这三个问题分别阐述,引用相关规范的实际内容(RFC 5321、RFC 5322、RFC 6531 以及 WHATWG HTML Living Standard),精确展示主流模式在两个方向上的失效情况,并提供可直接使用的实用 JS/TS 代码。
核心要点
- 电子邮件验证分为三个层次:UX 合理性检查、基于 RFC 5321/5322 的语法验证,以及地址存在性验证。只有确认邮件才能证明该地址确实能接收邮件。
- HTML Living Standard 的
<input type="email">使用的正则表达式,规范本身将其标注为”对 RFC 5322 的故意违反”;它有意不完全符合 RFC,但作为默认选择,仍优于任何手写模式。 - 主流的邮件正则表达式在两个方向上都会失效:它们会拒绝真实地址(加号寻址、新 gTLD、带引号的本地部分、符合 RFC 6531 的国际化地址),也会接受无法投递的地址。
- 在 Node.js 服务端运行容易引发回溯的邮件正则表达式,可被精心构造的短输入利用,阻塞事件循环——这是一种 ReDoS 拒绝服务攻击向量(CWE-1333)。
- 在任何模式运行之前,唯一值得强制执行的长度约束来自 RFC 5321 §4.5.3.1.3:地址本身上限为 254 个字符。
电子邮件验证的三个层次
电子邮件验证分为三个不同的层次:UX 合理性检查(看起来像邮件地址吗?)、语法验证(是否符合 RFC 5321/5322 规则?),以及地址存在性验证(这个邮箱是否真的能接收邮件?)。只有第三层才能证明地址有效,而且只有确认邮件才能完成这一验证。那些告诉你”停止使用正则表达式”的参考资料是对的,但它们把这几个层次混为一谈了。将它们分开,才能知道在哪个环节该用什么工具。
- 第一层——UX 合理性检查。 一种廉价、快速的客户端检查,用于捕捉明显的拼写错误(如
alicegmail.com、末尾有空格),并提供即时反馈。这是正则表达式唯一适用的层次,即便如此,你也应该使用能完成任务的最简模式。 - 第二层——语法验证。 字符串是否符合电子邮件 RFC 中定义的语法规则?这比看起来要难得多,手写正则表达式根本无法胜任,而且——关键在于——这并不能证明任何关于可投递性的事情。一个完全符合 RFC 的地址,可能指向一个根本不存在的域名。
- 第三层——地址存在性验证。 这个地址是否真的有邮箱在接收邮件?唯一能证明邮件地址有效的方式,是一封成功投递的邮件;确认邮件一步到位,完成了任何正则表达式都做不到的事情。
几乎所有”终极邮件正则表达式”都犯了同一个错误:试图完美地完成第二层,而第二层根本回答不了任何人真正关心的问题。真正的问题是第三层,而任何模式都无法触及它。
“有效”究竟意味着什么
Discover how at OpenReplay.com.
有效的电子邮件地址比大多数正则表达式所假设的要宽泛得多,因为 RFC 5321(SMTP)和 RFC 5322(邮件格式)中的语法允许一些看起来不正常的结构。本地部分——@ 之前的所有内容——可以包含一长串特殊字符,甚至可以是一个带引号的字符串。
未加引号的本地部分由 atext 构成,定义于 RFC 5322 §3.2.3,除字母和数字外,还允许以下字符:
! # $ % & ' * + - / = ? ^ _ ` { | } ~
这意味着 user+tag@example.com(加号寻址)是有效的——+ 是普通的 atext 字符,见 RFC 5321 §4.1.2。接收服务器是否将 +tag 视为子地址是实现层面的问题(RFC 5233),但地址本身是格式正确的。本地部分也可以是带引号的字符串:"user name"@example.com 根据 RFC 5321 §4.1.2 和 RFC 5322 §3.2.4 是有效的,包括其中的空格。域名可以是方括号内的 IP 地址字面量——user@[192.168.1.1] 根据 RFC 5321 §4.1.3 是有效的。
有一个约束值得廉价地强制执行。RFC 5321 §4.5.3.1.3 规定前向路径(forward-path)上限为 256 个字节(含尖括号),这意味着地址本身最多 254 个字符;本地部分上限为 64 个字节(§4.5.3.1.1),域名上限为 255 个字节(§4.5.3.1.2)。长度检查是字符串比较能正确处理的唯一一种验证,也是正则表达式不需要处理的。
国际化地址(EAI)
RFC 6531 中定义的国际化电子邮件地址——例如 用户@例子.广告——是有效的,且越来越常见;任何纯 ASCII 正则表达式都无法处理它们,这是一个应该交给库来解决的问题,而不是正则表达式的问题。EAI(RFC 6531 §3.3)将本地部分扩展为允许 UTF-8,域名也可以是非 ASCII 的 Unicode 字符。这与 IDNA punycode 编码的域名(RFC 5891)不同:EAI 也覆盖了本地部分。任何假设本地部分为 [a-zA-Z0-9] 的模式,对于世界上越来越多的用户来说都是错误的,而且也不存在一个正则表达式能在正确接受 ASCII 和 Unicode 本地部分的同时,不接受无效输入。
为什么邮件验证正则表达式在两个方向上都会失效
手写的邮件正则表达式既无法充当有效的守门人,也无法充当有效的过滤器:它会产生假阴性(拒绝可投递的地址)和假阳性(接受语法上符合规范但永远无法收到邮件的地址)。这两种失效模式之所以持续进入生产环境,是因为测试套件使用的是 test@example.com,而这个地址能通过所有模式。
以 Stack Overflow 上广为流传的复制粘贴模式为例——该模式要求 TLD 长度为 2 到 4 个字符:
// 一个常见的复制粘贴模式。请勿使用。
const bad = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/;
以下是它对真实地址的处理结果:
| 地址 | 测试内容 | 此正则表达式 | 正确结果 | 错误原因 |
|---|---|---|---|---|
name+filter@gmail.com | 加号寻址 | ✅ 接受 | ✅ 有效 | (此处通过,但更严格的模式会拒绝 +) |
user@studio.photography | 长 gTLD | ❌ 拒绝 | ✅ 有效 | {2,4} 拒绝长度超过 4 个字符的 TLD |
"user name"@example.com | 带引号的本地部分 | ❌ 拒绝 | ✅ 有效 | 带引号的字符串和空格是合法的 |
用户@例子.广告 | EAI(RFC 6531) | ❌ 拒绝 | ✅ 有效 | 仅限 ASCII 的字符类 |
someone@validformat.test | 不存在的域名 | ✅ 接受 | ❌ 无法投递 | 语法正确,但域名无法解析 |
拒绝 name+filter@gmail.com(加号寻址)或 user@studio.photography(ICANN 新 gTLD 计划下委派的 gTLD,.photography 于 2013 年加入根区)的正则表达式,并不是在严格验证——而是在犯错。这两个地址都是使用有效邮件特性的语法合法地址。仅 {2,4} 这一 TLD 约束,就会导致 .photography、.accountants、.engineering 以及数百个其他有效委派域名无法通过验证。
Session replay 数据经常揭示用户遭遇验证错误、多次修改输入后最终放弃表单的情况。表单可用性研究一贯将验证摩擦认定为导致用户流失和转化率下降的因素之一。
假阳性——通过了正则表达式却永远无法投递的地址——同样真实存在。someone@validformat.test 能通过上述模式以及大多数其他模式,但 .test 是一个保留 TLD(RFC 2606),永远无法投递。语法合规性与可投递性是相互独立的属性,而正则表达式只能看到前者。
ReDoS:当正则表达式本身成为漏洞
在 Node.js 服务端运行容易引发回溯的邮件正则表达式,可被精心构造的输入利用,阻塞事件循环——这是一种拒绝服务攻击向量(CWE-1333:低效正则表达式复杂度,以及 OWASP ReDoS 参考),与邮件本身无关,完全是灾难性回溯的问题。在重叠字符类上具有嵌套或相邻量词的模式,在几乎匹配的输入上可能需要指数级的时间。
以下是一个可复现的演示。该模式中的 (...)+ 包裹了一个可以用多种方式匹配同一字符的分组,因此一长串相同字符后跟一个不匹配的字符,会迫使引擎在失败之前尝试指数级数量的分区:
// Node.js v24。运行方式:node redos.js
// 一个故意设计为易受攻击、容易引发回溯的模式。
const evil = /^([a-zA-Z0-9]+)*@example\.com$/;
// 精心构造的近似匹配输入:大量 'a' 字符,后跟一个破坏匹配的字符。
const attack = "a".repeat(40) + "!";
console.time("redos");
evil.test(attack); // 阻塞事件循环
console.timeEnd("redos");
在当前版本的 Node.js 上,增加重复次数会导致匹配时间爆炸式增长——每增加一个字符,工作量大约翻倍。由于 Node 的正则引擎在主线程上同步运行,携带此类输入的单个请求就会使事件循环停滞,阻塞所有正在处理的其他请求。(x+)* 这种结构是危险信号:任何能以多种方式匹配同一子字符串的分组,在外层量词下都可能引发灾难性回溯。解决方案不是写一个更聪明的模式——而是根本不构建这类模式,这正是将验证工作委托给平台或成熟库所带来的好处。
语法合规不等于可投递
即使是完全符合 RFC 的地址,也无法告诉你邮件是否能送达。正则表达式无法检查域名是否存在、是否有 MX 记录、邮箱是否已开通,或者地址是否是一次性的临时邮箱。这些是网络和策略层面的问题,而不是语法问题。realuser@gmail.com 和拼写错误的 realuser@gmial.com 在语法上都是有效的;只有 DNS 查询才能区分它们,而只有实际投递才能区分活跃邮箱和无效邮箱。
一次性和临时邮件域名是一个相关但独立的问题:这些地址在语法上和操作上都是有效的,但其存在目的是规避你的注册流程。检测它们需要一份持续维护的提供商域名黑名单,而不是一个模式——域名列表在不断变化,任何硬编码的列表都会过时。将其视为验证之上的策略层,而不是验证本身的一部分。
应该怎么做
采用分层方法:为 UX 使用最简的合理性检查,为语法验证使用平台内置功能,仅在需要更多功能时才使用成熟的库,并通过确认邮件完成唯一真正重要的那一步。以下是从最廉价到最权威的顺序。
1. 最简合理性检查
对于即时的客户端反馈,最小实用模式来自最初”停止用正则表达式验证”论点:要求有内容、一个 @、内容、一个点,以及内容。配合长度检查一起使用。
/**
* 第一层合理性检查:捕捉明显的拼写错误,仅此而已。
* 故意设计为宽松——这不是有效性的证明。
* @param value - 原始输入字符串
* @returns 如果值具有电子邮件的大致形状且长度 <= 254 个字符,则返回 true
*/
export function looksLikeEmail(value: string): boolean {
if (value.length > 254) return false; // RFC 5321 §4.5.3.1.3
return /.+@.+\..+/.test(value);
}
这个合理性检查会拒绝 alicegmail.com 和 alice@localhost,接受加号寻址和长 gTLD,并在常数时间内运行。将其返回的 true 视为”有效”是不安全的——它只是一个拼写错误捕捉器。
2. 优先使用平台:<input type="email">
语法验证最好的默认选择是浏览器自带的 <input type="email">,了解它的具体行为是值得的。HTML Living Standard 的 <input type="email"> 使用的正则表达式,规范本身称之为”对 RFC 5322 的故意违反”——它有意不完全符合 RFC,以可用性换取规范准确性,作为默认选择优于任何手写模式。规范引用了确切的模式:
^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$
逐条解读:
[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+— 本地部分,允许atext特殊字符。它故意不支持带引号的本地部分("user name"@…)。@— 恰好一个分隔符。[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?— 域名标签:以字母数字开头和结尾,内部允许连字符,上限为 63 个字符。它故意不支持 IP 字面量域名(user@[192.168.1.1])。(?:\.<label>)*— 零个或多个以点分隔的附加标签,因此单标签和多标签域名都能通过。
WHATWG 公开记录了这一权衡:该模式故意拒绝某些技术上有效的 RFC 5322 地址(带引号的部分、IP 字面量),因为这些形式在真实注册场景中极为罕见,支持它们引入的 bug 比其带来的好处更多。对于表单字段来说,这是正确的权衡,这也是为什么 <input type="email"> 应该作为你的第二层基准——它没有回溯问题,并且与浏览器已有的强制行为保持一致。
3. 仅在需要时才使用成熟的库
如果你需要超出 HTML5 模式的服务端语法验证,请使用成熟的、经过充分测试的库,而不是自己造轮子。validator 包(npm validator,MIT 许可证)提供了一个 isEmail 函数,支持带引号的本地部分,并提供 IP 字面量域名和显示名称的选项:
import isEmail from "validator/lib/isEmail";
/**
* 第二层语法验证,服务端。
* @param email - 候选地址(已完成长度检查)
* @returns 如果根据 validator 的 RFC 对齐规则语法有效,则返回 true
*/
export function isSyntacticallyValid(email: string): boolean {
return isEmail(email, { allow_utf8_local_part: true });
}
相比之下,请避免使用较旧的 email-validator 包,该包自 2018 年以来未再发布更新。使用库能为你提供经过测试的边界情况处理,以及一个积极维护的维护者来修复你手写模式永远无法覆盖的情况——包括在正确配置选项下对 EAI 地址的支持。
4. 真正的答案:发送确认邮件
唯一能证明地址有效的步骤是实际投递。发送一封包含一次性链接的确认邮件;只有在用户点击链接后,才将地址标记为已验证。这就是双重确认(double opt-in),它使上游的复杂验证变得多余——格式错误或无法投递的地址根本不会完成确认。
/**
* 验证流程示意。存储和邮件发送逻辑取决于具体应用。
* @param email - 已通过长度和语法检查的字符串
*/
async function startEmailVerification(email: string): Promise<void> {
const token = crypto.randomUUID();
await storePendingVerification(email, token); // 例如 24 小时后过期
const link = `https://app.example.com/verify?token=${token}`;
await sendMail(email, "Confirm your email", `Click to confirm: ${link}`);
// 仅在 /verify 收到有效 token 时才将账户标记为已验证。
}
发送确认邮件是各种资料最终都会归结到的方案,原因相同:它一步到位,完成了任何正则表达式都做不到的事情。正如 Jamie Zawinski 所说:“有些人遇到问题时会想,‘我知道了,我来用正则表达式。‘现在他们有两个问题了。“对于邮件验证来说,第二个问题是:正则表达式仍然没有回答那个真正的问题。
结论
停止试图验证地址,开始着手验证邮箱。使用最简模式加 254 字符上限来提供即时 UX 反馈,依靠 <input type="email"> 或 validator 等成熟库进行语法验证,并通过确认邮件来保护每个真实账户——最后这一步是唯一能证明”有人在家”的方式。下次注册表单需要邮件字段时,请选择平台能力和确认流程,而不是 Stack Overflow 上的那个模式。
常见问题
电子邮件地址的最大有效长度是多少?
电子邮件地址上限为 254 个字符。这源自 RFC 5321 第 4.5.3.1.3 节,该节将前向路径(forward-path)限制为 256 个字节(含两侧尖括号),地址本身因此最多 254 个字符。本地部分单独上限为 64 个字节,域名上限为 255 个字节。简单的长度比较能正确执行此约束,这是在任何模式运行之前唯一值得进行的验证。
HTML5 邮件输入框是否根据完整的 RFC 5322 语法进行验证?
不是。HTML Living Standard 明确将其邮件输入框的正则表达式描述为"对 RFC 5322 的故意违反"。它故意拒绝带引号的本地部分("user name"@example.com)和 IP 字面量域名(user@[192.168.1.1])等技术上有效的形式,因为这些在真实注册场景中极为罕见。这一权衡以规范完整性换取可用性,使其成为比手写模式更安全的默认选择,但它并不是一个完整的 RFC 验证器。
邮件验证正则表达式如何导致拒绝服务攻击?
具有嵌套或相邻量词、作用于重叠字符类的正则表达式(例如 ([a-zA-Z0-9]+)* 这种结构),在几乎匹配的输入上可能需要指数级的时间。这就是灾难性回溯,被分类为 CWE-1333。在 Node.js 服务端运行时,由于正则引擎在主线程上同步执行,单个精心构造的请求就能使事件循环停滞,阻塞所有正在处理的其他请求。解决方案是完全避免这类模式,而不是写一个更聪明的版本。
正则表达式能检查电子邮件地址是否真实存在吗?
不能。正则表达式只检查字符串的形状;它无法验证域名是否存在、是否有 MX 记录,或者邮箱是否已开通。语法合规性与可投递性是相互独立的属性。realuser@gmial.com 这样的地址在语法上是有效的,但因为拼写错误而无法投递;someone@validformat.test 能通过大多数模式,但使用的是一个永远无法投递的保留 TLD。只有成功投递的确认邮件才能证明一个地址能接收邮件。
为什么邮件正则表达式会拒绝像 name+filter@gmail.com 这样的有效地址?
加号寻址是完全有效的,因为加号在 RFC 5322 第 3.2.3 节和 RFC 5321 第 4.1.2 节中是普通的 atext 字符。拒绝加号寻址的模式,连同拒绝 .photography 等长 gTLD 或 RFC 6531 中定义的国际化地址的模式,并不是在严格验证——而是在犯错。这些假阴性之所以进入生产环境,是因为测试套件使用的是 test@example.com,这个地址能通过所有模式,导致对真实地址的拒绝在测试中从未暴露出来。