Header matching

Mailman can do pattern based header matching during its normal rule processing. There is a set of site-wide default header matches specified in the configuration file under the [antispam] section.

>>> from mailman.app.lifecycle import create_list
>>> mlist = create_list('test@example.com')

In this section, the variable header_checks contains a list of the headers to check, and the patterns to check them against. By default, this list is empty.

It is also possible to programmatically extend these header checks. Here, we’ll extend the checks with a pattern that matches 4 or more stars.

>>> from mailman.config import config
>>> chain = config.chains['header-match']
>>> chain.extend('x-spam-score', '[*]{4,}')

First, if the message has no X-Spam-Score: header, the message passes through the chain with no matches.

>>> from mailman.testing.helpers import (specialized_message_from_string
...   as message_from_string)
>>> msg = message_from_string("""\
... From: aperson@example.com
... To: test@example.com
... Subject: Not spam
... Message-ID: <ant>
...
... This is a message.
... """)

Helper Function to help with printing rule hits and misses.

>>> def hits_and_misses(msgdata):
...     hits = msgdata.get('rule_hits', [])
...     if len(hits) == 0:
...         print('No rules hit')
...     else:
...         print('Rule hits:')
...         for rule_name in hits:
...             rule = config.rules[rule_name]
...             print('    {}: {}'.format(rule.header, rule.pattern))
...     misses = msgdata.get('rule_misses', [])
...     if len(misses) == 0:
...         print('No rules missed')
...     else:
...         print('Rule misses:')
...         for rule_name in misses:
...             rule = config.rules[rule_name]
...             print('    {}: {}'.format(rule.header, rule.pattern))

By looking at the message metadata after chain processing, we can see that none of the rules matched.

>>> from mailman.core.chains import process
>>> msgdata = {}
>>> process(mlist, msg, msgdata, 'header-match')
>>> hits_and_misses(msgdata)
No rules hit
Rule misses:
    x-spam-score: [*]{4,}

The header may exist but does not match the pattern.

>>> msg['X-Spam-Score'] = '***'
>>> msgdata = {}
>>> process(mlist, msg, msgdata, 'header-match')
>>> hits_and_misses(msgdata)
No rules hit
Rule misses:
    x-spam-score: [*]{4,}

The header may exist and match the pattern. By default, when the header matches, it gets held for moderator approval.

>>> from mailman.interfaces.chain import ChainEvent
>>> from mailman.testing.helpers import event_subscribers
>>> def handler(event):
...     if isinstance(event, ChainEvent):
...         print(event.__class__.__name__,
...               event.chain.name, event.msg['message-id'])

>>> del msg['x-spam-score']
>>> msg['X-Spam-Score'] = '*****'
>>> msgdata = {}
>>> with event_subscribers(handler):
...     process(mlist, msg, msgdata, 'header-match')
HoldEvent hold <ant>

>>> hits_and_misses(msgdata)
Rule hits:
    x-spam-score: [*]{4,}
No rules missed

The configuration file can also specify a different final disposition for messages that match their header checks. For example, we may just want to discard such messages.

>>> from mailman.testing.helpers import configuration
>>> msgdata = {}
>>> with event_subscribers(handler):
...     with configuration('antispam', jump_chain='discard'):
...         process(mlist, msg, msgdata, 'header-match')
DiscardEvent discard <ant>

These programmatically added headers can be removed by flushing the chain. Now, nothing with match this message.

>>> chain.flush()
>>> msgdata = {}
>>> process(mlist, msg, msgdata, 'header-match')
>>> hits_and_misses(msgdata)
No rules hit
No rules missed

List-specific header matching

Each mailing list can also be configured with a set of header matching regular expression rules. These can be used to impose list-specific header filtering with the same semantics as the global [antispam] section, or to have a different action.

To follow the global antispam action, the header match rule must not specify a chain to jump to. If the default antispam action is changed in the configuration file and Mailman is restarted, those rules will get the new jump action.

The list administrator wants to match not on four stars, but on three plus signs, but only for the current mailing list.

>>> from mailman.interfaces.mailinglist import IHeaderMatchList
>>> header_matches = IHeaderMatchList(mlist)
>>> header_matches.append('x-spam-score', '[+]{3,}')

A message with a spam score of two pluses does not match.

>>> msgdata = {}
>>> del msg['x-spam-score']
>>> msg['X-Spam-Score'] = '++'
>>> process(mlist, msg, msgdata, 'header-match')
>>> hits_and_misses(msgdata)
No rules hit
Rule misses:
    x-spam-score: [+]{3,}

But a message with a spam score of three pluses does match. Because a message with the previous Message-Id is already in the moderation queue, we need to give this message a new Message-Id.

>>> msgdata = {}
>>> del msg['x-spam-score']
>>> msg['X-Spam-Score'] = '+++'
>>> del msg['message-id']
>>> msg['Message-Id'] = '<bee>'
>>> process(mlist, msg, msgdata, 'header-match')
>>> hits_and_misses(msgdata)
Rule hits:
    x-spam-score: [+]{3,}
No rules missed

As does a message with a spam score of four pluses.

>>> msgdata = {}
>>> del msg['x-spam-score']
>>> msg['X-Spam-Score'] = '++++'
>>> del msg['message-id']
>>> msg['Message-Id'] = '<cat>'
>>> process(mlist, msg, msgdata, 'header-match')
>>> hits_and_misses(msgdata)
Rule hits:
    x-spam-score: [+]{3,}
No rules missed

Now, the list administrator wants to match on three plus signs, but wants those emails to be discarded instead of held.

>>> header_matches.remove('x-spam-score', '[+]{3,}')
>>> header_matches.append('x-spam-score', '[+]{3,}', 'discard')

A message with a spam score of three pluses will still match, and the message will be discarded.

>>> msgdata = {}
>>> del msg['x-spam-score']
>>> msg['X-Spam-Score'] = '+++'
>>> del msg['message-id']
>>> msg['Message-Id'] = '<dog>'
>>> with event_subscribers(handler):
...     process(mlist, msg, msgdata, 'header-match')
DiscardEvent discard <dog>
>>> hits_and_misses(msgdata)
Rule hits:
    x-spam-score: [+]{3,}
No rules missed