# Copyright (C) 1998,1999,2000 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. """Mixin class for MailList which handles administrative requests. Two types of admin requests are currently supported: adding members to a closed or semi-closed list, and moderated posts. Pending subscriptions which are requiring a user's confirmation are handled elsewhere (currently). """ import os import string import time import marshal import errno from Mailman import Message from Mailman import mm_cfg from Mailman import Utils from Mailman import Errors from Mailman.Handlers import HandlerAPI from Mailman.Logging.Syslog import syslog from Mailman.pythonlib.StringIO import StringIO # Request types requiring admin approval HELDMSG = 1 SUBSCRIPTION = 2 # Return status from __handlepost() DEFER = 0 REMOVE = 1 LOST = 2 class ListAdmin: def InitVars(self): # non-configurable data self.next_request_id = 1 def InitTempVars(self): self.__db = None self.__filename = None fullpath = self.fullpath() if fullpath: self.__filename = os.path.join(fullpath, 'request.db') def __opendb(self): if self.__db is None: assert self.Locked() and self.__filename try: fp = open(self.__filename) self.__db = marshal.load(fp) fp.close() except IOError, e: if e.errno <> errno.ENOENT: raise self.__db = {} def __closedb(self): if self.__db is not None: assert self.Locked() omask = os.umask(002) try: fp = open(self.__filename, 'w') marshal.dump(self.__db, fp) fp.close() self.__db = None finally: os.umask(omask) def __request_id(self): id = self.next_request_id self.next_request_id = self.next_request_id + 1 return id def SaveRequestsDb(self): self.__closedb() def NumRequestsPending(self): self.__opendb() return len(self.__db) def __getmsgids(self, rtype): self.__opendb() ids = [] for k, (type, data) in self.__db.items(): if type == rtype: ids.append(k) ids.sort() return ids def GetHeldMessageIds(self): return self.__getmsgids(HELDMSG) def GetSubscriptionIds(self): return self.__getmsgids(SUBSCRIPTION) def GetRecord(self, id): self.__opendb() type, data = self.__db[id] return data def GetRecordType(self, id): self.__opendb() type, data = self.__db[id] return type def HandleRequest(self, id, value, comment=None, preserve=None, forward=None, addr=None): self.__opendb() rtype, data = self.__db[id] if rtype == HELDMSG: status = self.__handlepost(data, value, comment, preserve, forward, addr) else: assert rtype == SUBSCRIPTION status = self.__handlesubscription(data, value, comment) if status: del self.__db[id] def HoldMessage(self, msg, reason, msgdata={}): # Make a copy of msgdata so that subsequent changes won't corrupt the # request database. TBD: remove the `filebase' key since this will # not be relevant when the message is resurrected. newmsgdata = {} newmsgdata.update(msgdata) msgdata = newmsgdata # assure that the database is open for writing self.__opendb() # get the next unique id id = self.__request_id() assert not self.__db.has_key(id) # get the message sender sender = msg.GetSender() # calculate the file name for the message text and write it to disk filename = 'heldmsg-%s-%d.txt' % (self.internal_name(), id) omask = os.umask(002) try: fp = open(os.path.join(mm_cfg.DATA_DIR, filename), 'w') fp.write(repr(msg)) fp.close() finally: os.umask(omask) # save the information to the request database. for held message # entries, each record in the database will be of the following # format: # # the time the message was received # the sender of the message # the message's subject # a string description of the problem # name of the file in $PREFIX/data containing the msg text # an additional dictionary of message metadata # msgsubject = msg.get('subject', _('(no subject)')) data = time.time(), sender, msgsubject, reason, filename, msgdata self.__db[id] = (HELDMSG, data) def __handlepost(self, record, value, comment, preserve, forward, addr): # For backwards compatibility with pre 2.0beta3 if len(record) == 5: ptime, sender, subject, reason, filename = record msgdata = {} else: # New format of record ptime, sender, subject, reason, filename, msgdata = record path = os.path.join(mm_cfg.DATA_DIR, filename) # Handle message preservation if preserve: parts = string.split(os.path.split(path)[1], '-') parts[0] = 'spam' spamfile = string.join(parts, '-') import shutil try: shutil.copy(path, os.path.join(mm_cfg.SPAM_DIR, spamfile)) except IOError, e: if e.errno <> errno.ENOENT: raise return LOST # Now handle updates to the database rejection = None msg = None status = REMOVE if value == mm_cfg.DEFER: # Defer status = DEFER elif value == mm_cfg.APPROVE: # Approved try: fp = open(path) except IOError, e: if e.errno <> errno.ENOENT: raise return LOST msg = Message.Message(fp) msgdata['approved'] = 1 # Calculate a new filebase for the approved message, otherwise # delivery errors will cause duplicates. try: del msgdata['filebase'] except KeyError: pass # Queue the file for delivery by qrunner. Trying to deliver the # message directly here can lead to a huge delay in web # turnaround. syslog('vette', 'approved held message enqueued: %s' % filename) msg.Enqueue(self, newdata=msgdata) elif value == mm_cfg.REJECT: # Rejected rejection = 'Refused' self.__refuse(_('Posting of your message titled "%s"') % subject, sender, comment or _('[No reason given]')) else: assert value == mm_cfg.DISCARD # Discarded rejection = 'Discarded' # # Forward the message if forward and addr: # If we've approved the message, we need to be sure to craft a # completely unique second message for the forwarding operation, # since we don't want to share any state or information with the # normal delivery. if msg: fp.seek(0) msg = Message.Message(fp) else: try: fp = open(path) except IOError, e: if e.errno <> errno.ENOENT: raise raise Errors.LostHeldMessage(path) msg = Message.Message(fp) # We don't want this message getting delivered to the list twice. # This should also uniquify the message enough for the hash-based # file naming (not foolproof though). msg['Resent-To'] = addr HandlerAPI.DeliverToUser(self, msg, {'_enqueue_immediate': 1, 'recips': [addr]}) # for safety def strquote(s): return string.replace(s, '%', '%%') # # Log the rejection if rejection: note = '''%(listname)s: %(rejection)s posting: \tFrom: %(sender)s \tSubject: %(subject)s''' % { 'listname' : self.internal_name(), 'rejection': rejection, 'sender' : strquote(sender), 'subject' : strquote(subject), } if comment: note = note + '\n\tReason: ' + strquote(comment) syslog('vette', note) # Always unlink the file containing the message text. It's not # necessary anymore, regardless of the disposition of the message. if status <> DEFER: try: os.unlink(path) except OSError, e: if e.errno <> errno.ENOENT: raise # We lost the message text file. Clean up our housekeeping # and raise an exception. return LOST return status def HoldSubscription(self, addr, password, digest): # assure that the database is open for writing self.__opendb() # get the next unique id id = self.__request_id() assert not self.__db.has_key(id) # # save the information to the request database. for held subscription # entries, each record in the database will be one of the following # format: # # the time the subscription request was received # the subscriber's address # the subscriber's selected password (TBD: is this safe???) # the digest flag # data = time.time(), addr, password, digest self.__db[id] = (SUBSCRIPTION, data) # # TBD: this really shouldn't go here but I'm not sure where else is # appropriate. syslog('vette', '%s: held subscription request from %s' % (self.real_name, addr)) # possibly notify the administrator if self.admin_immed_notify: subject = _('New subscription request to list %s from %s') % ( self.real_name, addr) text = Utils.maketext( 'subauth.txt', {'username' : addr, 'listname' : self.real_name, 'hostname' : self.host_name, 'admindb_url': self.GetScriptURL('admindb', absolute=1), }) adminaddr = self.GetAdminEmail() msg = Message.UserNotification(adminaddr, adminaddr, subject, text) HandlerAPI.DeliverToUser(self, msg, {'_enqueue_immediate': 1}) def __handlesubscription(self, record, value, comment): stime, addr, password, digest = record if value == mm_cfg.REJECT: # refused self.__refuse(_('Subscription request'), addr, comment) else: # subscribe assert value == mm_cfg.SUBSCRIBE self.ApprovedAddMember(addr, password, digest) # TBD: disgusting hack: ApprovedAddMember() can end up closing the # request database. self.__opendb() return REMOVE def __refuse(self, request, recip, comment, origmsg=None): adminaddr = self.GetAdminEmail() text = Utils.maketext( 'refuse.txt', {'listname' : self.real_name, 'request' : request, 'reason' : comment, 'adminaddr': adminaddr, }) # add in original message, but not wrap/filled if origmsg: text = string.join([text, '---------- ' + _('Original Message') + ' ----------', str(origmsg)], '\n') subject = _('Request to mailing list %s rejected') % self.real_name msg = Message.UserNotification(recip, adminaddr, subject, text) HandlerAPI.DeliverToUser(self, msg, {'_enqueue_immediate': 1})