主邮箱里塞满了各种"注册即送"的营销邮件,真正重要的账单通知反而被淹没——这个场景我遇到过不止一次。后来我开始用临时邮箱在线生成工具处理那些"只用一次"的注册场景:随便生成一个地址,收一封验证邮件,用完就丢,主邮箱干干净净。但用得多了就会发现,这类工具背后其实是一套挺完整的邮件系统:域名解析、SMTP 收信、通配符捕获、过期清理、前端轮询,每一环都有坑。这篇就把我从"直接用现成站点"到"自己搭一套最小可用系统"的全过程拆开讲,包括随机地址怎么生成才安全、MX 记录为什么改完不生效、验证码邮件为什么总是慢半拍、以及哪些用法是绝对不能碰的。不管你是只想找个顺手的工具,还是打算自己写一个,看完都能少走不少弯路。
1. 把"随机"两个字拆开看:临时邮箱到底临时在哪
很多人对临时邮箱的理解停留在"一个能收信的网页",但真正决定它好不好用的,是三个看不见的部分:地址怎么来、邮件怎么进来、邮件怎么消失。这三件事任何一件没做对,工具就会表现为"能打开但收不到信"或者"昨天还能用今天全挂了"。先把这套链路的骨架理清楚,后面所有实操才有落脚点。
1.1 一封邮件从发件方到被丢弃的完整链路
邮件不是"发到网页上"的,它是先发到一台服务器,网页再去那台服务器上取。完整过程大致是这样:发件方的邮件服务器拿到收件地址后,先做一次 DNS 查询,问"这个域名的 MX 记录指向哪台机器";拿到目标主机后,用 SMTP 协议连上去,走一遍 EHLO、MAIL FROM、RCPT TO、DATA 的握手,把整封邮件的内容推过去;接收方的服务器确认收下,回一个 250;最后把邮件落到存储里,网页端再通过接口把这封邮件读出来展示。
临时邮箱的"临时"体现在最后一步之后:邮件落库时会带一个创建时间戳,后台有个定时任务按 TTL 扫一遍,超时的记录直接删掉。所以它并不是"邮件凭空消失",而是"存了一段时间后主动清理"。理解了这一点,你就明白为什么有些站点标称"10 分钟有效",但你 15 分钟后刷新还能看到——清理任务是定时的,不是到点精确触发。
提示:临时邮箱的"有效期"通常是逻辑有效期(前端根据时间戳判断是否展示),不等于物理删除时间。如果你在意隐私,别指望它到点就物理消失,自建时把清理周期设短一点更靠谱。
这里还有个容易被忽略的细节:SMTP 会话是有大小限制的。默认情况下很多服务端会把单封邮件限制在 10MB 左右,超过就直接拒绝。做临时收件箱时这个限制反而是好事,能挡掉一部分恶意投递,但也意味着如果有人给你发带大附件的邮件,你会看到"投递失败"而不是"收到了但没显示"。
1.2 "随机地址"不是乱敲键盘,它得能被 DNS 解析
这是新手最容易搞错的地方。你在网页上看到的k3j9x2m1@某个域名,不是服务器凭空变出来的,它必须落在一个你实际控制的域名下,而且这个域名的 MX 记录要指向你的收信服务器。随机只发生在"@ 前面那一段"。
那一段在邮件协议里叫 local part(本地部分)。随机生成它的时候有个基本原则:字符集要收窄、长度要够、随机源要安全。我见过不少玩具项目直接用random.choice加时间戳拼,结果两台机器在同一毫秒生成的地址撞了,两个用户的邮件混在一起——这种 bug 排查起来非常难受。
我自己的做法是只保留小写字母和数字,去掉0/o、1/l这类容易看错的组合,长度取 10 位。这样单域名的碰撞概率低到可以忽略,用户手动抄写也不容易出错。
import secrets import string # 去掉易混淆字符,降低人工抄写出错率 ALPHABET = "abcdefghjkmnpqrstuvwxyz23456789" def gen_local_part(length: int = 10) -> str: # secrets 基于操作系统熵源,比 random 更适合生成对外可见的标识 return "".join(secrets.choice(ALPHABET) for _ in range(length)) def gen_address(domain: str) -> str: return f"{gen_local_part()}@{domain}"为什么强调用secrets而不是random?因为random是伪随机,种子可预测,生成的地址序列存在被枚举的风险。临时邮箱本身不收敏感邮件,但地址被猜到意味着别人能读到你的验证邮件,这就不是小事了。多花一行代码换个安全随机源,性价比极高。
1.3 三种常见实现流派,决定了你后面所有工作
市面上的临时邮箱站点,底层大致分三派,选哪一派直接决定你的维护成本。
第一派是通配符收信,也就是配置一个 catch-all 规则,任何任意字符@你的域名的邮件全部收进来,靠收件人地址区分归属。这种方案用户体验最好,地址想怎么变就怎么变,不需要提前注册。代价是必须自己维护一台能收任意收件人的邮件服务,配置稍复杂。
第二派是预生成池,后台提前生成一批地址存进数据库,用户点"生成"就从池子里取一个没用过的。实现简单,但地址有上限,用完了得补,而且用户没法自定义。
第三派是API 转发,自己不收信,把域名托管给某个邮件推送服务,由对方收下后通过 webhook 推给你的接口。省掉了服务器运维,但依赖外部服务,免费额度有限,且邮件内容要过一次第三方的服务器。
| 流派 | 实现难度 | 地址灵活度 | 依赖 | 适合场景 |
|---|---|---|---|---|
| 通配符收信 | 中 | 任意生成 | 自有服务器 + 域名 | 想长期用、要完全可控 |
| 预生成池 | 低 | 只能从池里取 | 数据库 | 演示、教学、内部小工具 |
| API 转发 | 低 | 取决于服务商 | 第三方服务 | 不想碰运维、量不大 |
我最后选的是第一派。原因很实际:这个工具的核心价值就是"随手一个地址就能收",预生成池一旦撞上高峰期地址耗尽,体验直接崩掉;而 API 转发虽然省事,但每次排查问题都要去第三方后台看日志,链路长了排查效率反而低。
2. 到底什么场景才该用它,什么场景不该
工具本身没有对错,关键是拿它干什么。我梳理了自己实际用过的几类场景,也顺带说说哪些用法我从一开始就划了红线。这部分想清楚了,你设计功能时才知道该往哪个方向加东西。
2.1 低频注册时给主邮箱止损
最典型的就是"我只想试一下这个服务"。很多在线工具、素材站、小型 SaaS 在注册时就要求邮箱验证,验证完还会定期推营销邮件,退订链接藏在很深的设置页里。这种场景用临时地址,注册完拿到验证链接,点完就走,后面它想发什么都发不进来。
这里的关键判断是这个账号你以后还要不要。如果是要长期用的主账号,比如涉及支付、身份绑定、重要资料的,绝对不能用临时邮箱——因为找回密码、安全通知都依赖这个地址,地址一过期,账号就等于丢了。我自己的分界线是:能在一个月内丢弃的账号,才用临时地址。
还有一个细节:有些服务会在注册后几天再发一封"确认你是否活跃"的邮件,不点就冻结账号。用临时邮箱注册这类服务,体验会很差。所以注册前扫一眼它的邮件策略,比注册后再后悔强。
2.2 开发阶段验证注册与找回密码链路
这是我认为临时邮箱最有价值的用途,而且完全是正当需求。做用户系统的时候,注册验证、邮箱变更、密码重置、邀请链接,这四条链路都依赖真实收信。用公司域名批量建测试账号成本高,用自己的私人邮箱又容易污染,临时邮箱刚好补上这个缺口。
具体做法是:写自动化测试时,先调用临时邮箱的接口生成一个地址,拿这个地址去调你系统的注册接口,然后轮询收件箱接口,等验证邮件到达,再用正则把验证码或链接抠出来,继续走下一步。整条链路可以完全无人值守。
import re import time import requests def wait_for_code(address: str, api_base: str, timeout: int = 120) -> str: deadline = time.time() + timeout pattern = re.compile(r"\b(\d{6})\b") # 六位数字验证码 while time.time() < deadline: resp = requests.get(f"{api_base}/api/mails", params={"rcpt": address}, timeout=5) mails = resp.json().get("items", []) for mail in mails: # 先匹配主题,再匹配正文,命中率更高 text = f"{mail.get('subject', '')}\n{mail.get('body', '')}" hit = pattern.search(text) if hit: return hit.group(1) time.sleep(2) raise TimeoutError(f"{address} 在 {timeout}s 内未收到验证码")这段代码里有个我踩过坑的地方:只匹配正文会漏。有些系统把验证码写在邮件主题里,有些写在图片里(这种就真的没法自动化了),还有些邮件是 multipart 结构,纯文本部分可能是空的,验证码只在 HTML 部分。所以实际匹配前要先把各个部分拼起来,不能只取body。
2.3 演示、培训与临时协作
第三个场景是做演示。给同事讲"邮件通知是怎么触发的",总不能拿真实客户地址演示。临时邮箱这时候就像一个一次性的沙盒,演示完地址自动过期,不会有任何残留。同理,临时拉一个小组做短期项目,需要一个公共收件箱接收外部反馈,用完即弃,也很合适。
但这里必须划清界限:临时邮箱不能用于批量注册、绕过平台风控、刷取新用户权益、发送垃圾邮件。这三条不是技术问题,是原则问题。任何工具被拿去干这些事,最终都会反噬到工具本身——域名被拉黑、IP 被拒收,最后正常用户也用不了。我自建时第一件事就是加了限流,单个 IP 每分钟最多生成 3 个地址,收件接口按地址做了频率限制,这不是防君子,是防止自己的系统被当成免费的基础设施。
2.4 一个反直觉的结论:收信域不需要配 SPF
很多教程一上来就让你配 SPF、DKIM、DMARC,搞得好像不配就收不到信。这里需要澄清一个常见的误解:SPF/DKIM/DMARC 是给"发信域"用的,用来证明"这封邮件确实是我发的"。你做一个纯收件的临时邮箱,角色是接收方,这三个记录跟你的收信能力没有关系。
那为什么有人配了之后感觉"好像好了一点"?大概率是同时配了别的东西,比如正确的 MX 优先级、有效的 A/AAAA 记录、PTR 反向解析。真正影响收信成功率的,是这几项:
- MX 记录存在且指向可达的主机
- 该主机 25 端口对外开放
- 主机有正确的反向解析(PTR),部分严格的发件方会做这项检查
- 域名不在主流黑名单里
我第一版就是被"必须配 SPF"的说法带偏,花了两小时配了一堆发信记录,结果收信依然失败,最后发现真正的原因是云主机的 25 端口默认没放行。
3. 搭一套最小可用系统:从域名解析到前端取件
目标定得很低:一个域名、一台小机器、一个能收任意收件人的服务、一个能生成地址并展示邮件的网页。功能不多,但每一块都有需要想清楚的地方。下面按实际搭建顺序走一遍。
3.1 域名与 MX 记录:最容易卡住的第一关
先准备一个独立域名,不要拿主站域名来玩。原因很简单:临时邮箱天然会吸引各种杂七杂八的邮件,域名信誉容易受影响,跟主业务混在一起风险不值得。
拿到域名后,添加 MX 记录,指向你的收信主机。这里有个参数必须理解:优先级(Priority)。数值越小优先级越高,只有一条记录时随便填个 10 就行,多条记录时才会按优先级依次尝试。我见过有人填了 0,某些发件方会认为这是无效配置,稳妥起见填 10 或者 20。
改完之后别急着测试,DNS 有缓存。用命令查一下实际生效情况:
# 查 MX 记录,确认指向正确 dig +short MX your-domain.example # 查主机本身的解析,确认 A 记录存在 dig +short A mail.your-domain.example # 从公共 DNS 服务器再查一次,排除本地缓存干扰 dig @1.1.1.1 +short MX your-domain.example如果本地查得到、公共 DNS 查不到,说明还没传播完,等几分钟到几小时都正常。如果两边都查不到,先检查记录是不是加错了主机名(比如把mail.前缀漏了),这是最常见的失误。
注意:部分云服务商默认会限制 25 端口的入站和出站访问,需要在控制台单独申请放行。这一步不做,MX 配得再对也收不到信,而且报错信息通常很含糊,容易让人误以为是 DNS 问题。
3.2 收信端:catch-all 的两种落地方式
收信端有两条路。一条是用成熟的邮件服务软件,通过虚拟别名表把@你的域名全部映射到一个本地邮箱,配置量中等,胜在稳定,日志和队列管理都是现成的。另一条是自己写一个极简的 SMTP 服务,只实现收信、落库两个动作,代码量很小,可控性最强。
我选了自己写,因为临时邮箱根本不需要队列、重试、转发这些能力,用完整邮件服务属于杀鸡用牛刀,配置文件看半天还不如几十行代码直观。用 Python 的话,一个轻量的 SMTP 服务库就能搞定。
import sqlite3 import time from email import message_from_bytes from email.header import decode_header from aiosmtpd.controller import Controller DB_PATH = "mailbox.db" def init_db(): conn = sqlite3.connect(DB_PATH) # WAL 模式让读写并发更顺,收信和网页查询不会互相卡住 conn.execute("PRAGMA journal_mode=WAL") conn.execute(""" CREATE TABLE IF NOT EXISTS mails( id INTEGER PRIMARY KEY AUTOINCREMENT, rcpt TEXT NOT NULL, sender TEXT, subject TEXT, body TEXT, created_at INTEGER NOT NULL ) """) conn.execute("CREATE INDEX IF NOT EXISTS idx_rcpt_created ON mails(rcpt, created_at)") conn.commit() conn.close() def decode_mime(value: str) -> str: if not value: return "" parts = decode_header(value) out = [] for text, charset in parts: if isinstance(text, bytes): out.append(text.decode(charset or "utf-8", errors="replace")) else: out.append(text) return "".join(out) def extract_body(msg) -> str: # 优先取纯文本,取不到再退回 HTML,避免大量标签干扰匹配 if msg.is_multipart(): plain, html = "", "" for part in msg.walk(): ctype = part.get_content_type() if part.get_content_maintype() == "multipart": continue payload = part.get_payload(decode=True) or b"" text = payload.decode(part.get_content_charset() or "utf-8", errors="replace") if ctype == "text/plain": plain += text elif ctype == "text/html": html += text return plain or html payload = msg.get_payload(decode=True) or b"" return payload.decode(msg.get_content_charset() or "utf-8", errors="replace") class CatchAllHandler: async def handle_DATA(self, server, session, envelope): rcpt = envelope.rcpt_tos[0].lower() msg = message_from_bytes(envelope.content) conn = sqlite3.connect(DB_PATH) conn.execute( "INSERT INTO mails(rcpt, sender, subject, body, created_at) VALUES(?,?,?,?,?)", ( rcpt, envelope.mail_from, decode_mime(msg.get("Subject")), extract_body(msg)[:200000], # 截断,防止单封邮件撑爆存储 int(time.time()), ), ) conn.commit() conn.close() return "250 Message accepted for delivery" if __name__ == "__main__": init_db() controller = Controller(CatchAllHandler(), hostname="0.0.0.0", port=25) controller.start() print("catch-all smtp listening on :25") try: while True: time.sleep(3600) except KeyboardInterrupt: controller.stop()这段代码有几个刻意为之的设计。第一,envelope.rcpt_tos[0]只取第一个收件人——临时邮箱的语义就是一址一信,多收件人的情况极少,不处理反而简单。第二,正文做了 20 万字符截断,防止有人投递超大正文把 SQLite 写爆。第三,建了(rcpt, created_at)的联合索引,因为前端查询永远是"按地址查最近若干封",这个索引能直接命中。
一个必须提醒的点:这个服务不会做任何发件人校验。它接受任何来源的投递,这是临时邮箱的固有属性。所以千万不要把它放在公网裸奔而不加限流,否则很容易被当成开放中继来试探。虽然它不收发信,但大量垃圾投递也会拖垮你的数据库。
3.3 存储与过期:别等磁盘满了才想起清理
邮件是只增不减的数据,一封带 HTML 的验证邮件动辄几十 KB,一天几百封就是几十 MB。如果不做清理,几个月后磁盘告警是必然的。
清理策略我采用的是"滚动窗口 + 定时任务"。给每封邮件打上创建时间,定时任务每 5 分钟跑一次,删掉超过 1 小时的记录。为什么不删得更勤?因为删太勤会打断正常的收信体验——用户生成地址、去注册、等邮件、回来刷新,这个流程走完可能就要两三分钟,TTL 设得太短反而会让人看到空收件箱。
import sqlite3 import time TTL_SECONDS = 3600 # 1 小时 def purge_expired(): conn = sqlite3.connect("mailbox.db") cutoff = int(time.time()) - TTL_SECONDS cur = conn.execute("DELETE FROM mails WHERE created_at < ?", (cutoff,)) conn.commit() deleted = cur.rowcount conn.close() return deleted跑一段时间后我发现一个细节:单纯DELETE不会立刻释放磁盘空间,SQLite 会把这些页标记为可复用但文件大小不变。如果你的场景对磁盘占用敏感,可以定期执行一次VACUUM,但注意它会锁库,别在高峰期跑。
另一件事是收件人地址的生命周期要和邮件解耦。地址本身不需要存库——反正是通配符,任何前缀都合法。但如果你想做"地址失效"的功能(比如生成后 30 分钟不再接收),就需要一张地址表记录创建时间,收信时先查这张表。我第一版没做,结果有人拿着半年前的地址还在收信,虽然无害,但统计口径会很乱。
3.4 前端接口:轮询、长轮询还是推送
网页端要做的事其实就三件:生成地址、查询该地址的邮件列表、展示邮件内容。生成地址是纯本地计算(随机串 + 域名),不需要请求后端;查询列表是核心接口。
最朴素的方案是前端每 3 秒轮询一次。缺点是空查询多,优点是实现极简、断线自动恢复、不需要维护连接状态。对于临时邮箱这种低频场景,轮询完全够用,我实测下来一个地址平均 2 到 5 次轮询就能拿到验证邮件。
from flask import Flask, request, jsonify, abort import sqlite3 import time app = Flask(__name__) @app.get("/api/mails") def list_mails(): rcpt = (request.args.get("rcpt") or "").lower().strip() if not rcpt or "@" not in rcpt: abort(400, "invalid address") conn = sqlite3.connect("mailbox.db") rows = conn.execute( "SELECT id, sender, subject, body, created_at FROM mails " "WHERE rcpt = ? ORDER BY created_at DESC LIMIT 20", (rcpt,), ).fetchall() conn.close() return jsonify({ "items": [ {"id": r[0], "sender": r[1], "subject": r[2], "body": r[3], "created_at": r[4]} for r in rows ], "server_time": int(time.time()), })接口层面有两个经验。一是返回server_time,前端可以用它和邮件的created_at做差值,显示"3 分钟前收到",比让浏览器本地算时间可靠得多——用户设备时间不准的情况太常见了。二是限制返回条数,我设的是 20 条,因为临时邮箱的使用模式决定了用户只关心最新的一两封,一次返回全部历史既慢又没必要。
至于长轮询和 WebSocket,我试过,在这个场景里收益不明显。临时邮件的等待时间本来就在几十秒量级,3 秒的轮询延迟用户根本感知不到,反而多了一堆连接管理的复杂度。什么时候该上长轮询?是你的用户量上来了、空轮询把接口打满的时候,那时候再换也不迟。
4. 邮件"发了但收不到":完整排查链路复盘
这部分是我踩坑最多的地方,也是我认为最值得写的部分。因为这些问题往往没有明确报错,只有一个现象:网页上刷新半天,就是没有邮件。下面按我实际的排查顺序还原一遍,你可以直接照着走。
4.1 先分清是"没投递"还是"没显示"
第一步永远是把问题分层,别一上来就改代码。判断方法很简单:直接看数据库里有没有这封邮件。有记录说明收信链路是通的,问题出在前端查询或展示;没记录说明邮件根本没到达,问题在 DNS 或网络层。
# 直接查库,绕开所有前端逻辑 sqlite3 mailbox.db "SELECT id, rcpt, subject, created_at FROM mails ORDER BY id DESC LIMIT 10;" # 同时在服务器上抓包,看 25 端口有没有连接进来 sudo tcpdump -i any -n port 25 -c 20抓包这招特别好用。如果抓不到任何来自外部的连接,说明发件方压根没找到你的服务器,问题在 DNS 或者端口不通。如果能看到连接但会话在中途断掉,通常是 TLS 协商失败或者服务端返回了非 250 的响应码。
我第一次排查时就是靠抓包定位的:连接进来了,但每次都在 RCPT TO 之后被拒。原因是我早期用的方案要求收件人必须存在于用户表里,而随机生成的地址当然不在表里。改成通配符匹配后立刻正常。
4.2 MX 生效了,为什么大厂还是把邮件退回来
第二个高频问题是:自己能收到某些来源的邮件,但某个特定的发件方一直收不到。这种情况八成不是你的配置错了,而是对方的投递策略更严格。
常见原因和对策我整理成了下面这张表:
| 现象 | 可能原因 | 验证方式 | 处理方向 |
|---|---|---|---|
| 特定发件方始终无邮件 | 对方拒绝投递到新建域名 | 查看对方是否给出退信 | 换用已有一定使用历史的域名 |
| 邮件延迟数分钟后才到 | 对方做了灰名单延迟投递 | 多次测试观察规律 | 属正常行为,前端提示"可能延迟" |
| 收到但内容为空 | 解析 multipart 时漏了 HTML 部分 | 打印原始邮件内容 | 修正正文提取逻辑 |
| 间歇性丢信 | 服务器负载高或连接超时 | 看服务端日志时间戳 | 提高并发处理能力 |
| 全部来源都收不到 | 25 端口未放行或 MX 未生效 | 抓包 + dig 查询 | 逐项排查网络与 DNS |
"新建域名被区别对待"这条我印象最深。刚注册没几天的域名,在某些发件方眼里信誉分很低,投递时会被更严格地审查,甚至直接不投。这不是配置能解决的,只能靠时间养——正常收发一段时间,信誉会慢慢上来。所以如果你打算长期做这件事,域名最好提前准备,别等到要用的时候才注册。
4.3 验证码邮件的两个隐藏陷阱
即使邮件顺利到达,提取验证码这一步仍然可能翻车。我遇到过两次,都很典型。
第一次是编码问题。某封邮件的主题是经过 MIME 编码的中文,直接msg["Subject"]拿出来是一串=?utf-8?B?...?=的乱码。如果你的正则匹配的是正文里的中文关键词,就会全部失效。解决办法就是前面代码里那个decode_header的处理,把编码后的片段还原成可读文本。
第二次是结构问题。有些系统发的验证邮件是 multipart/alternative,同时包含纯文本和 HTML 两个版本。纯文本版本里验证码可能被写成"您的验证码是:123456",而 HTML 版本里是排版精美的表格。如果你只取第一个 part,很可能拿到的是纯文本,这本来没问题;但如果某些邮件的纯文本部分是空的(只写了"请使用支持 HTML 的客户端查看"),那就什么都匹配不到。所以正文提取必须遍历所有 part,按类型分别拼接,最后优先返回有内容的那一份。
还有一个更隐蔽的坑:验证码可能是动态的。有些系统会在邮件发出的同时让验证码进入短时效状态,比如 5 分钟内有效。如果你的轮询间隔是 10 秒,加上网络延迟,实际留给用户的时间可能只剩 4 分钟。这不是技术 bug,是产品设计要考虑的事,前端最好直接把"剩余有效时间"显示出来。
4.4 并发写入与清理任务的冲突
这个问题在小流量时根本不会出现,一旦有几十个人同时用就会冒出来。现象是:偶尔有邮件明明收到了,但刷新几次后又消失了。查日志发现是清理任务把它删了。
根因是时间判断的精度。我用的是秒级时间戳,创建时间是int(time.time()),清理时用cutoff = now - TTL。如果一封邮件恰好在整秒边界创建,而清理任务又在同一秒执行,就可能出现边界情况下的误删。改成毫秒级时间戳,或者在清理条件上加一个安全余量(比如TTL + 60),问题就消失了。
另一个并发问题是 SQLite 的写锁。收信服务和清理任务同时写库时,后到的那个会拿到database is locked。开 WAL 模式能缓解大部分场景,但更稳妥的做法是让清理任务和收信服务错峰,或者给写操作加个短暂的超时重试。
import sqlite3 import time def connect_with_retry(path: str, retries: int = 3): for i in range(retries): try: conn = sqlite3.connect(path, timeout=5) conn.execute("PRAGMA journal_mode=WAL") return conn except sqlite3.OperationalError: time.sleep(0.2 * (i + 1)) raise RuntimeError("database unavailable after retries")5. 自建还是用现成的:算清楚这笔账
写到这里,可能有人会问:为了收几封验证邮件,搞这么多东西值得吗?我的回答是看你的使用频率和可控性要求。这一节把两边的成本摆出来,你自己权衡。
5.1 时间成本、维护量与可控性的三角关系
用现成服务的成本几乎为零:打开网页,复制地址,收信,完事。但它有三个隐性代价。
第一是地址不可控。你不知道服务商什么时候关停、域名什么时候被拉黑。我遇到过一次,用了半年的某个站点突然打不开,之前用它注册的几个账号全部失去找回途径。第二是隐私边界不清。邮件内容要经过别人的服务器,虽然大部分服务声称不留存,但你无法验证。第三是不能集成。如果你想把收信能力嵌到自己的自动化流程里,没有 API 就只能靠模拟点击,非常脆弱。
自建的成本主要在前期的两三小时,以及后续的零星维护。真正麻烦的不是写代码,而是域名和网络这两块"外部依赖":域名要养、端口要申请、证书要续。但只要跑顺了,后面基本不用管。
| 维度 | 现成服务 | 自建 |
|---|---|---|
| 上手时间 | 1 分钟 | 2 到 4 小时 |
| 持续成本 | 免费或订阅费 | 域名 + 服务器 |
| 地址可控性 | 低 | 完全可控 |
| 隐私可控性 | 依赖服务方 | 完全自己掌握 |
| 能否接 API | 通常可以 | 自己定义 |
| 故障排查 | 只能等 | 可看全链路日志 |
| 适合人群 | 偶尔用几次 | 高频使用或要集成 |
如果你一个月只用两三次,老实说用现成的更划算,把时间花在别处。但如果你在做自动化测试、需要稳定的收信能力,或者对隐私有明确要求,自建的那点前期投入很快就能赚回来。
5.2 挑现成服务时我会看哪几个细节
如果决定先用现成的,有几个细节值得花两分钟确认,能避开大部分坑。
看地址有效期写的是"页面保留时间"还是"收信有效期"。有些站点标称 10 分钟,实际是 10 分钟后页面刷新就换新地址,但旧地址其实还在收信;也有些反过来,页面还在但邮件已经不收了。这两种体验差别很大。
看是否支持自定义前缀。能自定义的话,你在不同平台注册时可以用有区分度的前缀(比如带上平台缩写),后面回溯起来方便得多。完全不支持自定义的,地址一堆随机串,时间久了根本对不上号。
看有没有 API。哪怕你现在不用,将来想接自动化的时候,有 API 和没有 API 是天壤之别。有 API 意味着你可以写脚本批量走流程,没有就只能人工复制粘贴。
看是否明文展示原始邮件。能看原始内容(包括邮件头)的,排查问题会方便很多,比如你可以确认发件时间、查看是否被标记为垃圾邮件。只看渲染后内容的,遇到"邮件到了但内容空白"就只能干瞪眼。
6. 用之前必须想清楚的几条边界
技术上的东西讲完了,最后说几条我自己坚持的原则。这些不是技术限制,是使用者的自我约束,但它决定了这个工具能不能长期健康地用下去。
不要把临时邮箱当成隐私保护的全部。它保护的是"这个地址不和我的真实身份绑定",但邮件内容本身在传输过程中是否加密、经过哪些节点,是另一回事。真要处理敏感信息,临时邮箱不是合适的载体,该用正规的加密邮箱。
不要用它注册任何涉及资金或身份的账号。银行、支付、政务、医疗这类服务的账号,找回密码和安全通知全靠邮箱,地址一过期就等于把账号锁死了。这条线我从来没越过。
不要拿它去规避平台的正常规则。有些平台限制一个用户只能注册一个账号,用临时邮箱绕过这个限制,本质上是在钻规则空子。工具是中性的,但用法有对错,这一点没什么可含糊的。
自建的时候顺手加限流。哪怕只是给自己用,也建议加上基础的频率限制。你的服务一旦暴露在公网,被扫描到只是时间问题,没有限流就等于把带宽和存储白白送人。我加的是单 IP 每分钟 3 次生成、单地址每分钟 10 次查询,跑了大半年没出过事。
定期看一眼日志和磁盘。临时邮箱是典型的"平时不出问题,出问题就是硬故障"的系统。我现在的习惯是每周瞟一眼磁盘占用和异常日志,成本很低,但能避免半夜被告警叫醒。
说到底,这类小工具的乐趣不在于多复杂,而在于每一环你都知道它在干什么。知道 MX 记录为什么填 10、知道secrets和random差在哪、知道邮件消失是清理任务干的而不是"云端自动回收",遇到问题时就不会抓瞎。我最初只是想找个地方收验证码,最后却把整套邮件收发的链路摸了一遍,这大概是折腾这类小项目最大的意外收获。