Convert the administrivia check from the Hold handler to the administrivia
[mailman.git] / Mailman / queue / command.py
blob8628411def4b5bd097b9e5f9cd779d253394ea2e
1 # Copyright (C) 1998-2007 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, USA.
17 """-request robot command queue runner."""
19 # See the delivery diagram in IncomingRunner.py. This module handles all
20 # email destined for mylist-request, -join, and -leave. It no longer handles
21 # bounce messages (i.e. -admin or -bounces), nor does it handle mail to
22 # -owner.
24 import re
25 import sys
26 import logging
28 from email.Errors import HeaderParseError
29 from email.Header import decode_header, make_header, Header
30 from email.Iterators import typed_subpart_iterator
31 from email.MIMEMessage import MIMEMessage
32 from email.MIMEText import MIMEText
34 from Mailman import Message
35 from Mailman import Utils
36 from Mailman.Handlers import Replybot
37 from Mailman.app.replybot import autorespond_to_sender
38 from Mailman.configuration import config
39 from Mailman.i18n import _
40 from Mailman.queue import Runner
42 NL = '\n'
44 log = logging.getLogger('mailman.vette')
48 class Results:
49 def __init__(self, mlist, msg, msgdata):
50 self.mlist = mlist
51 self.msg = msg
52 self.msgdata = msgdata
53 # Only set returnaddr if the response is to go to someone other than
54 # the address specified in the From: header (e.g. for the password
55 # command).
56 self.returnaddr = None
57 self.commands = []
58 self.results = []
59 self.ignored = []
60 self.lineno = 0
61 self.subjcmdretried = 0
62 self.respond = True
63 # Extract the subject header and do RFC 2047 decoding. Note that
64 # Python 2.1's unicode() builtin doesn't call obj.__unicode__().
65 subj = msg.get('subject', '')
66 try:
67 subj = make_header(decode_header(subj)).__unicode__()
68 # TK: Currently we don't allow 8bit or multibyte in mail command.
69 subj = subj.encode('us-ascii')
70 # Always process the Subject: header first
71 self.commands.append(subj)
72 except (HeaderParseError, UnicodeError, LookupError):
73 # We couldn't parse it so ignore the Subject header
74 pass
75 # Find the first text/plain part
76 part = None
77 for part in typed_subpart_iterator(msg, 'text', 'plain'):
78 break
79 if part is None or part is not msg:
80 # Either there was no text/plain part or we ignored some
81 # non-text/plain parts.
82 self.results.append(_('Ignoring non-text/plain MIME parts'))
83 if part is None:
84 # E.g the outer Content-Type: was text/html
85 return
86 body = part.get_payload(decode=True)
87 # text/plain parts better have string payloads
88 assert isinstance(body, basestring)
89 lines = body.splitlines()
90 # Use no more lines than specified
91 self.commands.extend(lines[:config.EMAIL_COMMANDS_MAX_LINES])
92 self.ignored.extend(lines[config.EMAIL_COMMANDS_MAX_LINES:])
94 def process(self):
95 # Now, process each line until we find an error. The first
96 # non-command line found stops processing.
97 stop = False
98 for line in self.commands:
99 if line and line.strip():
100 args = line.split()
101 cmd = args.pop(0).lower()
102 stop = self.do_command(cmd, args)
103 self.lineno += 1
104 if stop:
105 break
107 def do_command(self, cmd, args=None):
108 if args is None:
109 args = ()
110 # Try to import a command handler module for this command
111 modname = 'Mailman.Commands.cmd_' + cmd
112 try:
113 __import__(modname)
114 handler = sys.modules[modname]
115 # ValueError can be raised if cmd has dots in it.
116 except (ImportError, ValueError):
117 # If we're on line zero, it was the Subject: header that didn't
118 # contain a command. It's possible there's a Re: prefix (or
119 # localized version thereof) on the Subject: line that's messing
120 # things up. Pop the prefix off and try again... once.
122 # If that still didn't work it isn't enough to stop processing.
123 # BAW: should we include a message that the Subject: was ignored?
124 if not self.subjcmdretried and args:
125 self.subjcmdretried += 1
126 cmd = args.pop(0)
127 return self.do_command(cmd, args)
128 return self.lineno <> 0
129 return handler.process(self, args)
131 def send_response(self):
132 # Helper
133 def indent(lines):
134 return [' ' + line for line in lines]
135 # Quick exit for some commands which don't need a response
136 if not self.respond:
137 return
138 resp = [Utils.wrap(_("""\
139 The results of your email command are provided below.
140 Attached is your original message.
141 """))]
142 if self.results:
143 resp.append(_('- Results:'))
144 resp.extend(indent(self.results))
145 # Ignore empty lines
146 unprocessed = [line for line in self.commands[self.lineno:]
147 if line and line.strip()]
148 if unprocessed:
149 resp.append(_('\n- Unprocessed:'))
150 resp.extend(indent(unprocessed))
151 if not unprocessed and not self.results:
152 # The user sent an empty message; return a helpful one.
153 resp.append(Utils.wrap(_("""\
154 No commands were found in this message.
155 To obtain instructions, send a message containing just the word "help".
156 """)))
157 if self.ignored:
158 resp.append(_('\n- Ignored:'))
159 resp.extend(indent(self.ignored))
160 resp.append(_('\n- Done.\n\n'))
161 # Encode any unicode strings into the list charset, so we don't try to
162 # join unicode strings and invalid ASCII.
163 charset = Utils.GetCharSet(self.msgdata['lang'])
164 encoded_resp = []
165 for item in resp:
166 if isinstance(item, unicode):
167 item = item.encode(charset, 'replace')
168 encoded_resp.append(item)
169 results = MIMEText(NL.join(encoded_resp), _charset=charset)
170 # Safety valve for mail loops with misconfigured email 'bots. We
171 # don't respond to commands sent with "Precedence: bulk|junk|list"
172 # unless they explicitly "X-Ack: yes", but not all mail 'bots are
173 # correctly configured, so we max out the number of responses we'll
174 # give to an address in a single day.
176 # BAW: We wait until now to make this decision since our sender may
177 # not be self.msg.get_sender(), but I'm not sure this is right.
178 recip = self.returnaddr or self.msg.get_sender()
179 if not autorespond_to_sender(self.mlist, recip, self.msgdata['lang']):
180 return
181 msg = Message.UserNotification(
182 recip,
183 self.mlist.GetBouncesEmail(),
184 _('The results of your email commands'),
185 lang=self.msgdata['lang'])
186 msg.set_type('multipart/mixed')
187 msg.attach(results)
188 orig = MIMEMessage(self.msg)
189 msg.attach(orig)
190 msg.send(self.mlist)
194 class CommandRunner(Runner):
195 QDIR = config.CMDQUEUE_DIR
197 def _dispose(self, mlist, msg, msgdata):
198 # The policy here is similar to the Replybot policy. If a message has
199 # "Precedence: bulk|junk|list" and no "X-Ack: yes" header, we discard
200 # it to prevent replybot response storms.
201 precedence = msg.get('precedence', '').lower()
202 ack = msg.get('x-ack', '').lower()
203 if ack <> 'yes' and precedence in ('bulk', 'junk', 'list'):
204 log.info('Precedence: %s message discarded by: %s',
205 precedence, mlist.GetRequestEmail())
206 return False
207 # Do replybot for commands
208 mlist.Load()
209 Replybot.process(mlist, msg, msgdata)
210 if mlist.autorespond_requests == 1:
211 log.info('replied and discard')
212 # w/discard
213 return False
214 # Now craft the response
215 res = Results(mlist, msg, msgdata)
216 # This message will have been delivered to one of mylist-request,
217 # mylist-join, or mylist-leave, and the message metadata will contain
218 # a key to which one was used.
219 if msgdata.get('torequest'):
220 res.process()
221 elif msgdata.get('tojoin'):
222 res.do_command('join')
223 elif msgdata.get('toleave'):
224 res.do_command('leave')
225 elif msgdata.get('toconfirm'):
226 mo = re.match(config.VERP_CONFIRM_REGEXP, msg.get('to', ''))
227 if mo:
228 res.do_command('confirm', (mo.group('cookie'),))
229 res.send_response()
230 config.db.commit()