/[smecontribs]/rpms/mailman/contribs10/mailman-2.1.12-dmarc.patch
ViewVC logotype

Annotation of /rpms/mailman/contribs10/mailman-2.1.12-dmarc.patch

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.1 - (hide annotations) (download)
Mon Feb 22 15:33:48 2021 UTC (3 years, 9 months ago) by brianr
Branch: MAIN
CVS Tags: HEAD
Initial import

1 brianr 1.1 diff --git a/Mailman/Bouncers/SimpleMatch.py b/Mailman/Bouncers/SimpleMatch.py
2     index a6a952a..0901b50 100644
3     --- a/Mailman/Bouncers/SimpleMatch.py
4     +++ b/Mailman/Bouncers/SimpleMatch.py
5     @@ -42,7 +42,7 @@ PATTERNS = [
6     # sz-sb.de, corridor.com, nfg.nl
7     (_c('the following addresses had'),
8     _c('transcript of session follows'),
9     - _c(r'<(?P<fulladdr>[^>]*)>|\(expanded from: <?(?P<addr>[^>)]*)>?\)')),
10     + _c(r'^ *(\(expanded from: )?<?(?P<addr>[^\s@]+@[^\s@>]+?)>?\)?\s*$')),
11     # robanal.demon.co.uk
12     (_c('this message was created automatically by mail delivery software'),
13     _c('original message follows'),
14     diff --git a/Mailman/Bouncers/Yahoo.py b/Mailman/Bouncers/Yahoo.py
15     index b3edf4f..08ede54 100644
16     --- a/Mailman/Bouncers/Yahoo.py
17     +++ b/Mailman/Bouncers/Yahoo.py
18     @@ -20,9 +20,15 @@ import re
19     import email
20     from email.Utils import parseaddr
21    
22     -tcre = re.compile(r'message\s+from\s+yahoo\.\S+', re.IGNORECASE)
23     +tcre = (re.compile(r'message\s+from\s+yahoo\.\S+', re.IGNORECASE),
24     + re.compile(r'Sorry, we were unable to deliver your message to '
25     + r'the following address(\(es\))?\.',
26     + re.IGNORECASE),
27     + )
28     acre = re.compile(r'<(?P<addr>[^>]*)>:')
29     -ecre = re.compile(r'--- Original message follows')
30     +ecre = (re.compile(r'--- Original message follows'),
31     + re.compile(r'--- Below this line is a copy of the message'),
32     + )
33    
34    
35    
36     @@ -36,18 +42,26 @@ def process(msg):
37     # simple state machine
38     # 0 == nothing seen
39     # 1 == tag line seen
40     + # 2 == end line seen
41     state = 0
42     for line in email.Iterators.body_line_iterator(msg):
43     line = line.strip()
44     - if state == 0 and tcre.match(line):
45     - state = 1
46     + if state == 0:
47     + for cre in tcre:
48     + if cre.match(line):
49     + state = 1
50     + break
51     elif state == 1:
52     mo = acre.match(line)
53     if mo:
54     addrs.append(mo.group('addr'))
55     continue
56     - mo = ecre.match(line)
57     - if mo:
58     - # we're at the end of the error response
59     - break
60     + for cre in ecre:
61     + mo = cre.match(line)
62     + if mo:
63     + # we're at the end of the error response
64     + state = 2
65     + break
66     + elif state == 2:
67     + break
68     return addrs
69     diff --git a/Mailman/Defaults.py.in b/Mailman/Defaults.py.in
70     old mode 100644
71     new mode 100755
72     index 4fe63db..8e42f54
73     --- a/Mailman/Defaults.py.in
74     +++ b/Mailman/Defaults.py.in
75     @@ -505,6 +505,7 @@ GLOBAL_PIPELINE = [
76     # (outgoing) path, finally leaving the message in the outgoing queue.
77     'AfterDelivery',
78     'Acknowledge',
79     + 'WrapMessage',
80     'ToOutgoing',
81     ]
82    
83     @@ -914,6 +915,29 @@ DEFAULT_DEFAULT_MEMBER_MODERATION = No
84     # moderators?
85     DEFAULT_FORWARD_AUTO_DISCARDS = Yes
86    
87     +# Shall dmarc_moderation_action be applied to messages From: domains with
88     +# a DMARC policy of quarantine as well as reject? This sets the default for
89     +# the list setting that controls it.
90     +DEFAULT_DMARC_QUARANTINE_MODERATION_ACTION = Yes
91     +
92     +# Default action for posts whose From: address domain has a DMARC policy of
93     +# reject or quarantine. See DEFAULT_FROM_IS_LIST below. Whatever is set as
94     +# the default here precludes the list owner from setting a lower value.
95     +# 0 = Accept
96     +# 1 = Munge From
97     +# 2 = Wrap Message
98     +# 3 = Reject
99     +# 4 = Discard
100     +DEFAULT_DMARC_MODERATION_ACTION = 0
101     +
102     +# Parameters for DMARC DNS lookups. If you are seeing 'DNSException:
103     +# Unable to query DMARC policy ...' entries in your error log, you may need
104     +# to adjust these.
105     +# The time to wait for a response from a name server before timeout.
106     +DMARC_RESOLVER_TIMEOUT = seconds(3)
107     +# The total time to spend trying to get an answer to the question.
108     +DMARC_RESOLVER_LIFETIME = seconds(5)
109     +
110     # What shold happen to non-member posts which are do not match explicit
111     # non-member actions?
112     # 0 = Accept
113     @@ -950,6 +974,25 @@ DEFAULT_SEND_WELCOME_MSG = Yes
114     # Send goodbye messages to unsubscribed members?
115     DEFAULT_SEND_GOODBYE_MSG = Yes
116    
117     +# Some list posts and mail to the -owner address may contain DomainKey or
118     +# DomainKeys Identified Mail (DKIM) signature headers <http://www.dkim.org/>.
119     +# Various list transformations to the message such as adding a list header or
120     +# footer or scrubbing attachments or even reply-to munging can break these
121     +# signatures. It is generally felt that these signatures have value, even if
122     +# broken and even if the outgoing message is resigned. However, some sites
123     +# may wish to remove these headers by setting this to Yes.
124     +REMOVE_DKIM_HEADERS = No
125     +
126     +# The following is a three way setting. It sets the default for the list's
127     +# from_is_list policy which is applied to all posts except those for which a
128     +# dmarc_moderation_action other than accept applies.
129     +# 0 -> Do not rewrite the From: or wrap the message.
130     +# 1 -> Rewrite the From: header of posts replacing the posters address with
131     +# that of the list. Also see REMOVE_DKIM_HEADERS above.
132     +# 2 -> Do not modify the From: of the message, but wrap the message in an outer
133     +# message From the list address.
134     +DEFAULT_FROM_IS_LIST = 0
135     +
136     # Wipe sender information, and make it look like the list-admin
137     # address sends all messages
138     DEFAULT_ANONYMOUS_LIST = No
139     diff --git a/Mailman/Gui/General.py b/Mailman/Gui/General.py
140     index 8271a30..05dc9ba 100644
141     --- a/Mailman/Gui/General.py
142     +++ b/Mailman/Gui/General.py
143     @@ -153,6 +153,72 @@ class General(GUIBase):
144     (listname %%05d) -> (listname 00123)
145     """)),
146    
147     + ('from_is_list', mm_cfg.Radio,
148     + (_('No'), _('Munge From'), _('Wrap Message')), 0,
149     + _("""Replace the From: header address with the list's posting
150     + address to mitigate issues stemming from the original From:
151     + domain's DMARC or similar policies."""),
152     + _("""Several protocols now in wide use attempt to ensure that use
153     + of the domain in the author's address (ie, in the From: header
154     + field) is authorized by that domain. These protocols may be
155     + incompatible with common list features such as footers, causing
156     + participating email services to bounce list traffic merely
157     + because of the address in the From: field. <b>This has resulted
158     + in members being unsubscribed despite being perfectly able to
159     + receive mail.</b>
160     + <p>
161     + The following actions are applied to all list messages when
162     + selected here. To apply these actions only to messages where the
163     + domain in the From: header is determined to use such a protocol,
164     + see the <a
165     + href="?VARHELP=privacy/sender/dmarc_moderation_action">
166     + dmarc_moderation_action</a> settings under Privacy options...
167     + -&gt; Sender filters.
168     + <p>Settings:<p>
169     + <dl>
170     + <dt>No</dt>
171     + <dd>Do nothing special. This is appropriate for anonymous lists.
172     + It is appropriate for dedicated announcement lists, unless the
173     + From: address of authorized posters might be in a domain with a
174     + DMARC or similar policy. It is also appropriate if you choose to
175     + use dmarc_moderation_action other than Accept for this list.</dd>
176     + <dt>Munge From</dt>
177     + <dd>This action replaces the poster's address in the From: header
178     + with the list's posting address and adds the poster's address to
179     + the addresses in the original Reply-To: header.</dd>
180     + <dt>Wrap Message</dt>
181     + <dd>Just wrap the message in an outer message with the From:
182     + header containing the list's posting address and with the original
183     + From: address added to the addresses in the original Reply-To:
184     + header and with Content-Type: message/rfc822. This is effectively
185     + a one message MIME format digest.</dd>
186     + </dl>
187     + <p>The transformations for anonymous_list are applied before
188     + any of these actions. It is not useful to apply actions other
189     + than No to an anonymous list, and if you do so, the result may
190     + be surprising.
191     + <p>The Reply-To: header munging actions below interact with these
192     + actions as follows:
193     + <p> first_strip_reply_to = Yes will remove all the incoming
194     + Reply-To: addresses but will still add the poster's address to
195     + Reply-To: for all three settings of reply_goes_to_list which
196     + respectively will result in just the poster's address, the
197     + poster's address and the list posting address or the poster's
198     + address and the explicit reply_to_address in the outgoing
199     + Reply-To: header. If first_strip_reply_to = No the poster's
200     + address in the original From: header, if not already included in
201     + the Reply-To:, will be added to any existing Reply-To:
202     + address(es).
203     + <p>These actions, whether selected here or via <a
204     + href="?VARHELP=privacy/sender/dmarc_moderation_action">
205     + dmarc_moderation_action</a>, do not apply to messages in digests
206     + or archives or sent to usenet via the Mail&lt;-&gt;News gateways.
207     + <p>If <a
208     + href="?VARHELP=privacy/sender/dmarc_moderation_action">
209     + dmarc_moderation_action</a> applies to this message with an
210     + action other than Accept, that action rather than this is
211     + applied""")),
212     +
213     ('anonymous_list', mm_cfg.Radio, (_('No'), _('Yes')), 0,
214     _("""Hide the sender of a message, replacing it with the list
215     address (Removes From, Sender and Reply-To fields)""")),
216     diff --git a/Mailman/Gui/NonDigest.py b/Mailman/Gui/NonDigest.py
217     old mode 100644
218     new mode 100755
219     diff --git a/Mailman/Gui/Privacy.py b/Mailman/Gui/Privacy.py
220     index 75eff2b..5d717bb 100644
221     --- a/Mailman/Gui/Privacy.py
222     +++ b/Mailman/Gui/Privacy.py
223     @@ -158,6 +158,11 @@ class Privacy(GUIBase):
224     ]
225    
226     adminurl = mlist.GetScriptURL('admin', absolute=1)
227     +
228     + if mlist.dmarc_quarantine_moderation_action:
229     + quarantine = _('/Quarantine')
230     + else:
231     + quarantine = ''
232     sender_rtn = [
233     _("""When a message is posted to the list, a series of
234     moderation steps are taken to decide whether a moderator must
235     @@ -235,6 +240,59 @@ class Privacy(GUIBase):
236     >rejection notice</a> to
237     be sent to moderated members who post to this list.""")),
238    
239     + ('dmarc_moderation_action', mm_cfg.Radio,
240     + (_('Accept'), _('Munge From'), _('Wrap Message'), _('Reject'),
241     + _('Discard')), 0,
242     + _("""Action to take when anyone posts to the
243     + list from a domain with a DMARC Reject%(quarantine)s Policy."""),
244     +
245     + _("""<ul><li><b>Munge From</b> -- applies the <a
246     + href="?VARHELP=general/from_is_list">from_is_list Munge From</a>
247     + transformation to these messages.
248     +
249     + <p><li><b>Wrap Message</b> -- applies the <a
250     + href="?VARHELP=general/from_is_list">from_is_list Wrap
251     + Message</a> transformation to these messages.
252     +
253     + <p><li><b>Reject</b> -- this automatically rejects the message by
254     + sending a bounce notice to the post's author. The text of the
255     + bounce notice can be <a
256     + href="?VARHELP=privacy/sender/dmarc_moderation_notice"
257     + >configured by you</a>.
258     +
259     + <p><li><b>Discard</b> -- this simply discards the message, with
260     + no notice sent to the post's author.
261     + </ul>
262     +
263     + <p>This setting takes precedence over the <a
264     + href="?VARHELP=general/from_is_list"> from_is_list</a> setting
265     + if the message is From: an affected domain and the setting is
266     + other than Accept.""")),
267     +
268     + ('dmarc_quarantine_moderation_action', mm_cfg.Radio,
269     + (_('No'), _('Yes')), 0,
270     + _("""Shall the above dmarc_moderation_action apply to messages
271     + From: domains with DMARC p=quarantine as well as p=reject"""),
272     +
273     + _("""<ul><li><b>No</b> -- this applies dmarc_moderation_action to
274     + only those posts From: a domain with DMARC p=reject. This is
275     + appropriate if you are concerned about bounced messages, but
276     + want to apply dmarc_moderation_action to as few messages as
277     + possible.
278     + <p><li><b>Yes</b> -- this applies dmarc_moderation_action to
279     + posts From: a domain with DMARC p=reject or p=quarantine.
280     + </ul><p>If a message is From: a domain with DMARC p=quarantine
281     + and dmarc_moderation_action is not applied (this set to No)
282     + the message will likely not bounce, but will be delivered to
283     + recipients' spam folders or other hard to find places.""")),
284     +
285     + ('dmarc_moderation_notice', mm_cfg.Text, (10, WIDTH), 1,
286     + _("""Text to include in any
287     + <a href="?VARHELP=privacy/sender/dmarc_moderation_action"
288     + >rejection notice</a> to
289     + be sent to anyone who posts to this list from a domain
290     + with a DMARC Reject%(quarantine)s Policy.""")),
291     +
292     _('Non-member filters'),
293    
294     ('accept_these_nonmembers', mm_cfg.EmailListEx, (10, WIDTH), 1,
295     @@ -399,7 +457,7 @@ class Privacy(GUIBase):
296     case, each rule is matched in turn, with processing stopped after
297     the first match.
298    
299     - Note that headers are collected from all the attachments
300     + Note that headers are collected from all the attachments
301     (except for the mailman administrivia message) and
302     matched against the regular expressions. With this feature,
303     you can effectively sort out messages with dangerous file
304     @@ -442,6 +500,11 @@ class Privacy(GUIBase):
305     # an option.
306     if property == 'subscribe_policy' and not mm_cfg.ALLOW_OPEN_SUBSCRIBE:
307     val += 1
308     + if (property == 'dmarc_moderation_action' and
309     + val < mm_cfg.DEFAULT_DMARC_MODERATION_ACTION):
310     + doc.addError(_("""dmarc_moderation_action must be >= the configured
311     + default value."""))
312     + val = mm_cfg.DEFAULT_DMARC_MODERATION_ACTION
313     setattr(mlist, property, val)
314    
315     # We need to handle the header_filter_rules widgets specially, but
316     diff --git a/Mailman/Handlers/AvoidDuplicates.py b/Mailman/Handlers/AvoidDuplicates.py
317     index 038034c..549d8e7 100644
318     --- a/Mailman/Handlers/AvoidDuplicates.py
319     +++ b/Mailman/Handlers/AvoidDuplicates.py
320     @@ -24,6 +24,7 @@ warning header, or pass it through, depending on the user's preferences.
321    
322     from email.Utils import getaddresses, formataddr
323     from Mailman import mm_cfg
324     +from Mailman.Handlers.CookHeaders import change_header
325    
326     COMMASPACE = ', '
327    
328     @@ -95,6 +96,10 @@ def process(mlist, msg, msgdata):
329     # Set the new list of recipients
330     msgdata['recips'] = newrecips
331     # RFC 2822 specifies zero or one CC header
332     - del msg['cc']
333     if ccaddrs:
334     - msg['Cc'] = COMMASPACE.join([formataddr(i) for i in ccaddrs.values()])
335     + change_header('Cc',
336     + COMMASPACE.join([formataddr(i) for i in ccaddrs.values()]),
337     + mlist, msg, msgdata)
338     + else:
339     + del msg['cc']
340     +
341     diff --git a/Mailman/Handlers/CookHeaders.py b/Mailman/Handlers/CookHeaders.py
342     old mode 100644
343     new mode 100755
344     index 8e7e668..c556967
345     --- a/Mailman/Handlers/CookHeaders.py
346     +++ b/Mailman/Handlers/CookHeaders.py
347     @@ -64,13 +64,23 @@ def uheader(mlist, s, header_name=None, continuation_ws='\t', maxlinelen=None):
348     charset = 'us-ascii'
349     return Header(s, charset, maxlinelen, header_name, continuation_ws)
350    
351     +def change_header(name, value, mlist, msg, msgdata, delete=True, repl=True):
352     + if ((msgdata.get('from_is_list') == 2 or
353     + (msgdata.get('from_is_list') == 0 and mlist.from_is_list == 2)) and
354     + not msgdata.get('_fasttrack')
355     + ) or name.lower() in ('from', 'reply-to'):
356     + msgdata.setdefault('add_header', {})[name] = value
357     + elif repl or not msg.has_key(name):
358     + if delete:
359     + del msg[name]
360     + msg[name] = value
361     +
362    
363    
364     def process(mlist, msg, msgdata):
365     # Set the "X-Ack: no" header if noack flag is set.
366     if msgdata.get('noack'):
367     - del msg['x-ack']
368     - msg['X-Ack'] = 'no'
369     + change_header('X-Ack', 'no', mlist, msg, msgdata)
370     # Because we're going to modify various important headers in the email
371     # message, we want to save some of the information in the msgdata
372     # dictionary for later. Specifically, the sender header will get waxed,
373     @@ -87,7 +97,8 @@ def process(mlist, msg, msgdata):
374     pass
375     # Mark message so we know we've been here, but leave any existing
376     # X-BeenThere's intact.
377     - msg['X-BeenThere'] = mlist.GetListEmail()
378     + change_header('X-BeenThere', mlist.GetListEmail(),
379     + mlist, msg, msgdata, delete=False)
380     # Add Precedence: and other useful headers. None of these are standard
381     # and finding information on some of them are fairly difficult. Some are
382     # just common practice, and we'll add more here as they become necessary.
383     @@ -101,12 +112,31 @@ def process(mlist, msg, msgdata):
384     # known exploits in a particular version of Mailman and we know a site is
385     # using such an old version, they may be vulnerable. It's too easy to
386     # edit the code to add a configuration variable to handle this.
387     - if not msg.has_key('x-mailman-version'):
388     - msg['X-Mailman-Version'] = mm_cfg.VERSION
389     + change_header('X-Mailman-Version', mm_cfg.VERSION,
390     + mlist, msg, msgdata, repl=False)
391     # We set "Precedence: list" because this is the recommendation from the
392     # sendmail docs, the most authoritative source of this header's semantics.
393     - if not msg.has_key('precedence'):
394     - msg['Precedence'] = 'list'
395     + change_header('Precedence', 'list',
396     + mlist, msg, msgdata, repl=False)
397     + # Do we change the from so the list takes ownership of the email
398     + if (msgdata.get('from_is_list') or mlist.from_is_list) and not fasttrack:
399     + realname, email = parseaddr(msg['from'])
400     + if not realname:
401     + if mlist.isMember(email):
402     + realname = mlist.getMemberName(email) or email
403     + else:
404     + realname = email
405     + # Remove domain from realname if it looks like an email address
406     + realname = re.sub(r'@([^ .]+\.)+[^ .]+$', '---', realname)
407     + # Remember the original From: here for adding to Reply-To: below.
408     + o_from = parseaddr(msg['from'])
409     + change_header('From',
410     + formataddr(('%s via %s' % (realname, mlist.real_name),
411     + mlist.GetListEmail())),
412     + mlist, msg, msgdata)
413     + else:
414     + # Use this as a flag
415     + o_from = None
416     # Reply-To: munging. Do not do this if the message is "fast tracked",
417     # meaning it is internally crafted and delivered to a specific user. BAW:
418     # Yuck, I really hate this feature but I've caved under the sheer pressure
419     @@ -136,18 +166,23 @@ def process(mlist, msg, msgdata):
420     orig = msg.get_all('reply-to', [])
421     for pair in getaddresses(orig):
422     add(pair)
423     + # We also need to put the old From: in Reply-To: in all cases.
424     + if o_from:
425     + add(o_from)
426     # Set Reply-To: header to point back to this list. Add this last
427     # because some folks think that some MUAs make it easier to delete
428     # addresses from the right than from the left.
429     if mlist.reply_goes_to_list == 1:
430     i18ndesc = uheader(mlist, mlist.description, 'Reply-To')
431     add((str(i18ndesc), mlist.GetListEmail()))
432     - del msg['reply-to']
433     # Don't put Reply-To: back if there's nothing to add!
434     if new:
435     # Preserve order
436     - msg['Reply-To'] = COMMASPACE.join(
437     - [formataddr(pair) for pair in new])
438     + change_header('Reply-To',
439     + COMMASPACE.join([formataddr(pair) for pair in new]),
440     + mlist, msg, msgdata)
441     + else:
442     + del msg['reply-to']
443     # The To field normally contains the list posting address. However
444     # when messages are fully personalized, that header will get
445     # overwritten with the address of the recipient. We need to get the
446     @@ -158,18 +193,31 @@ def process(mlist, msg, msgdata):
447     # above code?
448     # Also skip Cc if this is an anonymous list as list posting address
449     # is already in From and Reply-To in this case.
450     - if mlist.personalize == 2 and mlist.reply_goes_to_list <> 1 \
451     - and not mlist.anonymous_list:
452     + # We do add the Cc in cases where From: header munging is being done
453     + # because even though the list address is in From:, the Reply-To:
454     + # poster will override it. Brain dead MUAs may then address the list
455     + # twice on a 'reply all', but reasonable MUAs should do the right
456     + # thing.
457     + if (mlist.personalize == 2 and mlist.reply_goes_to_list <> 1 and
458     + not mlist.anonymous_list):
459     # Watch out for existing Cc headers, merge, and remove dups. Note
460     # that RFC 2822 says only zero or one Cc header is allowed.
461     new = []
462     d = {}
463     - for pair in getaddresses(msg.get_all('cc', [])):
464     - add(pair)
465     + # AvoidDuplicates may have set a new Cc: in msgdata.add_header,
466     + # so check that.
467     + if (msgdata.has_key('add_header') and
468     + msgdata['add_header'].has_key('Cc')):
469     + for pair in getaddresses([msgdata['add_header']['Cc']]):
470     + add(pair)
471     + else:
472     + for pair in getaddresses(msg.get_all('cc', [])):
473     + add(pair)
474     i18ndesc = uheader(mlist, mlist.description, 'Cc')
475     add((str(i18ndesc), mlist.GetListEmail()))
476     - del msg['Cc']
477     - msg['Cc'] = COMMASPACE.join([formataddr(pair) for pair in new])
478     + change_header('Cc',
479     + COMMASPACE.join([formataddr(pair) for pair in new]),
480     + mlist, msg, msgdata)
481     # Add list-specific headers as defined in RFC 2369 and RFC 2919, but only
482     # if the message is being crafted for a specific list (e.g. not for the
483     # password reminders).
484     @@ -191,8 +239,7 @@ def process(mlist, msg, msgdata):
485     # without desc we need to ensure the MUST brackets
486     listid_h = '<%s>' % listid
487     # We always add a List-ID: header.
488     - del msg['list-id']
489     - msg['List-Id'] = listid_h
490     + change_header('List-Id', listid_h, mlist, msg, msgdata)
491     # For internally crafted messages, we also add a (nonstandard),
492     # "X-List-Administrivia: yes" header. For all others (i.e. those coming
493     # from list posts), we add a bunch of other RFC 2369 headers.
494     @@ -219,13 +266,12 @@ def process(mlist, msg, msgdata):
495     # First we delete any pre-existing headers because the RFC permits only
496     # one copy of each, and we want to be sure it's ours.
497     for h, v in headers.items():
498     - del msg[h]
499     # Wrap these lines if they are too long. 78 character width probably
500     # shouldn't be hardcoded, but is at least text-MUA friendly. The
501     # adding of 2 is for the colon-space separator.
502     if len(h) + 2 + len(v) > 78:
503     v = CONTINUATION.join(v.split(', '))
504     - msg[h] = v
505     + change_header(h, v, mlist, msg, msgdata)
506    
507    
508    
509     @@ -302,8 +348,7 @@ def prefix_subject(mlist, msg, msgdata):
510     h = u' '.join([prefix, subject])
511     h = h.encode('us-ascii')
512     h = uheader(mlist, h, 'Subject', continuation_ws=ws)
513     - del msg['subject']
514     - msg['Subject'] = h
515     + change_header('Subject', h, mlist, msg, msgdata)
516     ss = u' '.join([recolon, subject])
517     ss = ss.encode('us-ascii')
518     ss = uheader(mlist, ss, 'Subject', continuation_ws=ws)
519     @@ -321,8 +366,7 @@ def prefix_subject(mlist, msg, msgdata):
520     # TK: Subject is concatenated and unicode string.
521     subject = subject.encode(cset, 'replace')
522     h.append(subject, cset)
523     - del msg['subject']
524     - msg['Subject'] = h
525     + change_header('Subject', h, mlist, msg, msgdata)
526     ss = uheader(mlist, recolon, 'Subject', continuation_ws=ws)
527     ss.append(subject, cset)
528     msgdata['stripped_subject'] = ss
529     diff --git a/Mailman/Handlers/Moderate.py b/Mailman/Handlers/Moderate.py
530     index a362d96..2f1f38f 100644
531     --- a/Mailman/Handlers/Moderate.py
532     +++ b/Mailman/Handlers/Moderate.py
533     @@ -21,6 +21,7 @@
534     import re
535     from email.MIMEMessage import MIMEMessage
536     from email.MIMEText import MIMEText
537     +from email.Utils import parseaddr
538    
539     from Mailman import mm_cfg
540     from Mailman import Utils
541     @@ -47,9 +48,34 @@ class ModeratedMemberPost(Hold.ModeratedPost):
542    
543    
544     def process(mlist, msg, msgdata):
545     - if msgdata.get('approved') or msgdata.get('fromusenet'):
546     + if msgdata.get('approved'):
547     return
548     - # First of all, is the poster a member or not?
549     + # Before anything else, check DMARC if necessary.
550     + msgdata['from_is_list'] = 0
551     + dn, addr = parseaddr(msg.get('from'))
552     + if addr and mlist.dmarc_moderation_action > 0:
553     + if Utils.IsDMARCProhibited(mlist, addr):
554     + # Note that for dmarc_moderation_action, 0 = Accept,
555     + # 1 = Munge, 2 = Wrap, 3 = Reject, 4 = Discard
556     + if mlist.dmarc_moderation_action == 1:
557     + msgdata['from_is_list'] = 1
558     + elif mlist.dmarc_moderation_action == 2:
559     + msgdata['from_is_list'] = 2
560     + elif mlist.dmarc_moderation_action == 3:
561     + # Reject
562     + text = mlist.dmarc_moderation_notice
563     + if text:
564     + text = Utils.wrap(text)
565     + else:
566     + text = Utils.wrap(_(
567     +"""You are not allowed to post to this mailing list From: a domain which
568     +publishes a DMARC policy of reject or quarantine, and your message has been
569     +automatically rejected. If you think that your messages are being rejected in
570     +error, contact the mailing list owner at %(listowner)s."""))
571     + raise Errors.RejectMessage, text
572     + elif mlist.dmarc_moderation_action == 4:
573     + raise Errors.DiscardMessage
574     + # Then, is the poster a member or not?
575     for sender in msg.get_senders():
576     if mlist.isMember(sender):
577     break
578     @@ -105,7 +131,7 @@ def process(mlist, msg, msgdata):
579     # moderation configuration variables. Handle by way of generic non-member
580     # action.
581     assert 0 <= mlist.generic_nonmember_action <= 4
582     - if mlist.generic_nonmember_action == 0:
583     + if mlist.generic_nonmember_action == 0 or msgdata.get('fromusenet'):
584     # Accept
585     return
586     elif mlist.generic_nonmember_action == 1:
587     diff --git a/Mailman/Handlers/Tagger.py b/Mailman/Handlers/Tagger.py
588     index 0d3ce49..2117290 100644
589     --- a/Mailman/Handlers/Tagger.py
590     +++ b/Mailman/Handlers/Tagger.py
591     @@ -24,6 +24,7 @@ import email.Iterators
592     import email.Parser
593    
594     from Mailman.Logging.Syslog import syslog
595     +from Mailman.Handlers.CookHeaders import change_header
596    
597     CRNL = '\r\n'
598     EMPTYSTRING = ''
599     @@ -60,8 +61,9 @@ def process(mlist, msg, msgdata):
600     break
601     if hits:
602     msgdata['topichits'] = hits.keys()
603     - msg['X-Topics'] = NLTAB.join(hits.keys())
604     -
605     + change_header('X-Topics', NLTAB.join(hits.keys()),
606     + mlist, msg, msgdata, delete=False)
607     +
608    
609    
610     def scanbody(msg, numlines=None):
611     diff --git a/Mailman/Handlers/WrapMessage.py b/Mailman/Handlers/WrapMessage.py
612     new file mode 100644
613     index 0000000..9678f6f
614     --- /dev/null
615     +++ b/Mailman/Handlers/WrapMessage.py
616     @@ -0,0 +1,72 @@
617     +# Copyright (C) 2013-2014 by the Free Software Foundation, Inc.
618     +#
619     +# This program is free software; you can redistribute it and/or
620     +# modify it under the terms of the GNU General Public License
621     +# as published by the Free Software Foundation; either version 2
622     +# of the License, or (at your option) any later version.
623     +#
624     +# This program is distributed in the hope that it will be useful,
625     +# but WITHOUT ANY WARRANTY; without even the implied warranty of
626     +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
627     +# GNU General Public License for more details.
628     +#
629     +# You should have received a copy of the GNU General Public License
630     +# along with this program; if not, write to the Free Software
631     +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
632     +# USA.
633     +
634     +"""Wrap the message in an outer message/rfc822 part and transfer/add
635     +some headers from the original.
636     +
637     +Also, in the case of Munge From, replace the From: and Reply-To: in the
638     +original message.
639     +"""
640     +
641     +import copy
642     +
643     +from Mailman import mm_cfg
644     +from Mailman.Utils import unique_message_id
645     +from Mailman.Message import Message
646     +
647     +# Headers from the original that we want to keep in the wrapper.
648     +KEEPERS = ('to',
649     + 'in-reply-to',
650     + 'references',
651     + 'x-mailman-approved-at',
652     + )
653     +
654     +
655     +
656     +def process(mlist, msg, msgdata):
657     + # This is the negation of we're wrapping because dmarc_moderation_action
658     + # is wrap this message or from_is_list applies and is wrap.
659     + if not (msgdata.get('from_is_list') == 2 or
660     + (mlist.from_is_list == 2 and msgdata.get('from_is_list') == 0)):
661     + # Now see if we need to add a From: and/or Reply-To: without wrapping.
662     + a_h = msgdata.get('add_header')
663     + if a_h:
664     + if a_h.get('From'):
665     + del msg['from']
666     + msg['From'] = a_h.get('From')
667     + if a_h.get('Reply-To'):
668     + del msg['reply-to']
669     + msg['Reply-To'] = a_h.get('Reply-To')
670     + return
671     +
672     + # There are various headers in msg that we don't want, so we basically
673     + # make a copy of the msg, then delete almost everything and set/copy
674     + # what we want.
675     + omsg = copy.deepcopy(msg)
676     + for key in msg.keys():
677     + if key.lower() not in KEEPERS:
678     + del msg[key]
679     + msg['MIME-Version'] = '1.0'
680     + msg['Content-Type'] = 'message/rfc822'
681     + msg['Content-Disposition'] = 'inline'
682     + msg['Message-ID'] = unique_message_id(mlist)
683     + # Add the headers from CookHeaders.
684     + for k, v in msgdata['add_header'].items():
685     + msg[k] = v
686     + # And set the payload.
687     + msg.set_payload(omsg.as_string())
688     +
689     diff --git a/Mailman/MailList.py b/Mailman/MailList.py
690     old mode 100644
691     new mode 100755
692     index 6083fb1..f948b69
693     --- a/Mailman/MailList.py
694     +++ b/Mailman/MailList.py
695     @@ -346,6 +346,7 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin,
696     self.bounce_matching_headers = \
697     mm_cfg.DEFAULT_BOUNCE_MATCHING_HEADERS
698     self.header_filter_rules = []
699     + self.from_is_list = mm_cfg.DEFAULT_FROM_IS_LIST
700     self.anonymous_list = mm_cfg.DEFAULT_ANONYMOUS_LIST
701     internalname = self.internal_name()
702     self.real_name = internalname[0].upper() + internalname[1:]
703     @@ -386,6 +387,10 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin,
704     # 2==Discard
705     self.member_moderation_action = 0
706     self.member_moderation_notice = ''
707     + self.dmarc_moderation_action = mm_cfg.DEFAULT_DMARC_MODERATION_ACTION
708     + self.dmarc_quarantine_moderation_action = (
709     + mm_cfg.DEFAULT_DMARC_QUARANTINE_MODERATION_ACTION)
710     + self.dmarc_moderation_notice = ''
711     self.accept_these_nonmembers = []
712     self.hold_these_nonmembers = []
713     self.reject_these_nonmembers = []
714     @@ -712,7 +717,14 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin,
715     def CheckVersion(self, stored_state):
716     """Auto-update schema if necessary."""
717     if self.data_version >= mm_cfg.DATA_FILE_VERSION:
718     - return
719     + # Some lists could have been created by newer Mailman version than
720     + # this one. We are adding just few variables, so check for these
721     + # variables explicitely.
722     + if (hasattr(self, "from_is_list")
723     + and hasattr(self, "dmarc_moderation_action")
724     + and hasattr(self, "dmarc_moderation_notice")
725     + and hasattr(self, "dmarc_quarantine_moderation_action")):
726     + return
727     # Initialize any new variables
728     self.InitVars()
729     # Then reload the database (but don't recurse). Force a reload even
730     @@ -1025,7 +1030,8 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin,
731     # And send an acknowledgement to the user...
732     if userack:
733     self.SendUnsubscribeAck(emailaddr, userlang)
734     - # ...and to the administrator
735     + # ...and to the administrator in the correct language. (LP: #1308655)
736     + i18n.set_language(self.preferred_language)
737     if admin_notif:
738     realname = self.real_name
739     subject = _('%(realname)s unsubscribe notification')
740     diff --git a/Mailman/Message.py b/Mailman/Message.py
741     index 84e4aa2..13e7ff2 100644
742     --- a/Mailman/Message.py
743     +++ b/Mailman/Message.py
744     @@ -61,6 +61,43 @@ class Generator(email.Generator.Generator):
745    
746    
747    
748     +class Generator(email.Generator.Generator):
749     + """Generates output from a Message object tree, keeping signatures.
750     +
751     + Headers will by default _not_ be folded in attachments.
752     + """
753     + def __init__(self, outfp, mangle_from_=True,
754     + maxheaderlen=78, children_maxheaderlen=0):
755     + email.Generator.Generator.__init__(self, outfp,
756     + mangle_from_=mangle_from_, maxheaderlen=maxheaderlen)
757     + self.__children_maxheaderlen = children_maxheaderlen
758     +
759     + def clone(self, fp):
760     + """Clone this generator with maxheaderlen set for children"""
761     + return self.__class__(fp, self._mangle_from_,
762     + self.__children_maxheaderlen, self.__children_maxheaderlen)
763     +
764     + # This is the _handle_message method with the fix for bug 7970.
765     + def _handle_message(self, msg):
766     + s = StringIO()
767     + g = self.clone(s)
768     + # The payload of a message/rfc822 part should be a multipart sequence
769     + # of length 1. The zeroth element of the list should be the Message
770     + # object for the subpart. Extract that object, stringify it, and
771     + # write it out.
772     + # Except, it turns out, when it's a string instead, which happens when
773     + # and only when HeaderParser is used on a message of mime type
774     + # message/rfc822. Such messages are generated by, for example,
775     + # Groupwise when forwarding unadorned messages. (Issue 7970.) So
776     + # in that case we just emit the string body.
777     + payload = msg.get_payload()
778     + if isinstance(payload, list):
779     + g.flatten(msg.get_payload(0), unixfrom=False)
780     + payload = s.getvalue()
781     + self._fp.write(payload)
782     +
783     +
784     +
785     class Message(email.Message.Message):
786     def __init__(self):
787     # We need a version number so that we can optimize __setstate__()
788     @@ -243,6 +280,20 @@ class Message(email.Message.Message):
789     return fp.getvalue()
790    
791    
792     + def as_string(self, unixfrom=False, mangle_from_=True):
793     + """Return entire formatted message as a string using
794     + Mailman.Message.Generator.
795     +
796     + Operates like email.Message.Message.as_string, only
797     + using Mailman's Message.Generator class. Only the top headers will
798     + get folded.
799     + """
800     + fp = StringIO()
801     + g = Generator(fp, mangle_from_=mangle_from_)
802     + g.flatten(self, unixfrom=unixfrom)
803     + return fp.getvalue()
804     +
805     +
806    
807     class UserNotification(Message):
808     """Class for internally crafted messages."""
809     diff --git a/Mailman/Utils.py b/Mailman/Utils.py
810     index c8275df..8021942 100644
811     --- a/Mailman/Utils.py
812     +++ b/Mailman/Utils.py
813     @@ -71,6 +71,14 @@ except NameError:
814     True = 1
815     False = 0
816    
817     +try:
818     + import dns.resolver
819     + import dns.rdatatype
820     + from dns.exception import DNSException
821     + dns_resolver = True
822     +except ImportError:
823     + dns_resolver = False
824     +
825     EMPTYSTRING = ''
826     UEMPTYSTRING = u''
827     NL = '\n'
828     @@ -1047,3 +1055,91 @@ def suspiciousHTML(html):
829     else:
830     return False
831    
832     +
833     +
834     +
835     +# This takes an email address, and returns True if DMARC policy is p=reject
836     +# or possibly quarantine.
837     +def IsDMARCProhibited(mlist, email):
838     + if not dns_resolver:
839     + return False
840     +
841     + email = email.lower()
842     + at_sign = email.find('@')
843     + if at_sign < 1:
844     + return False
845     + dmarc_domain = '_dmarc.' + email[at_sign+1:]
846     +
847     + try:
848     + resolver = dns.resolver.Resolver()
849     + resolver.timeout = float(mm_cfg.DMARC_RESOLVER_TIMEOUT)
850     + resolver.lifetime = float(mm_cfg.DMARC_RESOLVER_LIFETIME)
851     + txt_recs = resolver.query(dmarc_domain, dns.rdatatype.TXT)
852     + except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
853     + return False
854     + except DNSException, e:
855     + syslog('error',
856     + 'DNSException: Unable to query DMARC policy for %s (%s). %s',
857     + email, dmarc_domain, e.__class__)
858     + return False
859     + else:
860     +# people are already being dumb, don't trust them to provide honest DNS
861     +# where the answer section only contains what was asked for, nor to include
862     +# CNAMEs before the values they point to.
863     + full_record = ""
864     + results_by_name = {}
865     + cnames = {}
866     + want_names = set([dmarc_domain + '.'])
867     + for txt_rec in txt_recs.response.answer:
868     + if txt_rec.rdtype == dns.rdatatype.CNAME:
869     + cnames[txt_rec.name.to_text()] = (
870     + txt_rec.items[0].target.to_text())
871     + if txt_rec.rdtype != dns.rdatatype.TXT:
872     + continue
873     + results_by_name.setdefault(txt_rec.name.to_text(), []).append(
874     + "".join(txt_rec.items[0].strings))
875     + expands = list(want_names)
876     + seen = set(expands)
877     + while expands:
878     + item = expands.pop(0)
879     + if item in cnames:
880     + if cnames[item] in seen:
881     + continue # cname loop
882     + expands.append(cnames[item])
883     + seen.add(cnames[item])
884     + want_names.add(cnames[item])
885     + want_names.discard(item)
886     +
887     + if len(want_names) != 1:
888     + syslog('error',
889     + """multiple DMARC entries in results for %s,
890     + processing each to be strict""",
891     + dmarc_domain)
892     + for name in want_names:
893     + if name not in results_by_name:
894     + continue
895     + dmarcs = filter(lambda n: n.startswith('v=DMARC1;'),
896     + results_by_name[name])
897     + if len(dmarcs) == 0:
898     + return False
899     + if len(dmarcs) > 1:
900     + syslog('error',
901     + """RRset of TXT records for %s has %d v=DMARC1 entries;
902     + testing them all""",
903     + dmarc_domain, len(dmarc))
904     + for entry in dmarcs:
905     + if re.search(r'\bp=reject\b', entry, re.IGNORECASE):
906     + syslog('vette',
907     + 'DMARC lookup for %s (%s) found p=reject in %s = %s',
908     + email, dmarc_domain, name, entry)
909     + return True
910     +
911     + if (mlist.dmarc_quarantine_moderation_action and
912     + re.search(r'\bp=quarantine\b', entry, re.IGNORECASE)):
913     + syslog('vette',
914     + 'DMARC lookup for %s (%s) found p=quarantine in %s = %s',
915     + email, dmarc_domain, name, entry)
916     + return True
917     +
918     + return False
919     +
920     diff --git a/Mailman/Version.py b/Mailman/Version.py
921     index 05e6500..af4a2df 100644
922     --- a/Mailman/Version.py
923     +++ b/Mailman/Version.py
924     @@ -37,7 +37,7 @@ HEX_VERSION = ((MAJOR_REV << 24) | (MINOR_REV << 16) | (MICRO_REV << 8) |
925     (REL_LEVEL << 4) | (REL_SERIAL << 0))
926    
927     # config.pck schema version number
928     -DATA_FILE_VERSION = 97
929     +DATA_FILE_VERSION = 98
930    
931     # qfile/*.db schema version number
932     QFILE_SCHEMA_VERSION = 3
933     diff --git a/Mailman/__init__.py b/Mailman/__init__.py
934     index f569e43..e773b2e 100644
935     --- a/Mailman/__init__.py
936     +++ b/Mailman/__init__.py
937     @@ -13,3 +13,6 @@
938     # You should have received a copy of the GNU General Public License
939     # along with this program; if not, write to the Free Software
940     # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
941     +
942     +import sys
943     +sys.path.append("/usr/lib/mailman/Mailman")
944     diff --git a/Mailman/versions.py b/Mailman/versions.py
945     old mode 100644
946     new mode 100755
947     index 81fafd5..138e770
948     --- a/Mailman/versions.py
949     +++ b/Mailman/versions.py
950     @@ -313,6 +313,9 @@ def UpdateOldVars(l, stored_state):
951     pass
952     else:
953     l.digest_members[k] = 0
954     + # from_is_list was called author_is_list in 2.1.16rc2 (only).
955     + PreferStored('author_is_list', 'from_is_list',
956     + mm_cfg.DEFAULT_FROM_IS_LIST)
957    
958    
959    
960     @@ -383,6 +386,11 @@ def NewVars(l):
961     # the current GUI description model. So, 0==Hold, 1==Reject, 2==Discard
962     add_only_if_missing('member_moderation_action', 0)
963     add_only_if_missing('member_moderation_notice', '')
964     + add_only_if_missing('dmarc_moderation_action',
965     + mm_cfg.DEFAULT_DMARC_MODERATION_ACTION)
966     + add_only_if_missing('dmarc_quarantine_moderation_action',
967     + mm_cfg.DEFAULT_DMARC_QUARANTINE_MODERATION_ACTION)
968     + add_only_if_missing('dmarc_moderation_notice', '')
969     add_only_if_missing('new_member_options',
970     mm_cfg.DEFAULT_NEW_MEMBER_OPTIONS)
971     # Emergency moderation flag
972     diff --git a/contrib/majordomo2mailman.pl b/contrib/majordomo2mailman.pl
973     index c874862..770dc57 100644
974     --- a/contrib/majordomo2mailman.pl
975     +++ b/contrib/majordomo2mailman.pl
976     @@ -480,6 +480,7 @@ sub init_defaultmmconf {
977     'max_num_recipients', "10",
978     'forbidden_posters', "[]",
979     'bounce_matching_headers', "\"\"\"\n\"\"\"\n",
980     + 'from_is_list', "0",
981     'anonymous_list', "0",
982     'nondigestable', "1",
983     'digestable', "1",

admin@koozali.org
ViewVC Help
Powered by ViewVC 1.2.1 RSS 2.0 feed