Merge lp:~abompard/mailman/bug-1130696 into lp:mailman

Proposed by Aurélien Bompard
Status: Merged
Approved by: Barry Warsaw
Approved revision: 7271
Merged at revision: 7275
Proposed branch: lp:~abompard/mailman/bug-1130696
Merge into: lp:mailman
Diff against target: 289 lines (+124/-46)
6 files modified
src/mailman/core/tests/test_runner.py (+23/-2)
src/mailman/email/message.py (+6/-0)
src/mailman/handlers/tests/test_cook_headers.py (+54/-0)
src/mailman/runners/digest.py (+2/-2)
src/mailman/runners/tests/test_digest.py (+17/-42)
src/mailman/testing/helpers.py (+22/-0)
To merge this branch: bzr merge lp:~abompard/mailman/bug-1130696
Reviewer Review Type Date Requested Status
Barry Warsaw Approve
Review via email: mp+243226@code.launchpad.net

Description of the change

Handle the MIMEMultipart created by the DigestRunner properly, fixes bug #1130696.

To post a comment you must log in.
Revision history for this message
Barry Warsaw (barry) wrote :

This doesn't quite feel like it's getting to the root cause. Why don't we get a mailman.email.message.Message object here?

Revision history for this message
Aurélien Bompard (abompard) wrote :

Because the digest runner is creating a MIMEMultipart instance on line 175 of src/mailman/runners/digest.py. Should we change that instead?

Revision history for this message
Barry Warsaw (barry) wrote :

> Because the digest runner is creating a MIMEMultipart instance on line 175 of
> src/mailman/runners/digest.py. Should we change that instead?

Good catch. Yes, I think we should change that. What I think I'd like to see is a test that creates a mime digest, and uses that to trigger the attribute errors, rather than creating a MIMEMultipart inside the tests themselves. Does that makes sense?

Then the fix would be to change MIMEDigester._make_message() to create the proper subclass. The question then is what that method should return. My first thought is a class that inherits both from MIMEMultipart and mailman.email.message.Message.

What do you think?

lp:~abompard/mailman/bug-1130696 updated
7271. By Aurélien Bompard

Make the MIMEDigester generate a subclass of MIMEMultipart with our additions

Fixes bug #1130696 (differently)

Revision history for this message
Aurélien Bompard (abompard) wrote :

Alright, I just changed that, and I tried generating a very simple subclass of MIMEMultipart and our Message instance. All tests seem to pass, but I'm not very familiar with the tricks of multiple inheritance / mixins.
I'm pushing the branch right now.

Revision history for this message
Barry Warsaw (barry) wrote :

The general approach works great. I just did some cleanup and added a test (mostly because during my merge, I made a typo that broke digester.rst and required more investigation ;).

