From b06b794de750d972a9dff9165e203815b79c5b62 Mon Sep 17 00:00:00 2001 From: Alexandre Pretto Date: Thu, 8 Jan 2015 18:48:48 -0200 Subject: #157: mail api to return htmlBody and textPlainBody so the UI doesnt have to parse the multipart mails anymore --- service/pixelated/adapter/model/mail.py | 54 ++++++++++++++++++---------- service/pixelated/adapter/search/__init__.py | 2 +- service/test/unit/adapter/mail_test.py | 35 +++++++++--------- 3 files changed, 53 insertions(+), 38 deletions(-) (limited to 'service') diff --git a/service/pixelated/adapter/model/mail.py b/service/pixelated/adapter/model/mail.py index 4b128d8c..98533a74 100644 --- a/service/pixelated/adapter/model/mail.py +++ b/service/pixelated/adapter/model/mail.py @@ -25,7 +25,6 @@ import pixelated.support.date from email.MIMEMultipart import MIMEMultipart from pycryptopp.hash import sha256 import re -import base64 from pixelated.support.functional import compact @@ -65,7 +64,13 @@ class Mail(object): mime = MIMEMultipart() for key, value in self.headers.items(): mime[str(key)] = str(value) - mime.attach(MIMEText(self.body, 'plain', self._charset())) + + try: + body_to_use = self.body + except AttributeError: + body_to_use = self.text_plain_body + + mime.attach(MIMEText(body_to_use, 'plain', self._charset())) self._mime = mime return mime @@ -193,6 +198,7 @@ class InputMail(Mail): class PixelatedMail(Mail): + @staticmethod def from_soledad(fdoc, hdoc, bdoc, parts=None, soledad_querier=None): mail = PixelatedMail() @@ -205,24 +211,33 @@ class PixelatedMail(Mail): mail._mime = None return mail + def _decode_part(self, part): + encoding = part['headers'].get('Content-Transfer-Encoding', '') + + decoding_map = { + 'quoted-printable': lambda content: unicode(content.decode('quopri')), + 'base64': lambda content: content.decode('base64').decode('utf-8') + } + if encoding: + return decoding_map[encoding](part['content']) + return part['content'] + @property - def body(self): - if self.parts and len(self.parts['alternatives']) > 1: - body = '' - for alternative in self.parts['alternatives']: - body += '--' + self.boundary + '\n' - for header, value in alternative['headers'].items(): - body += '%s: %s\n' % (header, value) - body += '\n' - body += alternative['content'] - body += '\n' - body += '--' + self.boundary + '--' - return body + def alternatives(self): + return self.parts.get('alternatives') + + @property + def text_plain_body(self): + if self.parts and len(self.alternatives) == 1: + return self._decode_part(self.alternatives[0]) else: - if self.parts and self.parts['alternatives'][0]['headers'].get('Content-Transfer-Encoding', '') == 'base64': - return unicode(base64.b64decode(self.parts['alternatives'][0]['content']), 'utf-8') - else: - return self.bdoc.content['raw'] + return self.bdoc.content['raw'] # plain + + @property + def html_body(self): + if self.parts and len(self.alternatives) > 1: + html_part = [e for e in self.alternatives if re.match('text/html', e['headers']['Content-Type'])][0] + return self._decode_part(html_part) @property def headers(self): @@ -352,7 +367,8 @@ class PixelatedMail(Mail): 'tags': list(self.tags), 'status': list(self.status), 'security_casing': self.security_casing, - 'body': self.body, + 'textPlainBody': self.text_plain_body, + 'htmlBody': self.html_body, 'mailbox': self.mailbox_name.lower(), 'attachments': self.parts['attachments'] if self.parts else []} dict_mail['replying'] = {'single': None, 'all': {'to-field': [], 'cc-field': []}} diff --git a/service/pixelated/adapter/search/__init__.py b/service/pixelated/adapter/search/__init__.py index bbefc487..a5eb4ef2 100644 --- a/service/pixelated/adapter/search/__init__.py +++ b/service/pixelated/adapter/search/__init__.py @@ -128,7 +128,7 @@ class SearchEngine(object): 'cc': u','.join(header.get('cc', [''])), 'bcc': u','.join(header.get('bcc', [''])), 'tag': u','.join(unique(tags)), - 'body': unicode(mdict['body']), + 'body': unicode(mdict['textPlainBody']), 'ident': unicode(mdict['ident']), 'flags': unicode(','.join(unique(mail.flags))), 'raw': unicode(mail.raw) diff --git a/service/test/unit/adapter/mail_test.py b/service/test/unit/adapter/mail_test.py index e0b7a498..c35bdb62 100644 --- a/service/test/unit/adapter/mail_test.py +++ b/service/test/unit/adapter/mail_test.py @@ -95,7 +95,10 @@ class TestPixelatedMail(unittest.TestCase): _dict = mail.as_dict() - self.assertEquals(_dict, {'body': 'body', + self.maxDiff = None + + self.assertEquals(_dict, {'htmlBody': None, + 'textPlainBody': 'body', 'header': { 'date': dateparser.parse(hdoc.content['date']).isoformat(), 'from': 'someone@pixelated.org', @@ -143,21 +146,20 @@ class TestPixelatedMail(unittest.TestCase): parts['alternatives'].append({'content': 'blablabla', 'headers': {'Content-Type': 'text/plain'}}) parts['alternatives'].append({'content': '

