Merge lp:~msapiro/mailman/bug_949924 into lp:mailman

Proposed by Mark Sapiro
Status: Merged
Approved by: Barry Warsaw
Approved revision: 7124
Merged at revision: 7142
Proposed branch: lp:~msapiro/mailman/bug_949924
Merge into: lp:mailman
Diff against target: 112 lines (+79/-1)
3 files modified
src/mailman/docs/NEWS.rst (+2/-0)
src/mailman/rules/approved.py (+3/-1)
src/mailman/rules/tests/test_approved.py (+74/-0)
To merge this branch: bzr merge lp:~msapiro/mailman/bug_949924
Reviewer Review Type Date Requested Status
Barry Warsaw Approve
Review via email: mp+97787@code.launchpad.net

Description of the change

This branch fixes mailman/rules/approved.py LP: #949924.

It is a very minimal fix. The issue is if we're looking for a Approve(d): pseudo header in the first text/plain part, and this part contains non-ascii characters, the statement "if ':' in line:" does an implicit decode to unicode which throws an exception on the non-ascii character. This apparently doesn't fail in 2.1 because 2.1 does line.find(':') instead. I fixed it by doing an explicit decode in the charset of the part with 'replace'.

I also changed a <> to != for Python 3

I didn't make any tests or NEWS entry. I did locally modify mailman/rules/docs/approve.rst (note the name discrepancy - approved.py vs approve.rst) like this:

=== modified file 'src/mailman/rules/docs/approve.rst'
--- src/mailman/rules/docs/approve.rst 2011-09-24 01:42:39 +0000
+++ src/mailman/rules/docs/approve.rst 2012-03-15 23:19:09 +0000
@@ -150,20 +150,25 @@

     >>> msg = message_from_string("""\
     ... From: <email address hidden>
+ ... MIME-Version: 1.0
+ ... Content-Type: text/plain; charset="iso-8859-1"
+ ... Content-Transfer-Encoding: quoted-printable
     ...
     ... Approved: abcxyz
     ... An important message.
+ ... A funny character =E4.
     ... """)
     >>> rule.check(mlist, msg, {})
     True

     >>> print msg.as_string()
     From: <email address hidden>
- Content-Transfer-Encoding: 7bit
+ Content-Transfer-Encoding: quoted-printable
     MIME-Version: 1.0
- Content-Type: text/plain; charset="us-ascii"
+ Content-Type: text/plain; charset="iso-8859-1"
     <BLANKLINE>
     An important message.
+ A funny character =E4.
     <BLANKLINE>

 As before, a mismatch in the pseudo-header does not approve the message, but

This does actually throw an exception without the fix and passes with it, but this is not the right place for this test, and there is no existing unit test module, and I wanted to expose this fix before taking the time to learn to construct a proper unit test.

There is also an extension to Approve.py in 2.1 which will reject the post if the (X-)Approve(d): header can't be found in an HTML part, but can be found after stripping out HTML tags. See comments #6 - #8 at LP: #266220 for more on this. I thought it was too much to try to incorporate that with this fix.

To post a comment you must log in.
lp:~msapiro/mailman/bug_949924 updated
7123. By Mark Sapiro

Merged from trunk.

7124. By Mark Sapiro

 * Added a unit test for rules/approved.py
 * Renamed rules/docs/approve.py to approved.py for consistency.

Revision history for this message
Mark Sapiro (msapiro) wrote :

I added a unit test. It probably needs some cleanup as I still don't really understand the unittest framework.

I renamed the approve.rst doctest to approved.rst for consistency.

I added a bug fix note to NEWS.

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

Thanks for backporting this fix to mm3 Mark! Here are some comments:

I noticed a lot of unused imports in test_approved.py. You might want to look into pyflakes (what I use) or pylint as tools to help find these things. I'll clean them up before I commit though.

I usually like to add a comment to the test_foo() method to explain exactly what the test is checking. Sometimes it's obvious, but you'd be surprised when looking at a test from the outside. :) Also, the comment can refer to the bug #. Since you expect the execution of the rule to not raise an exception, the test does not need to transform UnicodeError and UnicodeWarnings into AssertionErrors. Just let any UnicodeError that occurs cause the test to fail. So really, all you're checking is that the rule returns False, for which you can just use self.assertFalse().

I rewrote this line in approved.py:

cset = part.get_content_charset('us-ascii')

instead of using the 'or'.

All minor stuff. Thanks! Branch approved and I'll land it on trunk.

(Aside: I noticed that we're assuming the moderator password is stored in the clear, so after I land your branch I'm going to be sure it's hashed properly. I'll do that in a separate commit though.)

review: Approve
Revision history for this message
Mark Sapiro (msapiro) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 4/4/2012 2:18 PM, Barry Warsaw wrote:
>
> I noticed a lot of unused imports in test_approved.py. You might
> want to look into pyflakes (what I use) or pylint as tools to help
> find these things. I'll clean them up before I commit though.

Yes, I copied another unit test as a start point and I wasn't totally
sure if any of that stuff was required by the framework.

> I usually like to add a comment to the test_foo() method to
> explain exactly what the test is checking. Sometimes it's obvious,
> but you'd be surprised when looking at a test from the outside. :)
> Also, the comment can refer to the bug #. Since you expect the
> execution of the rule to not raise an exception, the test does not
> need to transform UnicodeError and UnicodeWarnings into
> AssertionErrors. Just let any UnicodeError that occurs cause the
> test to fail. So really, all you're checking is that the rule
> returns False, for which you can just use self.assertFalse().

