写了一个邮箱验证 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,有免费额度。
相关推荐
zttbee2 小时前
npm install卡顿
前端·npm·node.js
无责任此方_修行中3 小时前
如何不用括号调用 JS 的函数?
前端·javascript·node.js
无责任此方_修行中3 小时前
Deno 的八年:一场没逃出 Node 引力的运行时起义
前端·javascript·node.js
王大师王文峰6 小时前
HTML 入门到进阶:语义化标签、表单与多媒体一篇打通【语义化布局、表单校验、video/audio、Canvas、无障碍与 SEO】
前端·css·node.js·html·css3·jquery·html5
Flynt17 小时前
axios 被投毒半年后,我把 npm 的"安全防线"实测了一遍:最信任的 SHA512 是安慰剂
前端·npm·node.js
T_Wizard1 天前
Windows 下 NVM 踩坑实录:命令找不到、下载超时、C盘爆满、环境冲突一次搞定
node.js
第404号工位1 天前
40块钱二手瘦客户端,如何跑满家庭物联网中枢?(拒绝Docker爆盘实录)
linux·架构·node.js
定栓2 天前
Webpack迁到Vite第七节(二):全家桶移除 — Babel 全家桶深度分析
前端·webpack·node.js
wx47662 天前
用代码做视频:我把一个 HTML 页面变成了可复现的 MP4
node.js