aboutsummaryrefslogtreecommitdiffstats
path: root/Mailman
diff options
context:
space:
mode:
authorJim Popovitch <jimpop@gmail.com>2013-11-17 01:56:29 +0000
committerJim Popovitch <jimpop@gmail.com>2013-11-17 01:56:29 +0000
commit06d15b0a9949652f696d19903cef2a235ff3a428 (patch)
tree55a9c12de799cc056da986aafe3b8e66d0cbf94b /Mailman
parentbc05ad4e81bd2ce9ec0f36e5112eadf607a49195 (diff)
parentded18dda2b2f9ba00f8780b4eba6c398c5bff838 (diff)
downloadmailman2-06d15b0a9949652f696d19903cef2a235ff3a428.tar.gz
mailman2-06d15b0a9949652f696d19903cef2a235ff3a428.tar.xz
mailman2-06d15b0a9949652f696d19903cef2a235ff3a428.zip
Upstream changes
Diffstat (limited to '')
-rw-r--r--Mailman/Archiver/HyperArch.py5
-rw-r--r--Mailman/Bouncers/SimpleMatch.py6
-rw-r--r--Mailman/Bouncers/Yahoo.py35
-rw-r--r--Mailman/CSRFcheck.py7
-rw-r--r--Mailman/Cgi/admindb.py136
-rwxr-xr-xMailman/Defaults.py.in54
-rw-r--r--Mailman/Deliverer.py3
-rw-r--r--Mailman/Gui/Digest.py6
-rw-r--r--Mailman/Gui/GUIBase.py15
-rw-r--r--Mailman/Gui/General.py22
-rw-r--r--Mailman/Handlers/AvoidDuplicates.py9
-rw-r--r--Mailman/Handlers/Cleanse.py28
-rw-r--r--Mailman/Handlers/CleanseDKIM.py15
-rwxr-xr-xMailman/Handlers/CookHeaders.py73
-rw-r--r--Mailman/Handlers/SpamDetect.py8
-rw-r--r--Mailman/Handlers/Tagger.py8
-rw-r--r--Mailman/Handlers/ToDigest.py5
-rw-r--r--Mailman/Handlers/WrapMessage.py57
-rwxr-xr-xMailman/MailList.py3
-rw-r--r--Mailman/Queue/BounceRunner.py6
-rw-r--r--Mailman/Queue/Switchboard.py6
-rw-r--r--[-rwxr-xr-x]Mailman/Version.py8
-rwxr-xr-xMailman/versions.py5
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(['&nbsp;', '&nbsp;'])
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': '&#8249;', # single left angle quote
'\xbe': '&#190;', # > plus high order bit
'\xa2': '&#162;', # " 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)