diff --git a/addons/mail/models/mail_mail.py b/addons/mail/models/mail_mail.py index 3f206424e07..e53c24fb176 100644 --- a/addons/mail/models/mail_mail.py +++ b/addons/mail/models/mail_mail.py @@ -169,7 +169,7 @@ class MailMail(models.Model): - else fallback on mail.email_to splitting """ self.ensure_one() if partner: - email_to = [formataddr((partner.name, partner.email))] + email_to = [formataddr((partner.name or 'False', partner.email or 'False'))] else: email_to = tools.email_split_and_format(self.email_to) return email_to diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py index e9bf605c382..f44ee7be09a 100644 --- a/addons/mail/models/mail_template.py +++ b/addons/mail/models/mail_template.py @@ -306,8 +306,8 @@ class MailTemplate(models.Model): # form a tree root = lxml.html.fromstring(html) if not len(root) and root.text is None and root.tail is None: - html = '
%s
' % html - root = lxml.html.fromstring(html) + html = u'
%s
' % html + root = lxml.html.fromstring(html, encoding='unicode') base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') base = urls.url_parse(base_url) @@ -324,12 +324,12 @@ class MailTemplate(models.Model): for node in root.iter(): if node.tag == 'a' and node.get('href'): node.set('href', _process_link(node.get('href'))) - elif node.tag == 'img' and not node.get('src', 'data').startswith('data'): + elif node.tag == 'img' and not node.get('src', 'data').startswith(u'data'): node.set('src', _process_link(node.get('src'))) - html = lxml.html.tostring(root, pretty_print=False, method='html') + html = lxml.html.tostring(root, pretty_print=False, method='html', encoding='unicode') # this is ugly, but lxml/etree tostring want to put everything in a 'div' that breaks the editor -> remove that - if html.startswith('
') and html.endswith('
'): + if html.startswith(u'
') and html.endswith(u'
'): html = html[5:-6] return html diff --git a/addons/mail/tests/test_mail_features.py b/addons/mail/tests/test_mail_features.py index a6fce59c10a..8e2264ad6d3 100644 --- a/addons/mail/tests/test_mail_features.py +++ b/addons/mail/tests/test_mail_features.py @@ -131,7 +131,7 @@ class TestMessagePost(TestMail): self.assertEqual(set(msg.attachment_ids.mapped('res_id')), set([self.test_pigs.id]), 'message_post: all atttachments should be linked to the pigs group') self.assertEqual(set([base64.b64decode(x) for x in msg.attachment_ids.mapped('datas')]), - set(['migration test', _attachments[0][1], _attachments[1][1]])) + set([b'migration test', _attachments[0][1], _attachments[1][1]])) self.assertTrue(set([_attach_1.id, _attach_2.id]).issubset(msg.attachment_ids.ids), 'message_post: mail.message attachments duplicated') # notifications diff --git a/odoo/addons/base/ir/ir_mail_server.py b/odoo/addons/base/ir/ir_mail_server.py index 91cf0e61e73..eab160b4a84 100644 --- a/odoo/addons/base/ir/ir_mail_server.py +++ b/odoo/addons/base/ir/ir_mail_server.py @@ -39,20 +39,8 @@ class WriteToLogger(object): _logger.debug(s) smtplib.stderr = WriteToLogger() -def try_coerce_ascii(string_utf8): - """Attempts to decode the given utf8-encoded string - as ASCII after coercing it to UTF-8, then return - the confirmed 7-bit ASCII string. - - If the process fails (because the string - contains non-ASCII characters) returns ``None``. - """ - try: - string_utf8.decode('ascii') - except UnicodeDecodeError: - return - return string_utf8 - +def is_ascii(s): + return all(ord(cp) < 128 for cp in s) def encode_header(header_text): """Returns an appropriate representation of the given header value, @@ -70,16 +58,10 @@ def encode_header(header_text): """ if not header_text: return "" - # convert anything to utf-8, suitable for testing ASCIIness, as 7-bit chars are - # encoded as ASCII in utf-8 - header_text_utf8 = ustr(header_text).encode('utf-8') - header_text_ascii = try_coerce_ascii(header_text_utf8) - # if this header contains non-ASCII characters, - # we'll need to wrap it up in a message.header.Header - # that will take care of RFC2047-encoding it as - # 7-bit string. - return header_text_ascii or Header(header_text_utf8, 'utf-8') - + header_text = ustr(header_text) # FIXME: require unicode higher up? + if is_ascii(header_text): + return pycompat.to_native(header_text) + Header(header_text, 'utf-8') def encode_header_param(param_text): """Returns an appropriate RFC2047 encoded representation of the given @@ -97,10 +79,10 @@ def encode_header_param(param_text): # For details see the encode_header() method that uses the same logic if not param_text: return "" - param_text_utf8 = ustr(param_text).encode('utf-8') - param_text_ascii = try_coerce_ascii(param_text_utf8) - return param_text_ascii or Charset('utf8').header_encode(param_text_utf8) - + param_text = ustr(param_text) # FIXME: require unicode higher up? + if is_ascii(param_text): + return pycompat.to_native(param_text) # TODO: is that actually necessary? + return Charset("utf-8").header_encode(param_text) address_pattern = re.compile(r'([^ ,<@]+@[^> ,]+)') @@ -111,8 +93,8 @@ def extract_rfc2822_addresses(text): """ if not text: return [] - candidates = address_pattern.findall(ustr(text).encode('utf-8')) - return [c for c in candidates if try_coerce_ascii(c)] + candidates = address_pattern.findall(ustr(text)) + return [c for c in candidates if is_ascii(c)] def encode_rfc2822_address_header(header_text): @@ -124,11 +106,20 @@ def encode_rfc2822_address_header(header_text): """ def encode_addr(addr): name, email = addr - if not try_coerce_ascii(name): - name = str(Header(name, 'utf-8')) + # If s is a , then charset is a hint specifying the + # character set of the characters in the string. The Unicode string + # will be encoded using the following charsets in order: us-ascii, + # the charset hint, utf-8. The first character set to not provoke a + # UnicodeError is used. + # -> always pass a text string to Header + + # also Header.__str__ in Python 3 "Returns an approximation of the + # Header as a string, using an unlimited line length.", the old one + # was "A synonym for Header.encode()." so call encode() directly? + name = Header(pycompat.to_text(name)).encode() return formataddr((name, email)) - addresses = getaddresses([ustr(header_text).encode('utf-8')]) + addresses = getaddresses([pycompat.to_native(ustr(header_text))]) return COMMASPACE.join(encode_addr(a) for a in addresses) @@ -252,8 +243,8 @@ class IrMailServer(models.Model): # The user/password must be converted to bytestrings in order to be usable for # certain hashing schemes, like HMAC. # See also bug #597143 and python issue #5285 - smtp_user = ustr(smtp_user).encode('utf-8') - smtp_password = ustr(smtp_password).encode('utf-8') + smtp_user = pycompat.to_native(ustr(smtp_user)) + smtp_password = pycompat.to_native(ustr(smtp_password)) connection.login(smtp_user, smtp_password) return connection @@ -301,8 +292,8 @@ class IrMailServer(models.Model): email_bcc = email_bcc or [] body = body or u'' - email_body_utf8 = ustr(body).encode('utf-8') - email_text_part = MIMEText(email_body_utf8, _subtype=subtype, _charset='utf-8') + email_body = ustr(body) + email_text_part = MIMEText(email_body, _subtype=subtype, _charset='utf-8') msg = MIMEMultipart() if not message_id: @@ -328,20 +319,20 @@ class IrMailServer(models.Model): msg['Date'] = formatdate() # Custom headers may override normal headers or provide additional ones for key, value in pycompat.items(headers): - msg[ustr(key).encode('utf-8')] = encode_header(value) + msg[pycompat.to_native(ustr(key))] = encode_header(value) if subtype == 'html' and not body_alternative: # Always provide alternative text body ourselves if possible. - text_utf8 = html2text.html2text(email_body_utf8.decode('utf-8')).encode('utf-8') + text = html2text.html2text(email_body_utf8) alternative_part = MIMEMultipart(_subtype="alternative") - alternative_part.attach(MIMEText(text_utf8, _charset='utf-8', _subtype='plain')) + alternative_part.attach(MIMEText(text, _charset='utf-8', _subtype='plain')) alternative_part.attach(email_text_part) msg.attach(alternative_part) elif body_alternative: # Include both alternatives, as specified, within a multipart/alternative part alternative_part = MIMEMultipart(_subtype="alternative") - body_alternative_utf8 = ustr(body_alternative).encode('utf-8') - alternative_body_part = MIMEText(body_alternative_utf8, _subtype=subtype_alternative, _charset='utf-8') + body_alternative_ = ustr(body_alternative) + alternative_body_part = MIMEText(body_alternative_, _subtype=subtype_alternative, _charset='utf-8') alternative_part.attach(alternative_body_part) alternative_part.attach(email_text_part) msg.attach(alternative_part) diff --git a/odoo/addons/base/res/res_partner.py b/odoo/addons/base/res/res_partner.py index 972b74d7f3e..32772fa19a6 100644 --- a/odoo/addons/base/res/res_partner.py +++ b/odoo/addons/base/res/res_partner.py @@ -337,7 +337,7 @@ class Partner(models.Model): @api.depends('name', 'email') def _compute_email_formatted(self): for partner in self: - partner.email_formatted = formataddr((partner.name, partner.email)) + partner.email_formatted = formataddr((partner.name or u"False", partner.email or u"False")) @api.depends('is_company') def _compute_company_type(self): diff --git a/odoo/addons/base/tests/test_mail.py b/odoo/addons/base/tests/test_mail.py index 1a30ef4690b..84cf892ea30 100644 --- a/odoo/addons/base/tests/test_mail.py +++ b/odoo/addons/base/tests/test_mail.py @@ -171,14 +171,14 @@ class TestSanitizer(unittest.TestCase): for ext in test_mail_examples.QUOTE_BLOCKQUOTE_IN: self.assertIn(ext, html) for ext in test_mail_examples.QUOTE_BLOCKQUOTE_OUT: - self.assertIn('%s' % misc.html_escape(ext.decode('utf-8')), html) + self.assertIn(u'%s' % misc.html_escape(ext), html) def test_quote_thunderbird(self): html = html_sanitize(test_mail_examples.QUOTE_THUNDERBIRD_1) for ext in test_mail_examples.QUOTE_THUNDERBIRD_1_IN: self.assertIn(ext, html) for ext in test_mail_examples.QUOTE_THUNDERBIRD_1_OUT: - self.assertIn('%s' % misc.html_escape(ext.decode('utf-8')), html) + self.assertIn(u'%s' % misc.html_escape(ext), html) def test_quote_hotmail_html(self): html = html_sanitize(test_mail_examples.QUOTE_HOTMAIL_HTML) @@ -225,7 +225,7 @@ class TestSanitizer(unittest.TestCase): for text in in_lst: self.assertIn(text, new_html) for text in out_lst: - self.assertIn('%s' % misc.html_escape(text), new_html) + self.assertIn(u'%s' % misc.html_escape(text), new_html) def test_quote_signature(self): test_data = [ @@ -244,27 +244,27 @@ class TestSanitizer(unittest.TestCase): for ext in test_mail_examples.GMAIL_1_IN: self.assertIn(ext, html) for ext in test_mail_examples.GMAIL_1_OUT: - self.assertIn('%s' % misc.html_escape(ext), html) + self.assertIn(u'%s' % misc.html_escape(ext), html) def test_quote_text(self): html = html_sanitize(test_mail_examples.TEXT_1) for ext in test_mail_examples.TEXT_1_IN: self.assertIn(ext, html) for ext in test_mail_examples.TEXT_1_OUT: - self.assertIn('%s' % misc.html_escape(ext), html) + self.assertIn(u'%s' % misc.html_escape(ext), html) html = html_sanitize(test_mail_examples.TEXT_2) for ext in test_mail_examples.TEXT_2_IN: self.assertIn(ext, html) for ext in test_mail_examples.TEXT_2_OUT: - self.assertIn('%s' % misc.html_escape(ext), html) + self.assertIn(u'%s' % misc.html_escape(ext), html) def test_quote_bugs(self): html = html_sanitize(test_mail_examples.BUG1) for ext in test_mail_examples.BUG_1_IN: self.assertIn(ext, html) for ext in test_mail_examples.BUG_1_OUT: - self.assertIn('%s' % misc.html_escape(ext.decode('utf-8')), html) + self.assertIn(u'%s' % misc.html_escape(ext), html) def test_misc(self): # False / void should not crash diff --git a/odoo/addons/base/tests/test_mail_examples.py b/odoo/addons/base/tests/test_mail_examples.py index dfecfe7c0d4..4f3b5b48300 100644 --- a/odoo/addons/base/tests/test_mail_examples.py +++ b/odoo/addons/base/tests/test_mail_examples.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -MISC_HTML_SOURCE = """ +MISC_HTML_SOURCE = u""" test1
test2
@@ -19,7 +19,7 @@ test12

