1 |
stephdl |
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 |
|
|
+ -> 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<->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", |