# 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 maillist 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): # only need one newline if text and text[-1] == '\n': text = text[:-1] if trunc and len(text) > trunc: text = text[:trunc-3] + '...' self.__respbuf = self.__respbuf + text + "\n" def AddError(self, text, prefix='>>>>> ', trunc=MAXCOLUMN): self.__errors = self.__errors + 1 self.AddToResponse(prefix + text, trunc) def ParseMailCommands(self): msg = Message.IncomingMessage() subject = msg.getheader("subject") sender = string.lower(msg.GetSender()) # # XXX: shouldn't this be checking the username only part? # 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.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 -- confirmation of subscription' r' -- request (\d{6})' % re.escape(self.real_name)) mo = re.search(conf_pat, subject) if not mo: mo = re.search(conf_pat, msg.body) if mo: lines = ["confirm %s" % (mo.group(1))] else: self.AddError("Subject line ignored: %s" % subject, trunc=0) processed = {} # For avoiding redundancies. maxlines = mm_cfg.DEFAULT_MAIL_COMMANDS_MAX_LINES for linecount in range(len(lines)): line = string.strip(lines[linecount]) if not line: continue if linecount > maxlines: self.AddError("Maximum command lines (%d) encountered," " ignoring the rest..." % maxlines) self.AddToResponse("<<< " + string.join(lines[linecount:], "\n<<< ")) break ## self.AddToResponse("\n>>>> %s" % line) args = string.split(line) cmd = string.lower(args[0]) args = args[1:] if cmd in ['end', '--']: self.AddToResponse('\n***** End: ' + line) self.AddToResponse('\nThe rest of the message is ignored:') for line in lines[linecount+1:]: self.AddToResponse('> ' + line, 0) break if not self.__dispatch.has_key(cmd): self.AddError(line, prefix='Command? ') if self.__errors >= MAXERRORS: self.AddToResponse('') self.AddError('''\ Too many errors encountered; the rest of the message is ignored:''') for line in lines[linecount+1:]: self.AddToResponse('> ' + line, 0) break ## self.AddError("%s: Command UNKNOWN." % cmd) 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) for line in string.split(errmsg, '\n'): self.AddError(line, trunc=0) self.AddError('') self.AddToResponse(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. 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) except Errors.MMBadPasswordError: self.AddError("You gave the wrong password.") except Errors.MMBadUserError: self.AddError("Bad user - %s." % sender) 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()) 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("") self.AddToResponse("To change an option, do: " "set