Ubuntu 平台部署 Postal 邮件投递服务器:架构、安装、信誉、集成与运维报告

Postal 是面向网站、应用和自动化系统的开源邮件投递平台,提供 SMTP、HTTP API、入站邮件路由、投递队列、Webhook、抑制列表、IP 池及邮件跟踪等能力,定位接近自托管的 SendGrid、Mailgun 或 Postmark。它更适合作为事务邮件、通知邮件、批量邮件以及入站邮件转发平台,不应直接等同于包含 IMAP、POP3、日历和最终用户邮箱存储的传统企业邮箱套件。需要员工邮箱的场景,应另行设计 Dovecot、Microsoft Exchange、Google Workspace 等邮箱系统。citeturn20search6turn20search8

截至本报告日期 2026 年 8 月 7 日,Postal 官方支持的安装方式是容器化部署;官方最低资源要求为 2 个 CPU 核心、4 GB 内存和至少 25 GB 磁盘,并要求 MariaDB 10.6 或更高版本,明确不支持用 MySQL 替代。Postal v3 已移除 RabbitMQ、cron 和 requeuer 依赖,核心安装也不要求 Redis;Redis 通常只在接入 Rspamd 的学习、灰名单、限速和信誉模块时使用。citeturn20search4turn14search8

生产部署的关键难点并不是“让 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。citeturn17search2turn18search8turn18search9

用户没有提供实际域名、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 版本,而不是仅因为版本最新就直接升级。citeturn2search0turn2search1turn20search4

硬件规划。 官方最低配置适合低流量试运行,不应被理解为生产容量保证。生产规格应通过邮件大小、峰值并发、保留期限和 MariaDB 写入量压测确定。citeturn20search4

可使用以下公式估算磁盘下限:

邮件数据容量
≈ 每日接受邮件数
× 平均原始邮件大小
× 邮件保留天数
× 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。citeturn20search4

域名与 DNS 规划。 建议将管理/API、SMTP、退信路径和跟踪域分开。以下只是结构示例:

用途示例占位符说明
管理及 APIpostal.<BASE_DOMAIN>Web UI、HTTP API
SMTP 主机名smtp.<BASE_DOMAIN>PTR、HELO/EHLO、SMTP TLS 证书
SPF includespf.<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 配置。citeturn20search10turn20search8

示例记录如下:

; 地址记录
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 策略。citeturn17search2turn18search7turn18search8

PTR、HELO 与 IP 规划。 每个发送 IP 都应拥有有意义、非动态格式的 PTR;PTR 指向的主机名应通过 A 或 AAAA 再解析回同一 IP,并与 Postal 的 dns.helo_hostname 或 postal.smtp_hostname 保持一致。Google 明确要求发送 IP、PTR 主机名和该主机名的正向记录互相匹配;Yahoo 同样要求有效的正向和反向 DNS。citeturn17search2turn18search8

建议按业务性质隔离信誉:

事务邮件池:密码重置、订单、告警
营销邮件池:订阅、活动、推广
高风险租户池:用户生成内容、试用账号
内部测试池:开发与预生产

Postal 的 IP 池默认关闭;开启后,界面中声明的每个 IP 必须真实配置在发送节点网络接口上,否则邮件可能无法出队。Yahoo 也建议将营销邮件与事务邮件按 IP 或 DKIM 域分离,避免一种流量损害另一种流量的信誉。citeturn20search12turn18search8

端口。

端口方向用途暴露建议
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 行为后才应开放。citeturn14search0turn20search17

基础 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。citeturn20search3turn13view0

公有云与私有云差异。

项目公有云私有云或自建机房
TCP 25经常默认封禁,需要提交解封申请取决于 ISP 和边界防火墙
PTR 管理通常在云商控制台或工单设置通常由 ISP/IP 注册机构设置
IP 历史信誉新分配或回收 IP 可能已有历史问题自有长期 IP 更可控,但需自行维护信誉
NAT需确认出站固定源 IP及一对一映射需自行保证 SNAT、入站 DNAT 和连接跟踪
IP 扩展较容易增加,但每个 IP 都要重新预热受 ISP、地址资源和 BGP 能力限制
存储/数据库可使用托管数据库,但要评估延迟、权限和私网访问完全自管备份、高可用和恢复
DDoS/高可用云商可提供负载均衡和防护,但 SMTP 支持差异大需自行建设边界防护和备用链路
合规地域可选择区域,但应核查子处理方及跨境传输物理位置更可控,运维责任更高

Postal 官方特别提醒,虚拟服务器供应商可能分配已进入阻断名单的 IP,且许多供应商会封锁出站 25 端口;这两项应在采购和安装前验证,而不是部署完成后再处理。citeturn20search4

Postal 安装与配置

以下流程采用官方容器安装方法。命令应先在测试环境执行;密码、域名、版本标签和证书路径仍为“未指定”。

