Chains
======

When a new message comes into the system, Mailman uses a set of rule chains to
decide whether the message gets posted to the list, rejected, discarded, or
held for moderator approval.

There are a number of built-in chains available that act as end-points in the
processing of messages.


The Discard chain
-----------------

The Discard chain simply throws the message away.

    >>> from zope.interface.verify import verifyObject
    >>> from mailman.configuration import config
    >>> from mailman.interfaces import IChain
    >>> chain = config.chains['discard']
    >>> verifyObject(IChain, chain)
    True
    >>> chain.name
    'discard'
    >>> chain.description
    u'Discard a message and stop processing.'

    >>> from mailman.app.lifecycle import create_list
    >>> mlist = create_list(u'_xtest@example.com')
    >>> msg = message_from_string("""\
    ... From: aperson@example.com
    ... To: _xtest@example.com
    ... Subject: My first post
    ... Message-ID: <first>
    ...
    ... An important message.
    ... """)

    >>> from mailman.app.chains import process

    # XXX This checks the vette log file because there is no other evidence
    # that this chain has done anything.
    >>> import os
    >>> fp = open(os.path.join(config.LOG_DIR, 'vette'))
    >>> file_pos = fp.tell()
    >>> process(mlist, msg, {}, 'discard')
    >>> fp.seek(file_pos)
    >>> print 'LOG:', fp.read()
    LOG: ... DISCARD: <first>
    <BLANKLINE>


The Reject chain
----------------

The Reject chain bounces the message back to the original sender, and logs
this action.

    >>> chain = config.chains['reject']
    >>> verifyObject(IChain, chain)
    True
    >>> chain.name
    'reject'
    >>> chain.description
    u'Reject/bounce a message and stop processing.'
    >>> file_pos = fp.tell()
    >>> process(mlist, msg, {}, 'reject')
    >>> fp.seek(file_pos)
    >>> print 'LOG:', fp.read()
    LOG: ... REJECT: <first>

The bounce message is now sitting in the Virgin queue.

    >>> from mailman.queue import Switchboard
    >>> virginq = Switchboard(config.VIRGINQUEUE_DIR)
    >>> len(virginq.files)
    1
    >>> qmsg, qdata = virginq.dequeue(virginq.files[0])
    >>> print qmsg.as_string()
    Subject: My first post
    From: _xtest-owner@example.com
    To: aperson@example.com
    ...
    [No bounce details are available]
    ...
    Content-Type: message/rfc822
    MIME-Version: 1.0
    <BLANKLINE>
    From: aperson@example.com
    To: _xtest@example.com
    Subject: My first post
    Message-ID: <first>
    <BLANKLINE>
    An important message.
    <BLANKLINE>
    ...


The Hold Chain
--------------

The Hold chain places the message into the admin request database and
depending on the list's settings, sends a notification to both the original
sender and the list moderators.

    >>> mlist.web_page_url = u'http://www.example.com/'
    >>> chain = config.chains['hold']
    >>> verifyObject(IChain, chain)
    True
    >>> chain.name
    'hold'
    >>> chain.description
    u'Hold a message and stop processing.'

    >>> file_pos = fp.tell()
    >>> process(mlist, msg, {}, 'hold')
    >>> fp.seek(file_pos)
    >>> print 'LOG:', fp.read()
    LOG: ... HOLD: _xtest@example.com post from aperson@example.com held,
        message-id=<first>: n/a
    <BLANKLINE>

There are now two messages in the Virgin queue, one to the list moderators and
one to the original author.

    >>> len(virginq.files)
    2
    >>> qfiles = []
    >>> for filebase in virginq.files:
    ...     qmsg, qdata = virginq.dequeue(filebase)
    ...     virginq.finish(filebase)
    ...     qfiles.append(qmsg)
    >>> from operator import itemgetter
    >>> qfiles.sort(key=itemgetter('to'))

This message is addressed to the mailing list moderators.

    >>> print qfiles[0].as_string()
    Subject: _xtest@example.com post from aperson@example.com requires approval
    From: _xtest-owner@example.com
    To: _xtest-owner@example.com
    MIME-Version: 1.0
    ...
    As list administrator, your authorization is requested for the
    following mailing list posting:
    <BLANKLINE>
        List:    _xtest@example.com
        From:    aperson@example.com
        Subject: My first post
        Reason:  XXX
    <BLANKLINE>
    At your convenience, visit:
    <BLANKLINE>
        http://www.example.com/admindb/_xtest@example.com
    <BLANKLINE>
    to approve or deny the request.
    <BLANKLINE>
    ...
    Content-Type: message/rfc822
    MIME-Version: 1.0
    <BLANKLINE>
    From: aperson@example.com
    To: _xtest@example.com
    Subject: My first post
    Message-ID: <first>
    X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
    <BLANKLINE>
    An important message.
    <BLANKLINE>
    ...
    Content-Type: message/rfc822
    MIME-Version: 1.0
    <BLANKLINE>
    Content-Type: text/plain; charset="us-ascii"
    MIME-Version: 1.0
    Content-Transfer-Encoding: 7bit
    Subject: confirm ...
    Sender: _xtest-request@example.com
    From: _xtest-request@example.com
    ...
    <BLANKLINE>
    If you reply to this message, keeping the Subject: header intact,
    Mailman will discard the held message.  Do this if the message is
    spam.  If you reply to this message and include an Approved: header
    with the list password in it, the message will be approved for posting
    to the list.  The Approved: header can also appear in the first line
    of the body of the reply.
    ...

