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 = """