++ The following actions are applied to all list messages when ++ selected here. To apply these actions only to messages where the ++ domain in the From: header is determined to use such a protocol, ++ see the ++ dmarc_moderation_action settings under Privacy options... ++ -> Sender filters. ++
Settings:
++
The transformations for anonymous_list are applied before ++ any of these actions. It is not useful to apply actions other ++ than No to an anonymous list, and if you do so, the result may ++ be surprising. ++
The Reply-To: header munging actions below interact with these ++ actions as follows: ++
first_strip_reply_to = Yes will remove all the incoming ++ Reply-To: addresses but will still add the poster's address to ++ Reply-To: for all three settings of reply_goes_to_list which ++ respectively will result in just the poster's address, the ++ poster's address and the list posting address or the poster's ++ address and the explicit reply_to_address in the outgoing ++ Reply-To: header. If first_strip_reply_to = No the poster's ++ address in the original From: header, if not already included in ++ the Reply-To:, will be added to any existing Reply-To: ++ address(es). ++
These actions, whether selected here or via ++ dmarc_moderation_action, do not apply to messages in digests ++ or archives or sent to usenet via the Mail<->News gateways. ++
If ++ dmarc_moderation_action applies to this message with an ++ action other than Accept, that action rather than this is ++ applied""")), ++ + ('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)""")), +diff --git a/Mailman/Gui/NonDigest.py b/Mailman/Gui/NonDigest.py +old mode 100644 +new mode 100755 +diff --git a/Mailman/Gui/Privacy.py b/Mailman/Gui/Privacy.py +index 75eff2b..5d717bb 100644 +--- a/Mailman/Gui/Privacy.py ++++ b/Mailman/Gui/Privacy.py +@@ -158,6 +158,11 @@ class Privacy(GUIBase): + ] + + adminurl = mlist.GetScriptURL('admin', absolute=1) ++ ++ if mlist.dmarc_quarantine_moderation_action: ++ quarantine = _('/Quarantine') ++ else: ++ quarantine = '' + sender_rtn = [ + _("""When a message is posted to the list, a series of + moderation steps are taken to decide whether a moderator must +@@ -235,6 +240,59 @@ class Privacy(GUIBase): + >rejection notice to + be sent to moderated members who post to this list.""")), + ++ ('dmarc_moderation_action', mm_cfg.Radio, ++ (_('Accept'), _('Munge From'), _('Wrap Message'), _('Reject'), ++ _('Discard')), 0, ++ _("""Action to take when anyone posts to the ++ list from a domain with a DMARC Reject%(quarantine)s Policy."""), ++ ++ _("""
This setting takes precedence over the from_is_list setting ++ if the message is From: an affected domain and the setting is ++ other than Accept.""")), ++ ++ ('dmarc_quarantine_moderation_action', mm_cfg.Radio, ++ (_('No'), _('Yes')), 0, ++ _("""Shall the above dmarc_moderation_action apply to messages ++ From: domains with DMARC p=quarantine as well as p=reject"""), ++ ++ _("""
If a message is From: a domain with DMARC p=quarantine
++ and dmarc_moderation_action is not applied (this set to No)
++ the message will likely not bounce, but will be delivered to
++ recipients' spam folders or other hard to find places.""")),
++
++ ('dmarc_moderation_notice', mm_cfg.Text, (10, WIDTH), 1,
++ _("""Text to include in any
++ rejection notice to
++ be sent to anyone who posts to this list from a domain
++ with a DMARC Reject%(quarantine)s Policy.""")),
++
+ _('Non-member filters'),
+
+ ('accept_these_nonmembers', mm_cfg.EmailListEx, (10, WIDTH), 1,
+@@ -399,7 +457,7 @@ class Privacy(GUIBase):
+ case, each rule is matched in turn, with processing stopped after
+ the first match.
+
+- Note that headers are collected from all the attachments
++ Note that headers are collected from all the attachments
+ (except for the mailman administrivia message) and
+ matched against the regular expressions. With this feature,
+ you can effectively sort out messages with dangerous file
+@@ -442,6 +500,11 @@ class Privacy(GUIBase):
+ # an option.
+ if property == 'subscribe_policy' and not mm_cfg.ALLOW_OPEN_SUBSCRIBE:
+ val += 1
++ if (property == 'dmarc_moderation_action' and
++ val < mm_cfg.DEFAULT_DMARC_MODERATION_ACTION):
++ doc.addError(_("""dmarc_moderation_action must be >= the configured
++ default value."""))
++ val = mm_cfg.DEFAULT_DMARC_MODERATION_ACTION
+ setattr(mlist, property, val)
+
+ # We need to handle the header_filter_rules widgets specially, but
+diff --git a/Mailman/Handlers/AvoidDuplicates.py b/Mailman/Handlers/AvoidDuplicates.py
+index 038034c..549d8e7 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/CookHeaders.py b/Mailman/Handlers/CookHeaders.py
+old mode 100644
+new mode 100755
+index 8e7e668..c556967
+--- a/Mailman/Handlers/CookHeaders.py
++++ b/Mailman/Handlers/CookHeaders.py
+@@ -64,13 +64,23 @@ 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 ((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'):
++ 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 +97,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 +112,31 @@ 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:
++ realname, email = parseaddr(msg['from'])
++ 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)
++ # Remember the original From: here for adding to Reply-To: below.
++ o_from = parseaddr(msg['from'])
++ change_header('From',
++ formataddr(('%s via %s' % (realname, mlist.real_name),
++ 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
+@@ -136,18 +166,23 @@ def process(mlist, msg, msgdata):
+ orig = msg.get_all('reply-to', [])
+ for pair in getaddresses(orig):
+ add(pair)
++ # We also need to put the old From: in Reply-To: in all cases.
++ if o_from:
++ add(o_from)
+ # 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 +193,31 @@ 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.
++ if (mlist.personalize == 2 and mlist.reply_goes_to_list <> 1 and
++ not mlist.anonymous_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 = []
+ d = {}
+- for pair in getaddresses(msg.get_all('cc', [])):
+- add(pair)
++ # 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)
+ i18ndesc = uheader(mlist, mlist.description, 'Cc')
+ add((str(i18ndesc), mlist.GetListEmail()))
+- del msg['Cc']
+- msg['Cc'] = COMMASPACE.join([formataddr(pair) for pair in new])
++ 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 +239,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 +266,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 +348,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 +366,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/Moderate.py b/Mailman/Handlers/Moderate.py
+index a362d96..2f1f38f 100644
+--- a/Mailman/Handlers/Moderate.py
++++ b/Mailman/Handlers/Moderate.py
+@@ -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,9 +48,34 @@ 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?
++ # Before anything else, check DMARC if necessary.
++ 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
++ # Then, is the poster a member or not?
+ for sender in msg.get_senders():
+ if mlist.isMember(sender):
+ break
+@@ -105,7 +131,7 @@ def process(mlist, msg, msgdata):
+ # 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:
+diff --git a/Mailman/Handlers/Tagger.py b/Mailman/Handlers/Tagger.py
+index 0d3ce49..2117290 100644
+--- a/Mailman/Handlers/Tagger.py
++++ b/Mailman/Handlers/Tagger.py
+@@ -24,6 +24,7 @@ import email.Iterators
+
+ from Mailman import Utils
+ from Mailman.Logging.Syslog import syslog
++from Mailman.Handlers.CookHeaders import change_header
+
+ CRNL = '\r\n'
+ EMPTYSTRING = ''
+@@ -60,8 +61,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/WrapMessage.py b/Mailman/Handlers/WrapMessage.py
+new file mode 100644
+index 0000000..9678f6f
+--- /dev/null
++++ b/Mailman/Handlers/WrapMessage.py
+@@ -0,0 +1,72 @@
++# Copyright (C) 2013-2014 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: and Reply-To: in the
++original message.
++"""
++
++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):
++ # 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: and/or Reply-To: without wrapping.
++ 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')
++ 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
+old mode 100644
+new mode 100755
+index 6083fb1..f948b69
+--- a/Mailman/MailList.py
++++ b/Mailman/MailList.py
+@@ -346,6 +346,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:]
+@@ -386,6 +387,10 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin,
+ # 2==Discard
+ self.member_moderation_action = 0
+ self.member_moderation_notice = ''
++ self.dmarc_moderation_action = mm_cfg.DEFAULT_DMARC_MODERATION_ACTION
++ self.dmarc_quarantine_moderation_action = (
++ mm_cfg.DEFAULT_DMARC_QUARANTINE_MODERATION_ACTION)
++ self.dmarc_moderation_notice = ''
+ self.accept_these_nonmembers = []
+ self.hold_these_nonmembers = []
+ self.reject_these_nonmembers = []
+@@ -712,7 +717,14 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin,
+ def CheckVersion(self, stored_state):
+ """Auto-update schema if necessary."""
+ if self.data_version >= mm_cfg.DATA_FILE_VERSION:
+- return
++ # Some lists could have been created by newer Mailman version than
++ # this one. We are adding just few variables, so check for these
++ # variables explicitely.
++ if (hasattr(self, "from_is_list")
++ and hasattr(self, "dmarc_moderation_action")
++ and hasattr(self, "dmarc_moderation_notice")
++ and hasattr(self, "dmarc_quarantine_moderation_action")):
++ return
+ # Initialize any new variables
+ self.InitVars()
+ # Then reload the database (but don't recurse). Force a reload even
+@@ -1025,7 +1030,8 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin,
+ # And send an acknowledgement to the user...
+ if userack:
+ self.SendUnsubscribeAck(emailaddr, userlang)
+- # ...and to the administrator
++ # ...and to the administrator in the correct language. (LP: #1308655)
++ i18n.set_language(self.preferred_language)
+ if admin_notif:
+ realname = self.real_name
+ subject = _('%(realname)s unsubscribe notification')
+diff --git a/Mailman/Message.py b/Mailman/Message.py
+index 84e4aa2..13e7ff2 100644
+--- a/Mailman/Message.py
++++ b/Mailman/Message.py
+@@ -61,6 +61,43 @@ class Generator(email.Generator.Generator):
+
+
+
++class Generator(email.Generator.Generator):
++ """Generates output from a Message object tree, keeping signatures.
++
++ Headers will by default _not_ be folded in attachments.
++ """
++ def __init__(self, outfp, mangle_from_=True,
++ maxheaderlen=78, children_maxheaderlen=0):
++ email.Generator.Generator.__init__(self, outfp,
++ mangle_from_=mangle_from_, maxheaderlen=maxheaderlen)
++ self.__children_maxheaderlen = children_maxheaderlen
++
++ def clone(self, fp):
++ """Clone this generator with maxheaderlen set for children"""
++ return self.__class__(fp, self._mangle_from_,
++ self.__children_maxheaderlen, self.__children_maxheaderlen)
++
++ # This is the _handle_message method with the fix for bug 7970.
++ def _handle_message(self, msg):
++ s = StringIO()
++ g = self.clone(s)
++ # The payload of a message/rfc822 part should be a multipart sequence
++ # of length 1. The zeroth element of the list should be the Message
++ # object for the subpart. Extract that object, stringify it, and
++ # write it out.
++ # Except, it turns out, when it's a string instead, which happens when
++ # and only when HeaderParser is used on a message of mime type
++ # message/rfc822. Such messages are generated by, for example,
++ # Groupwise when forwarding unadorned messages. (Issue 7970.) So
++ # in that case we just emit the string body.
++ payload = msg.get_payload()
++ if isinstance(payload, list):
++ g.flatten(msg.get_payload(0), unixfrom=False)
++ payload = s.getvalue()
++ self._fp.write(payload)
++
++
++
+ class Message(email.Message.Message):
+ def __init__(self):
+ # We need a version number so that we can optimize __setstate__()
+@@ -243,6 +280,20 @@ class Message(email.Message.Message):
+ return fp.getvalue()
+
+
++ def as_string(self, unixfrom=False, mangle_from_=True):
++ """Return entire formatted message as a string using
++ Mailman.Message.Generator.
++
++ Operates like email.Message.Message.as_string, only
++ using Mailman's Message.Generator class. Only the top headers will
++ get folded.
++ """
++ fp = StringIO()
++ g = Generator(fp, mangle_from_=mangle_from_)
++ g.flatten(self, unixfrom=unixfrom)
++ return fp.getvalue()
++
++
+
+ class UserNotification(Message):
+ """Class for internally crafted messages."""
+diff --git a/Mailman/Utils.py b/Mailman/Utils.py
+index c8275df..8021942 100644
+--- a/Mailman/Utils.py
++++ b/Mailman/Utils.py
+@@ -71,6 +71,14 @@ except NameError:
+ True = 1
+ False = 0
+
++try:
++ import dns.resolver
++ import dns.rdatatype
++ from dns.exception import DNSException
++ dns_resolver = True
++except ImportError:
++ dns_resolver = False
++
+ EMPTYSTRING = ''
+ UEMPTYSTRING = u''
+ NL = '\n'
+@@ -1047,3 +1055,91 @@ def suspiciousHTML(html):
+ else:
+ return False
+
++
++
++
++# This takes an email address, and returns True if DMARC policy is p=reject
++# or possibly quarantine.
++def IsDMARCProhibited(mlist, email):
++ if not dns_resolver:
++ return False
++
++ email = email.lower()
++ at_sign = email.find('@')
++ if at_sign < 1:
++ return False
++ dmarc_domain = '_dmarc.' + email[at_sign+1:]
++
++ try:
++ resolver = dns.resolver.Resolver()
++ resolver.timeout = float(mm_cfg.DMARC_RESOLVER_TIMEOUT)
++ 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
++ except DNSException, e:
++ syslog('error',
++ 'DNSException: Unable to query DMARC policy for %s (%s). %s',
++ email, dmarc_domain, e.__class__)
++ return False
++ 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
++# CNAMEs before the values they point to.
++ full_record = ""
++ results_by_name = {}
++ cnames = {}
++ want_names = set([dmarc_domain + '.'])
++ for txt_rec in txt_recs.response.answer:
++ if txt_rec.rdtype == dns.rdatatype.CNAME:
++ cnames[txt_rec.name.to_text()] = (
++ txt_rec.items[0].target.to_text())
++ if txt_rec.rdtype != dns.rdatatype.TXT:
++ continue
++ results_by_name.setdefault(txt_rec.name.to_text(), []).append(
++ "".join(txt_rec.items[0].strings))
++ expands = list(want_names)
++ seen = set(expands)
++ while expands:
++ item = expands.pop(0)
++ if item in cnames:
++ if cnames[item] in seen:
++ continue # cname loop
++ expands.append(cnames[item])
++ seen.add(cnames[item])
++ want_names.add(cnames[item])
++ want_names.discard(item)
++
++ if len(want_names) != 1:
++ syslog('error',
++ """multiple DMARC entries in results for %s,
++ processing each to be strict""",
++ dmarc_domain)
++ for name in want_names:
++ if name not in results_by_name:
++ continue
++ dmarcs = filter(lambda n: n.startswith('v=DMARC1;'),
++ results_by_name[name])
++ if len(dmarcs) == 0:
++ return False
++ if len(dmarcs) > 1:
++ syslog('error',
++ """RRset of TXT records for %s has %d v=DMARC1 entries;
++ testing them all""",
++ dmarc_domain, len(dmarc))
++ for entry in dmarcs:
++ if re.search(r'\bp=reject\b', entry, re.IGNORECASE):
++ syslog('vette',
++ 'DMARC lookup for %s (%s) found p=reject in %s = %s',
++ email, dmarc_domain, name, entry)
++ return True
++
++ if (mlist.dmarc_quarantine_moderation_action and
++ re.search(r'\bp=quarantine\b', entry, re.IGNORECASE)):
++ syslog('vette',
++ 'DMARC lookup for %s (%s) found p=quarantine in %s = %s',
++ email, dmarc_domain, name, entry)
++ return True
++
++ return False
++
+diff --git a/Mailman/Version.py b/Mailman/Version.py
+index 05e6500..af4a2df 100644
+--- a/Mailman/Version.py
++++ b/Mailman/Version.py
+@@ -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 = 101
+
+ # qfile/*.db schema version number
+ QFILE_SCHEMA_VERSION = 3
+diff --git a/Mailman/versions.py b/Mailman/versions.py
+old mode 100644
+new mode 100755
+index 81fafd5..138e770
+--- a/Mailman/versions.py
++++ b/Mailman/versions.py
+@@ -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)
+
+
+
+@@ -383,6 +386,11 @@ def NewVars(l):
+ # the current GUI description model. So, 0==Hold, 1==Reject, 2==Discard
+ add_only_if_missing('member_moderation_action', 0)
+ add_only_if_missing('member_moderation_notice', '')
++ add_only_if_missing('dmarc_moderation_action',
++ mm_cfg.DEFAULT_DMARC_MODERATION_ACTION)
++ add_only_if_missing('dmarc_quarantine_moderation_action',
++ mm_cfg.DEFAULT_DMARC_QUARANTINE_MODERATION_ACTION)
++ add_only_if_missing('dmarc_moderation_notice', '')
+ add_only_if_missing('new_member_options',
+ mm_cfg.DEFAULT_NEW_MEMBER_OPTIONS)
+ # Emergency moderation flag
+diff --git a/contrib/majordomo2mailman.pl b/contrib/majordomo2mailman.pl
+index c874862..770dc57 100644
+--- a/contrib/majordomo2mailman.pl
++++ b/contrib/majordomo2mailman.pl
+@@ -480,6 +480,7 @@ sub init_defaultmmconf {
+ 'max_num_recipients', "10",
+ 'forbidden_posters', "[]",
+ 'bounce_matching_headers', "\"\"\"\n\"\"\"\n",
++ 'from_is_list', "0",
+ 'anonymous_list', "0",
+ 'nondigestable', "1",
+ 'digestable', "1",
diff --git a/SOURCES/mailman-2.1.12-init-not-on.patch b/SOURCES/mailman-2.1.12-init-not-on.patch
new file mode 100644
index 0000000..226f814
--- /dev/null
+++ b/SOURCES/mailman-2.1.12-init-not-on.patch
@@ -0,0 +1,13 @@
+diff -up mailman-2.1.12/misc/mailman.in.not-on mailman-2.1.12/misc/mailman.in
+--- mailman-2.1.12/misc/mailman.in.not-on 2009-12-22 13:09:58.000000000 +0100
++++ mailman-2.1.12/misc/mailman.in 2009-12-22 13:10:28.000000000 +0100
+@@ -36,8 +36,7 @@
+ # Required-Start: $local_fs $remote_fs $network $named
+ # Should-Start: httpd
+ # Required-Stop: $local_fs $remote_fs $network
+-# Default-Start: 3 4 5
+-# Default-Stop: 0 1 6
++# Default-Stop: 0 1 3 4 5 6
+ # Short-Description: start and stop Mailman
+ # Description: Mailman is the GNU mailing list manager.
+ ### END INIT INFO
diff --git a/SOURCES/mailman-2.1.12-initcleanup.patch b/SOURCES/mailman-2.1.12-initcleanup.patch
new file mode 100644
index 0000000..707a5a5
--- /dev/null
+++ b/SOURCES/mailman-2.1.12-initcleanup.patch
@@ -0,0 +1,65 @@
+diff -up mailman-2.1.12/misc/mailman.in.initcleanup mailman-2.1.12/misc/mailman.in
+--- mailman-2.1.12/misc/mailman.in.initcleanup 2009-10-05 09:09:35.000000000 -0400
++++ mailman-2.1.12/misc/mailman.in 2009-10-05 17:53:56.000000000 -0400
+@@ -91,6 +91,8 @@ function start()
+ then
+ touch /var/lock/subsys/$prog
+ InstallCron
++ else
++ RETVAL=6
+ fi
+ echo
+ return $RETVAL
+@@ -98,6 +100,8 @@ function start()
+
+ function stop()
+ {
++ if [ -f /var/lock/subsys/$prog ]
++ then
+ echo -n $"Shutting down $prog: "
+ mailman-update-cfg
+ daemon $MAILMANCTL -q stop
+@@ -108,6 +112,10 @@ function stop()
+ RemoveCron
+ fi
+ echo
++ else
++ echo $"$prog already stopped."
++ RETVAL=0
++ fi
+ return $RETVAL
+ }
+
+@@ -135,7 +143,7 @@ case "$1" in
+ RETVAL=$?
+ ;;
+
+-'condrestart')
++'condrestart'|'try-restart')
+ $MAILMANCTL -q -u status
+ retval=$?
+ if [ $retval -eq 0 ]
+@@ -146,13 +154,20 @@ case "$1" in
+ ;;
+
+ 'status')
+- $MAILMANCTL -u status
++ output=$($MAILMANCTL -u status)
+ RETVAL=$?
++ if [ $RETVAL -eq 3 -a -f /var/lock/subsys/$prog ]
++ then
++ echo $"$prog dead but subsys locked"
++ RETVAL=2
++ else
++ echo $output
++ fi
+ ;;
+
+ *)
+- echo $"Usage: $prog {start|stop|restart|force-reload|condrestart|status}"
+- RETVAL=3
++ echo $"Usage: $prog {start|stop|restart|force-reload|condrestart|try-restart|status}"
++ RETVAL=2
+ ;;
+
+ esac
diff --git a/SOURCES/mailman-2.1.12-mmcfg.patch b/SOURCES/mailman-2.1.12-mmcfg.patch
new file mode 100644
index 0000000..c68e4a4
--- /dev/null
+++ b/SOURCES/mailman-2.1.12-mmcfg.patch
@@ -0,0 +1,19 @@
+diff -ruN mailman-2.1.12-a/misc/mailman.in mailman-2.1.12-b/misc/mailman.in
+--- mailman-2.1.12-a/misc/mailman.in 2009-07-28 12:19:53.000000000 +0200
++++ mailman-2.1.12-b/misc/mailman.in 2009-07-28 12:19:55.000000000 +0200
+@@ -84,6 +84,7 @@
+ function start()
+ {
+ echo -n $"Starting $prog: "
++ mailman-update-cfg
+ daemon $MAILMANCTL -s -q start
+ RETVAL=$?
+ if [ $RETVAL -eq 0 ]
+@@ -98,6 +99,7 @@
+ function stop()
+ {
+ echo -n $"Shutting down $prog: "
++ mailman-update-cfg
+ daemon $MAILMANCTL -q stop
+ RETVAL=$?
+ if [ $RETVAL -eq 0 ]
diff --git a/SOURCES/mailman-2.1.12-multimail.patch b/SOURCES/mailman-2.1.12-multimail.patch
new file mode 100644
index 0000000..5264a77
--- /dev/null
+++ b/SOURCES/mailman-2.1.12-multimail.patch
@@ -0,0 +1,387 @@
+diff -ruN mailman-2.1.12-a/configure.in mailman-2.1.12-b/configure.in
+--- mailman-2.1.12-a/configure.in 2009-02-23 22:23:35.000000000 +0100
++++ mailman-2.1.12-b/configure.in 2009-07-28 12:19:47.000000000 +0200
+@@ -249,26 +249,101 @@
+ fi
+
+ # new macro for finding group names
+-AC_DEFUN([MM_FIND_GROUP_NAME], [
++# returns a comma separated list of quoted group names
++# the list is returned in the same order as specified with any duplicates removed
++# the filter flag must be "yes" or "no", e.g. this is permcheck
++# "no" ==> none existing groups are not filtered out
++# "yes" ==> only those groups that are in the group database are included
++# in the list
++AC_DEFUN(MM_FIND_GROUP_LIST, [
+ # $1 == variable name
+-# $2 == user id to check for
++# $2 == white space separated list of groups to check,
++# list may contain mix of id's and names
++# $3 == filter, if == 'yes' then remove any non-existing groups
+ AC_SUBST($1)
+ changequote(,)
+ if test -z "$$1"
+ then
+ cat > conftest.py <