1
# Copyright (C) 2007-2008 by the Free Software Foundation, Inc.
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.
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.
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,
18
"""The terminal 'hold' chain."""
20
from __future__ import with_statement
22
__all__ = ['HoldChain']
24
__i18n_templates__ = True
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
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
44
log = logging.getLogger('mailman.vette')
50
class HeldMessagePendable(dict):
52
PEND_KEY = 'held message'
56
class HoldChain(TerminalChainBase):
60
description = _('Hold a message and stop processing.')
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
67
rule_hits = msgdata.get('rule_hits')
69
msg['X-Mailman-Rule-Hits'] = SEMISPACE.join(rule_hits)
70
rule_misses = msgdata.get('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
78
pendable = HeldMessagePendable(type=HeldMessagePendable.PEND_KEY,
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)')
94
original_subject = oneline(original_subject, charset)
96
'listname' : mlist.fqdn_listname,
97
'subject' : original_subject,
99
'reason' : 'XXX', #reason,
100
'confirmurl' : '%s/%s' % (mlist.script_url('confirm'), token),
101
'admindb_url': mlist.script_url('admindb'),
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.
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
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,
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
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
147
'$mlist.fqdn_listname post from $sender requires approval')
148
nmsg = UserNotification(mlist.owner_address,
150
subject, lang=language)
151
nmsg.set_type('multipart/mixed')
153
maketext('postauth.txt', substitutions,
154
raw=True, mlist=mlist),
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()
169
nmsg.attach(MIMEMessage(msg))
170
nmsg.attach(MIMEMessage(dmsg))
171
nmsg.send(mlist, **{'tomoderators': 1})
172
# Log the held message
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)