From dbb6cfdbf803d2ab86ba11450b5928abf5ab658b Mon Sep 17 00:00:00 2001 From: Julien Castiaux Date: Tue, 20 Aug 2019 13:09:55 +0000 Subject: [PATCH] [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 --- addons/mail/models/mail_cc_mixin.py | 3 +- addons/mail/models/mail_channel.py | 3 +- addons/mail/models/mail_mail.py | 3 +- addons/mail/models/mail_message.py | 5 +-- addons/mail/models/mail_thread.py | 3 +- addons/mail/wizard/mail_compose_message.py | 4 +- addons/survey/tests/test_survey_invite.py | 4 +- addons/survey/wizard/survey_invite.py | 4 +- addons/test_mail/tests/common.py | 4 +- addons/test_mail/tests/test_mail_channel.py | 4 +- addons/test_mail/tests/test_mail_gateway.py | 29 ++++++++------ addons/test_mail/tests/test_mail_message.py | 3 +- addons/test_mail/tests/test_message_post.py | 3 +- addons/test_mail/tests/test_message_track.py | 4 +- addons/test_mail/tests/test_performance.py | 4 +- .../tests/test_mail_channel.py | 2 +- .../wizard/slide_channel_invite.py | 3 +- odoo/addons/base/models/res_partner.py | 4 +- odoo/tools/mail.py | 40 ++++++++++++++++++- 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 ', self.customer.email])) + set(['test1@example.com', '"Raoulette Vignolette" ', 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 ', self.customer.email])) + set(['test1@example.com', '"Raoulette Vignolette" ', 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 ', + extra='', email_from='"Sylvie Lelitre" ', 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 ') + plaintext = self.format(test_mail_data.MAIL_TEMPLATE_PLAINTEXT, email_from='"Sylvie Lelitre" ') 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 ') + multipart = self.format(MAIL_TEMPLATE, email_from='"Sylvie Lelitre" ') res = self.env['mail.thread'].message_parse(from_string(multipart)) self.assertIn('

Please call me as soon as possible this afternoon!

', 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 ', to='generic@test.com') + mail = self.format(test_mail_data.MAIL_EML_ATTACHMENT, email_from='"Sylvie Lelitre" ', 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 ', to='generic@test.com') + mail = self.format(test_mail_data.MAIL_TEMPLATE_PLAINTEXT, email_from='"Sylvie Lelitre" ', to='generic@test.com') res = self.env['mail.thread'].message_parse(from_string(mail)) self.assertIn('
\nPlease call me as soon as possible this afternoon!\n\n--\nSylvie\n
', 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 ' + cls.email_from = '"Sylvie Lelitre" ' 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 ') @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 ') # 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 ') @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 ') @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 , cc3@example.com', target_model='mail.test.cc') cc = email_split_and_format(record.email_cc) - self.assertEqual(sorted(cc), ['cc1 ', 'cc2@example.com', 'cc3@example.com'], 'new cc should have been added on record (unique)') + self.assertEqual(sorted(cc), ['"cc1" ', '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 , cc3@example.com', target_model='mail.test.cc') cc = email_split_and_format(record.email_cc) - self.assertEqual(sorted(cc), ['cc2 ', 'cc3@example.com'], 'new cc should have been added on record (unique)') + self.assertEqual(sorted(cc), ['"cc2" ', '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" ' + + >>> 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