diff --git a/modoboa/lib/email_utils.py b/modoboa/lib/email_utils.py index 123478f1e..4fb6c7a38 100644 --- a/modoboa/lib/email_utils.py +++ b/modoboa/lib/email_utils.py @@ -20,9 +20,10 @@ from modoboa.lib import u2u_decode class EmailAddress(object): def __init__(self, address): - self.fulladdress = u2u_decode.u2u_decode(address).strip("\r\t\n") - (self.name, self.address) = parseaddr(self.fulladdress) - if self.name == "": + self.name, self.address = u2u_decode.decode_address(address) + if self.name: + self.fulladdress = "{} <{}>".format(self.name, self.address) + else: self.fulladdress = self.address def __str__(self): diff --git a/modoboa/lib/tests.py b/modoboa/lib/tests.py index 3fbab598a..aac065fcb 100644 --- a/modoboa/lib/tests.py +++ b/modoboa/lib/tests.py @@ -14,6 +14,7 @@ from rest_framework.test import APITestCase from modoboa.core import models as core_models from . import sysutils +from . import u2u_decode try: s = socket.create_connection(('127.0.0.1', 25)) @@ -103,3 +104,46 @@ class ModoAPITestCase(ParametersMixin, APITestCase): """Setup.""" super(ModoAPITestCase, self).setUp() self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key) + + +class U2UTestCase(TestCase): + """Test RFC1342 decoding utilities.""" + + def test_header_decoding(self): + """Simple decoding.""" + samples = [ + ("=?ISO-8859-15?Q?=20Profitez de tous les services en ligne sur " + "impots.gouv.fr?=", + "Profitez de tous les services en ligne sur impots.gouv.fr"), + ("=?ISO-8859-1?Q?Accus=E9?= de =?ISO-8859-1?Q?r=E9ception?= de " + "votre annonce", + "Accusé de réception de votre annonce"), + ("Sm=?ISO-8859-1?B?9g==?=rg=?ISO-8859-1?B?5Q==?=sbord", + "Sm\xf6rg\xe5sbord") + ] + for sample in samples: + self.assertEqual(u2u_decode.u2u_decode(sample[0]), sample[1]) + + def test_address_header_decoding(self): + """Check address decoding.""" + mailsploit_sample = ( + "=?utf-8?b?cG90dXNAd2hpdGVob3VzZS5nb3Y=?==?utf-8?Q?=00?=" + "=?utf-8?b?cG90dXNAd2hpdGVob3VzZS5nb3Y=?=@mailsploit.com") + expected_result = ( + "=?utf-8?b?cG90dXNAd2hpdGVob3VzZS5nb3Y=?==?utf-8?Q??=" + "=?utf-8?b?cG90dXNAd2hpdGVob3VzZS5nb3Y=?=@mailsploit.com") + self.assertEqual( + u2u_decode.decode_address(mailsploit_sample), + ("", expected_result) + ) + mailsploit_sample = ( + '"=?utf-8?b?cG90dXNAd2hpdGVob3VzZS5nb3Y=?==?utf-8?Q?=0A=00?="\n' + '<=?utf-8?b?cG90dXNAd2hpdGVob3VzZS5nb3Y=?==?utf-8?Q?=0A=00?=' + '@mailsploit.com>') + expected_result = ( + 'potus@whitehouse.gov', + '=?utf-8?b?cG90dXNAd2hpdGVob3VzZS5nb3Y=?==?utf-8?Q??=' + '@mailsploit.com') + self.assertEqual( + u2u_decode.decode_address(mailsploit_sample), + expected_result) diff --git a/modoboa/lib/u2u_decode.py b/modoboa/lib/u2u_decode.py index 39c498977..e8aa6ee03 100644 --- a/modoboa/lib/u2u_decode.py +++ b/modoboa/lib/u2u_decode.py @@ -5,14 +5,17 @@ from __future__ import unicode_literals from email.header import decode_header, make_header +from email.utils import parseaddr import re from django.utils.encoding import smart_text # check spaces between encoded_words (and strip them) -sre = re.compile(r'\?=[ \t]+=\?') +sre = re.compile(r"\?=[ \t]+=\?") # re pat for MIME encoded_word (without trailing spaces) -mre = re.compile(r'=\?[^?]*?\?[bq]\?[^?\t]*?\?=', re.I) +mre = re.compile(r"=\?[^?]*?\?[bq]\?[^?\t]*?\?=", re.I) +# re do detect encoded ASCII characters +ascii_re = re.compile(r"=[\dA-F]{2,3}", re.I) def clean_spaces(m): @@ -24,6 +27,14 @@ def clean_spaces(m): return m.group(0).replace(" ", "=20") +def clean_non_printable_char(m): + """Strip non printable characters.""" + code = int(m.group(0)[1:], 16) + if code < 20: + return "" + return m.group(0) + + def decode_mime(m): """Substitute matching encoded_word with unicode equiv.""" h = decode_header(clean_spaces(m)) @@ -34,20 +45,34 @@ def decode_mime(m): return u +def clean_header(header): + """Clean header function.""" + header = "".join(header.splitlines()) + header = sre.sub("?==?", header) + return ascii_re.sub(clean_non_printable_char, header) + + def u2u_decode(s): """utility function for (final) decoding of mime header note: resulting string is in one line (no \n within) note2: spaces between enc_words are stripped (see RFC2047) - - >>> u2u_decode('=?ISO-8859-15?Q?=20Profitez de tous les services en ligne sur impots.gouv.fr?=') - u' Profitez de tous les services en ligne sur impots.gouv.fr' - >>> u2u_decode('=?ISO-8859-1?Q?Accus=E9?= de =?ISO-8859-1?Q?r=E9ception?= de votre annonce') - u'Accus\xe9 de r\xe9ception de votre annonce' - >>> u2u_decode('Sm=?ISO-8859-1?B?9g==?=rg=?ISO-8859-1?B?5Q==?=sbord') - u'Sm\xf6rg\xe5sbord' """ - s = ''.join(s.splitlines()) - s = sre.sub('?==?', s) - u = mre.sub(decode_mime, s) - return u + return mre.sub(decode_mime, clean_header(s)).strip(" \r\t\n") + + +def decode_address(value): + """Special function for address decoding. + + We need a dedicated processing because RFC1342 explicitely says + address MUST NOT contain encoded-word: + + These are the ONLY locations where an encoded-word may appear. In + particular, an encoded-word MUST NOT appear in any portion of an + "address". In addition, an encoded-word MUST NOT be used in a + Received header field. + """ + phrase, address = parseaddr(clean_header(value)) + if phrase: + phrase = mre.sub(decode_mime, phrase) + return phrase, address