
本文详解在邮件转发(尤其是 SRS 改写 Return-Path)场景下,如何不解析、不重建、不重编码地原样转发原始邮件,从而确保 DKIM 签名体哈希(body hash)完全一致,避免 DMARC 失败。核心方案是绕过 MIME 解析器与 MTA 重封装,直接复用原始 RFC 5322 邮件流。
本文详解在邮件转发(尤其是 srs 改写 return-path)场景下,如何**不解析、不重建、不重编码**地原样转发原始邮件,从而确保 dkim 签名体哈希(body hash)完全一致,避免 dmarc 失败。核心方案是绕过 mime 解析器与 mta 重封装,直接复用原始 rfc 5322 邮件流。
在构建合规的邮件转发服务(如支持 SRS 的别名转发、群组邮件代理或企业邮件网关)时,一个关键挑战是:任何对原始邮件正文的微小改动——包括空行增删、边界(boundary)重生成、CRLF 标准化、HTML/Text 部分的自动换行插入——都会导致 DKIM bh=(body hash)校验失败,进而触发 DMARC p=reject 策略。 这正是使用 PhpMimeMailParser + SwiftMailer/Laravel Mailable 模式时的典型痛点:一旦调用 $parser->getMessageBody() 或 ->getHtmlPart(),就已脱离原始字节流,进入“语义重建”阶段,DKIM 完整性必然丧失。
因此,正确路径不是“修复重建逻辑”,而是彻底规避重建。理想方案需满足三点:
✅ 保留原始邮件全部字节(含所有 CRLF、空行、多边界嵌套、无结构纯文本等边缘情况);
✅ 仅修改 SMTP envelope(即 MAIL FROM),用于注入 SRS 编码后的 Return-Path;
✅ 不触碰邮件头(headers)和正文(body)的任意字节,包括 From:、To:、Subject: 等可见字段。
✅ 推荐方案:Raw Email Relay via Postfix sendmail Interface(最稳定)
Laravel 应用通过 Postfix pipe 接收原始邮件后,不应调用任何 MIME 解析器,而应直接将原始字节流(含原始 \r\n 行结束符)交由本地 sendmail 命令转发,并通过 -f 参数指定 SRS 改写后的 envelope sender:
// 在 Laravel 邮件处理脚本中(如 /usr/local/bin/forward-mail.php)
$rawEmail = file_get_contents('php://stdin'); // 完整原始 RFC 5322 邮件
// ✅ 正确:仅改 envelope,不碰邮件内容
$srsSender = \SRS\SRS::new()->forward('original@example.com');
$command = sprintf(
'/usr/sbin/sendmail -f %s -- %s',
escapeshellarg($srsSender),
escapeshellarg('recipient@domain.com')
);
$process = proc_open($command, [
['pipe', 'w'], // stdin
['pipe', 'r'], // stdout
['pipe', 'r'] // stderr
], $pipes);
if (is_resource($process)) {
fwrite($pipes[0], $rawEmail); // ⚠️ 关键:原样写入,不作任何转换
fclose($pipes[0]);
$output = stream_get_contents($pipes[1]);
$error = stream_get_contents($pipes[2]);
proc_close($process);
if (!empty($error)) {
error_log("Sendmail error: $error");
exit(1);
}
}? 为什么安全?
Postfix 的 sendmail 接口(通常为 /usr/sbin/sendmail)严格遵循 RFC 5321,它接收的是完整的、已格式化的 RFC 5322 邮件消息,并仅将其作为数据块投递至 Postfix 的 cleanup 队列。Postfix 不会重新解析或重写邮件体,仅校验语法合法性(如是否含空行分隔 header/body)。只要原始邮件本身有效,bh= 哈希就 100% 保持不变。
❌ 不推荐方案及其风险
| 方案 | 问题根源 | DKIM 风险 |
|---|---|---|
| SwiftMailer + setBody() 重建 | 自动标准化换行、添加空行、重生成 boundary | ✅ 必然失败(bh 变更) |
| PhpMimeMailParser 提取再组装 | 丢失原始空白、强制解码 base64/quoted-printable | ✅ 高概率失败 |
| fsockopen 直连 SMTP | 需手动实现 SMTP 协议、易出错;连接池管理复杂;TLS 握手开销大 | ⚠️ 可行但运维成本高,非必要不选 |
⚙️ 关键注意事项
- 必须保留原始 CRLF:file_get_contents('php://stdin') 默认返回原始字节,切勿用 trim()、str_replace("\n", "\r\n") 或 PHP_EOL 替换换行符;
- SRS 实现需独立于邮件解析:推荐使用 php-srs 库,在获取原始 Return-Path: 头后直接计算,不依赖解析结果;
- Postfix 配置加固:确保 smtpd_relay_restrictions 允许本地 sendmail 提交(默认 permit_mynetworks 已满足),且禁用 header_checks 对转发邮件的修改;
- 日志与监控:记录原始邮件 Message-ID 与转发状态,便于排查 DKIM 失败是否源于上游而非本机。
✅ 总结
DKIM 的设计初衷是保证“邮件体不可篡改”,因此真正的合规转发,永远是字节级透传(byte-for-byte relay),而非“智能重建”。放弃对邮件内容的控制欲,转而信任 Postfix 的原生投递能力,是解决 SRS+DKIM+DMARC 兼容问题的最简、最稳、最标准路径。记住:你不是在发送一封新邮件,而是在转交一封原始信封——只需换发件人地址(envelope),其余一切照旧。









