~barry/mailman/events-and-web

« back to all changes in this revision

Viewing changes to Mailman/chains/hold.py

  • Committer: Barry Warsaw
  • Date: 2008-02-03 04:03:19 UTC
  • mfrom: (6581.1.27 rules)
  • Revision ID: barry@python.org-20080203040319-mnb1sar9bumaih01
Merge the 'rules' branch.

Give the first alpha a code name.

This branch mostly gets rid of all the approval oriented handlers in favor of
a chain-of-rules based approach.  This will be much more powerful and
extensible, allowing rule definition by plugin and chain creation via web
page.

When a message is processed by the incoming queue, it gets sent through a
chain of rules.  The starting chain is defined on the mailing list object, and
there is a built-in default starting chain, called 'built-in'.  Each chain is
made up of links, which describe a rule and an action, along with possibly
some other information.  Actions allow processing to take a detour through
another chain, jump to another chain, stop processing, run a function, etc.

The built-in chain essentially implements the original early part of the
handler pipeline.  If a message makes it through the built-in chain, it gets
sent to the prep queue, where the message is decorated and such before sending
out to the list membership.  The 'accept' chain is what moves the message into
the prep queue.

There are also 'hold', 'discard', and 'reject' chains, which do what you would
expect them to.  There are lots of built-in rules, implementing everything
from the old emergency handler to new handlers such as one not allowing empty
subject headers.

IMember grows an is_moderated attribute.

The 'adminapproved' metadata key is renamed 'moderator_approved'.

Fix some bogus uses of noreply_address to no_reply_address.

Stash an 'original_size' attribute on the message after parsing its plain
text.  This can be used later to ensure the original message does not exceed a
specified size without have to flatten the message again.

The KNOWN_SPAMMERS global variable is replaced with HEADER_MATCHES.  The
mailing list's header_filter_rules variable is replaced with header_matches
which has the same semantics as HEADER_MATCHES, but is list-specific.

DEFAULT_MAIL_COMMANDS_MAX_LINES -> EMAIL_COMMANDS_MAX_LINES.

Update smtplistener.py to be much better, to use maildir format instead of
mbox format, to respond to RSET commands by clearing the maildir, and by
silencing annoying asyncore error messages.

Extend the doctest runner so that it will run .txt files in any docs
subdirectory in the code tree.

Add plugable keys 'mailman.mta' and 'mailman.rules'.  The latter may have only
one setting while the former is extensible.

There are lots of doctests which should give all the gory details.

Mailman/Post.py -> Mailman/inject.py and the command line usage of this module
is removed.

SQLALCHEMY_ECHO, which was unused, is removed.

Backport the ability to specify additional footer interpolation variables by
the message metadata 'decoration-data' key.

can_acknowledge() defines whether a message can be responded to by the email
robot.

Simplify the implementation of _reset() based on Storm fixes.  Be able to
handle lists in Storm values.

Do some reorganization.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2007-2008 by the Free Software Foundation, Inc.
 
2
#
 
3
# This program is free software; you can redistribute it and/or
 
4
# modify it under the terms of the GNU General Public License
 
5
# as published by the Free Software Foundation; either version 2
 
6
# of the License, or (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
 
16
# USA.
 
17
 
 
18
"""The terminal 'hold' chain."""
 
19
 
 
20
from __future__ import with_statement
 
21
 
 
22
__all__ = ['HoldChain']
 
23
__metaclass__ = type
 
24
__i18n_templates__ = True
 
25
 
 
26
 
 
27
import logging
 
28
 
 
29
from email.mime.message import MIMEMessage
 
30
from email.mime.text import MIMEText
 
31
from email.utils import formatdate, make_msgid
 
32
from zope.interface import implements
 
33
 
 
34
from Mailman import i18n
 
35
from Mailman.Message import UserNotification
 
36
from Mailman.Utils import maketext, oneline, wrap, GetCharSet
 
37
from Mailman.app.moderator import hold_message
 
38
from Mailman.app.replybot import autorespond_to_sender, can_acknowledge
 
39
from Mailman.chains.base import TerminalChainBase
 
40
from Mailman.configuration import config
 
41
from Mailman.interfaces import IPendable
 
42
 
 
43
 
 
44
log = logging.getLogger('mailman.vette')
 
45
SEMISPACE = '; '
 
46
_ = i18n._
 
47
 
 
48
 
 
49
 
 
50
class HeldMessagePendable(dict):
 
51
    implements(IPendable)
 
52
    PEND_KEY = 'held message'
 
53
 
 
54
 
 
55
 
 
56
class HoldChain(TerminalChainBase):
 
57
    """Hold a message."""
 
58
 
 
59
    name = 'hold'
 
60
    description = _('Hold a message and stop processing.')
 
61
 
 
62
    def _process(self, mlist, msg, msgdata):
 
63
        """See `TerminalChainBase`."""
 
64
        # Start by decorating the message with a header that contains a list
 
65
        # of all the rules that matched.  These metadata could be None or an
 
66
        # empty list.
 
67
        rule_hits = msgdata.get('rule_hits')
 
68
        if rule_hits:
 
69
            msg['X-Mailman-Rule-Hits'] = SEMISPACE.join(rule_hits)
 
70
        rule_misses = msgdata.get('rule_misses')
 
71
        if rule_misses:
 
72
            msg['X-Mailman-Rule-Misses'] = SEMISPACE.join(rule_misses)
 
73
        # Hold the message by adding it to the list's request database.
 
74
        # XXX How to calculate the reason?
 
