邮箱验证的正确姿势:RFC 5322标准与实战
📋 目录
- 引言
- 一、RFC 5322 到底规定了什么
- 二、"完整实现 RFC"是一个陷阱
- 三、工程视角:我们要的其实是"可投递性"
- 四、ValidX @Email 规则解剖
- 五、@Email 与 RFC 5322 的差异清单
- 六、实战:注册表单与批量导入
- 七、格式之外:MX、可达性与归属验证
- 八、边界与坑
- 总结
- 项目地址
引言
邮箱校验大概是互联网上被误解最深的校验规则——它看起来只是"有没有@和点号",但一深究就涉及一整套 RFC 语法、DNS 体系、以及无数工程取舍。
先给结论:"校验邮箱"是三件完全不同的事:
| 层次 | 回答的问题 | 手段 |
|---|---|---|
| 格式 | 长得像不像合法邮箱? | 正则 / RFC 语法 |
| 可达 | 这个域名存在吗?能收信吗? | DNS MX 查询 |
| 归属 | 是用户本人的邮箱吗? | 发验证邮件、点链接 |
格式校验只是入场券。本文聚焦第一层:RFC 5322 标准到底说了什么、为什么工程上没人"完整实现"它、以及 ValidX 的@Email在这份标准与工程现实之间选择了哪条路线。
一、RFC 5322 到底规定了什么
RFC 5322 定义了 Internet Message Format(邮件消息格式),其中邮箱地址(addr-spec)的 ABNF 骨架是:
addr-spec = local-part "@" domain local-part = dot-atom / quoted-string domain = dot-atom / domain-literal拆开看每个概念:
| 概念 | 规则 | 合法示例 |
|---|---|---|
local-part | 用点号分隔的atom,或整体加引号的quoted-string | john.doe、"john doe" |
atom | 由atext字符组成:字母数字 +!#$%&'*+-/=?^_{ | }~` |
domain | 点号分隔的域名标签,或方括号 IP | example.com、[192.168.0.1] |
| 兼容层 | obs-*允许注释(...)、折叠空白几乎随处插入 | john(备注)@example.com |
几点 RFC 的"反直觉"事实:
- local-part 大小写理论上敏感——
John@example.com与john@example.com在标准上可以是两个不同邮箱; - quoted-string 是合法格式——
"john doe"@example.com完全符合规范(虽然几乎没人这么注册); - domain 可以是方括号 IP——
user@[192.168.0.1]合法,甚至user@[IPv6:2001:db8::1]; - 注释、多余空白是合法输入的一部分——RFC 允许
(收件人)user@example.com这种带注释的写法; - 长度限制不在 RFC 5322,而在配套的 RFC 5321(投递协议):local-part ≤ 64 字符、domain ≤ 255 字符、整条路径 ≤ 256 字符。
看懂这张地图就知道:RFC 5322 定义的是一份"格式上限",而不是"可用性下限"。它关心的是"消息能不能按语法被解析",从不保证"这个地址有人用、能投递"。
二、"完整实现 RFC"是一个陷阱
把 RFC 5322 的 ABNF 完整翻译成正则/状态机,你会得到一个"极其诚实但没人想要"的验证器——下面这些全是它眼里的合法地址:
"john doe"@example.com ← quoted-string user@[192.168.0.1] ← domain-literal(IP 域) user@[IPv6:2001:db8::1] ← IPv6 域 john(备注)@example.com ← 注释(obs- 兼容层)(注意:像John..Doe@example.com这种连续点即使在 RFC 5322 里也是非法的——dot-atom不允许空段,别被"RFC 很宽松"误导成"什么都能过"。)
完整实现还会带来四类实际问题:
① 把罕见的合法写全,等于把常见垃圾也放进来。quoted-string 里可以有空格、引号、几乎任何 ASCII——" "@example.com(内容为两个空格)语法上合法,你会想放行吗?
② 注释与空白规则引入无限复杂度。obs-兼容层是为了处理 1982 年 RFC 822 时代的旧邮件,现代投递体系早已不需要,但完整 ABNF 必须处理它们。
③ 工程上要的"格式正确"其实比 RFC 窄得多。绝大多数人是注册账号、填表单,他们只会用字母数字 + 点 + 常用符号写 local-part。为一个 0.001% 的场景(quoted-string)牺牲 99.9% 场景的清晰度,不值。
④ RFC 对现代域名体系几乎无约束。RFC 5322 的domain语法不限制标签长度、不要求顶级域存在、不知道 IDN(国际化域名)的存在。而现实中"格式正确"意味着能被 DNS 解析、能被 SMTP 投递——这层约束来自 DNS 协议和 ICANN 的域名政策,不在 5322 里。
所以行业共识是:参考 RFC 的语法骨架,叠加 DNS/域名体系的现实约束。主流验证库(Hibernate Validator、Apache Commons Validator 及 ValidX)走的都是这条路线,差异只在"收得多紧、放得多宽"。
三、工程视角:我们要的其实是"可投递性"
把 RFC 5322 的 ABNF 与 DNS 规则合并,得到一份工程上认可的"常识邮箱规则":
| 部分 | 规则 | 依据 |
|---|---|---|
| local-part | 字母数字 +_.+-等字符,点不能连续、不能在首尾 | RFC 5322 dot-atom 精神 |
@ | 恰好一个 | 语法 |
| 域标签 | 字母数字 + 连字符,不空 | DNS(RFC 1035) |
| 顶级域 | 至少 2 个字母 | 现实 TLD 政策(com/cn/io…) |
| 总长度 | ≤ 64(local) + ≤255(domain) | RFC 5321 |
| 大小写 | 接受大写,投递前建议小写化 | 投递实践 |
这套规则过滤掉 99% 的乱填(abc、a@b、a..b@x),同时不误伤正常地址(first.last+tag@sub.example.co.uk)。
四、ValidX @Email 规则解剖
ValidX 的@Email注解对应EmailValidator,内置正则如下(来自源码):
// EmailValidator.javaprivatestaticfinalStringEMAIL_PATTERN="^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";逐段拆解:
| 片段 | 约束 |
|---|---|
[a-zA-Z0-9_+&*-]+ | local-part 首段:字母数字 +_ + & * - |
(?:\.[a-zA-Z0-9_+&*-]+)* | 后续段必须以点开头——天然禁止连续点、首尾点 |
@ | 恰好一个@ |
(?:[a-zA-Z0-9-]+\.)+ | 域部分:至少一段"标签 + 点"(支持co.uk等多级域) |
[a-zA-Z]{2,7}$ | 顶级域:2~7 位纯字母 |
实测判定(与源码逐一核对):
| 输入 | 说明 | |
|---|---|---|
test@example.com | ✅ | 基础场景 |
first.last+tag@mail.example.co.uk | ✅ | plus 标签 + 多级域 |
USER+tag_1@example.com | ✅ | 大写、下划线均可 |
a-b@foo.com | ✅ | local 含连字符 |
123@qq.com | ✅ | 全数字 local |
user..name@example.com | ❌ | 连续点 |
.user@example.com/user.@example.com | ❌ | 首/尾点 |
plainaddress/a@b/a@b.c | ❌ | 缺@、缺域结构、TLD 过短 |
user@example-.com | ✅ | 见第五节,比 RFC 宽松的点 |
"john doe"@example.com | ❌ | quoted-string,工程路线不放行 |
user@[192.168.0.1] | ❌ | domain-literal,不放行 |
张三@qq.com | ❌ | 中文 IDN,需先转 punycode(见第八节) |
user name@example.com | ❌ | 未加引号的空格 |
使用方式与其它注解一致:
publicclassSignUpDTO{@NotBlank@Email// 邮箱地址格式不正确privateStringemail;@NotBlank@ChinesePhoneprivateStringphone;}默认消息{io.github.vipxieliang.validx.annotation.email},中文为"邮箱地址格式不正确",9 种语言内置,也支持message覆盖与groups。
五、@Email 与 RFC 5322 的差异清单
ValidX 的路线是"RFC 5322 骨架 + DNS 现实",因此与 RFC 完整语法有明确差异,多数是刻意为之:
| 能力 | RFC 5322 | ValidX @Email | 评价 |
|---|---|---|---|
| quoted-string | 允许 | 拒绝 | 刻意收窄,工程正确 |
| domain-literal(IP 域) | 允许 | 拒绝 | 刻意收窄 |
| 注释 / 折叠空白 | 允许 | 拒绝 | 刻意收窄 |
| local 大小写敏感 | 标准如此 | 接受任何大小写 | 与投递实践一致 |
| 域标签首/尾连字符 | 语法宽松 | 放行 | 比 DNS 规则松,见坑 4 |
| 顶级域长度 | 无限制 | 2~7 位字母 | 会误伤部分新 gTLD,见坑 5 |
| 域标签长度 | 需叠加 RFC 1035 | 未单独限长 | 随总长兜底 |
| IDN(中文域名) | 需 punycode | 拒绝中文 | 需调用方先转码 |
一句话总结差异哲学:RFC 允许的,ValidX 大多拒绝了(quoted/注释/IP 域);RFC 没管的,ValidX 按 DNS 规则收紧了(TLD 结构)。两处"放松"(域标签首尾-、TLD 上限 7)则是下一步要注意的边界。
六、实战:注册表单与批量导入
场景一:注册表单(注解模式)
publicclassRegisterDTO{@NotBlank@Email(message="请输入正确的邮箱地址")privateStringemail;@NotBlank@ChinesePhoneprivateStringphone;@NotBlank@Size(min=6,max=20)privateStringpassword;}场景二:CRM 批量导入(链式 API)
没有 DTO 的脚本/导入场景,用链式 API 逐行校验并收集全部错误:
List<String>lines=readCsv();// 读取待导入邮箱ValidXv=ValidX.init();for(Stringraw:lines){Stringemail=raw.trim().toLowerCase();// 先 trim + 小写化if(v.isEmail(email).failed()){// 链式:失败即记入 errorsbadLines.add(email+" -> "+v.getErrors());}}链式 API 的语义:
isEmail(null)返回合法(空值放行),所以必填校验要前置——导入场景先判blank,再用isEmail判格式,职责分离。
场景三:写进 Spring Validation
@PostMapping("/signup")publicResultsignup(@Valid@RequestBodyRegisterDTOdto){// 进入这里时 @Email/@NotBlank 已全部通过returnuserService.register(dto);}七、格式之外:MX、可达性与归属验证
格式通过 ≠ 邮箱存在。三道防线按成本递增:
① DNS 检查(防格式正确但不存在的域名)
user@nonexistent-domain-xyz.com格式完全合法,但域名没人注册。注册/导入场景可以先查该域的 MX 记录(有邮件服务器的域一定有 MX,根域可能只有 A 记录,做 MX 兜底查 A):
// 伪代码:查 MX,无 MX 再查 Abooleanreachable=hasMxRecord(domain)||hasARecord(domain);注意:example.com、test.com、.test、.invalid、.local等保留/测试域名永远能通过格式校验,但不可用于真实业务。
② 发送验证邮件(防冒用他人邮箱)
"邮箱是不是本人的"只能靠验证邮件 + 点击链接闭环,这是所有认真做注册的产品的标配。格式校验管第一道,验证链接管最后一道。
③ 业务防刷(防批量注册垃圾邮箱)
同 IP 注册频率、一次性邮箱域名黑名单、邮箱 + 验证码二次确认——格式层之上的工程手段。
分层责任表:
| 层 | 谁负责 | ValidX 角色 |
|---|---|---|
| 格式 | @Email | ✅ 本文主角 |
| 域名可达 | 业务代码 + DNS | 需自行实现 |
| 归属 | 邮件服务 + 链接 | 需自行实现 |
八、边界与坑
1. 空值放行,必填交给@NotBlank
@Email对null/空串返回合法——"是否必填"和"格式是否对"是两件事,永远拆开声明:
@NotBlank// 管必填@Email// 管格式privateStringemail;2. 中文邮箱(IDN)先转码再校验
张三@qq.com这类国际化地址要先转成 ASCII 形式(punycode)再交给@Email:xn--...@qq.com。Java 自带java.net.IDN:
Stringascii=IDN.toASCII(localPartOrDomain,IDN.ALLOW_UNASSIGNED);多数业务直接拒绝中文邮箱(提示用户用字母注册),@Email默认行为正好如此——转码支持是可选增强,不是必须。
3. plus 标签是合法的,别误杀
user+tag@example.com(Gmail/Outlook 别名)能过@Email,这是特性不是漏洞。要提醒的是存库时别把+后缀当唯一性的一部分,同一用户的多个 tag 是同一个收件箱。
4. 域标签首/尾连字符:@Email 放行,DNS 不允许
user@example-.com、user@-example.com能通过@Email(正则里标签字符集含-且未约束首尾),但DNS 标签规则禁止首尾连字符,这类域名现实中不存在,也不会被解析。若想与 DNS 规则对齐,可在通过@Email后用域名解析兜底(解析失败即拒绝)——格式层放宽一格、DNS 层收紧一格的组合拳,比单靠正则更接近真相。
5. 顶级域上限 7 位会误伤部分新 gTLD
a@example.technology(10 位)、.photography、.international等 ICANN 新顶级域会被[a-zA-Z]{2,7}拒绝。取舍说明:
- 如果你的用户几乎全是中国大陆邮箱(
.com/.cn/.qq.com等),2~7完全够用且能挡掉更多乱填; - 如果做海外业务且要放行新 gTLD,
@Email当前无参数可调,需要自定义一个放宽版约束(把 TLD 上限提到 63 位字母数字)。
6. 大小写与收尾处理
local-part 大小写理论敏感、实践投递基本都小写化处理。存库前统一trim().toLowerCase(),避免A@X.com和a@x.com在库里变成两条记录。
7. 别用一条正则同时校验"多邮箱"
a@x.com;b@y.com、逗号分隔的多个地址无法被@Email直接处理——先按分隔符拆分、逐条校验、再收集错误,而不是写一条"万能正则"。
总结
| 观点 | 结论 |
|---|---|
| RFC 5322 是什么 | 邮箱格式上限,允许 quoted-string、注释、IP 域等罕见写法 |
| 为什么不全实现 | 完整 ABNF 会放行垃圾、管不住 DNS 现实,工程上无价值 |
| ValidX 的路线 | RFC 骨架 + DNS 约束:local@多级域.2~7位字母TLD |
| 典型收窄 | quoted-string、domain-literal、注释、中文 IDN 一律拒绝 |
| 两处放松 | 域标签首尾-放行、7 位 TLD 上限(新 gTLD 需自定义放宽) |
| 格式之外 | MX/域名可达、验证邮件、业务防刷——@Email只管第一层 |
邮箱验证的正确姿势:用@Email守好格式关,在它之上用 DNS 验证守可达关,用验证邮件守归属关——每道防线只做自己该做的事,而不是指望一条正则解决所有问题。
项目地址
- GitHub:https://github.com/vipxieliang/ValidX
- Gitee:https://gitee.com/vipxieliang/ValidX
- Maven Central:https://central.sonatype.com/artifact/io.github.vipxieliang/validx