test link """ -EDI_LIKE_HTML_SOURCE = """
+EDI_LIKE_HTML_SOURCE = u"""

Hello ${object.partner_id.name},

A new invoice is available for you:

@@ -63,7 +63,7 @@ EDI_LIKE_HTML_SOURCE = """

+QUOTE_BLOCKQUOTE = u""" @@ -103,14 +103,14 @@ web: https://opener.am """ -QUOTE_BLOCKQUOTE_IN = ["""
"""] -QUOTE_BLOCKQUOTE_OUT = ["""-- +QUOTE_BLOCKQUOTE_IN = [u"""
"""] +QUOTE_BLOCKQUOTE_OUT = [u"""-- Opener B.V. - Business solutions driven by open source collaboration Stefan Rijnhart - Consultant/developer"""] -QUOTE_THUNDERBIRD_HTML = """ +QUOTE_THUNDERBIRD_HTML = u""" @@ -137,13 +137,13 @@ Raoul Poilvache """ -QUOTE_THUNDERBIRD_HTML_IN = ["""
"""] -QUOTE_THUNDERBIRD_HTML_OUT = ["""
-- 
+QUOTE_THUNDERBIRD_HTML_IN = [u"""
"""] +QUOTE_THUNDERBIRD_HTML_OUT = [u"""
-- 
 Raoul Poilvache
 
"""] -QUOTE_HOTMAIL_HTML = """ +QUOTE_HOTMAIL_HTML = u"""