SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 更新于 2026-08-21

在VPS上自建一次性邮箱收件箱

用Docker Compose在VPS部署Mailpit,捕获预发布应用发往任意地址的测试邮件,通过Web界面查看,并阻止误发给真实客户。

一次性电子邮件收件箱是什么

一次性电子邮件收件箱是一个小型 SMTP(简单邮件传输协议)服务器,可接受发往任意地址的邮件,但不会投递任何邮件。您的预发布应用会将邮件发送到该服务器,而不是真实的邮件服务商,所有邮件都会停留在那里。您可以在 Web 界面中查看收到的邮件。因此,即使收件人列表错误或模板损坏,也不会产生任何成本,因为邮件不会离开该收件箱。

本指南使用 Docker Compose,在一台 VPS 上部署一个此类收件箱。Mailpit 负责接收所有邮件。其 SMTP 监听器绑定到只有您的应用可以访问的位置;其 Web 界面通过 nginx、传输层安全协议(TLS)和密码进行保护;保留上限可防止邮箱占满磁盘。如果您还不熟悉 Compose,请参阅 VPS 的 Compose 基础知识,其中介绍了本指南所使用的文件布局。

最终得到的是测试工具,而不是邮件服务器。它没有账户,不会投递邮件,也不提供垃圾邮件过滤。面向真实用户的真实邮箱需要使用 完整的邮件服务器,例如 Mailcow,实现起来要复杂得多。

Mailpit、Inbucket 与 MailHog:应运行哪个邮件接收器

有 3 个工具可以完成这项工作。它们的区别在于维护状态、监听的端口,以及接收邮件后可以执行的操作。以下版本信息于 2026 年 8 月核查。

MailHogmailhog/mailhog)在 1025 端口监听 SMTP,并在 8025 端口提供 Web 界面。它仍然可以运行。但其默认分支自 2022 年 8 月以来没有提交,跟踪器中有超过 250 个未解决问题,因此测试路径会使用未修补的依赖项。不要再基于它开展新工作。

Inbucketinbucket/inbucket)在 2500 端口监听 SMTP,在 9000 端口提供 Web 界面,并在 1100 端口监听 POP3(邮局协议第 3 版)。3.1.1 版于 2025 年 12 月发布。它将邮件存储为 /storage 下的文件,并自动清理邮件:镜像设置了 INBUCKET_STORAGE_RETENTIONPERIOD=72hINBUCKET_STORAGE_MAILBOXMSGCAP=300。如果测试需要使用 POP3 客户端库而不是 HTTP 请求来收取邮件,请选择它。

Mailpitaxllent/mailpit)使用与 MailHog 相同的端口,即 1025 和 8025,因此无需修改应用配置即可替换 MailHog。1.30.7 版于 2026 年 8 月 8 日发布。它在二进制文件中内置了本指南所需的功能:用于 Web 界面和 API(应用程序编程接口)的密码文件、邮件数量上限、邮件存留时间上限,以及收件人过滤器。本指南其余部分使用 Mailpit。

通配接收的工作原理,以及为什么不涉及 DNS

您的应用不会在此处查询邮件的投递位置。您向它提供主机和端口,它建立 TCP 连接,然后声明 RCPT TO:<anyone@example.test>。Mailpit 会接受该收件人地址,不论其内容是什么,存储邮件,但不会转发邮件。系统不会解析该域名,因此即使 .test 是域名系统(DNS)中不存在的保留名称,example.test 仍然可以正常工作。

这就是完整的工作机制,也是收件箱默认安全的原因。整个过程不涉及 MX(邮件交换)记录,不会尝试投递,也不会有任何邮件到达真实用户。

将预发布应用指向邮件接收器

当应用作为容器运行在同一个 Compose 项目中时,将应用的 SMTP 主机设置为 mailpit;当应用运行在宿主机上时,设置为 127.0.0.1。将端口设置为 1025,关闭 TLS,并将用户名和密码留空。Mailpit 接受匿名邮件。

某些框架在没有凭据时不会发送邮件。MP_SMTP_AUTH_ACCEPT_ANY=1 使 Mailpit 接受任意用户名和密码,MP_SMTP_AUTH_ALLOW_INSECURE=1 允许在未加密连接上使用 PLAIN 和 LOGIN 机制。这两个设置在此处是安全的,原因是监听器无法从互联网访问,下面的部署配置会强制实现这一点。

建议从第一天起就设置 MP_SMTP_ALLOWED_RECIPIENTS。它接受一个正则表达式,并拒绝所有不匹配的收件人。将其指向测试域名。这样,即使预发布数据库中仍保存真实客户地址,应用日志中也会显示明确的失败,而不是让邮件静默进入接收器。

