Postal 是面向网站、应用和自动化系统的开源邮件投递平台,提供 SMTP、HTTP API、入站邮件路由、投递队列、Webhook、抑制列表、IP 池及邮件跟踪等能力,定位接近自托管的 SendGrid、Mailgun 或 Postmark。它更适合作为事务邮件、通知邮件、批量邮件以及入站邮件转发平台,不应直接等同于包含 IMAP、POP3、日历和最终用户邮箱存储的传统企业邮箱套件。需要员工邮箱的场景,应另行设计 Dovecot、Microsoft Exchange、Google Workspace 等邮箱系统。citeturn20search6turn20search8
截至本报告日期 2026 年 8 月 7 日,Postal 官方支持的安装方式是容器化部署;官方最低资源要求为 2 个 CPU 核心、4 GB 内存和至少 25 GB 磁盘,并要求 MariaDB 10.6 或更高版本,明确不支持用 MySQL 替代。Postal v3 已移除 RabbitMQ、cron 和 requeuer 依赖,核心安装也不要求 Redis;Redis 通常只在接入 Rspamd 的学习、灰名单、限速和信誉模块时使用。citeturn20search4turn14search8
生产部署的关键难点并不是“让 SMTP 服务运行”,而是获得持续稳定的收件箱送达率。至少应满足以下条件:
| 领域 | 上线前最低要求 |
|---|---|
| 网络 | 固定公网 IPv4;出站 TCP 25 不受云商限制;PTR 可由服务商设置 |
| 标识一致性 | PTR、A/AAAA、SMTP HELO/EHLO 与 Postal smtp_hostname 一致 |
| 身份认证 | SPF、DKIM、DMARC 均通过并对齐;营销邮件支持一键退订 |
| 加密 | Web 管理接口 HTTPS;SMTP STARTTLS;应用凭据不通过明文公网传输 |
| 反滥发 | 按租户和凭据限速;内容扫描;退信和投诉实时抑制;发送域/IP 隔离 |
| 可观测性 | 原生 /metrics、Webhook 指标、队列年龄、退信率、投诉率和供应商错误码 |
| 运维 | 数据库与 /opt/postal/config 备份;固定版本升级;日志轮转;证书自动续期 |
| 信誉 | Gmail Postmaster Tools、Microsoft SNDS/JMRP、Yahoo Sender Hub/CFL |
Google 要求所有发往个人 Gmail 的发送者至少配置 SPF 或 DKIM、有效的正向和反向 DNS、TLS,并将 Postmaster Tools 中的垃圾邮件率维持在 0.3% 以下;每日向 Gmail 发送超过 5,000 封邮件的域必须同时配置 SPF、DKIM 和 DMARC,并为营销或订阅邮件提供一键退订。Yahoo 的大批量发送要求与之相近,并要求在两天内处理退订。Microsoft 自 2025 年 5 月起对每日超过 5,000 封的高容量发送域强制执行 SPF、DKIM 和 DMARC,认证不合规可返回 550 5.7.515。citeturn17search2turn18search8turn18search9
用户没有提供实际域名、IP、云平台、邮件量、平均邮件大小、保留期限、合规辖区及可用性目标,因此这些值均视为未指定。下文命令中的 <BASE_DOMAIN>、<MAIL_FQDN>、<PUBLIC_IPV4>、<ADMIN_CIDR>、<SECRET> 等均为必须替换的占位符;example.com 只用于演示,不代表对实际环境作出假设。
环境与前提条件
Ubuntu 版本。 Postal 本身只要求底层能够稳定运行 Docker;官方安装说明不绑定特定 Linux 发行版。Ubuntu 26.04 LTS 已于 2026 年 4 月发布,Docker 官方当前列出的 Ubuntu 支持范围包括 26.04 LTS、24.04 LTS 和 22.04 LTS。实际版本仍为“未指定”,生产环境应选择同时满足组织补丁策略、Docker 支持周期和监控代理兼容性的 LTS 版本,而不是仅因为版本最新就直接升级。citeturn2search0turn2search1turn20search4
硬件规划。 官方最低配置适合低流量试运行,不应被理解为生产容量保证。生产规格应通过邮件大小、峰值并发、保留期限和 MariaDB 写入量压测确定。citeturn20search4
可使用以下公式估算磁盘下限:
邮件数据容量
≈ 每日接受邮件数
× 平均原始邮件大小
× 邮件保留天数
× 1.3~1.8 的数据库、索引与元数据系数
总磁盘
≈ 邮件数据容量
+ MariaDB 临时空间
+ Docker 镜像
+ 日志
+ 本地备份缓冲
+ 至少 25% 空闲空间
建议至少记录以下“未指定”容量参数并在部署前完成压测:
| 参数 | 当前值 | 建议测量方式 |
|---|---|---|
| 峰值邮件接受速率 | 未指定 | 以 1 分钟、5 分钟和 1 小时窗口分别统计 |
| 每日消息量 | 未指定 | 区分事务、营销和入站邮件 |
| 平均及 P95 邮件大小 | 未指定 | 从真实模板、附件和 MIME 编码后采样 |
| 消息保留天数 | 未指定 | 根据排错、审计及隐私要求确定 |
| 目标投递延迟 | 未指定 | 分事务邮件和批量邮件设置 SLO |
| 可接受停机时间 | 未指定 | 决定单机、主备还是编排平台架构 |
MariaDB 的 innodb_log_file_size 应至少为最大允许邮件大小的十倍。例如允许 15 MB 邮件时,官方建议至少 150 MB;允许 25 MB 时至少 250 MB。citeturn20search4
域名与 DNS 规划。 建议将管理/API、SMTP、退信路径和跟踪域分开。以下只是结构示例:
| 用途 | 示例占位符 | 说明 |
|---|---|---|
| 管理及 API | postal.<BASE_DOMAIN> | Web UI、HTTP API |
| SMTP 主机名 | smtp.<BASE_DOMAIN> | PTR、HELO/EHLO、SMTP TLS 证书 |
| SPF include | spf.<BASE_DOMAIN> | 多 IP 或多池时集中声明 |
| 退信域 | rp.<BASE_DOMAIN> | VERP/Return-Path 和退信关联 |
| 路由域 | routes.<BASE_DOMAIN> | Postal 入站路由 |
| 跟踪入口 | track.<BASE_DOMAIN> | 平台级跟踪目标 |
| 每发送域跟踪名 | click.<SENDING_DOMAIN> | 邮件链接应尽量与发送域同组织域 |
Postal 官方要求配置管理/API/SMTP 主机的 A/AAAA、MX、SPF include、退信路径、路由域和跟踪域;Postal 还可在界面内检查发送域的 SPF、DKIM、MX 和 Return-Path 配置。citeturn20search10turn20search8
示例记录如下:
; 地址记录
postal.<BASE_DOMAIN>. 300 IN A <PUBLIC_IPV4>
smtp.<BASE_DOMAIN>. 300 IN A <PUBLIC_IPV4>
; 反向 DNS 由云商或 IP 提供方设置,而不是在普通 DNS 区中设置
<PUBLIC_IPV4> PTR smtp.<BASE_DOMAIN>.
; Postal 基础设施域
<BASE_DOMAIN>. 300 IN MX 10 smtp.<BASE_DOMAIN>.
spf.<BASE_DOMAIN>. 300 IN TXT "v=spf1 ip4:<PUBLIC_IPV4> -all"
; 发送域示例
<SENDING_DOMAIN>. 300 IN TXT "v=spf1 ip4:<PUBLIC_IPV4> -all"
postal._domainkey.<SENDING_DOMAIN>. 300 IN TXT "<POSTAL_UI_GENERATED_DKIM_VALUE>"
_dmarc.<SENDING_DOMAIN>. 300 IN TXT
"v=DMARC1; p=none; rua=mailto:dmarc@<REPORT_DOMAIN>; adkim=r; aspf=r; pct=100"
; 自定义退信和跟踪域,实际目标以 Postal 页面给出的值为准
psrp.<SENDING_DOMAIN>. 300 IN CNAME rp.<BASE_DOMAIN>.
click.<SENDING_DOMAIN>. 300 IN CNAME track.<BASE_DOMAIN>.
DMARC 建议先以 p=none 收集并分析聚合报告,确认所有合法发送源均完成 SPF 或 DKIM 对齐后,再逐步转为 quarantine 和 reject。不要在没有报告处理能力时直接将 rua 指向无人维护的邮箱。Google 和 Microsoft 对大批量发送允许从 p=none 开始;Yahoo 同样要求大批量发送域至少发布有效的 p=none 策略。citeturn17search2turn18search7turn18search8
PTR、HELO 与 IP 规划。 每个发送 IP 都应拥有有意义、非动态格式的 PTR;PTR 指向的主机名应通过 A 或 AAAA 再解析回同一 IP,并与 Postal 的 dns.helo_hostname 或 postal.smtp_hostname 保持一致。Google 明确要求发送 IP、PTR 主机名和该主机名的正向记录互相匹配;Yahoo 同样要求有效的正向和反向 DNS。citeturn17search2turn18search8
建议按业务性质隔离信誉:
事务邮件池:密码重置、订单、告警
营销邮件池:订阅、活动、推广
高风险租户池:用户生成内容、试用账号
内部测试池:开发与预生产
Postal 的 IP 池默认关闭;开启后,界面中声明的每个 IP 必须真实配置在发送节点网络接口上,否则邮件可能无法出队。Yahoo 也建议将营销邮件与事务邮件按 IP 或 DKIM 域分离,避免一种流量损害另一种流量的信誉。citeturn20search12turn18search8
端口。
| 端口 | 方向 | 用途 | 暴露建议 |
|---|---|---|---|
| TCP 22 | 入站 | SSH 管理 | 仅 <ADMIN_CIDR>、VPN 或堡垒机 |
| TCP 25 | 双向 | SMTP 接受及对外投递 | 必需;确认云商允许出站 |
| TCP 80 | 入站 | ACME HTTP 验证、HTTP 跳转 | 公网开放,或改用 DNS-01 |
| TCP 443 | 入站 | Postal Web/API/跟踪 | 公网开放并启用 TLS |
| TCP 3306 | 内部 | MariaDB | 仅回环或数据库私网 |
| TCP 6379 | 内部 | Redis/Rspamd | 不得暴露公网 |
| TCP 783 | 内部 | SpamAssassin spamd | 仅回环或受控私网 |
| TCP 11334 | 内部 | Rspamd HTTP worker | 仅回环或受控私网 |
| TCP 9090 | 内部 | Worker 健康与指标 | 默认回环;仅允许监控器 |
| TCP 9091 | 内部 | SMTP 健康与指标 | 默认回环;仅允许监控器 |
Postal 默认 SMTP 端口是 25,Web 默认监听 127.0.0.1:5000,Worker 与 SMTP 健康端点分别为 9090 和 9091。端口 587 或 465 不是本报告默认假设;只有在专门配置额外 SMTP 实例、验证认证和 TLS 行为后才应开放。citeturn14search0turn20search17
基础 UFW 示例:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from <ADMIN_CIDR> to any port 22 proto tcp
sudo ufw allow 25/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
SSL/TLS。 Web 层可以使用官方示例中的 Caddy 自动申请和更新证书。SMTP TLS 默认关闭,必须在 /opt/postal/config/postal.yml 中启用,并将证书放入 /opt/postal/config;容器内路径应写成 /config/smtp.cert 和 /config/smtp.key。citeturn20search3turn13view0
公有云与私有云差异。
| 项目 | 公有云 | 私有云或自建机房 |
|---|---|---|
| TCP 25 | 经常默认封禁,需要提交解封申请 | 取决于 ISP 和边界防火墙 |
| PTR 管理 | 通常在云商控制台或工单设置 | 通常由 ISP/IP 注册机构设置 |
| IP 历史信誉 | 新分配或回收 IP 可能已有历史问题 | 自有长期 IP 更可控,但需自行维护信誉 |
| NAT | 需确认出站固定源 IP及一对一映射 | 需自行保证 SNAT、入站 DNAT 和连接跟踪 |
| IP 扩展 | 较容易增加,但每个 IP 都要重新预热 | 受 ISP、地址资源和 BGP 能力限制 |
| 存储/数据库 | 可使用托管数据库,但要评估延迟、权限和私网访问 | 完全自管备份、高可用和恢复 |
| DDoS/高可用 | 云商可提供负载均衡和防护,但 SMTP 支持差异大 | 需自行建设边界防护和备用链路 |
| 合规地域 | 可选择区域,但应核查子处理方及跨境传输 | 物理位置更可控,运维责任更高 |
Postal 官方特别提醒,虚拟服务器供应商可能分配已进入阻断名单的 IP,且许多供应商会封锁出站 25 端口;这两项应在采购和安装前验证,而不是部署完成后再处理。citeturn20search4
Postal 安装与配置
以下流程采用官方容器安装方法。命令应先在测试环境执行;密码、域名、版本标签和证书路径仍为“未指定”。
安装 Docker。 建议使用 Docker 官方 APT 仓库,而不是 Ubuntu 中可能滞后的兼容包或生产环境中的 convenience script。citeturn2search1
sudo apt-get update
sudo apt-get install -y ca-certificates curl git jq
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
. /etc/os-release
echo
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc]
https://download.docker.com/linux/ubuntu
${UBUNTU_CODENAME:-$VERSION_CODENAME} stable"
| sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt-get update
sudo apt-get install -y
docker-ce docker-ce-cli containerd.io
docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo docker version
sudo docker compose version
安装 MariaDB。 Postal 要求 MariaDB 10.6 或以上,不支持 MySQL。下面的 mariadb:10.11 是可固定测试的示例标签,而不是对实际版本的假设;正式上线前应确认 Postal 版本、备份工具和组织安全基线的兼容性。citeturn20search4
sudo install -d -m 0750 /opt/postal/secrets
sudo tee /opt/postal/secrets/mariadb.env >/dev/null <<'EOF'
MARIADB_DATABASE=postal
MARIADB_ROOT_PASSWORD=<未指定-请替换为高强度随机密码>
EOF
sudo chmod 0600 /opt/postal/secrets/mariadb.env
sudo docker volume create postal-mariadb-data
sudo docker run -d
--name postal-mariadb
--restart always
-p 127.0.0.1:3306:3306
--env-file /opt/postal/secrets/mariadb.env
-v postal-mariadb-data:/var/lib/mysql
mariadb:10.11
确认启动状态:
sudo docker ps --filter name=postal-mariadb
sudo docker logs --tail 100 postal-mariadb
sudo docker exec -it postal-mariadb
mariadb -uroot -p -e
"SELECT VERSION(); SHOW VARIABLES LIKE 'innodb_log_file_size';"
生产环境可创建独立数据库账号,账号必须能够完全操作 postal 和 postal-* 数据库,并允许 Postal 为每个虚拟邮件服务器建立消息数据库。citeturn20search4
CREATE USER 'postal'@'127.0.0.1'
IDENTIFIED BY '<未指定-数据库密码>';
GRANT ALL PRIVILEGES ON `postal`.* TO 'postal'@'127.0.0.1';
GRANT ALL PRIVILEGES ON `postal-%`.* TO 'postal'@'127.0.0.1';
FLUSH PRIVILEGES;
如果容器连接源并非 127.0.0.1,应根据实际 Docker 网络添加精确的主机或网段授权,不应简单使用对公网开放的 'postal'@'%'。
Redis。 Postal v3 核心不依赖 Redis。只有在 Rspamd、外部限速器或自建指标组件需要时才安装。Rspamd 在没有 Redis 时仍能执行部分静态检查,但学习、历史、灰名单、限速和多项信誉功能需要 Redis 才能完整工作。citeturn14search8turn10search15
可选安装:
sudo apt-get install -y redis-server
sudo systemctl enable --now redis-server
redis-cli -h 127.0.0.1 ping
# 预期:PONG
sudo ss -lntp | grep 6379
确认 /etc/redis/redis.conf 至少保持:
bind 127.0.0.1 ::1
protected-mode yes
appendonly yes
安装 Postal helper。citeturn20search4
sudo git clone https://github.com/postalserver/install /opt/postal/install
sudo ln -s /opt/postal/install/bin/postal /usr/bin/postal
postal --help
系统用户。 官方容器部署不要求在宿主机创建一个长期运行 Postal 进程的 postal 用户;helper 通常由具备 Docker 权限的管理员执行,Postal 应用在容器内部使用非 root 用户运行。不要为了“看起来更传统”而把 /opt/postal 全部改属一个任意宿主机用户,否则可能破坏 helper、证书或容器挂载权限。citeturn12view0
如需多人运维,建议用受控管理员组而非开放 Docker 权限:
sudo groupadd --system postal-admin
sudo usermod -aG postal-admin <OPERATOR_USER>
sudo chown -R root:postal-admin /opt/postal/config
sudo chmod 0750 /opt/postal/config
sudo chmod 0600 /opt/postal/config/*.key 2>/dev/null || true
需注意:Docker 组通常等价于宿主机 root 权限。更严格的环境应通过 sudoers 限制可执行命令,而不是直接把普通用户加入 docker 组。
生成配置、初始化并启动。citeturn20search3
sudo postal bootstrap postal.<BASE_DOMAIN>
sudo ls -la /opt/postal/config
# postal.yml
# signing.key
# Caddyfile
sudoedit /opt/postal/config/postal.yml
sudo postal initialize
sudo postal make-user
sudo postal start
sudo postal status
postal bootstrap 生成的关键文件如下:
| 文件 | 作用 | 备份等级 |
|---|---|---|
/opt/postal/config/postal.yml | 主配置、数据库连接、域名和服务设置 | 必须加密备份 |
/opt/postal/config/signing.key | Postal 内部签名私钥 | 必须加密备份 |
/opt/postal/config/Caddyfile | Web 反向代理配置 | 必须备份 |
/opt/postal/config/smtp.key | SMTP TLS 私钥 | 必须加密备份 |
/opt/postal/config/smtp.cert | SMTP TLS 证书链 | 必须备份 |
示例 /opt/postal/config/postal.yml:
version: 2
postal:
web_hostname: postal.<BASE_DOMAIN>
web_protocol: https
smtp_hostname: smtp.<BASE_DOMAIN>
# 初始单 IP 环境保持 false;建立并验证 IP 池后再启用
use_ip_pools: false
default_maximum_delivery_attempts: 18
default_maximum_hold_expiry_days: 7
default_suppression_list_automatic_removal_days: 30
default_spam_threshold: 5
default_spam_failure_threshold: 20
signing_key_path: /config/signing.key
# 默认拒绝向私有、回环、链路本地等保留地址发起 Webhook/HTTP 请求;
# 只有确有内部目标时才添加最小范围。
allowed_request_destinations: []
web_server:
default_port: 5000
default_bind_address: 127.0.0.1
max_threads: 5
worker:
default_health_server_port: 9090
default_health_server_bind_address: 127.0.0.1
threads: 2
main_db:
host: 127.0.0.1
port: 3306
username: postal
password: "<未指定-数据库密码>"
database: postal
pool_size: 5
encoding: utf8mb4
message_db:
host: 127.0.0.1
port: 3306
username: postal
password: "<未指定-数据库密码>"
database_name_prefix: postal
encoding: utf8mb4
smtp_server:
default_port: 25
default_bind_address: "::"
default_health_server_port: 9091
default_health_server_bind_address: 127.0.0.1
tls_enabled: true
tls_certificate_path: /config/smtp.cert
tls_private_key_path: /config/smtp.key
log_connections: true
max_message_size: 14
dns:
mx_records:
- mx1.<BASE_DOMAIN>
- mx2.<BASE_DOMAIN>
spf_include: spf.<BASE_DOMAIN>
return_path_domain: rp.<BASE_DOMAIN>
route_domain: routes.<BASE_DOMAIN>
track_domain: track.<BASE_DOMAIN>
helo_hostname: smtp.<BASE_DOMAIN>
dkim_identifier: postal
logging:
rails_log_enabled: false
enabled: true
highlighting_enabled: false
spamd:
enabled: false
host: 127.0.0.1
port: 783
rspamd:
enabled: false
host: 127.0.0.1
port: 11334
ssl: false
password:
flags:
clamav:
enabled: false
host: 127.0.0.1
port: 2000
以上字段来自 Postal 当前 v2 配置格式。容器将宿主机 /opt/postal/config 挂载为 /config,因此证书和签名密钥不能在配置中写为宿主机路径。allowed_request_destinations 用于限制 Webhook 和 HTTP 路由访问保留地址,是当前版本的重要 SSRF 防护。citeturn14search0turn20search3
配置修改后执行:
sudo postal stop
sudo postal start
sudo postal status
反向代理。 官方示例使用 Caddy,并以 host network 将公网 80/443 反代到 127.0.0.1:5000。Caddy可自动申请 Web 证书。citeturn20search3
sudo docker run -d
--name postal-caddy
--restart always
--network host
-v /opt/postal/config/Caddyfile:/etc/caddy/Caddyfile:ro
-v /opt/postal/caddy-data:/data
caddy
示例 /opt/postal/config/Caddyfile:
postal.<BASE_DOMAIN> {
reverse_proxy 127.0.0.1:5000
}如使用 Nginx:
# /etc/nginx/sites-available/postal.conf
server {
listen 80;
listen [::]:80;
server_name postal.<BASE_DOMAIN>;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name postal.<BASE_DOMAIN>;
ssl_certificate /etc/letsencrypt/live/postal.<BASE_DOMAIN>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/postal.<BASE_DOMAIN>/privkey.pem;
client_max_body_size 20m;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_read_timeout 120s;
}
}
启用:
sudo ln -s /etc/nginx/sites-available/postal.conf
/etc/nginx/sites-enabled/postal.conf
sudo nginx -t
sudo systemctl reload nginx
跟踪域需要反向代理加入 X-Postal-Track-Host: 1;Postal 官方建议跟踪子域与邮件发送域保持同一组织域,并使用 HTTPS,否则可能降低信誉或产生损坏的跟踪链接。citeturn20search7
server {
listen 443 ssl;
server_name click.<SENDING_DOMAIN>;
ssl_certificate /etc/letsencrypt/live/click.<SENDING_DOMAIN>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/click.<SENDING_DOMAIN>/privkey.pem;
location / {
proxy_set_header X-Postal-Track-Host 1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_pass http://127.0.0.1:5000;
}
}服务管理。 正常情况下依靠 Docker 的 restart policy 和 postal start|stop|status 管理,不必再创建一套重复的 systemd 守护进程。常用命令:
sudo postal status
sudo postal logs
sudo postal logs web
sudo postal logs smtp
sudo postal logs worker
sudo docker ps
sudo docker stats
sudo docker inspect <CONTAINER>
常见错误。
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 外发全部超时 | 云商封锁出站 25;安全组/NAT 错误 | nc -vz gmail-smtp-in.l.google.com 25;向云商申请解封 |
| Web 可访问但无法申请证书 | A/AAAA 不正确,80/443 未开放,AAAA 指向错误主机 | 检查 dig A/AAAA,暂时移除不可达 AAAA |
| SMTP STARTTLS 无证书 | TLS 未启用;路径写成宿主机路径;权限不可读 | 使用 /config/smtp.cert、/config/smtp.key 并重启 |
530 或 From 被拒绝 | 发送域未加入/验证,或凭据不属于该虚拟服务器 | 在 Postal UI 验证域,使用对应服务器凭据 |
| 初始化或升级迁移失败 | MariaDB 版本不符;表不是 DYNAMIC row format | 检查 MariaDB;执行 SHOW TABLE STATUS FROM postal |
| 大邮件数据库错误 | innodb_log_file_size 太小 | 调整至最大邮件大小至少十倍 |
| 队列存在但不发送 | IP 池中声明了宿主机不存在的 IP | 将 IP 配置到接口或移出池 |
| 9090/9091 无指标 | 端口被占用时健康服务不会启动,但主进程仍可能运行 | 查日志与 ss -lntp,修改健康端口 |
| 内部 Webhook 被拒绝 | 目标解析到私网地址,被 SSRF 防护阻止 | 仅将所需主机/CIDR加入 allowed_request_destinations |
| Gmail/Yahoo 临时 4xx 激增 | 发送突增、投诉、认证问题或信誉下降 | 降速,检查 Postmaster/SNDS/CFL、DNS和内容 |
| Docker 磁盘持续增长 | 容器日志未轮转、消息保留过长 | 设置 Docker log driver 和 Postal 保留策略 |
Postal 官方调试建议包括 dig 检查 DNS、telnet 或 nc 测试 25 端口,以及使用 openssl s_client -starttls smtp 检查 SMTP 证书。citeturn20search16
dig +short A postal.<BASE_DOMAIN>
dig +short A smtp.<BASE_DOMAIN>
dig +short -x <PUBLIC_IPV4>
dig +short TXT <SENDING_DOMAIN>
dig +short TXT postal._domainkey.<SENDING_DOMAIN>
dig +short TXT _dmarc.<SENDING_DOMAIN>
nc -vz smtp.<BASE_DOMAIN> 25
openssl s_client
-connect smtp.<BASE_DOMAIN>:25
-starttls smtp
-servername smtp.<BASE_DOMAIN>
-showcerts
过滤、反滥发与信誉平台
Postal 可集成 SpamAssassin、Rspamd 和 ClamAV。SpamAssassin 官方集成文档明确说明扫描功能默认关闭,可对入站和出站邮件评分,并按路由设置 Mark、Quarantine 或 Fail;官方建议初始垃圾邮件阈值从 5 开始,再根据真实误报情况调整。citeturn20search0turn20search6
SpamAssassin 与 Rspamd 对比。
| 维度 | SpamAssassin | Rspamd |
|---|---|---|
| 架构 | 传统规则与插件体系,由 spamd 提供 TCP 扫描 | 事件驱动、异步处理,面向较高并发 |
| Postal 接入 | 官方文档完整,TCP 783 | Postal 配置原生支持 HTTP worker,默认示例端口 11334 |
| 基础复杂度 | 低至中 | 中至高 |
| Redis | 不要求 | 静态检查可不使用;完整学习、灰名单、限速和信誉功能建议使用 |
| 规则更新 | sa-update,需重载服务 | 模块化本地配置、规则和统计模型 |
| 内容检查 | 成熟规则、Bayes、插件 | 规则、Bayes、神经网络、模糊哈希、URL/IP/邮件 RBL |
| SPF/DKIM/DMARC | 可通过插件检查 | 内置相关模块较丰富 |
| 灰名单/限速 | 通常依赖 MTA 或额外组件 | 有相应模块,但真正 SMTP 阶段 4xx 仍需正确接入 MTA 流程 |
| 管理界面 | 通常无统一 Web UI | 提供控制器/Web UI |
| 性能特征 | 中低流量和简单部署较直接 | 更适合较高吞吐和复杂策略 |
| 调优风险 | 规则叠加造成误报 | 模块多,Redis、控制器权限和 action 阈值配置更复杂 |
| 推荐场景 | 首次部署、流量较低、希望跟随 Postal 官方简单示例 | 多租户、高吞吐、需要信誉、限速和复杂列表策略 |
Rspamd 官方文档将其描述为事件驱动的邮件过滤系统,并提供 RBL、Bayes、神经网络、模糊匹配、灰名单、限速、信誉和 Redis 集成。SpamAssassin 则拥有成熟的规则更新和插件生态。上述“复杂度”和“推荐场景”是运维层面的相对判断,不是统一性能基准;正式选型应使用本组织邮件样本压测误报率、P95 扫描延迟和 CPU 开销。citeturn10search10turn10search15turn10search3
SpamAssassin 安装。citeturn20search0
sudo apt-get update
sudo apt-get install -y spamassassin
sudo systemctl enable --now spamassassin-maintenance.timer
sudo systemctl restart spamassassin
sudo ss -lntp | grep 783
sudo sa-update
Postal 配置:
spamd:
enabled: true
host: 127.0.0.1
port: 783
rspamd:
enabled: false
重启:
sudo postal stop
sudo postal start
初始策略建议:
score < 5 正常
score 5~9.9 标记或隔离
score 10~19.9 隔离并告警
score >= 20 失败或人工审核
不要直接将阈值 5 以上的所有邮件永久丢弃;先运行一段观察期,分别统计入站和出站误报,并建立“规则名—租户—模板—误报率”关系。
Rspamd 安装示例。
sudo apt-get install -y rspamd redis-server
sudo systemctl enable --now redis-server
sudo systemctl enable --now rspamd
sudo rspamadm configtest
sudo ss -lntp | grep 11334
/etc/rspamd/local.d/redis.conf:
servers = "127.0.0.1";
/etc/rspamd/local.d/worker-normal.inc:
bind_socket = "127.0.0.1:11334";
Postal 配置:
spamd:
enabled: false
rspamd:
enabled: true
host: 127.0.0.1
port: 11334
ssl: false
password: "<未指定-Rspamd访问密码>"
flags:
随后:
sudo rspamadm configtest
sudo systemctl restart rspamd
sudo postal stop
sudo postal start
Rspamd Web 控制器不应直接暴露公网。若启用,应只监听回环或管理私网,并配置强密码和反向代理访问控制。
内容过滤与用户生成内容。 过滤不仅应检查“垃圾邮件词汇”,还要检测:
| 检查对象 | 建议动作 |
|---|---|
| 可执行附件、宏、双扩展名 | 拒绝或隔离 |
| MIME 类型与文件扩展名不一致 | 隔离 |
| 大量短链、混淆域、IDN 欺骗域 | 加分并限流 |
| HTML 中表单、脚本、远程跟踪异常 | 清洗或拒绝 |
| 发件人显示名冒充组织高管 | 标记并触发人工审核 |
| 用户生成主题和正文 | 发信前同步扫描 |
| 新租户首次批量投递 | 低额度、人工审核、仅测试收件人 |
| 模板突然改变或链接域改变 | 暂停并要求重新审核 |
ClamAV 可作为病毒扫描层,但病毒扫描不能取代反垃圾、URL 信誉、附件沙箱和应用层权限控制。
RBL。 RBL 应主要用于入站来源、URL、HELO 和 Received 链分析,不能将单一公共 RBL 作为自动永久拒绝的唯一依据。Rspamd 支持对 IP、URL、电子邮件地址、Received 和 HELO 进行 RBL 检查;使用公共列表前必须确认查询限额和商业许可。citeturn10search5
示例 /etc/rspamd/local.d/rbl.conf,其中域名必须替换为经过授权的实际 RBL:
rbls {
organisation_rbl {
rbl = "rbl.<未指定-已授权的RBL提供方>";
symbol = "ORG_RBL_HIT";
ipv4 = true;
ipv6 = true;
received = true;
helo = true;
}
}应用配置前:
sudo rspamadm configtest
sudo systemctl reload rspamd
灰名单。 灰名单通过首次返回临时 4xx、待正常发送方重试后接受来工作,因此它是 SMTP 会话阶段的控制。Postal 的内容扫描接口不应被假定为可以自动把已接收邮件转换为真正的 SMTP 灰名单流程。若确需入站灰名单,应在 Postal 前部署能够执行 pre-queue 策略的边缘 MTA,或确认当前 Postal/Rspamd 版本的处理阶段后再启用。灰名单一般不适用于 Postal 的出站业务,并可能延迟密码重置、验证码等时间敏感邮件。Rspamd 的灰名单状态依赖 Redis。citeturn10search4turn10search15
速率限制。 应至少在四层实施:
应用用户 → 应用 API 配额
应用/租户 → Postal SMTP/API 凭据配额
发送域/IP 池 → 每分钟和每小时速率
目标收件域 → Gmail、Microsoft、Yahoo 等分别自适应限速
建议默认采用令牌桶或漏桶,并区分接受速率与实际外投速率:
tenant_rate = min(
租户合同额度,
IP 当前预热额度,
目标域动态额度,
全局安全上限
)
Rspamd 限速模块可以用 Redis 保存令牌桶,但应用侧仍应先限制恶意或失控请求,避免垃圾邮件已经进入 Postal 队列后才发现。citeturn10search7
退信、投诉和抑制。 Postal 通过专用 Return-Path 路由关联退信,检测到退信后更新原始消息并发送 Webhook。永久退信、投诉和明确退订应立即进入抑制列表;临时退信则按增强状态码和次数处理。citeturn0search6turn14search11
建议规则:
| 事件 | 动作 |
|---|---|
5.1.1 用户不存在 | 立即永久抑制 |
| 其他明确 5xx 永久错误 | 按地址永久抑制,保留原因 |
| 4xx 邮箱已满 | 暂停 24~72 小时后有限重试 |
| 4xx 信誉/速率限制 | 降低该收件域速率,不抑制用户 |
| 用户投诉 | 立即抑制营销和非必要邮件 |
| RFC 8058 一键退订 | 立即处理,目标远短于平台两天上限 |
| 连续软退信 | 达到内部阈值后进入临时抑制 |
| 地址长期无互动 | 降频、再确认或停止营销 |
黑白名单。 Rspamd 的 multimap 可基于发件人、收件人、IP、域、Header 等实现列表,但“白名单”不应绕过病毒扫描、最大邮件大小或身份认证。citeturn10search9
建议顺序:
强制安全拒绝
→ 账户/租户封禁
→ 永久投诉与退信抑制
→ 企业黑名单
→ 受限白名单减分
→ RBL与内容评分
→ 路由级动作
信誉平台的注册与审核。
| 平台 | 性质 | 所需材料 | 验证与接入流程 | 常见失败与改进 |
|---|---|---|---|---|
| Gmail Postmaster Tools | 域验证与信誉监控,不是白名单审批 | Google 账号;发送认证域;DNS 修改权;实际发往个人 Gmail 的流量 | 添加 DKIM d= 域或 SPF Return-Path 域;发布平台提供的 TXT;返回验证;为运维账号授权 | TXT 发布到错误域;DNS 尚未传播;添加了显示 From 域但实际认证域不同;流量过低无数据;SPF/DKIM/DMARC 未通过 |
| Google/Gmail 投递合规 | 自动化合规和信誉判断,无供应商 allowlist 申请 | SPF、DKIM、DMARC、PTR、TLS、RFC 5322、一键退订、低投诉 | Postmaster Tools 查看合规、域/IP 信誉、垃圾邮件率和投递错误 | 垃圾邮件率达到 0.3%;突增;认证不对齐;列表非自愿;退订失效;PTR/HELO 不一致 |
| Microsoft SNDS | IP 信誉与流量监控 | Microsoft 账号;发送 IP 或网段;IP 控制权;有效网络授权联系人 | 登录 SNDS;申请访问 IP/网段;完成控制权授权;查看 IP 状态和数据 | 无法证明 IP 控制权;共享/动态 IP;授权联系人不可用;网段申请过宽 |
| Microsoft JMRP | 投诉反馈循环 | 已授权 SNDS 网络;投诉接收地址;ARF 处理器 | 在 SNDS 中配置 Junk Mail Reporting Program;接收并解析投诉;立即抑制 | 投诉邮箱退信;ARF 未处理;无法将投诉关联到内部消息;认证和自定义关联头不足 |
| Yahoo Sender Hub/CFL | 域级投诉反馈循环 | Sender Hub 账号;域控制权;DKIM 签名;投诉接收地址 | 建立 Sender profile;添加并验证域;为 DKIM 域登记 CFL;接收 ARF | DKIM 不通过;selector 或 d= 域不匹配;验证记录错误;投诉处理地址无效 |
| Yahoo 发送合规 | 自动化信誉和策略判断 | SPF、DKIM、DMARC、PTR、一键退订、低投诉 | 按 Sender Requirements 持续合规并监控 CFL | 超过 0.3% 投诉;未在两天内退订;突发发送;购买名单;营销与事务共用信誉 |
Google Postmaster Tools 验证时会提供 TXT 值;发布后返回平台点击 Verify。Google 指出验证通常很快,但 DNS 状态更新可能需要约十分钟;只有验证域后才能显示该域数据,并且低流量域可能没有足够数据。Google 和 Gmail 不接受邮件服务提供商的白名单申请,因此“提交审核后进入 Gmail 白名单”不是可行流程。citeturn17search15turn17search2
Gmail 建议垃圾邮件率始终低于 0.1%,并避免达到 0.3%;新 IP 应缓慢、稳定预热,避免突然改变发送量。大批量营销邮件必须同时包含 RFC 8058 一键退订 Header 和正文中清晰可见的退订入口。citeturn5view0turn17search2
Microsoft SNDS 提供发送 IP 的信誉数据,并整合 JMRP 投诉反馈;发送方仍需对列表清洁和异常 IP 行为负责。2026 年 SNDS 门户和数据访问机制发生了迁移,自动化采集程序应以当前官方门户展示的 API、授权和续期要求为准,不应继续依赖多年以前的固定 CSV 字段或永久链接。citeturn19search13turn6view3
Yahoo CFL 是基于 DKIM 域的服务;只有 DKIM 签名邮件才能登记并收到 ARF 投诉报告。Yahoo 对大批量发送者要求 SPF 和 DKIM、至少 p=none 的 DMARC、一键退订、正文退订入口、两天内处理退订,并将垃圾邮件投诉率维持在 0.3% 以下。citeturn18search3turn18search8
审核或解封申请前,建议准备以下证据包:
组织法定名称与网站
发送域及所有 DKIM d= 域
全部发送 IP 与 CIDR
每个 IP 的 PTR、A/AAAA 与 HELO
SPF、DKIM、DMARC 查询结果
近 30 天按供应商统计的发送量
硬退信、投诉、退订和 4xx/5xx 比率
名单来源、双重确认流程与同意时间戳示例
投诉和退信抑制时延
一键退订 Header 示例
最近安全事件及修复说明
新 IP 预热计划
实际生产邮件的完整 Header
常见拒绝根因通常不是“缺少一句申请说明”,而是平台仍观测到认证失败、投诉偏高、发送突增、动态或共享 IP、购买名单、退订故障、无效地址过多、内容或链接域异常。应先降低或暂停高风险流量,修复根因并积累一段稳定数据,再申请复核。
源码与 API 集成
Postal 主仓库是以 Ruby/Rails 为主体的应用,根目录包含 app、config、db、docker、lib、public、spec 等;app 下进一步包含 controllers、models、senders、services 和 views,lib/postal 中包含消息数据库、解析、签名、HTTP、垃圾邮件检查和消息检查器等基础逻辑。citeturn14search1turn9view0turn9view1turn9view2
| 路径 | 主要职责 | 二次开发关注点 |
|---|---|---|
app/controllers | Web UI 与 API 请求处理 | 鉴权、参数验证、API 行为 |
app/models | 组织、服务器、消息、域、凭据等数据模型 | 状态机、关联、持久化 |
app/senders | 消息投递相关执行逻辑 | SMTP/HTTP 投递、重试 |
app/services | 跨模型业务服务 | 编排和领域逻辑 |
app/scheduled_tasks | 周期性维护任务 | 清理、状态更新 |
lib/postal/message_parser | MIME/RFC 消息解析 | 自定义 Header 和内容处理 |
lib/postal/signer | 签名相关逻辑 | 不应绕过或替换密钥管理 |
lib/postal/spam_check | 反垃圾调用与结果 | Rspamd/SpamAssassin 扩展 |
lib/postal/message_db | 消息数据库访问 | 分库与数据迁移 |
docker | 容器构建 | 依赖和运行用户 |
db | 主数据库迁移 | 升级兼容与回滚评估 |
spec | 自动化测试 | 定制修改必须增加回归测试 |
不建议直接修改生产容器内源码。需要定制时,应 fork 仓库、固定上游版本、用 CI 构建私有镜像,并维护以下过程:
上游 release
→ 安全审查
→ 合并到内部 fork
→ 单元与集成测试
→ 数据库迁移演练
→ 构建签名镜像
→ 预生产真实流量回放
→ 分批上线
SMTP 集成。 Postal SMTP 凭据在 Web UI 对应虚拟邮件服务器中创建。官方支持常见 SMTP AUTH 机制,From 域必须属于该服务器或组织,否则可能返回 530。基于 IP 的认证虽然可用,但容易因网络边界错误造成滥用,应优先使用每应用独立凭据。citeturn13view1
Python 示例:
import os
import ssl
import smtplib
from email.message import EmailMessage
smtp_host = os.environ["POSTAL_SMTP_HOST"]
smtp_port = int(os.getenv("POSTAL_SMTP_PORT", "25"))
smtp_user = os.environ["POSTAL_SMTP_USER"]
smtp_password = os.environ["POSTAL_SMTP_PASSWORD"]
message = EmailMessage()
message["From"] = "Example App <no-reply@<SENDING_DOMAIN>>"
message["To"] = "[email protected]"
message["Subject"] = "Postal SMTP 测试"
message["X-Campaign-ID"] = "transactional-welcome-v1"
message.set_content("这是一封测试邮件。")
message.add_alternative(
"<html><body><p>这是一封测试邮件。</p></body></html>",
subtype="html",
)
context = ssl.create_default_context()
with smtplib.SMTP(smtp_host, smtp_port, timeout=30) as client:
client.ehlo()
client.starttls(context=context)
client.ehlo()
client.login(smtp_user, smtp_password)
client.send_message(message)
应为事务、营销、测试环境分别创建凭据,不得在代码仓库保存密码。每个应用至少应设置连接超时、请求重试上限、消息幂等键和本地熔断。
HTTP API 集成。 Postal 官方认为 API 客户端是优先集成方式;API 使用 JSON,通过 X-Server-API-Key 认证,可发送结构化邮件或完整 RFC 2822 消息。响应包含全局 message_id 以及每个收件人的内部 ID 和 token。当前 API 主要面向投递,不是完整的 Postal 管理 API。citeturn20search1turn20search15
cURL 示例:
curl --fail-with-body
--request POST
"https://postal.<BASE_DOMAIN>/api/v1/send/message"
--header "Content-Type: application/json"
--header "X-Server-API-Key: <未指定-API密钥>"
--data '{
"to": ["[email protected]"],
"from": "Example App <no-reply@<SENDING_DOMAIN>>",
"subject": "Postal API 测试",
"plain_body": "这是一封 API 测试邮件。",
"html_body": "<p>这是一封 API 测试邮件。</p>",
"tag": "transactional-test",
"headers": {
"X-Campaign-ID": "transactional-test-v1",
"X-Application-Message-ID": "01J-EXAMPLE-ID"
}
}'
Python API 示例:
import os
import uuid
import requests
postal_url = os.environ["POSTAL_URL"].rstrip("/")
api_key = os.environ["POSTAL_API_KEY"]
application_message_id = str(uuid.uuid4())
payload = {
"to": ["[email protected]"],
"from": "Example App <no-reply@<SENDING_DOMAIN>>",
"subject": "Postal API 测试",
"plain_body": "测试正文",
"html_body": "<p>测试正文</p>",
"tag": "welcome",
"headers": {
"X-Application-Message-ID": application_message_id,
"X-Campaign-ID": "welcome-v1",
},
}
response = requests.post(
f"{postal_url}/api/v1/send/message",
headers={
"X-Server-API-Key": api_key,
"Content-Type": "application/json",
},
json=payload,
timeout=(5, 30),
)
response.raise_for_status()
result = response.json()
if result.get("status") != "success":
raise RuntimeError(f"Postal API error: {result}")
print(result["data"])
应用数据库应保存至少以下关联字段:
application_message_id
postal_message_id
postal_recipient_id
postal_recipient_token
tenant_id
campaign_id
template_version
recipient_hash 或受控明文
accepted_at
last_delivery_status
suppression_reason
Webhook。 Postal 可以在 Sent、Delayed、DeliveryFailed、Held、Bounce、Click、Loaded 和 DNS Error 等事件发生时发送 HTTP Webhook;若负载信息不足,可再通过 API 查询消息详情。citeturn20search2turn14search11
Webhook 接收器必须满足:
快速返回 2xx
持久化后异步处理
幂等去重
验证来源或网关凭据
限制请求体大小
记录 Postal ID 和应用 ID
失败重试和死信队列
敏感字段脱敏
Flask 示例:
import hmac
import os
from flask import Flask, abort, jsonify, request
app = Flask(__name__)
EXPECTED_PATH_SECRET = os.environ["POSTAL_WEBHOOK_PATH_SECRET"]
def persist_idempotently(event: dict) -> None:
"""
实际实现应写入数据库,并为下面的组合建立唯一索引:
status / event type + message.id + message.token + bounce.id
"""
message = event.get("message") or {}
bounce = event.get("bounce") or {}
dedup_key = (
str(event.get("status") or event.get("event") or ""),
str(message.get("id") or ""),
str(message.get("token") or ""),
str(bounce.get("id") or ""),
)
# TODO:
# INSERT ... ON CONFLICT DO NOTHING
print("Webhook:", dedup_key)
@app.post("/webhooks/postal/<path_secret>")
def postal_webhook(path_secret: str):
if not hmac.compare_digest(path_secret, EXPECTED_PATH_SECRET):
abort(401)
if request.content_length and request.content_length > 1_000_000:
abort(413)
event = request.get_json(silent=False)
persist_idempotently(event)
# 在后台任务中完成抑制、统计和通知,不要阻塞 Postal 重试。
return jsonify({"ok": True}), 200
官方 Webhook 文档展示的是 HTTP JSON 负载;除非部署版本明确提供并验证了签名 Header,否则不要假定 Webhook 天然具备加密签名。可在 API Gateway 或反向代理层增加 mTLS、Basic Auth、不可猜测路径、WAF 和源网络限制。内部 Webhook 地址若解析到私网,需在 allowed_request_destinations 中精确放行,不能添加过宽的 10.0.0.0/8 或所有回环地址。
推荐事件处理映射:
| Postal 事件 | 应用动作 |
|---|---|
| Sent | 记录成功投递和延迟 |
| Delayed | 按供应商和错误码累计 4xx;必要时降速 |
| DeliveryFailed | 判断永久/临时失败并更新抑制 |
| Bounced | 关联原消息,提取 DSN 和增强状态码 |
| Held | 创建人工审核工单 |
| MessageLinkClicked | 仅在已合法告知和允许跟踪时记录 |
| MessageLoaded | 将其视为近似信号,不作为“真人已读”的确定证据 |
| DomainDNSError | 立即告警 DNS、SPF、DKIM、MX 或 Return-Path 异常 |
邮件客户端可能代理图片、预取链接或执行隐私保护,因此打开和点击事件不能直接等同于用户真实意图;监控和分析系统应将它们标记为近似指标。
监控、告警与仪表盘
Postal Worker 和 SMTP 进程原生提供 Prometheus 格式的 /metrics 及 /health;默认端口分别是 9090 和 9091,默认绑定回环。端口被占用时健康服务器可能不启动,但 Postal 主进程仍继续运行,因此既要检查业务端口,也要检查健康端口。citeturn20search17
推荐架构:
Prometheus 抓取示例:
# /etc/prometheus/prometheus.yml
scrape_configs:
- job_name: postal-worker
static_configs:
- targets:
- "127.0.0.1:9090"
- job_name: postal-smtp
static_configs:
- targets:
- "127.0.0.1:9091"
- job_name: postal-webhook-exporter
static_configs:
- targets:
- "127.0.0.1:9188"
如果 Prometheus 位于独立节点,应将健康端点绑定到 Postal 私网 IP,并通过主机防火墙只允许 Prometheus 源地址;不得直接把 9090/9091 暴露公网。
首先检查实际版本暴露的指标名:
curl --fail http://127.0.0.1:9090/health
curl --fail http://127.0.0.1:9091/health
curl --fail http://127.0.0.1:9090/metrics | less
curl --fail http://127.0.0.1:9091/metrics | less
Postal 官方没有承诺下列自定义业务指标名称,因此告警示例中的 postal_delivery_*、postal_complaints_total、postal_queue_* 应由 Webhook Collector、数据库 exporter 或 Prometheus recording rules 生成,不能未经检查直接复制上线。
指标定义。
接受量
= Postal 成功接受的收件人数量
成功投递率
= 成功投递收件人数 / 接受收件人数 × 100%
永久退信率
= 永久失败收件人数 / 接受收件人数 × 100%
临时延迟率
= 发生 4xx 的投递尝试数 / 总投递尝试数 × 100%
投诉率
= 平台反馈的投诉人数 / 平台所定义的已投递或收件箱人数 × 100%
队列年龄
= 当前时间 - 队列中最老消息进入时间
端到端延迟
= 最终成功或失败时间 - Postal 接受时间
Webhook 失败率
= 非 2xx 或超时 Webhook 请求 / Webhook 总请求 × 100%
不同平台的投诉率分母不完全相同。Yahoo 明确说明其垃圾邮件率以投递到收件箱的邮件为基础,因此不能简单拿内部“总接受量”与 Yahoo CFL 报告直接比较。Google 应以 Postmaster Tools 展示的垃圾邮件率为主要平台信号。citeturn18search8turn17search2
建议初始阈值。 除 Google/Yahoo 的 0.3% 要求外,下表是需要按实际流量和业务 SLO 调整的运维基线,不是各平台统一官方标准。
| 指标 | 建议警告 | 建议严重 | 处理 |
|---|---|---|---|
| 投诉率,1h 且样本充分 | ≥ 0.08% | ≥ 0.20%;达到 0.30% 停止营销流量 | 暂停高投诉活动、抑制投诉者、审查名单来源 |
| 永久退信率,24h | ≥ 2% | ≥ 5% | 停止对应导入名单,验证地址来源 |
| 成功投递率,15m | < 98% | < 95% | 按收件域拆分 4xx/5xx |
| 临时延迟率,15m | > 5% | > 15% | 对目标域降速,检查突发流量和信誉 |
| SPF/DKIM/DMARC 通过率 | < 99.5% | < 98% | 暂停有问题的发送源 |
| 事务邮件最老队列年龄 | > 5 分钟 | > 15 分钟 | 检查 Worker、DB、DNS 和目标域 |
| 批量邮件最老队列年龄 | > 30 分钟 | > 2 小时 | 降低入队速度或扩容 |
| 事务邮件 P95 投递延迟 | > 2 分钟 | > 10 分钟 | 按收件域分析 |
| Webhook 失败率,15m | > 1% | > 5% | 切换重试/死信,修复接收器 |
| MariaDB 磁盘使用 | > 70% | > 85% | 扩容、清理、降低保留期 |
| 宿主机磁盘 inode | > 70% | > 85% | 检查日志和临时文件 |
Worker/SMTP /health | 失败 1 分钟 | 失败 5 分钟 | 重启、回滚或切换节点 |
Google 建议垃圾邮件率保持低于 0.1%,并避免达到 0.3%;因此 0.08% 是用于提前介入的内部警告值,而不是 Google 的额外要求。citeturn5view0turn17search2
低流量环境不要仅按百分比告警。例如一次投诉除以 100 封邮件就是 1%。建议同时加最小样本:
投诉率告警条件
= 投递量 >= 1,000
AND 投诉数 >= 3
AND 投诉率超过阈值
队列阈值可采用静态值和动态基线的较大者:
queue_warning
= max(
固定最低阈值,
过去 28 天同一星期与同一时段队列 P95 × 3
)
queue_critical
= max(
queue_warning × 2,
对应业务的 SLO 上限
)
自定义 Prometheus 告警示例:
groups:
- name: postal-deliverability
rules:
- alert: PostalHighComplaintRate
expr: |
(
sum(rate(postal_complaints_total[1h]))
/
clamp_min(sum(rate(postal_delivered_total[1h])), 1)
) > 0.002
and
sum(increase(postal_delivered_total[1h])) >= 1000
for: 30m
labels:
severity: critical
annotations:
summary: "Postal 投诉率超过 0.20%"
description: "应暂停高风险营销流量并按租户、活动和收件域排查。"
- alert: PostalHighHardBounceRate
expr: |
(
sum(rate(postal_hard_bounces_total[24h]))
/
clamp_min(sum(rate(postal_accepted_total[24h])), 1)
) > 0.05
for: 30m
labels:
severity: critical
annotations:
summary: "Postal 24 小时永久退信率超过 5%"
- alert: PostalOldestTransactionalMessage
expr: postal_queue_oldest_seconds{mail_type="transactional"} > 900
for: 5m
labels:
severity: critical
annotations:
summary: "事务邮件最老队列年龄超过 15 分钟"
- alert: PostalWebhookFailureRate
expr: |
sum(rate(postal_webhook_requests_total{code!~"2.."}[15m]))
/
clamp_min(sum(rate(postal_webhook_requests_total[15m])), 1)
> 0.05
for: 10m
labels:
severity: critical
annotations:
summary: "Postal Webhook 失败率超过 5%"
- alert: PostalHealthDown
expr: up{job=~"postal-worker|postal-smtp"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "Postal 健康或指标端点不可达"
Grafana 仪表盘。
| 行 | 面板 |
|---|---|
| 总览 | 接受量、投递量、成功率、永久退信、投诉、最老队列年龄 |
| 收件域 | Gmail、Microsoft、Yahoo、其他域的吞吐、4xx、5xx、P95 延迟 |
| 信誉 | Gmail spam rate、Microsoft SNDS 状态、Yahoo CFL 投诉、公共 RBL |
| 认证 | SPF、DKIM、DMARC pass/fail、PTR/HELO 检查、TLS 占比 |
| 队列 | 当前长度、按年龄桶、按租户、按活动、按目标域 |
| 应用 | SMTP/API 请求率、认证失败、HTTP 错误、Webhook 重试 |
| 过滤 | Rspamd/SpamAssassin 分数分布、主要规则、隔离和误报 |
| 基础设施 | 容器 CPU/内存、MariaDB 连接/慢查询/磁盘、Redis、网络连接 |
| 发布事件 | Postal 升级、配置修改、DNS 修改、证书更新、活动上线标记 |
日志分析应提取 SMTP 增强状态码、目标 MX、源 IP、租户、Postal 消息 ID、活动 ID 和重试次数。重点聚类:
421 / 451 临时限流或信誉问题
4.7.x 安全策略、灰名单或认证临时故障
5.1.1 用户不存在
5.2.x 邮箱状态或配额
5.7.1 策略、信誉或认证拒绝
5.7.26 Gmail 身份认证问题
5.7.515 Microsoft 高容量发送认证不合规
Yahoo 官方说明 421/451 可能由异常流量、疑似垃圾内容、用户投诉、服务器繁忙、DNS 不可用或认证暂时无法判断等原因引起;因此不应把所有 4xx 视为“对方故障”并无限高速重试。citeturn18search12
运维、安全与合规
备份。 至少备份全部 postal 和 postal-* 数据库、/opt/postal/config、SMTP 私钥、Postal signing.key、Caddy 数据,以及有业务价值的 Rspamd/Redis 学习状态。备份应加密、异地保存、限制访问并定期进行恢复演练。
数据库备份示例:
sudo install -d -m 0700 /var/backups/postal
DBS=$(
sudo docker exec postal-mariadb
mariadb -uroot -p'<未指定-ROOT密码>' -N
-e "SHOW DATABASES LIKE 'postal%';"
| tr 'n' ' '
)
sudo docker exec postal-mariadb
mariadb-dump
-uroot
-p'<未指定-ROOT密码>'
--single-transaction
--routines
--triggers
--events
--databases ${DBS}
| zstd -T0 -19
> "/var/backups/postal/postal-db-$(date -u +%Y%m%dT%H%M%SZ).sql.zst"
避免把密码直接放在 shell 历史中。生产脚本应使用权限为 0600 的客户端配置文件、Docker secret 或专用备份账号。
配置备份:
sudo tar
--xattrs
--acls
-C /opt/postal
-czf "/var/backups/postal/postal-config-$(date -u +%Y%m%dT%H%M%SZ).tar.gz"
config caddy-data
校验:
sha256sum /var/backups/postal/*
> "/var/backups/postal/SHA256SUMS-$(date -u +%Y%m%dT%H%M%SZ)"
恢复演练至少验证:
能否解密备份
能否恢复主数据库与各 postal-* 消息库
signing.key 是否完整
SMTP 私钥与证书是否匹配
管理员能否登录
既有虚拟服务器、域、凭据和路由是否存在
历史消息是否可查询
新邮件是否能成功投递和触发 Webhook
升级。 官方升级命令会拉取 helper 和容器、执行数据库迁移并重启容器,不是零停机升级。应在低流量维护窗口执行,并固定版本,而不是生产环境中无审查地永远升级到 latest。citeturn14search10
sudo postal status
# 先备份和验证备份
cd /opt/postal/install
sudo git fetch --all --tags
sudo git pull origin
# 推荐固定经过预生产验证的版本
sudo postal upgrade <PINNED_POSTAL_VERSION>
sudo postal status
研究时 Postal 发布页显示的最新版本为 v3.3.7,发布时间为 2026 年 6 月 3 日,并包含 SSRF、SQL 注入等安全修复;实际维护时仍应重新查看官方 release、安全公告和镜像摘要,再决定固定版本。citeturn8search3
从旧版本升级至 v3 时应确认数据库表使用 DYNAMIC row format;v3 不再需要 RabbitMQ、cron 和 requeuer。citeturn14search8
SHOW TABLE STATUS FROM postal;
ALTER TABLE `<TABLE_NAME>` ROW_FORMAT=DYNAMIC;
日志轮转。 Postal 主要将日志写到 stdout/stderr,由 Docker 日志驱动处理。官方建议为 Docker 设置受限大小的本地日志,或发送到 GELF/syslog 等集中平台。citeturn13view2
/etc/docker/daemon.json 示例:
{
"log-driver": "local",
"log-opts": {
"max-size": "100m"
}
}应用并检查:
sudo systemctl restart docker
docker info --format '{{.LoggingDriver}}'
重启 Docker 会影响容器,应在维护窗口执行,并确认 restart policy 正常。
Postal 也可配置 GELF:
logging:
enabled: true
gelf:
host: "<LOG_PRIVATE_IP>"
port: 12201
facility: postal
集中日志系统应对收件地址、主题、正文片段、Webhook 负载、API Key、SMTP 密码和认证 Header 做脱敏。
入侵防护与访问控制。
SSH 仅允许密钥、VPN或堡垒机
禁用 root 密码登录
Postal UI 强制 HTTPS
管理界面启用 OIDC 或强 MFA 上游
每应用、每环境、每租户独立 SMTP/API 凭据
密钥定期轮换并保存在 Secret Manager
数据库、Redis、Rspamd、指标端口仅绑定私网或回环
Webhook 出站目标使用最小允许列表
应用入口实施请求限速和租户额度
持续检查开放中继和匿名发送
Postal 支持 OIDC,可将登录委托给外部身份提供商;如启用,应明确是否继续允许本地密码登录。citeturn20search13
开放中继测试必须使用组织授权的外部测试节点,并验证未经认证的任意 From/To 组合无法投递。不要使用公共“开放中继测试网站”提交真实凭据。
Fail2ban 可以基于 Postal syslog 或反向代理日志封禁暴力认证,但日志格式可能随版本变化。先提取实际失败样本:
sudo postal logs smtp
| grep -Ei 'auth|authentication|credential|login|failed'
| tail -100
| sudo tee /var/tmp/postal-auth-failures.log
过滤器模板:
# /etc/fail2ban/filter.d/postal-smtp.conf
[Definition]
# 必须用当前版本的真实日志字段替换 <EXACT_AUTH_FAILURE_PATTERN>
failregex = ^.*<EXACT_AUTH_FAILURE_PATTERN>.*(?:ip|remote_ip)[=: ]+"?<HOST>"?.*$
ignoreregex =
测试后才能启用:
sudo fail2ban-regex
/var/tmp/postal-auth-failures.log
/etc/fail2ban/filter.d/postal-smtp.conf
# /etc/fail2ban/jail.d/postal-smtp.local
[postal-smtp]
enabled = true
filter = postal-smtp
backend = systemd
maxretry = 10
findtime = 10m
bantime = 1h
不要复制未经验证的正则表达式,否则可能完全不生效,或错误封禁正常邮件服务器。
TLS 自动续期。 Caddy 可自动管理 Web 证书,但 SMTP TLS 证书是 Postal 单独读取的文件。可使用 Certbot DNS 插件申请 smtp.<BASE_DOMAIN>,再通过 deploy hook 原子复制并重启 Postal。DNS 提供商未指定,因此认证器名称仍为占位符。
sudo certbot certonly
--dns-<DNS_PROVIDER_PLUGIN>
-d smtp.<BASE_DOMAIN>
/etc/letsencrypt/renewal-hooks/deploy/postal-smtp-cert.sh:
#!/usr/bin/env bash
set -euo pipefail
DOMAIN="smtp.<BASE_DOMAIN>"
SRC="/etc/letsencrypt/live/${DOMAIN}"
DST="/opt/postal/config"
install -o root -g root -m 0644
"${SRC}/fullchain.pem" "${DST}/smtp.cert.new"
install -o root -g root -m 0600
"${SRC}/privkey.pem" "${DST}/smtp.key.new"
mv "${DST}/smtp.cert.new" "${DST}/smtp.cert"
mv "${DST}/smtp.key.new" "${DST}/smtp.key"
postal stop
postal start
openssl s_client
-connect "${DOMAIN}:25"
-starttls smtp
-servername "${DOMAIN}"
</dev/null 2>/dev/null
| openssl x509 -noout -subject -issuer -dates
sudo chmod 0750
/etc/letsencrypt/renewal-hooks/deploy/postal-smtp-cert.sh
sudo certbot renew --dry-run
如停启造成的短暂停机不可接受,应在预生产确认 Postal 当前版本是否支持平滑重载,或采用多节点滚动更新。
安全更新与供应链。
固定 Postal、MariaDB、Caddy、Rspamd 和 Redis 镜像版本
记录并验证镜像 digest
订阅 Postal GitHub release 与安全公告
对镜像运行漏洞扫描
禁止在生产主机直接修改容器文件
所有配置变更进入版本控制,但秘密单独管理
定期审计 Docker socket、sudoers 和云 IAM
合规与隐私。 实际适用法律和行业规则均为“未指定”,应由法务或数据保护负责人根据发送方、收件人、组织注册地、数据存储地和邮件目的确定。本报告不能替代法律意见。
Postal 数据库、邮件历史、日志和 Webhook 可能包含收件地址、发件地址、主题、邮件正文、附件、IP、User-Agent、打开/点击信息和退信内容,因此应将 Postal 视为处理个人数据和通信内容的系统。建议建立:
| 控制项 | 实施要求 |
|---|---|
| 处理目的 | 区分事务通信、营销、反欺诈、日志和分析 |
| 合法基础/同意 | 保存同意来源、时间、文本版本和撤回记录 |
| 数据最小化 | 不在日志保存完整正文和凭据;只保留排错所需字段 |
| 保留期限 | 分消息、Webhook、日志、投诉、退信和抑制记录制定期限 |
| 数据主体请求 | 支持查询、更正、删除、限制和退出请求 |
| 跨境与供应商 | 评估云商、DNS、监控、备份和告警平台 |
| 加密 | 传输 TLS;备份和敏感数据库加密 |
| 事件响应 | 定义泄露判断、通知、证据保全和复盘流程 |
| 跟踪透明度 | 明确披露打开像素、链接重写及分析目的 |
| 抑制例外 | 删除请求与防止再次发送所需最小抑制信息之间进行法律评估 |
美国 CAN-SPAM 对商业邮件要求真实 Header、非欺骗性主题、有效退出方式、及时处理退出以及有效的实体邮寄地址;FTC 说明退出请求通常须在十个工作日内处理。平台信誉标准往往更严格,Yahoo 要求大批量发送者在两天内处理退订,因此技术目标应远短于法律最长时限。citeturn15search6turn18search8
若适用 CCPA/CPRA,可能涉及消费者的知情、删除、更正、退出出售或共享以及限制敏感个人信息使用等权利;覆盖范围和例外必须结合组织情况判断。citeturn15search1
若涉及欧盟、英国或类似隐私制度,应评估处理合法基础、透明度、数据最小化、安全、处理者合同和直接营销规则。英国 ICO 明确指出,使用批量邮件平台并不会把营销合规责任转移给平台;针对个人订阅者的未经请求电子营销通常需要有效同意或满足特定 soft opt-in 条件,购买名单也必须证明收件人明确同意由实际发送组织通过相应渠道联系。citeturn16search2turn16search13
打开像素和点击跟踪会产生额外的 IP、User-Agent、时间和行为数据。在不能证明必要性和合法基础时,应默认关闭,或允许按租户、活动和收件人类别禁用。Postal 支持通过 Header 或链接标记跳过部分跟踪。citeturn20search7
附录
常用命令速查。
# Postal
postal status
postal start
postal stop
postal logs
postal logs web
postal logs smtp
postal logs worker
postal initialize
postal make-user
# Docker
docker ps
docker stats
docker logs --tail 200 <CONTAINER>
docker inspect <CONTAINER>
docker system df
# 网络与端口
ss -lntp
nc -vz smtp.<BASE_DOMAIN> 25
curl -I https://postal.<BASE_DOMAIN>
# SMTP TLS
openssl s_client
-connect smtp.<BASE_DOMAIN>:25
-starttls smtp
-servername smtp.<BASE_DOMAIN>
# DNS
dig A postal.<BASE_DOMAIN>
dig A smtp.<BASE_DOMAIN>
dig -x <PUBLIC_IPV4>
dig TXT <SENDING_DOMAIN>
dig TXT postal._domainkey.<SENDING_DOMAIN>
dig TXT _dmarc.<SENDING_DOMAIN>
dig MX <SENDING_DOMAIN>
# 健康与指标
curl http://127.0.0.1:9090/health
curl http://127.0.0.1:9091/health
curl http://127.0.0.1:9090/metrics
curl http://127.0.0.1:9091/metrics
# SpamAssassin
sa-update
systemctl status spamassassin
systemctl status spamassassin-maintenance.timer
# Rspamd
rspamadm configtest
rspamc < sample.eml
systemctl status rspamd
# Redis
redis-cli ping
redis-cli INFO memory
# MariaDB
docker exec -it postal-mariadb mariadb -uroot -p
生成 Postal 默认 DKIM 记录时,可使用当前 helper 提供的命令,并以 Web UI 对具体发送域给出的记录为最终依据:
postal default-dkim-record
最小上线检查表。
| 检查 | 验收结果 |
|---|---|
| Ubuntu 和 Docker 版本进入补丁管理 | 未验证 |
| 出站 25 到 Gmail/Microsoft/Yahoo 可达 | 未验证 |
| 公网 IP 为固定地址 | 未验证 |
| PTR → SMTP FQDN → 同一 IP | 未验证 |
| HELO 与 PTR 主机名一致 | 未验证 |
| SPF 通过且 DNS lookup 未超限 | 未验证 |
| DKIM 使用有效 selector 并通过 | 未验证 |
| DMARC 通过且 From 域对齐 | 未验证 |
| Web HTTPS 证书自动续期 | 未验证 |
| SMTP STARTTLS 证书有效 | 未验证 |
| 未认证开放中继测试失败 | 未验证 |
| 每应用/租户凭据及限额 | 未验证 |
| 永久退信与投诉自动抑制 | 未验证 |
| RFC 8058 一键退订 | 未验证 |
| Gmail Postmaster Tools | 未注册 |
| Microsoft SNDS/JMRP | 未注册 |
| Yahoo Sender Hub/CFL | 未注册 |
| Prometheus、Grafana、Alertmanager | 未部署 |
| 数据库和配置备份恢复演练 | 未完成 |
| 安全事件和信誉事故 Runbook | 未完成 |
排错流程。
邮件未送达
│
├─ Postal 是否接受?
│ ├─ 否:检查 API/SMTP 凭据、From 域、TLS、请求格式和限速
│ └─ 是
│
├─ 是否仍在队列?
│ ├─ 是:检查 Worker、MariaDB、DNS、源 IP、出站 25、IP 池
│ └─ 否
│
├─ 最终状态是什么?
│ ├─ Sent:检查收件箱/垃圾箱、完整 Header 和目标平台信誉
│ ├─ Delayed:按 MX、4xx 增强状态码、IP 池和时间窗口聚类
│ ├─ Failed:区分永久地址失败、认证、信誉、内容和策略
│ └─ Held:检查 SpamAssassin/Rspamd 分数和路由动作
│
├─ 认证是否全部通过?
│ ├─ SPF:Return-Path 域及源 IP
│ ├─ DKIM:selector、DNS、签名和正文变更
│ └─ DMARC:From 与 SPF/DKIM 对齐
│
├─ 平台信号
│ ├─ Gmail Postmaster:spam rate、域/IP信誉、合规
│ ├─ Microsoft SNDS/JMRP:IP 状态和投诉
│ └─ Yahoo Sender Hub/CFL:投诉与 SMTP 错误
│
└─ 采取动作
├─ 认证故障:暂停受影响发送源
├─ 投诉上升:停止活动并抑制投诉者
├─ 地址质量差:停止名单导入并清洗
├─ 4xx 限流:按目标域降速
└─ IP 信誉事故:隔离流量、调查账号滥用、重新预热
关键官方链接。
| 主题 | 官方入口 |
|---|---|
| Postal 前提条件 | Postal Prerequisites |
| Postal 安装 | Postal Installation |
| Postal 配置 | Postal Configuration |
| Postal DNS | Postal DNS Configuration |
| Postal 升级 | Postal Upgrading |
| Postal Spam/Virus | Spam & Virus Checking |
| Postal 指标 | Health & Metrics |
| Postal API | Using the API |
| Postal Webhook | Webhooks |
| Postal 源码 | postalserver/postal |
| Docker Ubuntu 安装 | Install Docker Engine on Ubuntu |
| Gmail 发送要求 | Email Sender Guidelines |
| Gmail Postmaster Tools | Set up Postmaster Tools |
| Microsoft 高容量要求 | Outlook High-Volume Sender Requirements |
| Microsoft SNDS | Smart Network Data Services |
| Yahoo 发送要求 | Yahoo Sender Best Practices |
| Yahoo CFL | Yahoo Complaint Feedback Loop |
| Yahoo SMTP 错误 | Yahoo SMTP Error Codes |
| Rspamd 文档 | Rspamd Documentation |
| SpamAssassin | Apache SpamAssassin |
| 美国商业邮件合规 | FTC CAN-SPAM Guidance |
| 加州隐私要求 | California CCPA |
| 欧盟 GDPR 正文 | EUR-Lex GDPR |