diff options
author | Jim Popovitch <jimpop@gmail.com> | 2013-11-17 01:56:29 +0000 |
---|---|---|
committer | Jim Popovitch <jimpop@gmail.com> | 2013-11-17 01:56:29 +0000 |
commit | 06d15b0a9949652f696d19903cef2a235ff3a428 (patch) | |
tree | 55a9c12de799cc056da986aafe3b8e66d0cbf94b /Mailman | |
parent | bc05ad4e81bd2ce9ec0f36e5112eadf607a49195 (diff) | |
parent | ded18dda2b2f9ba00f8780b4eba6c398c5bff838 (diff) | |
download | mailman2-06d15b0a9949652f696d19903cef2a235ff3a428.tar.gz mailman2-06d15b0a9949652f696d19903cef2a235ff3a428.tar.xz mailman2-06d15b0a9949652f696d19903cef2a235ff3a428.zip |
Upstream changes
Diffstat (limited to '')
-rw-r--r-- | Mailman/Archiver/HyperArch.py | 5 | ||||
-rw-r--r-- | Mailman/Bouncers/SimpleMatch.py | 6 | ||||
-rw-r--r-- | Mailman/Bouncers/Yahoo.py | 35 | ||||
-rw-r--r-- | Mailman/CSRFcheck.py | 7 | ||||
-rw-r--r-- | Mailman/Cgi/admindb.py | 136 | ||||
-rwxr-xr-x | Mailman/Defaults.py.in | 54 | ||||
-rw-r--r-- | Mailman/Deliverer.py | 3 | ||||
-rw-r--r-- | Mailman/Gui/Digest.py | 6 | ||||
-rw-r--r-- | Mailman/Gui/GUIBase.py | 15 | ||||
-rw-r--r-- | Mailman/Gui/General.py | 22 | ||||
-rw-r--r-- | Mailman/Handlers/AvoidDuplicates.py | 9 | ||||
-rw-r--r-- | Mailman/Handlers/Cleanse.py | 28 | ||||
-rw-r--r-- | Mailman/Handlers/CleanseDKIM.py | 15 | ||||
-rwxr-xr-x | Mailman/Handlers/CookHeaders.py | 73 | ||||
-rw-r--r-- | Mailman/Handlers/SpamDetect.py | 8 | ||||
-rw-r--r-- | Mailman/Handlers/Tagger.py | 8 | ||||
-rw-r--r-- | Mailman/Handlers/ToDigest.py | 5 | ||||
-rw-r--r-- | Mailman/Handlers/WrapMessage.py | 57 | ||||
-rwxr-xr-x | Mailman/MailList.py | 3 | ||||
-rw-r--r-- | Mailman/Queue/BounceRunner.py | 6 | ||||
-rw-r--r-- | Mailman/Queue/Switchboard.py | 6 | ||||
-rw-r--r--[-rwxr-xr-x] | Mailman/Version.py | 8 | ||||
-rwxr-xr-x | Mailman/versions.py | 5 |
23 files changed, 404 insertions, 116 deletions
diff --git a/Mailman/Archiver/HyperArch.py b/Mailman/Archiver/HyperArch.py index 33a77f0b..4d95c5ab 100644 --- a/Mailman/Archiver/HyperArch.py +++ b/Mailman/Archiver/HyperArch.py @@ -226,9 +226,9 @@ def quick_maketext(templatefile, dict=None, lang=None, mlist=None): Utils.GetCharSet(lang), 'replace') text = sdict.interpolate(utemplate) - except (TypeError, ValueError): + except (TypeError, ValueError), e: # The template is really screwed up - pass + syslog('error', 'broken template: %s\n%s', filepath, e) # Make sure the text is in the given character set, or html-ify any bogus # characters. return Utils.uncanonstr(text, lang) @@ -471,6 +471,7 @@ class Article(pipermail.Article): d["email_html"] = self.quote(self.email) d["title"] = self.quote(self.subject) d["subject_html"] = self.quote(self.subject) + d["message_id"] = self.quote(self._message_id) # TK: These two _url variables are used to compose a response # from the archive web page. So, ... d["subject_url"] = url_quote('Re: ' + self.subject) diff --git a/Mailman/Bouncers/SimpleMatch.py b/Mailman/Bouncers/SimpleMatch.py index 0607ce86..2aa082a2 100644 --- a/Mailman/Bouncers/SimpleMatch.py +++ b/Mailman/Bouncers/SimpleMatch.py @@ -42,7 +42,7 @@ PATTERNS = [ # sz-sb.de, corridor.com, nfg.nl (_c('the following addresses had'), _c('transcript of session follows'), - _c(r'<(?P<fulladdr>[^>]*)>|\(expanded from: <?(?P<addr>[^>)]*)>?\)')), + _c(r'^ *(\(expanded from: )?<?(?P<addr>[^\s@]+@[^\s@>]+?)>?\)?\s*$')), # robanal.demon.co.uk (_c('this message was created automatically by mail delivery software'), _c('original message follows'), @@ -184,6 +184,10 @@ PATTERNS = [ _c( 'Your message to (?P<addr>[^\s@]+@[^\s@]+) was automatically rejected' )), + # mail.ru + (_c('A message that you sent was rejected'), + _c('This is a copy of your message'), + _c('\s(?P<addr>[^\s@]+@[^\s@]+)')), # Next one goes here... ] diff --git a/Mailman/Bouncers/Yahoo.py b/Mailman/Bouncers/Yahoo.py index b3edf4fa..47fedce2 100644 --- a/Mailman/Bouncers/Yahoo.py +++ b/Mailman/Bouncers/Yahoo.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2013 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 @@ -12,7 +12,8 @@ # # 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. +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +# USA. """Yahoo! has its own weird format for bounces.""" @@ -20,9 +21,15 @@ import re import email from email.Utils import parseaddr -tcre = re.compile(r'message\s+from\s+yahoo\.\S+', re.IGNORECASE) +tcre = (re.compile(r'message\s+from\s+yahoo\.\S+', re.IGNORECASE), + re.compile(r'Sorry, we were unable to deliver your message to ' + r'the following address(\(es\))?\.', + re.IGNORECASE), + ) acre = re.compile(r'<(?P<addr>[^>]*)>:') -ecre = re.compile(r'--- Original message follows') +ecre = (re.compile(r'--- Original message follows'), + re.compile(r'--- Below this line is a copy of the message'), + ) @@ -36,18 +43,26 @@ def process(msg): # simple state machine # 0 == nothing seen # 1 == tag line seen + # 2 == end line seen state = 0 for line in email.Iterators.body_line_iterator(msg): line = line.strip() - if state == 0 and tcre.match(line): - state = 1 + if state == 0: + for cre in tcre: + if cre.match(line): + state = 1 + break elif state == 1: mo = acre.match(line) if mo: addrs.append(mo.group('addr')) continue - mo = ecre.match(line) - if mo: - # we're at the end of the error response - break + for cre in ecre: + mo = cre.match(line) + if mo: + # we're at the end of the error response + state = 2 + break + elif state == 2: + break return addrs diff --git a/Mailman/CSRFcheck.py b/Mailman/CSRFcheck.py index a3b6885a..d531ffc2 100644 --- a/Mailman/CSRFcheck.py +++ b/Mailman/CSRFcheck.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2012 by the Free Software Foundation, Inc. +# Copyright (C) 2011-2013 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 @@ -55,8 +55,9 @@ def csrf_check(mlist, token): try: issued, keymac = marshal.loads(binascii.unhexlify(token)) key, received_mac = keymac.split(':', 1) - klist, key = key.split('+', 1) - assert klist == mlist.internal_name() + if not key.startswith(mlist.internal_name() + '+'): + return False + key = key[len(mlist.internal_name()) + 1:] if '+' in key: key, user = key.split('+', 1) else: diff --git a/Mailman/Cgi/admindb.py b/Mailman/Cgi/admindb.py index d1873321..d3350ea7 100644 --- a/Mailman/Cgi/admindb.py +++ b/Mailman/Cgi/admindb.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2011 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2013 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 @@ -50,16 +50,38 @@ i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE) EXCERPT_HEIGHT = 10 EXCERPT_WIDTH = 76 +SSENDER = mm_cfg.SSENDER +SSENDERTIME = mm_cfg.SSENDERTIME +STIME = mm_cfg.STIME +if mm_cfg.DISPLAY_HELD_SUMMARY_SORT_BUTTONS in (SSENDERTIME, STIME): + ssort = mm_cfg.DISPLAY_HELD_SUMMARY_SORT_BUTTONS +else: + ssort = SSENDER -def helds_by_sender(mlist): +def helds_by_skey(mlist, ssort=SSENDER): heldmsgs = mlist.GetHeldMessageIds() - bysender = {} + byskey = {} for id in heldmsgs: + ptime = mlist.GetRecord(id)[0] sender = mlist.GetRecord(id)[1] - bysender.setdefault(sender, []).append(id) - return bysender + if ssort in (SSENDER, SSENDERTIME): + skey = (0, sender) + else: + skey = (ptime, sender) + byskey.setdefault(skey, []).append((ptime, id)) + # Sort groups by time + for k, v in byskey.items(): + if len(v) > 1: + v.sort() + byskey[k] = v + if ssort == SSENDERTIME: + # Rekey with time + newkey = (v[0][0], k[1]) + del byskey[k] + byskey[newkey] = v + return byskey def hacky_radio_buttons(btnname, labels, values, defaults, spacing=3): @@ -76,6 +98,7 @@ def hacky_radio_buttons(btnname, labels, values, defaults, spacing=3): def main(): + global ssort # Figure out which list is being requested parts = Utils.GetPathPieces() if not parts: @@ -253,7 +276,7 @@ def main(): raw=1, mlist=mlist)) num = show_pending_subs(mlist, form) num += show_pending_unsubs(mlist, form) - num += show_helds_overview(mlist, form) + num += show_helds_overview(mlist, form, ssort) addform = num > 0 # Finish up the document, adding buttons to the form if addform: @@ -314,10 +337,10 @@ def show_pending_subs(mlist, form): for id in pendingsubs: addr = mlist.GetRecord(id)[1] byaddrs.setdefault(addr, []).append(id) - addrs = byaddrs.keys() + addrs = byaddrs.items() addrs.sort() num = 0 - for addr, ids in byaddrs.items(): + for addr, ids in addrs: # Eliminate duplicates for id in ids[1:]: mlist.HandleRequest(id, mm_cfg.DISCARD) @@ -365,10 +388,10 @@ def show_pending_unsubs(mlist, form): for id in pendingunsubs: addr = mlist.GetRecord(id) byaddrs.setdefault(addr, []).append(id) - addrs = byaddrs.keys() + addrs = byaddrs.items() addrs.sort() num = 0 - for addr, ids in byaddrs.items(): + for addr, ids in addrs: # Eliminate duplicates for id in ids[1:]: mlist.HandleRequest(id, mm_cfg.DISCARD) @@ -402,20 +425,29 @@ def show_pending_unsubs(mlist, form): -def show_helds_overview(mlist, form): - # Sort the held messages by sender - bysender = helds_by_sender(mlist) - if not bysender: +def show_helds_overview(mlist, form, ssort=SSENDER): + # Sort the held messages. + byskey = helds_by_skey(mlist, ssort) + if not byskey: return 0 form.AddItem('<hr>') form.AddItem(Center(Header(2, _('Held Messages')))) + # Add the sort sequence choices if wanted + if mm_cfg.DISPLAY_HELD_SUMMARY_SORT_BUTTONS: + form.AddItem(Center(_('Show this list grouped/sorted by'))) + form.AddItem(Center(hacky_radio_buttons( + 'summary_sort', + (_('sender/sender'), _('sender/time'), _('ungrouped/time')), + (SSENDER, SSENDERTIME, STIME), + (ssort == SSENDER, ssort == SSENDERTIME, ssort == STIME)))) # Add the by-sender overview tables admindburl = mlist.GetScriptURL('admindb', absolute=1) table = Table(border=0) form.AddItem(table) - senders = bysender.keys() - senders.sort() - for sender in senders: + skeys = byskey.keys() + skeys.sort() + for skey in skeys: + sender = skey[1] qsender = quote_plus(sender) esender = Utils.websafe(sender) senderurl = admindburl + '?sender=' + qsender @@ -499,7 +531,7 @@ def show_helds_overview(mlist, form): right.AddCellInfo(right.GetCurrentRowIndex(), 0, colspan=2) right.AddRow([' ', ' ']) counter = 1 - for id in bysender[sender]: + for ptime, id in byskey[skey]: info = mlist.GetRecord(id) ptime, sender, subject, reason, filename, msgdata = info # BAW: This is really the size of the message pickle, which should @@ -540,13 +572,14 @@ def show_helds_overview(mlist, form): def show_sender_requests(mlist, form, sender): - bysender = helds_by_sender(mlist) - if not bysender: + byskey = helds_by_skey(mlist, SSENDER) + if not byskey: return - sender_ids = bysender.get(sender) + sender_ids = byskey.get((0, sender)) if sender_ids is None: # BAW: should we print an error message? return + sender_ids = [x[1] for x in sender_ids] total = len(sender_ids) count = 1 for id in sender_ids: @@ -709,7 +742,9 @@ def show_post_requests(mlist, id, info, total, count, form): def process_form(mlist, doc, cgidata): + global ssort senderactions = {} + badaddrs = [] # Sender-centric actions for k in cgidata.keys(): for prefix in ('senderaction-', 'senderpreserve-', 'senderforward-', @@ -729,6 +764,8 @@ def process_form(mlist, doc, cgidata): discardalldefersp = cgidata.getvalue('discardalldefersp', 0) except ValueError: discardalldefersp = 0 + # Get the summary sequence + ssort = int(cgidata.getvalue('summary_sort', SSENDER)) for sender in senderactions.keys(): actions = senderactions[sender] # Handle what to do about all this sender's held messages @@ -743,8 +780,8 @@ def process_form(mlist, doc, cgidata): preserve = actions.get('senderpreserve', 0) forward = actions.get('senderforward', 0) forwardaddr = actions.get('senderforwardto', '') - bysender = helds_by_sender(mlist) - for id in bysender.get(sender, []): + byskey = helds_by_skey(mlist, SSENDER) + for ptime, id in byskey.get((0, sender), []): if id not in senderactions[sender]['message_ids']: # It arrived after the page was displayed. Skip it. continue @@ -762,20 +799,27 @@ def process_form(mlist, doc, cgidata): # Now see if this sender should be added to one of the nonmember # sender filters. if actions.get('senderfilterp', 0): + # Check for an invalid sender address. try: - which = int(actions.get('senderfilter')) - except ValueError: - # Bogus form - which = 'ignore' - if which == mm_cfg.ACCEPT: - mlist.accept_these_nonmembers.append(sender) - elif which == mm_cfg.HOLD: - mlist.hold_these_nonmembers.append(sender) - elif which == mm_cfg.REJECT: - mlist.reject_these_nonmembers.append(sender) - elif which == mm_cfg.DISCARD: - mlist.discard_these_nonmembers.append(sender) - # Otherwise, it's a bogus form, so ignore it + Utils.ValidateEmail(sender) + except Errors.EmailAddressError: + # Don't check for dups. Report it once for each checked box. + badaddrs.append(sender) + else: + try: + which = int(actions.get('senderfilter')) + except ValueError: + # Bogus form + which = 'ignore' + if which == mm_cfg.ACCEPT: + mlist.accept_these_nonmembers.append(sender) + elif which == mm_cfg.HOLD: + mlist.hold_these_nonmembers.append(sender) + elif which == mm_cfg.REJECT: + mlist.reject_these_nonmembers.append(sender) + elif which == mm_cfg.DISCARD: + mlist.discard_these_nonmembers.append(sender) + # Otherwise, it's a bogus form, so ignore it # And now see if we're to clear the member's moderation flag. if actions.get('senderclearmodp', 0): try: @@ -785,8 +829,15 @@ def process_form(mlist, doc, cgidata): pass # And should this address be banned? if actions.get('senderbanp', 0): - if sender not in mlist.ban_list: - mlist.ban_list.append(sender) + # Check for an invalid sender address. + try: + Utils.ValidateEmail(sender) + except Errors.EmailAddressError: + # Don't check for dups. Report it once for each checked box. + badaddrs.append(sender) + else: + if sender not in mlist.ban_list: + mlist.ban_list.append(sender) # Now, do message specific actions banaddrs = [] erroraddrs = [] @@ -836,6 +887,8 @@ def process_form(mlist, doc, cgidata): if cgidata.getvalue(bankey): sender = mlist.GetRecord(request_id)[1] if sender not in mlist.ban_list: + # We don't need to validate the sender. An invalid address + # can't get here. mlist.ban_list.append(sender) # Handle the request id try: @@ -854,7 +907,14 @@ def process_form(mlist, doc, cgidata): doc.AddItem(Header(2, _('Database Updated...'))) if erroraddrs: for addr in erroraddrs: + addr = Utils.websafe(addr) doc.AddItem(`addr` + _(' is already a member') + '<br>') if banaddrs: for addr, patt in banaddrs: + addr = Utils.websafe(addr) doc.AddItem(_('%(addr)s is banned (matched: %(patt)s)') + '<br>') + if badaddrs: + for addr in badaddrs: + addr = Utils.websafe(addr) + doc.AddItem(`addr` + ': ' + _('Bad/Invalid email address') + + '<br>') diff --git a/Mailman/Defaults.py.in b/Mailman/Defaults.py.in index be1ac735..a7bf31e5 100755 --- a/Mailman/Defaults.py.in +++ b/Mailman/Defaults.py.in @@ -1,6 +1,6 @@ # -*- python -*- -# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2013 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 @@ -108,6 +108,10 @@ ALLOW_SITE_ADMIN_COOKIES = No # expire that many seconds following their last use. AUTHENTICATION_COOKIE_LIFETIME = 0 +# The following must be set to Yes to enable the 'author is list' feature. +# See DEFAULT_FROM_IS_LIST below. +ALLOW_FROM_IS_LIST = No + # Form lifetime is set against Cross Site Request Forgery. FORM_LIFETIME = hours(1) @@ -247,6 +251,13 @@ BROKEN_BROWSER_REPLACEMENTS = {'\x8b': '‹', # single left angle quote '\xbe': '¾', # > plus high order bit '\xa2': '¢', # " plus high order bit } +# +# Shall the admindb held message summary display the grouping and sorting +# option radio buttons? Set this in mm_cfg.py to one of the following: +# SSENDER -> Default to grouped and sorted by sender. +# SSENDERTIME -> Default to grouped by sender and sorted by time. +# STIME -> Default to ungrouped and sorted by time. +DISPLAY_HELD_SUMMARY_SORT_BUTTONS = No @@ -548,7 +559,10 @@ NNTP_REWRITE_DUPLICATE_HEADERS = [ # footer or scrubbing attachments or even reply-to munging can break these # signatures. It is generally felt that these signatures have value, even if # broken and even if the outgoing message is resigned. However, some sites -# may wish to remove these headers by setting this to Yes. +# may wish to remove these headers. Possible values and meanings are: +# No, 0, False -> do not remove headers. +# 1 -> remove headers only if the list's from_is_list setting is 1. +# Yes, 2, True -> always remove headers. REMOVE_DKIM_HEADERS = No # All `normal' messages which are delivered to the entire list membership go @@ -580,6 +594,7 @@ GLOBAL_PIPELINE = [ # (outgoing) path, finally leaving the message in the outgoing queue. 'AfterDelivery', 'Acknowledge', + 'WrapMessage', 'ToOutgoing', ] @@ -968,6 +983,27 @@ USER_FRIENDLY_PASSWORDS = Yes MEMBER_PASSWORD_LENGTH = 8 ADMIN_PASSWORD_LENGTH = 10 +# The following headers are always removed from posts to anonymous lists as +# they can reveal the identity of the poster or at least the poster's domain. +# +# From:, Reply-To:, Sender:, Return-Path:, X-Originating-Email:, Received:, +# Message-ID: and X-Envelope-From:. +# +# In addition, Return-Receipt-To:, Disposition-Notification-To:, +# X-Confirm-Reading-To: and X-Pmrqc: headers are removed from all posts as +# they can be used to fish for list membership in addition to possibly +# revealing sender information. +# +# In addition to the above removals, all other headers except those matching +# regular expressions in the following setting are also removed. The default +# setting below keeps all non X- headers, those X- headers added by Mailman +# and any X-Spam- headers. +ANONYMOUS_LIST_KEEP_HEADERS = ['^(?!x-)', '^x-mailman-', + '^x-content-filtered-by:', '^x-topics:', + '^x-ack:', '^x-beenthere:', + '^x-list-administrivia:', '^x-spam-', + ] + ##### @@ -1065,6 +1101,14 @@ DEFAULT_SEND_WELCOME_MSG = Yes # Send goodbye messages to unsubscribed members? DEFAULT_SEND_GOODBYE_MSG = Yes +# The following is a three way setting. +# 0 -> Do not rewrite the From: or wrap the message. +# 1 -> Rewrite the From: header of posts replacing the posters address with +# that of the list. Also see REMOVE_DKIM_HEADERS above. +# 2 -> Do not modify the From: of the message, but wrap the message in an outer +# message From the list address. +DEFAULT_FROM_IS_LIST = 0 + # Wipe sender information, and make it look like the list-admin # address sends all messages DEFAULT_ANONYMOUS_LIST = No @@ -1396,6 +1440,11 @@ UNSUBSCRIBE = 5 ACCEPT = 6 HOLD = 7 +# admindb summary sort button settings. All must evaluate to True. +SSENDER = 1 +SSENDERTIME = 2 +STIME = 3 + # Standard text field width TEXTFIELDWIDTH = 40 @@ -1515,6 +1564,7 @@ add_language('en', _('English (USA)'), 'us-ascii', 'ltr') add_language('es', _('Spanish (Spain)'), 'iso-8859-1', 'ltr') add_language('et', _('Estonian'), 'iso-8859-15', 'ltr') add_language('eu', _('Euskara'), 'iso-8859-15', 'ltr') # Basque +add_language('fa', _('Persian'), 'utf-8', 'rtl') add_language('fi', _('Finnish'), 'iso-8859-1', 'ltr') add_language('fr', _('French'), 'iso-8859-1', 'ltr') add_language('gl', _('Galician'), 'utf-8', 'ltr') diff --git a/Mailman/Deliverer.py b/Mailman/Deliverer.py index 0f8f26b8..2e65e87f 100644 --- a/Mailman/Deliverer.py +++ b/Mailman/Deliverer.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2006 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2013 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 @@ -81,6 +81,7 @@ your membership administrative address, %(addr)s.''')) def SendUnsubscribeAck(self, addr, lang): realname = self.real_name + i18n.set_language(lang) msg = Message.UserNotification( self.GetMemberAdminEmail(addr), self.GetBouncesEmail(), _('You have been unsubscribed from the %(realname)s mailing list'), diff --git a/Mailman/Gui/Digest.py b/Mailman/Gui/Digest.py index f7722019..77691aee 100644 --- a/Mailman/Gui/Digest.py +++ b/Mailman/Gui/Digest.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2013 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 @@ -56,8 +56,8 @@ class Digest(GUIBase): _('When receiving digests, which format is default?')), ('digest_size_threshhold', mm_cfg.Number, 3, 0, - _('How big in Kb should a digest be before it gets sent out?')), - # Should offer a 'set to 0' for no size threshhold. + _('How big in Kb should a digest be before it gets sent out?' + ' 0 implies no maximum size.')), ('digest_send_periodic', mm_cfg.Radio, (_('No'), _('Yes')), 1, _('Should a digest be dispatched daily when the size threshold ' diff --git a/Mailman/Gui/GUIBase.py b/Mailman/Gui/GUIBase.py index a365acaf..9a8b68fb 100644 --- a/Mailman/Gui/GUIBase.py +++ b/Mailman/Gui/GUIBase.py @@ -63,6 +63,7 @@ class GUIBase: if isinstance(val, ListType): return val addrs = [] + bad_addrs = [] for addr in [s.strip() for s in val.split(NL)]: # Discard empty lines if not addr: @@ -77,22 +78,24 @@ class GUIBase: try: re.compile(addr) except re.error: - raise ValueError + bad_addrs.append(addr) elif (wtype == mm_cfg.EmailListEx and addr.startswith('@') and property.endswith('_these_nonmembers')): # XXX Needs to be reviewed for list@domain names. # don't reference your own list if addr[1:] == mlist.internal_name(): - raise ValueError + bad_addrs.append(addr) # check for existence of list? For now allow # reference to list before creating it. else: - raise + bad_addrs.append(addr) if property in ('regular_exclude_lists', 'regular_include_lists'): if addr.lower() == mlist.GetListEmail().lower(): - raise Errors.EmailAddressError + bad_addrs.append(addr) addrs.append(addr) + if bad_addrs: + raise Errors.EmailAddressError, ', '.join(bad_addrs) return addrs # This is a host name, i.e. verbatim if wtype == mm_cfg.Host: @@ -168,9 +171,9 @@ class GUIBase: except ValueError: doc.addError(_('Invalid value for variable: %(property)s')) # This is the parent of MMBadEmailError and MMHostileAddress - except Errors.EmailAddressError: + except Errors.EmailAddressError, error: doc.addError( - _('Bad email address for option %(property)s: %(val)s')) + _('Bad email address for option %(property)s: %(error)s')) else: # Set the attribute, which will normally delegate to the mlist self._setValue(mlist, property, val, doc) diff --git a/Mailman/Gui/General.py b/Mailman/Gui/General.py index e9f8f9b5..24bc009a 100644 --- a/Mailman/Gui/General.py +++ b/Mailman/Gui/General.py @@ -1,4 +1,4 @@ -# Copyright (C) 2001-2011 by the Free Software Foundation, Inc. +# Copyright (C) 2001-2013 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 @@ -153,7 +153,25 @@ class General(GUIBase): directive. eg.; [listname %%d] -> [listname 123] (listname %%05d) -> (listname 00123) """)), + ] + if mm_cfg.ALLOW_FROM_IS_LIST: + rtn.append( + ('from_is_list', mm_cfg.Radio, + (_('No'), _('Mung From'), _('Wrap Message')), 0, + _("""Replace the sender with the list address to conform with + policies like ADSP and DMARC. It replaces the poster's + address in the From: header with the list address and adds the + poster to the Reply-To: header, but the anonymous_list and + Reply-To: header munging settings below take priority. If + setting this to Yes, it is advised to set the MTA to DKIM sign + all emails.""") + + _("""<br>If this is set to Wrap Message, just wrap the message + in an outer message From: the list with Content-Type: + message/rfc822.""")) + ) + + rtn.extend([ ('anonymous_list', mm_cfg.Radio, (_('No'), _('Yes')), 0, _("""Hide the sender of a message, replacing it with the list address (Removes From, Sender and Reply-To fields)""")), @@ -374,7 +392,7 @@ class General(GUIBase): useful for selecting among alternative names of a host that has multiple addresses.""")), - ] + ]) if mm_cfg.ALLOW_RFC2369_OVERRIDES: rtn.append( diff --git a/Mailman/Handlers/AvoidDuplicates.py b/Mailman/Handlers/AvoidDuplicates.py index 038034c7..549d8e79 100644 --- a/Mailman/Handlers/AvoidDuplicates.py +++ b/Mailman/Handlers/AvoidDuplicates.py @@ -24,6 +24,7 @@ warning header, or pass it through, depending on the user's preferences. from email.Utils import getaddresses, formataddr from Mailman import mm_cfg +from Mailman.Handlers.CookHeaders import change_header COMMASPACE = ', ' @@ -95,6 +96,10 @@ def process(mlist, msg, msgdata): # Set the new list of recipients msgdata['recips'] = newrecips # RFC 2822 specifies zero or one CC header - del msg['cc'] if ccaddrs: - msg['Cc'] = COMMASPACE.join([formataddr(i) for i in ccaddrs.values()]) + change_header('Cc', + COMMASPACE.join([formataddr(i) for i in ccaddrs.values()]), + mlist, msg, msgdata) + else: + del msg['cc'] + diff --git a/Mailman/Handlers/Cleanse.py b/Mailman/Handlers/Cleanse.py index 725cb41b..c3e7aa43 100644 --- a/Mailman/Handlers/Cleanse.py +++ b/Mailman/Handlers/Cleanse.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2010 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2013 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 @@ -19,12 +19,32 @@ import re -from email.Utils import formataddr +from email.Utils import formataddr, getaddresses, parseaddr +from Mailman import mm_cfg from Mailman.Utils import unique_message_id from Mailman.Logging.Syslog import syslog from Mailman.Handlers.CookHeaders import uheader +cres = [] +for regexp in mm_cfg.ANONYMOUS_LIST_KEEP_HEADERS: + try: + cres.append(re.compile(regexp, re.IGNORECASE)) + except re.error, e: + syslog('error', + 'ANONYMOUS_LIST_KEEP_HEADERS: ignored bad regexp %s: %s', + regexp, e) + +def remove_nonkeepers(msg): + for hdr in msg.keys(): + keep = False + for cre in cres: + if cre.search(hdr): + keep = True + break + if not keep: + del msg[hdr] + def process(mlist, msg, msgdata): # Always remove this header from any outgoing messages. Be sure to do @@ -53,6 +73,10 @@ def process(mlist, msg, msgdata): # And so can the message-id so replace it. del msg['message-id'] msg['Message-ID'] = unique_message_id(mlist) + # And something sets this + del msg['x-envelope-from'] + # And now remove all but the keepers. + remove_nonkeepers(msg) i18ndesc = str(uheader(mlist, mlist.description, 'From')) msg['From'] = formataddr((i18ndesc, mlist.GetListEmail())) msg['Reply-To'] = mlist.GetListEmail() diff --git a/Mailman/Handlers/CleanseDKIM.py b/Mailman/Handlers/CleanseDKIM.py index c4b06613..0df2d97f 100644 --- a/Mailman/Handlers/CleanseDKIM.py +++ b/Mailman/Handlers/CleanseDKIM.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2007 by the Free Software Foundation, Inc. +# Copyright (C) 2006-2013 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 @@ -29,8 +29,13 @@ from Mailman import mm_cfg def process(mlist, msg, msgdata): - if mm_cfg.REMOVE_DKIM_HEADERS: - del msg['domainkey-signature'] - del msg['dkim-signature'] - del msg['authentication-results'] + if not mm_cfg.REMOVE_DKIM_HEADERS: + return + if (mm_cfg.ALLOW_FROM_IS_LIST and + mm_cfg.REMOVE_DKIM_HEADERS == 1 and + mlist.from_is_list != 1): + return + del msg['domainkey-signature'] + del msg['dkim-signature'] + del msg['authentication-results'] diff --git a/Mailman/Handlers/CookHeaders.py b/Mailman/Handlers/CookHeaders.py index a2096172..150b4922 100755 --- a/Mailman/Handlers/CookHeaders.py +++ b/Mailman/Handlers/CookHeaders.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2011 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2013 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 @@ -64,13 +64,20 @@ def uheader(mlist, s, header_name=None, continuation_ws='\t', maxlinelen=None): charset = 'us-ascii' return Header(s, charset, maxlinelen, header_name, continuation_ws) +def change_header(name, value, mlist, msg, msgdata, delete=True, repl=True): + if mm_cfg.ALLOW_FROM_IS_LIST and mlist.from_is_list == 2: + msgdata.setdefault('add_header', {})[name] = value + elif repl or not msg.has_key(name): + if delete: + del msg[name] + msg[name] = value + def process(mlist, msg, msgdata): # Set the "X-Ack: no" header if noack flag is set. if msgdata.get('noack'): - del msg['x-ack'] - msg['X-Ack'] = 'no' + change_header('X-Ack', 'no', mlist, msg, msgdata) # Because we're going to modify various important headers in the email # message, we want to save some of the information in the msgdata # dictionary for later. Specifically, the sender header will get waxed, @@ -87,7 +94,8 @@ def process(mlist, msg, msgdata): pass # Mark message so we know we've been here, but leave any existing # X-BeenThere's intact. - msg['X-BeenThere'] = mlist.GetListEmail() + change_header('X-BeenThere', mlist.GetListEmail(), + mlist, msg, msgdata, delete=False) # Add Precedence: and other useful headers. None of these are standard # and finding information on some of them are fairly difficult. Some are # just common practice, and we'll add more here as they become necessary. @@ -101,12 +109,32 @@ def process(mlist, msg, msgdata): # known exploits in a particular version of Mailman and we know a site is # using such an old version, they may be vulnerable. It's too easy to # edit the code to add a configuration variable to handle this. - if not msg.has_key('x-mailman-version'): - msg['X-Mailman-Version'] = mm_cfg.VERSION + change_header('X-Mailman-Version', mm_cfg.VERSION, + mlist, msg, msgdata, repl=False) # We set "Precedence: list" because this is the recommendation from the # sendmail docs, the most authoritative source of this header's semantics. - if not msg.has_key('precedence'): - msg['Precedence'] = 'list' + change_header('Precedence', 'list', + mlist, msg, msgdata, repl=False) + # Do we change the from so the list takes ownership of the email + if mm_cfg.ALLOW_FROM_IS_LIST and mlist.from_is_list: + realname, email = parseaddr(msg['from']) + replies = getaddresses(msg.get('reply-to', '')) + reply_addrs = [x[1].lower() for x in replies] + if reply_addrs: + if email.lower() not in reply_addrs: + rt = msg['reply-to'] + ', ' + msg['from'] + else: + rt = msg['reply-to'] + else: + rt = msg['from'] + change_header('Reply-To', rt, mlist, msg, msgdata) + change_header('From', + formataddr(('%s via %s' % (realname, mlist.real_name), + mlist.GetListEmail())), + mlist, msg, msgdata) + if mlist.from_is_list != 2: + del msg['sender'] + #MAS ?? mlist.include_sender_header = 0 # Reply-To: munging. Do not do this if the message is "fast tracked", # meaning it is internally crafted and delivered to a specific user. BAW: # Yuck, I really hate this feature but I've caved under the sheer pressure @@ -142,12 +170,14 @@ def process(mlist, msg, msgdata): if mlist.reply_goes_to_list == 1: i18ndesc = uheader(mlist, mlist.description, 'Reply-To') add((str(i18ndesc), mlist.GetListEmail())) - del msg['reply-to'] # Don't put Reply-To: back if there's nothing to add! if new: # Preserve order - msg['Reply-To'] = COMMASPACE.join( - [formataddr(pair) for pair in new]) + change_header('Reply-To', + COMMASPACE.join([formataddr(pair) for pair in new]), + mlist, msg, msgdata) + else: + del msg['reply-to'] # The To field normally contains the list posting address. However # when messages are fully personalized, that header will get # overwritten with the address of the recipient. We need to get the @@ -157,9 +187,11 @@ def process(mlist, msg, msgdata): # Cc header. BAW: should we force it into a Reply-To header in the # above code? # Also skip Cc if this is an anonymous list as list posting address - # is already in From and Reply-To in this case. + # is already in From and Reply-To in this case and similarly for + # an 'author is list' list. if mlist.personalize == 2 and mlist.reply_goes_to_list <> 1 \ - and not mlist.anonymous_list: + and not mlist.anonymous_list and not (mlist.from_is_list and + mm_cfg.ALLOW_FROM_IS_LIST): # Watch out for existing Cc headers, merge, and remove dups. Note # that RFC 2822 says only zero or one Cc header is allowed. new = [] @@ -168,6 +200,9 @@ def process(mlist, msg, msgdata): add(pair) i18ndesc = uheader(mlist, mlist.description, 'Cc') add((str(i18ndesc), mlist.GetListEmail())) + # We don't worry about what AvoidDuplicates may have done with a + # Cc: header or using change_header here since we never get here + # if from_is_list is allowed and True. del msg['Cc'] msg['Cc'] = COMMASPACE.join([formataddr(pair) for pair in new]) # Add list-specific headers as defined in RFC 2369 and RFC 2919, but only @@ -191,8 +226,7 @@ def process(mlist, msg, msgdata): # without desc we need to ensure the MUST brackets listid_h = '<%s>' % listid # We always add a List-ID: header. - del msg['list-id'] - msg['List-Id'] = listid_h + change_header('List-Id', listid_h, mlist, msg, msgdata) # For internally crafted messages, we also add a (nonstandard), # "X-List-Administrivia: yes" header. For all others (i.e. those coming # from list posts), we add a bunch of other RFC 2369 headers. @@ -219,13 +253,12 @@ def process(mlist, msg, msgdata): # First we delete any pre-existing headers because the RFC permits only # one copy of each, and we want to be sure it's ours. for h, v in headers.items(): - del msg[h] # Wrap these lines if they are too long. 78 character width probably # shouldn't be hardcoded, but is at least text-MUA friendly. The # adding of 2 is for the colon-space separator. if len(h) + 2 + len(v) > 78: v = CONTINUATION.join(v.split(', ')) - msg[h] = v + change_header(h, v, mlist, msg, msgdata) @@ -302,8 +335,7 @@ def prefix_subject(mlist, msg, msgdata): h = u' '.join([prefix, subject]) h = h.encode('us-ascii') h = uheader(mlist, h, 'Subject', continuation_ws=ws) - del msg['subject'] - msg['Subject'] = h + change_header('Subject', h, mlist, msg, msgdata) ss = u' '.join([recolon, subject]) ss = ss.encode('us-ascii') ss = uheader(mlist, ss, 'Subject', continuation_ws=ws) @@ -321,8 +353,7 @@ def prefix_subject(mlist, msg, msgdata): # TK: Subject is concatenated and unicode string. subject = subject.encode(cset, 'replace') h.append(subject, cset) - del msg['subject'] - msg['Subject'] = h + change_header('Subject', h, mlist, msg, msgdata) ss = uheader(mlist, recolon, 'Subject', continuation_ws=ws) ss.append(subject, cset) msgdata['stripped_subject'] = ss diff --git a/Mailman/Handlers/SpamDetect.py b/Mailman/Handlers/SpamDetect.py index 8d26da03..9e01f623 100644 --- a/Mailman/Handlers/SpamDetect.py +++ b/Mailman/Handlers/SpamDetect.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2013 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 @@ -27,6 +27,7 @@ TBD: This needs to be made more configurable and robust. import re +from email.Errors import HeaderParseError from email.Header import decode_header from Mailman import mm_cfg @@ -68,7 +69,10 @@ def getDecodedHeaders(msg, cset='utf-8'): headers = '' for h, v in msg.items(): uvalue = u'' - v = decode_header(re.sub('\n\s', ' ', v)) + try: + v = decode_header(re.sub('\n\s', ' ', v)) + except HeaderParseError: + v = [(v, 'us-ascii')] for frag, cs in v: if not cs: cs = 'us-ascii' diff --git a/Mailman/Handlers/Tagger.py b/Mailman/Handlers/Tagger.py index 38a8e465..cb90bfc4 100644 --- a/Mailman/Handlers/Tagger.py +++ b/Mailman/Handlers/Tagger.py @@ -1,4 +1,4 @@ -# Copyright (C) 2001-2011 by the Free Software Foundation, Inc. +# Copyright (C) 2001-2013 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 @@ -27,6 +27,7 @@ from email.Header import decode_header from Mailman import Utils from Mailman.Logging.Syslog import syslog +from Mailman.Handlers.CookHeaders import change_header CRNL = '\r\n' EMPTYSTRING = '' @@ -69,8 +70,9 @@ def process(mlist, msg, msgdata): break if hits: msgdata['topichits'] = hits.keys() - msg['X-Topics'] = NLTAB.join(hits.keys()) - + change_header('X-Topics', NLTAB.join(hits.keys()), + mlist, msg, msgdata, delete=False) + def scanbody(msg, numlines=None): diff --git a/Mailman/Handlers/ToDigest.py b/Mailman/Handlers/ToDigest.py index edbf40dc..2027a46c 100644 --- a/Mailman/Handlers/ToDigest.py +++ b/Mailman/Handlers/ToDigest.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2011 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2013 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 @@ -86,7 +86,8 @@ def process(mlist, msg, msgdata): # whether the size threshold has been reached. mboxfp.flush() size = os.path.getsize(mboxfile) - if size / 1024.0 >= mlist.digest_size_threshhold: + if (mlist.digest_size_threshhold > 0 and + size / 1024.0 >= mlist.digest_size_threshhold): # This is a bit of a kludge to get the mbox file moved to the digest # queue directory. try: diff --git a/Mailman/Handlers/WrapMessage.py b/Mailman/Handlers/WrapMessage.py new file mode 100644 index 00000000..68c89ff2 --- /dev/null +++ b/Mailman/Handlers/WrapMessage.py @@ -0,0 +1,57 @@ +# Copyright (C) 2013 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. + +"""Wrap the message in an outer message/rfc822 part and transfer/add +some headers from the original. +""" + +import copy + +from Mailman import mm_cfg +from Mailman.Utils import unique_message_id +from Mailman.Message import Message + +# Headers from the original that we want to keep in the wrapper. +KEEPERS = ('to', + 'in-reply-to', + 'references', + 'x-mailman-approved-at', + ) + + + +def process(mlist, msg, msgdata): + if not mm_cfg.ALLOW_FROM_IS_LIST or mlist.from_is_list != 2: + return + + # There are various headers in msg that we don't want, so we basically + # make a copy of the msg, then delete almost everything and set/copy + # what we want. + omsg = copy.deepcopy(msg) + for key in msg.keys(): + if key.lower() not in KEEPERS: + del msg[key] + msg['MIME-Version'] = '1.0' + msg['Content-Type'] = 'message/rfc822' + msg['Content-Disposition'] = 'inline' + msg['Message-ID'] = unique_message_id(mlist) + # Add the headers from CookHeaders. + for k, v in msgdata['add_header'].items(): + msg[k] = v + # And set the payload. + msg.set_payload(omsg.as_string()) + diff --git a/Mailman/MailList.py b/Mailman/MailList.py index 4a3e92a8..d13ca169 100755 --- a/Mailman/MailList.py +++ b/Mailman/MailList.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2013 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 @@ -347,6 +347,7 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, self.bounce_matching_headers = \ mm_cfg.DEFAULT_BOUNCE_MATCHING_HEADERS self.header_filter_rules = [] + self.from_is_list = mm_cfg.DEFAULT_FROM_IS_LIST self.anonymous_list = mm_cfg.DEFAULT_ANONYMOUS_LIST internalname = self.internal_name() self.real_name = internalname[0].upper() + internalname[1:] diff --git a/Mailman/Queue/BounceRunner.py b/Mailman/Queue/BounceRunner.py index d219d6e9..fcd6e3fb 100644 --- a/Mailman/Queue/BounceRunner.py +++ b/Mailman/Queue/BounceRunner.py @@ -1,4 +1,4 @@ -# Copyright (C) 2001-2008 by the Free Software Foundation, Inc. +# Copyright (C) 2001-2013 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 @@ -244,6 +244,7 @@ class BounceRunner(Runner, BounceMixin): return # If that still didn't return us any useful addresses, then send it on # or discard it. + addrs = filter(None, addrs) if not addrs: syslog('bounce', '%s: bounce message w/no discernable addresses: %s', @@ -254,7 +255,8 @@ class BounceRunner(Runner, BounceMixin): # BAW: It's possible that there are None's in the list of addresses, # although I'm unsure how that could happen. Possibly ScanMessages() # can let None's sneak through. In any event, this will kill them. - addrs = filter(None, addrs) + # addrs = filter(None, addrs) + # MAS above filter moved up so we don't try to queue an empty list. self._queue_bounces(mlist.internal_name(), addrs, msg) _doperiodic = BounceMixin._doperiodic diff --git a/Mailman/Queue/Switchboard.py b/Mailman/Queue/Switchboard.py index bd1cd357..a2c31263 100644 --- a/Mailman/Queue/Switchboard.py +++ b/Mailman/Queue/Switchboard.py @@ -1,4 +1,4 @@ -# Copyright (C) 2001-2008 by the Free Software Foundation, Inc. +# Copyright (C) 2001-2013 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 @@ -184,8 +184,8 @@ class Switchboard: else: os.unlink(bakfile) except EnvironmentError, e: - syslog('error', 'Failed to unlink/preserve backup file: %s', - bakfile) + syslog('error', 'Failed to unlink/preserve backup file: %s\n%s', + bakfile, e) def files(self, extension='.pck'): times = {} diff --git a/Mailman/Version.py b/Mailman/Version.py index 4cd2d551..1df71cdd 100755..100644 --- a/Mailman/Version.py +++ b/Mailman/Version.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2011 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2013 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 @@ -16,7 +16,7 @@ # USA. # Mailman version -VERSION = '2.1.15' +VERSION = '2.1.16' # And as a hex number in the manner of PY_VERSION_HEX ALPHA = 0xa @@ -28,7 +28,7 @@ FINAL = 0xf MAJOR_REV = 2 MINOR_REV = 1 -MICRO_REV = 15 +MICRO_REV = 16 REL_LEVEL = FINAL # at most 15 beta releases! REL_SERIAL = 0 @@ -37,7 +37,7 @@ HEX_VERSION = ((MAJOR_REV << 24) | (MINOR_REV << 16) | (MICRO_REV << 8) | (REL_LEVEL << 4) | (REL_SERIAL << 0)) # config.pck schema version number -DATA_FILE_VERSION = 100 +DATA_FILE_VERSION = 102 # qfile/*.db schema version number QFILE_SCHEMA_VERSION = 3 diff --git a/Mailman/versions.py b/Mailman/versions.py index 7973e427..db5b2914 100755 --- a/Mailman/versions.py +++ b/Mailman/versions.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2011 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2013 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 @@ -313,6 +313,9 @@ def UpdateOldVars(l, stored_state): pass else: l.digest_members[k] = 0 + # from_is_list was called author_is_list in 2.1.16rc2 (only). + PreferStored('author_is_list', 'from_is_list', + mm_cfg.DEFAULT_FROM_IS_LIST) |