------------------------------------------------------------ revno: 6552 committer: Barry Warsaw <[EMAIL PROTECTED]> branch nick: 3.0 timestamp: Sun 2007-09-16 22:10:05 -0400 message: Finish clean up of ListAdmin class removal. Start by actually removing the module. Then, fix a few tests that failed as a result of this work. Mailman/Handlers/Hold.py: Call hold_message() instead of mlist.HoldMessage(). The message store also no longer requires a Date: header, so clean up a few tests that were still expecting that. Extend cleaning_teardown() in test_documentation.py so that both the message store and any list-centric requests are cleaned up after each test. removed: Mailman/ListAdmin.py modified: Mailman/Handlers/Hold.py Mailman/docs/hold.txt Mailman/docs/messagestore.txt Mailman/tests/test_documentation.py
=== removed file 'Mailman/ListAdmin.py' --- a/Mailman/ListAdmin.py 2007-07-03 05:09:53 +0000 +++ b/Mailman/ListAdmin.py 1970-01-01 00:00:00 +0000 @@ -1,494 +0,0 @@ -# Copyright (C) 1998-2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, -# 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. -""" - -from __future__ import with_statement - -import os -import time -import email -import errno -import cPickle -import logging -import marshal - -from cStringIO import StringIO -from email.Generator import Generator -from email.MIMEMessage import MIMEMessage -from email.Utils import getaddresses - -from Mailman import Errors -from Mailman import Message -from Mailman import Utils -from Mailman import i18n -from Mailman.Queue.sbcache import get_switchboard -from Mailman.UserDesc import UserDesc -from Mailman.configuration import config - -_ = i18n._ - -# Request types requiring admin approval -IGN = 0 -HELDMSG = 1 -SUBSCRIPTION = 2 -UNSUBSCRIPTION = 3 - -# Return status from __handlepost() -DEFER = 0 -REMOVE = 1 -LOST = 2 - -DASH = '-' -NL = '\n' - -log = logging.getLogger('mailman.vette') - - - -class ListAdmin: - def InitTempVars(self): - self._db = None - self._filename = os.path.join(self.full_path, 'request.pck') - - def _opendb(self): - if self._db is None: - assert self.Locked() - try: - with open(self._filename) as fp: - self._db = cPickle.load(fp) - except IOError, e: - if e.errno <> errno.ENOENT: - raise - self._db = {} - # put version number in new database - self._db['version'] = IGN, config.REQUESTS_FILE_SCHEMA_VERSION - - def _closedb(self): - if self._db is not None: - assert self.Locked() - # Save the version number - self._db['version'] = IGN, config.REQUESTS_FILE_SCHEMA_VERSION - # Now save a temp file and do the tmpfile->real file dance. BAW: - # should we be as paranoid as for the config.pck file? Should we - # use pickle? - tmpfile = self._filename + '.tmp' - with open(tmpfile, 'w') as fp: - cPickle.dump(self._db, fp, 1) - fp.flush() - os.fsync(fp.fileno()) - self._db = None - # Do the dance - os.rename(tmpfile, self._filename) - - @property - def _next_id(self): - assert self.Locked() - while True: - missing = object() - next = self.next_request_id - self.next_request_id += 1 - if self._db.setdefault(next, missing) is missing: - return next - - def SaveRequestsDb(self): - self._closedb() - - def NumRequestsPending(self): - self._opendb() - # Subtract one for the version pseudo-entry - return len(self._db) - 1 - - def _getmsgids(self, rtype): - self._opendb() - ids = sorted([k for k, (op, data) in self._db.items() if op == rtype]) - return ids - - def GetHeldMessageIds(self): - return self._getmsgids(HELDMSG) - - def GetSubscriptionIds(self): - return self._getmsgids(SUBSCRIPTION) - - def GetUnsubscriptionIds(self): - return self._getmsgids(UNSUBSCRIPTION) - - 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) - elif rtype == UNSUBSCRIPTION: - status = self._handleunsubscription(data, value, comment) - else: - assert rtype == SUBSCRIPTION - status = self._handlesubscription(data, value, comment) - if status <> DEFER: - # BAW: Held message ids are linked to Pending cookies, allowing - # the user to cancel their post before the moderator has approved - # it. We should probably remove the cookie associated with this - # id, but we have no way currently of correlating them. :( - 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. - msgdata = msgdata.copy() - # assure that the database is open for writing - self._opendb() - # get the next unique id - id = self._next_id - # get the message sender - sender = msg.get_sender() - # calculate the file name for the message text and write it to disk - if config.HOLD_MESSAGES_AS_PICKLES: - ext = 'pck' - else: - ext = 'txt' - filename = 'heldmsg-%s-%d.%s' % (self.fqdn_listname, id, ext) - with open(os.path.join(config.DATA_DIR, filename), 'w') as fp: - if config.HOLD_MESSAGES_AS_PICKLES: - cPickle.dump(msg, fp, 1) - else: - g = Generator(fp) - g(msg, 1) - fp.flush() - os.fsync(fp.fileno()) - # 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) - return id - - def _handlepost(self, record, value, comment, preserve, forward, addr): - # For backwards compatibility with pre 2.0beta3 - ptime, sender, subject, reason, filename, msgdata = record - path = os.path.join(config.DATA_DIR, filename) - # Handle message preservation - if preserve: - parts = os.path.split(path)[1].split(DASH) - parts[0] = 'spam' - spamfile = DASH.join(parts) - # Preserve the message as plain text, not as a pickle - try: - with open(path) as fp: - msg = cPickle.load(fp) - except IOError, e: - if e.errno <> errno.ENOENT: - raise - return LOST - # Save the plain text to a .msg file, not a .pck file - outpath = os.path.join(config.SPAM_DIR, spamfile) - head, ext = os.path.splitext(outpath) - outpath = head + '.msg' - with open(outpath, 'w') as outfp: - g = Generator(outfp) - g(msg, 1) - # Now handle updates to the database - rejection = None - fp = None - msg = None - status = REMOVE - if value == config.DEFER: - # Defer - status = DEFER - elif value == config.APPROVE: - # Approved. - try: - msg = readMessage(path) - except IOError, e: - if e.errno <> errno.ENOENT: - raise - return LOST - msg = readMessage(path) - msgdata['approved'] = 1 - # adminapproved is used by the Emergency handler - msgdata['adminapproved'] = 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. Log the moderation and add a header. - msg['X-Mailman-Approved-At'] = email.Utils.formatdate(localtime=1) - log.info('held message approved, message-id: %s', - msg.get('message-id', 'n/a')) - # Stick the message back in the incoming queue for further - # processing. - inq = get_switchboard(config.INQUEUE_DIR) - inq.enqueue(msg, _metadata=msgdata) - elif value == config.REJECT: - # Rejected - rejection = 'Refused' - self._refuse(_('Posting of your message titled "%(subject)s"'), - sender, comment or _('[No reason given]'), - lang=self.getMemberLanguage(sender)) - else: - assert value == config.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. - try: - copy = readMessage(path) - except IOError, e: - if e.errno <> errno.ENOENT: - raise - raise Errors.LostHeldMessage(path) - # It's possible the addr is a comma separated list of addresses. - addrs = getaddresses([addr]) - if len(addrs) == 1: - realname, addr = addrs[0] - # If the address getting the forwarded message is a member of - # the list, we want the headers of the outer message to be - # encoded in their language. Otherwise it'll be the preferred - # language of the mailing list. - lang = self.getMemberLanguage(addr) - else: - # Throw away the realnames - addr = [a for realname, a in addrs] - # Which member language do we attempt to use? We could use - # the first match or the first address, but in the face of - # ambiguity, let's just use the list's preferred language - lang = self.preferred_language - otrans = i18n.get_translation() - i18n.set_language(lang) - try: - fmsg = Message.UserNotification( - addr, self.GetBouncesEmail(), - _('Forward of moderated message'), - lang=lang) - finally: - i18n.set_translation(otrans) - fmsg.set_type('message/rfc822') - fmsg.attach(copy) - fmsg.send(self) - # Log the rejection - if rejection: - note = '''%(listname)s: %(rejection)s posting: -\tFrom: %(sender)s -\tSubject: %(subject)s''' % { - 'listname' : self.internal_name(), - 'rejection': rejection, - 'sender' : str(sender).replace('%', '%%'), - 'subject' : str(subject).replace('%', '%%'), - } - if comment: - note += '\n\tReason: ' + comment.replace('%', '%%') - log.info('%s', 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 inform of this status. - return LOST - return status - - def HoldSubscription(self, addr, fullname, password, digest, lang): - # Assure that the database is open for writing - self._opendb() - # Get the next unique id - id = self._next_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 - # the user's preferred language - data = time.time(), addr, fullname, password, digest, lang - self._db[id] = (SUBSCRIPTION, data) - # - # TBD: this really shouldn't go here but I'm not sure where else is - # appropriate. - log.info('%s: held subscription request from %s', - self.internal_name(), addr) - # Possibly notify the administrator in default list language - if self.admin_immed_notify: - realname = self.real_name - subject = _( - 'New subscription request to list %(realname)s from %(addr)s') - text = Utils.maketext( - 'subauth.txt', - {'username' : addr, - 'listname' : self.internal_name(), - 'hostname' : self.host_name, - 'admindb_url': self.GetScriptURL('admindb', absolute=1), - }, mlist=self) - # This message should appear to come from the <list>-owner so as - # to avoid any useless bounce processing. - owneraddr = self.GetOwnerEmail() - msg = Message.UserNotification(owneraddr, owneraddr, subject, text, - self.preferred_language) - msg.send(self, **{'tomoderators': 1}) - - def __handlesubscription(self, record, value, comment): - stime, addr, fullname, password, digest, lang = record - if value == config.DEFER: - return DEFER - elif value == config.DISCARD: - pass - elif value == config.REJECT: - self._refuse(_('Subscription request'), addr, - comment or _('[No reason given]'), - lang=lang) - else: - # subscribe - assert value == config.SUBSCRIBE - try: - userdesc = UserDesc(addr, fullname, password, digest, lang) - self.ApprovedAddMember(userdesc, whence='via admin approval') - except Errors.MMAlreadyAMember: - # User has already been subscribed, after sending the request - pass - # TBD: disgusting hack: ApprovedAddMember() can end up closing - # the request database. - self._opendb() - return REMOVE - - def HoldUnsubscription(self, addr): - # Assure the database is open for writing - self._opendb() - # Get the next unique id - id = self._next_id - # All we need to do is save the unsubscribing address - self._db[id] = (UNSUBSCRIPTION, addr) - log.info('%s: held unsubscription request from %s', - self.internal_name(), addr) - # Possibly notify the administrator of the hold - if self.admin_immed_notify: - realname = self.real_name - subject = _( - 'New unsubscription request from %(realname)s by %(addr)s') - text = Utils.maketext( - 'unsubauth.txt', - {'username' : addr, - 'listname' : self.internal_name(), - 'hostname' : self.host_name, - 'admindb_url': self.GetScriptURL('admindb', absolute=1), - }, mlist=self) - # This message should appear to come from the <list>-owner so as - # to avoid any useless bounce processing. - owneraddr = self.GetOwnerEmail() - msg = Message.UserNotification(owneraddr, owneraddr, subject, text, - self.preferred_language) - msg.send(self, **{'tomoderators': 1}) - - def _handleunsubscription(self, record, value, comment): - addr = record - if value == config.DEFER: - return DEFER - elif value == config.DISCARD: - pass - elif value == config.REJECT: - self._refuse(_('Unsubscription request'), addr, comment) - else: - assert value == config.UNSUBSCRIBE - try: - self.ApprovedDeleteMember(addr) - except Errors.NotAMemberError: - # User has already been unsubscribed - pass - return REMOVE - - def _refuse(self, request, recip, comment, origmsg=None, lang=None): - # As this message is going to the requestor, try to set the language - # to his/her language choice, if they are a member. Otherwise use the - # list's preferred language. - realname = self.real_name - if lang is None: - lang = self.getMemberLanguage(recip) - text = Utils.maketext( - 'refuse.txt', - {'listname' : realname, - 'request' : request, - 'reason' : comment, - 'adminaddr': self.GetOwnerEmail(), - }, lang=lang, mlist=self) - otrans = i18n.get_translation() - i18n.set_language(lang) - try: - # add in original message, but not wrap/filled - if origmsg: - text = NL.join( - [text, - '---------- ' + _('Original Message') + ' ----------', - str(origmsg) - ]) - subject = _('Request to mailing list %(realname)s rejected') - finally: - i18n.set_translation(otrans) - msg = Message.UserNotification(recip, self.GetBouncesEmail(), - subject, text, lang) - msg.send(self) - - - -def readMessage(path): - # For backwards compatibility, we must be able to read either a flat text - # file or a pickle. - ext = os.path.splitext(path)[1] - with open(path) as fp: - if ext == '.txt': - msg = email.message_from_file(fp, Message.Message) - else: - assert ext == '.pck' - msg = cPickle.load(fp) - return msg === modified file 'Mailman/Handlers/Hold.py' --- a/Mailman/Handlers/Hold.py 2007-08-02 14:47:56 +0000 +++ b/Mailman/Handlers/Hold.py 2007-09-17 02:10:05 +0000 @@ -41,6 +41,7 @@ from Mailman import Message from Mailman import Utils from Mailman import i18n +from Mailman.app.moderator import hold_message from Mailman.configuration import config from Mailman.interfaces import IPendable @@ -228,7 +229,7 @@ # translator again, because of the games we play above reason = Utils.wrap(exc.reason_notice()) msgdata['rejection_notice'] = Utils.wrap(exc.rejection_notice(mlist)) - id = mlist.HoldMessage(msg, reason, msgdata) + id = hold_message(mlist, msg, msgdata, reason) # Now we need to craft and send a message to the list admin so they can # deal with the held message. d = {'listname' : listname, === modified file 'Mailman/docs/hold.txt' --- a/Mailman/docs/hold.txt 2007-08-02 14:47:56 +0000 +++ b/Mailman/docs/hold.txt 2007-09-17 02:10:05 +0000 @@ -142,6 +142,10 @@ >>> print msg.as_string() From: [EMAIL PROTECTED] Subject: An implicit message + Message-ID: ... + X-List-ID-Hash: ... + X-List-Sequence-Number: ... + <BLANKLINE> <BLANKLINE> >>> print msgdata {'fromusenet': True} @@ -276,6 +280,9 @@ MIME-Version: 1.0 <BLANKLINE> From: [EMAIL PROTECTED] + Message-ID: ... + X-List-ID-Hash: ... + X-List-Sequence-Number: ... <BLANKLINE> <BLANKLINE> --... @@ -356,15 +363,17 @@ >>> data = config.db.pendings.confirm(cookie) >>> sorted(data.items()) [('id', '...'), ('type', 'held message')] - >>> filename = '[EMAIL PROTECTED]' % data['id'] - >>> heldmsg = os.path.join(config.DATA_DIR, filename) - -Use helper function to read the held message. - - >>> from Mailman.ListAdmin import readMessage - >>> msg = readMessage(heldmsg) + +The message itself is held in the message store. + + >>> rkey, rdata = config.db.requests.get_list_requests(mlist).get_request( + ... data['id']) + >>> msg = config.db.message_store.get_message(rdata['_mod_global_id']) >>> print msg.as_string() From: [EMAIL PROTECTED] + Message-ID: ... + X-List-ID-Hash: ... + X-List-Sequence-Number: ... <BLANKLINE> <BLANKLINE> === modified file 'Mailman/docs/messagestore.txt' --- a/Mailman/docs/messagestore.txt 2007-08-02 14:47:56 +0000 +++ b/Mailman/docs/messagestore.txt 2007-09-17 02:10:05 +0000 @@ -5,18 +5,18 @@ identifiers. A global id for a message is calculated relative to the message store's base URL and its components are stored as headers on the message. One piece of information is the X-List-ID-Hash, a base-32 encoding of the SHA1 -hash of the message's Message-ID and Date headers, which the message must -have. The second piece of information is supplied by the message store; it -is a sequence number that will uniquely identify the message even when the -X-List-ID-Hash collides. +hash of the message's Message-ID header, which the message must have. The +second piece of information is supplied by the message store; it is a sequence +number that will uniquely identify the message even when the X-List-ID-Hash +collides. >>> from email import message_from_string >>> from Mailman.configuration import config >>> from Mailman.database import flush >>> store = config.db.message_store -If you try to add a message to the store which is missing either the -Message-ID header or the Date header, you will get a ValueError. +If you try to add a message to the store which is missing the Message-ID +header, you will get an exception. >>> msg = message_from_string("""\ ... Subject: An important message @@ -26,26 +26,9 @@ >>> store.add(msg) Traceback (most recent call last): ... - ValueError: Exactly one Message-ID and one Date header required - -Adding a Message-ID header alone doesn't help. - - >>> msg['Message-ID'] = '<[EMAIL PROTECTED]>' - >>> store.add(msg) - Traceback (most recent call last): - ... - ValueError: Exactly one Message-ID and one Date header required - -Neither does adding just a Date header. - - >>> del msg['message-id'] - >>> msg['Date'] = 'Wed, 04 Jul 2007 16:49:58 +0900' - >>> store.add(msg) - Traceback (most recent call last): - ... - ValueError: Exactly one Message-ID and one Date header required - -However, having them both is all you need. + ValueError: Exactly one Message-ID header required + +However, if the message has a Message-ID header, it can be stored. >>> msg['Message-ID'] = '<[EMAIL PROTECTED]>' >>> store.add(msg) @@ -53,9 +36,8 @@ >>> flush() >>> print msg.as_string() Subject: An important message - Date: Wed, 04 Jul 2007 16:49:58 +0900 Message-ID: <[EMAIL PROTECTED]> - X-List-ID-Hash: RXTJ357KFOTJP3NFJA6KMO65X7VQOHJI + X-List-ID-Hash: AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35 X-List-Sequence-Number: 1 <BLANKLINE> This message is very important. @@ -80,9 +62,8 @@ 1 >>> print msgs[0].as_string() Subject: An important message - Date: Wed, 04 Jul 2007 16:49:58 +0900 Message-ID: <[EMAIL PROTECTED]> - X-List-ID-Hash: RXTJ357KFOTJP3NFJA6KMO65X7VQOHJI + X-List-ID-Hash: AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35 X-List-Sequence-Number: 1 <BLANKLINE> This message is very important. @@ -97,9 +78,8 @@ 1 >>> print msgs[0].as_string() Subject: An important message - Date: Wed, 04 Jul 2007 16:49:58 +0900 Message-ID: <[EMAIL PROTECTED]> - X-List-ID-Hash: RXTJ357KFOTJP3NFJA6KMO65X7VQOHJI + X-List-ID-Hash: AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35 X-List-Sequence-Number: 1 <BLANKLINE> This message is very important. @@ -117,9 +97,8 @@ >>> global_id = id_hash + '/' + seqno >>> print store.get_message(global_id).as_string() Subject: An important message - Date: Wed, 04 Jul 2007 16:49:58 +0900 Message-ID: <[EMAIL PROTECTED]> - X-List-ID-Hash: RXTJ357KFOTJP3NFJA6KMO65X7VQOHJI + X-List-ID-Hash: AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35 X-List-Sequence-Number: 1 <BLANKLINE> This message is very important. @@ -137,9 +116,8 @@ 1 >>> print msgs[0].as_string() Subject: An important message - Date: Wed, 04 Jul 2007 16:49:58 +0900 Message-ID: <[EMAIL PROTECTED]> - X-List-ID-Hash: RXTJ357KFOTJP3NFJA6KMO65X7VQOHJI + X-List-ID-Hash: AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35 X-List-Sequence-Number: 1 <BLANKLINE> This message is very important. === modified file 'Mailman/tests/test_documentation.py' --- a/Mailman/tests/test_documentation.py 2007-08-05 12:42:16 +0000 +++ b/Mailman/tests/test_documentation.py 2007-09-17 02:10:05 +0000 @@ -45,6 +45,9 @@ member.unsubscribe() for admin in mlist.administrators.members: admin.unsubscribe() + requestdb = config.db.requests.get_list_requests(mlist) + for request in requestdb.held_requests: + requestdb.delete_request(request.id) listmgr.delete(mlist) flush() assert not list(listmgr.mailing_lists), ( @@ -62,6 +65,16 @@ for style in style_manager.styles: if style.name <> 'default': style_manager.unregister(style) + # Clear the message store. + global_ids = [] + for msg in config.db.message_store.messages: + global_ids.append('%s/%s' % ( + msg['X-List-ID-Hash'], msg['X-List-Sequence-Number'])) + for global_id in global_ids: + config.db.message_store.delete_message(global_id) + flush() + assert not list(config.db.message_store.messages), ( + 'There should be no messages left in the message store.') -- https://code.launchpad.net/~mailman-coders/mailman/3.0 You are receiving this branch notification because you are subscribed to it. To unsubscribe from this branch go to https://code.launchpad.net/~mailman-coders/mailman/3.0/+subscription/mailman-checkins. _______________________________________________ Mailman-checkins mailing list Mailman-checkins@python.org Unsubscribe: http://mail.python.org/mailman/options/mailman-checkins/archive%40jab.org