diff --git a/addons/mail/tests/test_res_partner.py b/addons/mail/tests/test_res_partner.py index 97828c0981df4c622d21ce2332e21519bb06ef2b..56fb86f4db59f2a6a2a374f3759e399af9858e9a 100644 --- a/addons/mail/tests/test_res_partner.py +++ b/addons/mail/tests/test_res_partner.py @@ -3,8 +3,10 @@ from odoo.addons.mail.tests.common import MailCommon from odoo.tests.common import Form, users +from odoo.tests import tagged +@tagged('res_partner') class TestPartner(MailCommon): def test_res_partner_find_or_create(self): @@ -27,6 +29,73 @@ class TestPartner(MailCommon): self.assertEqual(new2.email, '2patrick@example.com') self.assertEqual(new2.email_normalized, '2patrick@example.com') + @users('admin') + def test_res_partner_find_or_create_email(self): + """ Test 'find_or_create' tool used in mail, notably when linking emails + found in recipients to partners when sending emails using the mail + composer. """ + partners = self.env['res.partner'].create([ + { + 'email': 'classic.format@test.example.com', + 'name': 'Classic Format', + }, + { + 'email': '"FindMe Format" <find.me.format@test.example.com>', + 'name': 'FindMe Format', + }, { + 'email': 'find.me.multi.1@test.example.com, "FindMe Multi" <find.me.multi.2@test.example.com>', + 'name': 'FindMe Multi', + }, + ]) + # check data used for finding / searching + self.assertEqual( + partners.mapped('email_formatted'), + ['"Classic Format" <classic.format@test.example.com>', + '"FindMe Format" <"FindMe Format" <find.me.format@test.example.com>>', + '"FindMe Multi" <find.me.multi.1@test.example.com, "FindMe Multi" <find.me.multi.2@test.example.com>>'] + ) + self.assertEqual( + partners.mapped('email_normalized'), + ['classic.format@test.example.com', 'find.me.format@test.example.com', False] + ) + + # classic find or create: use normalized email to compare records + for email in ('CLASSIC.FORMAT@TEST.EXAMPLE.COM', '"Another Name" <classic.format@test.example.com>'): + with self.subTest(email=email): + self.assertEqual(self.env['res.partner'].find_or_create(email), partners[0]) + # find on encapsulated email: comparison of normalized should work + for email in ('FIND.ME.FORMAT@TEST.EXAMPLE.COM', '"Different Format" <find.me.format@test.example.com>'): + with self.subTest(email=email): + self.assertEqual(self.env['res.partner'].find_or_create(email), partners[1]) + # multi-emails -> no normalized email -> fails each time, create new partner (FIXME) + for email in ('find.me.multi.1@test.example.com', 'find.me.multi.2@test.example.com'): + with self.subTest(email=email): + partner = self.env['res.partner'].find_or_create(email) + self.assertNotIn(partner, partners) + self.assertEqual(partner.email, email) + partner.unlink() # do not mess with subsequent tests + + # now input is multi email -> '_parse_partner_name' used in 'find_or_create' + # before trying to normalize is quite tolerant, allowing positive checks + for email_input, match_partner, exp_email_partner in [ + ('classic.format@test.example.com,another.email@test.example.com', + partners[0], 'classic.format@test.example.com'), # first found email matches existing + ('another.email@test.example.com,classic.format@test.example.com', + self.env['res.partner'], 'another.email@test.example.com'), # first found email does not match + ('find.me.multi.1@test.example.com,find.me.multi.2@test.example.com', + self.env['res.partner'], 'find.me.multi.1@test.example.com'), + ]: + with self.subTest(email_input=email_input): + partner = self.env['res.partner'].find_or_create(email_input) + # either matching existing, either new partner + if match_partner: + self.assertEqual(partner, match_partner) + else: + self.assertNotIn(partner, partners) + self.assertEqual(partner.email, exp_email_partner) + if partner not in partners: + partner.unlink() # do not mess with subsequent tests + @users('admin') def test_res_partner_merge_wizards(self): Partner = self.env['res.partner'] diff --git a/addons/mass_mailing/tests/common.py b/addons/mass_mailing/tests/common.py index 9cb2b9bf1f076b499ab49e116387630fa03fdc2c..09f4508326a616482e6f13f7c2f2dfd17baf2b39 100644 --- a/addons/mass_mailing/tests/common.py +++ b/addons/mass_mailing/tests/common.py @@ -48,6 +48,8 @@ class MassMailCase(MailCase, MockLinkTracker): 'record: linked record, # MAIL.MAIL 'content': optional content that should be present in mail.mail body_html; + 'email_to_mail': optional email used for the mail, when different from the + one stored on the trace itself; 'email_to_recipients': optional, see '_assertMailMail'; 'failure_type': optional failure reason; }, { ... }] @@ -97,6 +99,7 @@ class MassMailCase(MailCase, MockLinkTracker): for recipient_info, link_info, record in zip(recipients_info, mail_links_info, records): partner = recipient_info.get('partner', self.env['res.partner']) email = recipient_info.get('email') + email_to_mail = recipient_info.get('email_to_mail') or email email_to_recipients = recipient_info.get('email_to_recipients') state = recipient_info.get('state', 'sent') record = record or recipient_info.get('record') @@ -150,7 +153,7 @@ class MassMailCase(MailCase, MockLinkTracker): ) else: self.assertMailMailWEmails( - [email], state_mapping[state], + [email_to_mail], state_mapping[state], author=author, content=content, email_to_recipients=email_to_recipients, diff --git a/addons/test_mail/models/test_mail_models.py b/addons/test_mail/models/test_mail_models.py index 9b0e15b3337fc0a43ada9f340b6b525d102662b7..3237833991845ff4742a34de4a9d15ad77bd67df 100644 --- a/addons/test_mail/models/test_mail_models.py +++ b/addons/test_mail/models/test_mail_models.py @@ -27,6 +27,16 @@ class MailTestGateway(models.Model): email_from = fields.Char() custom_field = fields.Char() + @api.model + def message_new(self, msg_dict, custom_values=None): + """ Check override of 'message_new' allowing to update record values + base on incoming email. """ + defaults = { + 'email_from': msg_dict.get('from'), + } + defaults.update(custom_values or {}) + return super().message_new(msg_dict, custom_values=defaults) + class MailTestGatewayGroups(models.Model): """ A model looking like discussion channels / groups (flat thread and diff --git a/addons/test_mail/tests/__init__.py b/addons/test_mail/tests/__init__.py index 631462616cf8f09b426c3ada9423d58a2f25b29f..d4baf2016e83d94b58d84149b5637cb737e5ad02 100644 --- a/addons/test_mail/tests/__init__.py +++ b/addons/test_mail/tests/__init__.py @@ -13,6 +13,7 @@ from . import test_mail_gateway from . import test_mail_multicompany from . import test_mail_template_preview from . import test_mail_thread_internals +from . import test_mail_thread_mixins from . import test_mail_template from . import test_mail_tools from . import test_message_management diff --git a/addons/test_mail/tests/test_mail_activity.py b/addons/test_mail/tests/test_mail_activity.py index e6d4b00374c1ec932329c006615d90a2b26bca83..68f17739a75605bf54968ea38a3ba178682644b8 100644 --- a/addons/test_mail/tests/test_mail_activity.py +++ b/addons/test_mail/tests/test_mail_activity.py @@ -366,41 +366,42 @@ class TestActivityMixin(TestActivityCommon): record = self.env['mail.test.activity'].search([('my_activity_date_deadline', '=', date_today)]) self.assertEqual(test_record_1, record) -class TestReadProgressBar(tests.TransactionCase): +@tests.tagged('mail_activity') +class TestORM(TestActivityCommon): """Test for read_progress_bar""" def test_week_grouping(self): """The labels associated to each record in read_progress_bar should match the ones from read_group, even in edge cases like en_US locale on sundays """ - model = self.env['mail.test.activity'].with_context(lang='en_US') + MailTestActivityCtx = self.env['mail.test.activity'].with_context({"lang": "en_US"}) # Don't mistake fields date and date_deadline: # * date is just a random value # * date_deadline defines activity_state - model.create({ + self.env['mail.test.activity'].create({ 'date': '2021-05-02', 'name': "Yesterday, all my troubles seemed so far away", }).activity_schedule( 'test_mail.mail_act_test_todo', summary="Make another test super asap (yesterday)", - date_deadline=fields.Date.context_today(model) - timedelta(days=7), + date_deadline=fields.Date.context_today(MailTestActivityCtx) - timedelta(days=7), ) - model.create({ + self.env['mail.test.activity'].create({ 'date': '2021-05-09', 'name': "Things we said today", }).activity_schedule( 'test_mail.mail_act_test_todo', summary="Make another test asap", - date_deadline=fields.Date.context_today(model), + date_deadline=fields.Date.context_today(MailTestActivityCtx), ) - model.create({ + self.env['mail.test.activity'].create({ 'date': '2021-05-16', 'name': "Tomorrow Never Knows", }).activity_schedule( 'test_mail.mail_act_test_todo', summary="Make a test tomorrow", - date_deadline=fields.Date.context_today(model) + timedelta(days=7), + date_deadline=fields.Date.context_today(MailTestActivityCtx) + timedelta(days=7), ) domain = [('date', "!=", False)] @@ -415,8 +416,8 @@ class TestReadProgressBar(tests.TransactionCase): } # call read_group to compute group names - groups = model.read_group(domain, fields=['date'], groupby=[groupby]) - progressbars = model.read_progress_bar(domain, group_by=groupby, progress_bar=progress_bar) + groups = MailTestActivityCtx.read_group(domain, fields=['date'], groupby=[groupby]) + progressbars = MailTestActivityCtx.read_progress_bar(domain, group_by=groupby, progress_bar=progress_bar) self.assertEqual(len(groups), 3) self.assertEqual(len(progressbars), 3) diff --git a/addons/test_mail/tests/test_mail_composer.py b/addons/test_mail/tests/test_mail_composer.py index a1bc46eee09b990a76d3cf05e6d44e091ba099b0..107f6abad4bf27bb7d7cf711b0250a344f330a8e 100644 --- a/addons/test_mail/tests/test_mail_composer.py +++ b/addons/test_mail/tests/test_mail_composer.py @@ -380,7 +380,7 @@ class TestComposerInternals(TestMailComposer): self.assertEqual(composer.mail_server_id.id, False) @users('employee') - @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail') + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.tests') def test_mail_composer_parent(self): """ Test specific management in comment mode when having parent_id set: record_name, subject, parent's partners. """ @@ -397,6 +397,7 @@ class TestComposerInternals(TestMailComposer): self.assertEqual(composer.body, '<p>Test Body</p>') self.assertEqual(composer.partner_ids, self.partner_1 + self.partner_2) + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.tests') def test_mail_composer_rights_portal(self): portal_user = self._create_portal_user() @@ -447,7 +448,7 @@ class TestComposerResultsComment(TestMailComposer): notification and emails generated during this process. """ @users('employee') - @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail') + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.tests') def test_mail_composer_notifications_delete(self): """ Notifications are correctly deleted once sent """ composer = self.env['mail.compose.message'].with_context( @@ -489,7 +490,7 @@ class TestComposerResultsComment(TestMailComposer): self.assertEqual(len(self._new_mails.exists()), 2, 'Should not have deleted mail.mail records') @users('employee') - @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail') + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.tests') def test_mail_composer_recipients(self): """ Test partner_ids given to composer are given to the final message. """ composer = self.env['mail.compose.message'].with_context( @@ -605,6 +606,146 @@ class TestComposerResultsComment(TestMailComposer): self.assertEqual(set(message.attachment_ids.mapped('res_id')), set(self.test_record.ids)) self.assertTrue(all(attach not in message.attachment_ids for attach in attachs), 'Should have copied attachments') + @users('employee') + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_mail_composer_wtpl_recipients_email_fields(self): + """ Test various combinations of corner case / not standard filling of + email fields: multi email, formatted emails, ... on template, used to + post a message using the composer.""" + existing_partners = self.env['res.partner'].search([]) + partner_format_tofind, partner_multi_tofind = self.env['res.partner'].create([ + { + 'email': '"FindMe Format" <find.me.format@test.example.com>', + 'name': 'FindMe Format', + }, { + 'email': 'find.me.multi.1@test.example.com, "FindMe Multi" <find.me.multi.2@test.example.com>', + 'name': 'FindMe Multi', + } + ]) + email_ccs = ['"Raoul" <test.cc.1@example.com>', '"Raoulette" <test.cc.2@example.com>', 'test.cc.2.2@example.com>', 'invalid', ' '] + email_tos = ['"Micheline, l\'Immense" <test.to.1@example.com>', 'test.to.2@example.com', 'wrong', ' '] + + self.template.write({ + 'email_cc': ', '.join(email_ccs), + 'email_from': '${user.email_formatted | safe}', + 'email_to':', '.join(email_tos + (partner_format_tofind + partner_multi_tofind).mapped('email')), + 'partner_to': f'{self.partner_1.id},{self.partner_2.id},0,test', + }) + self.user_employee.write({'email': 'email.from.1@test.example.com, email.from.2@test.example.com'}) + self.partner_1.write({'email': '"Valid Formatted" <valid.lelitre@agrolait.com>'}) + self.partner_2.write({'email': 'valid.other.1@agrolait.com, valid.other.cc@agrolait.com'}) + # ensure values used afterwards for testing + self.assertEqual( + self.partner_employee.email_formatted, + '"Ernest Employee" <email.from.1@test.example.com, email.from.2@test.example.com>', + 'Formatting: wrong formatting due to multi-email') + self.assertEqual( + self.partner_1.email_formatted, + '"Valid Lelitre" <"Valid Formatted" <valid.lelitre@agrolait.com>>', + 'Formatting: wrong double encapsulation') + self.assertEqual( + self.partner_2.email_formatted, + '"Valid Poilvache" <valid.other.1@agrolait.com, valid.other.cc@agrolait.com>', + 'Formatting: wrong formatting due to multi-email') + + # instantiate composer, post message + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context( + self.test_record, + add_web=True, + default_template_id=self.template.id, + ) + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_mail_app(): + composer.send_mail() + + # find partners created during sending (as emails are transformed into partners) + # FIXME: currently email finding based on formatted / multi emails does + # not work + new_partners = self.env['res.partner'].search([]).search([('id', 'not in', existing_partners.ids)]) + self.assertEqual(len(new_partners), 9, + 'Mail (FIXME): multiple partner creation due to formatted / multi emails: 2 extra partners') + self.assertIn(partner_format_tofind, new_partners) + self.assertIn(partner_multi_tofind, new_partners) + self.assertEqual( + sorted(new_partners.mapped('email')), + sorted(['"FindMe Format" <find.me.format@test.example.com>', + 'find.me.multi.1@test.example.com, "FindMe Multi" <find.me.multi.2@test.example.com>', + 'find.me.multi.1@test.example.com', + 'find.me.multi.2@test.example.com', + 'test.cc.1@example.com', 'test.cc.2@example.com', 'test.cc.2.2@example.com', + 'test.to.1@example.com', 'test.to.2@example.com']), + 'Mail: created partners for valid emails (wrong / invalid not taken into account) + did not find corner cases (FIXME)' + ) + self.assertEqual( + sorted(new_partners.mapped('email_formatted')), + sorted(['"FindMe Format" <"FindMe Format" <find.me.format@test.example.com>>', + '"FindMe Multi" <find.me.multi.1@test.example.com, "FindMe Multi" <find.me.multi.2@test.example.com>>', + '"find.me.multi.1@test.example.com" <find.me.multi.1@test.example.com>', + '"find.me.multi.2@test.example.com" <find.me.multi.2@test.example.com>', + '"test.cc.1@example.com" <test.cc.1@example.com>', + '"test.cc.2@example.com" <test.cc.2@example.com>', + '"test.cc.2.2@example.com" <test.cc.2.2@example.com>', + '"test.to.1@example.com" <test.to.1@example.com>', + '"test.to.2@example.com" <test.to.2@example.com>']), + ) + self.assertEqual( + sorted(new_partners.mapped('name')), + sorted(['FindMe Format', + 'FindMe Multi', + 'find.me.multi.1@test.example.com', + 'find.me.multi.2@test.example.com', + 'test.cc.1@example.com', 'test.to.1@example.com', 'test.to.2@example.com', + 'test.cc.2@example.com', 'test.cc.2.2@example.com']), + 'Mail: currently setting name = email, not taking into account formatted emails' + ) + + # global outgoing: two mail.mail (all customer recipients, then all employee recipients) + # and 11 emails, and 1 inbox notification (admin) + # FIXME template is sent only to partners (email_to are transformed) -> + # wrong / weird emails (see email_formatted of partners) is kept + # FIXME: more partners created than real emails (see above) -> due to + # transformation from email -> partner in template 'generate_recipients' + # there are more partners than email to notify; + # NOTE: 'Findme Multi' is excluded as it has the same email as 'find.me.multi.1@test.example.com' + # (created by template) and comes second in a search based on email + mailed_new_partners = new_partners.filtered(lambda p: p.name != 'FindMe Multi') + self.assertEqual(len(mailed_new_partners), 8) + self.assertEqual(len(self._new_mails), 2, 'Should have created 2 mail.mail') + self.assertEqual( + len(self._mails), len(mailed_new_partners) + 3, + f'Should have sent {len(mailed_new_partners) + 3} emails, one / recipient ({len(mailed_new_partners)} mailed partners + partner_1 + partner_2 + partner_employee)') + self.assertMailMail( + self.partner_employee_2, 'sent', + author=self.partner_employee, + email_values={ + 'body_content': f'TemplateBody {self.test_record.name}', + # currently holding multi-email 'from' + 'email_from': formataddr((self.user_employee.name, 'email.from.1@test.example.com, email.from.2@test.example.com')), + 'subject': f'TemplateSubject {self.test_record.name}', + }, + fields_values={ + # currently holding multi-email 'email_from' + 'email_from': formataddr((self.user_employee.name, 'email.from.1@test.example.com, email.from.2@test.example.com')), + }, + mail_message=self.test_record.message_ids[0], + ) + self.assertMailMail( + self.partner_1 + self.partner_2 + mailed_new_partners, 'sent', + author=self.partner_employee, + email_values={ + 'body_content': f'TemplateBody {self.test_record.name}', + 'email_from': formataddr((self.user_employee.name, 'email.from.1@test.example.com, email.from.2@test.example.com')), + 'subject': f'TemplateSubject {self.test_record.name}', + }, + fields_values={ + # currently holding multi-email 'email_from' + 'email_from': formataddr((self.user_employee.name, 'email.from.1@test.example.com, email.from.2@test.example.com')), + }, + mail_message=self.test_record.message_ids[0], + ) + @tagged('mail_composer') class TestComposerResultsMass(TestMailComposer): @@ -908,3 +1049,139 @@ class TestComposerResultsMass(TestMailComposer): composer = composer_form.save() with self.mock_mail_gateway(mail_unlink_sent=False), self.assertRaises(ValueError): composer.send_mail() + + @users('employee') + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_mail_composer_wtpl_recipients_email_fields(self): + """ Test various combinations of corner case / not standard filling of + email fields: multi email, formatted emails, ... """ + existing_partners = self.env['res.partner'].search([]) + partner_format_tofind, partner_multi_tofind = self.env['res.partner'].create([ + { + 'email': '"FindMe Format" <find.me.format@test.example.com>', + 'name': 'FindMe Format', + }, { + 'email': 'find.me.multi.1@test.example.com, "FindMe Multi" <find.me.multi.2@test.example.com>', + 'name': 'FindMe Multi', + } + ]) + email_ccs = ['"Raoul" <test.cc.1@example.com>', '"Raoulette" <test.cc.2@example.com>', 'test.cc.2.2@example.com>', 'invalid', ' '] + email_tos = ['"Micheline, l\'Immense" <test.to.1@example.com>', 'test.to.2@example.com', 'wrong', ' '] + + self.template.write({ + 'email_cc': ', '.join(email_ccs), + 'email_from': '${user.email_formatted | safe}', + 'email_to':', '.join(email_tos + (partner_format_tofind + partner_multi_tofind).mapped('email')), + 'partner_to': f'{self.partner_1.id},{self.partner_2.id},0,test', + }) + self.user_employee.write({'email': 'email.from.1@test.example.com, email.from.2@test.example.com'}) + self.partner_1.write({'email': '"Valid Formatted" <valid.lelitre@agrolait.com>'}) + self.partner_2.write({'email': 'valid.other.1@agrolait.com, valid.other.cc@agrolait.com'}) + # ensure values used afterwards for testing + self.assertEqual( + self.partner_employee.email_formatted, + '"Ernest Employee" <email.from.1@test.example.com, email.from.2@test.example.com>', + 'Formatting: wrong formatting due to multi-email') + self.assertEqual( + self.partner_1.email_formatted, + '"Valid Lelitre" <"Valid Formatted" <valid.lelitre@agrolait.com>>', + 'Formatting: wrong double encapsulation') + self.assertEqual( + self.partner_2.email_formatted, + '"Valid Poilvache" <valid.other.1@agrolait.com, valid.other.cc@agrolait.com>', + 'Formatting: wrong formatting due to multi-email') + + # instantiate composer, send mailing + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context( + self.test_records, + add_web=True, + default_template_id=self.template.id, + ) + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_mail_app(): + composer.send_mail() + + # find partners created during sending (as emails are transformed into partners) + # FIXME: currently email finding based on formatted / multi emails does + # not work + new_partners = self.env['res.partner'].search([]).search([('id', 'not in', existing_partners.ids)]) + self.assertEqual(len(new_partners), 9, + 'Mail (FIXME): did not find existing partners for formatted / multi emails: 2 extra partners') + self.assertIn(partner_format_tofind, new_partners) + self.assertIn(partner_multi_tofind, new_partners) + self.assertEqual( + sorted(new_partners.mapped('email')), + sorted(['"FindMe Format" <find.me.format@test.example.com>', + 'find.me.multi.1@test.example.com, "FindMe Multi" <find.me.multi.2@test.example.com>', + 'find.me.multi.1@test.example.com', + 'find.me.multi.2@test.example.com', + 'test.cc.1@example.com', 'test.cc.2@example.com', 'test.cc.2.2@example.com', + 'test.to.1@example.com', 'test.to.2@example.com']), + 'Mail: created partners for valid emails (wrong / invalid not taken into account) + did not find corner cases (FIXME)' + ) + self.assertEqual( + sorted(new_partners.mapped('email_formatted')), + sorted(['"FindMe Format" <"FindMe Format" <find.me.format@test.example.com>>', + '"FindMe Multi" <find.me.multi.1@test.example.com, "FindMe Multi" <find.me.multi.2@test.example.com>>', + '"find.me.multi.1@test.example.com" <find.me.multi.1@test.example.com>', + '"find.me.multi.2@test.example.com" <find.me.multi.2@test.example.com>', + '"test.cc.1@example.com" <test.cc.1@example.com>', + '"test.cc.2@example.com" <test.cc.2@example.com>', + '"test.cc.2.2@example.com" <test.cc.2.2@example.com>', + '"test.to.1@example.com" <test.to.1@example.com>', + '"test.to.2@example.com" <test.to.2@example.com>']), + ) + self.assertEqual( + sorted(new_partners.mapped('name')), + sorted(['FindMe Format', + 'FindMe Multi', + 'find.me.multi.1@test.example.com', + 'find.me.multi.2@test.example.com', + 'test.cc.1@example.com', 'test.to.1@example.com', 'test.to.2@example.com', + 'test.cc.2@example.com', 'test.cc.2.2@example.com']), + 'Mail: currently setting name = email, not taking into account formatted emails' + ) + + # global outgoing: one mail.mail (all customer recipients), * 2 records + # Note that employee is not mailed here compared to 'comment' mode as he + # is not in the template recipients, only a follower + # FIXME template is sent only to partners (email_to are transformed) -> + # wrong / weird emails (see email_formatted of partners) is kept + # FIXME: more partners created than real emails (see above) -> due to + # transformation from email -> partner in template 'generate_recipients' + # there are more partners than email to notify; + # NOTE: 'Findme Multi' is excluded as it has the same email as 'find.me.multi.1@test.example.com' + # (created by template) and comes second in a search based on email + mailed_new_partners = new_partners.filtered(lambda p: p.name != 'FindMe Multi') + self.assertEqual(len(mailed_new_partners), 8) + self.assertEqual(len(self._new_mails), 2, 'Should have created 2 mail.mail') + self.assertEqual( + len(self._mails), (len(mailed_new_partners) + 2) * 2, + f'Should have sent {(len(mailed_new_partners) + 2) * 2} emails, one / recipient ({len(mailed_new_partners)} mailed partners + partner_1 + partner_2) * 2 records') + for record in self.test_records: + self.assertMailMail( + self.partner_1 + self.partner_2 + mailed_new_partners, + 'sent', + author=self.partner_employee, + email_values={ + 'body_content': f'TemplateBody {record.name}', + # currently holding multi-email 'email_from' + 'email_from': self.partner_employee.email_formatted, + 'reply_to': formataddr(( + f'{self.env.user.company_id.name} {record.name}', + f'{self.alias_catchall}@{self.alias_domain}' + )), + 'subject': f'TemplateSubject {record.name}', + }, + fields_values={ + # currently holding multi-email 'email_from' + 'email_from': self.partner_employee.email_formatted, + 'reply_to': formataddr(( + f'{self.env.user.company_id.name} {record.name}', + f'{self.alias_catchall}@{self.alias_domain}' + )), + }, + mail_message=record.message_ids[0], # message copy is kept + ) diff --git a/addons/test_mail/tests/test_mail_gateway.py b/addons/test_mail/tests/test_mail_gateway.py index 4eb22aa18620bba000e6c13d96f33fcfc161e843..103877f6b24d4ece0dad0e4e9f560c227fc651cc 100644 --- a/addons/test_mail/tests/test_mail_gateway.py +++ b/addons/test_mail/tests/test_mail_gateway.py @@ -430,6 +430,31 @@ class TestMailgateway(TestMailCommon): self.assertEqual(record.message_ids[0].email_from, self.partner_1.email) self.assertNotSentEmail() # No notification / bounce should be sent + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_process_email_author_multiemail(self): + """ Incoming email: recognized author: check multi/formatted email in field """ + test_email = 'valid.lelitre@agrolait.com' + # Email not recognized if partner has a multi-email (source = formatted email) + self.partner_1.write({'email': f'{test_email}, "Valid Lelitre" <another.email@test.example.com>'}) + with self.mock_mail_gateway(): + record = self.format_and_process( + MAIL_TEMPLATE, f'"Valid Lelitre" <{test_email}>', 'groups@test.com', subject='Test3') + + self.assertFalse(record.message_ids[0].author_id, + 'message_process (FIXME): unrecognized email -> author_id due to multi email') + self.assertEqual(record.message_ids[0].email_from, f'"Valid Lelitre" <{test_email}>') + self.assertNotSentEmail() # No notification / bounce should be sent + + # Email not recognized if partner has a multi-email (source = std email) + with self.mock_mail_gateway(): + record = self.format_and_process( + MAIL_TEMPLATE, test_email, 'groups@test.com', subject='Test4') + + self.assertFalse(record.message_ids[0].author_id, + 'message_process (FIXME): unrecognized email -> author_id due to multi email') + self.assertEqual(record.message_ids[0].email_from, test_email) + self.assertNotSentEmail() # No notification / bounce should be sent + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') def test_message_process_email_partner_find(self): """ Finding the partner based on email, based on partner / user / follower """ @@ -621,6 +646,39 @@ class TestMailgateway(TestMailCommon): # Test: one group created by Raoul (or Sylvie maybe, if we implement it) self.assertEqual(len(record), 1) + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_process_alias_followers_multiemail(self): + """ Incoming email from a parent document follower on a Followers only + alias depends on email_from / partner recognition, to be tested when + dealing with multi emails / formatted emails. """ + self.alias.write({ + 'alias_contact': 'followers', + 'alias_parent_model_id': self.env['ir.model']._get('mail.test.gateway').id, + 'alias_parent_thread_id': self.test_record.id, + }) + self.test_record.message_subscribe(partner_ids=[self.partner_1.id]) + email_from = formataddr(("Another Name", self.partner_1.email_normalized)) + + for partner_email, passed in [ + (formataddr((self.partner_1.name, self.partner_1.email_normalized)), True), + (f'{self.partner_1.email_normalized}, "Multi Email" <multi.email@test.example.com>', False), + (f'"Multi Email" <multi.email@test.example.com>, {self.partner_1.email_normalized}', False), + ]: + with self.subTest(partner_email=partner_email): + self.partner_1.write({'email': partner_email}) + record = self.format_and_process( + MAIL_TEMPLATE, email_from, 'groups@test.com', + subject=f'Test for {partner_email}') + + if passed: + self.assertEqual(len(record), 1) + self.assertEqual(record.email_from, email_from) + self.assertEqual(record.message_partner_ids, self.partner_1) + # multi emails not recognized (no normalized email, recognition) + else: + self.assertEqual(len(record), 0, + 'Alias check (FIXME): multi-emails bad support for recognition') + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') def test_message_process_alias_update(self): """ Incoming email update discussion + notification email """ @@ -1573,6 +1631,7 @@ class TestMailgateway(TestMailCommon): # Corner cases / Bugs during message process # -------------------------------------------------- + @mute_logger('odoo.addons.mail.models.mail_thread') def test_message_process_file_encoding_ascii(self): """ Incoming email containing an xml attachment with unknown characters (�) but an ASCII charset should not raise an Exception. UTF-8 is used as a safe fallback. @@ -1598,6 +1657,7 @@ class TestMailgateway(TestMailCommon): self.assertEqual(record.message_main_attachment_id.name, 'bis3.xml') self.assertEqual("<Invoice>Chaussée de Bruxelles</Invoice>", record.message_main_attachment_id.raw.decode()) +@tagged('mail_gateway', 'mail_thread') class TestMailThreadCC(TestMailCommon): @classmethod diff --git a/addons/test_mail/tests/test_mail_mail.py b/addons/test_mail/tests/test_mail_mail.py index 3ebee26330b69d8a5e57d7200e47bfaff3380c53..9cc292c6dfd94eee37e5758cf97d24af1f21bd88 100644 --- a/addons/test_mail/tests/test_mail_mail.py +++ b/addons/test_mail/tests/test_mail_mail.py @@ -3,7 +3,7 @@ import psycopg2 -from odoo import api +from odoo import api, tools from odoo.addons.test_mail.tests.common import TestMailCommon from odoo.tests import common, tagged from odoo.tools import mute_logger @@ -23,7 +23,7 @@ class TestMailMail(TestMailCommon): }).with_context({}) @mute_logger('odoo.addons.mail.models.mail_mail') - def test_mail_message_notify_from_mail_mail(self): + def test_mail_mail_notify_from_mail_mail(self): # Due ot post-commit hooks, store send emails in every step mail = self.env['mail.mail'].sudo().create({ 'body_html': '<p>Test</p>', @@ -35,6 +35,7 @@ class TestMailMail(TestMailCommon): self.assertSentEmail(mail.env.user.partner_id, ['test@example.com']) self.assertEqual(len(self._mails), 1) + @mute_logger('odoo.addons.mail.models.mail_mail') def test_mail_mail_return_path(self): # mail without thread-enabled record base_values = { @@ -74,7 +75,112 @@ class TestMailMail(TestMailCommon): mail.send() self.assertEqual(self._mails[0]['headers']['Return-Path'], '%s@%s' % (self.alias_bounce, self.alias_domain)) + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_mail_mail_values_email_formatted(self): + """ Test outgoing email values, with formatting """ + customer = self.env['res.partner'].create({ + 'name': 'Tony Customer', + 'email': '"Formatted Emails" <tony.customer@test.example.com>', + }) + mail = self.env['mail.mail'].create({ + 'body_html': '<p>Test</p>', + 'email_cc': '"Ignasse, le Poilu" <test.cc.1@test.example.com>', + 'email_to': '"Raoul, le Grand" <test.email.1@test.example.com>, "Micheline, l\'immense" <test.email.2@test.example.com>', + 'recipient_ids': [(4, self.user_employee.partner_id.id), (4, customer.id)] + }) + with self.mock_mail_gateway(): + mail.send() + self.assertEqual(len(self._mails), 3, 'Mail: sent 3 emails: 1 for email_to, 1 / recipient') + self.assertEqual( + sorted(sorted(_mail['email_to']) for _mail in self._mails), + sorted([sorted(['"Raoul, le Grand" <test.email.1@test.example.com>', '"Micheline, l\'immense" <test.email.2@test.example.com>']), + [tools.formataddr((self.user_employee.name, self.user_employee.email_normalized))], + [tools.formataddr(("Tony Customer", '"Formatted Emails" <tony.customer@test.example.com>'))] + ]), + 'Mail (FIXME): double encapsulation of emails ("Tony" <"Formatted" <tony@e.com>>)' + ) + # Currently broken: CC are added to ALL emails (spammy) + self.assertEqual( + [_mail['email_cc'] for _mail in self._mails], + [['test.cc.1@test.example.com']] * 3, + 'Mail: currently always removing formatting in email_cc' + ) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_mail_mail_values_email_multi(self): + """ Test outgoing email values, with email field holding multi emails """ + # Multi + customer = self.env['res.partner'].create({ + 'name': 'Tony Customer', + 'email': 'tony.customer@test.example.com, norbert.customer@test.example.com', + }) + mail = self.env['mail.mail'].create({ + 'body_html': '<p>Test</p>', + 'email_cc': 'test.cc.1@test.example.com, test.cc.2@test.example.com', + 'email_to': 'test.email.1@test.example.com, test.email.2@test.example.com', + 'recipient_ids': [(4, self.user_employee.partner_id.id), (4, customer.id)] + }) + with self.mock_mail_gateway(): + mail.send() + self.assertEqual(len(self._mails), 3, 'Mail: sent 3 emails: 1 for email_to, 1 / recipient') + self.assertEqual( + sorted(sorted(_mail['email_to']) for _mail in self._mails), + sorted([sorted(['test.email.1@test.example.com', 'test.email.2@test.example.com']), + [tools.formataddr((self.user_employee.name, self.user_employee.email_normalized))], + [tools.formataddr(("Tony Customer", 'tony.customer@test.example.com, norbert.customer@test.example.com'))] + ]), + 'Mail: currenty broken (multi email in a single address) but supported by some providers ("Tony" <tony@e.com, tony2@e.com>)' + ) + # Currently broken: CC are added to ALL emails (spammy) + self.assertEqual( + [_mail['email_cc'] for _mail in self._mails], + [['test.cc.1@test.example.com', 'test.cc.2@test.example.com']] * 3, + ) + + # Multi + formatting + customer = self.env['res.partner'].create({ + 'name': 'Tony Customer', + 'email': 'tony.customer@test.example.com, "Norbert Customer" <norbert.customer@test.example.com>', + }) + mail = self.env['mail.mail'].create({ + 'body_html': '<p>Test</p>', + 'email_cc': 'test.cc.1@test.example.com, test.cc.2@test.example.com', + 'email_to': 'test.email.1@test.example.com, test.email.2@test.example.com', + 'recipient_ids': [(4, self.user_employee.partner_id.id), (4, customer.id)] + }) + with self.mock_mail_gateway(): + mail.send() + self.assertEqual(len(self._mails), 3, 'Mail: sent 3 emails: 1 for email_to, 1 / recipient') + self.assertEqual( + sorted(sorted(_mail['email_to']) for _mail in self._mails), + sorted([sorted(['test.email.1@test.example.com', 'test.email.2@test.example.com']), + [tools.formataddr((self.user_employee.name, self.user_employee.email_normalized))], + [tools.formataddr(("Tony Customer", 'tony.customer@test.example.com, "Norbert Customer" <norbert.customer@test.example.com>'))] + ]), + 'Mail: currently broken, double encapsulation with formatting ("Tony" <tony@e.com, "Tony2" <tony2@e.com>>)' + ) + # Currently broken: CC are added to ALL emails (spammy) + self.assertEqual( + [_mail['email_cc'] for _mail in self._mails], + [['test.cc.1@test.example.com', 'test.cc.2@test.example.com']] * 3, + ) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_mail_mail_values_unicode(self): + """ Unicode should be fine. """ + mail = self.env['mail.mail'].create({ + 'body_html': '<p>Test</p>', + 'email_cc': 'test.😊.cc@example.com', + 'email_to': 'test.😊@example.com', + }) + with self.mock_mail_gateway(): + mail.send() + self.assertEqual(len(self._mails), 1) + self.assertEqual(self._mails[0]['email_cc'], ['test.😊.cc@example.com']) + self.assertEqual(self._mails[0]['email_to'], ['test.😊@example.com']) + +@tagged('mail_mail') class TestMailMailRace(common.TransactionCase): @mute_logger('odoo.addons.mail.models.mail_mail') diff --git a/addons/test_mail/tests/test_mail_thread_mixins.py b/addons/test_mail/tests/test_mail_thread_mixins.py new file mode 100644 index 0000000000000000000000000000000000000000..875a0e8aa0b89c23437df0fd8e4ccf90eea5e930 --- /dev/null +++ b/addons/test_mail/tests/test_mail_thread_mixins.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import exceptions, tools +from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients +from odoo.tests.common import tagged +from odoo.tools import mute_logger + + +@tagged('mail_thread', 'mail_blacklist') +class TestMailThread(TestMailCommon, TestRecipients): + + @mute_logger('odoo.models.unlink') + def test_blacklist_mixin_email_normalized(self): + """ Test email_normalized and is_blacklisted fields behavior, notably + when dealing with encapsulated email fields and multi-email input. """ + base_email = 'test.email@test.example.com' + + # test data: source email, expected email normalized + valid_pairs = [ + (base_email, base_email), + (tools.formataddr(('Another Name', base_email)), base_email), + (f'Name That Should Be Escaped <{base_email}>', base_email), + ('test.😊@example.com', 'test.😊@example.com'), + ('"Name 😊" <test.😊@example.com>', 'test.😊@example.com'), + ] + void_pairs = [(False, False), + ('', False), + (' ', False)] + multi_pairs = [ + (f'{base_email}, other.email@test.example.com', + False), # multi not supported currently + (f'{tools.formataddr(("Another Name", base_email))}, other.email@test.example.com', + False), # multi not supported currently + ] + for email_from, exp_email_normalized in valid_pairs + void_pairs + multi_pairs: + with self.subTest(email_from=email_from, exp_email_normalized=exp_email_normalized): + new_record = self.env['mail.test.gateway'].create({ + 'email_from': email_from, + 'name': 'BL Test', + }) + self.assertEqual(new_record.email_normalized, exp_email_normalized) + self.assertFalse(new_record.is_blacklisted) + + # blacklist email should fail as void + if email_from in [pair[0] for pair in void_pairs]: + with self.assertRaises(exceptions.UserError): + bl_record = self.env['mail.blacklist']._add(email_from) + # blacklist email currently fails but could not + elif email_from in [pair[0] for pair in multi_pairs]: + with self.assertRaises(exceptions.UserError): + bl_record = self.env['mail.blacklist']._add(email_from) + # blacklist email ok + else: + bl_record = self.env['mail.blacklist']._add(email_from) + self.assertEqual(bl_record.email, exp_email_normalized) + new_record.invalidate_cache(fnames=['is_blacklisted']) + self.assertTrue(new_record.is_blacklisted) + + bl_record.unlink() diff --git a/addons/test_mail/tests/test_mail_tools.py b/addons/test_mail/tests/test_mail_tools.py index b36a1335c5d3c450cac0c62ef8f3dcc45c84de1f..284b3623a83d4f9e13dba8e049e470069a5ca3f8 100644 --- a/addons/test_mail/tests/test_mail_tools.py +++ b/addons/test_mail/tests/test_mail_tools.py @@ -22,62 +22,124 @@ class TestMailTools(TestMailCommon, TestRecipients): }) @users('employee') - def test_find_partner_from_emails(self): + def test_mail_find_partner_from_emails(self): Partner = self.env['res.partner'] test_partner = Partner.browse(self.test_partner.ids) self.assertEqual(test_partner.email, self._test_email) - # test direct match - found = Partner._mail_find_partner_from_emails([self._test_email]) - self.assertEqual(found, [test_partner]) - - # test encapsulated email - found = Partner._mail_find_partner_from_emails(['"Norbert Poiluchette" <%s>' % self._test_email]) - self.assertEqual(found, [test_partner]) + sources = [ + self._test_email, # test direct match + f'"Norbert Poiluchette" <{self._test_email}>', # encapsulated + 'fredoastaire@test.example.com', # partial email -> should not match ! + ] + expected_partners = [ + test_partner, + test_partner, + self.env['res.partner'], + ] + for source, expected_partner in zip(sources, expected_partners): + with self.subTest(source=source): + found = Partner._mail_find_partner_from_emails([source]) + self.assertEqual(found, [expected_partner]) # test with wildcard "_" found = Partner._mail_find_partner_from_emails(['alfred_astaire@test.example.com']) self.assertEqual(found, [self.env['res.partner']]) - # sub-check: this search does not consider _ as a wildcard found = Partner._mail_search_on_partner(['alfred_astaire@test.example.com']) self.assertEqual(found, self.env['res.partner']) # test partners with encapsulated emails # ------------------------------------------------------------ - test_partner.sudo().write({'email': '"Alfred Mighty Power Astaire" <%s>' % self._test_email}) - - # test direct match - found = Partner._mail_find_partner_from_emails([self._test_email]) - self.assertEqual(found, [test_partner]) - - # test encapsulated email - found = Partner._mail_find_partner_from_emails(['"Norbert Poiluchette" <%s>' % self._test_email]) - self.assertEqual(found, [test_partner]) + test_partner.sudo().write({'email': f'"Alfred Mighty Power Astaire" <{self._test_email}>'}) + + sources = [ + self._test_email, # test direct match + f'"Norbert Poiluchette" <{self._test_email}>', # encapsulated + ] + expected_partners = [ + test_partner, + test_partner, + ] + for source, expected_partner in zip(sources, expected_partners): + with self.subTest(source=source): + found = Partner._mail_find_partner_from_emails([source]) + self.assertEqual(found, [expected_partner]) # test with wildcard "_" found = Partner._mail_find_partner_from_emails(['alfred_astaire@test.example.com']) self.assertEqual(found, [self.env['res.partner']]) - # sub-check: this search does not consider _ as a wildcard found = Partner._mail_search_on_partner(['alfred_astaire@test.example.com']) self.assertEqual(found, self.env['res.partner']) - # test partners with look-alike emails - # ------------------------------------------------------------ - for email_lookalike in [ - 'alfred.astaire@test.example.com', - 'alfredoastaire@example.com', - 'aalfredoastaire@test.example.com', - 'alfredoastaire@test.example.comm']: - test_partner.sudo().write({'email': '"Alfred Astaire" <%s>' % email_lookalike}) - - # test direct match - found = Partner._mail_find_partner_from_emails([self._test_email]) - self.assertEqual(found, [self.env['res.partner']]) - # test encapsulated email - found = Partner._mail_find_partner_from_emails(['"Norbert Poiluchette" <%s>' % self._test_email]) - self.assertEqual(found, [self.env['res.partner']]) - # test with wildcard "_" - found = Partner._mail_find_partner_from_emails(['alfred_astaire@test.example.com']) - self.assertEqual(found, [self.env['res.partner']]) + @users('employee') + def test_mail_find_partner_from_emails_followers(self): + """ Test '_mail_find_partner_from_emails' when dealing with records on + which followers have to be found based on email. Check multi email + and encapsulated email support. """ + linked_record = self.env['mail.test.simple'].create({'name': 'Record for followers'}) + follower_partner = self.env['res.partner'].sudo().create({ + 'email': self._test_email, + 'name': 'Duplicated, follower of record', + }) + linked_record.message_subscribe(partner_ids=follower_partner.ids) + test_partner = self.test_partner.with_env(self.env) + + # standard test, no multi-email, to assert base behavior + sources = [(self._test_email, True), (self._test_email, False),] + expected = [follower_partner, test_partner] + for (source, follower_check), expected in zip(sources, expected): + with self.subTest(source=source, follower_check=follower_check): + partner = self.env['res.partner']._mail_find_partner_from_emails( + [source], records=linked_record if follower_check else None + )[0] + self.assertEqual(partner, expected) + + # formatted email + encapsulated_test_email = f'"Robert Astaire" <{self._test_email}>' + (follower_partner + test_partner).sudo().write({'email': encapsulated_test_email}) + sources = [ + (self._test_email, True), # normalized + (self._test_email, False), # normalized + (encapsulated_test_email, True), # encapsulated, same + (encapsulated_test_email, False), # encapsulated, same + (f'"AnotherName" <{self._test_email}', True), # same normalized, other name + (f'"AnotherName" <{self._test_email}', False), # same normalized, other name + ] + expected = [follower_partner, test_partner, + follower_partner, test_partner, + follower_partner, test_partner, + follower_partner, test_partner] + for (source, follower_check), expected in zip(sources, expected): + with self.subTest(source=source, follower_check=follower_check): + partner = self.env['res.partner']._mail_find_partner_from_emails( + [source], records=linked_record if follower_check else None + )[0] + self.assertEqual(partner, expected, + 'Mail: formatted email is recognized through usage of normalized email') + + # multi-email + _test_email_2 = '"Robert Astaire" <not.alfredoastaire@test.example.com>' + (follower_partner + test_partner).sudo().write({'email': f'{self._test_email}, {_test_email_2}'}) + sources = [ + (self._test_email, True), # first email + (self._test_email, False), # first email + (_test_email_2, True), # second email + (_test_email_2, False), # second email + ('not.alfredoastaire@test.example.com', True), # normalized second email in field + ('not.alfredoastaire@test.example.com', False), # normalized second email in field + (f'{self._test_email}, {_test_email_2}', True), # multi-email, both matching, depends on comparison + (f'{self._test_email}, {_test_email_2}', False) # multi-email, both matching, depends on comparison + ] + expected = [self.env['res.partner'], self.env['res.partner'], + self.env['res.partner'], self.env['res.partner'], + self.env['res.partner'], self.env['res.partner'], + self.env['res.partner'], self.env['res.partner']] + for (source, follower_check), expected in zip(sources, expected): + with self.subTest(source=source, follower_check=follower_check): + partner = self.env['res.partner']._mail_find_partner_from_emails( + [source], records=linked_record if follower_check else None + )[0] + self.assertEqual(partner, expected, + 'Mail (FIXME): not recognized due to usage of email_normalize that does not accept multi emails') diff --git a/addons/test_mail/tests/test_message_post.py b/addons/test_mail/tests/test_message_post.py index 14673a99ca944fa99b4dca88365ad7c72dfd8e6f..d2b450100a9c7cdee23e3cf8e75f02bbe5d07a07 100644 --- a/addons/test_mail/tests/test_message_post.py +++ b/addons/test_mail/tests/test_message_post.py @@ -37,12 +37,13 @@ class TestMessagePost(TestMailCommon, TestRecipients): self.assertTrue(isinstance(messageId, int)) @users('employee') + @mute_logger('odoo.addons.mail.models.mail_mail') def test_notify_mail_add_signature(self): - self.test_track = self.env['mail.test.track'].with_context(self._test_context).with_user(self.user_employee).create({ + test_track = self.env['mail.test.track'].with_context(self._test_context).with_user(self.user_employee).create({ 'name': 'Test', 'email_from': 'ignasse@example.com' }) - self.test_track.user_id = self.env.user + test_track.user_id = self.env.user signature = self.env.user.signature @@ -50,13 +51,13 @@ class TestMessagePost(TestMailCommon, TestRecipients): self.assertIn("record.user_id.sudo().signature", template.arch) with self.mock_mail_gateway(): - self.test_track.message_post(body="Test body", mail_auto_delete=False, add_sign=True, partner_ids=[self.partner_1.id, self.partner_2.id], email_layout_xmlid="mail.mail_notification_paynow") + test_track.message_post(body="Test body", mail_auto_delete=False, add_sign=True, partner_ids=[self.partner_1.id, self.partner_2.id], email_layout_xmlid="mail.mail_notification_paynow") found_mail = self._new_mails self.assertIn(signature, found_mail.body_html) self.assertEqual(found_mail.body_html.count(signature), 1) with self.mock_mail_gateway(): - self.test_track.message_post(body="Test body", mail_auto_delete=False, add_sign=False, partner_ids=[self.partner_1.id, self.partner_2.id], email_layout_xmlid="mail.mail_notification_paynow") + test_track.message_post(body="Test body", mail_auto_delete=False, add_sign=False, partner_ids=[self.partner_1.id, self.partner_2.id], email_layout_xmlid="mail.mail_notification_paynow") found_mail = self._new_mails self.assertNotIn(signature, found_mail.body_html) self.assertEqual(found_mail.body_html.count(signature), 0) @@ -203,7 +204,7 @@ class TestMessagePost(TestMailCommon, TestRecipients): self.test_record.message_post( body='Test', message_type='comment', subtype_xmlid='mail.mt_comment') - @mute_logger('odoo.addons.mail.models.mail_mail') + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.tests') def test_post_notifications(self): _body, _subject = '<p>Test Body</p>', 'Test Subject' @@ -238,6 +239,51 @@ class TestMessagePost(TestMailCommon, TestRecipients): self.assertFalse(copy.notified_partner_ids) @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.tests') + def test_post_notifications_email_field(self): + """ Test various combinations of corner case / not standard filling of + email fields: multi email, formatted emails, ... """ + partner_emails = [ + 'valid.lelitre@agrolait.com, valid.lelitre.cc@agrolait.com', # multi email + '"Valid Lelitre" <valid.lelitre@agrolait.com>', # email contains formatted email + 'wrong', # wrong + False, '', ' ', # falsy + ] + expected_tos = [ + # Currently semi-broken: sending to an incorrect email "Name" <address1, address2> + [f'"{self.partner_1.name}" <valid.lelitre@agrolait.com, valid.lelitre.cc@agrolait.com>',], + # Currently broken: sending to an incorrect email "Name" <"Name" <address>> + [f'"{self.partner_1.name}" <"Valid Lelitre" <valid.lelitre@agrolait.com>>',], + # sent "normally": formats email based on wrong / falsy email + [f'"{self.partner_1.name}" <@wrong>',], + [f'"{self.partner_1.name}" <@False>',], + [f'"{self.partner_1.name}" <@False>',], + [f'"{self.partner_1.name}" <@ >',], + ] + + for partner_email, expected_to in zip(partner_emails, expected_tos): + with self.subTest(partner_email=partner_email, expected_to=expected_to): + self.partner_1.write({'email': partner_email}) + with self.mock_mail_gateway(): + self.test_record.with_user(self.user_employee).message_post( + body='Test multi email', + message_type='comment', + partner_ids=[self.partner_1.id], + subject='Exotic email', + subtype_xmlid='mt_comment', + ) + + self.assertSentEmail( + self.user_employee.partner_id, + [self.partner_1], + email_to=expected_to, + ) + + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + def test_post_notifications_emails_tweak(self): + pass + # we should check _notification_groups behavior, for emails and buttons + + @mute_logger('odoo.addons.mail.models.mail_mail') def test_post_notifications_keep_emails(self): self.test_record.message_subscribe(partner_ids=[self.user_admin.partner_id.id]) @@ -251,11 +297,6 @@ class TestMessagePost(TestMailCommon, TestRecipients): # notifications emails should not have been deleted: one for customers, one for user self.assertEqual(len(self.env['mail.mail'].sudo().search([('mail_message_id', '=', msg.id)])), 2) - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_post_notifications_emails_tweak(self): - pass - # we should check _notification_groups behavior, for emails and buttons - @mute_logger('odoo.addons.mail.models.mail_mail') def test_post_attachments(self): _attachments = [ @@ -357,7 +398,7 @@ class TestMessagePost(TestMailCommon, TestRecipients): references='%s %s' % (parent_msg.message_id, new_msg.message_id), ) - @mute_logger('odoo.addons.mail.models.mail_mail') + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.tests') def test_post_email_with_multiline_subject(self): _body, _body_alt, _subject = '<p>Test Body</p>', 'Test Body', '1st line\n2nd line' msg = self.test_record.with_user(self.user_employee).message_post( diff --git a/addons/test_mass_mailing/models/mailing_models.py b/addons/test_mass_mailing/models/mailing_models.py index 54ede3f8e14adc7de1d2d09fbe9f5f964a840660..8748f4792110b56443436d88d97a1bf577654ae5 100644 --- a/addons/test_mass_mailing/models/mailing_models.py +++ b/addons/test_mass_mailing/models/mailing_models.py @@ -4,9 +4,39 @@ from odoo import api, fields, models +class MailingCustomer(models.Model): + """ A model inheriting from mail.thread with a partner field, to test + mass mailing flows involving checking partner email. """ + _description = 'Mailing with partner' + _name = 'mailing.test.customer' + _inherit = ['mail.thread'] + + name = fields.Char() + email_from = fields.Char(compute='_compute_email_from', readonly=False, store=True) + customer_id = fields.Many2one('res.partner', 'Customer', tracking=True) + + @api.depends('customer_id') + def _compute_email_from(self): + for mailing in self.filtered(lambda rec: not rec.email_from and rec.customer_id): + mailing.email_from = mailing.customer_id.email + + def _message_get_default_recipients(self): + """ Default recipient checks for 'partner_id', here the field is named + 'customer_id'. """ + default_recipients = super()._message_get_default_recipients() + for record in self: + if record.customer_id: + default_recipients[record.id] = { + 'email_cc': False, + 'email_to': False, + 'partner_ids': record.customer_id.ids, + } + return default_recipients + + class MailingSimple(models.Model): - """ A very simple model only inheriting from mail.thread to test pure mass - mailing features and base performances. """ + """ Model only inheriting from mail.thread to test base mailing features and + performances. """ _description = 'Simple Mailing' _name = 'mailing.test.simple' _inherit = ['mail.thread'] @@ -16,7 +46,8 @@ class MailingSimple(models.Model): class MailingUTM(models.Model): - """ Model inheriting from mail.thread and utm.mixin for checking utm of mailing is caught and set on reply """ + """ Model inheriting from mail.thread and utm.mixin for checking utm of mailing + is caught and set on reply """ _description = 'Mailing: UTM enabled to test UTM sync with mailing' _name = 'mailing.test.utm' _inherit = ['mail.thread', 'utm.mixin'] @@ -36,6 +67,19 @@ class MailingBLacklist(models.Model): customer_id = fields.Many2one('res.partner', 'Customer', tracking=True) user_id = fields.Many2one('res.users', 'Responsible', tracking=True) + def _message_get_default_recipients(self): + """ Default recipient checks for 'partner_id', here the field is named + 'customer_id'. """ + default_recipients = super()._message_get_default_recipients() + for record in self: + if record.customer_id: + default_recipients[record.id] = { + 'email_cc': False, + 'email_to': False, + 'partner_ids': record.customer_id.ids, + } + return default_recipients + class MailingOptOut(models.Model): """ Model using blacklist mechanism and a hijacked opt-out mechanism for @@ -51,6 +95,19 @@ class MailingOptOut(models.Model): customer_id = fields.Many2one('res.partner', 'Customer', tracking=True) user_id = fields.Many2one('res.users', 'Responsible', tracking=True) + def _message_get_default_recipients(self): + """ Default recipient checks for 'partner_id', here the field is named + 'customer_id'. """ + default_recipients = super()._message_get_default_recipients() + for record in self: + if record.customer_id: + default_recipients[record.id] = { + 'email_cc': False, + 'email_to': False, + 'partner_ids': record.customer_id.ids, + } + return default_recipients + class MailingPerformance(models.Model): """ A very simple model only inheriting from mail.thread to test pure mass diff --git a/addons/test_mass_mailing/security/ir.model.access.csv b/addons/test_mass_mailing/security/ir.model.access.csv index b21c7be49b14a3b910c38d8686f4e2c8e086c056..617995ff0fac1bfeb8130746c698e825449ccf96 100644 --- a/addons/test_mass_mailing/security/ir.model.access.csv +++ b/addons/test_mass_mailing/security/ir.model.access.csv @@ -1,4 +1,6 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_mailing_test_customer_all,access.mailing.test.customer.all,model_mailing_test_customer,,0,0,0,0 +access_mailing_test_customer_user,access.mailing.test.customer.user,model_mailing_test_customer,base.group_user,1,1,1,1 access_mailing_test_simple_all,access.mailing.test.simple.all,model_mailing_test_simple,,0,0,0,0 access_mailing_test_simple_user,access.mailing.test.simple.user,model_mailing_test_simple,base.group_user,1,1,1,1 access_mailing_test_blacklist_all,access.mailing.test.blacklist.all,model_mailing_test_blacklist,,0,0,0,0 diff --git a/addons/test_mass_mailing/tests/test_mailing.py b/addons/test_mass_mailing/tests/test_mailing.py index 41fc0f1df3fea14aa5deda5b6262d5fa1a1999ce..83c6bb2838043272e3e919b7ab4d74988b92a299 100644 --- a/addons/test_mass_mailing/tests/test_mailing.py +++ b/addons/test_mass_mailing/tests/test_mailing.py @@ -119,6 +119,134 @@ class TestMassMailing(TestMassMailCommon): self.assertMailingStatistics(mailing, expected=5, delivered=4, sent=5, opened=1, clicked=1, bounced=1) self.assertEqual(recipients[1].message_bounce, 1) + @users('user_marketing') + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_mailing_recipients(self): + """ Test recipient-specific computation, with email, formatting, + multi-emails, ... to test corner cases. Blacklist mixin impact is + tested. """ + (customer_mult, customer_fmt, customer_unic, + customer_case, customer_weird, customer_weird_2 + ) = self.env['res.partner'].create([ + { + 'email': 'customer.multi.1@example.com, "Test Multi 2" <customer.multi.2@example.com>', + 'name': 'MultiEMail', + }, { + 'email': '"Formatted Customer" <test.customer.format@example.com>', + 'name': 'FormattedEmail', + }, { + 'email': '"Unicode Customer" <test.customer.😊@example.com>', + 'name': 'UnicodeEmail', + }, { + 'email': 'TEST.CUSTOMER.CASE@EXAMPLE.COM', + 'name': 'CaseEmail', + }, { + 'email': 'test.customer.weird@example.com Weird Format', + 'name': 'WeirdFormatEmail', + }, { + 'email': 'Weird Format2 test.customer.weird.2@example.com', + 'name': 'WeirdFormatEmail2', + } + ]) + + # check difference of email management between a classic model and a model + # with an 'email_normalized' field (blacklist mixin) + for dst_model in ['mailing.test.customer', 'mailing.test.blacklist']: + with self.subTest(dst_model=dst_model): + (record_p_mult, record_p_fmt, record_p_unic, + record_p_case, record_p_weird, record_p_weird_2, + record_mult, record_fmt, record_unic + ) = self.env[dst_model].create([ + { + 'customer_id': customer_mult.id, + }, { + 'customer_id': customer_fmt.id, + }, { + 'customer_id': customer_unic.id, + }, { + 'customer_id': customer_case.id, + }, { + 'customer_id': customer_weird.id, + }, { + 'customer_id': customer_weird_2.id, + }, { + 'email_from': 'record.multi.1@example.com, "Record Multi 2" <record.multi.2@example.com>', + }, { + 'email_from': '"Formatted Record" <record.format@example.com>', + }, { + 'email_from': '"Unicode Record" <record.😊@example.com>', + } + ]) + test_records = ( + record_p_mult + record_p_fmt + record_p_unic + + record_p_case + record_p_weird + record_p_weird_2 + + record_mult + record_fmt + record_unic + ) + mailing = self.env['mailing.mailing'].create({ + 'body_html': """<div><p>Hello ${object.name}</p>""", + 'mailing_domain': [('id', 'in', test_records.ids)], + 'mailing_model_id': self.env['ir.model']._get_id(dst_model), + 'mailing_type': 'mail', + 'name': 'SourceName', + 'preview': 'Hi ${object.name} :)', + 'reply_to_mode': 'thread', + 'subject': 'MailingSubject', + }) + + with self.mock_mail_gateway(mail_unlink_sent=False): + mailing.action_send_mail() + + # difference in email_to management when inheriting from blacklist mixin + # as it uses email_normalized if possible + if dst_model == 'mailing.test.customer': + formatted_mailmail_email = '"Formatted Record" <record.format@example.com>' + unicode_mailmail_email = '"Unicode Record" <record.😊@example.com>' + else: + formatted_mailmail_email = 'record.format@example.com' + unicode_mailmail_email = 'record.😊@example.com' + self.assertMailTraces( + [ + {'email': False, + 'failure_type': False, + 'partner': customer_mult, + 'state': 'ignored'}, + {'email': 'test.customer.format@example.com', + 'failure_type': False, + 'partner': customer_fmt, + 'state': 'sent'}, + {'email': 'test.customer.😊@example.com', + 'failure_type': False, + 'partner': customer_unic, + 'state': 'ignored'}, # email_re usage forbids mailing to unicode + {'email': 'test.customer.case@example.com', + 'failure_type': False, + 'partner': customer_case, + 'state': 'sent'}, # lower cased + {'email': 'test.customer.weird@example.comweirdformat', + 'failure_type': False, + 'partner': customer_weird, + 'state': 'sent'}, # concatenates everything after domain + {'email': 'weird format2 test.customer.weird.2@example.com', + 'failure_type': False, + 'partner': customer_weird_2, + 'state': 'sent'}, + {'email': 'record.multi.1@example.com, "Record Multi 2" <record.multi.2@example.com>', + 'failure_type': False, + 'state': 'ignored'}, + {'email': 'record.format@example.com', + 'email_to_mail': formatted_mailmail_email, + 'failure_type': False, + 'state': 'sent'}, + {'email': 'record.😊@example.com', + 'email_to_mail': unicode_mailmail_email, + 'failure_type': False, + 'state': 'ignored'}, # email_re usage forbids mailing to unicode + ], + mailing, + test_records, + check_mail=True, + ) + @users('user_marketing') @mute_logger('odoo.addons.mail.models.mail_mail') def test_mailing_reply_to_mode_new(self): diff --git a/odoo/addons/base/tests/test_base.py b/odoo/addons/base/tests/test_base.py index f28d1f7c2e5c2c08f3602f9ba102121494692cb2..67dde0af0af1cf2b474187fa90c7a37da70e7877 100644 --- a/odoo/addons/base/tests/test_base.py +++ b/odoo/addons/base/tests/test_base.py @@ -5,6 +5,7 @@ import ast from odoo import SUPERUSER_ID from odoo.exceptions import UserError, ValidationError +from odoo.tests import tagged from odoo.tests.common import TransactionCase, BaseCase from odoo.tools import mute_logger from odoo.tools.safe_eval import safe_eval, const_eval, expr_eval @@ -73,6 +74,7 @@ SAMPLES = [ ] +@tagged('res_partner') class TestBase(TransactionCase): def _check_find_or_create(self, test_string, expected_name, expected_email, check_partner=False, should_create=False): @@ -88,12 +90,13 @@ class TestBase(TransactionCase): def test_00_res_partner_name_create(self): res_partner = self.env['res.partner'] parse = res_partner._parse_partner_name - for text, name, mail in SAMPLES: - self.assertEqual((name, mail.lower()), parse(text)) - partner_id, dummy = res_partner.name_create(text) - partner = res_partner.browse(partner_id) - self.assertEqual(name or mail.lower(), partner.name) - self.assertEqual(mail.lower() or False, partner.email) + for text, expected_name, expected_mail in SAMPLES: + with self.subTest(text=text): + self.assertEqual((expected_name, expected_mail.lower()), parse(text)) + partner_id, dummy = res_partner.name_create(text) + partner = res_partner.browse(partner_id) + self.assertEqual(expected_name or expected_mail.lower(), partner.name) + self.assertEqual(expected_mail.lower() or False, partner.email) # name_create supports default_email fallback partner = self.env['res.partner'].browse( diff --git a/odoo/addons/base/tests/test_mail.py b/odoo/addons/base/tests/test_mail.py index 01009f5d3c3b015e708a98295b42e5e793d7a212..7ed4164e36e8405ffc95928dbb4c672742040f6b 100644 --- a/odoo/addons/base/tests/test_mail.py +++ b/odoo/addons/base/tests/test_mail.py @@ -8,10 +8,11 @@ import email.message import re import threading +from odoo.tests import tagged from odoo.tests.common import BaseCase, SavepointCase, TransactionCase from odoo.tools import ( is_html_empty, html_sanitize, append_content_to_html, plaintext2html, - email_split, + email_normalize, email_split, email_split_and_format, misc, formataddr, prepend_html_content, ) @@ -388,10 +389,40 @@ class TestHtmlTools(BaseCase): self.assertEqual(result, "<html><body><div>test</div><div>test</div></body></html>") +@tagged('mail_tools') class TestEmailTools(BaseCase): """ Test some of our generic utility functions for emails """ + def test_email_normalize(self): + """ 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. """ + sources = [ + '"Super Déboulonneur" <deboulonneur@example.com>', # formatted + 'Déboulonneur deboulonneur@example.com', # wrong formatting + 'deboulonneur@example.com Déboulonneur', # wrong formatting (happens, alas) + '"Super Déboulonneur" <DEBOULONNEUR@example.com>, "Super Déboulonneur 2" <deboulonneur2@EXAMPLE.com>', # multi + case + ' Déboulonneur deboulonneur@example.com déboulonneur deboulonneur2@example.com', # wrong formatting + wrong multi + '"Déboulonneur 😊" <deboulonneur.😊@example.com>', # unicode in name and email left-part + '"Déboulonneur" <déboulonneur@examplé.com>', # utf-8 + '"Déboulonneur" <DéBoulonneur@Examplé.com>', # utf-8 + ] + expected_list = [ + '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', + ] + for source, expected in zip(sources, expected_list): + with self.subTest(source=source): + self.assertEqual(email_normalize(source), expected) + def test_email_split(self): + """ Test 'email_split' """ cases = [ ("John <12345@gmail.com>", ['12345@gmail.com']), # regular form ("d@x; 1@2", ['d@x', '1@2']), # semi-colon + extra space @@ -402,6 +433,51 @@ class TestEmailTools(BaseCase): for text, expected in cases: self.assertEqual(email_split(text), expected, 'email_split is broken') + def test_email_split_and_format(self): + """ Test 'email_split_and_format', notably in case of multi encapsulation + or multi emails. """ + sources = [ + 'deboulonneur@example.com', + '"Super Déboulonneur" <deboulonneur@example.com>', # formatted + # wrong formatting + 'Déboulonneur <deboulonneur@example.com', # with a final typo + 'Déboulonneur deboulonneur@example.com', # wrong formatting + 'deboulonneur@example.com Déboulonneur', # wrong formatting (happens, alas) + # multi + 'Déboulonneur, deboulonneur@example.com', # multi-like with errors + 'deboulonneur@example.com, deboulonneur2@example.com', # multi + ' Déboulonneur deboulonneur@example.com déboulonneur deboulonneur2@example.com', # wrong formatting + wrong multi + # format / misc + '"Déboulonneur" <"Déboulonneur Encapsulated" <deboulonneur@example.com>>', # double formatting + '"Super Déboulonneur" <deboulonneur@example.com>, "Super Déboulonneur 2" <deboulonneur2@example.com>', + '"Super Déboulonneur" <deboulonneur@example.com>, wrong, ', + '"Déboulonneur 😊" <deboulonneur@example.com>', # unicode in name + '"Déboulonneur 😊" <deboulonneur.😊@example.com>', # unicode in name and email left-part + '"Déboulonneur" <déboulonneur@examplé.com>', # utf-8 + ] + expected_list = [ + ['deboulonneur@example.com'], + ['"Super Déboulonneur" <deboulonneur@example.com>'], + # wrong formatting + ['"Déboulonneur" <deboulonneur@example.com>'], + ['Déboulonneur deboulonneur@example.com'], # returned as it, a bit strange but hey + ['deboulonneur@example.comDéboulonneur'], # concatenated, not sure why + # multi + ['deboulonneur@example.com'], + ['deboulonneur@example.com', 'deboulonneur2@example.com'], + ['@example.com'], # funny one + # format / misc + ['deboulonneur@example.com'], + ['"Super Déboulonneur" <deboulonneur@example.com>', '"Super Déboulonneur 2" <deboulonneur2@example.com>'], + ['"Super Déboulonneur" <deboulonneur@example.com>'], + ['"Déboulonneur 😊" <deboulonneur@example.com>'], + ['"Déboulonneur 😊" <deboulonneur.😊@example.com>'], + ['"Déboulonneur" <déboulonneur@examplé.com>'], + ] + for source, expected in zip(sources, expected_list): + with self.subTest(source=source): + self.assertEqual(email_split_and_format(source), expected) + def test_email_formataddr(self): email = 'joe@example.com' email_idna = 'joe@examplé.com' diff --git a/odoo/addons/base/tests/test_res_partner.py b/odoo/addons/base/tests/test_res_partner.py index 28e6f172f9565a9d010bd9fb6201bb7591fffc5d..2ec3458de8034a87fb15f940f18ed5ca94b89165 100644 --- a/odoo/addons/base/tests/test_res_partner.py +++ b/odoo/addons/base/tests/test_res_partner.py @@ -4,10 +4,89 @@ from odoo.tests import Form from odoo.tests.common import TransactionCase from odoo.exceptions import AccessError, UserError +from odoo.tests import tagged +@tagged('res_partner') class TestPartner(TransactionCase): + def test_email_formatted(self): + """ Test various combinations of name / email, notably to check result + of email_formatted field. """ + # multi create + new_partners = self.env['res.partner'].create([{ + 'name': "Vlad the Impaler", + 'email': f'vlad.the.impaler.{idx:02d}@example.com', + } for idx in range(2)]) + self.assertEqual( + sorted(new_partners.mapped('email_formatted')), + sorted([f'"Vlad the Impaler" <vlad.the.impaler.{idx:02d}@example.com>' for idx in range(2)]), + 'Email formatted should be "name" <email>' + ) + + # test name_create with formatting / multi emails + for source, (exp_name, exp_email, exp_email_formatted) in [ + ( + 'Balázs <vlad.the.negociator@example.com>, vlad.the.impaler@example.com', + ("Balázs", "vlad.the.negociator@example.com", '"Balázs" <vlad.the.negociator@example.com>') + ), + ( + 'Balázs <vlad.the.impaler@example.com>', + ("Balázs", "vlad.the.impaler@example.com", '"Balázs" <vlad.the.impaler@example.com>') + ), + ]: + with self.subTest(source=source): + new_partner_id = self.env['res.partner'].name_create(source)[0] + new_partner = self.env['res.partner'].browse(new_partner_id) + self.assertEqual(new_partner.name, exp_name) + self.assertEqual(new_partner.email, exp_email) + self.assertEqual( + new_partner.email_formatted, exp_email_formatted, + 'Name_create should take first found email' + ) + + # check name updates + for source, exp_email_formatted in [ + ('Vlad the Impaler', '"Vlad the Impaler" <vlad.the.impaler@example.com>'), + ('Balázs', '"Balázs" <vlad.the.impaler@example.com>'), + ('Balázs <email.in.name@example.com>', '"Balázs <email.in.name@example.com>" <vlad.the.impaler@example.com>'), + ]: + with self.subTest(source=source): + new_partner.write({'name': source}) + self.assertEqual(new_partner.email_formatted, exp_email_formatted) + + # check email updates + new_partner.write({'name': 'Balázs'}) + for source, exp_email_formatted in [ + # encapsulated email + ( + "Vlad the Impaler <vlad.the.impaler@example.com>", + '"Balázs" <Vlad the Impaler <vlad.the.impaler@example.com>>' + ), ( + '"Balázs" <balazs@adam.hu>', + '"Balázs" <"Balázs" <balazs@adam.hu>>' + ), + # multi email + ( + "vlad.the.impaler@example.com, vlad.the.dragon@example.com", + '"Balázs" <vlad.the.impaler@example.com, vlad.the.dragon@example.com>' + ), ( + "vlad.the.impaler.com, vlad.the.dragon@example.com", + '"Balázs" <vlad.the.impaler.com, vlad.the.dragon@example.com>' + ), ( + 'vlad.the.impaler.com, "Vlad the Dragon" <vlad.the.dragon@example.com>', + '"Balázs" <vlad.the.impaler.com, "Vlad the Dragon" <vlad.the.dragon@example.com>>' + ), + # falsy emails + (False, ''), + ('', ''), + (' ', '"Balázs" <@ >'), + ('notanemail', '"Balázs" <@notanemail>'), + ]: + with self.subTest(source=source): + new_partner.write({'email': source}) + self.assertEqual(new_partner.email_formatted, exp_email_formatted) + def test_name_search(self): """ Check name_search on partner, especially with domain based on auto_join user_ids field. Check specific SQL of name_search correctly handle joined tables. """