跳转到主要内容

于 2025年04月22日 摘录自 Postfix After-Queue Content Filter

简介

本文件要求使用 Postfix 版本 2.1 或更高版本。

通常,Postfix 接收邮件、将其存储在邮件队列中,然后进行投递。通过本文所述的外部内容过滤器,邮件将在队列中存储后再进行过滤。这种方法将邮件接收过程与邮件过滤过程解耦,使您能够最大程度地控制同时运行的过滤进程数量。

队列后内容过滤器应按以下方式使用:

网络或
本地用户
-> Postfix
队列
-> 内容
过滤器
-> Postfix
队列
-> 网络或
本地邮箱

本文档描述了使用单个 Postfix 实例处理所有任务的实现方式:接收、过滤和投递邮件。使用两个独立 Postfix 实例的应用程序将在本文档的后续版本中进行说明。

队列后内容过滤器不应与 SMTPD_PROXY_READMEMILTER_README 文档中描述的方法混淆,其中入站 SMTP 邮件在存储到 Postfix 队列之前会被过滤。

本文档描述了两种过滤所有电子邮件的方法,以及几种选择性过滤邮件的选项:

工作原理

一个队列后内容过滤器从 Postfix 接收未过滤的邮件(如下面进一步描述),并可以执行以下操作之一:

  1. 将邮件重新注入 Postfix,可能在更改内容和/或目的地后。
  2. 丢弃或隔离邮件。
  3. 拒绝邮件(通过向 Postfix 发送适当的状态代码)。Postfix 将邮件发回发件人地址。

注意:在当前邮件蠕虫和伪造垃圾邮件泛滥的时代,将病毒发送回发件人地址是一个非常糟糕的主意,因为发件人地址几乎肯定不是病毒的来源。最好丢弃已知的病毒,并将可疑内容隔离,以便人工决定如何处理。

简单内容过滤示例

第一个示例设置简单,但存在重大限制,将在第二个示例中解决。Postfix 通过 smtpd(8) 服务器接收未过滤的邮件,并通过 Postfix 的 pipe(8) 交付代理将未过滤的邮件传递给内容过滤器。内容过滤器使用 Postfix 的 sendmail(1) 命令将过滤后的邮件重新注入 Postfix,以便 Postfix 将其传递到最终目的地。

这意味着通过 Postfix sendmail(1) 命令提交的邮件无法进行内容过滤。

在下图中,带数字的名称代表 Postfix 命令或守护进程。请参阅 OVERVIEW 文档,以了解 Postfix 架构的概述。

未过滤

 
->

 
smtpd(8)

pickup(8)
>- cleanup(8)-> qmgr(8)
Postfix 
队列
-< local(8)
smtp(8)
pipe(8)
->
->
 
过滤
过滤
 
 ^
 |
 
 maildrop 
queue 
<- Postfix
postdrop(1)
<- Postfix
sendmail(1)
<- 内容 
过滤器
 

内容过滤器可以是一个简单的 shell 脚本,例如:

 1 #!/bin/sh
2
3 # 简单的 shell 基于过滤器。它旨在以以下方式调用:
4 # /path/to/script -f 发件人 收件人...
5
6 # 根据实际情况调整这些选项。在 Postfix 2.3 之前,-G 选项没有作用。
7 INSPECT_DIR=/var/spool/filter
8 SENDMAIL="/usr/sbin/sendmail -G -i" # 切勿在此处使用 "-t"。
9
10 # 退出代码来自 <sysexits.h>
11 EX_TEMPFAIL=75
12 EX_UNAVAILABLE=69
13 
14 # 完成或中止时清理。
15 trap "rm -f in.$$" 0 1 2 3 15
16
17 # 开始处理。
18 cd $INSPECT_DIR || {
19 echo $INSPECT_DIR 不存在;退出 $EX_TEMPFAIL; }
20
21 cat >in.$$ || { 
22 echo 无法将邮件保存到文件;退出 $EX_TEMPFAIL; }
23
24 # 在此指定您的内容过滤器。
25 # filter <in.$$ || {
26 # echo 消息内容被拒绝;退出 $EX_UNAVAILABLE; }
27
28 $SENDMAIL "$@" <in.$$
29
30 exit $?