安装 Docker。 建议使用 Docker 官方 APT 仓库,而不是 Ubuntu 中可能滞后的兼容包或生产环境中的 convenience script。citeturn2search1

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 版本、备份工具和组织安全基线的兼容性。citeturn20search4

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 为每个虚拟邮件服务器建立消息数据库。citeturn20search4

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 才能完整工作。citeturn14search8turn10search15

可选安装:

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。citeturn20search4

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、证书或容器挂载权限。citeturn12view0

如需多人运维,建议用受控管理员组而非开放 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 组。

生成配置、初始化并启动。citeturn20search3

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.keyPostal 内部签名私钥必须加密备份
/opt/postal/config/CaddyfileWeb 反向代理配置必须备份
/opt/postal/config/smtp.keySMTP TLS 私钥必须加密备份
/opt/postal/config/smtp.certSMTP 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 防护。citeturn14search0turn20search3

配置修改后执行:

sudo postal stop
sudo postal start
sudo postal status

反向代理。 官方示例使用 Caddy,并以 host network 将公网 80/443 反代到 127.0.0.1:5000。Caddy可自动申请 Web 证书。citeturn20search3

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,否则可能降低信誉或产生损坏的跟踪链接。citeturn20search7

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 证书。citeturn20search16

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 开始,再根据真实误报情况调整。citeturn20search0turn20search6

SpamAssassin 与 Rspamd 对比。

维度SpamAssassinRspamd
架构传统规则与插件体系,由 spamd 提供 TCP 扫描事件驱动、异步处理,面向较高并发
Postal 接入官方文档完整,TCP 783Postal 配置原生支持 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 开销。citeturn10search10turn10search15turn10search3

SpamAssassin 安装。citeturn20search0

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 检查;使用公共列表前必须确认查询限额和商业许可。citeturn10search5

示例 /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。citeturn10search4turn10search15

速率限制。 应至少在四层实施:

应用用户 → 应用 API 配额
应用/租户 → Postal SMTP/API 凭据配额
发送域/IP 池 → 每分钟和每小时速率
目标收件域 → Gmail、Microsoft、Yahoo 等分别自适应限速

建议默认采用令牌桶或漏桶,并区分接受速率与实际外投速率:

tenant_rate = min(
租户合同额度,
IP 当前预热额度,
目标域动态额度,
全局安全上限
)

Rspamd 限速模块可以用 Redis 保存令牌桶,但应用侧仍应先限制恶意或失控请求,避免垃圾邮件已经进入 Postal 队列后才发现。citeturn10search7

退信、投诉和抑制。 Postal 通过专用 Return-Path 路由关联退信,检测到退信后更新原始消息并发送 Webhook。永久退信、投诉和明确退订应立即进入抑制列表;临时退信则按增强状态码和次数处理。citeturn0search6turn14search11

建议规则:

事件动作
5.1.1 用户不存在立即永久抑制
其他明确 5xx 永久错误按地址永久抑制,保留原因
4xx 邮箱已满暂停 24~72 小时后有限重试
4xx 信誉/速率限制降低该收件域速率,不抑制用户
用户投诉立即抑制营销和非必要邮件
RFC 8058 一键退订立即处理,目标远短于平台两天上限
连续软退信达到内部阈值后进入临时抑制
地址长期无互动降频、再确认或停止营销

黑白名单。 Rspamd 的 multimap 可基于发件人、收件人、IP、域、Header 等实现列表,但“白名单”不应绕过病毒扫描、最大邮件大小或身份认证。citeturn10search9

建议顺序:

强制安全拒绝
→ 账户/租户封禁
→ 永久投诉与退信抑制
→ 企业黑名单
→ 受限白名单减分
→ 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 SNDSIP 信誉与流量监控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;接收 ARFDKIM 不通过;selector 或 d= 域不匹配;验证记录错误;投诉处理地址无效
Yahoo 发送合规自动化信誉和策略判断SPF、DKIM、DMARC、PTR、一键退订、低投诉按 Sender Requirements 持续合规并监控 CFL超过 0.3% 投诉;未在两天内退订;突发发送;购买名单;营销与事务共用信誉

Google Postmaster Tools 验证时会提供 TXT 值;发布后返回平台点击 Verify。Google 指出验证通常很快,但 DNS 状态更新可能需要约十分钟;只有验证域后才能显示该域数据,并且低流量域可能没有足够数据。Google 和 Gmail 不接受邮件服务提供商的白名单申请,因此“提交审核后进入 Gmail 白名单”不是可行流程。citeturn17search15turn17search2

Gmail 建议垃圾邮件率始终低于 0.1%,并避免达到 0.3%;新 IP 应缓慢、稳定预热,避免突然改变发送量。大批量营销邮件必须同时包含 RFC 8058 一键退订 Header 和正文中清晰可见的退订入口。citeturn5view0turn17search2

Microsoft SNDS 提供发送 IP 的信誉数据,并整合 JMRP 投诉反馈;发送方仍需对列表清洁和异常 IP 行为负责。2026 年 SNDS 门户和数据访问机制发生了迁移,自动化采集程序应以当前官方门户展示的 API、授权和续期要求为准,不应继续依赖多年以前的固定 CSV 字段或永久链接。citeturn19search13turn6view3