75
        request_id = hold_message(mlist, msg, msgdata, None)
 
76
        # Calculate a confirmation token to send to the author of the
 
77
        # message.
 
78
        pendable = HeldMessagePendable(type=HeldMessagePendable.PEND_KEY,
 
79
                                       id=request_id)
 
80
        token = config.db.pendings.add(pendable)
 
81
        # Get the language to send the response in.  If the sender is a
 
82
        # member, then send it in the member's language, otherwise send it in
 
83
        # the mailing list's preferred language.
 
84
        sender = msg.get_sender()
 
85
        member = mlist.members.get_member(sender)
 
86
        language = (member.preferred_language
 
87
                    if member else mlist.preferred_language)
 
88
        # A substitution dictionary for the email templates.
 
89
        charset = GetCharSet(mlist.preferred_language)
 
90
        original_subject = msg.get('subject')
 
91
        if original_subject is None:
 
92
            original_subject = _('(no subject)')
 
93
        else:
 
94
            original_subject = oneline(original_subject, charset)
 
95
        substitutions = {
 
96
            'listname'   : mlist.fqdn_listname,
 
97
            'subject'    : original_subject,
 
98
            'sender'     : sender,
 
99
            'reason'     : 'XXX', #reason,
 
100
            'confirmurl' : '%s/%s' % (mlist.script_url('confirm'), token),
 
101
            'admindb_url': mlist.script_url('admindb'),
 
102
            }
 
103
        # At this point the message is held, but now we have to craft at least
 
104
        # two responses.  The first will go to the original author of the
 
105
        # message and it will contain the token allowing them to approve or
 
106
        # discard the message.  The second one will go to the moderators of
 
107
        # the mailing list, if the list is so configured.
 
108
        #
 
109
        # Start by possibly sending a response to the message author.  There
 
110
        # are several reasons why we might not go through with this.  If the
 
111
        # message was gated from NNTP, the author may not even know about this
 
112
        # list, so don't spam them.  If the author specifically requested that
 
113
        # acknowledgments not be sent, or if the message was bulk email, then
 
114
        # we do not send the response.  It's also possible that either the
 
115
        # mailing list, or the author (if they are a member) have been
 
116
        # configured to not send such responses.
 
117
        if (not msgdata.get('fromusenet') and
 
118
            can_acknowledge(msg) and
 
119
            mlist.respond_to_post_requests and
 
120
            autorespond_to_sender(mlist, sender, language)):
 
121
            # We can respond to the sender with a message indicating their
 
122
            # posting was held.
 
123
            subject = _(
 
124
              'Your message to $mlist.fqdn_listname awaits moderator approval')
 
125
            send_language = msgdata.get('lang', language)
 
126
            text = maketext('postheld.txt', substitutions,
 
127
                            lang=send_language, mlist=mlist)
 
128
            adminaddr = mlist.bounces_address
 
129
            nmsg = UserNotification(sender, adminaddr, subject, text,
 
130
                                    send_language)
 
131
            nmsg.send(mlist)
 
132
        # Now the message for the list moderators.  This one should appear to
 
133
        # come from <list>-owner since we really don't need to do bounce
 
134
        # processing on it.
 
135
        if mlist.admin_immed_notify:
 
136
            # Now let's temporarily set the language context to that which the
 
137
            # administrators are expecting.
 
138
            with i18n.using_language(mlist.preferred_language):
 
139
                language = mlist.preferred_language
 
140
                charset = GetCharSet(language)
 
141
                # We need to regenerate or re-translate a few values in the
 
142
                # substitution dictionary.
 
143
                #d['reason'] = _(reason) # XXX reason
 
144
                substitutions['subject'] = original_subject
 
145
                # craft the admin notification message and deliver it
 
146
                subject = _(
 
147
                    '$mlist.fqdn_listname post from $sender requires approval')
 
148
                nmsg = UserNotification(mlist.owner_address,
 
149
                                        mlist.owner_address,
 
150
                                        subject, lang=language)
 
151
                nmsg.set_type('multipart/mixed')
 
152
                text = MIMEText(
 
153
                    maketext('postauth.txt', substitutions,
 
154
                             raw=True, mlist=mlist),
 
155
                    _charset=charset)
 
156
                dmsg = MIMEText(wrap(_("""\
 
157
If you reply to this message, keeping the Subject: header intact, Mailman will
 
158
discard the held message.  Do this if the message is spam.  If you reply to
 
159
this message and include an Approved: header with the list password in it, the
 
160
message will be approved for posting to the list.  The Approved: header can
 
161
also appear in the first line of the body of the reply.""")),
 
162
                                _charset=GetCharSet(language))
 
163
                dmsg['Subject'] = 'confirm ' + token
 
164
                dmsg['Sender'] = mlist.request_address
 
165
                dmsg['From'] = mlist.request_address
 
166
                dmsg['Date'] = formatdate(localtime=True)
 
167
                dmsg['Message-ID'] = make_msgid()
 
168
                nmsg.attach(text)
 
169
                nmsg.attach(MIMEMessage(msg))
 
170
                nmsg.attach(MIMEMessage(dmsg))
 
171
                nmsg.send(mlist, **{'tomoderators': 1})
 
172
        # Log the held message
 
173
        # XXX reason
 
174
        reason = 'n/a'
 
175
        log.info('HOLD: %s post from %s held, message-id=%s: %s',
 
176
                 mlist.fqdn_listname, sender,
 
177
                 msg.get('message-id', 'n/a'), reason)