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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
|
# Copyright (C) 2007 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.
"""The built-in rule chains."""
from __future__ import with_statement
__all__ = [
'AcceptChain',
'DiscardChain',
'HoldChain',
'RejectChain',
]
__metaclass__ = type
import logging
from Mailman import i18n
from Mailman.Message import UserNotification
from Mailman.Utils import maketext, oneline, wrap, GetCharSet
from Mailman.app.bounces import bounce_message
from Mailman.app.moderator import hold_message
from Mailman.app.replybot import autorespond_to_sender, can_acknowledge
from Mailman.configuration import config
from Mailman.i18n import _
from Mailman.interfaces import IChain, IChainLink, IMutableChain, IPendable
from Mailman.queue import Switchboard
log = logging.getLogger('mailman.vette')
elog = logging.getLogger('mailman.error')
SEMISPACE = '; '
class HeldMessagePendable(dict):
implements(IPendable)
PEND_KEY = 'held message'
class DiscardChain:
"""Discard a message."""
implements(IChain)
name = 'discard'
description = _('Discard a message and stop processing.')
def process(self, mlist, msg, msgdata):
"""See `IChain`."""
log.info('DISCARD: %s', msg.get('message-id', 'n/a'))
# Nothing more needs to happen.
class HoldChain:
"""Hold a message."""
implements(IChain)
name = 'hold'
description = _('Hold a message and stop processing.')
def process(self, mlist, msg, msgdata):
"""See `IChain`."""
# Start by decorating the message with a header that contains a list
# of all the rules that matched.
msg['X-Mailman-Rule-Hits'] = SEMISPACE.join(msgdata['rules'])
# Hold the message by adding it to the list's request database.
# XXX How to calculate the reason?
request_id = hold_message(mlist, msg, msgdata, None)
# Calculate a confirmation token to send to the author of the
# message.
pendable = HeldMessagePendable(type=HeldMessagePendable.PEND_KEY,
id=request_id)
token = config.db.pendings.add(pendable)
# Get the language to send the response in. If the sender is a
# member, then send it in the member's language, otherwise send it in
# the mailing list's preferred language.
member = mlist.members.get_member(sender)
language = (member.preferred_language
if member else mlist.preferred_language)
# A substitution dictionary for the email templates.
charset = GetCharSet(mlist.preferred_language)
original_subject = msg.get('subject')
if original_subject is None:
original_subject = _('(no subject)')
else:
original_subject = oneline(original_subject, charset)
substitutions = {
'listname' : mlist.fqdn_listname,
'subject' : original_subject,
'reason' : 'XXX', #reason,
'confirmurl': '%s/%s' % (mlist.script_url('confirm'), token),
}
sender = msg.get_sender()
# At this point the message is held, but now we have to craft at least
# two responses. The first will go to the original author of the
# message and it will contain the token allowing them to approve or
# discard the message. The second one will go to the moderators of
# the mailing list, if the list is so configured.
#
# Start by possibly sending a response to the message author. There
# are several reasons why we might not go through with this. If the
# message was gated from NNTP, the author may not even know about this
# list, so don't spam them. If the author specifically requested that
# acknowledgments not be sent, or if the message was bulk email, then
# we do not send the response. It's also possible that either the
# mailing list, or the author (if they are a member) have been
# configured to not send such responses.
if (not msgdata.get('fromusenet') and
can_acknowledge(msg) and
mlist.respond_to_post_requests and
autorespond_to_sender(mlist, sender, language)):
# We can respond to the sender with a message indicating their
# posting was held.
subject = _(
'Your message to $mlist.fqdn_listname awaits moderator approval')
language = msgdata.get('lang', lang)
text = maketext('postheld.txt', substitutions,
lang=language, mlist=mlist)
nmsg = UserNotification(sender, adminaddr, subject, text, language)
nmsg.send(mlist)
# Now the message for the list moderators. This one should appear to
# come from <list>-owner since we really don't need to do bounce
# processing on it.
if mlist.admin_immed_notify:
# Now let's temporarily set the language context to that which the
# administrators are expecting.
with i18n.using_language(mlist.preferred_language):
language = mlist.preferred_language
charset = GetCharSet(language)
# We need to regenerate or re-translate a few values in the
# substitution dictionary.
d['reason'] = _(reason)
d['subject'] = original_subject
# craft the admin notification message and deliver it
subject = _(
'$mlist.fqdn_listname post from $sender requires approval')
nmsg = UserNotification(mlist.owner_address,
mlist.owner_address,
subject, lang=language)
nmsg.set_type('multipart/mixed')
text = MIMEText(
maketext('postauth.txt', substitution,
raw=True, mlist=mlist),
_charset=charset)
dmsg = MIMEText(wrap(_("""\
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.""")),
_charset=GetCharSet(lang))
dmsg['Subject'] = 'confirm ' + token
dmsg['Sender'] = requestaddr
dmsg['From'] = requestaddr
dmsg['Date'] = email.utils.formatdate(localtime=True)
dmsg['Message-ID'] = email.utils.make_msgid()
nmsg.attach(text)
nmsg.attach(MIMEMessage(msg))
nmsg.attach(MIMEMessage(dmsg))
nmsg.send(mlist, **{'tomoderators': 1})
# Log the held message
log.info('HELD: %s post from %s held, message-id=%s: %s',
listname, sender, message_id, reason)
class RejectChain:
"""Reject/bounce a message."""
implements(IChain)
name = 'reject'
description = _('Reject/bounce a message and stop processing.')
def process(self, mlist, msg, msgdata):
"""See `IChain`."""
# XXX Exception/reason
bounce_message(mlist, msg)
log.info('REJECT: %s', msg.get('message-id', 'n/a'))
class AcceptChain:
"""Accept the message for posting."""
implements(IChain)
name = 'accept'
description = _('Accept a message.')
def process(self, mlist, msg, msgdata):
"""See `IChain.`"""
accept_queue = Switchboard(config.PREPQUEUE_DIR)
accept_queue.enqueue(msg, msgdata)
log.info('ACCEPT: %s', msg.get('message-id', 'n/a'))
class Link:
"""A chain link."""
implements(IChainLink)
def __init__(self, rule, jump):
self.rule = rule
self.jump = jump
class Chain:
"""Default built-in moderation chain."""
implements(IMutableChain)
def __init__(self, name, description):
self.name = name
self.description = description
self._links = []
def append_link(self, link):
"""See `IMutableChain`."""
self._links.append(link)
def flush(self):
"""See `IMutableChain`."""
self._links = []
def process(self, mlist, msg, msgdata):
"""See `IMutableChain`."""
msgdata['rules'] = rules = []
jump = None
for link in self._links:
# The None rule always match.
if link.rule is None:
jump = link.jump
break
# If the rule hits, just to the given chain.
rule = config.rules.get(link.rule)
if rule is None:
elog.error('Rule not found: %s', rule)
elif rule.check(mlist, msg, msgdata):
rules.append(link.rule.name)
# None is a special jump meaning "keep processing this chain".
if link.jump is not None:
jump = link.jump
break
else:
# We got through the entire chain without a jumping rule match, so
# we really don't know what to do. Rather than raise an
# exception, jump to the discard chain.
log.info('Jumping to the discard chain by default.')
jump = 'discard'
# Find the named chain.
chain = config.chains.get(jump)
if chain is None:
elog.error('Chain not found: %s', chain)
# Well, what now? Nothing much left to do but discard the
# message, which we can do by simply returning.
else:
chain.process(mlist, msg, msgdata)
def initialize():
"""Set up chains, both built-in and from the database."""
for chain_class in (DiscardChain, HoldChain, RejectChain, AcceptChain):
chain = chain_class()
assert chain.name not in config.chains, (
'Duplicate chain name: %s' % chain.name)
config.chains[chain.name] = chain
# Set up a couple of other default chains.
default = Chain('built-in', _('The built-in moderation chain'), 'accept')
default.append_link(Link('approved', 'accept'))
default.append_link(Link('emergency', 'hold'))
default.append_link(Link('loop', 'discard'))
# Do all these before deciding whether to hold the message for moderation.
default.append_link(Link('administrivia', None))
default.append_link(Link('implicit-dest', None))
default.append_link(Link('max-recipients', None))
default.append_link(Link('max-size', None))
default.append_link(Link('news-moderation', None))
default.append_link(Link('no-subject', None))
default.append_link(Link('suspicious', None))
# Now if any of the above hit, jump to the hold chain.
default.append_link(Link('any', 'hold'))
# Finally, the builtin chain defaults to acceptance.
default.append_link(Link(None, 'accept'))
# XXX Read chains from the database and initialize them.
pass
|