As always, thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/mailman/core/tests/test_runner.py'
2--- src/mailman/core/tests/test_runner.py 2014-01-01 14:59:42 +0000
3+++ src/mailman/core/tests/test_runner.py 2014-12-09 11:27:43 +0000
4@@ -25,6 +25,7 @@
5 ]
6
7
8+import os
9 import unittest
10
11 from mailman.app.lifecycle import create_list
12@@ -32,8 +33,9 @@
13 from mailman.core.runner import Runner
14 from mailman.interfaces.runner import RunnerCrashEvent
15 from mailman.testing.helpers import (
16- configuration, event_subscribers, get_queue_messages,
17- make_testable_runner, specialized_message_from_string as mfs)
18+ configuration, event_subscribers, get_queue_messages, LogFileMark,
19+ make_testable_runner, specialized_message_from_string as mfs,
20+ make_digest_messages)
21 from mailman.testing.layers import ConfigLayer
22
23
24@@ -42,6 +44,11 @@
25 def _dispose(self, mlist, msg, msgdata):
26 raise RuntimeError('borked')
27
28+
29
30+class StoringRunner(Runner):
31+ _disposed = []
32+ def _dispose(self, mlist, msg, msgdata):
33+ self._disposed.append((mlist, msg, msgdata))
34
35
36
37 class TestRunner(unittest.TestCase):
38@@ -87,3 +94,17 @@
39 shunted = get_queue_messages('shunt')
40 self.assertEqual(len(shunted), 1)
41 self.assertEqual(shunted[0].msg['message-id'], '<ant>')
42+
43+ def test_digest_messages(self):
44+ # Digest messages can be MIMEMultipart (LP#1130696)
45+ runner = make_testable_runner(StoringRunner, 'in')
46+ make_digest_messages(self._mlist)
47+ for bag in get_queue_messages("virgin"):
48+ config.switchboards['in'].enqueue(bag.msg, listname='test@example.com')
49+ error_log = LogFileMark('mailman.error')
50+ with event_subscribers(self._got_event):
51+ runner.run()
52+ errors = error_log.read()
53+ self.assertEqual(len(self._events), 0, errors)
54+ self.assertEqual(len(get_queue_messages("shunt")), 0, errors)
55+ self.assertEqual(len(runner._disposed), 2)
56
57=== modified file 'src/mailman/email/message.py'
58--- src/mailman/email/message.py 2014-12-09 01:38:26 +0000
59+++ src/mailman/email/message.py 2014-12-09 11:27:43 +0000
60@@ -36,6 +36,7 @@
61 import email
62 import email.message
63 import email.utils
64+import email.mime.multipart
65
66 from email.header import Header
67
68@@ -133,6 +134,11 @@
69
70
71
72
73+class MultipartMessage(email.mime.multipart.MIMEMultipart, Message):
74+ """Class for MIME digest messages"""
75+
76+
77+
78
79 class UserNotification(Message):
80 """Class for internally crafted messages."""
81
82
83=== added file 'src/mailman/handlers/tests/test_cook_headers.py'
84--- src/mailman/handlers/tests/test_cook_headers.py 1970-01-01 00:00:00 +0000
85+++ src/mailman/handlers/tests/test_cook_headers.py 2014-12-09 11:27:43 +0000
86@@ -0,0 +1,54 @@
87+# Copyright (C) 2012-2014 by the Free Software Foundation, Inc.
88+#
89+# This file is part of GNU Mailman.
90+#
91+# GNU Mailman is free software: you can redistribute it and/or modify it under
92+# the terms of the GNU General Public License as published by the Free
93+# Software Foundation, either version 3 of the License, or (at your option)
94+# any later version.
95+#
96+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
97+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
98+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
99+# more details.
100+#
101+# You should have received a copy of the GNU General Public License along with
102+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
103+
104+"""Test the cook_headers handler."""
105+
106+from __future__ import absolute_import, print_function, unicode_literals
107+
108+__metaclass__ = type
109+__all__ = [
110+ 'TestCookHeaders',
111+ ]
112+
113+
114+import unittest
115+
116+from mailman.app.lifecycle import create_list
117+from mailman.handlers import cook_headers
118+from mailman.testing.helpers import get_queue_messages, make_digest_messages
119+from mailman.testing.layers import ConfigLayer
120+
121+
122+
123
124+class TestCookHeaders(unittest.TestCase):
125+ """Test the cook_headers handler."""
126+
127+ layer = ConfigLayer
128+
129+ def setUp(self):
130+ self._mlist = create_list('test@example.com')
131+
132+ def test_process_digest(self):
133+ # Digest messages can be MIMEMultipart (LP#1130696)
134+ make_digest_messages(self._mlist)
135+ messages = [ bag.msg for bag in get_queue_messages("virgin") ]
136+ self.assertEqual(len(messages), 2)
137+ for msg in messages:
138+ try:
139+ cook_headers.process(self._mlist, msg, {})
140+ except AttributeError as e:
141+ self.fail(e)
142
143=== modified file 'src/mailman/runners/digest.py'
144--- src/mailman/runners/digest.py 2014-11-29 20:10:59 +0000
145+++ src/mailman/runners/digest.py 2014-12-09 11:27:43 +0000
146@@ -32,7 +32,6 @@
147 from StringIO import StringIO
148 from copy import deepcopy
149 from email.header import Header
150-from email.message import Message
151 from email.mime.message import MIMEMessage
152 from email.mime.multipart import MIMEMultipart
153 from email.mime.text import MIMEText
154@@ -42,6 +41,7 @@
155 from mailman.config import config
156 from mailman.core.i18n import _
157 from mailman.core.runner import Runner
158+from mailman.email.message import Message, MultipartMessage
159 from mailman.handlers.decorate import decorate
160 from mailman.interfaces.member import DeliveryMode, DeliveryStatus
161 from mailman.utilities.i18n import make
162@@ -172,7 +172,7 @@
163 self._keepers = set(config.digests.mime_digest_keep_headers.split())
164
165 def _make_message(self):
166- return MIMEMultipart('mixed')
167+ return MultipartMessage('mixed')
168
169 def add_toc(self, count):
170 """Add the table of contents."""
171
172=== modified file 'src/mailman/runners/tests/test_digest.py'
173--- src/mailman/runners/tests/test_digest.py 2014-11-29 20:10:59 +0000
174+++ src/mailman/runners/tests/test_digest.py 2014-12-09 11:27:43 +0000
175@@ -35,7 +35,7 @@
176 from mailman.runners.digest import DigestRunner
177 from mailman.testing.helpers import (
178 LogFileMark, digest_mbox, get_queue_messages, make_testable_runner,
179- specialized_message_from_string as mfs)
180+ specialized_message_from_string as mfs, make_digest_messages)
181 from mailman.testing.layers import ConfigLayer
182
183
184@@ -54,31 +54,21 @@
185 self._runner = make_testable_runner(DigestRunner, 'digest')
186 self._process = config.handlers['to-digest'].process
187
188+ def _check_virgin_queue(self):
189+ # There should be two messages in the virgin queue: the digest as
190+ # plain-text and as multipart.
191+ messages = get_queue_messages('virgin')
192+ self.assertEqual(len(messages), 2)
193+ self.assertEqual(
194+ sorted(item.msg.get_content_type() for item in messages),
195+ ['multipart/mixed', 'text/plain'])
196+ for item in messages:
197+ self.assertEqual(item.msg['subject'],
198+ 'Test Digest, Vol 1, Issue 1')
199+
200 def test_simple_message(self):
201- msg = mfs("""\
202-From: anne@example.org
203-To: test@example.com
204-
205-message triggering a digest
206-""")
207- mbox_path = os.path.join(self._mlist.data_path, 'digest.mmdf')
208- self._process(self._mlist, msg, {})
209- self._digestq.enqueue(
210- msg,
211- listname=self._mlist.fqdn_listname,
212- digest_path=mbox_path,
213- volume=1, digest_number=1)
214- self._runner.run()
215- # There are two messages in the virgin queue: the digest as plain-text
216- # and as multipart.
217- messages = get_queue_messages('virgin')
218- self.assertEqual(len(messages), 2)
219- self.assertEqual(
220- sorted(item.msg.get_content_type() for item in messages),
221- ['multipart/mixed', 'text/plain'])
222- for item in messages:
223- self.assertEqual(item.msg['subject'],
224- 'Test Digest, Vol 1, Issue 1')
225+ make_digest_messages(self._mlist)
226+ self._check_virgin_queue()
227
228 def test_non_ascii_message(self):
229 msg = Message()
230@@ -88,25 +78,10 @@
231 msg.attach(MIMEText('message with non-ascii chars: \xc3\xa9',
232 'plain', 'utf-8'))
233 mbox = digest_mbox(self._mlist)
234- mbox_path = os.path.join(self._mlist.data_path, 'digest.mmdf')
235 mbox.add(msg.as_string())
236- self._digestq.enqueue(
237- msg,
238- listname=self._mlist.fqdn_listname,
239- digest_path=mbox_path,
240- volume=1, digest_number=1)
241 # Use any error logs as the error message if the test fails.
242 error_log = LogFileMark('mailman.error')
243- self._runner.run()
244+ make_digest_messages(self._mlist, msg)
245 # The runner will send the file to the shunt queue on exception.
246 self.assertEqual(len(self._shuntq.files), 0, error_log.read())
247- # There are two messages in the virgin queue: the digest as plain-text
248- # and as multipart.
249- messages = get_queue_messages('virgin')
250- self.assertEqual(len(messages), 2)
251- self.assertEqual(
252- sorted(item.msg.get_content_type() for item in messages),
253- ['multipart/mixed', 'text/plain'])
254- for item in messages:
255- self.assertEqual(item.msg['subject'],
256- 'Test Digest, Vol 1, Issue 1')
257+ self._check_virgin_queue()
258
259=== modified file 'src/mailman/testing/helpers.py'
260--- src/mailman/testing/helpers.py 2014-11-29 20:10:59 +0000
261+++ src/mailman/testing/helpers.py 2014-12-09 11:27:43 +0000
262@@ -72,6 +72,7 @@
263 from mailman.interfaces.messages import IMessageStore
264 from mailman.interfaces.styles import IStyleManager
265 from mailman.interfaces.usermanager import IUserManager
266+from mailman.runners.digest import DigestRunner
267 from mailman.utilities.mailbox import Mailbox
268
269
270@@ -529,3 +530,24 @@
271 with open(self._filename) as fp:
272 fp.seek(self._filepos)
273 return fp.read()
274+
275+
276+
277
278+def make_digest_messages(mlist, msg=None):
279+ if msg is None:
280+ msg = specialized_message_from_string("""\
281+From: anne@example.org
282+To: {listname}
283+Message-ID: <testing>
284+
285+message triggering a digest
286+""".format(listname=mlist.fqdn_listname))
287+ mbox_path = os.path.join(mlist.data_path, 'digest.mmdf')
288+ config.handlers['to-digest'].process(mlist, msg, {})
289+ config.switchboards['digest'].enqueue(
290+ msg,
291+ listname=mlist.fqdn_listname,
292+ digest_path=mbox_path,
293+ volume=1, digest_number=1)
294+ runner = make_testable_runner(DigestRunner, 'digest')
295+ runner.run()