blablabla

', 'headers': {'Content-Type': 'text/html'}}) - mail = PixelatedMail.from_soledad(None, None, None, parts=parts, soledad_querier=None) + mail = PixelatedMail.from_soledad(None, None, self._create_bdoc(raw='blablabla'), parts=parts, soledad_querier=None) - self.assertRegexpMatches(mail.body, '^--' + mail.boundary + '\n.*') - self.assertRegexpMatches(mail.body, '\nContent-Type: text/html\n\n

blablabla

\n') - self.assertRegexpMatches(mail.body, '\nContent-Type: text/plain\n\nblablabla\n') - self.assertRegexpMatches(mail.body, '.*--' + mail.boundary + '--$') + self.assertRegexpMatches(mail.html_body, '^

blablabla

$') + self.assertRegexpMatches(mail.text_plain_body, '^blablabla$') def test_percent_character_is_allowed_on_body(self): parts = {'alternatives': [], 'attachments': []} parts['alternatives'].append({'content': '100% happy with percentage symbol', 'headers': {'Content-Type': 'text/plain'}}) parts['alternatives'].append({'content': '

100% happy with percentage symbol

', 'headers': {'Content-Type': 'text/html'}}) - mail = PixelatedMail.from_soledad(None, None, None, parts=parts, soledad_querier=None) + mail = PixelatedMail.from_soledad(None, None, self._create_bdoc(raw="100% happy with percentage symbol"), parts=parts, soledad_querier=None) - self.assertRegexpMatches(mail.body, '([\s\S]*100%){2}') + self.assertRegexpMatches(mail.text_plain_body, '([\s\S]*100%)') + self.assertRegexpMatches(mail.html_body, '([\s\S]*100%)') def test_clean_line_breaks_on_address_headers(self): many_recipients = 'One ,\nTwo , Normal ,\nalone@mail.com' @@ -175,25 +177,22 @@ class TestPixelatedMail(unittest.TestCase): self.assertNotIn(',', address) self.assertEquals(4, len(mail.headers[header_label])) - def test_content_type_is_read_from_headers_for_plain_mail_when_converted_to_raw(self): - fdoc, hdoc, bdoc = test_helper.leap_mail(flags=['\\Recent'], body=u'some umlaut \xc3', extra_headers={'Content-Type': 'text/plain; charset=ISO-8859-1'}) - hdoc.content['headers']['Subject'] = 'The subject' - hdoc.content['headers']['From'] = 'me@pixelated.org' - mail = PixelatedMail.from_soledad(fdoc, hdoc, bdoc, soledad_querier=self.querier) - - mail.raw - def test_that_body_understands_base64(self): body = u'bl\xe1' - encoded_body = unicode(base64.b64encode(body.encode('utf-8'))) + encoded_body = unicode(body.encode('utf-8').encode('base64')) fdoc, hdoc, bdoc = test_helper.leap_mail() parts = {'alternatives': []} parts['alternatives'].append({'content': encoded_body, 'headers': {'Content-Transfer-Encoding': 'base64'}}) mail = PixelatedMail.from_soledad(fdoc, hdoc, bdoc, soledad_querier=self.querier, parts=parts) - self.assertEquals(body, mail.body) + self.assertEquals(body, mail.text_plain_body) + def _create_bdoc(self, raw): + class FakeBDoc: + def __init__(self, raw): + self.content = {'raw': raw} + return FakeBDoc(raw) class InputMailTest(unittest.TestCase): mail_dict = lambda x: { -- cgit v1.2.3