Docker Compose 文件

首先创建目录和 Web 界面使用的密码文件。htpasswd -B 会写入 bcrypt 哈希,Mailpit 同样支持 bcrypt 和纯文本密码。

mkdir -p ~/mailpit/data
cd ~/mailpit
sudo apt update && sudo apt install -y apache2-utils
htpasswd -B -c data/ui-auth qa

写入 compose.yaml

services:
  mailpit:
    image: axllent/mailpit:v1.30
    container_name: mailpit
    restart: unless-stopped
    ports:
      - "127.0.0.1:8025:8025"
      - "127.0.0.1:1025:1025"
    volumes:
      - ./data:/data
    environment:
      MP_DATABASE: /data/mailpit.db
      MP_MAX_MESSAGES: 2000
      MP_MAX_AGE: 14d
      MP_UI_AUTH_FILE: /data/ui-auth
      MP_SMTP_AUTH_ACCEPT_ANY: 1
      MP_SMTP_AUTH_ALLOW_INSECURE: 1
      MP_SMTP_ALLOWED_RECIPIENTS: '@example\.test$$'

两个连续的美元符号不是笔误。Compose 会将单个 $ 识别为变量展开的起始符号,因此 $$ 才能将一个字面量美元符号传递给容器。正则表达式传递到 Mailpit 后为 @example\.test$

启动服务并检查健康状态:

docker compose up -d
docker compose ps

STATUS 列应显示 Up ... (healthy)。该镜像自带健康检查,每 15 秒运行一次 /mailpit readyz。如果容器一直处于 starting,或变为 unhealthy,则它没有在容器内的 8025 端口提供服务。先查看 docker compose logs mailpit,再进行其他修改。

发布的两个端口都包含一个地址,该地址就是安全控制点。在容器内,Mailpit 监听 0.0.0.0,这没有问题,因为容器拥有独立的网络命名空间。映射的左侧决定外部访问者的范围。写入 8025:8025 后,Docker 会绑定主机上的所有地址,包括公网地址。

如果 staging 应用是同一文件中的服务,请完全删除 1025 映射,并让应用通过主机名 mailpit 访问 1025 端口。共享 Compose 网络中的容器可以直接访问彼此,因此 SMTP 端口完全不会接触主机。Compose 网络如何解析服务名称介绍了这一解析过程。

发送一条消息并确认其已存储

python3 - <<'EOF'
import smtplib
from email.message import EmailMessage

m = EmailMessage()
m["From"] = "staging@example.test"
m["To"] = "anyone@example.test"
m["Subject"] = "Mailpit smoke test"
m.set_content("If this appears in the web interface, the sink works.")
with smtplib.SMTP("127.0.0.1", 1025) as s:
    s.send_message(m)
EOF

脚本成功时不会输出任何内容。通过 API 确认消息已存储:

curl -s -u qa:yourpassword http://127.0.0.1:8025/api/v1/messages

该命令会返回包含已存储消息的 JSON。去掉 -u 标志后,同一个请求会被拒绝,因为 MP_UI_AUTH_FILE 同时保护 API 和 Web 界面。任何读取收件箱的测试也必须发送这些凭据。

Python 脚本返回 ConnectionRefusedError,表示 127.0.0.1:1025 上没有进程监听。如果移除了 SMTP 映射,这是预期结果。此时必须从同一 Compose 网络中的容器运行检查。

通过 nginx 使用密码发布 Web 界面

该界面目前只在回环地址上响应。nginx 终止 TLS,并在请求到达界面前要求输入密码。

