aboutsummaryrefslogtreecommitdiffstats
path: root/Mailman/Handlers
diff options
context:
space:
mode:
Diffstat (limited to 'Mailman/Handlers')
-rw-r--r--Mailman/Handlers/AvoidDuplicates.py9
-rwxr-xr-xMailman/Handlers/CalcRecips.py9
-rw-r--r--Mailman/Handlers/Cleanse.py34
-rw-r--r--Mailman/Handlers/CleanseDKIM.py30
-rwxr-xr-xMailman/Handlers/CookHeaders.py227
-rw-r--r--Mailman/Handlers/Decorate.py11
-rw-r--r--Mailman/Handlers/Hold.py12
-rw-r--r--Mailman/Handlers/MimeDel.py7
-rw-r--r--Mailman/Handlers/Moderate.py84
-rw-r--r--Mailman/Handlers/SMTPDirect.py38
-rw-r--r--Mailman/Handlers/SpamDetect.py93
-rw-r--r--Mailman/Handlers/Tagger.py12
-rw-r--r--Mailman/Handlers/ToDigest.py28
-rw-r--r--Mailman/Handlers/WrapMessage.py89
14 files changed, 557 insertions, 126 deletions
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/CalcRecips.py b/Mailman/Handlers/CalcRecips.py
index 39fe0671..069c88a8 100755
--- a/Mailman/Handlers/CalcRecips.py
+++ b/Mailman/Handlers/CalcRecips.py
@@ -63,7 +63,8 @@ def process(mlist, msg, msgdata):
missing = []
password = msg.get('urgent', missing)
if password is not missing:
- if mlist.Authenticate((mm_cfg.AuthListModerator,
+ if mlist.Authenticate((mm_cfg.AuthListPoster,
+ mm_cfg.AuthListModerator,
mm_cfg.AuthListAdmin),
password):
recips = mlist.getMemberCPAddresses(mlist.getRegularMemberKeys() +
@@ -183,6 +184,12 @@ def do_exclude(mlist, msg, msgdata, recips):
for sender in msg.get_senders():
if slist.isMember(sender):
break
+ for sender in Utils.check_eq_domains(sender,
+ slist.equivalent_domains):
+ if slist.isMember(sender):
+ break
+ if slist.isMember(sender):
+ break
else:
continue
srecips = set([slist.getMemberCPAddress(m)
diff --git a/Mailman/Handlers/Cleanse.py b/Mailman/Handlers/Cleanse.py
index 725cb41b..5270bb5a 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-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
@@ -19,12 +19,34 @@
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:
+ if regexp.endswith(':'):
+ regexp = regexp[:-1] + '$'
+ 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
@@ -38,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',
@@ -45,6 +70,7 @@ def process(mlist, msg, msgdata):
del msg['from']
del msg['reply-to']
del msg['sender']
+ del msg['organization']
del msg['return-path']
# Hotmail sets this one
del msg['x-originating-email']
@@ -53,6 +79,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..3e70313b 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-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
@@ -29,8 +29,28 @@ 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 or mlist.anonymous_list):
+ # We want to remove these headers from posts to anonymous lists.
+ # There can be interaction with the next test, but anonymous_list
+ # and Munge From are not compatible anyway, so don't worry.
+ return
+ if (mm_cfg.REMOVE_DKIM_HEADERS == 1 and not
+ # The following means 'Munge From' applies to this message.
+ # So this whole stanza means if RDH is 1 and we're not Munging,
+ # return and don't remove the headers. See Defaults.py.
+ (msgdata.get('from_is_list') == 1 or
+ (mlist.from_is_list == 1 and msgdata.get('from_is_list') != 2)
+ )
+ ):
+ return
+ if (mm_cfg.REMOVE_DKIM_HEADERS == 3):
+ for value in msg.get_all('domainkey-signature', []):
+ msg['X-Mailman-Original-DomainKey-Signature'] = value
+ for value in msg.get_all('dkim-signature', []):
+ msg['X-Mailman-Original-DKIM-Signature'] = value
+ for value in msg.get_all('authentication-results', []):
+ msg['X-Mailman-Original-Authentication-Results'] = value
+ 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..3e2806f0 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-2017 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
@@ -15,7 +15,10 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
-"""Cook a message's Subject header."""
+"""Cook a message's Subject header.
+Also do other manipulations of From:, Reply-To: and Cc: depending on
+list configuration.
+"""
from __future__ import nested_scopes
import re
@@ -26,12 +29,13 @@ from email.Header import Header, decode_header, make_header
from email.Utils import parseaddr, formataddr, getaddresses
from email.Errors import HeaderParseError
+from Mailman import i18n
from Mailman import mm_cfg
from Mailman import Utils
from Mailman.i18n import _
from Mailman.Logging.Syslog import syslog
-CONTINUATION = ',\n\t'
+CONTINUATION = ',\n '
COMMASPACE = ', '
MAXLINELEN = 78
@@ -49,7 +53,7 @@ def _isunicode(s):
nonascii = re.compile('[^\s!-~]')
-def uheader(mlist, s, header_name=None, continuation_ws='\t', maxlinelen=None):
+def uheader(mlist, s, header_name=None, continuation_ws=' ', maxlinelen=None):
# Get the charset to encode the string in. Then search if there is any
# non-ascii character is in the string. If there is and the charset is
# us-ascii then we use iso-8859-1 instead. If the string is ascii only
@@ -62,20 +66,42 @@ def uheader(mlist, s, header_name=None, continuation_ws='\t', maxlinelen=None):
else:
# there is no nonascii so ...
charset = 'us-ascii'
- return Header(s, charset, maxlinelen, header_name, continuation_ws)
+ try:
+ return Header(s, charset, maxlinelen, header_name, continuation_ws)
+ except UnicodeError:
+ syslog('error', 'list: %s: can\'t decode "%s" as %s',
+ mlist.internal_name(), s, charset)
+ return Header('', charset, maxlinelen, header_name, continuation_ws)
+
+def change_header(name, value, mlist, msg, msgdata, delete=True, repl=True):
+ if ((msgdata.get('from_is_list') == 2 or
+ (msgdata.get('from_is_list') == 0 and mlist.from_is_list == 2)) and
+ not msgdata.get('_fasttrack')
+ ) or name.lower() in ('from', 'reply-to', 'cc'):
+ # The or name.lower() in ... above is because when we are munging
+ # the From:, we want to defer the resultant changes to From:,
+ # Reply-To:, and/or Cc: until after the message passes through
+ # ToDigest, ToArchive and ToUsenet. Thus, we put them in
+ # msgdata[add_header] here and apply them in WrapMessage.
+ 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,
# 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:
@@ -87,7 +113,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 +128,68 @@ 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 (msgdata.get('from_is_list') or mlist.from_is_list) and not fasttrack:
+ # Be as robust as possible here.
+ faddrs = getaddresses(msg.get_all('from', []))
+ # Strip the nulls and bad emails.
+ faddrs = [x for x in faddrs if x[1].find('@') > 0]
+ if len(faddrs) == 1:
+ realname, email = o_from = faddrs[0]
+ else:
+ # No From: or multiple addresses. Just punt and take
+ # the get_sender result.
+ realname = ''
+ email = msgdata['original_sender']
+ o_from = (realname, email)
+ if not realname:
+ if mlist.isMember(email):
+ realname = mlist.getMemberName(email) or email
+ else:
+ realname = email
+ # Remove domain from realname if it looks like an email address
+ realname = re.sub(r'@([^ .]+\.)+[^ .]+$', '---', realname)
+ # Make a display name and RFC 2047 encode it if necessary. This is
+ # difficult and kludgy. If the realname came from From: it should be
+ # ascii or RFC 2047 encoded. If it came from the list, it should be
+ # in the charset of the list's preferred language or possibly unicode.
+ # if it's from the email address, it should be ascii. In any case,
+ # make it a unicode.
+ if isinstance(realname, unicode):
+ urn = realname
+ else:
+ rn, cs = ch_oneline(realname)
+ urn = unicode(rn, cs, errors='replace')
+ # likewise, the list's real_name which should be ascii, but use the
+ # charset of the list's preferred_language which should be a superset.
+ lcs = Utils.GetCharSet(mlist.preferred_language)
+ ulrn = unicode(mlist.real_name, lcs, errors='replace')
+ # get translated 'via' with dummy replacements
+ realname = '%(realname)s'
+ lrn = '%(lrn)s'
+ # We want the i18n context to be the list's preferred_language. It
+ # could be the poster's.
+ otrans = i18n.get_translation()
+ i18n.set_language(mlist.preferred_language)
+ via = _('%(realname)s via %(lrn)s')
+ i18n.set_translation(otrans)
+ uvia = unicode(via, lcs, errors='replace')
+ # Replace the dummy replacements.
+ uvia = re.sub(u'%\(lrn\)s', ulrn, re.sub(u'%\(realname\)s', urn, uvia))
+ # And get an RFC 2047 encoded header string.
+ dn = str(Header(uvia, lcs))
+ change_header('From',
+ formataddr((dn, mlist.GetListEmail())),
+ mlist, msg, msgdata)
+ else:
+ # Use this as a flag
+ o_from = None
# 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
@@ -115,6 +198,23 @@ def process(mlist, msg, msgdata):
# augment it. RFC 2822 allows max one Reply-To: header so collapse them
# if we're adding a value, otherwise don't touch it. (Should we collapse
# in all cases?)
+ # MAS: We need to do some things with the original From: if we've munged
+ # it for DMARC mitigation. We have goals for this process which are
+ # not completely compatible, so we do the best we can. Our goals are:
+ # 1) as long as the list is not anonymous, the original From: address
+ # should be obviously exposed, i.e. not just in a header that MUAs
+ # don't display.
+ # 2) the original From: address should not be in a comment or display
+ # name in the new From: because it is claimed that multiple domains
+ # in any fields in From: are indicative of spamminess. This means
+ # it should be in Reply-To: or Cc:.
+ # 3) the behavior of an MUA doing a 'reply' or 'reply all' should be
+ # consistent regardless of whether or not the From: is munged.
+ # Goal 3) implies sometimes the original From: should be in Reply-To:
+ # and sometimes in Cc:, and even so, this goal won't be achieved in
+ # all cases with all MUAs. In cases of conflict, the above ordering of
+ # goals is priority order.
+
if not fasttrack:
# A convenience function, requires nested scopes. pair is (name, addr)
new = []
@@ -132,22 +232,43 @@ def process(mlist, msg, msgdata):
# the original Reply-To:'s to the list we're building up. In both
# cases we'll zap the existing field because RFC 2822 says max one is
# allowed.
+ o_rt = False
if not mlist.first_strip_reply_to:
orig = msg.get_all('reply-to', [])
for pair in getaddresses(orig):
+ # There's an original Reply-To: and we're not removing it.
add(pair)
+ o_rt = True
+ # We also need to put the old From: in Reply-To: in all cases where
+ # it is not going in Cc:. This is when reply_goes_to_list == 0 and
+ # either there was no original Reply-To: or we stripped it.
+ # However, if there was an original Reply-To:, unstripped, and it
+ # contained the original From: address we need to flag that it's
+ # there so we don't add the original From: to Cc:
+ if o_from and mlist.reply_goes_to_list == 0:
+ if o_rt:
+ if d.has_key(o_from[1].lower()):
+ # Original From: address is in original Reply-To:.
+ # Pretend we added it.
+ o_from = None
+ else:
+ add(o_from)
+ # Flag that we added it.
+ o_from = None
# Set Reply-To: header to point back to this list. Add this last
# because some folks think that some MUAs make it easier to delete
# addresses from the right than from the left.
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
@@ -158,18 +279,38 @@ def process(mlist, msg, msgdata):
# 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.
- if mlist.personalize == 2 and mlist.reply_goes_to_list <> 1 \
- and not mlist.anonymous_list:
+ # We do add the Cc in cases where From: header munging is being done
+ # because even though the list address is in From:, the Reply-To:
+ # poster will override it. Brain dead MUAs may then address the list
+ # twice on a 'reply all', but reasonable MUAs should do the right
+ # thing. We also add the original From: to Cc: if it wasn't added
+ # to Reply-To:
+ add_list = (mlist.personalize == 2 and
+ mlist.reply_goes_to_list <> 1 and
+ not mlist.anonymous_list)
+ if add_list or o_from:
# Watch out for existing Cc headers, merge, and remove dups. Note
# that RFC 2822 says only zero or one Cc header is allowed.
new = []
d = {}
- for pair in getaddresses(msg.get_all('cc', [])):
- add(pair)
- i18ndesc = uheader(mlist, mlist.description, 'Cc')
- add((str(i18ndesc), mlist.GetListEmail()))
- del msg['Cc']
- msg['Cc'] = COMMASPACE.join([formataddr(pair) for pair in new])
+ # If we're adding the original From:, add it first.
+ if o_from:
+ add(o_from)
+ # AvoidDuplicates may have set a new Cc: in msgdata.add_header,
+ # so check that.
+ if (msgdata.has_key('add_header') and
+ msgdata['add_header'].has_key('Cc')):
+ for pair in getaddresses([msgdata['add_header']['Cc']]):
+ add(pair)
+ else:
+ for pair in getaddresses(msg.get_all('cc', [])):
+ add(pair)
+ if add_list:
+ i18ndesc = uheader(mlist, mlist.description, 'Cc')
+ add((str(i18ndesc), mlist.GetListEmail()))
+ change_header('Cc',
+ COMMASPACE.join([formataddr(pair) for pair in new]),
+ mlist, msg, msgdata)
# Add list-specific headers as defined in RFC 2369 and RFC 2919, but only
# if the message is being crafted for a specific list (e.g. not for the
# password reminders).
@@ -191,8 +332,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 +359,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)
@@ -242,7 +381,7 @@ def prefix_subject(mlist, msg, msgdata):
lines = str(subject).splitlines()
else:
lines = subject.splitlines()
- ws = '\t'
+ ws = ' '
if len(lines) > 1 and lines[1] and lines[1][0] in ' \t':
ws = lines[1][0]
msgdata['origsubj'] = subject
@@ -272,16 +411,29 @@ def prefix_subject(mlist, msg, msgdata):
else:
old_style = mm_cfg.OLD_STYLE_PREFIXING
subject = re.sub(prefix_pattern, '', subject)
- rematch = re.match('((RE|AW|SV|VS)\s*(\[\d+\])?\s*:\s*)+', subject, re.I)
+ # Previously the following re didn't have the first \s*. It would fail
+ # if the incoming Subject: was like '[prefix] Re: Re: Re:' because of the
+ # leading space after stripping the prefix. It is not known what MUA would
+ # create such a Subject:, but the issue was reported.
+ rematch = re.match(
+ '(\s*(RE|AW|SV|VS)\s*(\[\d+\])?\s*:\s*)+',
+ subject, re.I)
if rematch:
subject = subject[rematch.end():]
recolon = 'Re:'
else:
recolon = ''
+ # Strip leading and trailing whitespace from subject.
+ subject = subject.strip()
# At this point, subject may become null if someone post mail with
- # subject: [subject prefix]
- if subject.strip() == '':
+ # Subject: [subject prefix]
+ if subject == '':
+ # We want the i18n context to be the list's preferred_language. It
+ # could be the poster's.
+ otrans = i18n.get_translation()
+ i18n.set_language(mlist.preferred_language)
subject = _('(no subject)')
+ i18n.set_translation(otrans)
cset = Utils.GetCharSet(mlist.preferred_language)
subject = unicode(subject, cset)
# and substitute %d in prefix with post_id
@@ -302,8 +454,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)
@@ -312,6 +463,11 @@ def prefix_subject(mlist, msg, msgdata):
except UnicodeError:
pass
# Get the header as a Header instance, with proper unicode conversion
+ # Because of rfc2047 encoding, spaces between encoded words can be
+ # insignificant, so we need to append spaces to our encoded stuff.
+ prefix += ' '
+ if recolon:
+ recolon += ' '
if old_style:
h = uheader(mlist, recolon, 'Subject', continuation_ws=ws)
h.append(prefix)
@@ -321,8 +477,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/Decorate.py b/Mailman/Handlers/Decorate.py
index 69e86d5b..d1c8c5b4 100644
--- a/Mailman/Handlers/Decorate.py
+++ b/Mailman/Handlers/Decorate.py
@@ -1,4 +1,4 @@
-# Copyright (C) 1998-2008 by the Free Software Foundation, Inc.
+# Copyright (C) 1998-2017 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,7 +210,11 @@ def process(mlist, msg, msgdata):
def decorate(mlist, template, what, extradict=None):
# `what' is just a descriptive phrase used in the log message
- #
+
+ # If template is only whitespace, ignore it.
+ if len(re.sub('\s', '', template)) == 0:
+ return ''
+
# BAW: We've found too many situations where Python can be fooled into
# interpolating too much revealing data into a format string. For
# example, a footer of "% silly %(real_name)s" would give a header
@@ -240,4 +244,7 @@ def decorate(mlist, template, what, extradict=None):
except (ValueError, TypeError), e:
syslog('error', 'Exception while calculating %s:\n%s', what, e)
text = template
+ # Ensure text ends with new-line
+ if not text.endswith('\n'):
+ text += '\n'
return text
diff --git a/Mailman/Handlers/Hold.py b/Mailman/Handlers/Hold.py
index d0d22690..2faebae1 100644
--- a/Mailman/Handlers/Hold.py
+++ b/Mailman/Handlers/Hold.py
@@ -1,4 +1,4 @@
-# Copyright (C) 1998-2011 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
@@ -220,7 +220,12 @@ def hold_for_approval(mlist, msg, msgdata, exc):
# We need to send both the reason and the rejection notice through the
# translator again, because of the games we play above
reason = Utils.wrap(exc.reason_notice())
- msgdata['rejection_notice'] = Utils.wrap(exc.rejection_notice(mlist))
+ if isinstance(exc, NonMemberPost) and mlist.nonmember_rejection_notice:
+ msgdata['rejection_notice'] = Utils.wrap(
+ mlist.nonmember_rejection_notice.replace(
+ '%(listowner)s', owneraddr))
+ else:
+ msgdata['rejection_notice'] = Utils.wrap(exc.rejection_notice(mlist))
id = mlist.HoldMessage(msg, reason, msgdata)
# Now we need to craft and send a message to the list admin so they can
# deal with the held message.
@@ -264,7 +269,8 @@ def hold_for_approval(mlist, msg, msgdata, exc):
d['subject'] = usersubject
# craft the admin notification message and deliver it
subject = _('%(listname)s post from %(sender)s requires approval')
- nmsg = Message.OwnerNotification(mlist, subject, tomoderators=1)
+ nmsg = Message.UserNotification(owneraddr, owneraddr, subject,
+ lang=lang)
nmsg.set_type('multipart/mixed')
text = MIMEText(
Utils.maketext('postauth.txt', d, raw=1, mlist=mlist),
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'):
diff --git a/Mailman/Handlers/Moderate.py b/Mailman/Handlers/Moderate.py
index f9e79cbe..49ed1d7e 100644
--- a/Mailman/Handlers/Moderate.py
+++ b/Mailman/Handlers/Moderate.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2001-2011 by the Free Software Foundation, Inc.
+# Copyright (C) 2001-2015 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
@@ -21,6 +21,7 @@
import re
from email.MIMEMessage import MIMEMessage
from email.MIMEText import MIMEText
+from email.Utils import parseaddr
from Mailman import mm_cfg
from Mailman import Utils
@@ -47,12 +48,18 @@ class ModeratedMemberPost(Hold.ModeratedPost):
def process(mlist, msg, msgdata):
- if msgdata.get('approved') or msgdata.get('fromusenet'):
+ if msgdata.get('approved'):
return
- # First of all, is the poster a member or not?
+ # Is the poster a member or not?
for sender in msg.get_senders():
if mlist.isMember(sender):
break
+ for sender in Utils.check_eq_domains(sender,
+ mlist.equivalent_domains):
+ if mlist.isMember(sender):
+ break
+ if mlist.isMember(sender):
+ break
else:
sender = None
if sender:
@@ -90,22 +97,34 @@ def process(mlist, msg, msgdata):
sender = msg.get_sender()
# From here on out, we're dealing with non-members.
listname = mlist.internal_name()
- if matches_p(sender, mlist.accept_these_nonmembers, listname):
+ if mlist.GetPattern(sender,
+ mlist.accept_these_nonmembers,
+ at_list='accept_these_nonmembers'
+ ):
return
- if matches_p(sender, mlist.hold_these_nonmembers, listname):
+ if mlist.GetPattern(sender,
+ mlist.hold_these_nonmembers,
+ at_list='hold_these_nonmembers'
+ ):
Hold.hold_for_approval(mlist, msg, msgdata, Hold.NonMemberPost)
# No return
- if matches_p(sender, mlist.reject_these_nonmembers, listname):
+ if mlist.GetPattern(sender,
+ mlist.reject_these_nonmembers,
+ at_list='reject_these_nonmembers'
+ ):
do_reject(mlist)
# No return
- if matches_p(sender, mlist.discard_these_nonmembers, listname):
+ if mlist.GetPattern(sender,
+ mlist.discard_these_nonmembers,
+ at_list='discard_these_nonmembers'
+ ):
do_discard(mlist, msg)
# No return
# Okay, so the sender wasn't specified explicitly by any of the non-member
# moderation configuration variables. Handle by way of generic non-member
# action.
assert 0 <= mlist.generic_nonmember_action <= 4
- if mlist.generic_nonmember_action == 0:
+ if mlist.generic_nonmember_action == 0 or msgdata.get('fromusenet'):
# Accept
return
elif mlist.generic_nonmember_action == 1:
@@ -117,43 +136,6 @@ def process(mlist, msg, msgdata):
-def matches_p(sender, nonmembers, listname):
- # First strip out all the regular expressions and listnames
- plainaddrs = [addr for addr in nonmembers if not (addr.startswith('^')
- or addr.startswith('@'))]
- addrdict = Utils.List2Dict(plainaddrs, foldcase=1)
- if addrdict.has_key(sender):
- return 1
- # Now do the regular expression matches
- for are in nonmembers:
- if are.startswith('^'):
- try:
- cre = re.compile(are, re.IGNORECASE)
- except re.error:
- continue
- if cre.search(sender):
- return 1
- elif are.startswith('@'):
- # XXX Needs to be reviewed for list@domain names.
- try:
- mname = are[1:].lower().strip()
- if mname == listname:
- # don't reference your own list
- syslog('error',
- '*_these_nonmembers in %s references own list',
- listname)
- else:
- mother = MailList(mname, lock=0)
- if mother.isMember(sender):
- return 1
- except Errors.MMUnknownListError:
- syslog('error',
- '*_these_nonmembers in %s references non-existent list %s',
- listname, mname)
- return 0
-
-
-
def do_reject(mlist):
listowner = mlist.GetOwnerEmail()
if mlist.nonmember_rejection_notice:
@@ -161,9 +143,10 @@ def do_reject(mlist):
Utils.wrap(_(mlist.nonmember_rejection_notice))
else:
raise Errors.RejectMessage, Utils.wrap(_("""\
-You are not allowed to post to this mailing list, and your message has been
-automatically rejected. If you think that your messages are being rejected in
-error, contact the mailing list owner at %(listowner)s."""))
+Your message has been rejected, probably because you are not subscribed to the
+mailing list and the list's policy is to prohibit non-members from posting to
+it. If you think that your messages are being rejected in error, contact the
+mailing list owner at %(listowner)s."""))
@@ -174,9 +157,10 @@ def do_discard(mlist, msg):
lang = mlist.preferred_language
varhelp = '%s/?VARHELP=privacy/sender/discard_these_nonmembers' % \
mlist.GetScriptURL('admin', absolute=1)
- nmsg = Message.OwnerNotification(mlist,
+ nmsg = Message.UserNotification(mlist.GetOwnerEmail(),
+ mlist.GetBouncesEmail(),
_('Auto-discard notification'),
- lang=lang, tomoderators=0)
+ lang=lang)
nmsg.set_type('multipart/mixed')
text = MIMEText(Utils.wrap(_(
'The attached message has been automatically discarded.')),
diff --git a/Mailman/Handlers/SMTPDirect.py b/Mailman/Handlers/SMTPDirect.py
index 1d11d19a..ca6aebdd 100644
--- a/Mailman/Handlers/SMTPDirect.py
+++ b/Mailman/Handlers/SMTPDirect.py
@@ -1,4 +1,4 @@
-# Copyright (C) 1998-2011 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
@@ -30,6 +30,7 @@ import copy
import time
import socket
import smtplib
+from base64 import b64encode
from types import UnicodeType
from Mailman import mm_cfg
@@ -61,7 +62,38 @@ class Connection:
def __connect(self):
self.__conn = smtplib.SMTP()
+ self.__conn.set_debuglevel(mm_cfg.SMTPLIB_DEBUG_LEVEL)
self.__conn.connect(mm_cfg.SMTPHOST, mm_cfg.SMTPPORT)
+ if mm_cfg.SMTP_AUTH:
+ if mm_cfg.SMTP_USE_TLS:
+ try:
+ self.__conn.starttls()
+ except SMTPException, e:
+ syslog('smtp-failure', 'SMTP TLS error: %s', e)
+ self.quit()
+ raise
+ try:
+ self.__conn.ehlo(mm_cfg.SMTP_HELO_HOST)
+ except SMTPException, e:
+ syslog('smtp-failure', 'SMTP EHLO error: %s', e)
+ self.quit()
+ raise
+ try:
+ self.__conn.login(mm_cfg.SMTP_USER, mm_cfg.SMTP_PASSWD)
+ except smtplib.SMTPHeloError, e:
+ syslog('smtp-failure', 'SMTP HELO error: %s', e)
+ self.quit()
+ raise
+ except smtplib.SMTPAuthenticationError, e:
+ syslog('smtp-failure', 'SMTP AUTH error: %s', e)
+ self.quit()
+ raise
+ except smtplib.SMTPException, e:
+ syslog('smtp-failure',
+ 'SMTP - no suitable authentication method found: %s', e)
+ self.quit()
+ raise
+
self.__numsessions = mm_cfg.SMTP_MAX_SESSIONS_PER_CONNECTION
def sendmail(self, envsender, recips, msgtext):
@@ -340,6 +372,10 @@ def verpdeliver(mlist, msg, msgdata, envsender, failures, conn):
del msgcopy['x-mailman-copy']
if msgdata.get('add-dup-header', {}).has_key(recip):
msgcopy['X-Mailman-Copy'] = 'yes'
+ # If desired, add the RCPT_BASE64_HEADER_NAME header
+ if len(mm_cfg.RCPT_BASE64_HEADER_NAME) > 0:
+ del msgcopy[mm_cfg.RCPT_BASE64_HEADER_NAME]
+ msgcopy[mm_cfg.RCPT_BASE64_HEADER_NAME] = b64encode(recip)
# For the final delivery stage, we can just bulk deliver to a party of
# one. ;)
bulkdeliver(mlist, msgcopy, msgdata, envsender, failures, conn)
diff --git a/Mailman/Handlers/SpamDetect.py b/Mailman/Handlers/SpamDetect.py
index 8d26da03..aaddff5f 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-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
@@ -27,13 +27,17 @@ TBD: This needs to be made more configurable and robust.
import re
+from unicodedata import normalize
+from email.Errors import HeaderParseError
from email.Header import decode_header
+from email.Utils import parseaddr
from Mailman import mm_cfg
from Mailman import Errors
from Mailman import i18n
-from Mailman.Utils import GetCharSet
+from Mailman import Utils
from Mailman.Handlers.Hold import hold_for_approval
+from Mailman.Logging.Syslog import syslog
try:
True, False
@@ -61,24 +65,81 @@ _ = i18n._
def getDecodedHeaders(msg, cset='utf-8'):
- """Returns a string containing all the headers of msg, unfolded and
- RFC 2047 decoded and encoded in cset.
+ """Returns a unicode containing all the headers of msg, unfolded and
+ RFC 2047 decoded, normalized and separated by new lines.
"""
- headers = ''
+ headers = u''
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'
- uvalue += unicode(frag, cs, 'replace')
- headers += '%s: %s\n' % (h, uvalue.encode(cset, 'replace'))
+ try:
+ uvalue += unicode(frag, cs, 'replace')
+ except LookupError:
+ # The encoding charset is unknown. At this point, frag
+ # has been QP or base64 decoded into a byte string whose
+ # charset we don't know how to handle. We will try to
+ # unicode it as iso-8859-1 which may result in a garbled
+ # mess, but we have to do something.
+ uvalue += unicode(frag, 'iso-8859-1', 'replace')
+ uhdr = h.decode('us-ascii', 'replace')
+ headers += u'%s: %s\n' % (h, normalize(mm_cfg.NORMALIZE_FORM, uvalue))
return headers
def process(mlist, msg, msgdata):
+ # Before anything else, check DMARC if necessary. We do this as early
+ # as possible so reject/discard actions trump other holds/approvals and
+ # wrap/munge actions get flagged even for approved messages.
+ # But not for owner mail which should not be subject to DMARC reject or
+ # discard actions.
+ if not msgdata.get('toowner'):
+ msgdata['from_is_list'] = 0
+ dn, addr = parseaddr(msg.get('from'))
+ if addr and mlist.dmarc_moderation_action > 0:
+ if Utils.IsDMARCProhibited(mlist, addr):
+ # Note that for dmarc_moderation_action, 0 = Accept,
+ # 1 = Munge, 2 = Wrap, 3 = Reject, 4 = Discard
+ if mlist.dmarc_moderation_action == 1:
+ msgdata['from_is_list'] = 1
+ elif mlist.dmarc_moderation_action == 2:
+ msgdata['from_is_list'] = 2
+ elif mlist.dmarc_moderation_action == 3:
+ # Reject
+ text = mlist.dmarc_moderation_notice
+ if text:
+ text = Utils.wrap(text)
+ else:
+ text = Utils.wrap(_(
+"""You are not allowed to post to this mailing list From: a domain which
+publishes a DMARC policy of reject or quarantine, and your message has been
+automatically rejected. If you think that your messages are being rejected in
+error, contact the mailing list owner at %(listowner)s."""))
+ raise Errors.RejectMessage, text
+ elif mlist.dmarc_moderation_action == 4:
+ raise Errors.DiscardMessage
+
+ # Get member address if any.
+ for sender in msg.get_senders():
+ if mlist.isMember(sender):
+ break
+ else:
+ sender = msg.get_sender()
+ if (mlist.member_verbosity_threshold > 0 and
+ Utils.IsVerboseMember(mlist, sender)
+ ):
+ mlist.setMemberOption(sender, mm_cfg.Moderate, 1)
+ syslog('vette',
+ '%s: Automatically Moderated %s for verbose postings.',
+ mlist.real_name, sender)
+
if msgdata.get('approved'):
return
# First do site hard coded header spam checks
@@ -92,9 +153,9 @@ def process(mlist, msg, msgdata):
# Now do header_filter_rules
# TK: Collect headers in sub-parts because attachment filename
# extension may be a clue to possible virus/spam.
- headers = ''
+ headers = u''
# Get the character set of the lists preferred language for headers
- lcset = GetCharSet(mlist.preferred_language)
+ lcset = Utils.GetCharSet(mlist.preferred_language)
for p in msg.walk():
headers += getDecodedHeaders(p, lcset)
for patterns, action, empty in mlist.header_filter_rules:
@@ -106,7 +167,17 @@ def process(mlist, msg, msgdata):
# ignore 'empty' patterns
if not pattern.strip():
continue
- if re.search(pattern, headers, re.IGNORECASE|re.MULTILINE):
+ pattern = Utils.xml_to_unicode(pattern, lcset)
+ pattern = normalize(mm_cfg.NORMALIZE_FORM, pattern)
+ try:
+ mo = re.search(pattern,
+ headers,
+ re.IGNORECASE|re.MULTILINE|re.UNICODE)
+ except (re.error, TypeError):
+ syslog('error',
+ 'ignoring header_filter_rules invalid pattern: %s',
+ pattern)
+ if mo:
if action == mm_cfg.DISCARD:
raise Errors.DiscardMessage
if action == mm_cfg.REJECT:
diff --git a/Mailman/Handlers/Tagger.py b/Mailman/Handlers/Tagger.py
index 38a8e465..ed9a7e71 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-2015 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,7 +27,9 @@ from email.Header import decode_header
from Mailman import Utils
from Mailman.Logging.Syslog import syslog
+from Mailman.Handlers.CookHeaders import change_header
+OR = '|'
CRNL = '\r\n'
EMPTYSTRING = ''
NLTAB = '\n\t'
@@ -62,15 +64,17 @@ def process(mlist, msg, msgdata):
# added to the specific topics bucket.
hits = {}
for name, pattern, desc, emptyflag in mlist.topics:
- cre = re.compile(pattern, re.IGNORECASE | re.VERBOSE)
+ pattern = OR.join(pattern.splitlines())
+ cre = re.compile(pattern, re.IGNORECASE)
for line in matchlines:
if cre.search(line):
hits[name] = 1
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..046cbaba 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-2017 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
@@ -68,6 +68,17 @@ except NameError:
+def to_cset_out(text, lcset):
+ # Convert text from unicode or lcset to output cset.
+ ocset = Charset(lcset).get_output_charset() or lcset
+ if isinstance(text, unicode):
+ return text.encode(ocset, errors='replace')
+ else:
+ return text.decode(lcset, errors='replace').encode(ocset,
+ errors='replace')
+
+
+
def process(mlist, msg, msgdata):
# Short circuit non-digestable lists.
if not mlist.digestable or msgdata.get('isdigest'):
@@ -86,7 +97,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:
@@ -203,8 +215,8 @@ def send_i18n_digests(mlist, mboxfp):
# RFC 1153
print >> plainmsg, mastheadtxt
print >> plainmsg
- # Now add the optional digest header
- if mlist.digest_header:
+ # Now add the optional digest header but only if more than whitespace.
+ if re.sub('\s', '', mlist.digest_header):
headertxt = decorate(mlist, mlist.digest_header, _('digest header'))
# MIME
header = MIMEText(headertxt, _charset=lcset)
@@ -298,7 +310,7 @@ def send_i18n_digests(mlist, mboxfp):
if msgcount == 0:
# Why did we even get here?
return
- toctext = toc.getvalue()
+ toctext = to_cset_out(toc.getvalue(), lcset)
# MIME
tocpart = MIMEText(toctext, _charset=lcset)
tocpart['Content-Description']= _("Today's Topics (%(msgcount)d messages)")
@@ -353,8 +365,8 @@ def send_i18n_digests(mlist, mboxfp):
print >> plainmsg, payload
if not payload.endswith('\n'):
print >> plainmsg
- # Now add the footer
- if mlist.digest_footer:
+ # Now add the footer but only if more than whitespace.
+ if re.sub('\s', '', mlist.digest_footer):
footertxt = decorate(mlist, mlist.digest_footer, _('digest footer'))
# MIME
footer = MIMEText(footertxt, _charset=lcset)
@@ -411,7 +423,7 @@ def send_i18n_digests(mlist, mboxfp):
listname=mlist.internal_name(),
isdigest=True)
# RFC 1153
- rfc1153msg.set_payload(plainmsg.getvalue(), lcset)
+ rfc1153msg.set_payload(to_cset_out(plainmsg.getvalue(), lcset), lcset)
virginq.enqueue(rfc1153msg,
recips=plainrecips,
listname=mlist.internal_name(),
diff --git a/Mailman/Handlers/WrapMessage.py b/Mailman/Handlers/WrapMessage.py
new file mode 100644
index 00000000..2bb540d6
--- /dev/null
+++ b/Mailman/Handlers/WrapMessage.py
@@ -0,0 +1,89 @@
+# Copyright (C) 2013-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
+# 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.
+
+Also, in the case of Munge From, replace the From:, Reply-To: and Cc: in the
+original message.
+"""
+
+import copy
+
+from email.MIMEMessage import MIMEMessage
+from email.MIMEText import MIMEText
+
+from Mailman import Utils
+
+# Headers from the original that we want to keep in the wrapper.
+KEEPERS = ('to',
+ 'in-reply-to',
+ 'references',
+ 'x-mailman-approved-at',
+ 'date',
+ )
+
+
+
+def process(mlist, msg, msgdata):
+ # This is the negation of we're wrapping because dmarc_moderation_action
+ # is wrap this message or from_is_list applies and is wrap.
+ if not (msgdata.get('from_is_list') == 2 or
+ (mlist.from_is_list == 2 and msgdata.get('from_is_list') == 0)):
+ # Now see if we need to add a From:, Reply-To: or Cc: without wrapping.
+ # See comments in CookHeaders.change_header for why we do this here.
+ a_h = msgdata.get('add_header')
+ if a_h:
+ if a_h.get('From'):
+ del msg['from']
+ msg['From'] = a_h.get('From')
+ if a_h.get('Reply-To'):
+ del msg['reply-to']
+ msg['Reply-To'] = a_h.get('Reply-To')
+ if a_h.get('Cc'):
+ del msg['cc']
+ msg['Cc'] = a_h.get('Cc')
+ 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['Message-ID'] = Utils.unique_message_id(mlist)
+ # Add the headers from CookHeaders.
+ for k, v in msgdata['add_header'].items():
+ msg[k] = v
+ # Are we including dmarc_wrapped_message_text? I.e., do we have text and
+ # are we wrapping because of dmarc_moderation_action?
+ if mlist.dmarc_wrapped_message_text and msgdata.get('from_is_list') == 2:
+ part1 = MIMEText(Utils.wrap(mlist.dmarc_wrapped_message_text),
+ 'plain',
+ Utils.GetCharSet(mlist.preferred_language))
+ part1['Content-Disposition'] = 'inline'
+ part2 = MIMEMessage(omsg)
+ part2['Content-Disposition'] = 'inline'
+ msg['Content-Type'] = 'multipart/mixed'
+ msg.set_payload([part1, part2])
+ else:
+ msg['Content-Type'] = 'message/rfc822'
+ msg['Content-Disposition'] = 'inline'
+ msg.set_payload([omsg])
+