注释:

  • 第 8 行: -G 选项表示过滤器输出不是本地邮件提交: 不要做一些愚蠢的事情,比如在消息头中的地址后附加本地域名。此选项在 Postfix 2.3 版本之前不起作用。
  • 第 8 行:-i 选项表示当行仅包含 "." 时不要停止读取输入。
  • 第 8 行:切勿在此处使用 "-t" 命令行选项。它会导致邮件投递错误,例如将邮件列表中的消息发回邮件列表。
  • 第 21 行:该方案是先将邮件内容捕获到文件中,再通过第三方内容过滤程序处理内容。
  • 第 22 行:若无法将邮件内容捕获到文件,则通过退出状态码 75(EX_TEMPFAIL)终止邮件投递。Postfix 将邮件放入延迟投递队列并稍后重试。
  • 第25行:您需要在此处指定一个实际的内容过滤程序,该程序通过标准输入接收内容。
  • 第26行:如果内容过滤程序发现问题,邮件将通过退出状态69(EX_UNAVAILABLE)被退回。Postfix会将邮件作为无法投递的邮件发送回发件人。
  • 注意:在当前邮件蠕虫和垃圾邮件泛滥的时代,将已知病毒或垃圾邮件退回到发件人是一个糟糕的主意,因为该地址很可能已被伪造。更安全的做法是丢弃已知病毒,并将可疑内容隔离以便人工审查。
  • 第 28 行:如果内容正常,则将其作为输入传递给 Postfix sendmail 命令,过滤器命令的退出状态即为 Postfix sendmail 命令的退出状态。Postfix 将按常规方式投递邮件。
  • 第 30 行:Postfix 返回 Postfix sendmail 命令的退出状态。

建议您先手动运行此脚本直至对结果满意。使用真实邮件(包含头部和正文)作为输入运行:

% /path/to/script -f sender -- recipient... <message-file

一旦对内容过滤脚本满意:

  • 创建一个名为"filter"的专用本地用户账户。该用户负责处理所有潜在危险的邮件内容,因此必须使用独立账户。切勿使用"nobody",更不要使用"root"或"postfix"。
  • 创建仅对"filter"用户可访问的目录/var/spool/filter。该目录用于存储内容过滤脚本的临时文件。
  • 配置 Postfix 使用 pipe(8) 传递代理将邮件转发至内容过滤器(请参阅 pipe(8) 手册页以了解命令语法)。

    /etc/postfix/master.cf:
    # =============================================================
    # 服务类型 权限 是否启用 chroot 唤醒 最大进程数 命令
    # (是) (是) (是) (从不) (100)
    # =============================================================
    filter unix - n n - 10 pipe
    flags=Rq user=filter null_sender=
    argv=/path/to/script -f ${sender} -- ${recipient}
    

    这将并行运行最多 10 个内容过滤器。建议使用系统可承受的最大进程数替代 10 的限制。内容检测软件可能消耗大量系统资源,因此不应同时运行过多实例。空的 null_sender 设置在 Postfix 2.3 及更高版本中是必需的。

  • 要仅对通过 SMTP 接收的邮件启用内容过滤,请在 master.cf 条目中,该条目定义了 Postfix SMTP 服务器:

    /etc/postfix/master.cf:
    # =============================================================
    # 服务类型 权限 chroot 唤醒 最大进程数 命令
    # (yes) (yes) (yes) (never) (100)
    # =============================================================
    smtp inet ...其他内容在此,请勿修改... smtpd
    -o content_filter=filter:dummy
    

    "-o content_filter" 这行配置会导致 Postfix 在每个传入邮件中添加一个内容过滤请求记录,内容为"filter:dummy"。该记录会覆盖正常的邮件路由,并将邮件转发给内容过滤器处理。

    content_filter 配置参数期望的值格式为 transport:destinationtransport 名称指定了 master.cf 中邮件投递代理定义的第一个字段;下一跳 destination 的语法在对应投递代理的手册页中描述。

    空的下一跳过滤器 destination 的含义取决于版本。Postfix 2.7 及更高版本将使用收件人域名;较早版本将使用 $myhostname。为兼容 Postfix 2.6 或更早版本,或指定一个非空的下一跳过滤器 destination

    content_filter 设置的优先级低于在 access(5)header_checks(5)body_checks(5) 表中指定的 FILTER 操作。

  • 执行 "postfix reload" 以完成更改。

简单内容过滤性能

使用上述 shell 脚本,通过 SMTP 传输的邮件在 Postfix 性能上将损失四倍。对于内容过滤过程中创建和删除的每个临时文件,转发性能还会再降低一个数量级。对于本地提交或投递的邮件,性能影响较小,因为此类投递本身就比 SMTP 转发邮件更慢。

简单内容过滤器限制

上述内容过滤器的问题在于其不够健壮。原因是软件与 Postfix 之间未采用明确定义的协议。如果过滤器 shell 脚本因内存分配问题而异常终止,脚本将不会返回 /usr/include/sysexits.h 文件中定义的正常退出状态。此时邮件不会进入延迟队列,而是直接被退回。同样的健壮性问题也可能在内容过滤软件自身遇到资源问题时发生。

