diff --git a/addons/mail/tests/common.py b/addons/mail/tests/common.py index 9d6ccbabe07..59c0df79f3f 100644 --- a/addons/mail/tests/common.py +++ b/addons/mail/tests/common.py @@ -104,7 +104,7 @@ class MockEmail(common.BaseCase, MockSmtplibCase): cls.env['ir.config_parameter'].set_param('mail.catchall.domain', cls.alias_domain) cls.env['ir.config_parameter'].set_param('mail.catchall.alias', cls.alias_catchall) cls.env['ir.config_parameter'].set_param('mail.default.from', cls.default_from) - cls.env['ir.config_parameter'].sudo().set_param('mail.default.from_filter', cls.default_from_filter) + cls.env['ir.config_parameter'].set_param('mail.default.from_filter', cls.default_from_filter) # mailer daemon email preformatting cls.mailer_daemon_email = formataddr(('MAILER-DAEMON', f'{cls.alias_bounce}@{cls.alias_domain}')) diff --git a/addons/mail/tests/test_ir_mail_server.py b/addons/mail/tests/test_ir_mail_server.py index ffc1324eda6..9fbd71f6d27 100644 --- a/addons/mail/tests/test_ir_mail_server.py +++ b/addons/mail/tests/test_ir_mail_server.py @@ -91,8 +91,8 @@ class TestIrMailServer(MailCommon): # outside "from_filter" domain: we will use notifications emails in the # headers, and bounce address in the envelope because the "from_filter" # allows to use the entire domain - (self.default_bounce_address, '"test" '), - (self.default_bounce_address, '"Formatted Name" '), + (self.default_bounce_address, f'"test" <{self.default_from}@{self.alias_domain}>'), + (self.default_bounce_address, f'"Formatted Name" <{self.default_from}@{self.alias_domain}>'), ] ): for provide_smtp in [False, True]: # providing smtp session should ont impact test @@ -146,7 +146,8 @@ class TestIrMailServer(MailCommon): ('notifications', 'example_2.com, example_3.com'), ('notifications@example.com', 'dummy.com, full_email@example_2.com, dummy2.com'), ('notifications', 'dummy.com, full_email@example_2.com, dummy2.com'), - ('notifications@example.com', 'example.com'), + (f'notifications@{self.alias_domain}', f'{self.alias_domain}'), + (f'notifications@{self.alias_domain}', f'{self.alias_domain}, example_2.com'), # default relies on "odoo" (False, 'example.com'), # fallback on user email if no from_filter @@ -159,7 +160,8 @@ class TestIrMailServer(MailCommon): 'notifications@example_2.com', 'full_email@example_2.com', 'full_email@example_2.com', - 'notifications@example.com', + f'notifications@{self.alias_domain}', + f'notifications@{self.alias_domain}', 'odoo@example.com', self.env.user.email, self.env.user.email, @@ -219,7 +221,7 @@ class TestIrMailServer(MailCommon): (self.mail_server_domain, 'unknown_email@test.mycompany.com'), (self.mail_server_domain, 'unknown_email@TEST.MYCOMPANY.COM'), (self.mail_server_domain, '"Unknown" '), - (self.mail_server_notification, 'notifications.test@test.mycompany.com'), + (self.mail_server_notification, f'{self.default_from}@test.mycompany.com'), # mail_server_user multiple from_filter check (self.mail_server_user, '"Example" '), (self.mail_server_user, '"Example" '), @@ -247,9 +249,9 @@ class TestIrMailServer(MailCommon): ('specific_user@test.mycompany.com', 'specific_user@test.mycompany.com', self.mail_server_user), # No mail server are configured for the email address, so it will use the # notifications email instead and encapsulate the old email - ('notifications.test@test.mycompany.com', '"Name" ', self.mail_server_notification), + (f'{self.default_from}@{self.alias_domain}', f'"Name" <{self.default_from}@{self.alias_domain}>', self.mail_server_notification), # same situation, but the original email has no name part - ('notifications.test@test.mycompany.com', '"test" ', self.mail_server_notification), + (f'{self.default_from}@{self.alias_domain}', f'"test" <{self.default_from}@{self.alias_domain}>', self.mail_server_notification), # A mail server is configured for the entire domain name, so we can use the bounce # email address because the mail server supports it (self.default_bounce_address, '"Name" ', self.mail_server_domain), @@ -294,7 +296,7 @@ class TestIrMailServer(MailCommon): self.assertEqual(len(self.emails), 1) self.assertSMTPEmailsSent( smtp_from=self.default_bounce_address, - message_from='"Name" ', + message_from=f'"Name" <{self.default_from}@{self.alias_domain}>', mail_server=self.mail_server_domain, ) diff --git a/addons/test_mail/tests/test_mail_composer.py b/addons/test_mail/tests/test_mail_composer.py index d81162fdab4..12afea1728d 100644 --- a/addons/test_mail/tests/test_mail_composer.py +++ b/addons/test_mail/tests/test_mail_composer.py @@ -56,32 +56,32 @@ class TestMailComposer(MailCommon, TestRecipients): ) cls.env.ref('mail.group_mail_template_editor').users -= cls.user_rendering_restricted - cls.test_record = cls.env['mail.test.ticket'].with_context(cls._test_context).create({ + cls.test_record = cls.env['mail.test.ticket.mc'].with_context(cls._test_context).create({ 'name': 'TestRecord', 'customer_id': cls.partner_1.id, 'user_id': cls.user_employee_2.id, }) cls.test_records, cls.test_partners = cls._create_records_for_batch( - 'mail.test.ticket', 2, + 'mail.test.ticket.mc', 2, additional_values={'user_id': cls.user_employee_2.id}, ) cls.test_report, cls.test_report_2, cls.test_report_3 = cls.env['ir.actions.report'].create([ { 'name': 'Test Report on Mail Test Ticket', - 'model': 'mail.test.ticket', + 'model': 'mail.test.ticket.mc', 'print_report_name': "'TestReport for %s' % object.name", 'report_type': 'qweb-pdf', 'report_name': 'test_mail.mail_test_ticket_test_template', }, { 'name': 'Test Report 2 on Mail Test Ticket', - 'model': 'mail.test.ticket', + 'model': 'mail.test.ticket.mc', 'print_report_name': "'TestReport2 for %s' % object.name", 'report_type': 'qweb-pdf', 'report_name': 'test_mail.mail_test_ticket_test_template_2', }, { 'name': 'Test Report 3 with variable data on Mail Test Ticket', - 'model': 'mail.test.ticket', + 'model': 'mail.test.ticket.mc', 'print_report_name': "'TestReport3 for %s' % object.name", 'report_type': 'qweb-pdf', 'report_name': 'test_mail.mail_test_ticket_test_variable_template', @@ -100,7 +100,7 @@ class TestMailComposer(MailCommon, TestRecipients): 'email_from': '{{ (object.user_id.email_formatted or user.email_formatted) }}', 'lang': '{{ object.customer_id.lang }}', 'mail_server_id': cls.mail_server_domain.id, - 'model_id': cls.env['ir.model']._get('mail.test.ticket').id, + 'model_id': cls.env['ir.model']._get('mail.test.ticket.mc').id, 'reply_to': '{{ ctx.get("custom_reply_to") or "info@test.example.com" }}', 'scheduled_date': '{{ (object.create_date or datetime.datetime(2022, 12, 26, 18, 0, 0)) + datetime.timedelta(days=2) }}', }) @@ -332,7 +332,7 @@ class TestComposerForm(TestMailComposer): when nothing is given as context. """ composer_form = Form(self.env['mail.compose.message'].with_context( default_composition_mode='comment', - default_model='mail.test.ticket', + default_model='mail.test.ticket.mc', default_template_id=self.template.id, )) self.assertTrue(composer_form.auto_delete, 'Should take composer value') @@ -454,7 +454,7 @@ class TestComposerForm(TestMailComposer): when nothing is given as context. """ composer_form = Form(self.env['mail.compose.message'].with_context( default_composition_mode='mass_mail', - default_model='mail.test.ticket', + default_model='mail.test.ticket.mc', default_template_id=self.template.id, )) self.assertTrue(composer_form.auto_delete, 'Should take composer value') @@ -1267,6 +1267,7 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase): self.assertEqual(self.test_records.message_partner_ids, self.partner_employee_2) self.assertEqual(self.test_records[0].customer_id.lang, 'en_US') self.assertEqual(self.test_records[1].customer_id.lang, 'en_US') + self.assertEqual(self.test_records.company_id, self.company_admin) self.assertEqual(len(self.test_partners), 2) @@ -1426,6 +1427,88 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase): self.assertEqual(message.subtype_id, self.env.ref('mail.mt_comment')) self.assertEqual(message.partner_ids, self.partner_1 | self.partner_2) + @users('employee') + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_mail_composer_server_config(self): + """ Test various configuration to check behavior of outgoing mail + servers, notifications, .... """ + # add access to second company to avoid MC rules on ticket model + self.env.user.company_ids = [(4, self.company_2.id)] + + # initial data + self.assertEqual(self.env.company, self.company_admin) + self.assertEqual(self.user_admin.company_id, self.company_admin) + + # update test configuration + test_records = self.test_records + test_companies = self.company_admin + self.company_2 + for company, record in zip(test_companies, test_records): + record.company_id = company.id + + # various from / servers configuration + self.template.write({'mail_server_id': False}) # allow server archive + server_other = self.env['ir.mail_server'].sudo().create({ + 'name': 'Server Other', + 'from_filter': 'test.othercompany.com', + 'sequence': 4, + 'smtp_encryption': 'none', + 'smtp_host': 'smtp_host', + }) + servers_all = self.mail_servers + server_other + for (emails_from, servers_active, mail_config), (exp_smtp_from_lst, exp_msg_from_lst, exp_from_filter) in zip( + [ + ( + [f'user.from@{self.alias_domain}', 'user@other.domain.com'], + self.env['ir.mail_server'], + {'default_from': f'notifications@{self.alias_domain}', 'from_filter': self.alias_domain} + ), # odoo-style configuration + ], [ + ( + [f'{self.alias_bounce}@{self.alias_domain}', f'{self.alias_bounce}@{self.alias_domain}'], + [f'"{self.env.user.name}" ', f'"{self.env.user.name}" '], + self.alias_domain + ), # no spoof + ], + ): + with self.subTest(emails_from=emails_from, + servers_active=servers_active): + # update servers + servers_all.active = False + if servers_active: + servers_active.active = True + # update mail config + default_from = mail_config.get('default_from', self.default_from) + from_filter = mail_config.get('from_filter', self.default_from_filter) + self.env['ir.config_parameter'].sudo().set_param('mail.default.from', default_from) + self.env['ir.config_parameter'].sudo().set_param('mail.default.from_filter', from_filter) + + for email_from, exp_smtp_from, exp_msg_from in zip(emails_from, exp_smtp_from_lst, exp_msg_from_lst): + self.env.user.email = email_from + + # open a composer and run it in comment mode + composer = Form(self.env['mail.compose.message'].with_context( + default_composition_mode='comment', + default_force_send=True, # force sending emails directly to check SMTP + default_model=test_records._name, + default_res_ids=test_records.ids, + # avoid successive tests issues with followers + mail_create_nosubscribe=True, + )) + composer.body = 'Hello {{ object.name }}' + composer.subject = 'My Subject' + composer = composer.save() + with self.mock_mail_gateway(mail_unlink_sent=False), \ + self.mock_mail_app(): + composer._action_send_mail() + + self.assertSMTPEmailsSent( + smtp_from=exp_smtp_from, + smtp_to_list=[self.partner_employee_2.email_normalized], + emails_count=2, # same on both records + message_from=exp_msg_from, + from_filter=exp_from_filter, + ) + @users('employee') @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail', 'odoo.addons.mail.models.mail_message_schedule') def test_mail_composer_wtpl_complete(self): @@ -1761,6 +1844,105 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase): self.assertEqual(set(message.attachment_ids.mapped('res_id')), set(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_mc(self): + """ Test specific to multi-company environment, notably company propagation + or aliases. """ + # add access to second company to avoid MC rules on ticket model + self.env.user.company_ids = [(4, self.company_2.id)] + + # initial data + self.assertEqual(self.env.company, self.company_admin) + self.assertEqual(self.user_admin.company_id, self.company_admin) + + attachment_data = self._generate_attachments_data(2, self.template._name, self.template.id) + email_to_1 = 'test.to.1@test.example.com' + self.template.write({ + 'auto_delete': False, # keep sent emails to check content + 'attachment_ids': [(0, 0, a) for a in attachment_data], + 'email_from': False, # use current user as author + 'email_layout_xmlid': 'mail.test_layout', + 'email_to': email_to_1, + 'mail_server_id': False, # let it find a server + 'partner_to': '%s, {{ object.customer_id.id if object.customer_id else "" }}' % self.partner_admin.id, + }) + attachs = self.env['ir.attachment'].search([('name', 'in', [a['name'] for a in attachment_data])]) + self.assertEqual(len(attachs), 2) + + for batch, companies, expected_companies in [ + ( + False, self.company_admin, self.company_admin + ), ( + True, self.company_admin + self.company_2, self.company_admin + self.company_2, + ), + ]: + with self.subTest(batch=batch, + companies=companies): + # update test configuration + test_records = self.test_records if batch else self.test_record + for company, record in zip(companies, test_records): + record.company_id = company.id + + # open a composer and run it in comment mode + composer = Form(self.env['mail.compose.message'].with_context( + default_composition_mode='comment', + default_force_send=True, # force sending emails directly to check SMTP + default_model=test_records._name, + default_res_ids=test_records.ids, + default_template_id=self.template.id, + # avoid successive tests issues with followers + mail_create_nosubscribe=True, + )).save() + with self.mock_mail_gateway(mail_unlink_sent=False), \ + self.mock_mail_app(): + composer._action_send_mail() + + new_partner = self.env['res.partner'].search([('email_normalized', '=', 'test.to.1@test.example.com')]) + self.assertEqual(len(new_partner), 1) + # check output, company-specific values mainly for this test + for record, exp_company in zip( + test_records, expected_companies + ): + message = record.message_ids[0] + for recipient in [self.partner_employee_2, new_partner, record.customer_id]: + self.assertMailMail( + recipient, + 'sent', + author=self.partner_employee, + mail_message=message, + email_values={ + 'headers': { + 'Return-Path': f'{self.alias_bounce}@{self.alias_domain}', + 'X-Odoo-Objects': f'{record._name}-{record.id}', + }, + 'subject': f'TemplateSubject {record.name}', + }, + fields_values={ + 'headers': { + 'X-Odoo-Objects': f'{record._name}-{record.id}', + }, + 'mail_server_id': self.env['ir.mail_server'], + 'subject': f'TemplateSubject {record.name}', + }, + ) + # to check behavior of extract_rfc2822_addresses + if recipient == new_partner: + smtp_to_list = ['"test.to.1@test.example.com"', 'test.to.1@test.example.com'] + else: + smtp_to_list = [recipient.email_normalized] + if recipient != record.customer_id: + emails_count = len(test_records) # not distinguishable using this assert + else: + emails_count = 1 + self.assertSMTPEmailsSent( + smtp_from=f'{self.default_from}@{self.alias_domain}', + smtp_to_list=smtp_to_list, + mail_server=self.mail_server_notification, + emails_count=emails_count, + ) + @users('employee') @mute_logger('odoo.addons.mail.models.mail_mail') def test_mail_composer_wtpl_recipients_email_fields(self): @@ -2462,6 +2644,105 @@ class TestComposerResultsMass(TestMailComposer): # self.assertIn(exp_button_es, sent_mail['body']) self.assertIn(exp_button_en, sent_mail['body']) + + @users('employee') + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_mail_composer_wtpl_mc(self): + """ Test specific to multi-company environment, notably company propagation + or aliases. """ + # add access to second company to avoid MC rules on ticket model + self.env.user.company_ids = [(4, self.company_2.id)] + + # initial data + self.assertEqual(self.env.company, self.company_admin) + self.assertEqual(self.user_admin.company_id, self.company_admin) + + attachment_data = self._generate_attachments_data(2, self.template._name, self.template.id) + email_to_1 = 'test.to.1@test.example.com' + self.template.write({ + 'auto_delete': False, # keep sent emails to check content + 'attachment_ids': [(0, 0, a) for a in attachment_data], + 'email_from': False, # use current user as author + 'email_layout_xmlid': 'mail.test_layout', + 'email_to': email_to_1, + 'mail_server_id': False, # let it find a server + 'partner_to': '%s, {{ object.customer_id.id if object.customer_id else "" }}' % self.partner_admin.id, + }) + attachs = self.env['ir.attachment'].search([('name', 'in', [a['name'] for a in attachment_data])]) + self.assertEqual(len(attachs), 2) + + for companies, expected_companies in [ + ( + self.company_admin + self.company_2, + self.company_admin + self.company_2, + ), + ]: + with self.subTest(companies=companies): + # update test configuration + test_records = self.test_records + for company, record in zip(companies, test_records): + record.company_id = company.id + + # open a composer and run it in comment mode + composer = Form(self.env['mail.compose.message'].with_context( + default_composition_mode='mass_mail', + default_force_send=True, # force sending emails directly to check SMTP + default_model=test_records._name, + default_res_ids=test_records.ids, + default_template_id=self.template.id, + # avoid successive tests issues with followers + mail_create_nosubscribe=True, + )).save() + with self.mock_mail_gateway(mail_unlink_sent=False), \ + self.mock_mail_app(): + composer._action_send_mail() + + new_partner = self.env['res.partner'].search([('email_normalized', '=', 'test.to.1@test.example.com')]) + self.assertEqual(len(new_partner), 1) + # check output, company-specific values mainly for this test + for record, exp_company in zip( + test_records, expected_companies + ): + # message copy is kept + message = record.message_ids[0] + recipients = record.customer_id + new_partner + self.partner_admin + self.assertMailMail( + record.customer_id + new_partner + self.partner_admin, + 'sent', + author=self.partner_employee, + mail_message=message, + email_values={ + 'headers': { + 'Return-Path': f'{self.alias_bounce}@{self.alias_domain}', + 'X-Odoo-Objects': f'{record._name}-{record.id}', + }, + 'subject': f'TemplateSubject {record.name}', + }, + fields_values={ + 'headers': { + 'X-Odoo-Objects': f'{record._name}-{record.id}', + }, + 'mail_server_id': self.env['ir.mail_server'], + 'subject': f'TemplateSubject {record.name}', + }, + ) + for recipient in recipients: + # to check behavior of extract_rfc2822_addresses + if recipient == new_partner: + smtp_to_list = ['"test.to.1@test.example.com"', 'test.to.1@test.example.com'] + else: + smtp_to_list = [recipient.email_normalized] + if recipient != record.customer_id: + emails_count = len(test_records) # not distinguishable using this assert + else: + emails_count = 1 + self.assertSMTPEmailsSent( + smtp_from=f'{self.default_from}@{self.alias_domain}', + smtp_to_list=smtp_to_list, + mail_server=self.mail_server_notification, + emails_count=emails_count, + ) + @users('employee') @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') def test_mail_composer_wtpl_recipients(self): @@ -2581,7 +2862,7 @@ class TestComposerResultsMass(TestMailComposer): # 6: void is void: raise in comment mode, just don't send anything in mass mail mode composer_form = Form(self.env['mail.compose.message'].with_context( - default_model='mail.test.ticket', + default_model='mail.test.ticket.mc', default_template_id=self.template.id )) composer = composer_form.save() @@ -2592,7 +2873,7 @@ class TestComposerResultsMass(TestMailComposer): composer_form = Form(self.env['mail.compose.message'].with_context( default_composition_mode='mass_mail', - default_model='mail.test.ticket', + default_model='mail.test.ticket.mc', default_template_id=self.template.id )) composer = composer_form.save() diff --git a/addons/test_mail/tests/test_mail_gateway.py b/addons/test_mail/tests/test_mail_gateway.py index 63c8a3444d7..50baa6f4485 100644 --- a/addons/test_mail/tests/test_mail_gateway.py +++ b/addons/test_mail/tests/test_mail_gateway.py @@ -128,9 +128,7 @@ class TestEmailParsing(MailCommon): email_to='bounce@xxx.odoo.com', delivered_to='bounce@xxx.odoo.com' ) - - msg_dict = {} - msg = self.env['mail.thread']._message_parse_extract_bounce(self.from_string(incoming_bounce), msg_dict) + msg = self.env['mail.thread'].message_parse(self.from_string(incoming_bounce)) self.assertEqual(msg['bounced_email'], partner.email, "The sender email should be correctly parsed") self.assertEqual(msg['bounced_partner'], partner, "A partner with this email should exist") self.assertEqual(msg['bounced_msg_ids'][0], message.message_id, "The sender message-id should correctly parsed") diff --git a/addons/test_mass_mailing/tests/test_mailing_server.py b/addons/test_mass_mailing/tests/test_mailing_server.py index fda438bf015..874aed17e90 100644 --- a/addons/test_mass_mailing/tests/test_mailing_server.py +++ b/addons/test_mass_mailing/tests/test_mailing_server.py @@ -112,8 +112,8 @@ class TestMassMailingServer(TestMassMailCommon): ('unknow_email@unknow_domain.com', 'unknow_email@unknow_domain.com', self.mail_server_notification), ]: self.assertSMTPEmailsSent( - smtp_from=expected_smtp_from, - message_from=expected_msg_from, - mail_server=expected_mail_server, - emails_count=8, - ) + smtp_from=expected_smtp_from, + message_from=expected_msg_from, + mail_server=expected_mail_server, + emails_count=8, + )