From 9eecc5d5d26373741ba5dfc03872098c3a6e602c Mon Sep 17 00:00:00 2001 From: Mark Sapiro Date: Wed, 17 Feb 2016 10:43:31 -0800 Subject: Acknowledge option will now be honored for posts to anonymous lists --- Mailman/Handlers/Cleanse.py | 5 ++++- Mailman/Handlers/CookHeaders.py | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) (limited to 'Mailman') diff --git a/Mailman/Handlers/Cleanse.py b/Mailman/Handlers/Cleanse.py index c684cd19..5270bb5a 100644 --- a/Mailman/Handlers/Cleanse.py +++ b/Mailman/Handlers/Cleanse.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2015 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2016 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 @@ -60,6 +60,9 @@ def process(mlist, msg, msgdata): del msg['x-approve'] # Also remove this header since it can contain a password del msg['urgent'] + # If we're anonymizing, we need to save the sender here, and we may as + # well do it for all. + msgdata['original_sender'] = msg.get_sender() # We remove other headers from anonymous lists if mlist.anonymous_list: syslog('post', 'post to %s from %s anonymized', diff --git a/Mailman/Handlers/CookHeaders.py b/Mailman/Handlers/CookHeaders.py index 8b371b77..59eb67b7 100755 --- a/Mailman/Handlers/CookHeaders.py +++ b/Mailman/Handlers/CookHeaders.py @@ -98,7 +98,9 @@ def process(mlist, msg, msgdata): # message, we want to save some of the information in the msgdata # dictionary for later. Specifically, the sender header will get waxed, # but we need it for the Acknowledge module later. - msgdata['original_sender'] = msg.get_sender() + # We may have already saved it; if so, don't clobber it here. + if 'original_sender' not in msgdata: + msgdata['original_sender'] = msg.get_sender() # VirginRunner sets _fasttrack for internally crafted messages. fasttrack = msgdata.get('_fasttrack') if not msgdata.get('isdigest') and not fasttrack: -- cgit v1.2.3 From 6aaafb6c05840389f1dd9139da9694f3b43c57df Mon Sep 17 00:00:00 2001 From: Yasuhito FUTATSUKI at POEM Date: Mon, 22 Feb 2016 17:51:37 +0900 Subject: Importing locale patch for command line utils, from RHEL6 rpm source (for -japan-poem, originally imported from 2.1.12-18 package for RHEL6 rpm source) --- Mailman/MTA/Manual.py | 8 ++++---- Mailman/MTA/Postfix.py | 14 +++++++------- Mailman/i18n.py | 27 +++++++++++++++++++++++++-- 3 files changed, 36 insertions(+), 13 deletions(-) (limited to 'Mailman') diff --git a/Mailman/MTA/Manual.py b/Mailman/MTA/Manual.py index 92e1c03c..0abde2e2 100644 --- a/Mailman/MTA/Manual.py +++ b/Mailman/MTA/Manual.py @@ -25,7 +25,7 @@ from Mailman import mm_cfg from Mailman import Message from Mailman import Utils from Mailman.Queue.sbcache import get_switchboard -from Mailman.i18n import _ +from Mailman.i18n import _, C_ from Mailman.MTA.Utils import makealiases try: @@ -74,12 +74,12 @@ Here are the entries for the /etc/aliases file: outfp = sfp else: if not quiet: - print _("""\ + print C_("""\ To finish creating your mailing list, you must edit your /etc/aliases (or equivalent) file by adding the following lines, and possibly running the `newaliases' program: """) - print _("""\ + print C_("""\ ## %(listname)s mailing list""") outfp = sys.stdout # Common path @@ -120,7 +120,7 @@ Here are the entries in the /etc/aliases file that should be removed: """) outfp = sfp else: - print _(""" + print C_(""" To finish removing your mailing list, you must edit your /etc/aliases (or equivalent) file by removing the following lines, and possibly running the `newaliases' program: diff --git a/Mailman/MTA/Postfix.py b/Mailman/MTA/Postfix.py index 3f5c9984..b662d4ce 100644 --- a/Mailman/MTA/Postfix.py +++ b/Mailman/MTA/Postfix.py @@ -27,7 +27,7 @@ from stat import * from Mailman import mm_cfg from Mailman import Utils from Mailman import LockFile -from Mailman.i18n import _ +from Mailman.i18n import C_ from Mailman.MTA.Utils import makealiases from Mailman.Logging.Syslog import syslog @@ -358,7 +358,7 @@ def checkperms(state): targetmode = S_IFREG | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP for file in ALIASFILE, VIRTFILE: if state.VERBOSE: - print _('checking permissions on %(file)s') + print C_('checking permissions on %(file)s') stat = None try: stat = os.stat(file) @@ -368,9 +368,9 @@ def checkperms(state): if stat and (stat[ST_MODE] & targetmode) <> targetmode: state.ERRORS += 1 octmode = oct(stat[ST_MODE]) - print _('%(file)s permissions must be 066x (got %(octmode)s)'), + print C_('%(file)s permissions must be 066x (got %(octmode)s)'), if state.FIX: - print _('(fixing)') + print C_('(fixing)') os.chmod(file, stat[ST_MODE] | targetmode) else: print @@ -386,7 +386,7 @@ def checkperms(state): raise continue if state.VERBOSE: - print _('checking ownership of %(dbfile)s') + print C_('checking ownership of %(dbfile)s') user = mm_cfg.MAILMAN_USER ownerok = stat[ST_UID] == pwd.getpwnam(user)[2] if not ownerok: @@ -394,10 +394,10 @@ def checkperms(state): owner = pwd.getpwuid(stat[ST_UID])[0] except KeyError: owner = 'uid %d' % stat[ST_UID] - print _('%(dbfile)s owned by %(owner)s (must be owned by %(user)s'), + print C_('%(dbfile)s owned by %(owner)s (must be owned by %(user)s'), state.ERRORS += 1 if state.FIX: - print _('(fixing)') + print C_('(fixing)') uid = pwd.getpwnam(user)[2] gid = grp.getgrnam(mm_cfg.MAILMAN_GROUP)[2] os.chown(dbfile, uid, gid) diff --git a/Mailman/i18n.py b/Mailman/i18n.py index 5f926b77..0cfdb995 100644 --- a/Mailman/i18n.py +++ b/Mailman/i18n.py @@ -15,6 +15,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, # USA. +import locale import sys import time import gettext @@ -25,6 +26,15 @@ from Mailman.SafeDict import SafeDict _translation = None + +def _get_ctype_charset(): + old = locale.setlocale(locale.LC_CTYPE, '') + charset = locale.nl_langinfo(locale.CODESET) + locale.setlocale(locale.LC_CTYPE, old) + return charset + +_ctype_charset = _get_ctype_charset() + def set_language(language=None): @@ -54,7 +64,7 @@ if _translation is None: -def _(s): +def _(s, frame = 1): if s == '': return s assert s @@ -70,7 +80,7 @@ def _(s): # original string is 1) locals dictionary, 2) globals dictionary. # # First, get the frame of the caller - frame = sys._getframe(1) + frame = sys._getframe(frame) # A `safe' dictionary is used so we won't get an exception if there's a # missing key in the dictionary. dict = SafeDict(frame.f_globals.copy()) @@ -94,6 +104,19 @@ def _(s): return tns + +def tolocale(s): + global _ctype_charset + if isinstance(s, UnicodeType): + return s + source = _translation.charset () + if not source: + return s + return unicode(s, source, 'replace').encode(_ctype_charset, 'replace') + +def C_(s): + return tolocale(_(s, 2)) + def ctime(date): # Don't make these module globals since we have to do runtime translation -- cgit v1.2.3 From e7519290ad288df840bc86d8dcaea524407df964 Mon Sep 17 00:00:00 2001 From: Yasuhito FUTATSUKI at POEM Date: Mon, 22 Feb 2016 20:06:27 +0900 Subject: * add option to pick up C_() texts to make potfile * revise command line utils _()/C_() usage (not tested at all) --- Mailman/MTA/Postfix.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'Mailman') diff --git a/Mailman/MTA/Postfix.py b/Mailman/MTA/Postfix.py index b662d4ce..add5453e 100644 --- a/Mailman/MTA/Postfix.py +++ b/Mailman/MTA/Postfix.py @@ -406,9 +406,9 @@ def checkperms(state): if stat and (stat[ST_MODE] & targetmode) <> targetmode: state.ERRORS += 1 octmode = oct(stat[ST_MODE]) - print _('%(dbfile)s permissions must be 066x (got %(octmode)s)'), + print C_('%(dbfile)s permissions must be 066x (got %(octmode)s)'), if state.FIX: - print _('(fixing)') + print C_('(fixing)') os.chmod(dbfile, stat[ST_MODE] | targetmode) else: print -- cgit v1.2.3 From da9d83cf1291701261bec4a9faafbe01b60a72a4 Mon Sep 17 00:00:00 2001 From: Mark Sapiro Date: Wed, 24 Feb 2016 23:24:26 -0800 Subject: If DMARC lookup fails to find a policy, also try the Organizational Domain. --- Mailman/Utils.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) (limited to 'Mailman') diff --git a/Mailman/Utils.py b/Mailman/Utils.py index 1aa49954..0344ef58 100644 --- a/Mailman/Utils.py +++ b/Mailman/Utils.py @@ -1170,7 +1170,28 @@ def IsDMARCProhibited(mlist, email): at_sign = email.find('@') if at_sign < 1: return False - dmarc_domain = '_dmarc.' + email[at_sign+1:] + dparts = email[at_sign+1:].split('.') + # The following is a way of testing the "Organizational Domain" for DMARC + # policy if the From: domain doesn't publish a policy. What we're doing + # is clearly wrong. I.e., if the From: domain is a.b.c.example.com, we + # should lookup _dmarc.a.b.c.example.com and if no DMARC policy there, + # we should look up only _dmarc.example.com. The problem is not all + # Organizational Domains are two "words" and determining any particular + # Organizational Domain requires applying a non-trivial algorithm to a + # large, somewhat dynamic data set. What we do is look up all the + # intermediate domains on the theory that if _dmarc.a.b.c.example.com has + # no valid DMARC policy then the intermediates won't either. We will also + # err with a domain like x.y.x.co.uk. Here we will go to far and also look + # up _dmarc.co.uk which is also wrong but hopefully won't return a policy. + # This is clearly a flawed approach, but hopefully good enough. + while len(dparts) > 1: + x = _DMARCProhibited(mlist, email, '_dmarc.' + '.'.join(dparts)) + if x != 'continue': + return x + dparts = dparts[1:] + return False + +def _DMARCProhibited(mlist, email, dmarc_domain): try: resolver = dns.resolver.Resolver() @@ -1178,12 +1199,12 @@ def IsDMARCProhibited(mlist, email): resolver.lifetime = float(mm_cfg.DMARC_RESOLVER_LIFETIME) txt_recs = resolver.query(dmarc_domain, dns.rdatatype.TXT) except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): - return False + return 'continue' except DNSException, e: syslog('error', 'DNSException: Unable to query DMARC policy for %s (%s). %s', email, dmarc_domain, e.__class__) - return False + return 'continue' else: # people are already being dumb, don't trust them to provide honest DNS # where the answer section only contains what was asked for, nor to include @@ -1223,7 +1244,7 @@ def IsDMARCProhibited(mlist, email): dmarcs = filter(lambda n: n.startswith('v=DMARC1;'), results_by_name[name]) if len(dmarcs) == 0: - return False + return 'continue' if len(dmarcs) > 1: syslog('error', """RRset of TXT records for %s has %d v=DMARC1 entries; -- cgit v1.2.3 From 1736634b57aa1ba6864e255e088e64eeca9ca16a Mon Sep 17 00:00:00 2001 From: Mark Sapiro Date: Thu, 25 Feb 2016 19:08:37 -0800 Subject: Refactored OrganizationalDomain fix. --- Mailman/Utils.py | 98 ++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 81 insertions(+), 17 deletions(-) (limited to 'Mailman') diff --git a/Mailman/Utils.py b/Mailman/Utils.py index 0344ef58..4a5dddef 100644 --- a/Mailman/Utils.py +++ b/Mailman/Utils.py @@ -34,6 +34,7 @@ import time import errno import base64 import random +import urllib2 import urlparse import htmlentitydefs import email.Header @@ -1156,6 +1157,79 @@ def suspiciousHTML(html): return False +# The next functions read data from +# https://publicsuffix.org/list/public_suffix_list.dat and implement the +# algorithm at https://publicsuffix.org/list/ to find the "Organizational +# Domain corresponding to a From: domain. + +URL = 'https://publicsuffix.org/list/public_suffix_list.dat' +s_dict = {} + +def get_suffixes(url): + """This loads the data from the url argument into s_dict for use by +get_org_dom.""" + global s_dict + if s_dict: + return + try: + d = urllib2.urlopen(url) + except urllib2.URLError, e: + syslog('error', + 'Unable to retrieve data from %s: %s', + url, e) + return + for line in d.readlines(): + if not line or line.startswith(' ') or line.startswith('//'): + continue + line = re.sub(' .*', '', line.strip()) + if not line: + continue + parts = line.split('.') + if parts[0].startswith('!'): + exc = True + parts = [parts[0][1:]] + parts[1:] + else: + exc = False + parts.reverse() + k = '.'.join(parts) + s_dict[k] = exc + +def _get_dom(d, l): + """A helper to get a domain name consisting of the first l labels in d.""" + dom = d[:min(l+1, len(d))] + dom.reverse() + return '.'.join(dom) + +def get_org_dom(domain): + """Given a domain name, this returns the corresponding Organizational +Domain which may be the same as the input.""" + global s_dict + if not s_dict: + get_suffixes(URL) + hits = [] + d = domain.split('.') + d.reverse() + for k in s_dict.keys(): + ks = k.split('.') + if len(d) >= len(ks): + for i in range(len(ks)-1): + if d[i] != ks[i] and ks[i] != '*': + break + else: + if d[len(ks)-1] == ks[-1] or ks[-1] == '*': + hits.append(k) + if not hits: + return _get_dom(d, 1) + l = 0 + for k in hits: + if s_dict[k]: + # It's an exception + return _get_dom(d, len(k.split('.'))-1) + if len(k.split('.')) > l: + l = len(k.split('.')) + return _get_dom(d, l) + + # This takes an email address, and returns True if DMARC policy is p=reject # or possibly quarantine. def IsDMARCProhibited(mlist, email): @@ -1170,25 +1244,15 @@ def IsDMARCProhibited(mlist, email): at_sign = email.find('@') if at_sign < 1: return False - dparts = email[at_sign+1:].split('.') - # The following is a way of testing the "Organizational Domain" for DMARC - # policy if the From: domain doesn't publish a policy. What we're doing - # is clearly wrong. I.e., if the From: domain is a.b.c.example.com, we - # should lookup _dmarc.a.b.c.example.com and if no DMARC policy there, - # we should look up only _dmarc.example.com. The problem is not all - # Organizational Domains are two "words" and determining any particular - # Organizational Domain requires applying a non-trivial algorithm to a - # large, somewhat dynamic data set. What we do is look up all the - # intermediate domains on the theory that if _dmarc.a.b.c.example.com has - # no valid DMARC policy then the intermediates won't either. We will also - # err with a domain like x.y.x.co.uk. Here we will go to far and also look - # up _dmarc.co.uk which is also wrong but hopefully won't return a policy. - # This is clearly a flawed approach, but hopefully good enough. - while len(dparts) > 1: - x = _DMARCProhibited(mlist, email, '_dmarc.' + '.'.join(dparts)) + f_dom = email[at_sign+1:] + x = _DMARCProhibited(mlist, email, '_dmarc.' + f_dom) + if x != 'continue': + return x + o_dom = get_org_dom(f_dom) + if o_dom != f_dom: + x = _DMARCProhibited(mlist, email, '_dmarc.' + o_dom) if x != 'continue': return x - dparts = dparts[1:] return False def _DMARCProhibited(mlist, email, dmarc_domain): -- cgit v1.2.3 From 12737ecf786a8d0315bc6a9a4448831cc19eead2 Mon Sep 17 00:00:00 2001 From: Mark Sapiro Date: Thu, 25 Feb 2016 20:45:44 -0800 Subject: Further refactoring of the Organizational Domain fix. --- Mailman/Defaults.py.in | 7 +++++++ Mailman/Utils.py | 3 +-- 2 files changed, 8 insertions(+), 2 deletions(-) (limited to 'Mailman') diff --git a/Mailman/Defaults.py.in b/Mailman/Defaults.py.in index 8c5d9e7b..4881bbba 100755 --- a/Mailman/Defaults.py.in +++ b/Mailman/Defaults.py.in @@ -1130,6 +1130,13 @@ DMARC_RESOLVER_TIMEOUT = seconds(3) # The total time to spend trying to get an answer to the question. DMARC_RESOLVER_LIFETIME = seconds(5) +# A URL from which to retrieve the data for the algorithm that computes +# Organizational Domains for DMARC policy lookup purposes. This can be +# anything handled by the Python urllib2.urlopen function. See +# https://publicsuffix.org/list/ for info. +DMARC_ORGANIZATIONAL_DOMAIN_DATA_URL = \ +'https://publicsuffix.org/list/public_suffix_list.dat' + # Should the list server auto-moderate members who post too frequently # This is intended to stop people who join a list and then use a bot to # send many spam messages in a short interval. These are default settings diff --git a/Mailman/Utils.py b/Mailman/Utils.py index 4a5dddef..8a54bfc5 100644 --- a/Mailman/Utils.py +++ b/Mailman/Utils.py @@ -1162,7 +1162,6 @@ def suspiciousHTML(html): # algorithm at https://publicsuffix.org/list/ to find the "Organizational # Domain corresponding to a From: domain. -URL = 'https://publicsuffix.org/list/public_suffix_list.dat' s_dict = {} def get_suffixes(url): @@ -1205,7 +1204,7 @@ def get_org_dom(domain): Domain which may be the same as the input.""" global s_dict if not s_dict: - get_suffixes(URL) + get_suffixes(mm_cfg.DMARC_ORGANIZATIONAL_DOMAIN_DATA_URL) hits = [] d = domain.split('.') d.reverse() -- cgit v1.2.3 From a31d04a8b2bd1480cf774b00aeb8b1deb6469291 Mon Sep 17 00:00:00 2001 From: Mark Sapiro Date: Thu, 25 Feb 2016 22:09:08 -0800 Subject: Lower case domains for the Organizational Domain fix. --- Mailman/Utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'Mailman') diff --git a/Mailman/Utils.py b/Mailman/Utils.py index 8a54bfc5..682f5058 100644 --- a/Mailman/Utils.py +++ b/Mailman/Utils.py @@ -1178,12 +1178,12 @@ get_org_dom.""" url, e) return for line in d.readlines(): - if not line or line.startswith(' ') or line.startswith('//'): + if not line.strip() or line.startswith(' ') or line.startswith('//'): continue line = re.sub(' .*', '', line.strip()) if not line: continue - parts = line.split('.') + parts = line.lower().split('.') if parts[0].startswith('!'): exc = True parts = [parts[0][1:]] + parts[1:] @@ -1206,7 +1206,7 @@ Domain which may be the same as the input.""" if not s_dict: get_suffixes(mm_cfg.DMARC_ORGANIZATIONAL_DOMAIN_DATA_URL) hits = [] - d = domain.split('.') + d = domain.lower().split('.') d.reverse() for k in s_dict.keys(): ks = k.split('.') -- cgit v1.2.3 From 1833e1e51f4994d733c4ef3fca7c6ef7a4fd519e Mon Sep 17 00:00:00 2001 From: Mark Sapiro Date: Fri, 26 Feb 2016 14:02:44 -0800 Subject: Minor documentation clean up. --- Mailman/Defaults.py.in | 2 +- Mailman/Utils.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) (limited to 'Mailman') diff --git a/Mailman/Defaults.py.in b/Mailman/Defaults.py.in index 4881bbba..06b3e0ad 100755 --- a/Mailman/Defaults.py.in +++ b/Mailman/Defaults.py.in @@ -136,7 +136,7 @@ SUBSCRIBE_FORM_MIN_TIME = seconds(5) # in the installation. This supplements the individual list's ban_list. # For example, to ban xxx@aol.com and any @gmail.com address beginning with # yyy, set -# GLOBAL_BAN_LIST = ['xxx@aol.com', '^yyy.*@gmail\.com'] +# GLOBAL_BAN_LIST = ['xxx@aol.com', '^yyy.*@gmail\.com$'] GLOBAL_BAN_LIST = [] # Command that is used to convert text/html parts into plain text. This diff --git a/Mailman/Utils.py b/Mailman/Utils.py index 682f5058..f821f13a 100644 --- a/Mailman/Utils.py +++ b/Mailman/Utils.py @@ -1165,8 +1165,8 @@ def suspiciousHTML(html): s_dict = {} def get_suffixes(url): - """This loads the data from the url argument into s_dict for use by -get_org_dom.""" + """This loads and parses the data from the url argument into s_dict for + use by get_org_dom.""" global s_dict if s_dict: return @@ -1194,14 +1194,15 @@ get_org_dom.""" s_dict[k] = exc def _get_dom(d, l): - """A helper to get a domain name consisting of the first l labels in d.""" + """A helper to get a domain name consisting of the first l+1 labels + in d.""" dom = d[:min(l+1, len(d))] dom.reverse() return '.'.join(dom) def get_org_dom(domain): """Given a domain name, this returns the corresponding Organizational -Domain which may be the same as the input.""" + Domain which may be the same as the input.""" global s_dict if not s_dict: get_suffixes(mm_cfg.DMARC_ORGANIZATIONAL_DOMAIN_DATA_URL) -- cgit v1.2.3 From 056b6a968fb3f96ba3bf4dcc00b82370c043154e Mon Sep 17 00:00:00 2001 From: Mark Sapiro Date: Sat, 27 Feb 2016 20:38:34 -0800 Subject: Added switch to disable the l10n cset recoding. --- Mailman/Defaults.py.in | 7 +++++++ Mailman/i18n.py | 11 ++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) (limited to 'Mailman') diff --git a/Mailman/Defaults.py.in b/Mailman/Defaults.py.in index 06b3e0ad..04d7db8a 100755 --- a/Mailman/Defaults.py.in +++ b/Mailman/Defaults.py.in @@ -153,6 +153,13 @@ ACCEPTABLE_LISTNAME_CHARACTERS = '[-+_.=a-z0-9]' # in list rosters? Defaults to No to preserve prior behavior. ROSTER_DISPLAY_REALNAME = No +# Beginning in Mailman 2.1.21, localized help and some other output from +# Mailman's bin/ commands is converted to the character set of the user's +# workstation (LC_CTYPE) if different from the character set of the language. +# This is not well tested over a wide range of locales, so if it causes +# problems, it can be disabled by setting the following to Yes. +DISABLE_COMMAND_LOCALE_CSET = No + ##### diff --git a/Mailman/i18n.py b/Mailman/i18n.py index 102bee83..b8b527e0 100644 --- a/Mailman/i18n.py +++ b/Mailman/i18n.py @@ -33,7 +33,8 @@ def _get_ctype_charset(): locale.setlocale(locale.LC_CTYPE, old) return charset -_ctype_charset = _get_ctype_charset() +if not mm_cfg.DISABLE_COMMAND_LOCALE_CSET: + _ctype_charset = _get_ctype_charset() @@ -114,8 +115,12 @@ def tolocale(s): return s return unicode(s, source, 'replace').encode(_ctype_charset, 'replace') -def C_(s): - return tolocale(_(s, 2)) +if mm_cfg.DISABLE_COMMAND_LOCALE_CSET: + C_ = _ +else: + def C_(s): + return tolocale(_(s, 2)) + def ctime(date): -- cgit v1.2.3 From ab5b961be9ed02d583185aabe6a8bbc52bdf3838 Mon Sep 17 00:00:00 2001 From: Mark Sapiro Date: Sun, 28 Feb 2016 12:43:02 -0800 Subject: Bumped branch version to: 2.1.21 --- Mailman/Version.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'Mailman') diff --git a/Mailman/Version.py b/Mailman/Version.py index 091e0c54..132648a8 100644 --- a/Mailman/Version.py +++ b/Mailman/Version.py @@ -16,7 +16,7 @@ # USA. # Mailman version -VERSION = '2.1.21rc2' +VERSION = '2.1.21' # And as a hex number in the manner of PY_VERSION_HEX ALPHA = 0xa @@ -29,9 +29,9 @@ FINAL = 0xf MAJOR_REV = 2 MINOR_REV = 1 MICRO_REV = 21 -REL_LEVEL = GAMMA +REL_LEVEL = FINAL # at most 15 beta releases! -REL_SERIAL = 2 +REL_SERIAL = 0 HEX_VERSION = ((MAJOR_REV << 24) | (MINOR_REV << 16) | (MICRO_REV << 8) | (REL_LEVEL << 4) | (REL_SERIAL << 0)) -- cgit v1.2.3 From 8f22ba21a32701d2efaac0b8b1a1c0a4522912b0 Mon Sep 17 00:00:00 2001 From: Mark Sapiro Date: Sun, 28 Feb 2016 22:28:44 -0800 Subject: Don't collapse multipart with a single sub-part inside multipart/signed parts. --- Mailman/Handlers/MimeDel.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'Mailman') diff --git a/Mailman/Handlers/MimeDel.py b/Mailman/Handlers/MimeDel.py index ab7483ba..691a6e85 100644 --- a/Mailman/Handlers/MimeDel.py +++ b/Mailman/Handlers/MimeDel.py @@ -1,4 +1,4 @@ -# Copyright (C) 2002-2011 by the Free Software Foundation, Inc. +# Copyright (C) 2002-2016 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 @@ -210,6 +210,11 @@ def recast_multipart(msg): # If we're left with a multipart message with only one sub-part, recast # the message to just the sub-part, but not if the part is message/rfc822 # because we don't want to lose the headers. + # Also, if this is a multipart/signed part, stop now as the original part + # may have had a multipart sub-part with only one sub-sub-part, the sig + # may still be valid and going further may break it. (LP: #1551075) + if msg.get_content_type() == 'multipart/signed': + return if msg.is_multipart(): if (len(msg.get_payload()) == 1 and msg.get_content_type() <> 'message/rfc822'): -- cgit v1.2.3