This message is addressed to the sender of the message.

    >>> print qfiles[1].as_string()
    MIME-Version: 1.0
    Content-Type: text/plain; charset="us-ascii"
    Content-Transfer-Encoding: 7bit
    Subject: Your message to _xtest@example.com awaits moderator approval
    From: _xtest-bounces@example.com
    To: aperson@example.com
    ...
    Your mail to '_xtest@example.com' with the subject
    <BLANKLINE>
        My first post
    <BLANKLINE>
    Is being held until the list moderator can review it for approval.
    <BLANKLINE>
    The reason it is being held:
    <BLANKLINE>
        XXX
    <BLANKLINE>
    Either the message will get posted to the list, or you will receive
    notification of the moderator's decision.  If you would like to cancel
    this posting, please visit the following URL:
    <BLANKLINE>
        http://www.example.com/confirm/_xtest@example.com/...
    <BLANKLINE>
    <BLANKLINE>

In addition, the pending database is holding the original messages, waiting
for them to be disposed of by the original author or the list moderators.  The
database is essentially a dictionary, with the keys being the randomly
selected tokens included in the urls and the values being a 2-tuple where the
first item is a type code and the second item is a message id.

    >>> import re
    >>> cookie = None
    >>> for line in qfiles[1].get_payload().splitlines():
    ...     mo = re.search('confirm/[^/]+/(?P<cookie>.*)$', line)
    ...     if mo:
    ...         cookie = mo.group('cookie')
    ...         break
    >>> assert cookie is not None, 'No confirmation token found'
    >>> data = config.db.pendings.confirm(cookie)
    >>> sorted(data.items())
    [(u'id', ...), (u'type', u'held message')]

The message itself is held in the message store.
 
    >>> rkey, rdata = config.db.requests.get_list_requests(mlist).get_request(
    ...     data['id'])
    >>> msg = config.db.message_store.get_message_by_id(
    ...     rdata['_mod_message_id'])
    >>> print msg.as_string()
    From: aperson@example.com
    To: _xtest@example.com
    Subject: My first post
    Message-ID: <first>
    X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
    <BLANKLINE>
    An important message.
    <BLANKLINE>


The Accept chain
----------------

The Accept chain sends the message on the 'prep' queue, where it will be
processed and sent on to the list membership.

    >>> chain = config.chains['accept']
    >>> verifyObject(IChain, chain)
    True
    >>> chain.name
    'accept'
    >>> chain.description
    u'Accept a message.'
    >>> file_pos = fp.tell()
    >>> process(mlist, msg, {}, 'accept')
    >>> fp.seek(file_pos)
    >>> print 'LOG:', fp.read()
    LOG: ... ACCEPT: <first>

    >>> pipelineq = Switchboard(config.PIPELINEQUEUE_DIR)
    >>> len(pipelineq.files)
    1
    >>> qmsg, qdata = pipelineq.dequeue(pipelineq.files[0])
    >>> print qmsg.as_string()
    From: aperson@example.com
    To: _xtest@example.com
    Subject: My first post
    Message-ID: <first>
    X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
    <BLANKLINE>
    An important message.
    <BLANKLINE>


Run-time chains
---------------

We can also define chains at run time, and these chains can be mutated.
Run-time chains are made up of links where each link associates both a rule
and a 'jump'.  The rule is really a rule name, which is looked up when
needed.  The jump names a chain which is jumped to if the rule matches.

There is one built-in run-time chain, called appropriately 'built-in'.  This
is the default chain to use when no other input chain is defined for a mailing
list.  It runs through the default rules, providing functionality similar to
the Hold handler from previous versions of Mailman.

    >>> chain = config.chains['built-in']
    >>> verifyObject(IChain, chain)
    True
    >>> chain.name
    'built-in'
    >>> chain.description
    u'The built-in moderation chain.'

The previously created message is innocuous enough that it should pass through
all default rules.  This message will end up in the pipeline queue.

    >>> file_pos = fp.tell()
    >>> from mailman.app.chains import process
    >>> process(mlist, msg, {})
    >>> fp.seek(file_pos)
    >>> print 'LOG:', fp.read()
    LOG: ... ACCEPT: <first>

    >>> qmsg, qdata = pipelineq.dequeue(pipelineq.files[0])
    >>> print qmsg.as_string()
    From: aperson@example.com
    To: _xtest@example.com
    Subject: My first post
    Message-ID: <first>
    X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
    X-Mailman-Rule-Misses: approved; emergency; loop; administrivia;
        implicit-dest;
    	max-recipients; max-size; news-moderation; no-subject;
        suspicious-header
    <BLANKLINE>
    An important message.
    <BLANKLINE>

In addition, the message metadata now contains lists of all rules that have
hit and all rules that have missed.

    >>> sorted(qdata['rule_hits'])
    []
    >>> sorted(qdata['rule_misses'])
    ['administrivia', 'approved', 'emergency', 'implicit-dest', 'loop',
     'max-recipients', 'max-size', 'news-moderation', 'no-subject',
     'suspicious-header']
