aboutsummaryrefslogtreecommitdiffstats
path: root/Mailman/Handlers/Approve.py
blob: cfd76f468804c98cc8fbfc43067d9394c4323352 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# Copyright (C) 1998-2011 by the Free Software Foundation, Inc.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.

"""Determine whether the message is approved for delivery.

This module only tests for definitive approvals.  IOW, this module only
determines whether the message is definitively approved or definitively
denied.  Situations that could hold a message for approval or confirmation are
not tested by this module.
"""

import re

from email.Iterators import typed_subpart_iterator

from Mailman import mm_cfg
from Mailman import Errors

# True/False
try:
    True, False
except NameError:
    True = 1
    False = 0

NL = '\n'

def _(s):
    # message is translated when used.
    return s
REJECT = _("""Message rejected.
It appears that this message contains an HTML part with the
Approved: password line, but due to the way it is coded in the
HTML it can't be safely removed.
""")
del _



def process(mlist, msg, msgdata):
    # Short circuits
    # Do not short circuit. The problem is SpamDetect comes before Approve.
    # Suppose a message with an Approved: header is held by SpamDetect (or
    # any other handler that might come before Approve) and then approved
    # by a moderator. When the approved message reaches Approve in the
    # pipeline, we still need to remove the Approved: (pseudo-)header, so
    # we can't short circuit.
    #if msgdata.get('approved'):
        # Digests, Usenet postings, and some other messages come pre-approved.
        # TBD: we may want to further filter Usenet messages, so the test
        # above may not be entirely correct.
        #return
    # See if the message has an Approved or Approve header with a valid
    # list-moderator, list-admin.  Also look at the first non-whitespace line
    # in the file to see if it looks like an Approved header.  We are
    # specifically /not/ allowing the site admins password to work here
    # because we want to discourage the practice of sending the site admin
    # password through email in the clear.
    missing = []
    for hdr in ('approved', 'approve', 'x-approved', 'x-approve'):
        passwd = msg.get(hdr, missing)
        if passwd is not missing:
            break
    if passwd is missing:
        # Find the first text/plain part in the message
        part = None
        stripped = False
        for part in typed_subpart_iterator(msg, 'text', 'plain'):
            break
        # XXX I'm not entirely sure why, but it is possible for the payload of
        # the part to be None, and you can't splitlines() on None.
        if part is not None and part.get_payload() is not None:
            lines = part.get_payload(decode=True).splitlines()
            line = ''
            for lineno, line in zip(range(len(lines)), lines):
                if line.strip():
                    break
            i = line.find(':')
            if i >= 0:
                name = line[:i]
                value = line[i+1:]
                if name.lower() in ('approve',
                                    'approved',
                                    'x-approve',
                                    'x-approved',
                                    ):
                    passwd = value.lstrip()
                    # Now strip the first line from the payload so the
                    # password doesn't leak.
                    del lines[lineno]
                    reset_payload(part, NL.join(lines))
                    stripped = True
        if stripped:
            # MAS: Bug 1181161 - Now try all the text parts in case it's
            # multipart/alternative with the approved line in HTML or other
            # text part.  We make a pattern from the Approved line and delete
            # it from all text/* parts in which we find it.  It would be
            # better to just iterate forward, but email compatability for pre
            # Python 2.2 returns a list, not a true iterator.  Also, there
            # are pathological MUAs that put the HTML part first.
            #
            # This will process all the multipart/alternative parts in the
            # message as well as all other text parts.  We shouldn't find the
            # pattern outside the mp/a parts, but if we do, it is probably
            # best to delete it anyway as it does contain the password.
            #
            # Make a pattern to delete.  We can't just delete a line because
            # line of HTML or other fancy text may include additional message
            # text.  This pattern works with HTML.  It may not work with rtf
            # or whatever else is possible.
            #
            # If we don't find the pattern in the decoded part, but we do
            # find it after stripping HTML tags, we don't know how to remove
            # it, so we just reject the post.
            pattern = name + ':(\xA0|\s| )*' + re.escape(passwd)
            for part in typed_subpart_iterator(msg, 'text'):
                if part is not None and part.get_payload() is not None:
                    lines = part.get_payload(decode=True)
                    if re.search(pattern, lines):
                        reset_payload(part, re.sub(pattern, '', lines))
                    elif re.search(pattern, re.sub('(?s)<.*?>', '', lines)):
                        raise Errors.RejectMessage, REJECT
    if passwd is not missing and mlist.Authenticate((mm_cfg.AuthListPoster,
                                                     mm_cfg.AuthListModerator,
                                                     mm_cfg.AuthListAdmin),
                                                    passwd):
        # BAW: should we definitely deny if the password exists but does not
        # match?  For now we'll let it percolate up for further determination.
        msgdata['approved'] = 1
        # Used by the Emergency module
        msgdata['adminapproved'] = 1
    # has this message already been posted to this list?
    beentheres = [s.strip().lower() for s in msg.get_all('x-beenthere', [])]
    if mlist.GetListEmail().lower() in beentheres:
        raise Errors.LoopError

def reset_payload(part, payload):
    # Set decoded payload maintaining content-type, format and delsp.
    # TK: Message with 'charset=' cause trouble. So, instead of
    #     part.get_content_charset('us-ascii') ...
    cset = part.get_content_charset() or 'us-ascii'
    ctype = part.get_content_type()
    format = part.get_param('format')
    delsp = part.get_param('delsp')
    del part['content-transfer-encoding']
    del part['content-type']
    part.set_payload(payload, cset)
    part.set_type(ctype)
    if format:
        part.set_param('Format', format)
    if delsp:
        part.set_param('DelSp', delsp)