Pre-approved postings
=====================

Messages can contain a pre-approval, which is used to bypass the message
approval queue.  This has several use cases:

- A list administrator can send an emergency message to the mailing list from
  an unregistered address, say if they are away from their normal email.

- An automated script can be programmed to send a message to an otherwise
  moderated list.

In order to support this, a mailing list can be given a 'moderator password'
which is shared among all the administrators.

    >>> from mailman.configuration import config
    >>> mlist = config.db.list_manager.create(u'_xtest@example.com')
    >>> mlist.moderator_password = u'abcxyz'

The 'approved' rule determines whether the message contains the proper
approval or not.

    >>> rule = config.rules['approved']
    >>> rule.name
    'approved'


No approval
-----------

If the message has no Approve or Approved header, then the rule does not
match.

    >>> msg = message_from_string("""\
    ... From: aperson@example.com
    ...
    ... An important message.
    ... """)
    >>> rule.check(mlist, msg, {})
    False

If the message has an Approve or Approved header with a value that does not
match the moderator password, then the rule does not match.  However, the
header is still removed.

    >>> msg['Approve'] = u'12345'
    >>> rule.check(mlist, msg, {})
    False
    >>> print msg['approve']
    None

    >>> del msg['approve']
    >>> msg['Approved'] = u'12345'
    >>> rule.check(mlist, msg, {})
    False
    >>> print msg['approved']
    None

    >>> del msg['approved']


Using an approval header
------------------------

If the moderator password is given in an Approve header, then the rule
matches, and the Approve header is stripped.

    >>> msg['Approve'] = u'abcxyz'
    >>> rule.check(mlist, msg, {})
    True
    >>> print msg['approve']
    None

Similarly, for the Approved header.

    >>> msg['Approved'] = u'abcxyz'
    >>> rule.check(mlist, msg, {})
    True
    >>> print msg['approved']
    None


Using a pseudo-header
---------------------

Different mail user agents have varying degrees to which they support custom
headers like Approve and Approved.  For this reason, Mailman also supports
using a 'pseudo-header', which is really just the first non-whitespace line in
the payload of the message.  If this pseudo-header looks like a matching
Approve or Approved header, the message is similarly allowed to pass.

    >>> msg = message_from_string("""\
    ... From: aperson@example.com
    ...
    ... Approve: abcxyz
    ... An important message.
    ... """)
    >>> rule.check(mlist, msg, {})
    True

The pseudo-header is removed.

    >>> print msg.as_string()
    From: aperson@example.com
    Content-Transfer-Encoding: 7bit
    MIME-Version: 1.0
    Content-Type: text/plain; charset="us-ascii"
    <BLANKLINE>
    An important message.
    <BLANKLINE>

Similarly for the Approved header.

    >>> msg = message_from_string("""\
    ... From: aperson@example.com
    ...
    ... Approved: abcxyz
    ... An important message.
    ... """)
    >>> rule.check(mlist, msg, {})
    True

    >>> print msg.as_string()
    From: aperson@example.com
    Content-Transfer-Encoding: 7bit
    MIME-Version: 1.0
    Content-Type: text/plain; charset="us-ascii"
    <BLANKLINE>
    An important message.
    <BLANKLINE>

As before, a mismatch in the pseudo-header does not approve the message, but
the pseudo-header line is still removed.

    >>> msg = message_from_string("""\
    ... From: aperson@example.com
    ...
    ... Approve: 123456
    ... An important message.
    ... """)
    >>> rule.check(mlist, msg, {})
    False

    >>> print msg.as_string()
    From: aperson@example.com
    Content-Transfer-Encoding: 7bit
    MIME-Version: 1.0
    Content-Type: text/plain; charset="us-ascii"
    <BLANKLINE>
    An important message.
    <BLANKLINE>

Similarly for the Approved header.

    >>> msg = message_from_string("""\
    ... From: aperson@example.com
    ...
    ... Approved: 123456
    ... An important message.
    ... """)
    >>> rule.check(mlist, msg, {})
    False

    >>> print msg.as_string()
    From: aperson@example.com
    Content-Transfer-Encoding: 7bit
    MIME-Version: 1.0
    Content-Type: text/plain; charset="us-ascii"
    <BLANKLINE>
    An important message.
    <BLANKLINE>


