可自部署10分钟临时邮箱:SMTP收信、TTL清理与验证码提取 简介面向开发者与Web学习者的10分钟临时邮箱源码包围绕短时有效邮箱地址的生成、收信与到期失效流程适合用于注册验证、下载试用等不愿暴露真实邮箱的场景也可作为PHP邮件系统与Web服务设计的实践项目。压缩包共492个文件以312个PHP脚本为核心配合HTML页面、JavaScript交互、CSS样式及PNG/GIF图标资源另有SQL建库脚本、TXT安装与许可说明、XML配置等整体约1.02MB目录涵盖前台入口、管理端、缓存、公共函数库、功能模块、文档与配置等层次。已有9470人学习说明其在临时邮箱类源码中具备一定参考热度。读者可据此梳理邮箱申请、邮件查看与过期清理的实现路径理解数据库交互、模块划分和缓存机制并基于开源许可进行安全加固、性能优化或功能扩展适合作为课程设计、二次开发与源码研读的样本。1. 十分钟临时邮箱不是“用完即弃”先把生命周期和资源边界讲清注册测试账号、跑自动化脚本、临时收一次验证码最怕把主邮箱暴露出去后面垃圾邮件和撞库风险都跟着来。这套 10分钟临时邮箱 就是把这个场景做成了可自部署的临时邮箱源码创建地址后只保留十分钟邮件落库、接口可取、到期自动清理。它适合做联调、验收和自动化测试的人不适合当长期归档邮箱。下面按收信链路、参数和翻车点拆开讲先让这套东西在你自己的环境里跑成一个闭环再谈限流和压测。2. 拆开源码看链路从 MX 收信到 10 分钟过期的四段流水这套临时邮箱源码包常见的拆法是四段收信入口、邮件解析、接口读取、过期清理。四段之间不靠“定时扫全表”硬扛而是把邮箱地址、过期时间、邮件正文分别放在合适的位置。临时邮箱的生命周期很短十分钟内要完成“创建地址、外部发信、接口取信、验证码提取”任何一段慢了都会让调用方以为没收到。我一般先把这条链路画成可观测的断点收信进程有没有接到连接、解析后有没有入库、接口能不能按地址查到、清理任务有没有在 TTL 到点后把状态翻掉。下面先讲接法选型再讲状态放在哪最后给一个最小闭环的启动方式。2.1 邮件进入系统的三种接法MX 直收、API 转发与本地 SMTP第一种是 MX 直收把测试域名的 MX 记录指到你的收信入口外部邮件按 SMTP 协议直接投递进来。这种接法最接近真实收信适合验证域名解析、端口连通和反垃圾策略缺点是调试时要等 DNS 生效云上还要确认 25 端口是否可用。第二种是 API 转发由上游邮件服务把邮件以 JSON 或 webhook 形式推到你的接口适合没有固定公网 IP 的测试环境但你要额外处理签名校验和重放。第三种是本地 SMTP 注入开发机直接向 2525 这类非标准端口发信不经过公网适合跑自动化用例。三种接法可以共存但不要在同一个环境里混用同一批地址否则排查时很难判断邮件是从哪条路进来的。我一般把“生产预演”走 MX 直收“日常联调”走本地 SMTP“没有公网入口的演示环境”走 API 转发。选型理由不是谁更高级而是看你要验证哪一段验证域名和投递就选 MX验证解析和接口就选本地注入验证跨环境集成就选 API 转发。源码包里通常会把收信入口做成独立进程解析逻辑做成可单独调用的模块这样换接法时不用改接口层。需要留意的是本地 SMTP 注入不要监听在公网地址上测试端口只绑 127.0.0.1 或内网网卡避免被当成开放中继。export MAIL_DOMAINtemp.example.test export MAIL_TTL_SECONDS600 export SMTP_LISTEN127.0.0.1:2525 export API_LISTEN0.0.0.0:8080 export DB_URLfile:./data/mail.db export CACHE_URLtcp://127.0.0.1:6379 export MAX_MAILBOXES_PER_IP5 export RATE_LIMIT_PER_MINUTE30这段环境变量是整套系统的“黑匣子面板”改参数优先改这里不要直接改业务代码。MAIL_DOMAIN决定临时地址的后缀必须和 MX 记录指向的域名一致MAIL_TTL_SECONDS默认 600也就是十分钟后面清理任务和缓存过期都用它做基准SMTP_LISTEN在本地联调时绑回环地址避免暴露到公网DB_URL和CACHE_URL分别指向持久存储和短期状态存储测试环境可以用文件库加本地缓存预演环境再换成独立服务MAX_MAILBOXES_PER_IP和RATE_LIMIT_PER_MINUTE是反滥用闸门先设小一点压测时再按实际流量调。2.2 解析、入库与过期队列为什么把状态放在缓存而不是只靠数据库邮件进来后要拆三样东西收件地址、邮件头、正文。收件地址决定这封信属于哪个临时邮箱邮件头里的发件人、主题、时间用于展示和排查正文里的验证码或链接用于自动化提取。解析时不要假设正文一定是纯文本常见邮件是 multipart 结构纯文本和 HTML 各一份验证码可能只出现在其中一份。我一般先取 text/plain取不到再降级到 text/html 去标签最后把原始报文也留一份方便出现“接口查不到验证码”时回看。状态存放要区分“必须持久”和“可以过期”。邮箱地址、创建时间、过期时间、状态要持久否则服务重启后查不到十分钟的短期计数、验证码提取结果、接口限流计数可以放缓存过期就自然消失。只靠数据库做 TTL 的问题是每次查询都要比对时间清理任务一旦延迟接口就可能返回已经过期的邮件只靠缓存的问题是缓存重启后状态丢了数据库里还留着“active”的记录。常见做法是双写但职责分开数据库存事实缓存存“是否还能读”的快门清理任务负责把事实翻成 expired接口读取时同时看数据库状态和缓存快门。from datetime import datetime, timezone from app.config import settings from app.db import db from app.cache import cache def accept_message(address: str, raw_message: str, parsed: dict): now datetime.now(timezone.utc).isoformat() row db.query_one( select address, expire_at, status from mailboxes where address ?, (address,), ) if not row or row[status] ! active: return {ok: False, reason: mailbox_not_active} if row[expire_at] now: db.execute( update mailboxes set status expired where address ?, (address,), ) cache.delete(fmb:{address}:open) return {ok: False, reason: mailbox_expired} db.execute( insert into messages(address, subject, sender, body_text, body_html, raw_message, created_at) values(?,?,?,?,?,?,?), ( address, parsed.get(subject, ), parsed.get(sender, ), parsed.get(body_text, ), parsed.get(body_html, ), raw_message, now, ), ) cache.set(fmb:{address}:last_message_at, now, exsettings.mail_ttl_seconds) return {ok: True}这段逻辑的关键在“先判状态再入库”而不是先入库再补状态。row[expire_at] now是兜底判断防止清理任务还没跑到时接口已经能读到过期邮件cache.delete(fmb:{address}:open)是把缓存快门关掉后续读取会走数据库状态不会出现“数据库过期但缓存还开着”的假象。raw_message建议保留但不要无限增长清理任务要按邮箱地址一起删body_text和body_html分开存提取验证码时优先用纯文本HTML 只做降级。参数上settings.mail_ttl_seconds不要写死在函数里否则改 TTL 要重新发版测试环境也没法用 60 秒快速验证。2.3 本地拉起最小闭环环境变量、初始化与健康检查最小闭环不需要一上来就接公网域名先用本地 SMTP 注入把“收信—入库—接口读取”跑通。初始化要做三件事建表、建过期索引、检查关键参数。建表不是每次都重建测试环境可以清库重来预演环境要走迁移过期索引至少覆盖mailboxes.expire_at和messages.address否则十分钟后清理任务会扫得很吃力参数检查要拦住空域名、非数字 TTL、过大的单 IP 配额这些错误在启动时暴露比在收信时暴露便宜得多。from app.config import settings from app.db import init_schema from app.cache import ensure_cache_ready def main(): assert settings.mail_domain, MAIL_DOMAIN 不能为空 assert 60 settings.mail_ttl_seconds 3600, TTL 建议在 60 到 3600 秒之间 assert settings.max_mailboxes_per_ip 0, 单 IP 配额必须大于 0 init_schema() ensure_cache_ready() print(init ok, settings.mail_domain, settings.mail_ttl_seconds) if __name__ __main__: main()这段初始化脚本把三类错误挡在启动阶段域名没配、TTL 超出合理区间、单 IP 配额为 0。init_schema()负责建表和索引ensure_cache_ready()只做缓存连通性检查不在这里灌数据。TTL 区间写成 60 到 3600 秒是工程习惯十分钟场景用 600自动化用例可以临时改成 60 秒但不要改成 5 秒邮件投递和轮询都需要时间窗口。启动后先看健康检查接口返回的域名、TTL、当前活跃邮箱数再发第一封本地测试邮件如果健康检查返回的域名和你在 MX 记录里配的不一致后面收不到信是必然的不用先怀疑代码。3. 本地跑通收信容器编排、域名解析与第一封验证码邮件这一章的目标很具体在你的机器上把收信、解析、接口、清理四个角色拉起来然后用一封带验证码的测试邮件走完整条链路。容器编排只是手段重点是每个服务的环境变量要对齐尤其是域名、TTL、SMTP 监听地址和数据库路径。编排文件里不要把数据库文件放在容器临时层否则容器一重启刚创建的临时邮箱和刚收到的邮件全没了。测试域名的解析也要提前规划本地联调可以用 hosts 或内网 DNS预演环境再配公网 MX。下面先看编排里的四个服务再看域名怎么指最后发一封信并核对落库结果。3.1 编排文件里的四个关键服务收信、解析、接口、清理四个服务不要合并成一个大进程临时邮箱的故障定位很依赖边界清晰。收信服务只负责接 SMTP 连接、做最小校验、把原始报文交给解析队列解析服务负责拆邮件头、正文和附件写数据库接口服务负责创建邮箱、查询邮件、提取验证码清理服务按 TTL 把过期邮箱翻状态并删除邮件。合并进程在本地看着省事一旦收信卡住接口也会被拖慢最后你分不清是投递慢还是查询慢。编排文件里把数据卷挂到持久目录缓存服务单独一个角色清理任务用短间隔轮询方便十分钟内看到状态变化。services: smtp-sink: image: internal/mail-sink:local ports: - 2525:2525 environment: MAIL_DOMAIN: temp.example.test SMTP_LISTEN: 0.0.0.0:2525 QUEUE_URL: tcp://queue:6379 volumes: - ./data:/app/data parser: image: internal/mail-parser:local environment: DB_URL: file:/app/data/mail.db QUEUE_URL: tcp://queue:6379 MAIL_TTL_SECONDS: 600 volumes: - ./data:/app/data depends_on: - smtp-sink - queue api: image: internal/mail-api:local ports: - 8080:8080 environment: DB_URL: file:/app/data/mail.db CACHE_URL: tcp://cache:6379 MAIL_DOMAIN: temp.example.test MAIL_TTL_SECONDS: 600 MAX_MAILBOXES_PER_IP: 5 depends_on: - parser - cache cleaner: image: internal/mail-cleaner:local environment: DB_URL: file:/app/data/mail.db CACHE_URL: tcp://cache:6379 MAIL_TTL_SECONDS: 600 CLEAN_INTERVAL_SECONDS: 30 depends_on: - api - cache queue: image: internal/queue:local cache: image: internal/cache:local这份编排是结构示意镜像名换成资源包里的本地镜像即可。smtp-sink暴露 2525 给本地注入不直接暴露 25parser和api共用./data保证数据库文件在重启后还在cleaner的CLEAN_INTERVAL_SECONDS30表示每 30 秒扫一次过期邮箱十分钟 TTL 下最多多活 30 秒测试环境可以接受预演环境可以缩到 10 秒MAX_MAILBOXES_PER_IP5是接口层的创建配额不是 SMTP 层。QUEUE_URL和CACHE_URL分开是为了不让“收信队列堆积”把“接口缓存”一起拖垮。需要提醒的是本地文件库只适合单机联调多副本部署时要换成独立数据库并把数据卷从多个服务共享改成网络访问。3.2 域名与 MX 记录把邮件指到你的入口临时邮箱的地址后缀由MAIL_DOMAIN决定邮件能不能进来由 MX 记录决定两者必须对齐。测试环境用temp.example.test这类保留域名做示例预演时换成你自己的测试子域名。MX 记录指向收信入口的主机名主机名再用 A 记录或 AAAA 记录指到公网地址如果收信入口不在 25 端口还要确认上游是否允许非标准端口投递。很多“收不到信”不是代码问题而是 MX 指到了旧地址、优先级写反、或者只有 A 记录没有 MX 记录。下面这张表是联调时最常对照的检查项。检查项示例值说明邮箱后缀temp.example.test必须与MAIL_DOMAIN一致MX 主机mail.temp.example.test指向收信入口主机名MX 优先级10单入口先设一个避免多入口分流难排查A/AAAA 记录127.0.0.1 或内网地址本地联调可用 hosts 替代SMTP 端口2525本地注入用非标准端口公网预演再确认 25反向解析按测试环境要求配置没有反解析时部分收件方会降级或拒收本地联调不必等公网 DNS直接在 hosts 里把mail.temp.example.test指到 127.0.0.1再用本地 SMTP 注入即可。预演环境才需要公网 MX并且要确认入口的 25 端口可达。注意不要拿正式业务域名做临时邮箱后缀临时地址会被外部系统当成真实地址后续退信和投诉会污染主域名信誉。测试子域名单独隔离出了问题只影响测试链路。3.3 发一封测试邮件并核对落库结果先用标准库发一封带验证码的纯文本邮件避免一开始就上复杂模板。收件地址用接口创建的临时地址主题里带验证码正文也带一份方便对比提取逻辑。发送端不要用真实业务邮箱测试环境用testerexample.test这类虚构地址即可。发送成功后先看收信服务日志有没有接到连接再看解析服务有没有入库最后调接口按地址查邮件。三步都过才说明链路通了只看到“发送成功”不代表对方收到SMTP 的 250 响应只表示当前 hop 接收。import smtplib from email.message import EmailMessage msg EmailMessage() msg[From] testerexample.test msg[To] demotemp.example.test msg[Subject] 验证码 482913 msg.set_content(你的验证码是 48291310 分钟内有效。) with smtplib.SMTP(127.0.0.1, 2525, timeout5) as s: s.send_message(msg) print(sent)这段发送脚本只做一件事把一封最小邮件投到本地 2525。From用虚构测试地址To必须和接口创建的临时地址完全一致大小写和前后空格都会影响匹配timeout5是防止收信服务没起来时脚本一直挂着。发送成功只说明本地 SMTP 接了连接接下来要查接口。import json import urllib.request url http://127.0.0.1:8080/api/mailbox/demotemp.example.test/messages with urllib.request.urlopen(url, timeout5) as r: data json.load(r) print(json.dumps(data, ensure_asciiFalse, indent2))查询接口按地址取邮件返回里至少要有主题、发件人、正文、收到时间。如果返回空数组先看解析服务日志有没有“mailbox_not_active”再看创建邮箱时返回的expire_at是不是已经过了十分钟。如果接口返回 404先确认地址有没有做 URL 编码在路径里通常没问题但空格和加号要处理。查到邮件后用验证码提取逻辑跑一遍确认能从主题或正文里拿到 482913拿不到就回看原始报文常见原因是验证码在 HTML 里被标签拆开纯文本分支没取到。4. 把“10分钟”做成可控参数有效期、清理任务与接口限流十分钟不是写死在文案里的数字它要同时落在创建接口、缓存过期、清理任务和查询判断四个位置。只在创建接口返回一个expire_at查询时不校验过期邮件还能被读出来只在缓存里设十分钟缓存重启后数据库里的记录还活着只在清理任务里删任务延迟期间接口会返回过期数据。我的做法是把 TTL 当成一个配置项往下传创建时写过期时间缓存设同样的过期秒数查询时做兜底判断清理任务按过期时间批量翻状态。限流也要跟着 TTL 走否则短时间大量创建地址会把收信队列打满。下面分三层讲TTL 放哪、清理怎么幂等、验证码和限流怎么卡住自动化场景。4.1 TTL 放在哪一层接口层、缓存层、清理任务的职责切分接口层负责“创建时写过期时间”和“查询时判过期”缓存层负责“十分钟内快速判断还能不能读”清理任务负责“到点后把状态翻掉并释放存储”。三层都做不是重复而是互相兜底。接口层不判过期缓存一丢就会读到旧数据缓存层不设过期清理任务一延迟就会一直放行清理任务只删邮件不翻状态接口还会认为邮箱是 active。常见做法是数据库存expire_at缓存键带 TTL查询先看缓存快门再看数据库时间。TTL 参数从环境变量读创建函数不要自己写 600。from datetime import datetime, timedelta, timezone from app.config import settings from app.db import db from app.cache import cache def create_mailbox(address: str, client_ip: str): now datetime.now(timezone.utc) expire_at now timedelta(secondssettings.mail_ttl_seconds) db.execute( insert into mailboxes(address, client_ip, created_at, expire_at, status) values(?,?,?,?,?), (address, client_ip, now.isoformat(), expire_at.isoformat(), active), ) cache.set(fmb:{address}:open, 1, exsettings.mail_ttl_seconds) return {address: address, expire_at: expire_at.isoformat()}expire_at用 UTC 时间存接口返回时再按客户端时区展示避免服务器时区不同导致“明明没过期却查不到”。cache.set(..., exsettings.mail_ttl_seconds)的过期秒数和数据库的expire_at对齐缓存先过期没关系查询会回落到数据库判断缓存后过期才危险所以清理任务翻状态时要主动删缓存键。address在入库前要做规范化统一小写、去掉首尾空格否则Demo和demo会变成两个邮箱。单 IP 配额在创建前检查不要等入库后再补偿补偿逻辑容易在并发下漏掉。4.2 清理任务与幂等删除避免“过期了还能查到”的假象清理任务不要只删邮件要先翻邮箱状态再删邮件最后删缓存。顺序反了会出现“邮件没了但邮箱还 active”的中间态接口查不到邮件却还能创建同地址排查时很迷惑。批量删除要带时间条件不要一次扫全表每批限制数量避免长事务锁住收信写入。幂等的意思是同一批数据被清理两次也不会报错删除语句带status active和expire_at now条件第二次执行影响行数为 0 也算成功。清理日志要记录批次、影响行数、最早过期时间出问题时能看出是没扫到还是扫到了没删掉。update mailboxes set status expired where status active and expire_at :now; delete from messages where address in ( select address from mailboxes where status expired limit :batch_size ); delete from mailboxes where status expired and expire_at :now limit :batch_size;这三条语句的顺序是“先翻状态、再删邮件、最后删邮箱”batch_size建议从 100 开始压测时再调。messages的删除用子查询按地址批量删避免按时间扫全表如果邮件表很大先按address建索引。注意limit在部分数据库里对子查询的支持不一样实际落地时按你选的数据库改写成先查地址列表再批量删。清理任务不要和收信服务抢同一把大锁翻状态用短事务删邮件分批提交。接口查询时仍然要判expire_at不能完全信任清理任务已经跑过。4.3 验证码提取与接口限流自动化场景的两条硬约束验证码提取不要贪多先定规则再降级。常见验证码是 4 到 8 位数字正则用前后断言卡住边界避免把日期、订单号、电话号码里的数字段误抓。先扫主题再扫纯文本正文最后扫 HTML 去标签后的文本三处都拿不到就返回空不要用“取最长数字串”这种玄学规则。自动化调用方通常只关心第一组有效验证码接口返回候选列表反而增加歧义。提取结果可以缓存几分钟但不要缓存到邮箱过期之后否则会出现邮箱已过期、验证码还能读的怪现象。import re CODE_RE re.compile(r(?!\d)(\d{4,8})(?!\d)) def extract_code(subject: str, body_text: str, body_html: str): for text in (subject or , body_text or , body_html or ): hits CODE_RE.findall(text) if hits: return hits[0] return None(?!\d)和(?!\d)是前后边界防止从更长数字串里截一段\d{4,8}覆盖常见验证码长度太长会误抓订单号太短会漏掉 6 位以上验证码。主题优先是因为很多系统把验证码放在标题里正文作为降级。HTML 分支实际使用前要先做去标签和实体解码否则48#2913;这类写法抓不到。提取函数返回第一组命中不返回全部候选接口层再决定要不要展示原文片段。限流要分创建、查询、收信三个维度。创建按 IP 限防止同一来源批量占地址查询按地址限防止轮询过猛收信按域名和发件人限防止测试环境被当成垃圾邮件入口。参数不要拍脑袋十分钟 TTL 下单 IP 同时活跃邮箱不超过 5 个每分钟创建不超过 30 次查询轮询间隔不低于 2 秒基本能覆盖联调。超过阈值返回 429 并带retry_after调用方才知道等多久。限流计数放缓存键里带时间窗口窗口过期自动清零不要用数据库计数十分钟场景下写入放大不划算。参数建议初始值作用MAIL_TTL_SECONDS600邮箱和邮件的有效期MAX_MAILBOXES_PER_IP5单 IP 同时活跃邮箱上限RATE_LIMIT_PER_MINUTE30单 IP 每分钟创建次数QUERY_INTERVAL_SECONDS2调用方轮询最小间隔CLEAN_INTERVAL_SECONDS30清理任务扫描间隔BATCH_SIZE100单批清理数量这些参数不是越大越好。MAX_MAILBOXES_PER_IP调到 50测试环境很快会被占满RATE_LIMIT_PER_MINUTE调到 300收信队列会先于接口崩CLEAN_INTERVAL_SECONDS调到 300十分钟 TTL 实际会多活五分钟。压测时先按表里的值跑一轮再按瓶颈调一个参数不要一次改三个。5. 避坑与排查收不到信、进垃圾箱、TTL 不生效的三个高频翻车点临时邮箱的坑大多不在业务代码而在链路边界和参数一致性。收不到信时很多人第一反应是改解析代码其实先要看 SMTP 有没有接到连接进垃圾箱时很多人以为是内容问题其实测试域名的解析和发件方信誉更常见TTL 不生效时很多人只改环境变量忘了缓存键和清理任务还按旧值跑。下面按“收信链路”和“生命周期”两组拆每条都按现象、原因、解决写方便对照日志逐项排除。5.1 收信链路排查端口、MX、反解析与日志断点现象一本地发送返回成功接口查不到邮件。原因通常是收信服务和解析服务之间断了或者收件地址没有先创建。SMTP 的 250 只表示收信进程接了邮件不代表解析入库成功。解决顺序是先看收信日志有没有accepted再看队列有没有积压再看解析日志有没有mailbox_not_active最后查数据库里有没有这条地址。如果地址是手工拼的而不是接口创建的先调创建接口拿返回的expire_at再用同一个地址发信。本地注入时还要确认To和创建地址完全一致大小写、空格、显示名都会导致匹配失败。现象二公网发信收不到本地注入正常。原因多在 MX 记录、25 端口和入口地址。先查 MX 是否指向当前入口主机名优先级是否只有一条再查入口的 25 端口是否可达云上安全组和本机防火墙都要看最后查反向解析没有反解析时部分收件方会直接拒收或降级。解决时不要同时改 DNS 和代码先保持代码不动用外部诊断工具确认 MX 解析结果再在入口抓 SMTP 握手日志。如果 MX 已经生效但日志里没有连接问题在网络层不在解析层。现象三邮件能收到但进了垃圾箱或直接被拒。原因通常是测试域名信誉低、发件方没有对齐、内容里链接过多。临时邮箱用来收验证码不应该承担营销发送测试时把邮件内容压到最小只留主题和一行验证码不要放大段 HTML 和短链接。解决时先换一个干净的测试子域名确认 SPF、DKIM、DMARC 的测试配置和发件方一致如果只是本地联调直接走本地 SMTP 注入不要绕公网。注意不要拿正式业务域名做临时邮箱后缀退信和投诉会污染主域名后面再恢复很麻烦。5.2 生命周期排查TTL 不生效、重复邮箱与并发覆盖现象四过了十分钟接口还能查到邮件。原因通常是查询接口只查数据库没判expire_at或者缓存键的 TTL 比数据库长。解决时先看创建返回的expire_at和当前时间差再看查询接口有没有做时间判断最后看缓存键的剩余过期时间。清理任务延迟只是表象接口层必须自己判过期缓存键要在翻状态时主动删除不能只依赖自然过期。测试时把 TTL 临时改成 60 秒创建后等 70 秒再查能稳定复现才算修好。现象五同一地址创建两次第二封邮件跑到旧记录里。原因通常是地址没有做唯一约束或者创建接口先查后写中间被并发插了一条。解决时在数据库层加唯一索引创建逻辑用“插入冲突则返回已有活跃记录”的方式不要先查再插。地址规范化也要在入库前做统一小写去空格否则Demo和demo会绕过唯一约束。并发下还要注意过期记录如果旧记录已经 expired新创建可以复用地址但要把旧邮件清掉或标记隔离避免新旧邮件混在一起。现象六清理任务跑完接口偶发 500。原因通常是删除顺序不对邮件删了但邮箱记录还在接口按地址查邮件时拿到空结果又去解引用或者批量删除时事务太长收信写入超时。解决时按“翻状态、删邮件、删邮箱”的顺序接口查询对空结果做正常返回不要抛异常清理分批提交单批 100 条起步观察收信写入延迟再调。日志里记录批次和影响行数出现 500 时先看清理任务是不是和查询撞在同一批地址上。血泪经验是清理任务不要图快一次删完十分钟 TTL 下数据量不大分批反而更稳。6. 进阶验证与上线前压测用脚本核对 TTL、并发和验证码命中率上线前我会跑一个很小的自检脚本不追求压到极限只验证三件事创建后十分钟内能查到过期后查不到并发创建不会串地址。脚本用标准库就够创建地址、发一封带验证码的邮件、轮询接口、等到过期再断言一次。并发部分用线程池开 10 个创建请求检查返回地址不重复、单 IP 配额生效、限流返回 429 时带retry_after。验证码命中率按主题、纯文本、HTML 三个分支各发一封统计提取成功率低于 95% 就先修提取规则不要急着放流量。import time import urllib.request BASE http://127.0.0.1:8080 def get(path): with urllib.request.urlopen(BASE path, timeout5) as r: return r.status, r.read().decode(utf-8) def create(addr): req urllib.request.Request( BASE /api/mailbox, data({address:%s} % addr).encode(), headers{Content-Type: application/json}, methodPOST, ) with urllib.request.urlopen(req, timeout5) as r: return r.status status, body get(/api/health) assert status 200 and temp.example.test in body create(ttl-checktemp.example.test) time.sleep(2) status, body get(/api/mailbox/ttl-checktemp.example.test/messages) assert status 200 print(ttl-check ok)这段脚本只做冒烟/api/health返回里要能看到当前域名创建后等 2 秒再查避免解析还没入库。真正验 TTL 时把等待时间改成 610 秒再断言查询返回 404 或空状态测试环境可以把 TTL 调成 60 秒等待 70 秒即可。并发脚本不要和冒烟脚本混在一起跑先单线程确认链路再开并发看限流。压测参数按表里的初始值来观察收信队列延迟、接口 P95、清理批次耗时一次只调一个参数。验证项通过标准失败时先看健康检查返回域名和 TTL 与配置一致环境变量是否被覆盖创建与查询2 秒内能查到空列表地址规范化、唯一约束验证码提取主题/纯文本/HTML 三路命中正则边界、HTML 去标签TTL 过期到期后查询不到邮件接口时间判断、缓存键并发创建地址不重复超限返回 429唯一索引、限流窗口清理任务批次有日志影响行数可对账删除顺序、事务长度从那以后我每次改 TTL 或限流参数都先用这个脚本跑一轮断言再放流量进来临时邮箱这种十分钟生命周期的系统最怕的不是代码复杂而是参数改了但缓存、清理任务和查询判断没跟着改。希望帮到你。本文还有配套的精品资源点击获取