Thanks.

> I rewrote this line in approved.py:
>
> cset = part.get_content_charset('us-ascii')
>
> instead of using the 'or'.

There's a reason for the or. Tokio said there are MUAs (wierd Japanese
ones) that do things like

Content-Type: xxx/xxx; charset=""

for the above, part.get_content_charset('us-ascii') returns the null
string. One could argue that the email package should handle this, but
that's not the case today. I should have commented that to indicate
why I was doing it. :(

> All minor stuff. Thanks! Branch approved and I'll land it on
> trunk.
>
> (Aside: I noticed that we're assuming the moderator password is
> stored in the clear, so after I land your branch I'm going to be
> sure it's hashed properly. I'll do that in a separate commit
> though.)

OK. Sounds good.

- --
Mark Sapiro <email address hidden> The highway is for gamblers,
San Francisco Bay Area, California better use your sense - B. Dylan

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.7 (MingW32)

iD8DBQFPfNd4VVuXXpU7hpMRAsrjAKCEKhnswux6ImO7jRprKqGGTVrXzwCgrzH5
ws4+GkSxBGwC30+h5RL4kew=
=3nS1
-----END PGP SIGNATURE-----

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/mailman/docs/NEWS.rst'
2--- src/mailman/docs/NEWS.rst 2012-03-17 16:54:26 +0000
3+++ src/mailman/docs/NEWS.rst 2012-03-18 22:11:19 +0000
4@@ -126,6 +126,8 @@
5
6 Bug fixes
7 ---------
8+ * Fixed a UnicodeError with non-ascii message bodies in rules/approved.py
9+ (LP: #949924)
10 * Subscription disabled probe warning notification messages are now sent
11 without a `Precedence:` header. Given by Mark Sapiro. (LP: #808821)
12 * Fixed KeyError in retry runner, contributed by Stephen A. Goss.
13
14=== modified file 'src/mailman/rules/approved.py'
15--- src/mailman/rules/approved.py 2012-01-01 19:14:46 +0000
16+++ src/mailman/rules/approved.py 2012-03-18 22:11:19 +0000
17@@ -73,10 +73,12 @@
18 break
19 payload = part.get_payload(decode=True)
20 if payload is not None:
21+ cset = part.get_content_charset() or 'us-ascii'
22+ payload = payload.decode(cset, 'replace')
23 line = ''
24 lines = payload.splitlines(True)
25 for lineno, line in enumerate(lines):
26- if line.strip() <> '':
27+ if line.strip() != '':
28 break
29 if ':' in line:
30 header, value = line.split(':', 1)
31
32=== renamed file 'src/mailman/rules/docs/approve.rst' => 'src/mailman/rules/docs/approved.rst'
33=== added directory 'src/mailman/rules/tests'
34=== added file 'src/mailman/rules/tests/__init__.py'
35=== added file 'src/mailman/rules/tests/test_approved.py'
36--- src/mailman/rules/tests/test_approved.py 1970-01-01 00:00:00 +0000
37+++ src/mailman/rules/tests/test_approved.py 2012-03-18 22:11:19 +0000
38@@ -0,0 +1,74 @@
39+# Copyright (C) 2012 by the Free Software Foundation, Inc.
40+#
41+# This file is part of GNU Mailman.
42+#
43+# GNU Mailman is free software: you can redistribute it and/or modify it under
44+# the terms of the GNU General Public License as published by the Free
45+# Software Foundation, either version 3 of the License, or (at your option)
46+# any later version.
47+#
48+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
49+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
50+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
51+# more details.
52+#
53+# You should have received a copy of the GNU General Public License along with
54+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
55+
56+"""Test the mime_delete handler."""
57+
58+from __future__ import absolute_import, print_function, unicode_literals
59+
60+__metaclass__ = type
61+__all__ = [
62+ 'TestApproved',
63+ ]
64+
65+
66+import unittest
67+
68+from zope.component import getUtility
69+
70+from mailman.app.lifecycle import create_list
71+from mailman.config import config
72+from mailman.core import errors
73+from mailman.interfaces.action import FilterAction
74+from mailman.interfaces.member import MemberRole
75+from mailman.interfaces.usermanager import IUserManager
76+from mailman.rules import approved
77+from mailman.testing.helpers import (
78+ LogFileMark,
79+ get_queue_messages,
80+ specialized_message_from_string as mfs)
81+from mailman.testing.layers import ConfigLayer
82+
83+
84+
85
86+class TestApproved(unittest.TestCase):
87+ """Test the approved handler."""
88+
89+ layer = ConfigLayer
90+
91+ def setUp(self):
92+ self._mlist = create_list('test@example.com')
93+ self._rule = approved.Approved()
94+ self._msg = mfs("""\
95+From: anne@example.com
96+To: test@example.com
97+Subject: A Message with non-ascii body
98+Message-ID: <ant>
99+MIME-Version: 1.0
100+Content-Type: text/plain; charset="iso-8859-1"
101+Content-Transfer-Encoding: quoted-printable
102+
103+This is a message body with a non-ascii character =E4
104+
105+""")
106+
107+ def test_approved_nonascii(self):
108+ result = True
109+ try:
110+ result = self._rule.check(self._mlist, self._msg, {})
111+ except (UnicodeError, UnicodeWarning):
112+ raise AssertionError('Non-ascii message raised UnicodeError')
113+ self.assertEqual(result, False)