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

Contents of /rpms/mailman/contribs9/mailman-2.1.12-dmarc.patch

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


Revision 1.1 - (show annotations) (download)
Sun Aug 23 19:54:39 2015 UTC (8 years, 8 months ago) by stephdl
Branch: MAIN
CVS Tags: mailman-2_1_12-103_el6_sme, mailman-2_1_12-100_el6_sme, mailman-2_1_12-104_el6_sme, mailman-2_1_12-25_el6_sme_sme, mailman-2_1_12-102_el6_sme, mailman-2_1_12-101_el6_sme, HEAD
added all files to cvs

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