summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulien Castiaux <juc@odoo.com>2019-08-20 13:09:55 +0000
committerJulien Castiaux <juc@odoo.com>2019-09-17 11:32:54 +0000
commitdbb6cfdbf803d2ab86ba11450b5928abf5ab658b (patch)
tree830220c1e66d96736e097219949b724b9db0744e
parent1b057dd76b19db00eb5b63dc02f87cdc13cc07cf (diff)
[REF] mail: remove deprecated email formataddr
[PEP 594] is going to depreciate the legacy `email.message.Message` API and its related modules. Among them the `email.utils.formataddr` function, this function takes a `(name, email)` pair and returns a string value suitable for From, To and Cc headers. The stdlib function is capable of handling several character encoding and two binary-to-ascii encoding. Odoo only uses `utf-8` which is compatible with the base64 b2a encoding. The re-implementation of that function has been simplified to only support base64-ed utf-8 and ascii. [PEP 594]: https://www.python.org/dev/peps/pep-0594/\#email-lagacy-api
-rw-r--r--addons/mail/models/mail_cc_mixin.py3
-rw-r--r--addons/mail/models/mail_channel.py3
-rw-r--r--addons/mail/models/mail_mail.py3
-rw-r--r--addons/mail/models/mail_message.py5
-rw-r--r--addons/mail/models/mail_thread.py3
-rw-r--r--addons/mail/wizard/mail_compose_message.py4
-rw-r--r--addons/survey/tests/test_survey_invite.py4
-rw-r--r--addons/survey/wizard/survey_invite.py4
-rw-r--r--addons/test_mail/tests/common.py4
-rw-r--r--addons/test_mail/tests/test_mail_channel.py4
-rw-r--r--addons/test_mail/tests/test_mail_gateway.py29
-rw-r--r--addons/test_mail/tests/test_mail_message.py3
-rw-r--r--addons/test_mail/tests/test_message_post.py3
-rw-r--r--addons/test_mail/tests/test_message_track.py4
-rw-r--r--addons/test_mail/tests/test_performance.py4
-rw-r--r--addons/test_mass_mailing/tests/test_mail_channel.py2
-rw-r--r--addons/website_slides/wizard/slide_channel_invite.py3
-rw-r--r--odoo/addons/base/models/res_partner.py4
-rw-r--r--odoo/tools/mail.py40
19 files changed, 75 insertions, 54 deletions
diff --git a/addons/mail/models/mail_cc_mixin.py b/addons/mail/models/mail_cc_mixin.py
index cc1554fcff5..427eb25a2bb 100644
--- a/addons/mail/models/mail_cc_mixin.py
+++ b/addons/mail/models/mail_cc_mixin.py
@@ -2,7 +2,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models, tools
-from email.utils import formataddr
class MailCCMixin(models.AbstractModel):
@@ -16,7 +15,7 @@ class MailCCMixin(models.AbstractModel):
'''return a dict of sanitize_email:raw_email from a string of cc'''
if not cc_string:
return {}
- return {tools.email_normalize(email): formataddr((name, tools.email_normalize(email)))
+ return {tools.email_normalize(email): tools.formataddr((name, tools.email_normalize(email)))
for (name, email) in tools.email_split_tuples(cc_string)}
@api.model
diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py
index 91d40bed9de..fb27b41830b 100644
--- a/addons/mail/models/mail_channel.py
+++ b/addons/mail/models/mail_channel.py
@@ -5,13 +5,12 @@ import base64
import logging
import re
-from email.utils import formataddr
from uuid import uuid4
from odoo import _, api, fields, models, modules, tools
from odoo.exceptions import UserError, ValidationError
from odoo.osv import expression
-from odoo.tools import ormcache
+from odoo.tools import ormcache, formataddr
MODERATION_FIELDS = ['moderation', 'moderator_ids', 'moderation_ids', 'moderation_notify', 'moderation_notify_msg', 'moderation_guidelines', 'moderation_guidelines_msg']
_logger = logging.getLogger(__name__)
diff --git a/addons/mail/models/mail_mail.py b/addons/mail/models/mail_mail.py
index 59a8e604fb1..c1e3a19fe54 100644
--- a/addons/mail/models/mail_mail.py
+++ b/addons/mail/models/mail_mail.py
@@ -10,7 +10,6 @@ import threading
import re
from collections import defaultdict
-from email.utils import formataddr
from odoo import _, api, fields, models
from odoo import tools
@@ -195,7 +194,7 @@ class MailMail(models.Model):
body = self._send_prepare_body()
body_alternative = tools.html2plaintext(body)
if partner:
- email_to = [formataddr((partner.name or 'False', partner.email or 'False'))]
+ email_to = [tools.formataddr((partner.name or 'False', partner.email or 'False'))]
else:
email_to = tools.email_split_and_format(self.email_to)
res = {
diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py
index e49a6153976..2e8b1e0441d 100644
--- a/addons/mail/models/mail_message.py
+++ b/addons/mail/models/mail_message.py
@@ -7,7 +7,6 @@ import re
from binascii import Error as binascii_error
from collections import defaultdict
from operator import itemgetter
-from email.utils import formataddr
from openerp.http import request
from odoo import _, api, fields, models, modules, tools
@@ -32,7 +31,7 @@ class Message(models.Model):
@api.model
def _get_default_from(self):
if self.env.user.email:
- return formataddr((self.env.user.name, self.env.user.email))
+ return tools.formataddr((self.env.user.name, self.env.user.email))
raise UserError(_("Unable to post message, please configure the sender's email address."))
@api.model
@@ -1179,7 +1178,7 @@ class Message(models.Model):
if not msg.email_from:
continue
if self.env.user.partner_id.email:
- email_from = formataddr((self.env.user.partner_id.name, self.env.user.partner_id.email))
+ email_from = tools.formataddr((self.env.user.partner_id.name, self.env.user.partner_id.email))
else:
email_from = self.env.company.catchall
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py
index 15d5f882f0a..af4001fb4e3 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -22,7 +22,6 @@ except ImportError:
from collections import namedtuple
from email.message import EmailMessage
-from email.utils import formataddr
from lxml import etree
from werkzeug import url_encode
from werkzeug import urls
@@ -2633,7 +2632,7 @@ class MailThread(models.AbstractModel):
company_name = company.name if company else self.env.company.name
for res_id in result_email.keys():
name = '%s%s%s' % (company_name, ' ' if doc_names.get(res_id) else '', doc_names.get(res_id, ''))
- result[res_id] = formataddr((name, result_email[res_id]))
+ result[res_id] = tools.formataddr((name, result_email[res_id]))
left_ids = set(_res_ids) - set(result_email)
if left_ids:
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index aeeba8c2339..b8b516f878c 100644
--- a/addons/mail/wizard/mail_compose_message.py
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from email.utils import formataddr
-
import base64
import re
@@ -68,7 +66,7 @@ class MailComposer(models.TransientModel):
elif 'email_from' not in result:
author = self.env['res.partner'].browse(result['author_id'])
if author.email:
- result['email_from'] = formataddr((author.name, author.email))
+ result['email_from'] = tools.formataddr((author.name, author.email))
# v6.1 compatibility mode
result['composition_mode'] = result.get('composition_mode', self._context.get('mail.compose.message.mode', 'comment'))
diff --git a/addons/survey/tests/test_survey_invite.py b/addons/survey/tests/test_survey_invite.py
index bf99cc93089..04a76d099dd 100644
--- a/addons/survey/tests/test_survey_invite.py
+++ b/addons/survey/tests/test_survey_invite.py
@@ -143,7 +143,7 @@ class TestSurveyInvite(common.SurveyCase):
self.assertEqual(len(answers), 3)
self.assertEqual(
set(answers.mapped('email')),
- set(['test1@example.com', 'Raoulette Vignolette <test2@example.com>', self.customer.email]))
+ set(['test1@example.com', '"Raoulette Vignolette" <test2@example.com>', self.customer.email]))
self.assertEqual(answers.mapped('partner_id'), self.customer)
@users('survey_manager')
@@ -164,7 +164,7 @@ class TestSurveyInvite(common.SurveyCase):
self.assertEqual(len(answers), 3)
self.assertEqual(
set(answers.mapped('email')),
- set(['test1@example.com', 'Raoulette Vignolette <test2@example.com>', self.customer.email]))
+ set(['test1@example.com', '"Raoulette Vignolette" <test2@example.com>', self.customer.email]))
self.assertEqual(answers.mapped('partner_id'), self.customer)
@users('survey_manager')
diff --git a/addons/survey/wizard/survey_invite.py b/addons/survey/wizard/survey_invite.py
index 06b60407119..48a6ee57c31 100644
--- a/addons/survey/wizard/survey_invite.py
+++ b/addons/survey/wizard/survey_invite.py
@@ -4,8 +4,6 @@
import logging
import re
-from email.utils import formataddr
-
from odoo import api, fields, models, tools, _
from odoo.exceptions import UserError
@@ -21,7 +19,7 @@ class SurveyInvite(models.TransientModel):
@api.model
def _get_default_from(self):
if self.env.user.email:
- return formataddr((self.env.user.name, self.env.user.email))
+ return tools.formataddr((self.env.user.name, self.env.user.email))
raise UserError(_("Unable to post message, please configure the sender's email address."))
@api.model
diff --git a/addons/test_mail/tests/common.py b/addons/test_mail/tests/common.py
index 17d8594a7d9..d73a091b7b2 100644
--- a/addons/test_mail/tests/common.py
+++ b/addons/test_mail/tests/common.py
@@ -4,12 +4,12 @@ import json
import time
from contextlib import contextmanager
-from email.utils import formataddr
from functools import partial
from odoo import api
from odoo.addons.bus.models.bus import json_dump
from odoo.tests import common, tagged, new_test_user
+from odoo.tools import formataddr
mail_new_test_user = partial(new_test_user, context={'mail_create_nolog': True, 'mail_create_nosubscribe': True, 'mail_notrack': True, 'no_reset_password': True})
@@ -327,7 +327,7 @@ class MockEmails(common.SingleTransactionCase):
self._mails[:] = []
def format(self, template, to='groups@example.com, other@gmail.com', subject='Frogs',
- extra='', email_from='Sylvie Lelitre <test.sylvie.lelitre@agrolait.com>',
+ extra='', email_from='"Sylvie Lelitre" <test.sylvie.lelitre@agrolait.com>',
cc='', msg_id='<1198923581.41972151344608186760.JavaMail@agrolait.com>'):
return template.format(to=to, subject=subject, cc=cc, extra=extra, email_from=email_from, msg_id=msg_id)
diff --git a/addons/test_mail/tests/test_mail_channel.py b/addons/test_mail/tests/test_mail_channel.py
index f1b482028c9..74ad29a6d3e 100644
--- a/addons/test_mail/tests/test_mail_channel.py
+++ b/addons/test_mail/tests/test_mail_channel.py
@@ -1,11 +1,9 @@
# -*- coding: utf-8 -*-
-from email.utils import formataddr
-
from odoo.tests import tagged
from odoo.addons.test_mail.tests import common
from odoo.addons.test_mail.tests.common import mail_new_test_user
from odoo.exceptions import AccessError, except_orm, ValidationError, UserError
-from odoo.tools import mute_logger
+from odoo.tools import mute_logger, formataddr
class TestChannelAccessRights(common.BaseFunctionalTest, common.MockEmails):
diff --git a/addons/test_mail/tests/test_mail_gateway.py b/addons/test_mail/tests/test_mail_gateway.py
index ff7b3580559..79fb1cf5c3c 100644
--- a/addons/test_mail/tests/test_mail_gateway.py
+++ b/addons/test_mail/tests/test_mail_gateway.py
@@ -5,7 +5,6 @@ import email
import email.policy
import socket
-from email.utils import formataddr
from unittest.mock import DEFAULT
from unittest.mock import patch
@@ -16,7 +15,11 @@ from odoo.addons.test_mail.models.test_mail_models import MailTestGateway
from odoo.addons.test_mail.tests.common import BaseFunctionalTest, MockEmails
from odoo.addons.test_mail.tests.common import mail_new_test_user
from odoo.tests import tagged
+<<<<<<< HEAD
from odoo.tools import email_split_and_format, mute_logger, pycompat
+=======
+from odoo.tools import mute_logger, pycompat, formataddr
+>>>>>>> [REF] mail: remove deprecated email formataddr
def from_string(text):
return email.message_from_string(pycompat.to_text(text), policy=email.policy.SMTP)
@@ -27,12 +30,12 @@ class TestEmailParsing(BaseFunctionalTest, MockEmails):
def test_message_parse_body(self):
# test pure plaintext
- plaintext = self.format(test_mail_data.MAIL_TEMPLATE_PLAINTEXT, email_from='Sylvie Lelitre <test.sylvie.lelitre@agrolait.com>')
+ plaintext = self.format(test_mail_data.MAIL_TEMPLATE_PLAINTEXT, email_from='"Sylvie Lelitre" <test.sylvie.lelitre@agrolait.com>')
res = self.env['mail.thread'].message_parse(from_string(plaintext))
self.assertIn('Please call me as soon as possible this afternoon!', res['body'])
# test multipart / text and html -> html has priority
- multipart = self.format(MAIL_TEMPLATE, email_from='Sylvie Lelitre <test.sylvie.lelitre@agrolait.com>')
+ multipart = self.format(MAIL_TEMPLATE, email_from='"Sylvie Lelitre" <test.sylvie.lelitre@agrolait.com>')
res = self.env['mail.thread'].message_parse(from_string(multipart))
self.assertIn('<p>Please call me as soon as possible this afternoon!</p>', res['body'])
@@ -59,12 +62,12 @@ class TestEmailParsing(BaseFunctionalTest, MockEmails):
def test_message_parse_eml(self):
# Test that the parsing of mail with embedded emails as eml(msg) which generates empty attachments, can be processed.
- mail = self.format(test_mail_data.MAIL_EML_ATTACHMENT, email_from='Sylvie Lelitre <test.sylvie.lelitre@agrolait.com>', to='generic@test.com')
+ mail = self.format(test_mail_data.MAIL_EML_ATTACHMENT, email_from='"Sylvie Lelitre" <test.sylvie.lelitre@agrolait.com>', to='generic@test.com')
self.env['mail.thread'].message_parse(from_string(mail))
def test_message_parse_plaintext(self):
""" Incoming email in plaintext should be stored as html """
- mail = self.format(test_mail_data.MAIL_TEMPLATE_PLAINTEXT, email_from='Sylvie Lelitre <test.sylvie.lelitre@agrolait.com>', to='generic@test.com')
+ mail = self.format(test_mail_data.MAIL_TEMPLATE_PLAINTEXT, email_from='"Sylvie Lelitre" <test.sylvie.lelitre@agrolait.com>', to='generic@test.com')
res = self.env['mail.thread'].message_parse(from_string(mail))
self.assertIn('<pre>\nPlease call me as soon as possible this afternoon!\n\n--\nSylvie\n</pre>', res['body'])
@@ -88,7 +91,7 @@ class TestMailgateway(BaseFunctionalTest, MockEmails):
def setUpClass(cls):
super(TestMailgateway, cls).setUpClass()
cls.test_model = cls.env['ir.model']._get('mail.test.gateway')
- cls.email_from = 'Sylvie Lelitre <test.sylvie.lelitre@agrolait.com>'
+ cls.email_from = '"Sylvie Lelitre" <test.sylvie.lelitre@agrolait.com>'
cls.test_record = cls.env['mail.test.gateway'].with_context(cls._test_context).create({
'name': 'Test',
@@ -190,7 +193,7 @@ class TestMailgateway(BaseFunctionalTest, MockEmails):
self.assertEqual(len(self._mails), 0, 'No notification / bounce should be sent')
# Email recognized if partner has a formatted email
- self.partner_1.write({'email': 'Valid Lelitre <%s>' % self.partner_1.email})
+ self.partner_1.write({'email': '"Valid Lelitre" <%s>' % self.partner_1.email})
record = self.format_and_process(MAIL_TEMPLATE, self.partner_1.email, 'groups@test.com', subject='Test2')
self.assertEqual(record.message_ids[0].author_id, self.partner_1,
@@ -269,7 +272,7 @@ class TestMailgateway(BaseFunctionalTest, MockEmails):
# Test bounce email
self.assertEqual(self._mails[0].get('subject'), 'Re: Should Bounce')
self.assertEqual(self._mails[0].get('email_to')[0], 'whatever-2a840@postmaster.twitter.com')
- self.assertEqual(self._mails[0].get('email_from'), formataddr(('MAILER-DAEMON', 'bounce.test@test.com')))
+ self.assertEqual(self._mails[0].get('email_from'), 'MAILER-DAEMON <bounce.test@test.com>')
@mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_message_process_alias_followers_bounce(self):
@@ -287,7 +290,7 @@ class TestMailgateway(BaseFunctionalTest, MockEmails):
'message_process: incoming email on Followers alias should send a bounce email')
self.assertEqual(self._mails[0].get('subject'), 'Re: Should Bounce')
self.assertEqual(self._mails[0].get('email_to')[0], 'whatever-2a840@postmaster.twitter.com')
- self.assertEqual(self._mails[0].get('email_from'), formataddr(('MAILER-DAEMON', 'bounce.test@test.com')))
+ self.assertEqual(self._mails[0].get('email_from'), 'MAILER-DAEMON <bounce.test@test.com>')
# Test: partner on followers alias -> bounce
self._init_mock_build_email()
@@ -297,7 +300,7 @@ class TestMailgateway(BaseFunctionalTest, MockEmails):
'message_process: incoming email on Followers alias should send a bounce email')
self.assertEqual(self._mails[0].get('subject'), 'Re: Should Bounce')
self.assertEqual(self._mails[0].get('email_to')[0], 'whatever-2a840@postmaster.twitter.com')
- self.assertEqual(self._mails[0].get('email_from'), formataddr(('MAILER-DAEMON', 'bounce.test@test.com')))
+ self.assertEqual(self._mails[0].get('email_from'), 'MAILER-DAEMON <bounce.test@test.com>')
@mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
def test_message_process_alias_partner(self):
@@ -421,7 +424,7 @@ class TestMailgateway(BaseFunctionalTest, MockEmails):
# Test bounce email
self.assertEqual(self._mails[0].get('subject'), 'Re: Should Bounce')
self.assertEqual(self._mails[0].get('email_to')[0], 'whatever-2a840@postmaster.twitter.com')
- self.assertEqual(self._mails[0].get('email_from'), formataddr(('MAILER-DAEMON', 'bounce.test@test.com')))
+ self.assertEqual(self._mails[0].get('email_from'), 'MAILER-DAEMON <bounce.test@test.com>')
@mute_logger('odoo.addons.mail.models.mail_thread')
def test_message_process_bounce_alias(self):
@@ -782,7 +785,7 @@ class TestMailThreadCC(BaseFunctionalTest, MockEmails):
self.format_and_process(MAIL_TEMPLATE, self.email_from, 'cc_record@test.com',
cc='cc2 <cc2@example.com>, cc3@example.com', target_model='mail.test.cc')
cc = email_split_and_format(record.email_cc)
- self.assertEqual(sorted(cc), ['cc1 <cc1@example.com>', 'cc2@example.com', 'cc3@example.com'], 'new cc should have been added on record (unique)')
+ self.assertEqual(sorted(cc), ['"cc1" <cc1@example.com>', 'cc2@example.com', 'cc3@example.com'], 'new cc should have been added on record (unique)')
@mute_logger('odoo.addons.mail.models.mail_thread')
def test_message_cc_update_no_old(self):
@@ -792,4 +795,4 @@ class TestMailThreadCC(BaseFunctionalTest, MockEmails):
self.format_and_process(MAIL_TEMPLATE, self.email_from, 'cc_record@test.com',
cc='cc2 <cc2@example.com>, cc3@example.com', target_model='mail.test.cc')
cc = email_split_and_format(record.email_cc)
- self.assertEqual(sorted(cc), ['cc2 <cc2@example.com>', 'cc3@example.com'], 'new cc should have been added on record (unique)')
+ self.assertEqual(sorted(cc), ['"cc2" <cc2@example.com>', 'cc3@example.com'], 'new cc should have been added on record (unique)')
diff --git a/addons/test_mail/tests/test_mail_message.py b/addons/test_mail/tests/test_mail_message.py
index 6d3290a8a95..be4b5e7887d 100644
--- a/addons/test_mail/tests/test_mail_message.py
+++ b/addons/test_mail/tests/test_mail_message.py
@@ -1,12 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
-from email.utils import formataddr
from odoo.addons.test_mail.tests import common
from odoo.addons.test_mail.tests.common import mail_new_test_user
from odoo.exceptions import AccessError, except_orm
-from odoo.tools import mute_logger
+from odoo.tools import mute_logger, formataddr
from odoo.tests import tagged
diff --git a/addons/test_mail/tests/test_message_post.py b/addons/test_mail/tests/test_message_post.py
index 2eecfb98b9e..4b4fa16493e 100644
--- a/addons/test_mail/tests/test_message_post.py
+++ b/addons/test_mail/tests/test_message_post.py
@@ -3,14 +3,13 @@
import base64
-from email.utils import formataddr
from unittest.mock import patch
from odoo.addons.test_mail.tests.common import BaseFunctionalTest, MockEmails, TestRecipients
from odoo.addons.test_mail.data.test_mail_data import MAIL_TEMPLATE_PLAINTEXT
from odoo.addons.test_mail.models.test_mail_models import MailTestSimple
from odoo.exceptions import AccessError
-from odoo.tools import mute_logger
+from odoo.tools import mute_logger, formataddr
class TestMessagePost(BaseFunctionalTest, TestRecipients, MockEmails):
diff --git a/addons/test_mail/tests/test_message_track.py b/addons/test_mail/tests/test_message_track.py
index 2771e7b8d45..c4482be9e9d 100644
--- a/addons/test_mail/tests/test_message_track.py
+++ b/addons/test_mail/tests/test_message_track.py
@@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
-
-from email.utils import formataddr
from unittest.mock import patch
-
from odoo.addons.test_mail.tests import common
+from odoo.tools import formataddr
class TestTracking(common.BaseFunctionalTest, common.MockEmails):
diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py
index 919d6ac9dd7..2d70c1b727c 100644
--- a/addons/test_mail/tests/test_performance.py
+++ b/addons/test_mail/tests/test_performance.py
@@ -2,11 +2,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
-from email.utils import formataddr
-
from odoo.tests.common import TransactionCase, users, warmup
from odoo.tests import tagged
-from odoo.tools import mute_logger
+from odoo.tools import mute_logger, formataddr
@tagged('mail_performance')
diff --git a/addons/test_mass_mailing/tests/test_mail_channel.py b/addons/test_mass_mailing/tests/test_mail_channel.py
index 9d285f42d47..c966447ca76 100644
--- a/addons/test_mass_mailing/tests/test_mail_channel.py
+++ b/addons/test_mass_mailing/tests/test_mail_channel.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from email.utils import formataddr
from odoo.addons.test_mail.tests import common
+from odoo.tools import formataddr
class TestChannelPartnersNotification(common.MockEmails):
diff --git a/addons/website_slides/wizard/slide_channel_invite.py b/addons/website_slides/wizard/slide_channel_invite.py
index be7db01a12e..43e384ade30 100644
--- a/addons/website_slides/wizard/slide_channel_invite.py
+++ b/addons/website_slides/wizard/slide_channel_invite.py
@@ -4,10 +4,9 @@
import logging
import re
-from email.utils import formataddr
-
from odoo import api, fields, models, _
from odoo.exceptions import UserError
+from odoo.tools import formataddr
_logger = logging.getLogger(__name__)
diff --git a/odoo/addons/base/models/res_partner.py b/odoo/addons/base/models/res_partner.py
index 7cf95adc2e1..48d6e90d1a4 100644
--- a/odoo/addons/base/models/res_partner.py
+++ b/odoo/addons/base/models/res_partner.py
@@ -9,8 +9,6 @@ import pytz
import threading
import re
-from email.utils import formataddr
-
import requests
from lxml import etree
from werkzeug import urls
@@ -355,7 +353,7 @@ class Partner(models.Model):
def _compute_email_formatted(self):
for partner in self:
if partner.email:
- partner.email_formatted = formataddr((partner.name or u"False", partner.email or u"False"))
+ partner.email_formatted = tools.formataddr((partner.name or u"False", partner.email or u"False"))
else:
partner.email_formatted = ''
diff --git a/odoo/tools/mail.py b/odoo/tools/mail.py
index 50ccd6b0375..d795922196e 100644
--- a/odoo/tools/mail.py
+++ b/odoo/tools/mail.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import base64
import collections
import logging
from lxml.html import clean
@@ -11,7 +12,7 @@ import threading
import time
from email.header import decode_header, Header
-from email.utils import getaddresses, formataddr
+from email.utils import getaddresses
from lxml import etree
import odoo
@@ -438,6 +439,8 @@ single_email_re = re.compile(r"""^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6
mail_header_msgid_re = re.compile('<[^<>]+>')
+email_addr_escapes_re = re.compile(r'[\\"]')
+
def generate_tracking_message_id(res_id):
"""Returns a string that can be used in the Message-ID RFC822 header field
@@ -561,3 +564,38 @@ def decode_smtp_header(smtp_header):
# was mail_thread.decode_header()
def decode_message_header(message, header, separator=' '):
return separator.join(decode_smtp_header(h) for h in message.get_all(header, []) if h)
+
+def formataddr(pair):
+ """Takes a 2-tuple of the form (realname, email_address) and returns
+ the string value suitable for an RFC 2822 From, To or Cc header.
+
+ The email address is considered valid and is left unmodified.
+
+ If the first element of pair is falsy then only the email address
+ is returned.
+
+ >>> formataddr(('John Doe', 'johndoe@example.com'))
+ '"John Doe" <johndoe@example.com>'
+
+ >>> formataddr(('', 'johndoe@example.com'))
+ 'johndoe@example.com'
+ """
+ name, address = pair
+ address.encode('ascii')
+ if name:
+ try:
+ name.encode('ascii')
+ except UnicodeEncodeError:
+ # non-ascii name, transcode the name in a safe format
+ # rfc2047 - MIME Message Header Extensions for Non-ASCII Text
+ return "=?utf-8?b?{name}?= {addr}".format(
+ name=base64.b64encode(name.encode('utf-8')).decode('ascii'),
+ addr=address)
+ else:
+ # ascii name, escape it if needed
+ # rfc2822 - Internet Message Format
+ # #section-3.4 - Address Specification
+ return '"{name}" <{addr}>'.format(
+ name=email_addr_escapes_re.sub(r'\\\g<0>', name),
+ addr=address)
+ return address