Yahoo CFL 是基于 DKIM 域的服务;只有 DKIM 签名邮件才能登记并收到 ARF 投诉报告。Yahoo 对大批量发送者要求 SPF 和 DKIM、至少 p=none 的 DMARC、一键退订、正文退订入口、两天内处理退订,并将垃圾邮件投诉率维持在 0.3% 以下。citeturn18search3turn18search8

审核或解封申请前,建议准备以下证据包:

组织法定名称与网站
发送域及所有 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、垃圾邮件检查和消息检查器等基础逻辑。citeturn14search1turn9view0turn9view1turn9view2

路径主要职责二次开发关注点
app/controllersWeb UI 与 API 请求处理鉴权、参数验证、API 行为
app/models组织、服务器、消息、域、凭据等数据模型状态机、关联、持久化
app/senders消息投递相关执行逻辑SMTP/HTTP 投递、重试
app/services跨模型业务服务编排和领域逻辑
app/scheduled_tasks周期性维护任务清理、状态更新
lib/postal/message_parserMIME/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 的认证虽然可用,但容易因网络边界错误造成滥用,应优先使用每应用独立凭据。citeturn13view1

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。citeturn20search1turn20search15

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 查询消息详情。citeturn20search2turn14search11

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 主进程仍继续运行,因此既要检查业务端口,也要检查健康端口。citeturn20search17

推荐架构:

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 展示的垃圾邮件率为主要平台信号。citeturn18search8turn17search2

建议初始阈值。 除 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 的额外要求。citeturn5view0turn17search2

低流量环境不要仅按百分比告警。例如一次投诉除以 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 视为“对方故障”并无限高速重试。citeturn18search12

运维、安全与合规

备份。 至少备份全部 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。citeturn14search10

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、安全公告和镜像摘要,再决定固定版本。citeturn8search3

从旧版本升级至 v3 时应确认数据库表使用 DYNAMIC row format;v3 不再需要 RabbitMQ、cron 和 requeuer。citeturn14search8

SHOW TABLE STATUS FROM postal;

ALTER TABLE `<TABLE_NAME>` ROW_FORMAT=DYNAMIC;

日志轮转。 Postal 主要将日志写到 stdout/stderr,由 Docker 日志驱动处理。官方建议为 Docker 设置受限大小的本地日志,或发送到 GELF/syslog 等集中平台。citeturn13view2

/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,可将登录委托给外部身份提供商;如启用,应明确是否继续允许本地密码登录。citeturn20search13

开放中继测试必须使用组织授权的外部测试节点,并验证未经认证的任意 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 要求大批量发送者在两天内处理退订,因此技术目标应远短于法律最长时限。citeturn15search6turn18search8

若适用 CCPA/CPRA,可能涉及消费者的知情、删除、更正、退出出售或共享以及限制敏感个人信息使用等权利;覆盖范围和例外必须结合组织情况判断。citeturn15search1

若涉及欧盟、英国或类似隐私制度,应评估处理合法基础、透明度、数据最小化、安全、处理者合同和直接营销规则。英国 ICO 明确指出,使用批量邮件平台并不会把营销合规责任转移给平台;针对个人订阅者的未经请求电子营销通常需要有效同意或满足特定 soft opt-in 条件,购买名单也必须证明收件人明确同意由实际发送组织通过相应渠道联系。citeturn16search2turn16search13

打开像素和点击跟踪会产生额外的 IP、User-Agent、时间和行为数据。在不能证明必要性和合法基础时,应默认关闭,或允许按租户、活动和收件人类别禁用。Postal 支持通过 Header 或链接标记跳过部分跟踪。citeturn20search7

附录

常用命令速查。

# 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 DNSPostal DNS Configuration
Postal 升级Postal Upgrading
Postal Spam/VirusSpam & Virus Checking
Postal 指标Health & Metrics
Postal APIUsing the API
Postal WebhookWebhooks
Postal 源码postalserver/postal
Docker Ubuntu 安装Install Docker Engine on Ubuntu
Gmail 发送要求Email Sender Guidelines
Gmail Postmaster ToolsSet up Postmaster Tools
Microsoft 高容量要求Outlook High-Volume Sender Requirements
Microsoft SNDSSmart Network Data Services
Yahoo 发送要求Yahoo Sender Best Practices
Yahoo CFLYahoo Complaint Feedback Loop
Yahoo SMTP 错误Yahoo SMTP Error Codes
Rspamd 文档Rspamd Documentation
SpamAssassinApache SpamAssassin
美国商业邮件合规FTC CAN-SPAM Guidance
加州隐私要求California CCPA
欧盟 GDPR 正文EUR-Lex GDPR

Comments

No comments yet. Why don’t you start the discussion?

    Leave a Reply

    Your email address will not be published. Required fields are marked *