Merge ~cjwatson/launchpad:py3-process-mail into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: b6c0e82c0c0ea57751550737e66a488e52605c8c
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:py3-process-mail
Merge into: launchpad:master
Diff against target: 197 lines (+88/-8)
5 files modified
lib/lp/services/mail/incoming.py (+3/-3)
lib/lp/services/mail/notification.py (+3/-3)
lib/lp/services/mail/sendmail.py (+2/-2)
lib/lp/services/mail/tests/incomingmail.txt (+53/-0)
lib/lp/services/mail/tests/test_incoming.py (+27/-0)
Reviewer Review Type Date Requested Status
Cristian Gonzalez (community) Approve
Review via email: mp+405924@code.launchpad.net

Commit message

process-mail: Improve handling of non-MIME-encoded headers on py3

Description of the change

Python 3's email package is somewhat stricter about various aspects of email encoding, so we need to be more careful to handle the case of incoming email with non-MIME-encoded non-ASCII From: headers.

To post a comment you must log in.
Revision history for this message
Cristian Gonzalez (cristiangsp) wrote :

Looks good!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/services/mail/incoming.py b/lib/lp/services/mail/incoming.py
2index 958985b..92ca11c 100644
3--- a/lib/lp/services/mail/incoming.py
4+++ b/lib/lp/services/mail/incoming.py
5@@ -186,7 +186,7 @@ def _verifyDkimOrigin(signed_message):
6 for origin in ['From', 'Sender']:
7 if signed_message[origin] is None:
8 continue
9- name, addr = parseaddr(signed_message[origin])
10+ addr = parseaddr(str(signed_message[origin]))[1]
11 try:
12 origin_domain = addr.split('@')[1]
13 except IndexError:
14@@ -265,7 +265,7 @@ def authenticateEmail(mail, signature_timestamp_checker=None):
15
16 principal, dkim_trusted_address = _getPrincipalByDkim(mail)
17 if dkim_trusted_address is None:
18- from_addr = parseaddr(mail['From'])[1]
19+ from_addr = parseaddr(str(mail['From']))[1]
20 try:
21 principal = authutil.getPrincipalByLogin(from_addr)
22 except (TypeError, UnicodeDecodeError):
23@@ -310,7 +310,7 @@ def _gpgAuthenticateEmail(mail, principal, person,
24 """
25 log = logging.getLogger('process-mail')
26 signature = mail.signature
27- email_addr = parseaddr(mail['From'])[1]
28+ email_addr = parseaddr(str(mail['From']))[1]
29 if signature is None:
30 # Mark the principal so that application code can check that the
31 # user was weakly authenticated.
32diff --git a/lib/lp/services/mail/notification.py b/lib/lp/services/mail/notification.py
33index 827cdf9..784a20e 100644
34--- a/lib/lp/services/mail/notification.py
35+++ b/lib/lp/services/mail/notification.py
36@@ -12,13 +12,13 @@ __all__ = [
37
38
39 from difflib import unified_diff
40-from email import message_from_string
41 from email.mime.message import MIMEMessage
42 from email.mime.multipart import MIMEMultipart
43 from email.mime.text import MIMEText
44 import re
45
46 from lp.bugs.mail.bugnotificationbuilder import get_bugmail_error_address
47+from lp.services.compat import message_from_bytes
48 from lp.services.config import config
49 from lp.services.mail.helpers import get_email_template
50 from lp.services.mail.mailwrapper import MailWrapper
51@@ -71,10 +71,10 @@ def send_process_error_notification(to_address, subject, error_msg,
52 msg['From'] = get_bugmail_error_address()
53 msg['Subject'] = subject
54 msg.attach(error_part)
55- original_msg_str = str(original_msg)
56+ original_msg_str = bytes(original_msg)
57 if len(original_msg_str) > max_return_size:
58 truncated_msg_str = original_msg_str[:max_return_size]
59- original_msg = message_from_string(truncated_msg_str)
60+ original_msg = message_from_bytes(truncated_msg_str)
61 msg.attach(MIMEMessage(original_msg))
62 sendmail(msg)
63
64diff --git a/lib/lp/services/mail/sendmail.py b/lib/lp/services/mail/sendmail.py
65index 72ca82d..f1ab3c6 100644
66--- a/lib/lp/services/mail/sendmail.py
67+++ b/lib/lp/services/mail/sendmail.py
68@@ -396,9 +396,9 @@ def sendmail(message, to_addrs=None, bulk=True):
69 """
70 validate_message(message)
71 if to_addrs is None:
72- to_addrs = get_addresses_from_header(message['to'])
73+ to_addrs = get_addresses_from_header(str(message['to']))
74 if message['cc']:
75- to_addrs = to_addrs + get_addresses_from_header(message['cc'])
76+ to_addrs = to_addrs + get_addresses_from_header(str(message['cc']))
77
78 do_paranoid_envelope_to_validation(to_addrs)
79
80diff --git a/lib/lp/services/mail/tests/incomingmail.txt b/lib/lp/services/mail/tests/incomingmail.txt
81index 341c248..4c21007 100644
82--- a/lib/lp/services/mail/tests/incomingmail.txt
83+++ b/lib/lp/services/mail/tests/incomingmail.txt
84@@ -244,6 +244,7 @@ attempting to process incoming mail.
85 ... pass
86 >>> @implementer(IMailHandler)
87 ... class OopsHandler:
88+ ... allow_unknown_users = True
89 ... def process(self, mail, to_addr, filealias):
90 ... raise TestOopsException()
91 >>> mail_handlers.add('oops.com', OopsHandler())
92@@ -298,6 +299,58 @@ to the user, citing the OOPS ID, with the original message attached.
93 >>> print(original_message.get_payload(0)['Subject'])
94 doesn't matter
95
96+OOPS notifications work even if the From: address isn't properly MIME-encoded.
97+
98+ >>> from lp.services.compat import message_from_bytes
99+ >>> msg = message_from_bytes(
100+ ... u"""From: \u05D1 <bet@canonical.com>
101+ ... To: launchpad@oops.com
102+ ... X-Launchpad-Original-To: launchpad@oops.com
103+ ... Subject: doesn't matter
104+ ...
105+ ... doesn't matter
106+ ... """.encode('UTF-8'))
107+ >>> msgid = sendmail(msg, ['edit@malone-domain'])
108+ >>> handleMailForTest()
109+ ERROR:process-mail:An exception was raised inside the handler:
110+ ...
111+ TestOopsException
112+
113+ >>> from email.header import (
114+ ... decode_header,
115+ ... make_header,
116+ ... )
117+ >>> from email.utils import parseaddr
118+ >>> notification = pop_notifications()[-1]
119+ >>> print(notification.get_content_type())
120+ multipart/mixed
121+ >>> print(pretty(six.ensure_text(decode_header(notification['To'])[0][0])))
122+ '\u05d1 <bet@canonical.com>'
123+ >>> error_message, original_message = notification.get_payload()
124+ >>> print(error_message.get_content_type())
125+ text/plain
126+ >>> print(error_message.get_payload(decode=True).decode())
127+ An error occurred while processing a mail you sent to Launchpad's email
128+ interface.
129+ ...
130+ Sorry, something went wrong when Launchpad tried processing your mail.
131+ We've recorded what happened, and we'll fix it as soon as possible.
132+ Apologies for the inconvenience.
133+ <BLANKLINE>
134+ If this is blocking your work, please file a question at
135+ https://answers.launchpad.net/launchpad/+addquestion
136+ and include the error ID OOPS-... in the descr...
137+ >>> print(original_message.get_content_type())
138+ message/rfc822
139+ >>> print(parseaddr(str(original_message.get_payload(0)['From']))[1])
140+ bet@canonical.com
141+ >>> print(original_message.get_payload(0)['To'])
142+ launchpad@oops.com
143+ >>> print(original_message.get_payload(0)['X-Launchpad-Original-To'])
144+ launchpad@oops.com
145+ >>> print(original_message.get_payload(0)['Subject'])
146+ doesn't matter
147+
148 Unauthorized exceptions, which are ignored for the purpose of OOPS
149 reporting in the web interface, are not ignored in the email interface.
150
151diff --git a/lib/lp/services/mail/tests/test_incoming.py b/lib/lp/services/mail/tests/test_incoming.py
152index 57735c3..15b751e 100644
153--- a/lib/lp/services/mail/tests/test_incoming.py
154+++ b/lib/lp/services/mail/tests/test_incoming.py
155@@ -8,8 +8,10 @@ from email.header import (
156 make_header,
157 )
158 from email.mime.multipart import MIMEMultipart
159+from email.utils import parseaddr
160 import logging
161 import os
162+from textwrap import dedent
163 import unittest
164
165 import six
166@@ -277,6 +279,31 @@ class IncomingTestCase(TestCaseWithFactory):
167 six.text_type(make_header(decode_header(
168 test_handler.handledMails[0]['From']))))
169
170+ def test_invalid_from_address_no_mime_encoding(self):
171+ # An invalid From: header with non-MIME-encoded non-ASCII characters
172+ # is handled.
173+ test_handler = FakeHandler()
174+ mail_handlers.add('lp.dev', test_handler)
175+ raw_message = dedent(u"""\
176+ Content-Type: text/plain; charset="UTF-8"
177+ MIME-Version: 1.0
178+ Message-Id: <message-id>
179+ To: test@lp.dev
180+ From: \u05D0 <alef@eg.dom>
181+ Subject: subject
182+
183+ body
184+ """).encode('UTF-8')
185+ TestMailer().send(
186+ u'\u05D0 <alef@eg.dom>'.encode('UTF-8'), 'test@lp.dev',
187+ raw_message)
188+ handleMail()
189+ self.assertEqual([], self.oopses)
190+ self.assertEqual(1, len(test_handler.handledMails))
191+ self.assertEqual(
192+ 'alef@eg.dom',
193+ parseaddr(str(test_handler.handledMails[0]['From']))[1])
194+
195 def test_invalid_cc_address_unicode(self):
196 # Invalid Cc: header such as no "@" is handled.
197 message, test_handler = self.makeSentMessage(

Subscribers

People subscribed via source and target branches

to status/vote changes: