# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from unittest.mock import patch from odoo.addons.mail.tests.common import MailCommon from odoo.tests import tagged, users from odoo.tools import config, mute_logger @tagged('mail_server') class TestIrMailServer(MailCommon): @classmethod def setUpClass(cls): super().setUpClass() 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) @patch.dict(config.options, {"email_from": "settings@example.com"}) def test_default_email_from(self, *args): """ Check that default_from parameter of alias domain respected. """ for (default_from, domain_name), expected_from in zip( [ ('icp', 'test.mycompany.com'), (False, 'test.mycompany.com'), (False, False), ], [ "icp@test.mycompany.com", "settings@example.com", "settings@example.com", ], ): with self.subTest(default_from=default_from, domain_name=domain_name): if domain_name: self.mail_alias_domain.name = domain_name self.mail_alias_domain.default_from = default_from self.env.company.alias_domain_id = self.mail_alias_domain else: self.env.company.alias_domain_id = False message = self.env["ir.mail_server"].build_email( False, "recipient@example.com", "Subject", "The body of an email", ) self.assertEqual(message["From"], expected_from) @mute_logger('odoo.models.unlink') @patch.dict(config.options, { "from_filter": "dummy@example.com, test.mycompany.com, dummy2@example.com", "smtp_server": "example.com", }) 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. Test with and without giving a pre-configured SMTP session, should not impact results. Also check "mail.default.from_filter" parameter usage that should overwrite odoo-bin argument "--from-filter". """ 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([])) for mail_from, (expected_smtp_from, expected_msg_from) in zip( [ # inside "from_filter" domain 'specific_user@test.mycompany.com', '"Formatted Name" ', '"Formatted Name" ', '"Formatted Name" ', # outside "from_filter" domain 'test@unknown_domain.com', '"Formatted Name" ', ], [ # inside "from_filter" domain: no rewriting (self.default_bounce_address, 'specific_user@test.mycompany.com'), (self.default_bounce_address, '"Formatted Name" '), (self.default_bounce_address, '"Formatted Name" '), (self.default_bounce_address, '"Formatted Name" '), # 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, 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 with self.subTest(mail_from=mail_from, provide_smtp=provide_smtp): 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.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="dummy@example.com, test.mycompany.com, dummy2@example.com", ) # for from_filter in ICP, overwrite the one from odoo-bin self.env['ir.config_parameter'].sudo().set_param('mail.default.from_filter', 'icp.example.com') # 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@icp.example.com') IrMailServer.send_email(message) self.assertSMTPEmailsSent( smtp_from='specific_user@icp.example.com', message_from='specific_user@icp.example.com', from_filter='icp.example.com', ) @users('admin') def test_mail_server_get_test_email_from(self): """ Test the email used to test the mail server connection. Check from_filter parsing / alias_domain.default_from support. """ 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', }) # check default.from / filter matching for (default_from, from_filter), expected_test_email in zip( [ ('notifications', 'dummy.com, full_email@example_2.com, dummy2.com'), ('notifications', self.mail_alias_domain.name), ('notifications', f'{self.mail_alias_domain.name}, example_2.com'), # default relies on "odoo" (False, self.mail_alias_domain.name), # fallback on user email if no from_filter ('notifications', ' '), ('notifications', ','), ('notifications', False), (False, False), ], [ 'full_email@example_2.com', f'notifications@{self.mail_alias_domain.name}', f'notifications@{self.mail_alias_domain.name}', f'odoo@{self.mail_alias_domain.name}', self.env.user.email, self.env.user.email, self.env.user.email, self.env.user.email, ], ): with self.subTest(default_from=default_from, from_filter=from_filter): self.mail_alias_domain.default_from = default_from test_server.from_filter = from_filter email_from = test_server._get_test_email_from() self.assertEqual(email_from, expected_test_email) @mute_logger('odoo.models.unlink') def test_mail_server_priorities(self): """ Test if we choose the right mail server to send an email. Priorities are 1. Forced mail server (e.g.: in mass mailing) - If the "from_filter" of the mail server match the notification email use the notifications email in the "From header" - Otherwise spoof the "From" (because we force the mail server but we don't know which email use to send it) 2. A mail server for which the "from_filter" match the "From" header 3. A mail server for which the "from_filter" match the domain of the "From" header 4. The mail server used for notifications 5. A mail server without "from_filter" (and so spoof the "From" header because we do not know for which email address it can be used) """ # this mail server can now be used for a specific email address and 2 domain names self.mail_server_user.from_filter = "domain1.com, specific_user@test.mycompany.com, domain2.com" for email_from, (expected_mail_server, expected_email_from) in zip( [ # matches user-specific server 'specific_user@test.mycompany.com', # matches user-specific server (with formatting') -> should extract # email from full name, must keep the given email_from '"Name name@strange.name" ', # case check 'SPECIFIC_USER@test.mycompany.com', 'specific_user@test.MYCOMPANY.com', # matches domain-based server: domain is case insensitive 'unknown_email@test.mycompany.com', 'unknown_email@TEST.MYCOMPANY.COM', '"Unknown" ', # fallback on notification email '"Test" ', # mail_server_user multiple from_filter check: can be used for a # specific email and 2 domain names -> check other domains in filter '"Example" ', '"Example" ', ], [ (self.mail_server_user, 'specific_user@test.mycompany.com'), (self.mail_server_user, '"Name name@strange.name" '), (self.mail_server_user, 'SPECIFIC_USER@test.mycompany.com'), (self.mail_server_user, 'specific_user@test.MYCOMPANY.com'), (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, f'{self.default_from}@test.mycompany.com'), # mail_server_user multiple from_filter check (self.mail_server_user, '"Example" '), (self.mail_server_user, '"Example" '), ], ): 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) @mute_logger('odoo.models.unlink', 'odoo.addons.base.models.ir_mail_server') def test_mail_server_send_email(self): """ Test main 'send_email' usage: check mail_server choice based on from filters, encapsulation, spoofing. """ IrMailServer = self.env['ir.mail_server'] for mail_from, (expected_smtp_from, expected_msg_from, expected_mail_server) in zip( [ 'specific_user@test.mycompany.com', '"Name" ', 'test@unknown_domain.com', '"Name" ' ], [ # A mail server is configured for the email ('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 (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 (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), ], ): # test with and without providing an SMTP session, which should not impact test for provide_smtp in [True, False]: 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.connect_mocked.assert_called_once() self.assertEqual(len(self.emails), 1) self.assertSMTPEmailsSent( smtp_from=expected_smtp_from, message_from=expected_msg_from, mail_server=expected_mail_server, ) # remove the notification server # so will use the mail server # 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" self.mail_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.connect_mocked.assert_called_once() self.assertEqual(len(self.emails), 1) self.assertSMTPEmailsSent( smtp_from=self.default_bounce_address, message_from=f'"Name" <{self.default_from}@{self.alias_domain}>', mail_server=self.mail_server_domain, ) # miss-configured database, no mail servers from filter # match the user / notification email self.env['ir.mail_server'].search([]).from_filter = "random.domain" self.mail_alias_domain.default_from = 'test' self.mail_alias_domain.name = 'custom_domain.com' with self.mock_smtplib_connection(): message = self._build_email(mail_from='specific_user@test.com') IrMailServer.send_email(message) self.connect_mocked.assert_called_once() self.assertSMTPEmailsSent( smtp_from='test@custom_domain.com', message_from='"specific_user" ', from_filter='random.domain', )