diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index 7b47d4d031f02ecc9db3b8dd6583d48bbef5a168..47a799e993ce84f34ae6d831572952f3449fd8c4 100644
--- a/addons/mail/wizard/mail_compose_message.py
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -8,7 +8,6 @@ import re
 from odoo import _, api, fields, models, tools, Command
 from odoo.exceptions import UserError
 from odoo.osv import expression
-from odoo.tools import email_re
 
 
 def _reopen(self, res_id, model, context=None):
@@ -466,12 +465,11 @@ class MailComposer(models.TransientModel):
 
         recipients_info = {}
         for record_id, mail_values in mail_values_dict.items():
-            mail_to = []
-            if mail_values.get('email_to'):
-                mail_to += email_re.findall(mail_values['email_to'])
-                # if unrecognized email in email_to -> keep it as used for further processing
-                if not mail_to:
-                    mail_to.append(mail_values['email_to'])
+            # add email from email_to; if unrecognized email in email_to keep
+            # it as used for further processing
+            mail_to = tools.email_split_and_format(mail_values.get('email_to'))
+            if not mail_to and mail_values.get('email_to'):
+                mail_to.append(mail_values['email_to'])
             # add email from recipients (res.partner)
             mail_to += [
                 recipient_emails[recipient_command[1]]
@@ -528,7 +526,7 @@ class MailComposer(models.TransientModel):
             elif not mail_to:
                 mail_values['state'] = 'cancel'
                 mail_values['failure_type'] = 'mail_email_missing'
-            elif not mail_to_normalized or not email_re.findall(mail_to):
+            elif not mail_to_normalized:
                 mail_values['state'] = 'cancel'
                 mail_values['failure_type'] = 'mail_email_invalid'
             elif done_emails is not None and not mailing_document_based:
diff --git a/addons/test_mass_mailing/tests/test_mailing.py b/addons/test_mass_mailing/tests/test_mailing.py
index 66e55fe496be6d755fea0ed386aea796fe258c14..1e7d3ae84686c2787bf0fffccab8ed4ac99009b2 100644
--- a/addons/test_mass_mailing/tests/test_mailing.py
+++ b/addons/test_mass_mailing/tests/test_mailing.py
@@ -221,11 +221,11 @@ class TestMassMailing(TestMassMailCommon):
                          'partner': customer_fmt,
                          'trace_status': 'sent'},
                         {'email': '"Unicode Customer" <test.customer.😊@example.com>',
-                         # double encapsulation, not good
-                         'email_to_recipients': [[f'"{customer_unic.name}" <"Unicode Customer" <test.customer.format@example.com>>']],
-                         'failure_type': 'mail_email_invalid',
+                         # mail to avoids double encapsulation
+                         'email_to_recipients': [[f'"{customer_unic.name}" <test.customer.😊@example.com>']],
+                         'failure_type': False,
                          'partner': customer_unic,
-                         'trace_status': 'cancel'},  # email_re usage forbids mailing to unicode
+                         'trace_status': 'sent'},
                         {'email': 'TEST.CUSTOMER.CASE@EXAMPLE.COM',
                          'email_to_recipients': [[f'"{customer_case.name}" <test.customer.case@example.com>']],
                          'failure_type': False,
@@ -254,8 +254,8 @@ class TestMassMailing(TestMassMailCommon):
                         {'email': 'record.😊@example.com',
                          'email_to_mail': 'record.😊@example.com',
                          'email_to_recipients': [['record.😊@example.com']],
-                         'failure_type': 'mail_email_invalid',
-                         'trace_status': 'cancel'},  # email_re usage forbids mailing to unicode
+                         'failure_type': False,
+                         'trace_status': 'sent'},
                         {'email': 'test.record.case@example.com',
                          'email_to_mail': 'test.record.case@example.com',
                          'email_to_recipients': [['test.record.case@example.com']],
diff --git a/odoo/addons/base/tests/test_mail.py b/odoo/addons/base/tests/test_mail.py
index a2bd023d7e6a83c344bfc429aae3ec130179621a..0465b563748041b7bdde6ce481f35eced27dce6f 100644
--- a/odoo/addons/base/tests/test_mail.py
+++ b/odoo/addons/base/tests/test_mail.py
@@ -418,6 +418,8 @@ class TestEmailTools(BaseCase):
         """ Test 'email_normalize'. Note that it is built on 'email_split' so
         some use cases are already managed in 'test_email_split(_and_format)'
         hence having more specific test cases here about normalization itself. """
+        format_name = 'My Super Prénom'
+        format_name_ascii = '=?utf-8?b?TXkgU3VwZXIgUHLDqW5vbQ==?='
         sources = [
             '"Super Déboulonneur" <deboulonneur@example.com>',  # formatted
             'Déboulonneur deboulonneur@example.com',  # wrong formatting
@@ -430,17 +432,42 @@ class TestEmailTools(BaseCase):
         ]
         expected_list = [
             'deboulonneur@example.com',
-            'déboulonneur deboulonneur@example.com',
+            'Déboulonneur deboulonneur@example.com',
             'deboulonneur@example.comdéboulonneur',
             False,
             '@example.com',  # funny
             'deboulonneur.😊@example.com',
             'déboulonneur@examplé.com',
-            'déboulonneur@examplé.com',
+            'DéBoulonneur@examplé.com',
         ]
-        for source, expected in zip(sources, expected_list):
+        expected_fmt_utf8_list = [
+            f'"{format_name}" <deboulonneur@example.com>',
+            f'"{format_name}" <Déboulonneur deboulonneur@example.com>',
+            f'"{format_name}" <deboulonneur@example.comdéboulonneur>',
+            f'"{format_name}" <@>',
+            f'"{format_name}" <@example.com>',
+            f'"{format_name}" <deboulonneur.😊@example.com>',
+            f'"{format_name}" <déboulonneur@examplé.com>',
+            f'"{format_name}" <DéBoulonneur@examplé.com>',
+        ]
+        expected_fmt_ascii_list = [
+            f'{format_name_ascii} <deboulonneur@example.com>',
+            f'{format_name_ascii} <Déboulonneur deboulonneur@example.com>',
+            f'{format_name_ascii} <deboulonneur@example.xn--comdboulonneur-ekb>',
+            f'{format_name_ascii} <@>',
+            f'{format_name_ascii} <@example.com>',
+            f'{format_name_ascii} <deboulonneur.😊@example.com>',
+            f'{format_name_ascii} <déboulonneur@xn--exampl-gva.com>',
+            f'{format_name_ascii} <DéBoulonneur@xn--exampl-gva.com>',
+        ]
+        for source, expected, expected_utf8_fmt, expected_ascii_fmt in zip(sources, expected_list, expected_fmt_utf8_list, expected_fmt_ascii_list):
             with self.subTest(source=source):
                 self.assertEqual(email_normalize(source, strict=True), expected)
+                # standard usage of formataddr
+                self.assertEqual(formataddr((format_name, (expected or '')), charset='utf-8'), expected_utf8_fmt)
+                # check using INDA at format time, using ascii charset as done when
+                # sending emails (see extract_rfc2822_addresses)
+                self.assertEqual(formataddr((format_name, (expected or '')), charset='ascii'), expected_ascii_fmt)
 
     def test_email_split(self):
         """ Test 'email_split' """
diff --git a/odoo/tools/mail.py b/odoo/tools/mail.py
index 720344aecf6ff848b18016b0fde59093797f6203..312578b418c38d394ea0050a46b29688f556e7b6 100644
--- a/odoo/tools/mail.py
+++ b/odoo/tools/mail.py
@@ -510,7 +510,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
 
@@ -551,14 +550,26 @@ def email_split_and_format(text):
     return [formataddr((name, email)) for (name, email) in email_split_tuples(text)]
 
 def email_normalize(text, strict=True):
-    """ Sanitize and standardize email address entries.
-        A normalized email is considered as :
-        - having a left part + @ + a right part (the domain can be without '.something')
-        - being lower case
-        - having no name before the address. Typically, having no 'Name <>'
-        Ex:
-        - Possible Input Email : 'Name <NaMe@DoMaIn.CoM>'
-        - Normalized Output Email : 'name@domain.com'
+    """ Sanitize and standardize email address entries. As of rfc5322 section
+    3.4.1 local-part is case-sensitive. However most main providers do consider
+    the local-part as case insensitive. With the introduction of smtp-utf8
+    within odoo, this assumption is certain to fall short for international
+    emails. We now consider that
+
+      * if local part is ascii: normalize still 'lower' ;
+      * else: use as it, SMTP-UF8 is made for non-ascii local parts;
+
+    Concerning domain part of the address, as of v14 international domain (IDNA)
+    are handled fine. The domain is always lowercase, lowering it is fine as it
+    is probably an error. With the introduction of IDNA, there is an encoding
+    that allow non-ascii characters to be encoded to ascii ones, using 'idna.encode'.
+
+    A normalized email is considered as :
+    - having a left part + @ + a right part (the domain can be without '.something')
+    - having no name before the address. Typically, having no 'Name <>'
+    Ex:
+    - Possible Input Email : 'Name <NaMe@DoMaIn.CoM>'
+    - Normalized Output Email : 'name@domain.com'
 
     :param boolean strict: if True, text should contain a single email
       (default behavior in stable 14+). If more than one email is found no
@@ -572,7 +583,16 @@ def email_normalize(text, strict=True):
     emails = email_split(text)
     if not emails or (strict and len(emails) != 1):
         return False
-    return emails[0].lower()
+
+    local_part, at, domain = emails[0].rpartition('@')
+    try:
+        local_part.encode('ascii')
+    except UnicodeEncodeError:
+        pass
+    else:
+        local_part = local_part.lower()
+
+    return local_part + at + domain.lower()
 
 def email_normalize_all(text):
     """ Tool method allowing to extract email addresses from a text input and returning