MIME multipart support
----------------------

Mailman searches for the pseudo-header as the first non-whitespace line in the
first text/plain message part of the message.  This allows the feature to be
used with MIME documents.

    >>> msg = message_from_string("""\
    ... From: aperson@example.com
    ... MIME-Version: 1.0
    ... Content-Type: multipart/mixed; boundary="AAA"
    ...
    ... --AAA
    ... Content-Type: application/x-ignore
    ...
    ... Approve: 123456
    ... The above line will be ignored.
    ...
    ... --AAA
    ... Content-Type: text/plain
    ...
    ... Approve: abcxyz
    ... An important message.
    ... --AAA--
    ... """)
    >>> rule.check(mlist, msg, {})
    True

Like before, the pseudo-header is removed, but only from the text parts.

    >>> print msg.as_string()
    From: aperson@example.com
    MIME-Version: 1.0
    Content-Type: multipart/mixed; boundary="AAA"
    <BLANKLINE>
    --AAA
    Content-Type: application/x-ignore
    <BLANKLINE>
    Approve: 123456
    The above line will be ignored.
    <BLANKLINE>
    --AAA
    Content-Transfer-Encoding: 7bit
    MIME-Version: 1.0
    Content-Type: text/plain; charset="us-ascii"
    <BLANKLINE>
    An important message.
    --AAA--
    <BLANKLINE>

The same goes for the Approved message.

    >>> msg = message_from_string("""\
    ... From: aperson@example.com
    ... MIME-Version: 1.0
    ... Content-Type: multipart/mixed; boundary="AAA"
    ...
    ... --AAA
    ... Content-Type: application/x-ignore
    ...
    ... Approved: 123456
    ... The above line will be ignored.
    ...
    ... --AAA
    ... Content-Type: text/plain
    ...
    ... Approved: abcxyz
    ... An important message.
    ... --AAA--
    ... """)
    >>> rule.check(mlist, msg, {})
    True

And the header is removed.

    >>> print msg.as_string()
    From: aperson@example.com
    MIME-Version: 1.0
    Content-Type: multipart/mixed; boundary="AAA"
    <BLANKLINE>
    --AAA
    Content-Type: application/x-ignore
    <BLANKLINE>
    Approved: 123456
    The above line will be ignored.
    <BLANKLINE>
    --AAA
    Content-Transfer-Encoding: 7bit
    MIME-Version: 1.0
    Content-Type: text/plain; charset="us-ascii"
    <BLANKLINE>
    An important message.
    --AAA--
    <BLANKLINE>

Here, the correct password is in the non-text/plain part, so it is ignored.

    >>> msg = message_from_string("""\
    ... From: aperson@example.com
    ... MIME-Version: 1.0
    ... Content-Type: multipart/mixed; boundary="AAA"
    ...
    ... --AAA
    ... Content-Type: application/x-ignore
    ...
    ... Approve: abcxyz
    ... The above line will be ignored.
    ...
    ... --AAA
    ... Content-Type: text/plain
    ...
    ... Approve: 123456
    ... An important message.
    ... --AAA--
    ... """)
    >>> rule.check(mlist, msg, {})
    False

And yet the pseudo-header is still stripped.

    >>> print msg.as_string()
    From: aperson@example.com
    MIME-Version: 1.0
    Content-Type: multipart/mixed; boundary="AAA"
    <BLANKLINE>
    --AAA
    Content-Type: application/x-ignore
    <BLANKLINE>
    Approve: abcxyz
    The above line will be ignored.
    <BLANKLINE>
    --AAA
    Content-Transfer-Encoding: 7bit
    MIME-Version: 1.0
    Content-Type: text/plain; charset="us-ascii"
    <BLANKLINE>
    An important message.
    --AAA--