sudo htpasswd -B -c /etc/nginx/mailpit.htpasswd qa
server {
    listen 443 ssl;
    server_name mail-test.example.com;

    ssl_certificate     /etc/letsencrypt/live/mail-test.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mail-test.example.com/privkey.pem;

    auth_basic           "mailpit";
    auth_basic_user_file /etc/nginx/mailpit.htpasswd;

    location / {
        proxy_pass http://127.0.0.1:8025;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

使用 sudo nginx -t && sudo systemctl reload nginx 检查语法后重新加载。如果这是您第一次配置代理,建议阅读一次 反向代理块中每个指令的作用

在 nginx 文件和 data/ui-auth 中使用相同的用户名和密码。nginx 会将浏览器的 Authorization 标头转发到上游,因此凭据一致时,一次输入即可通过两项检查。如果凭据不同,浏览器保存的一组凭据会被第二项检查拒绝。

UpgradeConnection 标头并非可有可无。Mailpit 会通过 WebSocket 将新邮件推送到打开的页面。代理使用 HTTP/1.1 但缺少这些标头时,连接无法升级。此时页面虽然可以正常加载,但不会更新:邮件会到达,API 可以显示邮件,但列表会保持不变,直到您重新加载页面。

请保留这两层保护。nginx 密码保护公网地址,MP_UI_AUTH_FILE 保护 8025 端口本身。这一点很重要,因为您的暂存应用生成过的所有密码重置链接,都可以在该界面中读取。

不要让 sink 变成开放中继

开放中继是指 SMTP 服务器接受任何人的邮件,并将其转发到任意目标。垃圾邮件发送者会持续扫描开放中继。一旦在您的地址上发现开放中继,就可能收到滥用举报,甚至导致账户被暂停。

Mailpit 默认不是开放中继,因为它从不转发邮件。只有将 MP_SMTP_RELAY_CONFIG 指向中继配置文件后,中继才会启用;在此之前,界面中的释放操作不会执行任何操作。保持该设置为空是有意的安全选择。

有两种情况会破坏这一属性。配置中继以启用释放按钮,然后将 SMTP 端口暴露到互联网,您就创建了一个可用的开放中继。不配置中继而暴露端口时,陌生人无法通过您的服务器发送邮件,但可以填满存储空间,并将内容写入您的团队信任的界面。

Docker 主机上的问题在于防火墙。发布端口后,Docker 会将规则写入 nat 表。发往容器的流量会先在那里匹配,然后才轮到 ufw(uncomplicated firewall)规则处理。sudo ufw deny 1025/tcp 会报告成功,但实际上不会改变任何内容。为什么 Docker 发布的端口会直接绕过 ufw详细说明了规则链的处理顺序。

解决方法是修改映射中的地址,而不是添加防火墙规则。检查实际绑定的地址:

sudo ss -ltnp | grep -E ':(1025|8025)'

正常输出应显示 127.0.0.1:1025127.0.0.1:8025。如果某行显示 0.0.0.0:1025,说明映射丢失了地址,sink 正在监听互联网。请从另一台机器测试,nc -vz mail-test.example.com 1025 应超时或被拒绝。

如果应用位于另一台服务器上,不要开放 1025 端口来连接两台服务器。应让两台机器加入私有网络,或通过 VPN 隧道连接,并将映射绑定到该接口的地址。

仅在确实需要接收外部邮件时发布 MX 记录

MX(邮件交换)记录会告知其他邮件服务器,哪个主机负责接收某个域的邮件。临时域名没有 MX 记录时,互联网中的邮件无法送达,因为发送方找不到投递目标。收件箱只会保存您自己的应用提交的邮件,这正是测试邮箱的用途。

接收外部邮件需要将 MX 记录指向此服务器、让 Mailpit 监听端口 25(MP_SMTP_BIND_ADDR=0.0.0.0:25),并开放该端口。此时,您的服务器实际上会为该域的所有地址运行一个公开的 catch-all。请明确了解后续影响。

  • DNS 中出现该记录后,几天内就会开始收到垃圾邮件,因为采集程序会读取 DNS。随后,字典攻击会遍历常见名称,并为每次尝试存储一封邮件。
  • 陌生人发送的附件会落到磁盘上并一直保留。没有任何过滤机制,因此未知发件人的归档文件会与您自己的测试邮件放在一起。
  • 任何知道该域名的人,都可以使用该域名下的地址注册第三方服务,确认邮件会被投递到您的服务器。如果密码保护失效,读取收件箱的人就可以控制这些账户。
  • 保留期限不再只是日常维护事项,而会成为系统负载的关键因素,因为邮件量已不再由您控制。

如果您需要外部邮件来检查投递能力,请为其分配专用子域名,将 MP_MAX_AGE 设置得较短,并将其中所有内容视为公开信息。如果您需要供他人长期使用的邮箱,应改为运行带有过滤和备份功能的正式邮件服务器。

保留策略:无限制的全捕获邮箱如何填满磁盘

Mailpit 默认保留 500 封邮件,并定期删除超出该数量的最旧邮件。MP_MAX_MESSAGES: 0 会完全关闭自动删除;正是这一项改动,可能让全捕获邮箱在无人察觉的情况下填满磁盘。MP_MAX_AGE 会增加时间限制,通常需要数小时或数天,写作 36h14d

MP_DATABASE 决定这些数据能否持久保留。未设置该项时,Mailpit 会将数据写入临时文件;进程退出时该文件会被删除,因此每次重启都会清空收件箱。设置该项后,邮件会在重启后保留,文件也会持续增长。

占用空间的主要是附件。每晚向 300 个测试地址发送一份 2 MB 的 PDF 报告,每晚就会产生 600 MB 数据;仅设置邮件数量上限无法及时控制增长。应根据同一卷上的其他数据规划这部分增长,因为像 PhotoPrism 或 Immich 这样的媒体密集型邻居服务,可能已经占用了小型 VPS 磁盘的大部分空间。

du -h ~/mailpit/data/mailpit.db
df -h /

与其等待上限触发,不如在每次 CI 运行之间清空存储:

curl -s -u qa:yourpassword -X DELETE http://127.0.0.1:8025/api/v1/messages

Inbucket 使用 INBUCKET_STORAGE_RETENTIONPERIOD(镜像中为 72h)和 INBUCKET_STORAGE_MAILBOXMSGCAP(300)处理同一问题。无论运行哪一种,都应在第一个测试套件指向它之前设置限制。

从测试套件读取收件箱

GET /api/v1/messages列出当前存储的内容,GET /api/v1/message/{ID}返回一封邮件及其各个部分和邮件头,GET /api/v1/search用于筛选,DELETE /api/v1/messages清空存储。您所运行版本的交互式文档位于 http://127.0.0.1:8025/api/v1/

实用的测试流程是:发送一封邮件,循环轮询直到邮件出现,检查主题和其中的链接,然后删除全部内容。应使用短时间重试循环进行轮询,而不是只发送一次请求。应用通过后台 worker 将邮件加入队列时,send 调用可能已经返回,但 Mailpit 尚未收到邮件。自托管 API 测试和模拟工具中也常见相同模式;这通常是完全不接触生产环境的 staging 环境的另一部分。

FAQ

自托管的一次性电子邮件收件箱是开放中继吗?

只要关闭中继,就不是。Mailpit 会存储邮件,但在您将 MP_SMTP_RELAY_CONFIG 指向中继配置之前不会转发邮件。因此,能够访问 1025 端口的陌生人无法通过您的服务器发送邮件。不过,他们仍然可以填满存储空间,因此应将 SMTP 端口绑定到只有您的应用可以访问的地址。在 Compose 中将其发布为 1025:1025 会绑定所有主机地址,而 sudo ufw deny 1025/tcp 也无法关闭该端口,因为 Docker 自身的 nat 规则会先匹配。

测试域需要 MX 记录吗?

只有在您希望互联网中的邮件能够到达时才需要。没有 MX 记录时,发送服务器无处投递,因此收件箱只会保存您自己的应用通过 SMTP 提交的邮件。发布该记录并开放 25 端口后,您就运行了一个公开的全收件箱:几天内会收到垃圾邮件,字典攻击会在每次尝试时存储一封邮件,陌生人发送的附件也会在未经过滤的情况下占用磁盘空间。

为什么只有重新加载页面后邮件列表才会更新?

Mailpit 会通过 WebSocket 将新邮件推送到已打开的页面。nginx 的 location 块如果缺少 proxy_http_version 1.1 以及 UpgradeConnection 标头,就无法升级该连接,因此页面可以正常加载,随后却会停止更新。邮件仍会到达,API 也仍会返回邮件,所以收件箱看起来像是过期,而不是完全失效。添加这些配置行,重新加载 nginx,然后重新加载页面。

如何防止收件箱占满磁盘?

MP_MAX_MESSAGES 保持为实际数值,并添加 MP_MAX_AGE。默认上限为 500 封邮件,将其设置为 0 会完全禁用删除功能;这会导致带附件的全收件箱持续增长而不易察觉。MP_MAX_AGE 接受小时或天数,例如 36h14d。在 CI 清理阶段使用 curl -X DELETE http://127.0.0.1:8025/api/v1/messages 清空存储。Inbucket 通过 INBUCKET_STORAGE_RETENTIONPERIOD(72h)和 INBUCKET_STORAGE_MAILBOXMSGCAP(300)实现相同功能。

应该运行 Mailpit、Inbucket 还是 MailHog?

截至 August 2026,新项目应选择 Mailpit。MailHog 仍可运行,但其默认分支自 August 2022 以来没有提交,因此包含未修补的依赖项。Inbucket 仍在积极维护(3.1.1,December 2025);如果测试需要 POP3,它是更好的选择,因为 Mailpit 只有在提供密码文件后才会启动 POP3 服务器。Mailpit 使用与 MailHog 相同的端口,即 1025 和 8025,因此替换 MailHog 只需在 Compose 文件中修改一个镜像名称。