Maildrop tips and tricks

Last update: 10/11/1998.

The include statement
The $LINES and $SIZE environment
Partial matches using the ! operator
Short-circuit evaluation using logical operators
The foreach statement
The $HOME/.tmp directory


Here's a list of tips and tricks to write efficient mail filters using maildrop. Maildrop - just like any programming language - can do certain things better than others.
 

The include statement

When maildrop is started, it reads $HOME/.mailfilter, or some other file containing mail filtering instructions. Before doing anything else, this entire file is read and semi-compiled. Any grammatical or syntax errors will result in maildrop terminating with a temporary error code, without doing anything with the message.

The entire file must be 'semi-compiled". The 'semi-compilation' process consists of building an logical representation of the filtering statements in memory.

If you have a long and complicated set of instructions, it will be semi-compiled whether or not it will actually be executed. For example, consider the following code:
 

if ( /^Subject: rosebud/ )
{
    ...
    some
    really
    long
    set
    of
    instructions
    ...
}
If the contents of the "if" construct are large, maildrop may spend considerable amount of time and memory semi-compiling filtering instructions that may never be used. Consider putting all these instructions into a separate file, and using the include statement to execute them.

The include statement is processed only when it is actually executed by maildrop. That carries certain side effects. One feature of maildrop is the semi-compilation which weeds out errors in the filtering recipe file, holding all mail and not doing anything until the errors are fixed. The include statement is not processed until it is actually executed. If there are errors in the file referenced by the include statement, maildrop will not know about them until it executes the include statement. When maildrop detects the error, it will terminate with a temporary error code, holding all mail. However, any filtering instructions before the include statement would've already been executed.

The $LINES and $SIZE environment variables.

These variables are initialized to the number of lines in the message, and the size of the message in bytes. They may not be exact, due to the presence of the From_ line, or -A arguments on the command line, but they will always be a reasonable approximation.

Scanning headers for patterns will take the same amount of time whether the body of the message has 100 or 1,000 lines. However, the "b" option causes the message body to be searched. If someone sends you a 5 megabyte binary file, each pattern matched against the message body will take a significant time to complete.

Whenever possible, the first thing you should do is check both variables to see if the message  is excessively large. If so, divert the message to a separate mailbox, or reject it.

Weighted scoring is especially sensitive to the size of the message being delivered. Normally, as soon as a pattern match is found, maildrop terminates the pattern scan. If weighted scoring is used then the pattern scan is immediately restarted, until the end of the message is reached.

A fixed overhead is required to start a pattern scan going, so that weighted scoring that matches a very short pattern, like /[:lower:]/:Dbw,1,1 will take a significant amount of CPU time to complete. Maildrop is simply not optimized for these kinds of situations.

Nevertheless, on a Pentium 200, /[:lower:]/:Dbw,1,1 perform reasonably well for messages less than 60K long.

Here's an example of using $LINES and $SIZE to divert large messages to a separate mailbox:

 
    if ($LINES > 1000 || $SIZE > 60000)
    {
        to "mail/IN.large"
    }

    .
    .
    .

Partial matches using the ! operator

Maildrop implements the ! operator in patterns by breaking up the pattern into two separate patterns, which are compiled and executed separately.

Whenever the first pattern is succesfully matched, maildrop runs the second pattern. If the second pattern does not match, maildrop resumes matching the first pattern until it matches again.

Since starting a pattern match involves some fixed overhead, minimizing the number of times the first pattern will match will reduce the amount of time it takes to match the entire pattern.

For example: you're trying to extract an IP address from the first Received header. Here's your first attempt at doing so:

 
    /^Received:.*![0-9\.]+/
    IP=$MATCH2
This will not work at all. From the manual page for maildropfilter:
 
       When  there  is  more  than  one  way  to  match a string,
       maildrop favors matching as much  as  possible  initially.


So this fragment will set IP to the last digit, or period found in the first Received: header, because the first half of the pattern will match as much as possible.

Here's your second attempt, then:

 
    /^Received:.*[^0-9\.]![0-9\.]+/
    IP=$MATCH2
This will work, except the first half of the pattern will end up matching every character in the Received: header that's not a digit or a period. Maildrop will then stop and try to run the second half of the pattern.

Your mail server will probably put a left bracket before the IP address of the relay. Noting that, the following code picks up the IP address as efficiently as possible:

 
    /^Received:.*\[![0-9\.]+/
    IP=$MATCH2

Short-circuit evaluation using logical operators

The logical operators || and && work in maildrop just like they work in C. Consider the following expression:
 
    if (/ ... some pattern ... / || / ... some other pattern ... /)
    {
        .
        .
        .
    }
If the first pattern is found, maildrop will not execute the second pattern, since the logical expression will be true no matter what. You can use that to your advantage. If you have two patterns, and one of them takes more time to process, put the other first. Even if both patterns are relatively quick, if you expect one of them to be found almost every time, put that one first so that it will not be necessary to run the second pattern most of the time.

The same concept works for the && operator:

 
    if (/ ... some pattern ... / && / ... some other pattern ... /)
    {
        .
        .
        .
    }
If the first pattern is not found, maildrop will not execute the second pattern, since the logical expression will be false no matter what.

The foreach statement

The foreach statement is implemented as follows. Maildrop will execute the regular expression, and compile a list of all the patterns in the message matched by the regular expression. Afterwards, maildrop will execute the foreach statement, once for each matched pattern.

It is important to note that maildrop will create a list in memory containing every pattern matched by the regular expression. if the regular expression is found many times in the message, this will require a lot of memory. For example, the following is a Very Bad Thing[tm]:

 
    foreach /[:alpha:]/
    {
        .
        .
        .
    }
Maildrop does not include any built-in limits on resource usage, except for the watchdog timer. Therefore, as a system administrator, it is your responsibility to limit resources used by the maildrop process.

This implementation of the foreach statement is the one that yields the least number of surprises. Observe that the contents of the foreach construct can modify the message using the xfilter statement. Furthermore, the regular expression may include variable substitution, and those variables could be modified by the foreach statement. If the foreach statement were to be executed "on the fly", these factors would yield unpredictable - and rather messy - results.

The $HOME/.tmp directory

Under certain conditions, maildrop will need to use a temporary file. Temporary files are created in the $HOME/.tmp directory. Temporary files are used to store large messages (instead of storing them in memory). Temporary files may also be used by the xfilter command, or in other situations.

Maildrop will automatically delete all temporary files before terminating. However, if the maildrop process is killed, or terminates abnormally, a temporary file may remain and take up disk space. To have those leftover files automatically cleaned up, put the following code in /etc/profile, or some other file that all users run after logging in:

 
    find $HOME/.tmp -depth -mtime +7 \( \( \
        -type d -exec rmdir {} \; \) -o -exec rm -f {} \; \) 2>/dev/null