diff options
| -rw-r--r-- | Mailman/ListAdmin.py | 363 |
1 files changed, 208 insertions, 155 deletions
diff --git a/Mailman/ListAdmin.py b/Mailman/ListAdmin.py index 6db0d405b..65d260e8e 100644 --- a/Mailman/ListAdmin.py +++ b/Mailman/ListAdmin.py @@ -14,140 +14,169 @@ # 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. -"""Mixin class which handles of 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). -# When an operation can't be completed, and is sent to the list admin for -# Handling, we consider that an error condition, and raise MMNeedApproval +""" import os -import time import string -import Errors -import Message -import Utils +import time +import anydbm +import marshal + +from Mailman.pythonlib.StringIO import StringIO +from Mailman import Message +from Mailman import mm_cfg +from Mailman import Utils +from Mailman import Errors + class ListAdmin: def InitVars(self): - # Non-configurable data: - self.requests = {} - self.next_request_id = 1 + # non-configurable data + self.requests = {} + self.next_request_id = 1 - def AddRequest(self, request, *args): - now = time.time() - request_id = self.GetRequestId() - if not self.requests.has_key(request): - self.requests[request] = [(request_id, now) + args] - else: - self.requests[request].append( (request_id, now) + args ) - self.Save() - if request == 'add_member': - who = args[1] - self.LogMsg("vette", ("%s: Subscribe request: %s" - % (self.real_name, who))) - if self.admin_immed_notify: - subj = 'New %s subscription request: %s' % (self.real_name, - who) - text = Utils.maketext( - 'subauth.txt', - {'username' : who, - 'listname' : self.real_name, - 'hostname' : self.host_name, - 'admindb_url': self.GetAbsoluteScriptURL('admindb'), - }) - self.SendTextToUser(subject = subj, - recipient = self.GetAdminEmail(), - text = text) - raise Errors.MMNeedApproval, "Admin approval required to subscribe" + def InitTempVars(self): + self.__db = None + self.__dbflags = None - elif request == 'post': - sender = args[0][0] - reason = args[1] - subject = args[2] - self.LogMsg("vette", ("%s: %s post hold\n\t%s" - % (self.real_name, sender, `reason`))) - if self.admin_immed_notify: - subj = '%s post approval required for %s' % (self.real_name, - sender) - text = Utils.maketext( - 'postauth.txt', - {'listname' : self.real_name, - 'hostname' : self.host_name, - 'reason' : reason, - 'sender' : sender, - 'subject' : subject, - 'admindb_url': self.GetAbsoluteScriptURL('admindb'), - }) - self.SendTextToUser(subject = subj, - recipient = self.GetAdminEmail(), - text = text) - raise Errors.MMNeedApproval, args[1] + def __opendb(self, flags='r'): + if self.__db is not None: + # if we want the database writeable, but it currently is not, then + # close it and re-open it with the given flags. we don't need to + # re-open it if it's already open writeable and we just want to + # read it. + if 'w' in self.__dbflags or 'w' not in flags: + return + self.__closedb() + dbfilename = os.path.join(self.fullpath(), 'request.db') + # For all writes we need to make sure the database is locked. We + # assert this by using the list's lock as the database lock. + if 'w' in flags: + assert self.Locked() + try: + omask = os.umask(002) + try: + self.__db = anydbm.open(dbfilename, flags) + finally: + os.umask(omask) + except anydbm.error: + # perhaps the db file exists but is zero sized? + from stat import ST_SIZE + if os.stat(dbfilename)[ST_SIZE] == 0: + os.unlink(dbfilename) + omask = os.umask(002) + try: + self.__db = anydbm.open(dbfilename, flags) + finally: + os.umask(omask) + else: + raise + self.__dbflags = flags - def CleanRequests(self): - for (key, val) in self.requests.items(): - if not len(val): - del self.requests[key] + def __closedb(self): + if self.__db is not None: + self.__db.close() + self.__db = None - def GetRequest(self, id): - for (key, val) in self.requests.items(): - for i in range(len(val)): - if val[i][0] == id: - return (key, i) - raise Errors.MMBadRequestId + def __request_id(self): + id = self.next_request_id + self.next_request_id = self.next_request_id + 1 + return id - def RemoveRequest(self, id): - for (key, val) in self.requests.items(): - for item in val: - if item[0] == id: - val.remove(item) - return - raise Errors.MMBadRequestId + def SaveRequestsDb(self): + if self.__db and 'w' in self.__dbflags: + self.__closedb() def NumRequestsPending(self): - self.CleanRequests() - total = 0 - for (k,v) in self.requests.items(): - total = total + len(v) - return total + return len(self.requests) - def HandleRequest(self, request_info, value, comment=None): - request = request_info[0] - index = request_info[1] - request_data = self.requests[request][index] - if request == 'add_member': - self.HandleAddMemberRequest(request_data[2:], value, comment) - elif request == 'post': - self.HandlePostRequest(request_data[2:], value, comment) - self.RemoveRequest(request_data[0]) + def __getmsgids(self, rtype): + ids = [] + for k, v in self.requests.items(): + if v == rtype: + ids.append(k) + return ids - def HandlePostRequest(self, data, value, comment): - destination_email = data[0][0] - msg = Message.IncomingMessage(data[0][1]) - rejection = None - if not value: - # Accept. - self.Post(msg, 1) - return - elif value == 1: - # Reject. - rejection = "Refused" - subj = msg.getheader('subject') - if subj == None: - request = 'Posting of your untitled message' - else: - request = ('Posting of your message entitled:\n\t\t %s' - % subj) - if not comment: - comment = data[1] - if not self.dont_respond_to_post_requests: - self.RefuseRequest(request, destination_email, - comment, msg) - else: - # Discard. - rejection = "Discarded" + def GetHeldMessageIds(self): + return self.__getmsgids(mm_cfg.HELDMSG) + + def GetSubscriptionIds(self): + return self.__getmsgids(mm_cfg.SUBSCRIPTION) + + def GetRecord(self, id): + self.__opendb() + data = self.__db[`id`] + return marshal.loads(data) + + def GetRecordType(self, id): + return self.requests[id] + def HandleRequest(self, id, value, comment): + self.__opendb('w') + record = self.GetRecord(id) + rtype = self.GetRecordType(id) + del self.__db[`id`] + del self.requests[id] + if rtype == mm_cfg.HELDMSG: + self.__handlepost(record, value, comment) + else: + assert rtype == mm_cfg.SUBSCRIPTION + self.__handlesubscription(record, value, comment) + + def HoldMessage(self, msg, reason): + # assure that the database is open for writing + self.__opendb('cw') + # get the next unique id + id = self.__request_id() + dbid = `id` + assert not self.__db.has_key(dbid) + # flatten the message and suck out the sender address + sender, text = Utils.SnarfMessage(msg) + # 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 + # the full text of the message + # + msgsubject = msg.get('subject', '(no subject)') + data = marshal.dumps((time.time(), sender, msgsubject, reason, text)) + self.__db[dbid] = data + # + # remember the request type for later + self.requests[id] = mm_cfg.HELDMSG + + def __handlepost(self, record, value, comment): + ptime, sender, subject, reason, text = record + rejection = None + if value == 0: + # Approved + msg = Message.Message(StringIO(text)) + msg.approved = 1 + self.Post(msg) + elif value == 1: + # Rejected + rejection = 'Refused' + if not self.dont_respond_to_post_requests: + self.__refuse('Posting of your message titled "%s"' % subject, + sender, comment or '[No reason given]') + else: + assert value == 2 + # Discarded + rejection = 'Discarded' + # Log the rejection def strquote(s): return string.replace(s, '%', '%%') @@ -155,59 +184,83 @@ class ListAdmin: note = '''%(listname)s: %(rejection)s posting: \tFrom: %(sender)s \tSubject: %(subject)s''' % { - 'listname' : self._internal_name, + 'listname' : self.internal_name(), 'rejection': rejection, - 'sender' : msg.GetSender(), - 'subject' : strquote(msg.getheader('subject', '<none>')), + 'sender' : sender, + 'subject' : strquote(subject), } - if data[1]: - note = note + '\n\tHeld: ' + strquote(data[1]) - if comment: - note = note + '\n\tDiscarded: ' + strquote(comment) - self.LogMsg("vette", note) - - def HandleAddMemberRequest(self, data, value, comment): - digest = data[0] - destination_email = data[1] - pw = data[2] - if value == 0: - if digest: - digest_text = 'digest' - else: - digest_text = 'nodigest' - self.RefuseRequest('subscribe %s %s' % (pw, digest_text), - destination_email, comment) - else: - try: - self.ApprovedAddMember(destination_email, pw, digest) - self.Save() - except Errors.MMAlreadyAMember: - pass + if comment: + note = note + '\n\tReason: ' + strquote(comment) + self.LogMsg('vette', note) + def HoldSubscription(self, addr, password, digest): + # assure that the database is open for writing + self.__opendb('cw') + # get the next unique id + id = self.__request_id() + dbid = `id` + assert not self.__db.has_key(dbid) + # + # 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 = marshal.dumps((time.time(), addr, password, digest)) + self.__db[dbid] = data + # + # remember the request type for later + self.requests[id] = mm_cfg.SUBSCRIPTION + # + # TBD: this really shouldn't go here but I'm not sure where else is + # appropriate. + self.LogMsg('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.GetAbsoluteScriptURL('admindb'), + }) + self.SendTextToUser(subject=subject, + recipient=self.GetAdminEmail(), + text=text) -# Don't call any methods below this point from outside this mixin. + def __handlesubscription(self, record, value, comment): + stime, addr, password, digest = record + if value == 0: + # refused + self.__refuse('Subscription request', addr, comment) + else: + # subscribe + assert value == 1 + self.ApprovedAddMember(addr, password, digest) - def GetRequestId(self): - id = self.next_request_id - self.next_request_id = self.next_request_id + 1 - # No need to save, we know it's about to be done. - return id - def RefuseRequest(self, request, destination_email, comment, msg=None): + def __refuse(self, request, recip, comment, msg=None): text = Utils.maketext( 'refuse.txt', {'listname' : self.real_name, 'request' : request, - 'reason' : comment or '[No reason given]', + 'reason' : comment, 'adminaddr': self.GetAdminEmail(), }) # add in original message, but not wrap/filled if msg: - text = text + string.join(msg.headers, '') + '\n\n' + msg.body - else: - text = text + '[Original message unavailable]' - # send it - self.SendTextToUser(subject = '%s request rejected' % self.real_name, - recipient = destination_email, - text = text) + text = text + \ + '\n---------- Original Message ----------\n' + \ + Utils.SnarfMessage(msg)[1] + subject = 'Request to mailing list %s rejected' % self.real_name + self.SendTextToUser(subject=subject, + recipient=recip, + text=text) |
