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
|
# Copyright (C) 2007-2009 by the Free Software Foundation, Inc.
#
# This file is part of GNU Mailman.
#
# GNU Mailman 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 3 of the License, or (at your option)
# any later version.
#
# GNU Mailman 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
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
"""The terminal 'hold' chain."""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'HoldChain',
]
import logging
from email.mime.message import MIMEMessage
from email.mime.text import MIMEText
from email.utils import formatdate, make_msgid
from zope.interface import implements
from mailman import i18n
from mailman.Utils import maketext, oneline, wrap
from mailman.app.moderator import hold_message
from mailman.app.replybot import autorespond_to_sender, can_acknowledge
from mailman.chains.base import TerminalChainBase
from mailman.config import config
from mailman.email.message import UserNotification
from mailman.interfaces.pending import IPendable
log = logging.getLogger('mailman.vette')
SEMISPACE = '; '
_ = i18n._
class HeldMessagePendable(dict):
implements(IPendable)
PEND_KEY = 'held message'
class HoldChain(TerminalChainBase):
"""Hold a message."""
name = 'hold'
description = _('Hold a message and stop processing.')
def _process(self, mlist, msg, msgdata):
"""See `TerminalChainBase`."""
# Start by decorating the message with a header that contains a list
# of all the rules that matched. These metadata could be None or an
# empty list.
rule_hits = msgdata.get('rule_hits')
if rule_hits:
msg['X-Mailman-Rule-Hits'] = SEMISPACE.join(rule_hits)
rule_misses = msgdata.get('rule_misses')
if rule_misses:
msg['X-Mailman-Rule-Misses'] = SEMISPACE.join(rule_misses)
# 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(msg.sender)
language = (member.preferred_language
if member else mlist.preferred_language)
# A substitution dictionary for the email templates.
charset = mlist.preferred_language.charset
original_subject = msg.get('subject')
if original_subject is None:
original_subject = _('(no subject)')
else:
original_subject = oneline(original_subject, charset)
substitutions = dict(
listname = mlist.fqdn_listname,
subject = original_subject,
sender = msg.sender,
reason = 'XXX', #reason,
confirmurl = '{0}/{1}'.format(mlist.script_url('confirm'), token),
admindb_url = mlist.script_url('admindb'),
)
# 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, msg.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')
send_language_code = msgdata.get('lang', language.code)
text = maketext('postheld.txt', substitutions,
lang=send_language_code, mlist=mlist)
adminaddr = mlist.bounces_address
nmsg = UserNotification(msg.sender, adminaddr, subject, text,
config.languages[send_language_code])
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.code):
language = mlist.preferred_language
charset = language.charset
# We need to regenerate or re-translate a few values in the
# substitution dictionary.
#d['reason'] = _(reason) # XXX reason
substitutions['subject'] = original_subject
# craft the admin notification message and deliver it
subject = _(
'$mlist.fqdn_listname post from $msg.sender requires '
'approval')
nmsg = UserNotification(mlist.owner_address,
mlist.owner_address,
subject, lang=language)
nmsg.set_type('multipart/mixed')
text = MIMEText(
maketext('postauth.txt', substitutions,
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=language.charset)
dmsg['Subject'] = 'confirm ' + token
dmsg['Sender'] = mlist.request_address
dmsg['From'] = mlist.request_address
dmsg['Date'] = formatdate(localtime=True)
dmsg['Message-ID'] = make_msgid()
nmsg.attach(text)
nmsg.attach(MIMEMessage(msg))
nmsg.attach(MIMEMessage(dmsg))
nmsg.send(mlist, **dict(tomoderators=True))
# Log the held message
# XXX reason
reason = 'n/a'
log.info('HOLD: %s post from %s held, message-id=%s: %s',
mlist.fqdn_listname, msg.sender,
msg.get('message-id', 'n/a'), reason)
|