# Copyright (C) 1998 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. """Process mailing list user commands arriving via email.""" # Try to stay close to majordomo commands, but accept common mistakes. # Not implemented: get / index / which. import os import sys import string import re import traceback import Message import Errors import mm_cfg import Utils import MailList try: from cStringIO import StringIO except ImportError: from StringIO import StringIO MAXERRORS = 5 MAXCOLUMN = 70 option_descs = { 'hide' : '''When turned on, your email address is concealed on the Web page that lists the members of the mailing list.''', 'nomail' : '''When turned on, delivery to your email address is disabled, but your address is still subscribed. This is useful if you plan on taking a short vacation.''', 'ack' : '''When turned on, you get a separate acknowledgement email when you post messages to the list.''', 'notmetoo': '''When turned on, you do *not* get copies of your own posts to the list. Otherwise, you do get copies of your own posts (yes, this seems a little backwards). This does not affect the contents of digests, so if you receive postings in digests, you will always get copies of your messages in the digest.''', 'digest' : '''When turned on, you get postings from the list bundled into digests. Otherwise, you get each individual message immediately as it is posted to the list.''', 'plain' : """When turned on, you get `plain' digests, which are actually formatted using the older RFC934 digest format. This format can be easier to read if you have a non-MIME compliant mail reader. When this option is turned off, you get digests in MIME format, which are much better if you have a mail reader that supports MIME.""", } option_info = {'digest' : 0, 'nomail' : mm_cfg.DisableDelivery, 'notmetoo': mm_cfg.DontReceiveOwnPosts, 'ack' : mm_cfg.AcknowlegePosts, 'plain' : mm_cfg.DisableMime, 'hide' : mm_cfg.ConcealSubscription } # ordered list options = ('hide', 'nomail', 'ack', 'notmetoo', 'digest', 'plain') class MailCommandHandler: def __init__(self): self.__errors = 0 self.__respbuf = '' self.__dispatch = { 'subscribe' : self.ProcessSubscribeCmd, 'confirm' : self.ProcessConfirmCmd, 'unsubscribe' : self.ProcessUnsubscribeCmd, 'who' : self.ProcessWhoCmd, 'info' : self.ProcessInfoCmd, 'lists' : self.ProcessListsCmd, 'help' : self.ProcessHelpCmd, 'set' : self.ProcessSetCmd, 'options' : self.ProcessOptionsCmd, 'password' : self.ProcessPasswordCmd, } self.__NoMailCmdResponse = 0 def AddToResponse(self, text, trunc=MAXCOLUMN, prefix=""): # Strip final newline if text and text[-1] == '\n': text = text[:-1] for line in string.split(text, '\n'): line = prefix + line if trunc and len(line) > trunc: line = line[:trunc-3] + '...' self.__respbuf = self.__respbuf + line + "\n" def AddError(self, text, prefix='>>>>> ', trunc=MAXCOLUMN): self.__errors = self.__errors + 1 self.AddToResponse(text, trunc=trunc, prefix=prefix) def ParseMailCommands(self): msg = Message.IncomingMessage() subject = msg.getheader("subject") sender = string.lower(msg.GetSender()) sender = string.split(sender, "@")[0] # # XXX: why 'orphanage'? if sender in ['daemon', 'nobody', 'mailer-daemon', 'postmaster', 'orphanage', 'postoffice']: # # This is for what are probably delivery-failure notices of # subscription confirmations that are, of necessity, bounced # back to the -request address. self.LogMsg("bounce", ("%s: Mailcmd rejected" "\n\tReason: Probable bounced subscribe-confirmation" "\n\tFrom: %s" "\n\tSubject: %s" ), self._internal_name, msg.getheader('from'), subject) return if subject: subject = string.strip(subject) if (subject and self.__dispatch.has_key(string.lower(string.split(subject)[0]))): lines = [subject] + string.split(msg.body, '\n') else: lines = string.split(msg.body, '\n') if subject: # # check to see if confirmation request -- special handling conf_pat = (r'%s\s+--\s+confirmation\s+of\s+subscription' r'\s+--\s+request\s+(\d{6})' % re.escape(self.real_name)) mo = re.search(conf_pat, subject, re.IGNORECASE) if not mo: mo = re.search(conf_pat, msg.body) if mo: lines = ["confirm %s" % (mo.group(1))] else: self.AddError('Subject line ignored:\n ' + subject) processed = {} # For avoiding redundancies. maxlines = mm_cfg.DEFAULT_MAIL_COMMANDS_MAX_LINES for linecount in range(len(lines)): if linecount > maxlines: self.AddError("Maximum command lines (%d) encountered," " ignoring the rest..." % maxlines) for line in lines[linecount:]: self.AddToResponse("> " + line, trunc=0) break line = string.strip(lines[linecount]) if not line: continue args = string.split(line) cmd = string.lower(args[0]) args = args[1:] if cmd in ['end', '--']: self.AddToResponse('\n***** End: ' + line + '\n' 'The rest of the message is ignored:') for line in lines[linecount+1:]: self.AddToResponse(line, trunc=0, prefix='> ') break if not self.__dispatch.has_key(cmd): self.AddError(line, prefix='Command? ') if self.__errors >= MAXERRORS: self.AddError('\nToo many errors encountered; ' 'the rest of the message is ignored:') for line in lines[linecount+1:]: self.AddToResponse(line, trunc=0, prefix='> ') break else: # We do not repeat identical commands. (Eg, it's common # with other mlm's for people to put a command in the # subject and the body, uncertain which one has effect...) isdup = 0 if not processed.has_key(cmd): processed[cmd] = [] else: for did in processed[cmd]: if args == did: isdup = 1 break if not isdup: processed[cmd].append(args) self.AddToResponse('\n***** ' + line) try: # note that all expected exceptions must be handled by # the dispatch function. We'll globally collect any # unexpected (e.g. uncaught) exceptions here. Such an # exception stops parsing of email commands # immediately self.__dispatch[cmd](args, line, msg) except: admin = self.GetAdminEmail() sfp = StringIO() traceback.print_exc(file=sfp) tbmsg = sfp.getvalue() errmsg = Utils.wrap('''\ An unexpected Mailman error has occurred. Please forward your request to the human list administrator in charge of this list at <%s>. The traceback is attached below and will be forwarded to the list administrator automatically.''' % admin) self.AddError(errmsg, trunc=0) self.AddToResponse('\n' + tbmsg, trunc=0) # log it to the error file self.LogMsg('error', 'Unexpected Mailman error:\n%s' % tbmsg) # and send the traceback to the user self.SendTextToUser(subject='Unexpected Mailman error', recipient=admin, text='''\ An unexpected Mailman error has occurred in MailCommandHandler.ParseMailCommands(). Here is the traceback: ''' + tbmsg, add_headers=['Errors-To: %s' % admin, 'X-No-Archive: yes', 'Precedence: bulk']) break # send the response if not self.__NoMailCmdResponse: if self.__errors > 0: header = Utils.wrap('''This is an automated response. There were problems with the email commands you sent to Mailman via the administrative address <%(sender)s>. To obtain instructions on valid Mailman email commands, send email to <%(sender)s> with the word "help" in the subject line or in the body of the message. If you want to reach the human being that manages this mailing list, please send your message to <%(admin)s>. The following is a detailed description of the problems. ''' % {'sender': self.GetRequestEmail(), 'admin' : self.GetAdminEmail(), }) self.__respbuf = header + self.__respbuf self.SendMailCmdResponse(msg) def SendMailCmdResponse(self, mail): subject = 'Mailman results for %s' % self.real_name self.SendTextToUser(subject = subject, recipient = mail.GetSender(), sender = self.GetRequestEmail(), text = self.__respbuf) self.__respbuf = '' self.__errors = 0 self.__NoMailCmdResponse = 0 def ProcessPasswordCmd(self, args, cmd, mail): if len(args) <> 2: self.AddError("Usage: password ") return sender = mail.GetSender() try: self.ConfirmUserPassword(sender, args[0]) self.ChangeUserPassword(sender, args[1], args[1]) self.AddToResponse('Succeeded.') except Errors.MMListNotReady: self.AddError("List is not functional.") except Errors.MMNotAMemberError: self.AddError("%s isn't subscribed to this list." % sender, trunc=0) except Errors.MMBadPasswordError: self.AddError("You gave the wrong password.") except Errors.MMBadUserError: self.AddError("Bad user - %s." % sender, trunc=0) def ProcessOptionsCmd(self, args, cmd, mail): sender = self.FindUser(mail.GetSender()) if not sender: self.AddError("%s is not a member of the list." % mail.GetSender(), trunc=0) return for option in options: if self.GetUserOption(sender, option_info[option]): value = 'on' else: value = 'off' self.AddToResponse('%8s: %s' % (option, value)) self.AddToResponse("\n" "To change an option, do: " "set