As before, the same goes for the Approved header.

    >>> msg = message_from_string("""\
    ... From: aperson@example.com
    ... MIME-Version: 1.0
    ... Content-Type: multipart/mixed; boundary="AAA"
    ...
    ... --AAA
    ... Content-Type: application/x-ignore
    ...
    ... Approved: abcxyz
    ... The above line will be ignored.
    ...
    ... --AAA
    ... Content-Type: text/plain
    ...
    ... Approved: 123456
    ... An important message.
    ... --AAA--
    ... """)
    >>> rule.check(mlist, msg, {})
    False

And the pseudo-header is removed.

    >>> print msg.as_string()
    From: aperson@example.com
    MIME-Version: 1.0
    Content-Type: multipart/mixed; boundary="AAA"
    <BLANKLINE>
    --AAA
    Content-Type: application/x-ignore
    <BLANKLINE>
    Approved: abcxyz
    The above line will be ignored.
    <BLANKLINE>
    --AAA
    Content-Transfer-Encoding: 7bit
    MIME-Version: 1.0
    Content-Type: text/plain; charset="us-ascii"
    <BLANKLINE>
    An important message.
    --AAA--


Stripping text/html parts
-------------------------

Because some mail readers will include both a text/plain part and a text/html
alternative, the 'approved' rule has to search the alternatives and strip
anything that looks like an Approve or Approved headers.

    >>> msg = message_from_string("""\
    ... From: aperson@example.com
    ... MIME-Version: 1.0
    ... Content-Type: multipart/mixed; boundary="AAA"
    ...
    ... --AAA
    ... Content-Type: text/html
    ...
    ... <html>
    ... <head></head>
    ... <body>
    ... <b>Approved: abcxyz</b>
    ... <p>The above line will be ignored.
    ... </body>
    ... </html>
    ...
    ... --AAA
    ... Content-Type: text/plain
    ...
    ... Approved: abcxyz
    ... An important message.
    ... --AAA--
    ... """)
    >>> rule.check(mlist, msg, {})
    True

And the header-like text in the text/html part was stripped.

    >>> print msg.as_string()
    From: aperson@example.com
    MIME-Version: 1.0
    Content-Type: multipart/mixed; boundary="AAA"
    <BLANKLINE>
    --AAA
    Content-Transfer-Encoding: 7bit
    MIME-Version: 1.0
    Content-Type: text/html; charset="us-ascii"
    <BLANKLINE>
    <html>
    <head></head>
    <body>
    <b></b>
    <p>The above line will be ignored.
    </body>
    </html>
    <BLANKLINE>
    --AAA
    Content-Transfer-Encoding: 7bit
    MIME-Version: 1.0
    Content-Type: text/plain; charset="us-ascii"
    <BLANKLINE>
    An important message.
    --AAA--
    <BLANKLINE>

This is true even if the rule does not match.

    >>> msg = message_from_string("""\
    ... From: aperson@example.com
    ... MIME-Version: 1.0
    ... Content-Type: multipart/mixed; boundary="AAA"
    ...
    ... --AAA
    ... Content-Type: text/html
    ...
    ... <html>
    ... <head></head>
    ... <body>
    ... <b>Approve: 123456</b>
    ... <p>The above line will be ignored.
    ... </body>
    ... </html>
    ...
    ... --AAA
    ... Content-Type: text/plain
    ...
    ... Approve: 123456
    ... An important message.
    ... --AAA--
    ... """)
    >>> rule.check(mlist, msg, {})
    False

    >>> print msg.as_string()
    From: aperson@example.com
    MIME-Version: 1.0
    Content-Type: multipart/mixed; boundary="AAA"
    <BLANKLINE>
    --AAA
    Content-Transfer-Encoding: 7bit
    MIME-Version: 1.0
    Content-Type: text/html; charset="us-ascii"
    <BLANKLINE>
    <html>
    <head></head>
    <body>
    <b></b>
    <p>The above line will be ignored.
    </body>
    </html>
    <BLANKLINE>
    --AAA
    Content-Transfer-Encoding: 7bit
    MIME-Version: 1.0
    Content-Type: text/plain; charset="us-ascii"
    <BLANKLINE>
    An important message.
    --AAA--
    <BLANKLINE>