简单内容过滤方法不适用于通过 header_checksbody_checks 模式触发的内容过滤操作。这些模式会在邮件通过 Postfix sendmail 命令重新注入后再次应用,导致邮件过滤循环。高级内容过滤方法(见下文)允许关闭 header_checksbody_checks 模式。

关闭简单内容过滤器

要关闭"简单"内容过滤:

  • 编辑 master.cf 文件,从定义 Postfix SMTP 服务器的条目中删除 "-o content_filter=filter:dummy" 文本。
  • 执行 "postsuper -r ALL" 以从现有队列文件中删除内容过滤请求记录。
  • 执行另一个 "postfix reload"。

高级内容过滤示例

第二个示例更为复杂,但性能更好,且在机器遇到资源问题时更少出现邮件弹回。此内容过滤器通过本地主机端口 10025 接收未过滤的 SMTP 邮件,并通过本地主机端口 10026 将过滤后的邮件发送回 Postfix。

对于不支持SMTP的内容过滤软件,Bennett Todd的SMTP代理实现了一个不错的PERL/SMTP内容过滤框架。参见: https://web.archive.org/web/20151022025756/http://bent.latency.net/smtpprox/

在下图中,名称后跟数字代表 Postfix 命令或守护进程程序。请参阅 OVERVIEW 文档以了解 Postfix 架构的概述。

未过滤

未过滤
->

-> 
smtpd(8)

pickup(8)
>- cleanup(8)-> qmgr(8)
Postfix 
队列
-< smtp(8)

local(8)
->

->
过滤

过滤
 ^
 |
 
 smtpd(8)
10026
 smtp(8)
 
 
 ^
 |
 
 内容过滤器 10025 

此处给出的示例会过滤所有邮件,包括通过 SMTP 到达的邮件以及通过 Postfix 的 sendmail 命令本地提交的邮件(本地提交通过 pickup(8) 服务器进入 Postfix;为了简化示例,我们省略了本地提交的详细信息)。请参阅本文档末尾的示例,了解如何排除本地用户免受过滤,或如何配置基于目标的內容过滤器。

对于通过 SMTP 到达和离开的邮件,Postfix 的性能大约会降低一倍,前提是内容过滤器不创建临时文件。内容过滤器创建的每个临时文件都会使性能损失增加一倍。

高级内容过滤器:要求所有邮件均经过过滤

要为所有邮件启用高级内容过滤器方法,请在 main.cf 中指定:

/etc/postfix/main.cfcontent_filter = scan:localhost:10025
receive_override_options = no_address_mappings
  • "receive_override_options"这一行禁用内容过滤器前的地址操作,因此内容过滤器看到的是原始邮件地址,而非虚拟别名展开、规范映射、自动抄送、地址伪装等操作的结果。
  • "content_filter"这一行会导致 Postfix 在每个传入邮件中添加一个内容过滤请求记录,内容为"scan:localhost:10025"。内容过滤请求记录由smtpd(8)pickup(8) 服务器(以及 qmqpd(8),如果你启用了此服务)添加。
  • 内容过滤请求存储在队列文件中;这是 Postfix 跟踪需要过滤邮件的方式。当队列文件中包含内容过滤请求时,队列管理器会将邮件交付给指定的内容过滤器,无论其最终目的地为何。
  • content_filter 配置参数期望的值为 transport:destination 格式。transport 名称指定了 master.cf 中邮件投递代理定义的第一个字段;下一跳 destination 的语法在对应投递代理的手册页中描述。
  • 空的下一跳过滤器 destination 的含义取决于版本。Postfix 2.7 及更高版本将使用收件人域名;较早版本将使用 $myhostname。为兼容 Postfix 2.6 或更早版本,或指定一个非空的下一跳过滤器 destination
  • content_filter 设置的优先级低于在 access(5)header_checks(5)body_checks(5) 表中指定的 FILTER 操作。

高级内容过滤器:将未过滤的邮件发送至内容过滤器

在此示例中,"scan" 是 Postfix SMTP 客户端的一个实例,其配置参数略有不同。以下是在 Postfix master.cf 文件中设置该服务的方式:

/etc/postfix/master.cf:
# =============================================================
# 服务类型 权限 是否启用 启动方式 最大进程数 命令
# (是) (是) (是) (从不) (100)
# =============================================================
scan unix - - n - 10 smtp
-o smtp_send_xforward_command=yes
-o disable_mime_output_conversion=yes
-o smtp_generic_maps=
  • 这将同时运行最多 10 个内容过滤器。建议使用系统可承受的进程限制,而非默认的 10 个并发进程。内容检测软件可能占用大量系统资源,因此不应让过多进程同时运行。
  • 若设置 "-o smtp_send_xforward_command=yes",扫描传输将尝试通过内容过滤器将原始客户端名称和 IP 地址转发给后过滤器 SMTP 进程,以便过滤后的邮件以真实客户端名称和 IP 地址进行日志记录。参见 smtp(8)XFORWARD_README 以获取更多信息。
  • "-o disable_mime_output_conversion=yes"是一个解决方法,可防止域密钥和其他数字签名被破坏。这是因为某些基于 SMTP 的内容过滤器不会声明支持 8BITMIME,尽管它们实际上可以处理 8 位邮件。
  • "-o smtp_generic_maps=" 是一个临时解决方案,用于防止使用 generic(5) 映射进行本地地址重写。此类重写仅应在邮件发送到互联网时发生。

高级内容过滤器:运行内容过滤器

内容过滤器可通过 Postfix 进程启动服务(相当于 inetd)进行配置。例如,要在本地主机端口 10025 上启动最多 10 个内容过滤进程:

/etc/postfix/master.cf:
# ===================================================================
# 服务类型 权限 是否启用 运行环境 最大进程数 命令
# (是) (是) (是) (从不) (100)
# ===================================================================
localhost:10025 inet n n n - 10 spawn
user=filter argv=/path/to/filter localhost 10026
  • "filter" 是一个专用的本地用户账户。该用户不会登录,可以设置 "*" 密码,并使用不存在的 shell 和 home 目录。该用户负责处理所有潜在危险的邮件内容,因此应作为独立账户。
  • 默认情况下,Postfix 会终止运行时间超过 command_time_limit 秒(默认:1000 秒)的命令。这是为了防止过滤器无限运行而采取的安全措施。

如果您希望过滤器监听端口 localhost:10025 而不是 Postfix,则必须将过滤器作为独立程序运行,并且不得使用 Postfix 的 spawn 服务。

高级过滤器:将邮件重新注入 Postfix

内容过滤器的任务是,要么以适当的诊断信息弹回邮件,要么通过端口 localhost 10026 上的专用监听器将邮件重新注入 Postfix。

