# Copyright (C) 1998,1999,2000,2001 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 re import traceback from Mailman import Message from Mailman import Errors from Mailman import mm_cfg from Mailman import Utils from Mailman.Logging.Syslog import syslog from Mailman.pythonlib.StringIO import StringIO import gettext MAXERRORS = 5 MAXCOLUMN = 70 NL = '\n' # jcrey: I must to do a trick to make pygettext detect this strings. First we # define a fake function. Idea taken from Mads. def _(s): return s option_descs = { 'hide' : _('When turned on, your email address is concealed\n' 'on the Web page that lists the members of the mailing list.'), 'nomail' : _('When turned on, delivery to your email address is disabled,\n' ' but your address is still subscribed. This is useful if you plan on\n' ' taking a short vacation.'), 'ack' : _('When turned on, you get a separate acknowledgement email\n' 'when you post messages to the list.'), 'notmetoo': _('When turned on, you do *not* get copies of your own\n' 'posts to the list. Otherwise, you do get copies of your own posts\n' ' (yes, this seems a little backwards). This does not affect the contents\n' 'of digests, so if you receive postings in digests, you will always get\n' 'copies of your messages in the digest.'), 'digest' : _('When turned on, you get postings from the list bundled\n' 'into digests. Otherwise, you get each individual message immediately as\n' 'it is posted to the list.'), 'plain' : _("When turned on, you get `plain' digests, which are actually\n" "formatted using the older RFC934 digest format. This format can be easier\n" "to read if you have a non-MIME compliant mail reader. When this option is\n" "turned off, you get digests in MIME format, which are much better if you\n" "have a mail reader that supports MIME."), } # jcrey: and then the real one _=gettext.gettext option_info = {'digest' : 0, 'nomail' : mm_cfg.DisableDelivery, 'notmetoo': mm_cfg.DontReceiveOwnPosts, 'ack' : mm_cfg.AcknowledgePosts, 'plain' : mm_cfg.DisableMime, 'hide' : mm_cfg.ConcealSubscription } # ordered list options = ('hide', 'nomail', 'ack', 'notmetoo', 'digest', 'plain') # strip just the outer layer of quotes quotecre = re.compile(r'["\'`](?P.*)["\'`]') 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 text.split('\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): # Break any infloops. If this has come from a Mailman server then # it'll have this header. It's still possible to infloop between two # servers because there's no guaranteed way to know it came from a # bot. if msg.get('x-beenthere') or msg.get('list-id'): return # check the autoresponse stuff if self.autorespond_requests: # TBD: this is a hack and is not safe with respect to errors in # the Replybot module. It should be redesigned to work with the # robust delivery scheme. from Mailman.Handlers import Replybot Replybot.process(self, msg, msgdata={'torequest':1}) if self.autorespond_requests == 1: # Yes, auto-respond and discard return subject = msg.getheader("subject") sender = msg.get_sender().lower() os.environ['LANG'] = self.GetPreferredLanguage(sender) sender = sender.split('@')[0] # # XXX: why 'orphanage'? if sender in mm_cfg.LIKELY_BOUNCE_SENDERS: # This is for what are probably delivery-failure notices of # subscription confirmations that are, of necessity, bounced # back to the -request address. syslog("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 = subject.strip() # remove quotes so "help" works mo = quotecre.search(subject) if mo: subject = mo.group('cmd') if (subject and self.__dispatch.has_key(subject.split()[0].lower())): # FIXME lines = [subject] + msg.body.split('\n') else: # FIXME lines = msg.body.split('\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 = lines[linecount].strip() if not line: continue args = line.split() cmd = args[0].lower() # remove quotes so "help" or `help' works mo = quotecre.search(cmd) if mo: cmd = mo.group('cmd') 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 syslog('error', _('Unexpected Mailman error:\n%s') % tbmsg) # and send the traceback to the user responsemsg = Message.UserNotification( admin, admin, 'Unexpected Mailman error', _('''\ An unexpected Mailman error has occurred in MailCommandHandler.ParseMailCommands(). Here is the traceback: ''') + tbmsg) responsemsg['X-No-Archive'] = 'yes' responsemsg.send(self) break # send the response if not self.__NoMailCmdResponse: adminaddr = self.GetAdminEmail() requestaddr = self.GetRequestEmail() 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': requestaddr, 'admin' : adminaddr, }) self.__respbuf = header + self.__respbuf # send the response subject = _('Mailman results for %s') % self.real_name responsemsg = Message.UserNotification(msg.GetSender(), self.GetRequestEmail(), subject, self.__respbuf) responsemsg.send(self) self.__respbuf = '' self.__errors = 0 self.__NoMailCmdResponse = 0 def ProcessPasswordCmd(self, args, cmd, mail): if len(args) not in [0,2]: self.AddError(_("Usage: password [ ]")) return sender = mail.GetSender() if len(args) == 0: # Mail user's password to user user = self.FindUser(sender) if user and self.passwords.has_key(user): self.AddToResponse(_("You are subscribed as %s,\n" " with password: %s") % (user, self.passwords[user]), trunc=0) else: self.AddError(_("Found no password for %s") %sender, trunc=0) return # Try to change password try: self.ConfirmUserPassword(sender, args[0]) self.ChangeUserPassword(sender, args[1], args[1]) self.AddToResponse(_('Succeeded.')) except Errors.MMListNotReadyError: 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