写了一个邮箱验证 API 之后,我才知道的几个 SMTP 冷知识

最近做了一个批量邮箱验证的服务,给 n8n 工作流和 AI Agent 用。原以为就是查个 MX、连一下 SMTP 的事,实际做下来,坑比代码多。把几个最反直觉的点记下来。

1. 验证邮箱,一封邮件都不用发

流程是:语法校验(RFC 5322)→ 查 MX(没有 MX 就退回 A 记录)→ 一次性邮箱 / 角色邮箱 / 免费邮箱服务商名单 → SMTP 握手。

关键在最后一步:连上对方的 MX,依次发 EHLO、MAIL FROM、RCPT TO,读返回码,然后断开。全程不发 DATA ,所以不存在任何邮件正文。RCPT TO 返回 250 说明收件地址被接受,550 说明邮箱不存在。

2. Gmail 对不存在的地址也说「好」

Gmail、Outlook、Yahoo、iCloud 这类免费邮箱服务商,在 RCPT TO 阶段对任何地址都回 250,真正的退信要等投递时才发生。

结论很直接:通过 SMTP 无法确认某个 Gmail 邮箱是否存在。 声称能做到的,都是在猜。正确的做法是如实返回「无法确认」,只保留语法、域名、是否一次性邮箱这些确定的结论。

3. catch-all:先问一个不存在的人

有些域名配置成「照单全收」,不管地址存不存在都返回 250。如果不先识别出来,你会把一堆死地址标成有效。

做法是在验证真实地址之前,先对一个随机生成、不可能存在的地址发 RCPT TO(例如 zzq-nonexist-<随机串>@domain)。如果它也被接受,这个域名就是 catch-all,上面的任何单个地址都只能标记为「有风险」。

4. 「查不出来」≠「不存在」

这是最容易做错、代价也最大的一点:

  • 灰名单:421 / 450 是对方服务器的反垃圾手段------第一次拒绝、稍后重试就接受。它是「稍后再试」,不是坏地址。
  • DNS:权威的 NXDOMAIN 才说明域名不存在;解析器返回 SERVFAIL 或超时,只说明这次没查到。
  • 5xx 也要看内容 :如果拒绝理由里提到 blocklist、reputation、policy,那是在拒绝发件方,和收件邮箱存不存在没有关系,不能据此判定地址无效。
  • 反过来的情况 :Microsoft 365 对不存在的收件人常回 550 5.4.1 ... Access denied(目录边缘阻止,DBEB)。字面看像是策略拒绝,实际多半是收件人不存在。如果只按「denied」这类关键词归类,就会把一批死地址错标成「未知」。

把「未知」和「不存在」混在一起的工具,报表会好看很多,代价是悄悄删掉了真实客户。

5. 云上出站 25 端口基本被封

主流云厂商默认封禁出站 25 端口(防止被拿去发垃圾邮件)。在云函数 / 容器里直接做 SMTP 握手,连接会被静默丢弃,表现为全部超时,而且不会给你任何报错提示。

解决办法是在允许 25 端口的机器上跑一个 relay。另外两个细节对成功率影响很大:

  • HELO 用的主机名必须能解析,最好就是指向当前出口 IP 的域名;
  • 给出口 IP 设置 PTR,并与 HELO 名一致(正反向解析一致,FCrDNS)。不少邮件服务器会据此给连接打分。

6. 一个 Node.js 小坑:autoSelectFamily

Node.js 20 起,net.connect 默认开启 autoSelectFamily(Happy Eyeballs),每个地址的连接尝试超时 autoSelectFamilyAttemptTimeout 默认只有 250ms。对同时有 AAAA 和 A 记录、响应又慢的 MX,握手还没建立就被判失败,最终表现成超时------你自己设置的 8 秒超时根本轮不到生效。

连接 25 端口时显式关闭(autoSelectFamily: false),或者调大 attempt timeout,就好了。

7. 给机器用的 API,不该有 null

最后是输出设计。人看 dashboard 时,字段缺了能自己脑补;代码和 AI Agent 不行,Agent 拿到什么就按什么行动。

所以输出做了两个硬约束:status 永远是 deliverable / undeliverable / risky / unknown 四个值之一;11 个 checks 字段每条记录都在,没执行的检查就是 false,原因写在 sub_status 里。调用方可以直接写:

js 复制代码
const mailable = results.filter(r => r.status === 'deliverable');
const retryLater = results.filter(r => r.status === 'unknown');

配套的 n8n 模板(Google Sheets、CSV 清洗、注册拦截、AI Agent 工具)、可直接导入 Dify / 扣子 的 OpenAPI 文件、MCP 配置都开源在这里,中英双语:

<https://github.com/sumitrmardy/email-verifier-api/blob/main/README.zh-CN.md>

说明:模板是 MIT 协议;底层调用的是我在 Apify 上维护的一个按次计费的 Actor,有免费额度。
相关推荐
一缕82年的清风2 小时前
用了 3 年 Notion 和 Obsidian 后,为什么我最终把所有身家搬到了思源笔记?
node.js
Dumbness5 小时前
一份 JSON 生成增删改查接口,入参清洗漏了什么:Number(" ") 是 0,搜 100% 把 1000 元券也搜出来
node.js·express
FungLeo1 天前
成为全栈·基础补充·Node.js 运行时:事件循环与异步
javascript·node.js·事件循环·后端开发·异步编程·成为全栈
用户087308654872 天前
用 GUI 自动化写了一个群聊 AI:5 个真实事故,和 7 条让 AI 不像机器人的设计
node.js
Dumbness3 天前
毕设后台导出 CSV 的 4 个坑:date 字段漏了公式防护,搜索框输入 % 能扫全表
node.js·express
wno7045 天前
node.js中使用jquery
前端·node.js·jquery
damoluomu6 天前
Node 的 Headless 出厂没有前台,Java 的 Headless 是从门户里长出来的
java·开发语言·node.js
Dumbness6 天前
一个手机号验证码登录接口的五个坑:无限发码、无限试码,传个数字就让 Express 进程退出
node.js·express
怒放的生命19916 天前
本地用 pnpm 省磁盘,团队继续用 npm:极简实践
前端·npm·node.js