现在我们已经掌握了 Procmail 的基本知识,我们可以继续前进,开始构建一个更完整的邮件处理系统。 本章的高级技术仅在您需要非常专门的邮件处理时才需要,而在设置基本的电子邮件服务器时不需要。 您可能希望跳过本章,并在您的服务器完全配置和运行后返回到它。
在本章中,我们将使用一些更高级的 Procmail 功能。 本章将涵盖:
- 传递和非传递食谱之间的区别
- 高级配方中变量、替换和伪变量的使用
- 锁定和使用各种标志来控制执行
- 如何应用条件来测试消息的各个部分
- 将消息转发、保存或传递给外部程序进行处理的高级操作
- 正则表达式的介绍
- 使用 Procmail 宏简化电子邮件标题分析
- 详细分析了一些高级食谱,并提供了一些示例食谱
在本章结束时,您应该拥有一个有用的工具箱,其中包含了一些例程,可以将自己的一组 Procmail 菜谱组合在一起,并控制您的邮件。
到目前为止,我们只讨论了将邮件最终传递给程序或文件或将消息转发给另一个邮件用户的那些菜谱。 还有另一个可用的选项,引用 Procmail 文档:
有两种食谱——传递食谱和非传递食谱。 如果找到匹配的交付配方,Procmail 将考虑交付的邮件(您猜对了),并将在成功执行配方的操作行后停止处理
.procmailrc
文件。 如果找到匹配的非交付配方,则在执行该配方的操作行之后,将继续处理.procmailrc
文件。
我们在前一章中介绍了一个示例,该示例旨在对邮件项进行备份,以防正在测试的菜谱删除所有邮件。 这是一个非常有用的非交付配方示例,可以在 Procmail 手册页面 procmailex
中找到。
如果你是 Procmail 新手,并计划尝试一下,那么拥有某种安全网通常会有所帮助。 插入前面提到的两个食谱,所有其他食谱将确保始终保留最后 32 个到达的邮件消息。 为了使其正常工作,我们必须在 $MAILDIR
中创建一个名为 backup
的目录,然后再插入这两个菜谱:
:0 c
backup
:0 ic
cd backup && rm -f dummy `ls -t msg.* | sed -e 1,32d`
第二个配方使用了 Procmail 的几个特性,我们将在本章后面的章节中更详细地探讨这些特性。
如果我们一步一步地完成这个配方,我们将得到一个有用的存档实用程序,它记录了最后 32 个要接收的邮件,并且允许我们在创建一个最终销毁邮件而不是存储邮件的配方时手动恢复邮件。 在繁忙的邮件服务器上,增加这个数字以保持更大的消息存档可能是明智的。
第一个配方执行一个简单的备份操作,将邮件的副本或克隆发送到 backup
目录:
:0 c
backup
在添加第二个配方之前,在 .procmailrc
文件中创建上面的配方,并向自己发送两个邮件消息。 我们可以看到,每个邮件项都存储在备份目录中(假设它存在并具有正确的权限)。
第二个配方同样简单,但是使用 Linux 系统命令的一些更复杂的特性删除 backup
目录中的所有邮件项,除了最近的 32 项。
:0 ic
| cd backup && rm -f dummy `ls -t msg.* | sed -e 1,32d`
让我们来看看这个食谱是如何运作的。 首先,我们将看到规则标志及其含义:
|国旗
|
意义
|
| --- | --- |
| i
| 忽略后续管道命令的返回代码 |
| c
| 克隆或复制传入的数据,使原始数据不受影响 |
|
指示 Procmail 将匹配的数据传递给下面的管道命令。 每个命令都执行一个特定的操作。
命令
|
行动
|
| --- | --- |
| cd backup
| 移动到 backup
目录。 |
| ls -t msg.*
| 获取以 msg
开头的文件列表,并按时间顺序对其进行排序。 |
| sed -e 1,32d
| 删除除最后 32 行以外的所有内容—即最近的 32 个邮件项。 |
| rm -f dummy...
| 参数 dummy
用于在没有文件要删除的情况下停止错误消息,然后 rm
命令继续删除 sed
过滤器列出的文件。 |
这两个菜谱是在每个收到的邮件消息上运行的无条件菜谱的示例。 没有条件行,即以星号(*)开头的行,这一事实表明配方是无条件的。 由于这两个菜谱都在菜谱中包含了 c
标志,所以它们也被定义为非交付菜谱。
一旦我们收集了大量的 Procmail 菜谱,我们会发现菜谱的处理顺序非常重要。 通过正确设置处理顺序,我们可以提高性能并减少处理传入邮件的时间。 我们还可以确保将更关键的规则应用于重要消息,然后再将更一般的规则应用于处理批量消息。
一个典型的场景可能是按照以下顺序应用规则:
- 首先处理守护进程或服务器消息。
- 邮件列表应该尽早处理,但在服务器消息之后处理,因为我们希望首先处理我们的服务。
- 应用
kill file
来阻止任何已知的垃圾邮件发送者。 - 在我们处理邮件列表之前,不要发送假期回复,以防止假期回复对邮件列表产生干扰。
- 保存私人信息。
- 检查垃圾邮件(UBE)。 这避免了对已知有效电子邮件进行垃圾邮件检查的高开销。
Formail 是一个外部实用程序(来自 Procmail),在安装 Procmail 的系统上几乎总是可用的。 它的功能是处理邮件消息并从消息头中提取信息。 它充当一个过滤器,可用于强制将邮件转换为适合在 Linux 邮件系统中存储的格式。 它还可以执行许多其他有用的功能,如“From”转义、生成自动回复标题、简单标题提取或分割邮箱/摘要/文章文件。
输入数据邮件/邮箱/物品内容需要使用标准输入来提供。 因此, formail
非常适合在管道命令链中使用。 输出数据在标准输出上提供。
在本章中,我们不打算深入探讨 formail
的微妙之处,但由于它是一个有用的工具,我们将在一些例子中参考它的一些功能。 更多信息可以从系统手册页面获得。
这里我们有一个复杂得多的配方,它实现了一种休假服务的形式,通知发送者您不在并且无法回复电子邮件。 起初认为这可能是一个简单的非传递配方,将消息发送回所有收到的消息。 然而,这并不理想,因为有些人可能最终会收到多个传递确认消息,而您也可能会将消息发送回系统实用程序,而这些实用程序无法理解您善意的回复。
该示例基于 Procmail procmailex
手册页面中的“假期示例”。
vacation.cache
文件由 Formail 维护。 它通过提取发送者的名称并将其插入到 vacation.cache
文件中来维护假期数据库。 这确保它总是包含最近的名称。 文件的大小被限制为大约 8192 字节的最大值。 如果发送者的名称是新的,将发送一个自动回复。
下面的配方实现了一个假期自动回复:
SHELL=/bin/sh # for other shells, this might need adjustment
:0 Whc: vacation.lock
# Perform a quick check to see if the mail was addressed to us
$TO_:.*\<$\LOGNAME\>
# Filter out the mail senders we don't want to send replies to - Ever
* !^FROM_DAEMON
# Make sure that we do not create an endless loop that keeps
# replying to the reply by checking to see if we have already processed
# this message and inserted a loop detection header
* !^X-Loop: [email protected]
| formail -rD 8192 vacation.cache
:0 ehc
# We are pretty certain it's OK to send a reply to the sender of this message
| (
formail -rA"Precedence: junk" \
-A"X-Loop: [email protected]" ; \
echo "Hi, Your message was delivered to my mailbox,"; \
echo "but I won't be back until Monday."; \
echo "-- "; cat $HOME/.signature \
) | $SENDMAIL -oi -t
我们将在本节的最后回到这个食谱,并使用我们在 Procmail 中学到的一些知识创建一个稍微更新的版本。 现在,这个示例将作为一个参考,帮助理解我们在接下来的一般配方结构分解中探索的一些概念。
为我们的规则和配方创建文档或添加注释始终是一项重要的任务。 所有注释都以 #
字符开始,并一直到行尾。 在大多数情况下,将注释放在一行的开头,或者在我们希望记录的一行后面加上一个或两个制表符是很有用的。
但是,在规则文件的一个部分中,注释必须包含在它们自己的行中,即Conditions部分。
# Here is a full line comment
MAILDIR=${HOME}/Maildir # This comment spans multiple
# lines for clarity.
:0: # Comment OK here
* condition # BAD comment. NOT allowed.
# Old versions of Procmail don't understand this.
* condition
{ # Comment OK
# Comment OK
do_something # Comment OK
}
为了跟踪设置、测试结果、默认值等等,我们可以将这些信息存储在变量中。 赋值操作很简单,并且遵循与其他 Linux 脚本语言相同的格式。 基本格式为 VARIABLENAME=VALUE.
变量名中不能有空格。 如果被赋值的值中有空格,则整个变量应该存储在双引号之间。
访问变量的正确方法是将 VARIABLENAME
括在大括号 {}
中,并以美元($)符号作为前缀。 在其他赋值中使用变量是可以接受的。 一些例子如下:
MAILDIR=${HOME}/Maildir # Set the value of the MAILDIR
LOGFILE=${MAILDIR}/log # Store logfiles in the MAILDIR
注意,在前面的示例中, ${HOME}
使用 shell 环境设置的值作为进程启动时设置的值。
仔细使用变量及其命名可以使配方更容易阅读和维护。
有时,能够用一个只能在运行时计算或计算的变量来替换文字元素是必要的或有用的。 Procmail 允许作者在大多数地方用变量替换或命令替换替换大多数文字元素。 使用变量的最简单方法是使用 $varname
格式,这在许多脚本语言中都很常见。
变量/命令
|
替换
|
| --- | --- |
| $VAR
| 在配方中出现 $VAR
的地方,将其替换为该变量持有的值。 |
| ${VAR}iable *
| 当需要将变量与文本字面值连接时,使用 {}
强制将名称改为 ${VAR}
而不是 $VARiable
。 |
如果需要将变量与固定文本或值组合,则 {}
元素允许建立变量名的绝对定义。 注意,除非我们包含 $
修饰符,否则这不会在条件行中发生。
Procmail 借用了一些标准 shell 语法来进行变量初始化。
如果我们希望能够为变量分配一个默认值,以便在变量没有被设置或由于某种原因无法计算的情况下使用,可以使用 or :-
分隔符。 如果我们希望在变量已设置或非 null 的地方应用一个可选值,使用 +
或 :+
分隔符。
分隔符
|
行动
|
| --- | --- |
| ${VAR:-value}
| 如果 VAR
未设置或为空,则替换 value
展开; 否则, VAR
的值被替换。 |
| ${VAR-value}
| 若未设置 VAR
,则替换 value
展开; 否则, VAR
的值被替换。 |
| ${VAR:+value}
| 如果 VAR
被设置或非空,则替换 value
展开; 否则, VAR
的值被替换。 |
| ${VAR+value}
| 设 VAR
,则替换 value
展开; 否则, VAR
的值被替换。 |
一些例子如下:
VAR = "" # Set VAR to null
VAR = ${VAR:-"val1"} # VAR = "val1"
VAR = ""
VAR = ${VAR-"val2"} # VAR = ""
VAR = ""
VAR = ${VAR:+"val3"} # VAR = ""
VAR = ""
VAR = ${VAR+"val4"} # VAR = "val4"
VAR = "val"
VAR = ${VAR:+"val3"} # VAR = "val3"
VAR = "val"
VAR = ${VAR+"val4"} # VAR = "val4"
VAR # unset VAR
VAR = ${VAR:-"val1"} # VAR = "val1"
VAR
VAR = ${VAR-"val2"} # VAR = "val2"
VAR
VAR = ${VAR:+"val3"} # no action
VAR
VAR = ${VAR+"val4"} # no action
可以使用(反标记)将命令的输出赋值给一个变量。 '操作符——反勾号(')的 ASCII 值为 96,而不是普通的撇号('),后者的 ASCII 值为 39。
`cmd1 | cmd2`
这个示例将管道中两个反标记之间的输出赋值给变量或在代码中内联。
Procmail 直接分配了许多特殊变量或伪变量。 更改其中一些值实际上可以改变 Procmail 的操作方式。
Procmail 使用以下变量来确定它将在何处存储任何传递的邮件。
|的名字
|
行动
|
| --- | --- |
| MAILDIR
| MAILDIR
的默认值取自 $HOME
环境变量的值。 它也是 Procmail 在执行期间用于当前工作目录的值。 除非输出文件名包含路径组件,否则它们将被创建在这个默认目录中。 |
| MSGPREFIX
| 当我们希望将文件按顺序写入某个目录时,可以使用此选项。 MSGPREFIX
前缀是使用此选项创建的文件的名称。 默认前缀是 msg.
,因此文件将被命名为 msg.xyz
。 在将文件传送到 maildir
或 MH
目录时,不使用该选项。 |
| DEFAULT
| 这是系统中默认邮件存储区域的位置。 通常我们不会修改这个变量。 |
| ORGMAIL
| 在 DEFAULT
由于任何原因不可用的情况下,它被用作灾难恢复位置。 这绝对不应该被修改。 |
Procmail 在编译时写入了合理的默认值。 大多数情况下,这些不需要改变。
|的名字
|
行动
|
| --- | --- |
| SHELL
| 这是一个标准的环境变量,它指定 Procmail 需要在其中调用子进程的 shell 环境。 赋给它的值应该是 Bourne shell 兼容的,比如 /bin/sh
。 |
| SHELLFLAGS
| 它指定在启动 SHELL
时应该传递给它的任何可选标志。 |
| SENDMAIL
| 这指示 Procmail 在哪里找到用于向其他用户发送邮件的 sendmail
程序。 (通常不会被玩弄)。 |
| SENDMAILFLAGS
| 与 SHELLFLAGS
一样,它指定在 SENDMAIL
程序执行时应该传递给它的任何标志或命令行参数。 |
在执行食谱期间,Procmail 可能需要运行外部命令、处理错误或创建文件。 这些变量控制 Procmail 如何与 shell 交互。
|的名字
|
行动
|
| --- | --- |
| UMASK
| 这提供了创建任何文件时使用的文件权限模式。 详情见 man umask
。 |
| SHELLMETAS
| shell 管道在执行前与 SHELLMETAS
的内容进行比较。 如果在管道命令中发现来自 SHELLMETAS
的任何字符,该命令会被认为过于复杂,以致于 Procmail 无法管理自己,并且会生成一个子 shell 进程。 如果我们知道一个特定的管道总是足够简单,可以让 Procmail 管理自己,但它包含在 SHELLMETAS
中的字符,那么我们可以在处理管道时将一个空字符串临时分配给 SHELLMETAS
,然后恢复 SHELLMETAS
。 这将避免产生一个副壳的开销。 |
| TRAP
| 这里,我们可以分配一个代码段,在 Procmail 执行结束时执行。 例如,它的一个用途是删除在食谱执行期间创建的临时文件。TEMPORARY=$HOME/tmp/pmail.$$``TRAP="/bin/rm -f $TEMPORARY"
|
| EXITCODE
| 当 Procmail 退出时,这个值返回给启动 Procmail 的进程。 通常,返回 0
的值表示成功,而非零值表示某种形式的失败。 通过修改 EXITCODE
值,我们可以返回关于所执行处理的特定信息。Procmail 启动的程序的退出代码存储在变量 $?
中。 |
配方执行期间所需的任何日志输出的详细程度和位置由以下变量控制:
|的名字
|
行动
|
| --- | --- |
| LOGFILE
| 这指定了 Procmail 应该将其所有日志和调试信息写入的位置。 如果该值为空,输出将被发送到标准错误输出,这意味着它将丢失,除非程序是交互运行的或者 stderr
被重定向到某个地方。 |
| LOG
| 如果我们希望自己直接将一些内容写入日志文件,可以给 LOG
变量赋值,然后将其添加到 LOGFILE
。 如果我们想格式化输出并在日志消息后包含一个空行,我们必须记住在输出的消息中包含一个空行。LOG="Procmail is great"
|
| VERBOSE
| 这允许输出为基本默认值或提供详细信息。 设置 VERBOSE=1
将包括详细的日志信息,这些信息将有助于调试我们的菜谱。 为了减少输出信息量,请记住在配方运行后设置 VERBOSE=0
。 |
| LOGABSTRACT
| 如果将 LOGABSTRACT
设置为 all
,那么所有的投递都将包含关于发件人、主题和所投递邮件的大小的信息。 如果您希望停止此日志记录,请设置 LOGABSTRACT=no
。 |
| COMSAT
| 如果设置为 yes
,Procmail 将生成 comsat/biff 通知。 有关更多信息,请参阅 comsat
和 biff
手册页。 |
在配方的处理过程中,Procmail 用配方的当前状态更新以下变量:
|的名字
|
行动
|
| --- | --- |
| PROCMAIL_OVERFLOW
| 如果 Procmail 在启动时读取文件时在 Procmail recipe 文件中发现任何超过缓冲区大小的行,它将把 PROCMAIL_OVERFLOW
的值设置为 yes
。 如果正在读取的行是条件或操作行,则该操作将被认为已失败。 然而,如果它是一个变量赋值或 recipe start, Procmail 将停止读取文件并以异常终止的方式退出。 |
| HOST
| 它包含进程运行所在主机的名称。 |
| DELIVERED
| 如果邮件消息传递成功,则将其设置为 yes
,Procmail 将通知调用进程。 如果我们手动将其设置为 yes
和,则未传递消息,它将丢失而不被跟踪,但调用进程仍认为已成功传递。 |
| LASTFOLDER
| 这将给出向其写入消息的最后一个文件或目录的名称。 |
| MATCH
| 它保存了上次正则表达式操作提取的信息。 |
| $=
| 这保存了最新的评分配方的结果。 更多信息请参见procmailsc手册页。 |
| $1, $2, ...; $@; $#
| 就像标准 shell 一样,它指定了 Procmail 启动时使用的命令行参数。
- 是第一个命令行参数,以此类推。
- 包含所有参数。
$#
包含参数个数。
同时参见 SHIFT
伪变量。 |
| $$
| 它保存当前进程 ID。 这对于创建进程唯一的临时文件很有用。 |
| $?
| 它保存了前一个 shell 命令的退出代码。 |
| $_
| 它保存当前正在处理的 Procmail 文件的名称。 |
| $-
| 这是 LASTFOLDER
的别名。$=
、 $@
不能直接使用; 我们必须将该值赋给另一个变量,然后才能将其用于任何有用的用途。 |
这些变量的主要用途是访问保存在适当部分的数据,但其中配方有一个标志,将处理限制在消息的其他部分。 通过使用 HB
,我们可以在整个消息中访问信息。
的名字
|
行动
|
| --- | --- |
| H
| 它保存当前正在处理的消息的头信息。 |
| B
| 它保存当前正在处理的消息体。 |
下表中的每个变量控制任何锁文件的名称,以及配方等待锁释放的时间。
|的名字
|
行动
|
| --- | --- |
| LOCKFILE
| 为该变量赋值将创建一个全局锁文件,该文件一直保持到 LOCKFILE
被赋值为止。 这个值可以是要创建的另一个锁文件的名称,也可以是要删除任何锁的空值。 |
| LOCKEXT
| 给这个赋值允许我们覆盖作为锁文件名一部分使用的扩展名。 这对于识别创建锁文件的进程非常有用。 |
| LOCKSLEEP
| 如果 Procmail 想要在一个已经被其他进程锁定的文件上创建一个锁,它将进入一个 retry
循环。 变量 T1 指定了在重试获取锁之前休眠和等待的秒数。 |
| LOCKTIMEOUT
| 这指定了锁定文件在被假定为无效并将被覆盖之前的时间(以秒为单位)。 如果值为 0
,那么锁文件将永远不会被覆盖。 缺省值为 1024
秒。 |
如果我们的配方出现错误,我们可以使用这些变量中的任何一个来决定采取什么行动。
|的名字
|
行动
|
| --- | --- |
| TIMEOUT
| 它指定在通知子进程终止之前等待子进程的时间。 缺省值是 960
秒。 |
| SUSPEND
| 它指定 NORESRETRY
重试之间的等待时间。 默认为 16
秒。 |
| NORESRETRY
| 当出现严重的系统资源短缺(如磁盘空间不足或系统已达到最大进程数)时,Procmail 在放弃之前重试的次数。 默认值是 4
,如果是负数,Procmail 将永远重试。 如果资源在重试期间不可用,则消息将被丢弃并归类为无法传递。 |
下表包含关于菜谱中可能使用的各种 Procmail 变量的信息:
|的名字
|
行动
|
| --- | --- |
| LINEBUF
| 这为 Procmail 准备处理的配方行长度设置了一个限制。 如果我们需要处理非常大的正则表达式或将大量数据存储到 MATCH
中,则增加这个值。 |
| SHIFT
| 这类似于正常 shell 处理中的 shift
特性。 将一个正数赋值给这个变量会向下移动 Procmail 的命令行参数。 |
| INCLUDERC
| 这将指示 Procmail 加载另一个包含 Procmail 菜谱的文件。 这个新文件在 Procmail 继续处理当前文件之前被加载和处理。 |
| DROPPRIVS
| 这确保当 Procmail 以 setuid
或 setgid
的形式执行时,没有根特权可用。 将此值设置为 yes
将使 Procmail 删除其所有特权。 |
下面的示例将打印响应中的大部分环境设置,并提供一些在尝试使用 Procmail 调试问题时可能很有帮助的信息。 我们不希望它包含在任何生产文件中,否则我们的日志文件会非常迅速地增长到非常大。
在与其他 Procmail recipe 文件相同的目录中创建一个名为 rc.dump
的文件,并将以下代码行放入该文件中:
请注意,在下一个示例的开始和结束处出现的引号(")是确保配方正确操作所必需的。
#
# Simple Procmail recipe to dump variables to a log file
#
LOG="Dump of ProcMail Variables
MAILDIR is currently :${MAILDIR}:
MSGPREFIX is currently :${MSGPREFIX}:
DEFAULT is currently :${DEFAULT}:
ORGMAIL is currently :${ORGMAIL}:
SHELL is currently :${SHELL}:
SHELLFLAGS is currently :${SHELLFLAGS}:
SENDMAIL is currently :${SENDMAIL}:
SENDMAILFLAGS is currently :${SENDMAILFLAGS}:
UMASK is currently :${UMASK}:
SHELLMETAS is currently :${SHELLMETAS}:
TRAP is currently :${TRAP}:
EXITCODE is currently :${EXITCODE}:
LOGFILE is currently :${LOGFILE}:
LOG is currently :${LOG}:
VERBOSE is currently :${VERBOSE}:
LOGABSTRACT is currently :${LOGABSTRACT}:
COMSAT is currently :${COMSAT}:
PROCMAIL_OVERFLOW is currently :${PROCMAIL_OVERFLOW}:
TODO is currently :${TODO}:
HOST is currently :${HOST}:
DELIVERED is currently :${DELIVERED}:
LASTFOLDER is currently :${LASTFOLDER}:
\$= is currently :$=:
\$1 is currently :$1:
\$2 is currently :$2:
\$$ is currently :$$:
\$? is currently :$?:
\$_ is currently :$_:
\$- is currently :$-:
LOCKFILE is currently :${LOCKFILE}:
LOCKEXT is currently :${LOCKEXT}:
LOCKSLEEP is currently :${LOCKSLEEP}:
LOCKTIMEOUT is currently :${LOCKTIMEOUT}:
TIMEOUT is currently :${TIMEOUT}:
NORESRETRY is currently :${NORESRETRY}:
SUSPEND is currently :${SUSPEND}:"
执行如下命令:
# procmail ./rc.dump
<CTRL-D>
这会创建以下输出:
# procmail ./rc.dump
<CTRL-D>
"Dump of ProcMail Variables
MAILDIR is currently :.:
MSGPREFIX is currently :msg.:
DEFAULT is currently :/var/spool/mail/root:
ORGMAIL is currently :/var/spool/mail/root:
SHELL is currently :/bin/bash:
SHELLFLAGS is currently :-c:
SENDMAIL is currently :/usr/sbin/sendmail:
SENDMAILFLAGS is currently :-oi:
UMASK is currently ::
SHELLMETAS is currently :&|<>~;?*[:
TRAP is currently ::
EXITCODE is currently ::
LOGFILE is currently ::
LOG is currently ::
VERBOSE is currently :1:
LOGABSTRACT is currently ::
COMSAT is currently :no:
PROCMAIL_OVERFLOW is currently ::
TODO is currently ::
HOST is currently :delta.adepteo.net:
DELIVERED is currently ::
LASTFOLDER is currently ::
$= is currently :0:
$1 is currently ::
$2 is currently ::
$$ is currently :9014:
$? is currently :0:
$_ is currently :./rc.dump:
$- is currently ::
LOCKFILE is currently ::
LOCKEXT is currently :.lock:
LOCKSLEEP is currently ::
LOCKTIMEOUT is currently ::
TIMEOUT is currently ::
NORESRETRY is currently ::
SUSPEND is currently ::
Procmail 菜谱遵循一种简单的格式。 然而,有许多方法可以指示 Procmail 解释或实现规则中的指令,这些方法基于许多标志以及规则和配方的编写方式。
正如我们已经发现的,到目前为止,所有规则都以 :0
开始,后面跟着一个或多个标志和指令。 以前,冒号后面有一个数字(:
)来指定规则中存在的条件的数量。 Procmail 的当前版本自动确定条件的数量,因此总是使用值 0
。
我们已经讨论了需要使用锁定机制来阻止多个进程在同一时间对同一个文件进行写操作。 当然,这个需求随过滤器试图调用的流程类型而变化。 例如,仅更改或赋值的过滤器对任何物理文件都没有影响,因此不需要锁定。 类似地,仅将数据转发到另一个进程或另一个接收方的过滤器本质上不需要应用锁。 在大多数情况下,当 Procmail 意识到它正在写文件时,将应用自动锁定,并提供对文件本身的锁定。 在某些情况下,可能需要显式地锁定资源。
为了了解何时自动应用锁,何时完全不需要锁,何时需要强制手动锁,下面是一些示例。
任何以 :0:
开头的规则都将应用自动文件锁定。 在这种情况下,Procmail 将自动确定要发送邮件的文件的名称并创建一个锁文件。 如果锁定文件已经存在,它将等待一段时间,然后重试创建锁。 当它最终创建锁文件时,它将继续进行处理。 如果无法创建锁文件,它将报告一个错误并继续执行下一个规则。
下面的规则使用自动锁定:
:0 <flags>:
有时可能需要强制锁定,特别是通过外部脚本处理邮件时。 在大多数情况下,Procmail 将通过检查进程命令行并查看输出指向何处来确定最终数据要写入的文件的名称。 但是,如果脚本负责选择输出位置本身,或者它依赖于一个可能被另一个 Procmail 进程修改的文件,那么必须按照以下方式特别请求一个锁文件:
:0 <flags> :scriptname.lock
您不太可能需要在您编写的大多数脚本中强制执行锁定。
当转发到执行自己的文件或记录锁定进程(例如在数据库中存储问题报告)的管道时,不需要记录锁定。 类似地,如果消息被转发给另一个用户,最终的传递将负责记录锁定。 简单规则定义为:
:0 <flags>
在我们到目前为止看到的示例中,我们允许 Procmail 的默认设置生效。 但是,可以设置许多标志来控制 Procmail 的工作方式。 【t】【t】
如果配方的冒号行上没有声明任何标志,Procmail 将假定以下标志(H, hb
)已被用作默认值。
国旗
|
行动
|
| --- | --- |
| H
| 只扫描邮件标题。 |
| hb
| 操作行同时传递邮件数据的标题和正文。 |
通常,匹配将在整个邮件包中进行,包括邮件的头和正文。 如果邮件正文可能很大,并且我们知道只需要对标题进行匹配,那么使用 H
标志将匹配操作的范围限制为只跨标题。
相反,有时我们可能正在寻找信息项,可能是只出现在文档正文中的重复页脚或签名,在这种情况下,我们可以使用 B
标志来限制只匹配正文。
国旗
|
行动
|
| --- | --- |
| H
| 仅跨邮件头执行匹配。 |
| B
| 只在整个邮件体中执行匹配。 |
| HB
| 跨整个邮件项执行匹配,包括邮件标题和正文。 |
默认情况下,操作行处理整个电子邮件项,包括邮件头和正文。 如果只需要处理邮件数据的一部分,则可以指定将哪一部分传递给操作行。
|国旗
|
行动
|
| --- | --- |
| h
| 只将头传递给操作行进行处理。 |
| b
| 只将消息体传递给操作行进行处理。 |
| hb
| 同时传递消息头和消息体,以便进行处理。 这是默认范围。 |
重要的是要注意“匹配范围”和“行动范围”之间的区别。 第一种情况下标志的值决定了需要扫描邮件头、正文或整个邮件的哪一部分以进行匹配。 第二种情况下标志的值决定了邮件的哪一部分需要处理。
这可能是所有 Procmail 标志中最难理解的标志集合。 本章后面的例子将解释使用这些标志的各种方式。 简单地说,可以对每一个标志假定如下:
|国旗
|
行动
|
| --- | --- |
| A
| 只有满足前一个配方的条件,该配方才会被处理。 |
| a
| 如果满足前一个配方的条件,且操作无误,则对配方进行处理。 |
| E
| 这与 A
正好相反。 如果不满足之前的配方条件,将对配方进行处理。 |
| e
| 如果满足前面的配方条件,但没有成功完成处理,则将对配方进行处理。 |
| c
| 这指示配方创建原始消息的副本或克隆,并在子流程中使用任何操作处理该副本。 父进程继续处理消息的原始副本。 |
c
标志应该读为 Clone
或 Copy
。 一个常见的误解是,这个标志应该被解释为 Continue
。 Clone
或 Copy
操作创建数据的单独副本,并创建单独的执行流来处理该数据,有时作为完全独立的子进程。 当这个克隆配方完成后,父配方继续执行原始数据。
顽固的 Linux 用户非常清楚大小写的敏感性,并且总是认为 Capitals
与 capitals
完全不同。 然而,Procmail 的默认操作在匹配字符串时是不区分大小写的。 这意味着,对于 Procmail, Capitals
和 capitals
是相同的,除非它被告知应该通过 D
标志应用大小写敏感性。 【5】
我们可以指导 Procmail 如何处理或执行配方,以及在处理过程中遇到错误时采取什么操作。 当只对前几行数据进行处理时,较小的邮件消息可能不会发生错误。 然而,对于较大的消息,当管道只读取部分可用数据时,Linux shell 可能认为存在错误。
理解执行的过滤模式是很重要的。 这个术语可能会令人困惑,因为 Procmail 的设计目的就是过滤邮件。 按照以下方式考虑执行模式“过滤器”:我们正在处理的邮件消息在被管道连接到 Procmail(或者至少是我们的食谱的其余部分)之前,将通过操作行上的任何内容进行管道连接。 查看筛选模式的另一种方式是将数据以某种方式修改并返回到控制 Procmail recipe 以便进一步执行的转换模式。
|国旗
|
行动
|
| --- | --- |
| f
| 通过配方将消息内容传递给外部管道流程进行处理,然后获取流程行的输出,以便替换原始消息内容。 |
| i
| 如果 Linux 管道进程只读取其输入的一部分,然后终止,shell 将向 Procmail 程序发送一个 SIGPIPE
错误信号—— i
标志指示 Procmail 忽略这个信号。 如果期望管道进程在只处理消息的一部分后返回,则应该使用此方法。 |
| r
| 传递给管道流程的数据应该按照原样传递,不需要任何修改。 |
| w
| 默认情况下,Procmail 进程将派生出一个子进程并继续其自己的处理。 w
标志指示 Procmail 等待子进程管道完成,然后再继续自己的处理。 |
| W
| 这与 w
的工作原理相同,但也隐藏管道进程中的任何错误或其他输出消息。 |
可以应用许多条件类型来决定给定的配方是否适用于特定的邮件项。 正确应用条件的思想是减少所执行的不必要处理的数量。
条件行总是以星号(*)开头,后面跟着一个或多个空格。 可以在一个菜谱中应用多个条件行,但它们必须被分组在连续的行上。 分组的逻辑操作是执行一个 AND
操作,这样在执行操作之前必须应用所有条件。
:0
* condition1
* condition2
action_on_condition1_and_condition2
可能要求必须将规则应用于所有消息,而不考虑任何条件。 例如,由于法律或公司策略的原因,这样的规则可以将邮件消息备份到邮件文件夹或将所有邮件归档。
无条件规则隐含在缺少条件行中。 也就是说,规则总是匹配的。
# Save all remaining messages to DEFAULT
:0:
${DEFAULT}/
无条件规则通常用于菜谱嵌套链的末尾,在菜谱没有交付邮件的情况下执行最终的默认操作。 记住,一旦传递了消息,处理就停止了。
那些熟悉的人简单的模式匹配操作,如 ?
或 *
通常用于匹配的文件在文件清单操作,可能想知道是否有可能创建类似的测试匹配的部分邮件头或身体。 好消息是有一个很好的特性,简称为正则表达式或regex。 它们为执行非常复杂的模式匹配操作提供了一种机制。 通常,该特性与 egrep
命令行正则表达式非常匹配。 然而,有经验的 regex
用户必须知道一些重要的区别,以便理解如何编写适合 Procmail 操作的表达式。 本章后面有一个完整的章节是关于写作的 regex
。
可以根据标志定义的邮件消息的数据部分(头、体或两者)运行正则表达式,也可以用于测试先前分配的变量。
|条件
|
行动
|
| --- | --- |
| * regex
| 测试根据正则表达式的标志传递的消息部分。 通常这将只处理头部,除非给出了一个 B
标志来表明匹配的范围是处理消息体。 |
| * variable ?? regex
| 这是将赋值变量与 regex
进行比较。 |
本章前面列出了各种伪变量,它们表示访问 Procmail 应用中包含的信息的方法。 这些伪变量可以像普通变量一样进行比较。
下面的示例将复制邮件主体中包含关键短语的所有邮件项。
VERBOSE=1
:0cB:
* [0-9]+ Linux Rules [ok!]
${MAILDIR}/linuxrules/
VERBOSE=0
下面是对前一个示例操作的快速说明:
- 我们指定
:0cB:
以确保只搜索正文,并创建一个副本,以便仍然处理原始消息。 - 如果正文中的任何地方有一个短语,该短语后面有一个或多个数字,然后是
<SPACE>Linux Rules<SPACE>
,然后是o, k
或!
,那么副本将存储在linuxrules
文件夹中。
在处理规则之前设置和取消 VERBOSE
选项允许在日志中更详细地显示该规则,这意味着在调试时要搜索的日志文件更少。
在某些情况下,我们可能不希望菜谱处理大型消息。 在这种情况下,我们可以设置一个限制,使配方不匹配超过一定大小的消息。 如果用户使用缓慢的数据连接(可能是通过移动电话连接),那么当用户回到更好的互联网连接时,将所有大型邮件项目移到一个单独的文件夹中进行检索可能会很有用。
|条件
|
行动
|
| --- | --- |
| * > number
| 如果消息大小大于给定的字节数,将返回 true
。 |
| * < number
| 如果消息大小小于给定的字节数,将返回 true
。 |
如果运行一个外部程序来提供处理的一部分,则可能需要检查退出码,以确保该进程正确完成,或者执行次要操作来完成整个处理。
*? /unix/command/line | another/command*emphasis>
?
指示 Procmail 将当前消息数据作为标准输入传递给 Linux 命令行。 如果命令行以零退出代码退出,则成功满足条件。 虽然命令行是几个进程的管道,但返回的退出码是管道中最后一个程序的退出码。
管道打印到标准错误的任何输出都显示在日志中。
在本例中,消息体被传递给命令管道,如果在第三行中找到短语(退出代码 0),则消息将被存档到文件夹中。
在 VERBOSE=1
和 VERBOSE=0
之间的行操作将被记录,但是在这个范围之外的所有行将不被记录。 这允许我们控制发生的日志量,因此更容易跟踪日志文件活动。
VERBOSE=1
:0B:
* ? /bin/sed -n 3p | /bin/egrep "Linux Rules"
${MAILDIR}/linuxrules/
VERBOSE=0
有时,为了以某种方式继续处理,能够检查特定条件是否不存在是很有用的。 感叹号(!),或者有时被称为Bang,用来反转条件的值,使 false 变为 true,反之亦然。
* ! condition
这将测试条件中的阴性结果,如果条件不满足,则返回 true
。
在这里,我们正在寻找任何项目,没有直接发送给我们,将存储在一个文件夹,以供以后查看。
:0:
* !^TO.*cjtaylor
${MAILDIR}/not_sent_to_me/
可以使用多个 $
标志强制应用多个替换传递。
* $ condition
$
指示 Procmail 用正常的 sh
规则处理条件,在实际计算条件之前执行变量和反标记替换。 替换过程将把变量($VAR
)解析为它们的值,而不是将它们作为文字进行处理。 任何带引号的字符串的引号将被删除,所有其他 shell 元字符也将被计算。 要让这些字符通过这个替换过程,应该使用标准的反斜杠()转义机制对它们进行转义。
下面的示例取自procmailex手册页面,即使在那里它被描述为相当奇特,但它确实作为一个示例。 假设在主目录中有一个名为 .urgent
的文件,该文件中名为(一个)人,他是收到邮件的发送者。 您希望将该邮件存储在 $MAILDIR/urgent
中,而不是将其分类到任何正常的邮件文件夹中。 然后您可以这样做(注意, $HOME/.urgent
的文件长度应该远低于 $LINEBUF
; 必要时增加 LINEBUF
:
URGMATCH=`cat $HOME/.urgent`
:0:
* $^From.*${URGMATCH}
$MAILDIR/urgent/
这是执行所有处理活动的行。 在大多数情况下,这将意味着写入一个物理文件或文件夹。 但它也可以包括将邮件转发给其他用户、将数据传递给命令或命令管道,或者在某些情况下,作为复合配方的一部分执行的一系列连续操作。 如果您想执行多个操作,您不能把它们一个接一个地堆叠起来——您需要多个 recipes(可能是无条件的,和/或分组在一对大括号中)和一个冒号行(当然,还有可选的条件)。
还请注意,影响操作行的标志在实际尝试操作之前并没有真正生效。 特别是, c
标志在其所有条件都满足之前不会生成消息的克隆。
将一个用户帐户的所有消息全局转发到另一个用户帐户,Postfix 本身可以更有效地处理这个过程。 但是,如果需要应用一些逻辑来决定发送消息的内容或位置,Procmail 可以提供帮助。
大多数邮件传输将允许我们传递多个电子邮件地址以进行后续传输。
! [email protected] [email protected] user3 ...
上述操作在功能上与将消息传递给下面的管道相同:
| $SENDMAIL "$SENDMAILFLAGS"
这是转发邮件的一种特殊情况,它指示 Procmail 从原始邮件的实际头中提取收件人列表:
! -t
在这里,我们将邮件转发给我们的支持团队,而不是自己处理。 邮件的主题行包含短语支持。
:0:
* ^Subject.*support
! [email protected]
Procmail 允许对电子邮件进行无限量的操作。 使用 Procmail 的一个更强大的特性是它能够根据给定的标准将电子邮件转发给应用或脚本。 一个可能的例子是跟踪支持请求,并将条目直接存储到数据库系统中,以便在专用的应用中跟踪它们。
管道过程负责节约其产量。 菜谱的标志能够告诉 Procmail 期望一些其他的东西。 通过使用>>
语法,Procmail 可以确定要使用的锁文件。 重要的是,在写文件时始终使用锁定,以避免两个操作同时写同一个文件并损坏彼此的数据。
| cmd1 param1 | cmd2 -opt param2 >>file
可以将命令管道的输出存储在一个变量中。 这通过其本身的操作使配方成为一个非交付配方。
VAR=| cmd1 | cmd2 ...
请注意,此语法只允许在操作行中使用。 对于普通赋值的相同结果,我们可以使用反勾(')操作符。
VERBOSE=1
#Copy the data and pass the headers to the process
:0hc:
* ^Subject: Book Pipeline Example
#Copy so that the next recipe will still work
| cat - > /tmp/cjt_header.txt
#Final recipe so do not copy here, but pass the body
:0b:
| cat - > /tmp/cjt_body.txt
VERBOSE=0
这将输出保存为一个普通文件。 如果只提供一个文件名,文件将在 MAILDIR
设置中指定的目录中创建。 始终确保在写入普通文件时使用某种形式的锁定。
/path/to/filename
当保存到一个目录时,文件将被创建,目录内的文件将按顺序编号。
在路径名的末尾使用尾随(/)斜杠指示 Procmail 将项目存储在 maildir
格式的文件夹中。 子文件夹 cur, new
、 tmp
将自动创建。
directory/
使用 /
。 在路径名称的末尾指示 Procmail 将项目存储在一个 MH
格式化的文件夹中。
directory/.
如果我们想将数据存储到几个 MH
或 maildir
文件夹中,我们可以同时列出它们。 结果将是只有一个文件将实际写入,其余的将被创建为硬链接。
如果我们想要在一个匹配的项目上执行许多条件处理或操作,那么我们可以使用 {
和 }
字符指定要使用的食谱块,而不是单个操作行。 在 {
之后和 }
之前必须至少有一个空格。
{
# ... more recipes
}
大括号之间的代码可以是任何有效的 Procmail 构造。
请注意,赋值变量的操作总是必须放在一组大括号内: { VAR=value }
。 只使用不带括号的 VAR=value
将导致数据被保存到名为 VAR=value
的文件夹中。
如果我们想要一个配方,不做任何处理,也许作为一个 if…else
操作的一部分,我们可以用一个空的 { }
,但有关空格仍然适用的规则,我们需要确保至少有一个空格字符之间的两个括号。
以下示例采用前一个示例,并对其进行了轻微修改,以便只执行一个测试,然后在测试通过时运行一系列无条件测试:
VERBOSE=1
:0:
* ^Subject: Book Pipeline Example
{
#Copy so that the next recipe will still work
:0hc:
| cat - > /tmp/cjt_header.txt
#Final recipe so do not copy here
:0b:
| cat - > /tmp/cjt_body.txt
}
VERBOSE=0
Procmail 实现了一种形式的正则表达式,其操作方式与其他 UNIX 实用程序略有不同。 在这里,我们将介绍基本的区别,并引导新用户了解正则表达式的强大世界、它们的含义、实现和用法。
我们已经看到 Procmail 匹配是不区分大小写的,除非使用了 D
标志。 对于正则表达式也是如此。 Procmail 默认情况下也使用多行匹配。
熟悉 Linux 和编程世界的新用户可能不知道正则表达式为处理数据带来的强大特性。 在最简单的形式中,正则表达式可以理解为在数据体的任何地方搜索短语或模式。 下面的简单示例演示如何匹配所有邮件项,其中邮件头和/或正文包含短语 mystical monsters
,并将邮件放入相关文件夹中。
:0 HB:
* mystical monsters
${MAILDIR}/monsters/
但是,该筛选器将不匹配包含短语 mystical monster
或 mystical-monsters
的项。 因此,正则表达式的真正威力体现在能够以简化格式描述文本或数据模式,然后在数据体中搜索与这些模式的匹配。 但是,你应该小心,不要被单词简化了所误导。 在现实生活中遇到的大多数正则表达式,如果以原生格式编写,那么读起来可能一点也不简单。 以下面的例子为例,它的目的是确定一个邮件项目是否是 MIME 编码的,并将其存储在一个合适的文件夹中:
:0:
* ^Content-Type: multipart/[^;]+;[ ]*boundary="?\/[^"]+
${MAILDIR}/mime/
字符 ., [, ^, ;, ], +, ?, \, /
和"
是特殊指令,而不是它们通常描述的字面 ASCII 字符。 为了理解这些字符及其含义,我们将快速浏览最重要的例子。
这是最简单、最常见的正则表达式形式,只是表示匹配任何单个字符(不包括换行符,它被认为是一种特殊情况)。 考虑以下表达式:
:0
* Dragons ... mystical monsters
${MAILDIR}/result/
这将匹配以下任何一个短语:
Dragons are mystical monsters
Dragons and mystical monsters
Dragons but mystical monsters
实际上,它将匹配带有 Dragons
和 mystical
之间的三个字符单词的任何短语。 如果我们想要匹配在 Dragons
和 mystical
之间包含三个或三个以上字符的任何长度的单词,我们可以使用 ?
或量词操作。
如果我们想匹配一个字面值'。 ' or more than one '。 ',我们可以转义任何对正则表达式字符串有特殊意义的字符,通过在其前面加一个反斜杠'',使''。 '将会匹配 a '。 '(句号)和'\'将匹配''(反斜杠)字符。
问号表示前一个字符应该匹配 0 次或只匹配一次。 因此,以下代码行将满足我们的需求:
:0
* Dragons ....? Mystical monsters
${MAILDIR}/result/
这个表达式可以被理解为:“匹配任何由三个或更多字符组成的单词,后面不跟任何字符或任何一个字符”。
在 ?
之前的字符也可能是一个简单的 ASCII 字符,在这种情况下表达式将匹配如下:
:0
* Dragons ..d? Mystical monsters
${MAILDIR}/result/
这可以被理解为“任何两个字符后面跟着一个字母 d.
或什么都没有”,因此这将匹配 an
和 and
,而不是 are.
星号修饰符的工作方式类似于量词操作符,但表示匹配 0 个或多个前一个字符,当然,换行符除外。 .*
是一个非常常见的序列,你会在大量的食谱中发现它。
下面的示例将匹配所有包含单词 choose
后跟一些其他单词后跟单词 online:
的消息
:0
* ^Subject: Choose.*online
${MAILDIR}/result/
Subject: Choose discount pharmacy and expedite the service online.
Subject: Choose hassle free online shopping
Subject: Choose reliable online shopping site for reliable service and quality meds
Subject: Choose reliable service provider and save more online.
Subject: Choose the supplier for more hot offers online
Subject: Choose to shop online and choose to save
下一个例子将查找“anything”(.)后面跟着两个或更多的感叹号(!!)和(!):
:0
* ^Subject: .*!!!*
${MAILDIR}/result/
Subject: Breathtaking New Year sale on now!!! Get ready for it!! Subject: Hey Ya!! New Year Sale on right now!! Subject: It Doesn't Matter!!
加号与 *
非常相似,只是它要求正则表达式中必须至少有一个字符的实例位于 +
之前。
如果我们考虑前面的例子,下一个例子将查找“anything” .*
后跟两个 !!
和至少一个(!+)感叹号。
:0
* ^Subject: .*!!!+
${MAILDIR}/result/
这将给我们一个更受限的输出,即一行中至少需要三个 !
。
Subject: Breathtaking New Year sale on now!!! Get ready for it!!
到目前为止,我们能够创建的匹配模式非常强大,但工作方式相当不集中。 例如,我们可以很容易地编写一个规则来查找以 t
结尾的任何三个字母的单词,但不能将匹配限制为仅以 t
结尾的给定单词集。 为了克服这个问题,我们可以将.
或单个字符替换为列表中的一组字符或一组字符,然后应用量词操作来准确地说明可以应用这些字符的次数。
通过仔细使用圆括号 ( )
,我们可以创建将在模式匹配规则中使用的字符串组。 例如,让我们假设我们正在尝试分割由系统脚本频繁发送的电子邮件。 脚本将主题行格式化为在主题行中包含下列短语之一。
There is only one problem
There are 10 problems
下面的正则表达式将匹配我们正在寻找的特定字符串,方法是匹配在 there
和 problem
之间有一个或多个短语 is only one
出现的任何字符串。
There (is only one)+ problem
如果我们想过滤单词或短语列表,我们将需要使用交替特性。
There (is only one|are)+ problem
字符将用于匹配模式的单词列表分隔开来。
下面的简单垃圾邮件过滤器使用交替特性来搜索文本替换,以避免使用简单的基于单词的过滤器。
随着我们每天收到的垃圾邮件数量的不断增长,我相信到目前为止你们中的一些人已经意识到我们可以开始过滤一些我们每天收到的常规邮件。 有许多特定的垃圾邮件过滤器被设计成与 Procmail 紧密合作,并为垃圾邮件过滤提供更大的测试集和覆盖范围。 其中一个应用 SpamAssassin 将在第 8 章中介绍。
以在线赌场为例,这是垃圾邮件发送者的热门话题,他们鼓励我们探索它们。 这是我们通常不感兴趣的内容,所以我们很乐意将所有包含“在线”和“赌场”字样的信息过滤到单独的文件夹中。
Subject: Online Casino
垃圾邮件发送者面临的挑战之一是编写我们可以阅读的主题行,而垃圾邮件过滤器却难以处理。 这样做的一个简单的方法是用常见的键入字符如零 O
(0
)字母或字母 o, 1
L
或 l
, A
和 4
或 a
。 【显示】
所以我们可以把规则写成:
Subject: (o|0)n(1|l)ine casin(o|0)
接下来展示的是这个配方的最后一次迭代,我们特别寻找包含单词“online”和“casino”的主题行,但包括单词可能以不同顺序出现的场合,每个单词都分别测试。
:0
* ^Subject: (o|0)n(1|l)ine
* ^Subject: casin(o|0)
${MAILDIR}/_maybespam/
虽然这工作很好,是没有有效的规则,以这种方式工作,像这种替换是一种常见的正则表达式,要求有一种特殊的方式表达这些术语的字符类。
方括号 [ ]
中包含的任何字符序列都表示要在表达式中检查列出的每个字符。 对于常见的字符序列,如字母表中的字母或一组数字,可以使用 [a-z]
或 [0-9]:
[a-e]
表示匹配所有的字母a, b, c, d, e
(包括 T1)。[1,3,5-9]
表示匹配1, 3, 5, 6, 7, 8
或9
中的任意一个数字。
下面的示例将查找在文本字符串中嵌入数字 0
和 1
的消息,使它们看起来像 O
、 L
或 I
。
:0
* ^Subject: [a-z]*[01]+[a-z]*
${MAILDIR}/_maybespam
Subject: Hot Shot St0ckInfo VCSC loadstone Subject: M1CR0S0FT, SYMANNTEC, MACR0MEDIA, PC GAMES FROM $20 EACH Subject: R0LEX Replica - make your first impressions count! Subject: Small-Cap DTOI St0cks reimburse Subject: TimelySt0ck DTOI Buy of the Week evasive
如果我们想匹配所有大范围的字符而不是匹配一小部分字符,那么使用 ^
字符指定负数匹配更容易。
[^0-9]
这意味着匹配任何以不在 0
和 9
之间的数字开头的字符串。
当我们知道模式应该从行开始时,给我们正在搜索的模式添加一个行锚开始是很有用的。 例如,所有标题必须从行首开始,因此搜索以下短语:
Subject: any subject message
也可以匹配以短语开头的标题,例如:
Old-Subject:
要停止此操作,可以添加Line Anchor 的起始字符(^),并将正则表达式更改为:
^Subject: any subject message
当我们正在计划匹配字符串,我们知道我们应该终止,我们可以添加锚行结束字符, $
,我们匹配的模式,以确保正确的字符串如下:
^Subject:.* now$
这将匹配任何以单词 now
结尾的主题行。
正则表达式是一个庞大的主题,但是非常值得学习,因为它们被大量的 Linux 工具和应用所使用。 网上有许多与正则表达式相关的资源。 以下是一些入门链接:
正如我们在前一章中简要介绍的,Procmail 有许多有用的“预先准备的”正则表达式或宏,它们提供一系列在 Procmail 菜谱中常用的匹配。
^TO
是最初用于处理“To”地址的 Procmail 宏。 这已经被 Procmail 3.11pre4 版本中引入的更新的 ^TO_
宏所取代。
这个集合包括大多数可以包含您的地址的头文件,例如 To:, Apparently-To:, Cc:, Resent-To:
,等等。
在大多数情况下,你应该使用 ^TO_
选项,因为它有更好的覆盖范围。
虽然用一个类似的宏来覆盖源地址细节似乎是合乎逻辑的,但请注意,对应的^FROM
或^FROM_
宏没有。
下面是来自 Procmail 源代码的正则表达式字符串:
"(^((Original-)?(Resent-)?(To|Cc|Bcc)|\
(X-Envelope|Apparently(-Resent)?)-To):(.*[^-a-zA-Z0-9_.])?)"
这个宏可以识别广泛的邮件生成程序,是一个有用的集合。 然而,新程序一直在创建,所以几乎总是需要额外的过滤器。
Procmail 将这个简短的宏扩展为以下正则表达式,这些正则表达式取自 Procmail 源代码。
"(^(Mailing-List:|Precedence:.*(junk|bulk|list)|\
To: Multiple recipients of |\
(((Resent-)?(From|Sender)|X-Envelope-From):|>?From )([^>]*[^(.%@a-z0-9])?(\
Post(ma?(st(e?r)?|n)|office)|(send)?Mail(er)?|daemon|m(mdf|ajordomo)|n?uucp|\
LIST(SERV|proc)|NETSERV|o(wner|ps)|r(e(quest|sponse)|oot)|b(ounce|bs\\.smtp)|\
echo|mirror|s(erv(ices?|er)|mtp(error)?|ystem)|\
A(dmin(istrator)?|MMGR|utoanswer)\
)(([^).!:a-z0-9][-_a-z0-9]*)?[%@> ][^<)]*(\\(.*\\).*)?)?$([^>]|$)))"
它采用了与 ^FROM_MAILER
类似的方法,但目的是识别来自更常见的 Linux 守护进程和系统进程的消息。
来自 Procmail 源代码的正则表达式字符串如下所示:
"(^(((Resent-)?(From|Sender)|X-Envelope-From):|\
>?From )([^>]*[^(.%@a-z0-9])?(\
Post(ma(st(er)?|n)|office)|(send)?Mail(er)?|daemon|mmdf|n?uucp|ops|\
r(esponse|oot)|(bbs\\.)?smtp(error)?|s(erv(ices?|er)|ystem)|A(dmin(istrator)?|\
MMGR)\
)(([^).!:a-z0-9][-_a-z0-9]*)?[%@> ][^<)]*(\\(.*\\).*)?)?$([^>]|$))"
下面的示例将在一个文件夹中存储接收到的守护进程消息,该文件夹将年和月作为路径的一部分。 前面在 Procmail 文件中分配了这些变量 ${YY}
和 ${MM}
,并且还创建了必要的目录。
:0:
* ^FROM_DAEMON
${YY}/${MM}/daemon
在这里,我们将把 Procmail 功能的各种项组合成几个有用的配方,这些配方可以作为我们自己组织中的工具的基础。 第一个示例基于传统的 Vacation
配方,该配方通知发送方收件人可能在一段时间内无法读取电子邮件。 第二部分展示了如何创建基于处理日期和可能的时间自动归档消息的支持。 最后,我们将完成上一章中开始的规则,该规则通知用户已过滤到单独文件夹中的大型邮件项。
这个例子是基于 man procmailex
中给出的假期的例子,并在本章前面简要地提到过。
正如我们已经讨论过的,盲目和自动地回复电子邮件是一个非常糟糕的主意,而且会产生严重的后果。 首先我们必须决定是否发送自动回复。 要做到这一点,我们需要确保条件有意义并得到满足。 如果是这样,当前消息的报头(用 h
标志表示)将被提供给 formail
,这是 Procmail 实用程序套件的一部分。 formail
然后检查 vacation.cache
文件,以确定发送者是否已经收到自动回复。 这是为了确保我们没有向一个用户发送多个报告。 在进行这部分处理时,我们的配方将创建一个锁 vacation.lock
。
这样做的主要原因是为了避免在更新缓存时发生冲突,因为冲突可能导致缓存信息的损坏。
这个食谱实际上包括两个单独的食谱。 第一种方法提供了对回复的检查和记录,以确保我们不会发送重复或重复的回复。
这个食谱 W
,等待 formail
的返回。 如果没有 c
,Procmail 将在完成该配方后停止处理,因为它是一个交付配方。 它将头发送到 formail
。
TO_
和 ^FROM_DAEMON
条件比我们看到的更多。
如果用户的登录名出现在任何收件人头中To:, Cc:, Bcc:,则满足TO_ $<logname>
。 这就避免了对发送到别名或邮件列表但未显式发送到用户的消息发送自动回复。
!^FROM_DAEMON
确保我们不会自动回复来自各种各样的守护进程的消息。
!^X-Loop: $RECIPIENT
不回复自动回复; 注意,这个 X-Loop
头被插入到我们发送的自动回复中。
:0 Whc: vacation.lock
# Perform a quick check to see if the mail was addressed to us
* $^To_:.*\<$\LOGNAME\>
# Don't reply to daemons and mailinglists
* !^FROM_DAEMON
# Mail loops are evil
* !^X-Loop: $RECIPIENT
| formail -rD 8192 vacation.cache
如果第一部分没有在缓存中找到匹配,就会执行第二部分。 这个地址可能没有被找到有两个原因——要么它从来没有被看到过,所以没有发送回复,要么它在很久以前被看到过,以至于条目被强制从缓存中删除。 在这两种情况下,都将发送休假消息的副本。 发送者永远不会收到他们发送的每条信息的自动回复——这真的会让一个多产的邮件作者心烦意乱。
:0 ehc
# if the name was not in the cache
| (
formail -rA"Precedence: junk" \
-A"X-Loop: $RECIPIENT" ; \
cat $HOME/.vacation_message \
) | $SENDMAIL -oi -t
由于 e
的原因,如果前一个配方返回错误状态,则执行前一个配方。 在这种情况下,它并不是一个真正的错误,它只是来自 formail
的信号,地址在缓存文件中不存在,我们可以继续进行自动回复。 请注意,如果在前面的配方中没有满足导致 formail
缓存检查被跳过的条件,Procmail 会非常聪明地跳过这个配方。
为了构造自动回复的标题,当前消息的标题被提供给这个配方中的 formail
。
此配方中的 c
将导致在此配方之后处理整个当前消息。 通常,这意味着它将被处理,没有进一步的食谱,这就是我们如何在我们的邮箱中获得一个副本。 在执行此菜谱时不需要锁,因此不需要使用锁。
要向原始消息的发送方发送回该消息,所需要的只是该消息的一个副本,该副本保存在用户的主目录中的文件 .vacation_message
中。
将消息信息存储在 Procmail recipe 之外,可以让您的系统用户轻松地更新他们发送的消息,而不会有破坏实际 recipe 本身的风险。
你可能不想删除你觉得有一天可能有用的邮件。 这很容易导致千兆字节的数据被存储在不同的位置。 我们可以根据年份、月份和主题的组合将部分或所有收到的邮件过滤到文件夹中,以便能够轻松地跟踪它们。
应用于每个邮件进程的通用规则确保存在必要的目录结构。
#Assign the name of the folder by extracting the year and month
# parts from the external date command.
MONTHFOLDER=`date +%y/%m`
#Unconditional rule to create the folder. Using the test
#command. we create the monthly folder if it does not exist.
:0 ic
* ? test ! -d ${MONTHFOLDER}
| mkdir -p ${MONTHFOLDER}
#Alternative way of creating the folder using an assignment operation
DUMMY=`test -d $MONTHFOLDER || mkdir $MONTHFOLDER`
#Now store any email matching 'meeting' in an appropriate folder
:0:
* meeting
${MONTHFOLDER}/meeting/
如果你希望对输出格式或位置有更多的控制,你可以使用以下规则:
#This obtains the date formatted as YYYY MM DD, e.g. 2009 09 08 date = `date "+%Y %m %d"`
#Now assign the Year YYYY style :0 * date ?? ^^()\/ { YYYY = $MATCH }
#Now assign the Year YY style :0 * date ?? ^^..\/ { YY = $MATCH }
#Now assign the Month MM style :0 * date ?? ^^.....\/ { MM = $MATCH }
#Now assign the Day DD style :0 * date ?? ()\/..^^ { DD = $MATCH }
#Create the various directory formats you are going to use
DUMMY=`test -d ${YYYY}/${MM}/${DD} || mkdir -p ${YYYY}/${MM}/${DD}`
DUMMY=`test -d ${YY}/${MM} || mkdir -p ${YY}/${MM}`
#Now store the data in an appropriate folder using the variables
#YYYY, MM and DD setup above.
:0:
* ^FROM_DAEMON
${YYYY}/${MM}/${DD}/daemon/
在前一章中,我们介绍了一个非常简单的规则,该规则将所有大小超过 100 KB 的传入邮件存储在 largemail
文件夹中。 这对于防止单个传入邮件文件夹的大小变得过大很有用,但这意味着必须定期进行特殊检查,以查看是否有邮件被过滤。
在此规则中,我们现在将提取标题和主题行,以及原始大型电子邮件的前几行,并创建一个带有修改过的主题行的新消息。 在将大型原始项目过滤到其单独的 largemail
文件夹的同时,此修改后的邮件将存储在用户的收件箱中。
只有当消息的大小超过 100,000 字节时,测试的主要部分才会被应用,所以我们需要一个类似以下配方的结构来进行初始测试,并决定这是否是一个大项目:
:0:
* >100000
{
MAIN PROCESS WILL GO HERE
}
假设我们有一个较大的条目,我们需要使用 c
标志复制该消息,并将该副本存储在 largemail
文件夹中:
#Place a copy in the largemail folder
:0 c:
largemail/
接下来提取消息体的第一部分,这可以使用多种选项来完成。 在本例中,我们将通过等待只将消息体传递给系统 head 命令并告诉它只返回前 1024 个字节的结果来剥离消息的前 1024 个字节。 这里使用的标志告诉 Procmail 等待命令行进程的结果,并忽略任何管道错误,因为 head 命令将只读取提供给它的部分数据。
#Strip the body to 1kb
:0 bfwi
| /usr/bin/head -c1024
现在我们需要重写主题行,这是使用 formail
程序完成的。 这一次,我们只将头传递给命令行并等待响应。
在本例中,我们需要获取当前的主题行,以便将其作为修改主题行的一部分传递给 formail
程序。 我们通过对主题内容进行简单匹配,然后将 $MATCH
变量传递给 formail
程序,该变量现在将主题行内容作为参数保存。 为了简洁起见,我们在原始主题行之前添加 {* -BIG- *}
措辞,以便于对这些消息进行分类和识别。
#ReWrite the subject line
:0 fhw
* ^Subject:\/.*
| formail -I "Subject: {* -BIG- *} $MATCH"
消息的正常传递将发生,新的短消息将存储在收件箱中。
如果我们把所有这些放在一起,我们最终会得到以下完整的食谱。
:0: * >100000
{
#Place a copy in the largemail folder
:0 c: largemail/
#Strip the body to 1kb :0 bfwi | /usr/bin/head -c1024 #ReWrite the subject line :0 fhw * ^Subject:\/.* | formail -I "Subject: {* -BIG- *} $MATCH" }
作为避免重复发明轮子的社区努力的一部分,Procmail 模块库提供了由 Procmail 用户贡献的有用的食谱集合。 下面来自 Procmail 模块库http://freshmeat.net/projects/procmail-lib的介绍将该包描述为:
Procmail 模块库是 Procmail 邮件处理实用程序的许多插件模块的集合。 这些模块允许执行一些常见的任务,比如解析日期、时间、MIME 和电子邮件地址、转发邮件、处理 POP3、屏蔽垃圾邮件、运行电子邮件 cron 作业、处理守护消息等等。
每个模块,或 Procmail 包含的文件,都有完整的文档,并展示了示例用法。 它们可以作为提供的,具有各种可配置选项或作为您自己的菜谱的基础使用。 我们在本章中介绍的许多技术以及一些更复杂的基于消息内容类型的过滤方法都在库中使用。
在本章中,我们已经涵盖了广泛的主题,现在我们可以把这些主题集中起来。 下面的例子使用了本章中展示的每一种技术,并且通常用于电子邮件处理。 我希望它对您创建自己的邮件过滤策略有用。
对 Procmail 规则和配置的相关方面进行分组将使您的安装更容易维护,并且在进行更改时不太可能产生问题。
在主 Procmail 目录中,按照一致的命名约定创建单独的文件,比如 rc.main, rc.spam, rc.lists
,等等。 然后将它们分别包含到您的主 .procmailrc
文件中,如下所示。
#This obtains the date formatted as YYYY MM DD date = `date "+%Y %m %d"`
#Now assign the Year YYYY style :0 * date ?? ^^()\/ { YYYY = $MATCH }
#Now assign the Year YY style :0 * date ?? ^^..\/ { YY = $MATCH }
#Now assign the Month MM style :0 * date ?? ^^.....\/ { MM = $MATCH }
#Now assign the Day DD style :0 * date ?? ()\/..^^ { DD = $MATCH }
#Create the various directory formats you are going to use
DUMMY=`test -d ${YYYY}/${MM}/${DD} || mkdir -p ${YYYY}/${MM}/${DD}`
DUMMY=`test -d ${YY}/${MM} || mkdir -p ${YY}/${MM}`
#Make a backup copy of all incoming mail
:0 c
backup/
#Restrict the history to just 32 mail items
:0 ic
| cd backup && rm -f dummy `ls -t msg.* | sed -e 1,32d`
#Make sure that all mails have a valid From value
:0 fhw
| formail -I "From " -a "From "
#
## Don't include this unless we need to
## INCLUDERC=${HOME}/Procmail/rc.testing
##
## Now include the various process listings
INCLUDERC=${HOME}/Procmail/rc.system
INCLUDERC=${HOME}/Procmail/rc.lists
INCLUDERC=${HOME}/Procmail/rc.killspam
INCLUDERC=${HOME}/Procmail/rc.vacation
INCLUDERC=${HOME}/Procmail/rc.largefiles
INCLUDERC=${HOME}/Procmail/rc.virusfilter
INCLUDERC=${HOME}/Procmail/rc.spamfilter
现在,对于列出的每个 include
文件,按名称创建文件,并在该文件中包含与容器相关的规则。 然后,这就变成了对临时隔离接收邮件的处理部分的 INCLUDERC
引用进行评论的问题。 注意不要盲目地剪切和粘贴这些示例,而不检查每个配方是否按预期执行,特别是在生产环境中。
在一个过时的文件夹结构中的文件信息系统和守护进程消息可以给出如下:
# Filter system mail messages into a dated folder structure.
# The variables YY and MM are defined in the calling recipe
# and each of the directories will have been created if necessary.
:0:
* ^From:.*[email protected]
${YY}/${MM}/daemon/
:0:
* ^From:.*[email protected]
${YY}/${MM}/daemon/
:0:
* ^[email protected]
${YY}/${MM}/daemon/
:0:
* ^From:.*[email protected]
${YY}/${MM}/daemon/
:0:
* ^From:.*[email protected]
${YY}/${MM}/daemon/
将我们订阅的所有邮件列表保存在有日期的文件夹中,以便以后阅读。
# Mailing lists
# Store by date folder
# The variables DD and MM are defined in the calling recipe.
# and each of the directories will have been created if necessary.
:0:
* ^From:.*[email protected]
${YY}/${MM}/mapserver/
:0:
* ^[email protected]
${YY}/${MM}/mapserver/
:0:
* ^From:.*[email protected]
${YY}/${MM}/jobs/
:0:
* ^Subject: silicon Jobs-by-Email Alert
${YY}/${MM}/jobs/
:0:
* ^Reply-To: Axandra Search Engine Facts <[email protected]>
${YY}/${MM}/lists/
:0:
* ^Subject: A Joke A Day
${YY}/${MM}/lists/
:0:
* ^List-Owner: <mailto:[email protected]>
${YY}/${MM}/lists/
:0:
* ^Reply-To: [email protected]
${YY}/${MM}/lists/
:0:
* ^Subject: Developer Shed Weekly Update
${YY}/${MM}/lists/
删除任何来自发件人的邮件,符合我们的杀死文件中的地址。
#Kill file for known spammers
# If the sender is in the killfile then discard the mail into the bit bucket
# Here we use the external command 'grep' to search our killfile for a
# matching sending sending by testing the return status from grep.
:0:
* ? grep -i `formail -rtzxTo:` $HOME/.killfile
/dev/null
我们的假期自动回复食谱:
#Vacation Replies
:0 Whc: vacation.lock
# Perform a quick check to see if the mail was addressed to us
* $^To_:.*\<$\LOGNAME\>
# Don't reply to daemons and mailinglists
* !^FROM_DAEMON
# Mail loops are evil
* !^X-Loop: $RECIPIENT
| formail -rD 8192 vacation.cache
:0 ehc
# if the name was not in the cache reply with the contents
# of our vacation message in the body of the email.
| (
formail -rA"Precedence: junk" \
-A"X-Loop: $RECIPIENT" ; \
cat $HOME/.vacation_message \
) | $SENDMAIL -oi -t
为了避免大邮件堵塞我们的收件箱,我们将大邮件归档到一个文件夹中,并向自己发送一个通知,告知我们收到了一个超大的邮件。
#Assume that files larger than 100k are not spam
:0: * >100000
{
#Place a copy in the largemail folder
:0 c: largemail/
#Strip the body to 1kb :0 bfwi | /usr/bin/head -c1024
#ReWrite the subject line :0 fhw * ^Subject:\/.* | formail -I "Subject: {* -BIG- *} $MATCH" }
任何带有表明邮件为病毒的电子邮件标题的文件都放在文件夹中。
#Virus Filter
#X-Virus-Status: Infected
:0:
* ^X-Virus-Status: Infected
_virus/
任何带有表明邮件为垃圾邮件的电子邮件标题的文件都放在文件夹中。
#Spam Filter
:0fw
* < 256000
| spamc
# Mails with a score of 15 or higher are almost certainly
# spam (with 0.05% false positives according to
# rules/STATISTICS.txt). Let's put them in a
# different mbox. (This one is optional.)
#
# The regular expression below matches the SpamAssassin
# header with 15 asterisks or more.
#
:0:
* ^X-Spam-Level: \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
_almost-certainly-spam/
# All mail tagged as spam (eg. with a score higher than the
# set threshold)
is moved to "probably-spam".
:0:
* ^X-Spam-Status: Yes
_probably-spam/
在本章中,我们探索了 Procmail,发现了大量的服务和功能,它们可以帮助我们控制邮件。 使用 Procmail 的高级功能,我们发现:
- 传递和非传递食谱之间的区别
- 如何订购每一道菜以避免延迟交货时间
- 使用 Procmail 变量和条件标志来控制传递
- 使用正则表达式进行复杂的模式匹配
- 大量可用的 Procmail 宏及其使用
- 最后,还有一些有效管理邮件的食谱示例
虽然我们已经介绍了很多内容,但仍有很多内容需要学习,并且 Web 上有大量资源专门用于这个特定的应用。
希望您现在已经掌握了 Procmail 的核心功能、如何实现它,以及如何探索您的实际需求,并创建您可以组合起来创建自己独特的邮件过滤策略的食谱集。