From 053290c6e787a1e7b67d2b9daeaad6efb065e3e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 8 Aug 2023 09:27:51 +0000 Subject: [PATCH] [IMP] base, mail: cleanup ir_mail_server tests, improve test logs RATIONALE This prepares the move of ICP to mail before replacing them by dynamic alias domains. Cleanup tests: try to use loops with input / expected to better understand the various test cases, add some comments, improve logs when failing to find the right sent email. Rename tests to have a better test structure when reading logs. Remove a test from odoo/odoo@3b6c20805cd976db269cbb88e2c22c5ff4de37b1 that adds nothing except testing the test suite. OTHER ADDONS In test_mail: have a specific class for testing servers as other data is not necessary, and it allows to have a tag for it. In mass mailing: concatenate test about server finding, several tests can be done in a single unit test. Task-3453577 (TestMail: Update Alias/Gateway tests for MC) Prepares Task-3453347 (Mail: Move Mail ICP from Base to Mail) Prepares Task-36879 (Mail: Support MultiCompany Aliases) closes odoo/odoo#131492 Signed-off-by: Thibault Delavallee (tde) --- addons/test_mail/tests/test_mail_mail.py | 34 +- .../tests/test_mailing_server.py | 94 +-- odoo/addons/base/tests/common.py | 25 +- odoo/addons/base/tests/test_ir_mail_server.py | 625 ++++++++---------- 4 files changed, 339 insertions(+), 439 deletions(-) diff --git a/addons/test_mail/tests/test_mail_mail.py b/addons/test_mail/tests/test_mail_mail.py index 0c7cad69a8c..f9740306bd2 100644 --- a/addons/test_mail/tests/test_mail_mail.py +++ b/addons/test_mail/tests/test_mail_mail.py @@ -28,12 +28,6 @@ class TestMailMail(MailCommon): super(TestMailMail, cls).setUpClass() cls._init_mail_servers() - cls.server_domain_2 = cls.env['ir.mail_server'].create({ - 'name': 'Server 2', - 'smtp_host': 'test_2.com', - 'from_filter': 'test_2.com', - }) - cls.test_record = cls.env['mail.test.gateway'].with_context(cls._test_context).create({ 'name': 'Test', 'email_from': 'ignasse@example.com', @@ -660,6 +654,20 @@ class TestMailMail(MailCommon): self.send_email_mocked.side_effect = _send_current +@tagged('mail_mail', 'mail_server') +class TestMailMailServer(MailCommon): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._init_mail_servers() + + cls.server_domain_2 = cls.env['ir.mail_server'].create({ + 'from_filter': 'test_2.com', + 'name': 'Server 2', + 'smtp_host': 'test_2.com', + }) + @mute_logger('odoo.addons.mail.models.mail_mail') def test_mail_mail_send_server(self): """Test that the mails are send in batch. @@ -722,13 +730,13 @@ class TestMailMail(MailCommon): any_order=True, ) - self.assert_email_sent_smtp(message_from=f'"test" ', - emails_count=5, from_filter=self.server_notification.from_filter) - self.assert_email_sent_smtp(message_from=f'"test_2" ', - emails_count=5, from_filter=self.server_notification.from_filter) - self.assert_email_sent_smtp(message_from='user_1@test_2.com', emails_count=5, from_filter=self.server_domain_2.from_filter) - self.assert_email_sent_smtp(message_from='user_2@test_2.com', emails_count=5, from_filter=self.server_domain_2.from_filter) - self.assert_email_sent_smtp(message_from='user_1@test_2.com', emails_count=5, from_filter=self.server_domain.from_filter) + self.assertSMTPEmailsSent(message_from=f'"test" ', + emails_count=5, from_filter=self.server_notification.from_filter) + self.assertSMTPEmailsSent(message_from=f'"test_2" ', + emails_count=5, from_filter=self.server_notification.from_filter) + self.assertSMTPEmailsSent(message_from='user_1@test_2.com', emails_count=5, from_filter=self.server_domain_2.from_filter) + self.assertSMTPEmailsSent(message_from='user_2@test_2.com', emails_count=5, from_filter=self.server_domain_2.from_filter) + self.assertSMTPEmailsSent(message_from='user_1@test_2.com', emails_count=5, from_filter=self.server_domain.from_filter) @tagged('mail_mail') diff --git a/addons/test_mass_mailing/tests/test_mailing_server.py b/addons/test_mass_mailing/tests/test_mailing_server.py index 0a768f3e69c..b533d26db9e 100644 --- a/addons/test_mass_mailing/tests/test_mailing_server.py +++ b/addons/test_mass_mailing/tests/test_mailing_server.py @@ -73,98 +73,48 @@ class TestMassMailingServer(TestMassMailCommon): @users('user_marketing') @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.addons.mass_mailing.models.mailing') - def test_mass_mailing_server_batch(self): + def test_mass_mailing_server_find(self): """Test that the right mail server is chosen to send the mailing. Test also the envelop and the SMTP headers. """ - # Test sending mailing in batch mailings = self.env['mailing.mailing'].create([{ 'subject': 'Mailing', 'body_html': 'Body for ', 'email_from': 'specific_user@test.mycompany.com', - 'mailing_model_id': self.env['ir.model']._get('mailing.test.optout').id, + 'mailing_model_id': self.env['ir.model']._get_id('mailing.test.optout'), }, { 'subject': 'Mailing', 'body_html': 'Body for ', 'email_from': 'unknown_name@test.mycompany.com', - 'mailing_model_id': self.env['ir.model']._get('mailing.test.optout').id, - }]) - with self.mock_smtplib_connection(): - mailings.action_send_mail() - self.assertEqual(self.find_mail_server_mocked.call_count, 2, 'Must be called only once per mail from') - - self.assert_email_sent_smtp( - smtp_from='specific_user@test.mycompany.com', - message_from='specific_user@test.mycompany.com', - from_filter=self.server_user.from_filter, - emails_count=8, - ) - - self.assert_email_sent_smtp( - # Must use the bounce address here because the mail server - # is configured for the entire domain "test.mycompany.com" - smtp_from=lambda x: 'bounce' in x, - message_from='unknown_name@test.mycompany.com', - from_filter=self.server_domain.from_filter, - emails_count=8, - ) - - @users('user_marketing') - @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.addons.mass_mailing.models.mailing') - def test_mass_mailing_server_default(self): - # We do not have a mail server for this address email, so fall back to the - # "notifications@domain" email. - mailings = self.env['mailing.mailing'].create([{ - 'subject': 'Mailing', - 'body_html': 'Body for ', - 'email_from': '"Testing" ', - 'mailing_model_id': self.env['ir.model']._get('mailing.test.optout').id, - }]) - - with self.mock_smtplib_connection(): - mailings.action_send_mail() - - self.assertEqual(self.find_mail_server_mocked.call_count, 1) - self.assert_email_sent_smtp( - smtp_from='notifications@test.mycompany.com', - message_from='"Testing" ', - from_filter=self.server_notification.from_filter, - emails_count=8, - ) - - self.assertEqual(self.find_mail_server_mocked.call_count, 1, 'Must be called only once') - - @users('user_marketing') - @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.addons.mass_mailing.models.mailing') - def test_mass_mailing_server_forced(self): - # We force a mail server on one mailing - mailings = self.env['mailing.mailing'].create([{ - 'subject': 'Mailing', - 'body_html': 'Body for ', - 'email_from': self.server_user.from_filter, - 'mailing_model_id': self.env['ir.model']._get('mailing.test.optout').id, + 'mailing_model_id': self.env['ir.model']._get_id('mailing.test.optout'), }, { 'subject': 'Mailing', 'body_html': 'Body for ', + 'email_from': '"Testing" ', + 'mailing_model_id': self.env['ir.model']._get_id('mailing.test.optout'), + }, { + 'subject': 'Mailing Forced', + 'body_html': 'Body for ', 'email_from': 'unknow_email@unknow_domain.com', - 'mailing_model_id': self.env['ir.model']._get('mailing.test.optout').id, + 'mailing_model_id': self.env['ir.model']._get_id('mailing.test.optout'), 'mail_server_id': self.server_notification.id, }]) with self.mock_smtplib_connection(): mailings.action_send_mail() - self.assertEqual(self.find_mail_server_mocked.call_count, 1, 'Must not be called when mail server is forced') + self.assertEqual(self.find_mail_server_mocked.call_count, 3, 'Must be called only once per mail from except when forced') - self.assert_email_sent_smtp( - smtp_from='specific_user@test.mycompany.com', - message_from='specific_user@test.mycompany.com', - from_filter=self.server_user.from_filter, - emails_count=8, - ) - - self.assert_email_sent_smtp( - smtp_from='unknow_email@unknow_domain.com', - message_from='unknow_email@unknow_domain.com', - from_filter=self.server_notification.from_filter, + for (expected_smtp_from, expected_msg_from, expected_from_filter) in [ + ('specific_user@test.mycompany.com', 'specific_user@test.mycompany.com', self.server_user.from_filter), + (f'{self.alias_bounce}@{self.alias_domain}', 'unknown_name@test.mycompany.com', self.server_domain.from_filter), + # We do not have a mail server for this address email, so fall back to the "notifications@domain" email. + ('notifications@test.mycompany.com', '"Testing" ', self.server_notification.from_filter), + # forced sever + ('unknow_email@unknow_domain.com', 'unknow_email@unknow_domain.com', self.server_notification.from_filter), + ]: + self.assertSMTPEmailsSent( + smtp_from=expected_smtp_from, + message_from=expected_msg_from, + from_filter=expected_from_filter, emails_count=8, ) diff --git a/odoo/addons/base/tests/common.py b/odoo/addons/base/tests/common.py index 86f35ae2f35..b2411e0eda4 100644 --- a/odoo/addons/base/tests/common.py +++ b/odoo/addons/base/tests/common.py @@ -354,7 +354,7 @@ class MockSmtplibCase: self.env['ir.mail_server'].send_email(msg, smtp_session=smtp_session) return smtp_session.messages.pop() - def assert_email_sent_smtp(self, smtp_from=None, smtp_to_list=None, message_from=None, from_filter=None, emails_count=1): + def assertSMTPEmailsSent(self, smtp_from=None, smtp_to_list=None, message_from=None, from_filter=None, emails_count=1): """Check that the given email has been sent. If one of the parameter is None, it's just ignored and not used to retrieve the email. @@ -369,22 +369,25 @@ class MockSmtplibCase: """ matching_emails = filter( lambda email: - (smtp_from is None or ( - smtp_from(email['smtp_from']) - if callable(smtp_from) - else smtp_from == email['smtp_from']) - ) + (smtp_from is None or smtp_from == email['smtp_from']) and (smtp_to_list is None or smtp_to_list == email['smtp_to_list']) and (message_from is None or 'From: %s' % message_from in email['message']) and (from_filter is None or from_filter == email['from_filter']), self.emails, ) + debug_info = '' matching_emails_count = len(list(matching_emails)) - - self.assertTrue( - matching_emails_count == emails_count, - msg='Emails not sent, %i emails match the condition but %i are expected' % (matching_emails_count, emails_count), + if matching_emails_count != emails_count: + debug_info = '\n'.join( + f"SMTP-From: {email['smtp_from']}, SMTP-To: ({email['smtp_to_list']}) - From_filter {email['from_filter']})" + for email in self.emails + ) + self.assertEqual( + matching_emails_count, emails_count, + msg=f'Incorrect emails sent: {matching_emails_count} found, {emails_count} expected' + f'\nConditions: SMTP-From {smtp_from}, SMTP-To {smtp_to_list}, Msg-From {message_from}, From_filter {from_filter}' + f'\n{debug_info}' ) @classmethod @@ -392,9 +395,11 @@ class MockSmtplibCase: cls.alias_bounce = 'bounce.test' cls.alias_domain = 'test.mycompany.com' cls.default_from = 'notifications' + cls.default_from_filter = False cls.env['ir.config_parameter'].sudo().set_param('mail.catchall.domain', cls.alias_domain) cls.env['ir.config_parameter'].sudo().set_param('mail.default.from', cls.default_from) cls.env['ir.config_parameter'].sudo().set_param('mail.bounce.alias', cls.alias_bounce) + cls.env['ir.config_parameter'].sudo().set_param('mail.default.from_filter', cls.default_from_filter) @classmethod def _init_mail_servers(cls): diff --git a/odoo/addons/base/tests/test_ir_mail_server.py b/odoo/addons/base/tests/test_ir_mail_server.py index 89a4415b4f3..9d97f6e640b 100644 --- a/odoo/addons/base/tests/test_ir_mail_server.py +++ b/odoo/addons/base/tests/test_ir_mail_server.py @@ -3,6 +3,7 @@ import email.message import email.policy +import itertools from unittest.mock import patch @@ -38,22 +39,26 @@ class EmailConfigCase(TransactionCase): @patch.dict(config.options, {"email_from": "settings@example.com"}) def test_default_email_from(self, *args): """Email from setting is respected.""" - # ICP setting is more important ICP = self.env["ir.config_parameter"].sudo() - ICP.set_param("mail.catchall.domain", "test.mycompany.com") - ICP.set_param("mail.default.from", "icp") - message = self.env["ir.mail_server"].build_email( - False, "recipient@example.com", "Subject", - "The body of an email", - ) - self.assertEqual(message["From"], "icp@test.mycompany.com") - # Without ICP, the config file/CLI setting is used - ICP.set_param("mail.default.from", False) - message = self.env["ir.mail_server"].build_email( - False, "recipient@example.com", "Subject", - "The body of an email", - ) - self.assertEqual(message["From"], "settings@example.com") + for (icp_from, icp_domain), expected_from in zip( + [ + ('icp', 'test.mycompany.com'), + ('icp@another.company.com', 'test.mycompany.com'), + (False, 'test.mycompany.com'), + ], [ + "icp@test.mycompany.com", + "icp@another.company.com", + "settings@example.com", + ], + ): + with self.subTest(icp_from=icp_from, icp_domain=icp_domain): + ICP.set_param("mail.default.from", icp_from) + ICP.set_param("mail.catchall.domain", icp_domain) + message = self.env["ir.mail_server"].build_email( + False, "recipient@example.com", "Subject", + "The body of an email", + ) + self.assertEqual(message["From"], expected_from) @tagged('mail_server') @@ -65,6 +70,17 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): cls._init_mail_config() cls._init_mail_servers() + cls.default_bounce_address = f'{cls.alias_bounce}@{cls.alias_domain}' + cls.default_from_address = f'{cls.default_from}@{cls.alias_domain}' + + def test_assert_base_values(self): + self.assertEqual( + self.env['ir.mail_server']._get_default_bounce_address(), + self.default_bounce_address) + self.assertEqual( + self.env['ir.mail_server']._get_default_from_address(), + self.default_from_address) + def test_bpo_34424_35805(self): """Ensure all email sent are bpo-34424 and bpo-35805 free""" fake_smtp = _FakeSMTP() @@ -108,10 +124,8 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): "There should be 3 headers MIME-Version: one on the enveloppe, " "one on the html part, one on the text part") - def test_match_from_filter(self): - """Test the from_filter field on the "ir.mail_server".""" - match_from_filter = self.env['ir.mail_server']._match_from_filter - + def test_mail_server_match_from_filter(self): + """ Test the from_filter field on the "ir.mail_server". """ # Should match tests = [ ('admin@mail.example.com', 'mail.example.com'), @@ -123,7 +137,7 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): ('"fake@test.mycompany.com" ', 'test.mycompany.com, mail.example.com, test2.com'), ] for email, from_filter in tests: - self.assertTrue(match_from_filter(email, from_filter)) + self.assertTrue(self.env['ir.mail_server']._match_from_filter(email, from_filter)) # Should not match tests = [ @@ -136,7 +150,7 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): ('"fake@test.mycompany.com" ', 'test.mycompany.com, wrong.mail.example.com, test3.com'), ] for email, from_filter in tests: - self.assertFalse(match_from_filter(email, from_filter)) + self.assertFalse(self.env['ir.mail_server']._match_from_filter(email, from_filter)) def test_mail_body(self): bodies = [ @@ -175,7 +189,7 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): @mute_logger('odoo.models.unlink') def test_mail_server_priorities(self): - """Test if we choose the right mail server to send an email. + """ Test if we choose the right mail server to send an email. Priorities are 1. Forced mail server (e.g.: in mass mailing) @@ -192,300 +206,246 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): # sanity checks self.assertTrue(self.env['ir.mail_server']._get_default_from_address(), 'Notifications email must be set for testing') self.assertTrue(self.env['ir.mail_server']._get_default_bounce_address(), 'Bounce email must be set for testing') - - mail_server, mail_from = self.env['ir.mail_server']._find_mail_server(email_from='specific_user@test.mycompany.com') - self.assertEqual(mail_server, self.server_user) - self.assertEqual(mail_from, 'specific_user@test.mycompany.com') - - mail_server, mail_from = self.env['ir.mail_server']._find_mail_server(email_from='"Name name@strange.name" ') - self.assertEqual(mail_server, self.server_user, 'Must extract email from full name') - self.assertEqual(mail_from, '"Name name@strange.name" ', 'Must keep the given mail from') - - # Should not be case sensitive - mail_server, mail_from = self.env['ir.mail_server']._find_mail_server(email_from='specific_user@test.mycompany.com') - self.assertEqual(mail_server, self.server_user, 'Mail from is case insensitive') - self.assertEqual(mail_from, 'specific_user@test.mycompany.com', 'Should not change the mail from') - - mail_server, mail_from = self.env['ir.mail_server']._find_mail_server(email_from='unknown_email@test.mycompany.com') - self.assertEqual(mail_server, self.server_domain) - self.assertEqual(mail_from, 'unknown_email@test.mycompany.com') - - # Cover a different condition that the "email case insensitive" test - mail_server, mail_from = self.env['ir.mail_server']._find_mail_server(email_from='unknown_email@TEST.MYCOMPANY.COM') - self.assertEqual(mail_server, self.server_domain, 'Domain is case insensitive') - self.assertEqual(mail_from, 'unknown_email@TEST.MYCOMPANY.COM', 'Domain is case insensitive') - - mail_server, mail_from = self.env['ir.mail_server']._find_mail_server(email_from='"Test" ') - self.assertEqual(mail_server, self.server_notification, 'Should take the notification email') - self.assertEqual(mail_from, 'notifications@test.mycompany.com') - # this mail server can not be used for a specific email address and 2 domain names self.server_user.from_filter = "domain1.com, specific_user@test.mycompany.com, domain2.com" - mail_server, mail_from = self.env['ir.mail_server']._find_mail_server(email_from='"Test" ') - self.assertEqual(mail_server, self.server_user, "The entire email matches, the server should be used") - self.assertEqual(mail_from, '"Test" ') - mail_server, mail_from = self.env['ir.mail_server']._find_mail_server(email_from='"Example" ') - self.assertEqual(mail_server, self.server_user, "The domain matches, the server should be used") - self.assertEqual(mail_from, '"Example" ') - mail_server, mail_from = self.env['ir.mail_server']._find_mail_server(email_from='"Example" ') - self.assertEqual(mail_server, self.server_user, "The domain matches, the server should be used") - self.assertEqual(mail_from, '"Example" ') - # check that we take the domain server and not the "user specific" server - mail_server, mail_from = self.env['ir.mail_server']._find_mail_server(email_from='"Test" ') - self.assertEqual(mail_server, self.server_domain) - self.assertEqual(mail_from, '"Test" ') + for email_from, (expected_mail_server, expected_email_from) in zip( + [ + 'specific_user@test.mycompany.com', + '"Name name@strange.name" ', + 'specific_user@test.mycompany.com', + 'unknown_email@test.mycompany.com', + 'unknown_email@TEST.MYCOMPANY.COM', + '"Test" ', + # server_user multiple from filter check + '"Test" ', + '"Example" ', + '"Example" ', + '"Test" ', + ], [ + (self.server_user, 'specific_user@test.mycompany.com'), + # must extract email from full name, must keep the given email from + (self.server_user, '"Name name@strange.name" '), + # should not be case sensitive + (self.server_user, 'specific_user@test.mycompany.com'), + (self.server_domain, 'unknown_email@test.mycompany.com'), + # cover a different condition that the "email case insensitive" test: domain is case insensitive + (self.server_domain, 'unknown_email@TEST.MYCOMPANY.COM'), + # should take the notification email + (self.server_notification, 'notifications@test.mycompany.com'), + # server_user multipl from_filter check: can be used for a sepcific emial and 2 domain names + # 1. entire email matches, server should be used + (self.server_user, '"Test" '), + # 2. domain matches, server should be used + (self.server_user, '"Example" '), + # 3. other domain matches, server shold be used + (self.server_user, '"Example" '), + # check that we take the domain server and not the "user specific" server + (self.server_domain, '"Test" '), + ], + ): + with self.subTest(email_from=email_from): + mail_server, mail_from = self.env['ir.mail_server']._find_mail_server(email_from=email_from) + self.assertEqual(mail_server, expected_mail_server) + self.assertEqual(mail_from, expected_email_from) - # remove the notifications email to simulate a mis-configured Odoo database - # so we do not have the choice, we have to spoof the FROM - # (otherwise we can not send the email) + @mute_logger('odoo.models.unlink') + def test_mail_server_priorities_base_config(self): + """ Test if we choose the right mail server to send an email, without + ICP configuration. Simulates unconfigured Odoo DB so we have to spoof + the FROM otherwise we cannot send any email. """ self.env['ir.config_parameter'].sudo().set_param('mail.catchall.domain', False) - with mute_logger('odoo.addons.base.models.ir_mail_server'): - mail_server, mail_from = self.env['ir.mail_server']._find_mail_server(email_from='test@unknown_domain.com') - self.assertEqual(mail_server.from_filter, False, 'No notifications email set, must be forced to spoof the FROM') - self.assertEqual(mail_from, 'test@unknown_domain.com') + self.env['ir.config_parameter'].sudo().set_param('mail.default.from', False) + for email_from, (expected_mail_server, expected_from_filter, expected_email_from) in zip( + [ + 'test@unknown_domain.com', + ], [ + (self.server_default, False, 'test@unknown_domain.com'), + ], + ): + with self.subTest(email_from=email_from): + mail_server, mail_from = self.env['ir.mail_server']._find_mail_server(email_from='test@unknown_domain.com') + self.assertEqual(mail_server.from_filter, expected_from_filter, 'No notifications email set, must be forced to spoof the FROM') + self.assertEqual(mail_server, expected_mail_server) + self.assertEqual(mail_from, expected_email_from) @mute_logger('odoo.models.unlink') def test_mail_server_send_email(self): IrMailServer = self.env['ir.mail_server'] - default_bounce_adress = self.env['ir.mail_server']._get_default_bounce_address() - # A mail server is configured for the email - with self.mock_smtplib_connection(): - message = self._build_email(mail_from='specific_user@test.mycompany.com') - IrMailServer.send_email(message) + for (mail_from, (expected_smtp_from, expected_msg_from, expected_from_filter)), provide_smtp in itertools.product( + zip( + [ + 'specific_user@test.mycompany.com', + '"Name" ', + 'test@unknown_domain.com', + 'unknown_name@test.mycompany.com' + ], [ + # A mail server is configured for the email + ('specific_user@test.mycompany.com', 'specific_user@test.mycompany.com', 'specific_user@test.mycompany.com'), + # No mail server are configured for the email address, so it will use the + # notifications email instead and encapsulate the old email + ('notifications@test.mycompany.com', '"Name" ', 'notifications@test.mycompany.com'), + # same situation, but the original email has no name part + ('notifications@test.mycompany.com', '"test" ', 'notifications@test.mycompany.com'), + # 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, 'unknown_name@test.mycompany.com', 'test.mycompany.com'), + ], + ), + [False, True], # provide SMTP session or not: should not impact sending + ): + with self.subTest(mail_from=mail_from): + with self.mock_smtplib_connection(): + if provide_smtp: + smtp_session = IrMailServer.connect(smtp_from=mail_from) + message = self._build_email(mail_from=mail_from) + IrMailServer.send_email(message, smtp_session=smtp_session) + else: + message = self._build_email(mail_from=mail_from) + IrMailServer.send_email(message) - self.assertEqual(len(self.emails), 1) - - self.assert_email_sent_smtp( - smtp_from='specific_user@test.mycompany.com', - message_from='specific_user@test.mycompany.com', - from_filter='specific_user@test.mycompany.com', - ) - - # No mail server are configured for the email address, - # so it will use the notifications email instead and encapsulate the old email - with self.mock_smtplib_connection(): - message = self._build_email(mail_from='"Name" ') - IrMailServer.send_email(message) - - self.assertEqual(len(self.emails), 1) - - self.assert_email_sent_smtp( - smtp_from='notifications@test.mycompany.com', - message_from='"Name" ', - from_filter='notifications@test.mycompany.com', - ) - - # Same situation, but the original email has no name part - with self.mock_smtplib_connection(): - message = self._build_email(mail_from='test@unknown_domain.com') - IrMailServer.send_email(message) - - self.assertEqual(len(self.emails), 1) - - self.assert_email_sent_smtp( - smtp_from='notifications@test.mycompany.com', - message_from='"test" ', - from_filter='notifications@test.mycompany.com', - ) - - # A mail server is configured for the entire domain name, so we can use the bounce - # email address because the mail server supports it - with self.mock_smtplib_connection(): - message = self._build_email(mail_from='unknown_name@test.mycompany.com') - IrMailServer.send_email(message) - - self.assertEqual(len(self.emails), 1) - - self.assert_email_sent_smtp( - smtp_from=default_bounce_adress, - message_from='unknown_name@test.mycompany.com', - from_filter='test.mycompany.com', - ) + self.connect_mocked.assert_called_once() + self.assertEqual(len(self.emails), 1) + self.assertSMTPEmailsSent( + smtp_from=expected_smtp_from, + message_from=expected_msg_from, + from_filter=expected_from_filter, + ) # remove the notification server # so will use the mail server - self.server_notification.unlink() - # The mail server configured for the notifications email has been removed # but we can still use the mail server configured for test.mycompany.com # and so we will be able to use the bounce address # because we use the mail server for "test.mycompany.com" - with self.mock_smtplib_connection(): - message = self._build_email(mail_from='"Name" ') - IrMailServer.send_email(message) + self.server_notification.unlink() + for provide_smtp in [False, True]: + with self.mock_smtplib_connection(): + if provide_smtp: + smtp_session = IrMailServer.connect(smtp_from='"Name" ') + message = self._build_email(mail_from='"Name" ') + IrMailServer.send_email(message, smtp_session=smtp_session) + else: + message = self._build_email(mail_from='"Name" ') + IrMailServer.send_email(message) - self.assertEqual(len(self.emails), 1) - - self.assert_email_sent_smtp( - smtp_from=default_bounce_adress, - message_from='"Name" ', - from_filter='test.mycompany.com', - ) + self.connect_mocked.assert_called_once() + self.assertEqual(len(self.emails), 1) + self.assertSMTPEmailsSent( + smtp_from=self.default_bounce_address, + message_from='"Name" ', + from_filter='test.mycompany.com', + ) + @mute_logger('odoo.models.unlink') + def test_mail_server_send_email_IDNA(self): # Test that the mail from / recipient envelop are encoded using IDNA self.env['ir.config_parameter'].sudo().set_param('mail.catchall.domain', 'ééééééé.com') with self.mock_smtplib_connection(): message = self._build_email(mail_from='test@ééééééé.com') - IrMailServer.send_email(message) + self.env['ir.mail_server'].send_email(message) self.assertEqual(len(self.emails), 1) - - self.assert_email_sent_smtp( - smtp_from='bounce.test@xn--9caaaaaaa.com', + self.assertSMTPEmailsSent( + smtp_from=f'{self.alias_bounce}@xn--9caaaaaaa.com', smtp_to_list=['dest@xn--example--i1a.com'], message_from='test@=?utf-8?b?w6nDqcOpw6nDqcOpw6k=?=.com', from_filter=False, ) + @mute_logger('odoo.models.unlink') + def test_mail_server_send_email_default_from(self): # Test the case when the "mail.default.from" contains a full email address and not just the local part # the domain of this default email address can be different than the catchall domain self.env['ir.config_parameter'].sudo().set_param('mail.default.from', 'test@custom_domain.com') - self.server_default.from_filter = 'custom_domain.com' + _custom_server = self.env['ir.mail_server'].create({ + 'from_filter': 'custom_domain.com', + 'name': 'Custom Domain Server', + 'smtp_host': 'smtp_host', + 'smtp_encryption': 'none', + }) - with self.mock_smtplib_connection(): - message = self._build_email(mail_from='"Name" ') - IrMailServer.send_email(message) + for mail_from, (expected_smtp_from, expected_msg_from, expected_from_filter) in zip( + [ + '"Name" ', + ], [ + ('test@custom_domain.com', '"Name" ', 'custom_domain.com'), + ], + ): + with self.subTest(mail_from=mail_from): + with self.mock_smtplib_connection(): + message = self._build_email(mail_from=mail_from) + self.env['ir.mail_server'].send_email(message) - self.assert_email_sent_smtp( - smtp_from='test@custom_domain.com', - smtp_to_list=['dest@xn--example--i1a.com'], - message_from='"Name" ', - from_filter='custom_domain.com', - ) - - # Test when forcing the mail server and when smtp_encryption is "starttls" - self.server_domain.smtp_encryption = "starttls" - with self.mock_smtplib_connection(): - message = self._build_email(mail_from='specific_user@test.mycompany.com') - IrMailServer.send_email(message, mail_server_id=self.server_domain.id) - - self.connect_mocked.assert_called_once() - self.assert_email_sent_smtp( - smtp_from='specific_user@test.mycompany.com', - message_from='specific_user@test.mycompany.com', - from_filter='test.mycompany.com', - ) - - @mute_logger('odoo.models.unlink') - def test_mail_server_send_email_smtp_session(self): - """Test all the cases when we provide the SMTP session. - - The results must be the same as passing directly the parameter to "send_email". - """ - IrMailServer = self.env['ir.mail_server'] - default_bounce_adress = self.env['ir.mail_server']._get_default_bounce_address() - - # A mail server is configured for the email - with self.mock_smtplib_connection(): - smtp_session = IrMailServer.connect(smtp_from='specific_user@test.mycompany.com') - message = self._build_email(mail_from='specific_user@test.mycompany.com') - IrMailServer.send_email(message, smtp_session=smtp_session) - - self.connect_mocked.assert_called_once() - self.assert_email_sent_smtp( - smtp_from='specific_user@test.mycompany.com', - message_from='specific_user@test.mycompany.com', - from_filter='specific_user@test.mycompany.com', - ) - - # No mail server are configured for the email address, - # so it will use the notifications email instead and encapsulate the old email - with self.mock_smtplib_connection(): - smtp_session = IrMailServer.connect(smtp_from='"Name" ') - message = self._build_email(mail_from='"Name" ') - IrMailServer.send_email(message, smtp_session=smtp_session) - - self.connect_mocked.assert_called_once() - self.assert_email_sent_smtp( - smtp_from='notifications@test.mycompany.com', - message_from='"Name" ', - from_filter='notifications@test.mycompany.com', - ) - - # A mail server is configured for the entire domain name, so we can use the bounce - # email address because the mail server supports it - with self.mock_smtplib_connection(): - smtp_session = IrMailServer.connect(smtp_from='unknown_name@test.mycompany.com') - message = self._build_email(mail_from='unknown_name@test.mycompany.com') - IrMailServer.send_email(message, smtp_session=smtp_session) - - self.connect_mocked.assert_called_once() - self.assert_email_sent_smtp( - smtp_from=default_bounce_adress, - message_from='unknown_name@test.mycompany.com', - from_filter='test.mycompany.com', - ) - - # remove the notification server - # so will use the mail server - self.server_notification.unlink() - - # The mail server configured for the notifications email has been removed - # but we can still use the mail server configured for test.mycompany.com - with self.mock_smtplib_connection(): - smtp_session = IrMailServer.connect(smtp_from='"Name" ') - message = self._build_email(mail_from='"Name" ') - IrMailServer.send_email(message, smtp_session=smtp_session) - - self.connect_mocked.assert_called_once() - self.assert_email_sent_smtp( - smtp_from=default_bounce_adress, - message_from='"Name" ', - from_filter='test.mycompany.com', - ) + self.assertSMTPEmailsSent( + smtp_from=expected_smtp_from, + smtp_to_list=['dest@xn--example--i1a.com'], + message_from=expected_msg_from, + from_filter=expected_from_filter, + ) @mute_logger('odoo.models.unlink') @patch.dict(config.options, {"from_filter": "test.mycompany.com", "smtp_server": "example.com"}) - def test_mail_server_binary_arguments_domain(self): + def test_mail_server_config_bin(self): """Test the configuration provided in the odoo-bin arguments. This config is used when no mail server exists. """ IrMailServer = self.env['ir.mail_server'] - default_bounce_adress = self.env['ir.mail_server']._get_default_bounce_address() # Remove all mail server so we will use the odoo-bin arguments self.env['ir.mail_server'].search([]).unlink() self.assertFalse(self.env['ir.mail_server'].search([])) - # Use an email in the domain of the "from_filter" + for email_from, (expected_smtp_from, expected_msg_from) in zip( + [ + # inside "from_filter" domain + 'specific_user@test.mycompany.com', + # outside "from_filter" domain + 'test@unknown_domain.com', + ], [ + (self.default_bounce_address, 'specific_user@test.mycompany.com'), + # 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" '), + ] + ): + with self.subTest(email_from=email_from): + with self.mock_smtplib_connection(): + message = self._build_email(mail_from=email_from) + IrMailServer.send_email(message) + + self.connect_mocked.assert_called_once() + self.assertSMTPEmailsSent( + smtp_from=expected_smtp_from, + message_from=expected_msg_from, + from_filter='test.mycompany.com', + ) + + @mute_logger('odoo.models.unlink') + @patch.dict(config.options, {'from_filter': 'test.mycompany.com', 'smtp_server': 'example.com'}) + def test_mail_server_config_bin_default_from_filter(self): + """Test that the config parameter "mail.default.from_filter" overwrites + the odoo-bin argument "--from-filter" """ + self.env['ir.config_parameter'].sudo().set_param('mail.default.from_filter', 'example.com') + + IrMailServer = self.env['ir.mail_server'] + + # Remove all mail server so we will use the odoo-bin arguments + IrMailServer.search([]).unlink() + self.assertFalse(IrMailServer.search([])) + + # Use an email in the domain of the config parameter "mail.default.from_filter" with self.mock_smtplib_connection(): - message = self._build_email(mail_from='specific_user@test.mycompany.com') + message = self._build_email(mail_from='specific_user@example.com') IrMailServer.send_email(message) - self.connect_mocked.assert_called_once() - self.assert_email_sent_smtp( - smtp_from=default_bounce_adress, - message_from='specific_user@test.mycompany.com', - from_filter='test.mycompany.com', - ) - - # Test if the domain name is normalized before comparison - with self.mock_smtplib_connection(): - message = self._build_email(mail_from='specific_user@test.mycompany.com') - IrMailServer.send_email(message) - - self.connect_mocked.assert_called_once() - self.assert_email_sent_smtp( - smtp_from=default_bounce_adress, - message_from='specific_user@test.mycompany.com', - from_filter='test.mycompany.com', - ) - - # Use an email outside of the domain of the "from_filter" - # So we will use the notifications email in the headers and the bounce address - # in the envelop because the "from_filter" allows to use the entire domain - with self.mock_smtplib_connection(): - message = self._build_email(mail_from='test@unknown_domain.com') - IrMailServer.send_email(message) - - self.connect_mocked.assert_called_once() - self.assert_email_sent_smtp( - smtp_from=default_bounce_adress, - message_from='"test" ', - from_filter='test.mycompany.com', + self.assertSMTPEmailsSent( + smtp_from='specific_user@example.com', + message_from='specific_user@example.com', + from_filter='example.com', ) @mute_logger('odoo.models.unlink') @@ -493,77 +453,79 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): "from_filter": "dummy@example.com, test.mycompany.com, dummy2@example.com", "smtp_server": "example.com", }) - def test_mail_server_binary_arguments_domain_smtp_session(self): + def test_mail_server_config_bin_smtp_session(self): """Test the configuration provided in the odoo-bin arguments. This config is used when no mail server exists. Use a pre-configured SMTP session. """ IrMailServer = self.env['ir.mail_server'] - default_bounce_adress = self.env['ir.mail_server']._get_default_bounce_address() # Remove all mail server so we will use the odoo-bin arguments self.env['ir.mail_server'].search([]).unlink() self.assertFalse(self.env['ir.mail_server'].search([])) - # Use an email in the domain of the "from_filter" - with self.mock_smtplib_connection(): - smtp_session = IrMailServer.connect(smtp_from='specific_user@test.mycompany.com') - message = self._build_email(mail_from='specific_user@test.mycompany.com') - IrMailServer.send_email(message, smtp_session=smtp_session) + for email_from, (expected_smtp_from, expected_msg_from) in zip( + [ + # use an email in the domain of the "from_filter" + 'specific_user@test.mycompany.com', + # Use an email outside of the domain of the "from_filter" + 'test@unknown_domain.com', + ], [ + (self.default_bounce_address, 'specific_user@test.mycompany.com'), + # Use an email outside of the domain of the "from_filter" + # So we will use the notifications email in the headers and the bounce address + # in the envelop because the "from_filter" allows to use the entire domain + (self.default_bounce_address, '"test" ') + ] + ): + with self.subTest(email_from=email_from): + with self.mock_smtplib_connection(): + smtp_session = IrMailServer.connect(smtp_from=email_from) + message = self._build_email(mail_from=email_from) + IrMailServer.send_email(message, smtp_session=smtp_session) - self.connect_mocked.assert_called_once() - self.assert_email_sent_smtp( - smtp_from=default_bounce_adress, - message_from='specific_user@test.mycompany.com', - from_filter='dummy@example.com, test.mycompany.com, dummy2@example.com', - ) + self.connect_mocked.assert_called_once() + self.assertSMTPEmailsSent( + smtp_from=expected_smtp_from, + message_from=expected_msg_from, + from_filter='dummy@example.com, test.mycompany.com, dummy2@example.com', + ) - # Use an email outside of the domain of the "from_filter" - # So we will use the notifications email in the headers and the bounce address - # in the envelop because the "from_filter" allows to use the entire domain - with self.mock_smtplib_connection(): - smtp_session = IrMailServer.connect(smtp_from='test@unknown_domain.com') - message = self._build_email(mail_from='test@unknown_domain.com') - IrMailServer.send_email(message, smtp_session=smtp_session) - - self.connect_mocked.assert_called_once() - self.assert_email_sent_smtp( - smtp_from=default_bounce_adress, - message_from='"test" ', - from_filter='dummy@example.com, test.mycompany.com, dummy2@example.com', - ) - - def test_mail_server_get_email_addresses(self): + def test_mail_server_get_test_email_addresses(self): """Test the email used to test the mail server connection.""" - self.server_notification.from_filter = 'example_2.com, example_3.com' + test_server = self.env['ir.mail_server'].create({ + 'from_filter': 'example_2.com, example_3.com', + 'name': 'Test Server', + 'smtp_host': 'smtp_host', + 'smtp_encryption': 'none', + }) - self.env['ir.config_parameter'].set_param('mail.default.from', 'notifications@example.com') - email_from = self.server_notification._get_test_email_addresses()[0] - self.assertEqual(email_from, 'noreply@example_2.com') - - self.env['ir.config_parameter'].set_param('mail.default.from', 'notifications') - email_from = self.server_notification._get_test_email_addresses()[0] - self.assertEqual(email_from, 'notifications@example_2.com') - - self.server_notification.from_filter = 'dummy.com, full_email@example_2.com, dummy2.com' - - self.env['ir.config_parameter'].set_param('mail.default.from', 'notifications') - email_from = self.server_notification._get_test_email_addresses()[0] - self.assertEqual(email_from, 'full_email@example_2.com') - - self.env['ir.config_parameter'].set_param('mail.default.from', 'notifications@example.com') - email_from = self.server_notification._get_test_email_addresses()[0] - self.assertEqual(email_from, 'full_email@example_2.com') - - self.env['ir.config_parameter'].set_param('mail.default.from', 'notifications@example.com') - self.server_notification.from_filter = 'example.com' - email_from = self.server_notification._get_test_email_addresses()[0] - self.assertEqual(email_from, 'notifications@example.com') + # check default.from / filter matching + for (default_from, from_filter), expected_test_email in zip( + [ + ('notifications@example.com', 'example_2.com, example_3.com'), + ('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'), + ], [ + 'noreply@example_2.com', + 'notifications@example_2.com', + 'full_email@example_2.com', + 'full_email@example_2.com', + 'notifications@example.com', + ], + ): + with self.subTest(default_from=default_from, from_filter=from_filter): + self.env['ir.config_parameter'].set_param('mail.default.from', default_from) + test_server.from_filter = from_filter + email_from = test_server._get_test_email_addresses()[0] + self.assertEqual(email_from, expected_test_email) @mute_logger('odoo.models.unlink') @patch.dict(config.options, {'from_filter': 'fake.com', 'smtp_server': 'cli_example.com'}) - def test_mail_server_configuration_cli(self): + def test_mail_server_config_cli(self): """Check the mail server when the "smtp_authentication" is "cli". Should take the configuration from the odoo-bin argument. @@ -589,7 +551,7 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): message = self._build_email(mail_from='test@cli_example.com') IrMailServer.send_email(message) - self.assert_email_sent_smtp( + self.assertSMTPEmailsSent( smtp_from='test@cli_example.com', message_from='test@cli_example.com', from_filter='dummy@example.com, cli_example.com, dummy2@example.com', @@ -600,33 +562,8 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): message = self._build_email(mail_from='specific_user@test.mycompany.com') IrMailServer.send_email(message) - self.assert_email_sent_smtp( + self.assertSMTPEmailsSent( smtp_from='specific_user@test.mycompany.com', message_from='specific_user@test.mycompany.com', from_filter='specific_user@test.mycompany.com', ) - - @mute_logger('odoo.models.unlink') - @patch.dict(config.options, {'from_filter': 'test.mycompany.com', 'smtp_server': 'example.com'}) - def test_mail_server_mail_default_from_filter(self): - """Test that the config parameter "mail.default.from_filter" overwrite the odoo-bin - argument "--from-filter" - """ - self.env['ir.config_parameter'].sudo().set_param('mail.default.from_filter', 'example.com') - - IrMailServer = self.env['ir.mail_server'] - - # Remove all mail server so we will use the odoo-bin arguments - IrMailServer.search([]).unlink() - self.assertFalse(IrMailServer.search([])) - - # Use an email in the domain of the config parameter "mail.default.from_filter" - with self.mock_smtplib_connection(): - message = self._build_email(mail_from='specific_user@example.com') - IrMailServer.send_email(message) - - self.assert_email_sent_smtp( - smtp_from='specific_user@example.com', - message_from='specific_user@example.com', - from_filter='example.com', - )