[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
This commit is contained in:
Julien Castiaux
2019-09-17 11:32:54 +00:00
parent 1b057dd76b
commit dbb6cfdbf8
19 changed files with 75 additions and 54 deletions
+1 -2
View File
@@ -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
+1 -2
View File
@@ -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__)
+1 -2
View File
@@ -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 = {
+2 -3
View File
@@ -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
+1 -2
View File
@@ -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:
+1 -3
View File
@@ -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'))
+2 -2
View File
@@ -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')
+1 -3
View File
@@ -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
+2 -2
View File
@@ -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)
+1 -3
View File
@@ -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):
+16 -13
View File
@@ -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)')
+1 -2
View File
@@ -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
+1 -2
View File
@@ -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):
+1 -3
View File
@@ -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):
+1 -3
View File
@@ -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')
@@ -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):
@@ -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__)
+1 -3
View File
@@ -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 = ''
+39 -1
View File
@@ -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