最简单的内容过滤器只是在输入和输出之间复制 SMTP 命令和数据。如果出现问题,它只需对 Postfix 发来的 `.' 输入回复 `550 content rejected`,并在将邮件重新注入 Postfix 的连接上断开连接而不发送 `.`。

/etc/postfix/master.cf:
# ===================================================================
# 服务类型 权限 是否启用 启动方式 最大进程数 命令
# (是) (是) (是) (从不) (100)
# ===================================================================
localhost:10026 inet n - n - 10 smtpd
-o content_filter= 
-o receive_override_options=no_unknown_recipient_checks,no_header_body_checks,no_milters
-o smtpd_helo_restrictions=
-o smtpd_client_restrictions=
-o smtpd_sender_restrictions=
# Postfix 2.10 及更高版本:指定空的 smtpd_relay_restrictions。
-o smtpd_relay_restrictions=
-o smtpd_recipient_restrictions=permit_mynetworks,reject
-o mynetworks=127.0.0.0/8
-o smtpd_authorized_xforward_hosts=127.0.0.0/8
  • 注意:请勿在 "=" 或 "," 字符周围添加空格。
  • 注意:SMTP 服务器的进程限制不得小于 "filter" master.cf 中的设置。
  • "-o content_filter="会覆盖main.cf中的设置,并要求对来自内容过滤器的邮件不进行内容过滤。这是必需的,否则邮件会循环。
  • "-o receive_override_options" 覆盖 main.cf 设置,以避免在内容过滤器之前已经完成的工作被重复执行。这些选项与在 main.cf 中指定的选项互补:

    这些接收覆盖选项要么由 SMTP 服务器本身实现,要么传递给清理服务器。

  • "-o smtpd_xxx_restrictions" 和 "-o mynetworks=127.0.0.0/8" 覆盖 main.cf 中的设置。它们会关闭在此处仅会浪费时间的垃圾邮件控制。
  • 使用 "-o smtpd_authorized_xforward_hosts=127.0.0.0/8",扫描传输将尝试将原始客户端名称和 IP 地址转发给后过滤器 SMTP 进程,以便过滤后的邮件使用真实客户端名称和 IP 地址进行日志记录。参见 XFORWARD_READMEsmtpd(8)

高级内容过滤性能

采用本文所述的"三明治"内容过滤方法时,需确保过滤器并发数与可用 CPU、内存和 I/O 资源相匹配。内容过滤进程过少,即使在低流量情况下,邮件也会在活动队列中积压;并发过多则会导致Postfix因资源不足而失败,进而推迟发送本应进入内容过滤的邮件。

目前,内容过滤器的性能调优是一个试错过程;由于过滤和未过滤的消息共享同一队列,分析受到限制。如本文档开头所述,使用多个 Postfix 实例进行内容过滤将在未来版本中覆盖。

关闭高级内容过滤器

要关闭"高级"内容过滤:

  • 删除或注释掉以下两行main.cf中的内容。关闭内容过滤后,为高级内容过滤所做的其他更改将无效。

    /etc/postfix/main.cfcontent_filter = scan:localhost:10025
    receive_override_options = no_address_mappings
    
  • 执行 "postsuper -r ALL" 以从现有队列文件中删除内容过滤请求记录。
  • 执行另一个 "postfix reload"。

仅过滤来自外部用户的邮件

最简单的方法是在 master.cf 中为一个 Postfix 实例配置多个 SMTP 服务器 IP 地址:

  • 两个 SMTP 服务器 IP 地址,仅用于内部用户的邮件,且内容过滤已关闭。

    /etc/postfix.master.cf:
    # ==================================================================
    # 服务类型 权限 是否启用 chroot 唤醒 最大进程数 命令
    # (是) (是) (是) (从不) (100)
    # ==================================================================
    1.2.3.4:smtp inet n - n - - smtpd
    -o smtpd_client_restrictions=permit_mynetworks,reject
    127.0.0.1:smtp inet n - n - - smtpd
    -o smtpd_client_restrictions=permit_mynetworks,reject
    
  • 用于外部用户发送邮件的单个 SMTP 服务器地址,启用了内容过滤。

    /etc/postfix.master.cf:
    # =================================================================
    # 服务类型 权限 是否启用 chroot 唤醒 最大进程数 命令
    # (是) (是) (是) (从不) (100)
    # =================================================================
    1.2.3.5:smtp inet n - n - - smtpd
    -o content_filter=filter-service:filter-destination 
    -o receive_override_options=no_address_mappings
    

之后,您可以按照上述"高级"或"简单"内容过滤示例中的步骤操作,但必须确保不指定"content_filter" 或 "receive_override_options" 在 main.cf 文件中。

不同域名的不同过滤器

如果您是 MX 服务提供商,并希望为不同域名应用不同的内容过滤器,可以在 master.cf 中配置一个 Postfix 实例,并为其分配多个 SMTP 服务器 IP 地址。每个地址提供不同的内容过滤服务。

/etc/postfix.master.cf:
# =================================================================
# 服务类型 权限 是否启用 chroot 唤醒 最大进程数 命令
# (是) (是) (是) (从不) (100)
# =================================================================
# 用于通过 service1:dest1 过滤的域的 SMTP 服务
1.2.3.4:smtp inet n - n - - smtpd
-o content_filter=service1:dest1 
-o receive_override_options=no_address_mappings
# 用于过滤服务2:dest2的域的SMTP服务
1.2.3.5:smtp inet n - n - - smtpd
-o content_filter=service2:dest2
-o receive_override_options=no_address_mappings

之后,您可以按照上述"高级"或"简单"内容过滤示例中的步骤操作,但必须不要指定"content_filter" 或 "receive_override_options"。 cf 文件中。

在 DNS 中设置 MX 记录,将每个域名路由到正确的 SMTP 服务器实例。

访问或标头/正文表中的过滤操作

上述过滤配置是静态的。遵循给定路径的邮件要么始终被过滤,要么从未被过滤。从 Postfix 2.0 开始,您还可以动态启用内容过滤。

要通过 access(5) 表规则启用内容过滤:

/etc/postfix/access:
whatever FILTER foo:bar

要通过 header_checks(5)body_checks(5) 表模式启用内容过滤:

/etc/postfix/header_checks:
/whatever/ FILTER foo:bar

您还可以在 smtpd 访问映射以及清理服务器的 header/body_checks 中进行此操作。此功能必须谨慎使用:您必须在 after-filter smtpd 和清理守护进程中禁用所有 UCE 功能,否则将导致内容过滤循环。

限制:

  • 来自 smtpd 访问映射和 header/body_checks 的 FILTER 操作优先于通过 main.cf content_filter 参数指定的过滤器。
  • 如果一条消息触发了多个过滤器操作,则仅最后一个操作生效。
  • 同一内容过滤器将应用于给定消息的所有收件人。