------------------------------------------------------------ revno: 6557 committer: Barry Warsaw <[EMAIL PROTECTED]> branch nick: 3.0 timestamp: Fri 2007-09-21 08:51:38 -0400 message: OMGW00T: After over a decade, the MailList mixin class is gone! Well, mostly. It's no longer needed by anything in the test suite, and therefore the list manager returns database MailingList objects directly. The wrapper cruft has been removed. To accomplish this, a couple of hacks were added to the Mailman.app package, which will get cleaned up over time. The MailList module itself (and its few remaining mixins) aren't yet removed from the tree because some of the code is still not tested, and I want to leave this code around until I've finished converting it. added: Mailman/app/archiving.py Mailman/app/bounces.py Mailman/app/replybot.py modified: Mailman/Bouncer.py Mailman/Handlers/CookHeaders.py Mailman/Handlers/Hold.py Mailman/Handlers/Replybot.py Mailman/Handlers/Scrubber.py Mailman/Handlers/ToDigest.py Mailman/MailList.py Mailman/Queue/CommandRunner.py Mailman/Utils.py Mailman/app/lifecycle.py Mailman/app/membership.py Mailman/database/listmanager.py Mailman/database/model/mailinglist.py Mailman/database/model/requests.py Mailman/docs/acknowledge.txt Mailman/docs/bounces.txt Mailman/docs/cook-headers.txt Mailman/docs/hold.txt Mailman/docs/listmanager.txt Mailman/docs/replybot.txt Mailman/docs/requests.txt Mailman/docs/scrubber.txt TODO.txt
=== added file 'Mailman/app/archiving.py' --- a/Mailman/app/archiving.py 1970-01-01 00:00:00 +0000 +++ b/Mailman/app/archiving.py 2007-09-21 12:51:38 +0000 @@ -0,0 +1,36 @@ +# Copyright (C) 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. + +"""Application level archiving support.""" + +from string import Template + +from Mailman.configuration import config + + + +def get_base_archive_url(mlist): + if mlist.archive_private: + url = mlist.script_url('private') + '/index.html' + else: + web_host = config.domains.get(mlist.host_name, mlist.host_name) + url = Template(config.PUBLIC_ARCHIVE_URL).safe_substitute( + listname=mlist.fqdn_listname, + hostname=web_host, + fqdn_listname=mlist.fqdn_listname, + ) + return url === added file 'Mailman/app/bounces.py' --- a/Mailman/app/bounces.py 1970-01-01 00:00:00 +0000 +++ b/Mailman/app/bounces.py 2007-09-21 12:51:38 +0000 @@ -0,0 +1,163 @@ +# Copyright (C) 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. + +"""Application level bounce handling.""" + +__all__ = [ + 'bounce_message', + 'has_explicit_destination', + 'has_matching_bounce_header', + ] + +import re +import logging + +from email.mime.message import MIMEMessage +from email.mime.text import MIMEText +from email.utils import getaddresses + +from Mailman import Message +from Mailman import Utils +from Mailman.i18n import _ + +log = logging.getLogger('mailman.config') + + + +def bounce_message(mlist, msg, e=None): + # Bounce a message back to the sender, with an error message if provided + # in the exception argument. + sender = msg.get_sender() + subject = msg.get('subject', _('(no subject)')) + subject = Utils.oneline(subject, + Utils.GetCharSet(mlist.preferred_language)) + if e is None: + notice = _('[No bounce details are available]') + else: + notice = _(e.notice) + # Currently we always craft bounces as MIME messages. + bmsg = Message.UserNotification(msg.get_sender(), + mlist.owner_address, + subject, + lang=mlist.preferred_language) + # BAW: Be sure you set the type before trying to attach, or you'll get + # a MultipartConversionError. + bmsg.set_type('multipart/mixed') + txt = MIMEText(notice, + _charset=Utils.GetCharSet(mlist.preferred_language)) + bmsg.attach(txt) + bmsg.attach(MIMEMessage(msg)) + bmsg.send(mlist) + + + +# Helper function used to match a pattern against an address. +def _domatch(pattern, addr): + try: + if re.match(pattern, addr, re.IGNORECASE): + return True + except re.error: + # The pattern is a malformed regexp -- try matching safely, + # with all non-alphanumerics backslashed: + if re.match(re.escape(pattern), addr, re.IGNORECASE): + return True + return False + + +def has_explicit_destination(mlist, msg): + """Does the list's name or an acceptable alias appear in the recipients? + + :param mlist: The mailing list the message is destined for. + :param msg: The email message object. + :return: True if the message is explicitly destined for the mailing list, + otherwise False. + """ + # Check all recipient addresses against the list's explicit addresses, + # specifically To: Cc: and Resent-to: + recipients = [] + to = [] + for header in ('to', 'cc', 'resent-to', 'resent-cc'): + to.extend(getaddresses(msg.get_all(header, []))) + for fullname, address in to: + # It's possible that if the header doesn't have a valid RFC 2822 + # value, we'll get None for the address. So skip it. + if address is None or '@' not in address: + continue + address = address.lower() + if address == mlist.posting_address: + return True + recipients.append(address) + # Match the set of recipients against the list's acceptable aliases. + aliases = mlist.acceptable_aliases.splitlines() + for address in recipients: + for alias in aliases: + stripped = alias.strip() + if not stripped: + # Ignore blank or empty lines + continue + if domatch(stripped, address): + return True + return False + + + +def _parse_matching_header_opt(mlist): + """Return a list of triples [(field name, regex, line), ...].""" + # - Blank lines and lines with '#' as first char are skipped. + # - Leading whitespace in the matchexp is trimmed - you can defeat + # that by, eg, containing it in gratuitous square brackets. + all = [] + for line in mlist.bounce_matching_headers.splitlines(): + line = line.strip() + # Skip blank lines and lines *starting* with a '#'. + if not line or line.startswith('#'): + continue + i = line.find(':') + if i < 0: + # This didn't look like a header line. BAW: should do a + # better job of informing the list admin. + log.error('bad bounce_matching_header line: %s\n%s', + mlist.real_name, line) + else: + header = line[:i] + value = line[i+1:].lstrip() + try: + cre = re.compile(value, re.IGNORECASE) + except re.error, e: + # The regexp was malformed. BAW: should do a better + # job of informing the list admin. + log.error("""\ +bad regexp in bounce_matching_header line: %s +\n%s (cause: %s)""", mlist.real_name, value, e) + else: + all.append((header, cre, line)) + return all + + +def has_matching_bounce_header(mlist, msg): + """Does the message have a matching bounce header? + + :param mlist: The mailing list the message is destined for. + :param msg: The email message object. + :return: True if a header field matches a regexp in the + bounce_matching_header mailing list variable. + """ + for header, cre, line in _parse_matching_header_opt(mlist): + for value in msg.get_all(header, []): + if cre.search(value): + return True + return False === added file 'Mailman/app/replybot.py' --- a/Mailman/app/replybot.py 1970-01-01 00:00:00 +0000 +++ b/Mailman/app/replybot.py 2007-09-21 12:51:38 +0000 @@ -0,0 +1,89 @@ +# Copyright (C) 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. + +"""Application level auto-reply code.""" + +# XXX This should undergo a rewrite to move this functionality off of the +# mailing list. The reply governor should really apply site-wide per +# recipient (I think). + +from __future__ import with_statement + +__all__ = [ + 'autorespond_to_sender', + ] + +import logging +import datetime + +from Mailman import Utils +from Mailman import i18n +from Mailman.configuration import config + + +log = logging.getLogger('mailman.vette') +_ = i18n._ + + + +def autorespond_to_sender(mlist, sender, lang=None): + """Return True if Mailman should auto-respond to this sender. + + This is only consulted for messages sent to the -request address, or + for posting hold notifications, and serves only as a safety value for + mail loops with email 'bots. + """ + if lang is None: + lang = mlist.preferred_language + if config.MAX_AUTORESPONSES_PER_DAY == 0: + # Unlimited. + return True + today = datetime.date.today() + info = mlist.hold_and_cmd_autoresponses.get(sender) + if info is None or info[0] <> today: + # This is the first time we've seen a -request/post-hold for this + # sender today. + mlist.hold_and_cmd_autoresponses[sender] = (today, 1) + return True + date, count = info + if count < 0: + # They've already hit the limit for today, and we've already notified + # them of this fact, so there's nothing more to do. + log.info('-request/hold autoresponse discarded for: %s', sender) + return False + if count >= config.MAX_AUTORESPONSES_PER_DAY: + log.info('-request/hold autoresponse limit hit for: %s', sender) + mlist.hold_and_cmd_autoresponses[sender] = (today, -1) + # Send this notification message instead. + text = Utils.maketext( + 'nomoretoday.txt', + {'sender' : sender, + 'listname': mlist.fqdn_listname, + 'num' : count, + 'owneremail': mlist.owner_address, + }, + lang=lang) + with i18n.using_language(lang): + msg = Message.UserNotification( + sender, mlist.owner_address, + _('Last autoresponse notification for today'), + text, lang=lang) + msg.send(mlist) + return False + mlist.hold_and_cmd_autoresponses[sender] = (today, count + 1) + return True + === modified file 'Mailman/Bouncer.py' --- a/Mailman/Bouncer.py 2007-08-05 04:32:09 +0000 +++ b/Mailman/Bouncer.py 2007-09-21 12:51:38 +0000 @@ -247,28 +247,3 @@ msg.send(self) info.noticesleft -= 1 info.lastnotice = time.localtime()[:3] - - def bounce_message(self, msg, e=None): - # Bounce a message back to the sender, with an error message if - # provided in the exception argument. - sender = msg.get_sender() - subject = msg.get('subject', _('(no subject)')) - subject = Utils.oneline(subject, - Utils.GetCharSet(self.preferred_language)) - if e is None: - notice = _('[No bounce details are available]') - else: - notice = _(e.notice) - # Currently we always craft bounces as MIME messages. - bmsg = Message.UserNotification(msg.get_sender(), - self.owner_address, - subject, - lang=self.preferred_language) - # BAW: Be sure you set the type before trying to attach, or you'll get - # a MultipartConversionError. - bmsg.set_type('multipart/mixed') - txt = MIMEText(notice, - _charset=Utils.GetCharSet(self.preferred_language)) - bmsg.attach(txt) - bmsg.attach(MIMEMessage(msg)) - bmsg.send(self) === modified file 'Mailman/Handlers/CookHeaders.py' --- a/Mailman/Handlers/CookHeaders.py 2007-06-21 14:23:40 +0000 +++ b/Mailman/Handlers/CookHeaders.py 2007-09-21 12:51:38 +0000 @@ -26,6 +26,7 @@ from Mailman import Utils from Mailman import Version +from Mailman.app.archiving import get_base_archive_url from Mailman.configuration import config from Mailman.constants import ReplyToMunging from Mailman.i18n import _ @@ -207,7 +208,7 @@ headers['List-Post'] = '<mailto:%s>' % mlist.posting_address # Add this header if we're archiving if mlist.archive: - archiveurl = mlist.GetBaseArchiveURL() + archiveurl = get_base_archive_url(mlist) if archiveurl.endswith('/'): archiveurl = archiveurl[:-1] headers['List-Archive'] = '<%s>' % archiveurl === modified file 'Mailman/Handlers/Hold.py' --- a/Mailman/Handlers/Hold.py 2007-09-19 11:28:58 +0000 +++ b/Mailman/Handlers/Hold.py 2007-09-21 12:51:38 +0000 @@ -43,7 +43,10 @@ from Mailman import Message from Mailman import Utils from Mailman import i18n +from Mailman.app.bounces import ( + has_explicit_destination, has_matching_bounce_header) from Mailman.app.moderator import hold_message +from Mailman.app.replybot import autorespond_to_sender from Mailman.configuration import config from Mailman.interfaces import IPendable @@ -88,7 +91,7 @@ reason = _('Message may contain administrivia') def rejection_notice(self, mlist): - listurl = mlist.GetScriptURL('listinfo', absolute=1) + listurl = mlist.script_url('listinfo') request = mlist.request_address return _("""Please do *not* post administrative requests to the mailing list. If you wish to subscribe, visit $listurl or send a message with the @@ -171,7 +174,7 @@ # Implicit destination? Note that message originating from the Usenet # side of the world should never be checked for implicit destination. if mlist.require_explicit_destination and \ - not mlist.HasExplicitDest(msg) and \ + not has_explicit_destination(mlist, msg) and \ not msgdata.get('fromusenet'): # then hold_for_approval(mlist, msg, msgdata, ImplicitDestination) @@ -179,7 +182,7 @@ # # Suspicious headers? if mlist.bounce_matching_headers: - triggered = mlist.hasMatchingHeader(msg) + triggered = has_matching_bounce_header(mlist, msg) if triggered: # TBD: Darn - can't include the matching line for the admin # message because the info would also go to the sender @@ -239,7 +242,7 @@ 'reason' : _(reason), 'sender' : sender, 'subject' : usersubject, - 'admindb_url': mlist.GetScriptURL('admindb', absolute=1), + 'admindb_url': mlist.script_url('admindb'), } # We may want to send a notification to the original sender too fromusenet = msgdata.get('fromusenet') @@ -259,10 +262,10 @@ member = mlist.members.get_member(sender) lang = (member.preferred_language if member else mlist.preferred_language) if not fromusenet and ackp(msg) and mlist.respond_to_post_requests and \ - mlist.autorespondToSender(sender, lang): + autorespond_to_sender(mlist, sender, lang): # Get a confirmation token - d['confirmurl'] = '%s/%s' % (mlist.GetScriptURL('confirm', absolute=1), - token) + d['confirmurl'] = '%s/%s' % ( + mlist.script_url('confirm'), token) lang = msgdata.get('lang', lang) subject = _('Your message to $listname awaits moderator approval') text = Utils.maketext('postheld.txt', d, lang=lang, mlist=mlist) === modified file 'Mailman/Handlers/Replybot.py' --- a/Mailman/Handlers/Replybot.py 2007-05-31 05:01:00 +0000 +++ b/Mailman/Handlers/Replybot.py 2007-09-21 12:51:38 +0000 @@ -19,6 +19,7 @@ import time import logging +import datetime from string import Template @@ -29,6 +30,7 @@ log = logging.getLogger('mailman.error') __i18n_templates__ = True +NODELTA = datetime.timedelta() @@ -63,7 +65,7 @@ sender = msg.get_sender() now = time.time() graceperiod = mlist.autoresponse_graceperiod - if graceperiod > 0 and ack <> 'yes': + if graceperiod > NODELTA and ack <> 'yes': if toadmin: quiet_until = mlist.admin_responses.get(sender, 0) elif torequest: @@ -79,7 +81,7 @@ 'Auto-response for your message to the "$realname" mailing list') # Do string interpolation into the autoresponse text d = dict(listname = realname, - listurl = mlist.GetScriptURL('listinfo'), + listurl = mlist.script_url('listinfo'), requestemail = mlist.request_address, owneremail = mlist.owner_address, ) @@ -98,7 +100,7 @@ outmsg['X-Ack'] = 'No' outmsg.send(mlist) # update the grace period database - if graceperiod > 0: + if graceperiod > NODELTA: # graceperiod is in days, we need # of seconds quiet_until = now + graceperiod * 24 * 60 * 60 if toadmin: === modified file 'Mailman/Handlers/Scrubber.py' --- a/Mailman/Handlers/Scrubber.py 2007-08-01 20:11:08 +0000 +++ b/Mailman/Handlers/Scrubber.py 2007-09-21 12:51:38 +0000 @@ -38,6 +38,7 @@ from Mailman import Message from Mailman import Utils from Mailman.Errors import DiscardMessage +from Mailman.app.archiving import get_base_archive_url from Mailman.configuration import config from Mailman.i18n import _ @@ -388,7 +389,8 @@ def save_attachment(mlist, msg, dir, filter_html=True): - fsdir = os.path.join(mlist.archive_dir(), dir) + fsdir = os.path.join(config.PRIVATE_ARCHIVE_FILE_DIR, + mlist.fqdn_listname, dir) makedirs(fsdir) # Figure out the attachment type and get the decoded data decodedpayload = msg.get_payload(decode=True) @@ -496,7 +498,7 @@ fp.write(decodedpayload) fp.close() # Now calculate the url - baseurl = mlist.GetBaseArchiveURL() + baseurl = get_base_archive_url(mlist) # Private archives will likely have a trailing slash. Normalize. if baseurl[-1] <> '/': baseurl += '/' === modified file 'Mailman/Handlers/ToDigest.py' --- a/Mailman/Handlers/ToDigest.py 2007-09-19 11:28:58 +0000 +++ b/Mailman/Handlers/ToDigest.py 2007-09-21 12:51:38 +0000 @@ -179,7 +179,7 @@ 'masthead.txt', {'real_name' : mlist.real_name, 'got_list_email': mlist.posting_address, - 'got_listinfo_url': mlist.GetScriptURL('listinfo', absolute=1), + 'got_listinfo_url': mlist.script_url('listinfo'), 'got_request_email': mlist.request_address, 'got_owner_email': mlist.owner_address, }, mlist=mlist) === modified file 'Mailman/MailList.py' --- a/Mailman/MailList.py 2007-09-20 02:35:37 +0000 +++ b/Mailman/MailList.py 2007-09-21 12:51:38 +0000 @@ -58,9 +58,7 @@ # Base classes from Mailman.Archiver import Archiver from Mailman.Bouncer import Bouncer -from Mailman.Deliverer import Deliverer from Mailman.Digester import Digester -from Mailman.HTMLFormatter import HTMLFormatter from Mailman.SecurityManager import SecurityManager # GUI components package @@ -85,8 +83,7 @@ # Use mixins here just to avoid having any one chunk be too large. -class MailList(object, HTMLFormatter, Deliverer, - Archiver, Digester, SecurityManager, Bouncer): +class MailList(object, Archiver, Digester, SecurityManager, Bouncer): implements( IMailingList, @@ -165,50 +162,6 @@ - # IMailingListAddresses - - @property - def posting_address(self): - return self.fqdn_listname - - @property - def noreply_address(self): - return '[EMAIL PROTECTED]' % (config.NO_REPLY_ADDRESS, self.host_name) - - @property - def owner_address(self): - return '[EMAIL PROTECTED]' % (self.list_name, self.host_name) - - @property - def request_address(self): - return '[EMAIL PROTECTED]' % (self.list_name, self.host_name) - - @property - def bounces_address(self): - return '[EMAIL PROTECTED]' % (self.list_name, self.host_name) - - @property - def join_address(self): - return '[EMAIL PROTECTED]' % (self.list_name, self.host_name) - - @property - def leave_address(self): - return '[EMAIL PROTECTED]' % (self.list_name, self.host_name) - - @property - def subscribe_address(self): - return '[EMAIL PROTECTED]' % (self.list_name, self.host_name) - - @property - def unsubscribe_address(self): - return '[EMAIL PROTECTED]' % (self.list_name, self.host_name) - - def confirm_address(self, cookie): - local_part = Template(config.VERP_CONFIRM_FORMAT).safe_substitute( - address = '%s-confirm' % self.list_name, - cookie = cookie) - return '[EMAIL PROTECTED]' % (local_part, self.host_name) - def GetConfirmJoinSubject(self, listname, cookie): if config.VERP_CONFIRMATIONS and cookie: cset = i18n.get_translation().charset() or \ @@ -393,9 +346,9 @@ invitee = userdesc.address Utils.ValidateEmail(invitee) # check for banned address - pattern = self.GetBannedPattern(invitee) + pattern = Utils.get_pattern(invitee, self.ban_list) if pattern: - raise Errors.MembershipIsBanned, pattern + raise Errors.MembershipIsBanned(pattern) # Hack alert! Squirrel away a flag that only invitations have, so # that we can do something slightly different when an invitation # subscription is confirmed. In those cases, we don't need further @@ -471,7 +424,7 @@ raise Errors.InvalidEmailAddress realname = self.real_name # Is the subscribing address banned from this list? - pattern = self.GetBannedPattern(email) + pattern = Utils.get_pattern(email, self.ban_list) if pattern: vlog.error('%s banned subscription: %s (matched: %s)', realname, email, pattern) @@ -588,7 +541,7 @@ # Don't allow changing to a banned address. MAS: maybe we should # unsubscribe the oldaddr too just for trying, but that's probably # too harsh. - pattern = self.GetBannedPattern(newaddr) + pattern = Utils.get_pattern(newaddr, self.ban_list) if pattern: vlog.error('%s banned address change: %s -> %s (matched: %s)', realname, oldaddr, newaddr, pattern) @@ -630,7 +583,7 @@ # confirmation was mailed. MAS: If it's global change should we just # skip this list and proceed to the others? For now we'll throw the # exception. - pattern = self.GetBannedPattern(newaddr) + pattern = Utils.get_pattern(newaddr, self.ban_list) if pattern: raise Errors.MembershipIsBanned, pattern # It's possible they were a member of this list, but choose to change @@ -655,7 +608,7 @@ if not mlist.isMember(oldaddr): continue # If new address is banned from this list, just skip it. - if mlist.GetBannedPattern(newaddr): + if Utils.get_pattern(newaddr, mlist.ban_list): continue mlist.Lock() try: @@ -859,194 +812,18 @@ # # Miscellaneous stuff # - def HasExplicitDest(self, msg): - """True if list name or any acceptable_alias is included among the - addresses in the recipient headers. - """ - # This is the list's full address. - recips = [] - # Check all recipient addresses against the list's explicit addresses, - # specifically To: Cc: and Resent-to: - to = [] - for header in ('to', 'cc', 'resent-to', 'resent-cc'): - to.extend(getaddresses(msg.get_all(header, []))) - for fullname, addr in to: - # It's possible that if the header doesn't have a valid RFC 2822 - # value, we'll get None for the address. So skip it. - if addr is None: - continue - addr = addr.lower() - localpart = addr.split('@')[0] - if (# TBD: backwards compatibility: deprecated - localpart == self.list_name or - # exact match against the complete list address - addr == self.fqdn_listname): - return True - recips.append((addr, localpart)) - # Helper function used to match a pattern against an address. - def domatch(pattern, addr): - try: - if re.match(pattern, addr, re.IGNORECASE): - return True - except re.error: - # The pattern is a malformed regexp -- try matching safely, - # with all non-alphanumerics backslashed: - if re.match(re.escape(pattern), addr, re.IGNORECASE): - return True - return False - # Here's the current algorithm for matching acceptable_aliases: - # - # 1. If the pattern does not have an `@' in it, we first try matching - # it against just the localpart. This was the behavior prior to - # 2.0beta3, and is kept for backwards compatibility. (deprecated). - # - # 2. If that match fails, or the pattern does have an `@' in it, we - # try matching against the entire recip address. - aliases = self.acceptable_aliases.splitlines() - for addr, localpart in recips: - for alias in aliases: - stripped = alias.strip() - if not stripped: - # Ignore blank or empty lines - continue - if '@' not in stripped and domatch(stripped, localpart): - return True - if domatch(stripped, addr): - return True - return False - - def parse_matching_header_opt(self): - """Return a list of triples [(field name, regex, line), ...].""" - # - Blank lines and lines with '#' as first char are skipped. - # - Leading whitespace in the matchexp is trimmed - you can defeat - # that by, eg, containing it in gratuitous square brackets. - all = [] - for line in self.bounce_matching_headers.split('\n'): - line = line.strip() - # Skip blank lines and lines *starting* with a '#'. - if not line or line[0] == "#": - continue - i = line.find(':') - if i < 0: - # This didn't look like a header line. BAW: should do a - # better job of informing the list admin. - clog.error('bad bounce_matching_header line: %s\n%s', - self.real_name, line) - else: - header = line[:i] - value = line[i+1:].lstrip() - try: - cre = re.compile(value, re.IGNORECASE) - except re.error, e: - # The regexp was malformed. BAW: should do a better - # job of informing the list admin. - clog.error("""\ -bad regexp in bounce_matching_header line: %s -\n%s (cause: %s)""", self.real_name, value, e) - else: - all.append((header, cre, line)) - return all - - def hasMatchingHeader(self, msg): - """Return true if named header field matches a regexp in the - bounce_matching_header list variable. - - Returns constraint line which matches or empty string for no - matches. - """ - for header, cre, line in self.parse_matching_header_opt(): - for value in msg.get_all(header, []): - if cre.search(value): - return line - return 0 - - def autorespondToSender(self, sender, lang=None): - """Return true if Mailman should auto-respond to this sender. - - This is only consulted for messages sent to the -request address, or - for posting hold notifications, and serves only as a safety value for - mail loops with email 'bots. - """ - # language setting - if lang == None: - lang = self.preferred_language - i18n.set_language(lang) - # No limit - if config.MAX_AUTORESPONSES_PER_DAY == 0: - return 1 - today = time.localtime()[:3] - info = self.hold_and_cmd_autoresponses.get(sender) - if info is None or info[0] <> today: - # First time we've seen a -request/post-hold for this sender - self.hold_and_cmd_autoresponses[sender] = (today, 1) - # BAW: no check for MAX_AUTORESPONSES_PER_DAY <= 1 - return 1 - date, count = info - if count < 0: - # They've already hit the limit for today. - vlog.info('-request/hold autoresponse discarded for: %s', sender) - return 0 - if count >= config.MAX_AUTORESPONSES_PER_DAY: - vlog.info('-request/hold autoresponse limit hit for: %s', sender) - self.hold_and_cmd_autoresponses[sender] = (today, -1) - # Send this notification message instead - text = Utils.maketext( - 'nomoretoday.txt', - {'sender' : sender, - 'listname': self.fqdn_listname, - 'num' : count, - 'owneremail': self.GetOwnerEmail(), - }, - lang=lang) - msg = Message.UserNotification( - sender, self.GetOwnerEmail(), - _('Last autoresponse notification for today'), - text, lang=lang) - msg.send(self) - return 0 - self.hold_and_cmd_autoresponses[sender] = (today, count+1) - return 1 - - def GetBannedPattern(self, email): - """Returns matched entry in ban_list if email matches. - Otherwise returns None. - """ - return self.ban_list and self.GetPattern(email, self.ban_list) def HasAutoApprovedSender(self, sender): """Returns True and logs if sender matches address or pattern in subscribe_auto_approval. Otherwise returns False. """ auto_approve = False - if self.GetPattern(sender, self.subscribe_auto_approval): + if Utils.get_pattern(sender, self.subscribe_auto_approval): auto_approve = True vlog.info('%s: auto approved subscribe from %s', self.internal_name(), sender) return auto_approve - def GetPattern(self, email, pattern_list): - """Returns matched entry in pattern_list if email matches. - Otherwise returns None. - """ - matched = None - for pattern in pattern_list: - if pattern.startswith('^'): - # This is a regular expression match - try: - if re.search(pattern, email, re.IGNORECASE): - matched = pattern - break - except re.error: - # BAW: we should probably remove this pattern - pass - else: - # Do the comparison case insensitively - if pattern.lower() == email.lower(): - matched = pattern - break - return matched - - # # Multilingual (i18n) support === modified file 'Mailman/Queue/CommandRunner.py' --- a/Mailman/Queue/CommandRunner.py 2007-01-19 04:38:06 +0000 +++ b/Mailman/Queue/CommandRunner.py 2007-09-21 12:51:38 +0000 @@ -36,6 +36,7 @@ from Mailman import Utils from Mailman.Handlers import Replybot from Mailman.Queue.Runner import Runner +from Mailman.app.replybot import autorespond_to_sender from Mailman.configuration import config from Mailman.i18n import _ @@ -176,7 +177,7 @@ # BAW: We wait until now to make this decision since our sender may # not be self.msg.get_sender(), but I'm not sure this is right. recip = self.returnaddr or self.msg.get_sender() - if not self.mlist.autorespondToSender(recip, self.msgdata['lang']): + if not autorespond_to_sender(self.mlist, recip, self.msgdata['lang']): return msg = Message.UserNotification( recip, === modified file 'Mailman/Utils.py' --- a/Mailman/Utils.py 2007-09-09 17:22:27 +0000 +++ b/Mailman/Utils.py 2007-09-21 12:51:38 +0000 @@ -887,3 +887,31 @@ newpattern += c i += 1 return newpattern + + + +def get_pattern(email, pattern_list): + """Returns matched entry in pattern_list if email matches. + Otherwise returns None. + """ + if not pattern_list: + return None + matched = None + for pattern in pattern_list: + if pattern.startswith('^'): + # This is a regular expression match + try: + if re.search(pattern, email, re.IGNORECASE): + matched = pattern + break + except re.error: + # BAW: we should probably remove this pattern + pass + else: + # Do the comparison case insensitively + if pattern.lower() == email.lower(): + matched = pattern + break + return matched + + === modified file 'Mailman/app/lifecycle.py' --- a/Mailman/app/lifecycle.py 2007-08-06 03:49:04 +0000 +++ b/Mailman/app/lifecycle.py 2007-09-21 12:51:38 +0000 @@ -53,7 +53,7 @@ # be necessary. Until then, setattr on the MailList instance won't # set the database column values, so pass the underlying database # object to .apply() instead. - style.apply(mlist._data) + style.apply(mlist) # Coordinate with the MTA, which should be defined by plugins. # XXX FIXME ## mta_plugin = get_plugin('mailman.mta') === modified file 'Mailman/app/membership.py' --- a/Mailman/app/membership.py 2007-09-19 11:28:58 +0000 +++ b/Mailman/app/membership.py 2007-09-21 12:51:38 +0000 @@ -58,7 +58,7 @@ raise Errors.AlreadySubscribedError(address) # Check for banned address here too for admin mass subscribes and # confirmations. - pattern = mlist.GetBannedPattern(address) + pattern = Utils.get_pattern(address, mlist.ban_list) if pattern: raise Errors.MembershipIsBanned(pattern) # Do the actual addition. First, see if there's already a user linked === modified file 'Mailman/database/listmanager.py' --- a/Mailman/database/listmanager.py 2007-08-05 04:32:09 +0000 +++ b/Mailman/database/listmanager.py 2007-09-21 12:51:38 +0000 @@ -17,7 +17,6 @@ """SQLAlchemy/Elixir based provider of IListManager.""" -import weakref import datetime from elixir import * @@ -34,9 +33,6 @@ class ListManager(object): implements(IListManager) - def __init__(self): - self._objectmap = weakref.WeakKeyDictionary() - def create(self, fqdn_listname): listname, hostname = split_listname(fqdn_listname) mlist = MailingList.get_by(list_name=listname, @@ -45,30 +41,18 @@ raise Errors.MMListAlreadyExistsError(fqdn_listname) mlist = MailingList(fqdn_listname) mlist.created_at = datetime.datetime.now() - # Wrap the database model object in an application MailList object and - # return the latter. Keep track of the wrapper so we can clean it up - # when we're done with it. - from Mailman.MailList import MailList - wrapper = MailList(mlist) - self._objectmap[mlist] = wrapper - return wrapper + return mlist def delete(self, mlist): - # Delete the wrapped backing data. XXX It's kind of icky to reach - # into the MailList object this way. - mlist._data.delete() - mlist._data = None + mlist.delete() def get(self, fqdn_listname): listname, hostname = split_listname(fqdn_listname) - mlist = MailingList.get_by(list_name=listname, - host_name=hostname) - if not mlist: - return None - mlist._restore() - from Mailman.MailList import MailList - wrapper = self._objectmap.setdefault(mlist, MailList(mlist)) - return wrapper + mlist = MailingList.get_by(list_name=listname, host_name=hostname) + if mlist is not None: + # XXX Fixme + mlist._restore() + return mlist @property def mailing_lists(self): === modified file 'Mailman/database/model/mailinglist.py' --- a/Mailman/database/model/mailinglist.py 2007-09-21 02:39:20 +0000 +++ b/Mailman/database/model/mailinglist.py 2007-09-21 12:51:38 +0000 @@ -211,3 +211,51 @@ # XXX Handle the case for when context is not None; those would be # relative URLs. return self.web_page_url + target + '/' + self.fqdn_listname + + # IMailingListAddresses + + @property + def posting_address(self): + return self.fqdn_listname + + @property + def noreply_address(self): + return '[EMAIL PROTECTED]' % (config.NO_REPLY_ADDRESS, self.host_name) + + @property + def owner_address(self): + return '[EMAIL PROTECTED]' % (self.list_name, self.host_name) + + @property + def request_address(self): + return '[EMAIL PROTECTED]' % (self.list_name, self.host_name) + + @property + def bounces_address(self): + return '[EMAIL PROTECTED]' % (self.list_name, self.host_name) + + @property + def join_address(self): + return '[EMAIL PROTECTED]' % (self.list_name, self.host_name) + + @property + def leave_address(self): + return '[EMAIL PROTECTED]' % (self.list_name, self.host_name) + + @property + def subscribe_address(self): + return '[EMAIL PROTECTED]' % (self.list_name, self.host_name) + + @property + def unsubscribe_address(self): + return '[EMAIL PROTECTED]' % (self.list_name, self.host_name) + + def confirm_address(self, cookie): + template = string.Template(config.VERP_CONFIRM_FORMAT) + local_part = template.safe_substitute( + address = '%s-confirm' % self.list_name, + cookie = cookie) + return '[EMAIL PROTECTED]' % (local_part, self.host_name) + + def __repr__(self): + return '<mailing list "%s" at %#x>' % (self.fqdn_listname, id(self)) === modified file 'Mailman/database/model/requests.py' --- a/Mailman/database/model/requests.py 2007-09-09 17:22:27 +0000 +++ b/Mailman/database/model/requests.py 2007-09-21 12:51:38 +0000 @@ -49,22 +49,22 @@ @property def count(self): - results = _Request.select_by(mailing_list=self.mailing_list._data) + results = _Request.select_by(mailing_list=self.mailing_list) return len(results) def count_of(self, request_type): - results = _Request.select_by(mailing_list=self.mailing_list._data, + results = _Request.select_by(mailing_list=self.mailing_list, type=request_type) return len(results) @property def held_requests(self): - results = _Request.select_by(mailing_list=self.mailing_list._data) + results = _Request.select_by(mailing_list=self.mailing_list) for request in results: yield request def of_type(self, request_type): - results = _Request.select_by(mailing_list=self.mailing_list._data, + results = _Request.select_by(mailing_list=self.mailing_list, type=request_type) for request in results: yield request @@ -88,12 +88,12 @@ # flush()'s. ## result = _Request.table.insert().execute( ## key=key, type=request_type, -## mailing_list=self.mailing_list._data, +## mailing_list=self.mailing_list, ## data_hash=data_hash) ## row_id = result.last_inserted_ids()[0] ## return row_id result = _Request(key=key, type=request_type, - mailing_list=self.mailing_list._data, + mailing_list=self.mailing_list, data_hash=data_hash) # XXX We need a handle on last_inserted_ids() instead of requiring a # flush of the database to get a valid id. === modified file 'Mailman/docs/acknowledge.txt' --- a/Mailman/docs/acknowledge.txt 2007-08-02 14:47:56 +0000 +++ b/Mailman/docs/acknowledge.txt 2007-09-21 12:51:38 +0000 @@ -15,7 +15,7 @@ >>> mlist.preferred_language = 'en' >>> # XXX This will almost certainly change once we've worked out the web >>> # space layout for mailing lists now. - >>> mlist._data.web_page_url = 'http://lists.example.com/' + >>> mlist.web_page_url = 'http://lists.example.com/' >>> flush() >>> # Ensure that the virgin queue is empty, since we'll be checking this === modified file 'Mailman/docs/bounces.txt' --- a/Mailman/docs/bounces.txt 2007-08-02 14:47:56 +0000 +++ b/Mailman/docs/bounces.txt 2007-09-21 12:51:38 +0000 @@ -37,7 +37,8 @@ >>> from Mailman.Queue.Switchboard import Switchboard >>> switchboard = Switchboard(config.VIRGINQUEUE_DIR) - >>> mlist.bounce_message(msg) + >>> from Mailman.app.bounces import bounce_message + >>> bounce_message(mlist, msg) >>> len(switchboard.files) 1 >>> filebase = switchboard.files[0] @@ -77,7 +78,7 @@ >>> from Mailman.Errors import RejectMessage >>> error = RejectMessage("This wasn't very important after all.") - >>> mlist.bounce_message(msg, error) + >>> bounce_message(mlist, msg, error) >>> len(switchboard.files) 1 >>> filebase = switchboard.files[0] === modified file 'Mailman/docs/cook-headers.txt' --- a/Mailman/docs/cook-headers.txt 2007-08-02 14:47:56 +0000 +++ b/Mailman/docs/cook-headers.txt 2007-09-21 12:51:38 +0000 @@ -18,7 +18,7 @@ >>> mlist.archive = True >>> # XXX This will almost certainly change once we've worked out the web >>> # space layout for mailing lists now. - >>> mlist._data.web_page_url = 'http://lists.example.com/' + >>> mlist.web_page_url = 'http://lists.example.com/' >>> flush() === modified file 'Mailman/docs/hold.txt' --- a/Mailman/docs/hold.txt 2007-09-20 02:35:37 +0000 +++ b/Mailman/docs/hold.txt 2007-09-21 12:51:38 +0000 @@ -16,7 +16,7 @@ >>> mlist.real_name = '_XTest' >>> # XXX This will almost certainly change once we've worked out the web >>> # space layout for mailing lists now. - >>> mlist._data.web_page_url = 'http://lists.example.com/' + >>> mlist.web_page_url = 'http://lists.example.com/' >>> flush() Here's a helper function used when we don't care about what's in the virgin === modified file 'Mailman/docs/listmanager.txt' --- a/Mailman/docs/listmanager.txt 2007-08-05 04:32:09 +0000 +++ b/Mailman/docs/listmanager.txt 2007-09-21 12:51:38 +0000 @@ -61,14 +61,6 @@ >>> sorted(listmgr.names) [] -Attempting to access attributes of the deleted mailing list raises an -exception: - - >>> mlist.fqdn_listname - Traceback (most recent call last): - ... - AttributeError: fqdn_listname - After deleting the list, you can create it again. >>> mlist = listmgr.create('[EMAIL PROTECTED]') === modified file 'Mailman/docs/replybot.txt' --- a/Mailman/docs/replybot.txt 2007-08-02 14:47:56 +0000 +++ b/Mailman/docs/replybot.txt 2007-09-21 12:51:38 +0000 @@ -13,6 +13,7 @@ >>> from Mailman.database import flush >>> mlist = config.db.list_manager.create('[EMAIL PROTECTED]') >>> mlist.real_name = 'XTest' + >>> mlist.web_page_url = 'http://www.example.com/' >>> flush() >>> # Ensure that the virgin queue is empty, since we'll be checking this @@ -32,8 +33,9 @@ grace period which describes how much time must pass before a second response will be sent, with 0 meaning "there is no grace period". + >>> import datetime >>> mlist.autorespond_admin = True - >>> mlist.autoresponse_graceperiod = 0 + >>> mlist.autoresponse_graceperiod = datetime.timedelta() >>> mlist.autoresponse_admin_text = 'admin autoresponse text' >>> flush() >>> msg = message_from_string("""\ === modified file 'Mailman/docs/requests.txt' --- a/Mailman/docs/requests.txt 2007-09-20 02:35:37 +0000 +++ b/Mailman/docs/requests.txt 2007-09-21 12:51:38 +0000 @@ -451,7 +451,7 @@ >>> mlist.admin_immed_notify = True >>> # XXX This will almost certainly change once we've worked out the web >>> # space layout for mailing lists now. - >>> mlist._data.web_page_url = 'http://www.example.com/' + >>> mlist.web_page_url = 'http://www.example.com/' >>> flush() >>> id_4 = moderator.hold_subscription(mlist, ... '[EMAIL PROTECTED]', 'Claire Person', === modified file 'Mailman/docs/scrubber.txt' --- a/Mailman/docs/scrubber.txt 2007-08-02 14:47:56 +0000 +++ b/Mailman/docs/scrubber.txt 2007-09-21 12:51:38 +0000 @@ -19,7 +19,8 @@ >>> import os, re >>> def read_attachment(filename, remove=True): - ... path = os.path.join(mlist.archive_dir(), filename) + ... path = os.path.join(config.PRIVATE_ARCHIVE_FILE_DIR, + ... mlist.fqdn_listname, filename) ... fp = open(path) ... try: ... data = fp.read() === modified file 'TODO.txt' --- a/TODO.txt 2007-09-21 02:39:20 +0000 +++ b/TODO.txt 2007-09-21 12:51:38 +0000 @@ -4,7 +4,7 @@ Fix the XXX in model/requests.py where we need a flush because we can't get to last_inserted_id() Get rid of PickleTypes -Get rid of MailList class! +Get rid of MailList class! (done for test suite!) Add tests for bin/newlist and bin/rmlist Add tests for plugins Rework MTA plugins and add tests -- 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