diff --git a/core/mail/protocol.py b/core/mail/protocol.py index 5d732af..bc83230 100644 --- a/core/mail/protocol.py +++ b/core/mail/protocol.py @@ -43,6 +43,11 @@ def unescape_and_decode_base64(s): return decoded +def unescape_simplified_quoted_printable(s): + import quopri + return quopri.decodestring(s).decode('utf-8') + + def collect_references(issue_thread): mails = issue_thread.emails.order_by('timestamp') references = [] @@ -127,6 +132,8 @@ def parse_email_body(raw, log=None): continue segment = unescape_and_decode_quoted_printable(segment) segment = unescape_and_decode_base64(segment) + if part.get('Content-Transfer-Encoding') == 'quoted-printable': + segment = unescape_simplified_quoted_printable(segment) log.debug(segment) body = body + segment elif 'attachment' in cdispo or 'inline' in cdispo: @@ -158,6 +165,8 @@ def parse_email_body(raw, log=None): body = "Unknown content type" body = unescape_and_decode_quoted_printable(body) body = unescape_and_decode_base64(body) + if parsed.get('Content-Transfer-Encoding') == 'quoted-printable': + body = unescape_simplified_quoted_printable(body) log.debug(body) return parsed, body, attachments diff --git a/core/mail/tests/v2/test_mails.py b/core/mail/tests/v2/test_mails.py index d5d1c0c..ac03bac 100644 --- a/core/mail/tests/v2/test_mails.py +++ b/core/mail/tests/v2/test_mails.py @@ -880,3 +880,56 @@ hello \xe4\xf6\xfc''' states = StateChange.objects.filter(issue_thread=IssueThread.objects.all()[0]) self.assertEqual(1, len(states)) self.assertEqual('pending_new', states[0].state) + + def test_mail_quoted_printable_transfer_encoding(self): + from aiosmtpd.smtp import Envelope + from asgiref.sync import async_to_sync + import aiosmtplib + + aiosmtplib.send = make_mocked_coro() + + handler = LMTPHandler() + server = mock.Mock() + session = mock.Mock() + envelope = Envelope() + + envelope.mail_from = 'test1@test' + envelope.rcpt_tos = ['test2@test'] + + envelope.content = b'''Subject: test +From: test1@test +To: test2@test +Message-ID: <1@test> +Content-Type: text/html; charset=utf-8 +Content-Transfer-Encoding: quoted-printable + +hello =C3=A4=C3=B6=C3=BC''' + + result = async_to_sync(handler.handle_DATA)(server, session, envelope) + self.assertEqual('250 Message accepted for delivery', result) + self.assertEqual(2, len(Email.objects.all())) + self.assertEqual(1, len(IssueThread.objects.all())) + aiosmtplib.send.assert_called_once() + self.assertEqual('test', Email.objects.all()[0].subject) + self.assertEqual('test1@test', Email.objects.all()[0].sender) + self.assertEqual('test2@test', Email.objects.all()[0].recipient) + self.assertEqual('hello äöü', Email.objects.all()[0].body) + self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread) + self.assertEqual('<1@test>', Email.objects.all()[0].reference) + self.assertEqual(None, Email.objects.all()[0].in_reply_to) + self.assertEqual(expected_auto_reply_subject.format('test', IssueThread.objects.all()[0].short_uuid()), + Email.objects.all()[1].subject) + self.assertEqual('test2@test', Email.objects.all()[1].sender) + self.assertEqual('test1@test', Email.objects.all()[1].recipient) + self.assertEqual(expected_auto_reply.format(IssueThread.objects.all()[0].short_uuid()), + Email.objects.all()[1].body) + self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[1].issue_thread) + self.assertTrue(Email.objects.all()[1].reference.startswith("<")) + self.assertTrue(Email.objects.all()[1].reference.endswith("@localhost>")) + self.assertEqual("<1@test>", Email.objects.all()[1].in_reply_to) + self.assertEqual('test', IssueThread.objects.all()[0].name) + self.assertEqual('pending_new', IssueThread.objects.all()[0].state) + self.assertEqual(None, IssueThread.objects.all()[0].assigned_to) + states = StateChange.objects.filter(issue_thread=IssueThread.objects.all()[0]) + self.assertEqual(1, len(states)) + self.assertEqual('pending_new', states[0].state)