title: "[ISSUE-0023] mailto 链接 URL 编码 body 污染邮箱字段" description: 邮箱正则把 mailto: 链接的 ?body=%xx 编码字符串当邮箱本地部分抓进来 tags: [issue, scraping, email, regex] created: 2026-05-26 updated: 2026-05-26 type: issue status: fixed severity: high fixed_version: v0.10.33
v0.10.51 Tool ③:列「修复后全仓应消失的旧模式」,commit hook 反查防漏修姊妹¶
audit_grep: - pattern: "\[a-zA-Z0-9\._%\+-\]\+@\[a-zA-Z0-9\.-\]\+\\\.\[a-zA-Z\]" description: "旧 EMAIL_REGEX 字符类含 % + 量词 + 无上限。修复后 src/ 应 0 命中。v0.10.47 才发现 storage-data.ts 漏修就是这种姊妹漂移。" related: - "解析层开关"
[ISSUE-0023] mailto 链接 URL 编码 body 污染邮箱字段¶
相关源码:
src/utils/scraper.ts(邮箱正则 + 提取逻辑)
用户感知的现象¶
截图展示邮箱列表中出现一条 168 字符的怪邮箱:
%20i%20encountered%20an%20error%20and%20need%20support.%0d%0a%0d%0a966df647f3badc9e28832fdc03580ecb%0d%0a%0d%0a%3a%0d%0adigitalcare@dollargeneral.com
URL 解码后:
i encountered an error and need support.
966df647f3badc9e28832fdc03580ecb
:
digitalcare@dollargeneral.com
明显是 mailto: 链接的 ?body=... 参数被当成邮箱本地部分。
根因分析¶
源 HTML 大概率是这种"举报错误"模板按钮:
<a href="mailto:?subject=...&body=i encountered an error and need support.%0d%0a%0d%0a
966df647...(错误 ID)%0d%0a%0d%0a:%0d%0adigitalcare@dollargeneral.com">举报错误</a>
注意 mailto 的 to 字段为空,邮箱地址塞在 body 末尾(用户写邮件时模板带的联系方式)。
三个叠加的设计缺陷(src/utils/scraper.ts:64):
| # | 缺陷 | 后果 |
|---|---|---|
| ① | 字符类含 % |
URL 编码 %20 %0a %3a 全被当合法本地部分 |
| ② | 量词 + 无上限 |
贪婪匹配到 64+ 字符也不停(RFC 5321 限 64) |
| ③ | 不解 mailto: | 直接对原始 HTML 跑正则,错过结构化机会 |
修复方案(三层防御)¶
Layer 1:收紧 EMAIL_REGEX¶
// 旧
const EMAIL_REGEX = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
// 新
const EMAIL_REGEX = /[a-zA-Z0-9._+\-]{1,64}@[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?){1,5}/g;
变化:
- 移除 % —— 邮箱实际用 % 概率 < 百万分之一,移除避免 URL 编码污染
- 加 {1,64} —— RFC 5321 local part 上限
- 域名分段限制 ≤ 63 字符(RFC 1035)
Layer 2:先解 mailto:(结构化提取)¶
新增 extractMailtoEmails(html):扫 href="mailto:...",只取 ? 之前的 to 字段,
支持多收件人(逗号分隔),URL decode 后用 STRICT_RE 验证整段。
在主提取流程之前调用,给结构化邮箱最高优先级。
Layer 3:脏邮箱兜底过滤¶
新增 isEmailLikelyBroken(email):硬性拒绝特征:
- 本地部分 > 64
- 本地部分含 %xx URL 编码
- 含控制字符 / HTML 残留 `<>"'``
- 域名无点 / 连续点
加进 filterEmail() 入口;data-view 派生 emails 时也调用(运行时双保险)。
存量数据清洗¶
新建 src/utils/email-cleanup.ts 和 src/sections/data/email-cleanup-button.tsx:
- 邮箱 tab 头部按钮"清理脏邮箱"
- 扫全表 → 剔除符合 isEmailLikelyBroken 的 → 写回 IndexedDB
- 给用户展示扫描结果 + 样例
改动文件¶
| 文件 | 改了什么 |
|---|---|
src/utils/scraper.ts |
EMAIL_REGEX 收紧 + extractMailtoEmails + isEmailLikelyBroken + 主流程接入 |
src/utils/email-cleanup.ts 🆕 |
存量清洗工具 cleanupBrokenEmails |
src/sections/data/email-cleanup-button.tsx 🆕 |
邮箱 tab 头部清理按钮 + 结果对话框 |
src/sections/data/data-view.tsx |
邮箱派生时调用 isEmailLikelyBroken;邮箱 tab 标题右侧加按钮 |
验证方式¶
- 进邮箱列表 → 168 字符脏邮箱不再显示(运行时过滤)
- 点"清理脏邮箱" → 对话框扫描 → 显示"剔除 N 个" + 样例
- 后续抓取含相同 mailto 模板的网站 → 不再写入脏数据
- 单元测试用例(待补):
- 输入:
<a href="mailto:hi@x.com">→ 提取hi@x.com - 输入:
<a href="mailto:?body=%20...@dollar.com">→ 提取空(因 STRICT_RE 拒) - 输入:自由文本
Contact us at hello@acme.com.→ 提取hello@acme.com
如何避免再犯¶
- 邮箱正则字符类不要含
%——%xxURL 编码遍地,污染源远多于合法邮箱 - 凡是有
+*的字符类要加上限 ——{1,N}而非+,避免贪婪吃跨段 - 结构化优先:HTML 里
mailto:tel:链接是已知结构,先用href锚点抽取,再用正则扫散落文本 - 写入前过滤 + 渲染时过滤:两道防线都要有,新规则上线时即时生效,存量数据靠清洗按钮
相关问题¶
- ~~同类风险:手机正则也可能误捕 mailto 的
body=数字(待排查)~~ 已排查(v0.10.37 后):3 个手机正则TEL_LINK_REGEX/PHONE_CTX_REGEX/PHONE_TEXT_REGEX字符类分别是[\d\s\-().]和[\s.\-],都不含%, 遇 URL 编码%xx立刻停。无 mailto 污染风险 ✅ - v0.10.33 修复后,可能少抓几个含
%的合法邮箱(概率极低,可接受)