diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py index 3706a601b20..0d3fb705dda 100644 --- a/addons/mail/__manifest__.py +++ b/addons/mail/__manifest__.py @@ -86,6 +86,7 @@ For more specific needs, you may also assign custom-defined actions 'views/mail_shortcode_views.xml', 'views/mail_activity_views.xml', 'views/res_config_settings_views.xml', + 'data/ir_config_parameter_data.xml', 'data/res_partner_data.xml', 'data/mail_message_subtype_data.xml', 'data/mail_templates_chatter.xml', diff --git a/odoo/addons/base/data/ir_config_parameter_data.xml b/addons/mail/data/ir_config_parameter_data.xml similarity index 100% rename from odoo/addons/base/data/ir_config_parameter_data.xml rename to addons/mail/data/ir_config_parameter_data.xml diff --git a/addons/mail/models/ir_mail_server.py b/addons/mail/models/ir_mail_server.py index 316b46ed58e..3c7f52dcba9 100644 --- a/addons/mail/models/ir_mail_server.py +++ b/addons/mail/models/ir_mail_server.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import _, fields, models +from odoo import _, api, fields, models class IrMailServer(models.Model): @@ -21,3 +21,45 @@ class IrMailServer(models.Model): map(lambda t: _('%s (Email Template)', t.display_name), record.mail_template_ids) ) return usages_super + + @api.model + def _get_default_bounce_address(self): + """ Compute the default bounce address. Try to use mail-defined config + parameter bounce alias if set. """ + ICP = self.env['ir.config_parameter'].sudo() + bounce_alias = ICP.get_param('mail.bounce.alias') + domain = ICP.get_param('mail.catchall.domain') + if bounce_alias and domain: + return f'{bounce_alias}@{domain}' + return super()._get_default_from_address() + + @api.model + def _get_default_from_address(self): + """ Default from: try to use ``mail.default.from`` ICP either as a + full email address, either completed with ``mail.catchall.domain`` + to form a alias-based default from. """ + get_param = self.env['ir.config_parameter'].sudo().get_param + email_from = get_param("mail.default.from") + if email_from and "@" in email_from: + return email_from + if email_from: + if domain := get_param("mail.catchall.domain"): + return f"{email_from}@{domain}" + return super()._get_default_from_address() + + def _get_test_email_from(self): + self.ensure_one() + if from_filter_parts := [part.strip() for part in (self.from_filter or '').split(",") if part.strip()]: + # find first found complete email in filter parts + if mail_from := next((email for email in from_filter_parts if "@" in email), None): + return mail_from + # try to complete with default.from in same domain of first found filter parts + default_from = self.env["ir.config_parameter"].sudo().get_param("mail.default.from", "odoo") + if "@" not in default_from: + return f"{default_from}@{from_filter_parts[0]}" + # the mail server is configured for a domain that matches the default email address + if self._match_from_filter(default_from, self.from_filter): + return default_from + # no from_filter or from_filter is configured for a domain different that + # the one of the full email configured in mail.default.from -> fallback + return super()._get_test_email_from() diff --git a/addons/mail/tests/__init__.py b/addons/mail/tests/__init__.py index 47b456a3f75..43b2f48dd5c 100644 --- a/addons/mail/tests/__init__.py +++ b/addons/mail/tests/__init__.py @@ -1,5 +1,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import test_ir_mail_server from . import test_link_preview from . import test_mail_activity from . import test_mail_composer diff --git a/addons/mail/tests/test_ir_mail_server.py b/addons/mail/tests/test_ir_mail_server.py new file mode 100644 index 00000000000..ffc1324eda6 --- /dev/null +++ b/addons/mail/tests/test_ir_mail_server.py @@ -0,0 +1,338 @@ +# -*- 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 'mail.default.from' setting is respected. """ + ICP = self.env["ir.config_parameter"].sudo() + for (icp_from, icp_domain), expected_from in zip( + [ + ('icp', 'test.mycompany.com'), + ('icp', False), + ('icp@another.company.com', 'test.mycompany.com'), + ('icp@another.company.com', False), + (False, 'test.mycompany.com'), + ], [ + "icp@test.mycompany.com", + "settings@example.com", + "icp@another.company.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) + + @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, '"test" '), + (self.default_bounce_address, '"Formatted Name" '), + ] + ): + 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 / mail.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@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'), + # default relies on "odoo" + (False, 'example.com'), + # fallback on user email if no from_filter + ('example_2.com', ' '), + ('example_2.com', ','), + ('example_2.com', False), + (False, False), + ], [ + 'noreply@example_2.com', + 'notifications@example_2.com', + 'full_email@example_2.com', + 'full_email@example_2.com', + 'notifications@example.com', + 'odoo@example.com', + 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.env['ir.config_parameter'].set_param('mail.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, 'notifications.test@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') + 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 + ('notifications.test@test.mycompany.com', '"Name" ', self.mail_server_notification), + # same situation, but the original email has no name part + ('notifications.test@test.mycompany.com', '"test" ', 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='"Name" ', + mail_server=self.mail_server_domain, + ) + + 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 """ + other_default_from = 'notifications.other.test@custom_domain.com' + self.env['ir.config_parameter'].sudo().set_param('mail.default.from', other_default_from) + custom_server = self.env['ir.mail_server'].create({ + 'from_filter': 'custom_domain.com', + 'name': 'Custom Domain Server', + 'smtp_host': 'smtp_host', + 'smtp_encryption': 'none', + }) + + for mail_from, (expected_smtp_from, expected_msg_from, expected_mail_server) in zip( + [ + # custom server domain + '"Custom Server Name" ', + # unknown domain + '"Name" ', + # notification server domain + '"Name" ' + ], [ + ('customer.test@custom_domain.com', '"Custom Server Name" ', custom_server), + (other_default_from, '"Name" ', custom_server), + (self.default_bounce_address, '"Name" ', self.mail_server_domain), + ], + ): + 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.assertSMTPEmailsSent( + smtp_from=expected_smtp_from, + smtp_to_list=['dest@xn--example--i1a.com'], + message_from=expected_msg_from, + mail_server=expected_mail_server, + ) diff --git a/odoo/addons/base/__manifest__.py b/odoo/addons/base/__manifest__.py index d77e0f11e8e..f57cf22966b 100644 --- a/odoo/addons/base/__manifest__.py +++ b/odoo/addons/base/__manifest__.py @@ -21,7 +21,6 @@ The kernel of Odoo, needed for all installation. 'data/report_paperformat_data.xml', 'data/res_country_data.xml', 'data/ir_demo_data.xml', - 'data/ir_config_parameter_data.xml', 'security/base_groups.xml', 'security/base_security.xml', 'views/base_menus.xml', diff --git a/odoo/addons/base/models/ir_mail_server.py b/odoo/addons/base/models/ir_mail_server.py index 72ec2917507..1f3238b51eb 100644 --- a/odoo/addons/base/models/ir_mail_server.py +++ b/odoo/addons/base/models/ir_mail_server.py @@ -202,24 +202,17 @@ class IrMailServer(models.Model): def _get_test_email_from(self): self.ensure_one() + email_from = False if from_filter_parts := [part.strip() for part in (self.from_filter or '').split(",") if part.strip()]: - if mail_from := next((email for email in from_filter_parts if "@" in email), None): - # All emails will be sent from the same address - return mail_from - # All emails will be sent from any address in the same domain - default_from = self.env["ir.config_parameter"].sudo().get_param("mail.default.from", "odoo") - if "@" not in default_from: - return f"{default_from}@{from_filter_parts[0]}" - elif self._match_from_filter(default_from, self.from_filter): - # the mail server is configured for a domain - # that match the default email address - return default_from - # the from_filter is configured for a domain different that the one - # of the full email configured in mail.default.from - return f"noreply@{from_filter_parts[0]}" - # Fallback to current user email if there's no from filter - email_from = self.env.user.email + # find first found complete email in filter parts + email_from = next((email for email in from_filter_parts if "@" in email), False) + # no complete email -> consider noreply + if not email_from: + email_from = f"noreply@{from_filter_parts[0]}" if not email_from: + # Fallback to current user email if there's no from filter + email_from = self.env.user.email + if not email_from or "@" not in email_from: raise UserError(_('Please configure an email on the current user to simulate ' 'sending an email message via this outgoing server')) return email_from @@ -531,47 +524,20 @@ class IrMailServer(models.Model): @api.model def _get_default_bounce_address(self): - '''Compute the default bounce address. + """ Computes the default bounce address. It is used to set the envelop + address if no envelop address is provided in the message. - The default bounce address is used to set the envelop address if no - envelop address is provided in the message. It is formed by properly - joining the parameters "mail.bounce.alias" and - "mail.catchall.domain". - - If "mail.bounce.alias" is not set it defaults to "postmaster-odoo". - - If "mail.catchall.domain" is not set, return None. - - ''' - ICP = self.env['ir.config_parameter'].sudo() - bounce_alias = ICP.get_param('mail.bounce.alias') - domain = ICP.get_param('mail.catchall.domain') - if bounce_alias and domain: - return '%s@%s' % (bounce_alias, domain) - return + :return str/None: defaults to the ``--email-from`` CLI/config parameter. + """ + return tools.config.get("email_from") @api.model def _get_default_from_address(self): - """Compute the default from address. + """ Computes the default from address. It is used for the "header from" + address when no other has been received. - Used for the "header from" address when no other has been received. - - :return str/None: - If the config parameter ``mail.default.from`` contains - a full email address, return it. - Otherwise, combines config parameters ``mail.default.from`` and - ``mail.catchall.domain`` to generate a default sender address. - - If some of those parameters is not defined, it will default to the - ``--email-from`` CLI/config parameter. + :return str/None: defaults to the ``--email-from`` CLI/config parameter. """ - get_param = self.env['ir.config_parameter'].sudo().get_param - email_from = get_param("mail.default.from") - if email_from and "@" in email_from: - return email_from - domain = get_param("mail.catchall.domain") - if email_from and domain: - return "%s@%s" % (email_from, domain) return tools.config.get("email_from") def _prepare_email_message(self, message, smtp_session): diff --git a/odoo/addons/base/tests/common.py b/odoo/addons/base/tests/common.py index ffe3649861f..1e20d0416cf 100644 --- a/odoo/addons/base/tests/common.py +++ b/odoo/addons/base/tests/common.py @@ -399,13 +399,7 @@ class MockSmtplibCase: @classmethod def _init_mail_gateway(cls): - cls.alias_bounce = 'bounce.test' - cls.alias_domain = 'test.mycompany.com' - cls.default_from = 'notifications.test' 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 diff --git a/odoo/addons/base/tests/test_ir_mail_server.py b/odoo/addons/base/tests/test_ir_mail_server.py index f5666adac52..7557421649a 100644 --- a/odoo/addons/base/tests/test_ir_mail_server.py +++ b/odoo/addons/base/tests/test_ir_mail_server.py @@ -36,32 +36,13 @@ class _FakeSMTP: 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 = self.env["ir.config_parameter"].sudo() - for (icp_from, icp_domain), expected_from in zip( - [ - ('icp', 'test.mycompany.com'), - ('icp', False), - ('icp@another.company.com', 'test.mycompany.com'), - ('icp@another.company.com', False), - (False, 'test.mycompany.com'), - ], [ - "icp@test.mycompany.com", - "settings@example.com", - "icp@another.company.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) + def test_default_email_from(self): + """ Email from setting is respected and comes from configuration. """ + 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") @tagged('mail_server') @@ -70,19 +51,16 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): @classmethod def setUpClass(cls): super().setUpClass() - cls._init_mail_gateway() + # TEMPORARY + for icp in {'mail.default.from', 'mail.catchall.alias', 'mail.bounce.alias', 'mail.catchall.domain', 'mail.default.from_filter'}: + cls.env['ir.config_parameter'].sudo().set_param(icp, False) 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}' + cls.default_bounce_address = 'CACA' + cls.default_from_address = 'PROUT' 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) + self.assertFalse(self.env['ir.mail_server']._get_default_bounce_address()) + self.assertFalse(self.env['ir.mail_server']._get_default_from_address()) def test_bpo_34424_35805(self): """Ensure all email sent are bpo-34424 and bpo-35805 free""" @@ -164,44 +142,31 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): @users('admin') def test_mail_server_get_test_email_from(self): - """ Test the email used to test the mail server connection. """ + """ Test the email used to test the mail server connection. Check + from_filter parsing / default fallback value. """ 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( + for 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'), - # default relies on "odoo" - (False, 'example.com'), - # fallback on user email if no from_filter - ('example_2.com', ' '), - ('example_2.com', ','), - ('example_2.com', False), - (False, False), + 'example_2.com, example_3.com', + 'dummy.com, full_email@example_2.com, dummy2.com', + # fallback on user's email + ' ', + ',', + False, ], [ 'noreply@example_2.com', - 'notifications@example_2.com', 'full_email@example_2.com', - 'full_email@example_2.com', - 'notifications@example.com', - 'odoo@example.com', - 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.env['ir.config_parameter'].set_param('mail.default.from', default_from) + with self.subTest(from_filter=from_filter): test_server.from_filter = from_filter email_from = test_server._get_test_email_from() self.assertEqual(email_from, expected_test_email) @@ -236,73 +201,9 @@ 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. - - 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) - """ - # 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') - # this mail server can not 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, 'notifications.test@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') - 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) - self.env['ir.config_parameter'].sudo().set_param('mail.default.from', False) - + """ Test if we choose the right mail server to send an email. Simulates + simple Odoo DB so we have to spoof the FROM otherwise we cannot send + any email. """ for email_from, (expected_mail_server, expected_email_from) in zip( [ 'specific_user@test.mycompany.com', @@ -337,12 +238,12 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): ('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), + ('test@unknown_domain.com', '"Name" ', self.mail_server_default), # same situation, but the original email has no name part - ('notifications.test@test.mycompany.com', '"test" ', self.mail_server_notification), + ('test@unknown_domain.com', 'test@unknown_domain.com', self.mail_server_default), # 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), + ('unknown_name@test.mycompany.com', '"Name" ', self.mail_server_domain), ] ): # test with and without providing an SMTP session, which should not impact test @@ -385,9 +286,9 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): self.connect_mocked.assert_called_once() self.assertEqual(len(self.emails), 1) self.assertSMTPEmailsSent( - smtp_from=self.default_bounce_address, - message_from='"Name" ', - mail_server=self.mail_server_domain, + smtp_from='test@unknown_domain.com', + message_from='"Name" ', + from_filter=False, ) @mute_logger('odoo.models.unlink') @@ -400,52 +301,12 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): self.assertEqual(len(self.emails), 1) self.assertSMTPEmailsSent( - smtp_from=f'{self.alias_bounce}@xn--9caaaaaaa.com', + smtp_from='test@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 """ - other_default_from = 'notifications.other.test@custom_domain.com' - self.env['ir.config_parameter'].sudo().set_param('mail.default.from', other_default_from) - custom_server = self.env['ir.mail_server'].create({ - 'from_filter': 'custom_domain.com', - 'name': 'Custom Domain Server', - 'smtp_host': 'smtp_host', - 'smtp_encryption': 'none', - }) - - for mail_from, (expected_smtp_from, expected_msg_from, expected_mail_server) in zip( - [ - # custom server domain - '"Custom Server Name" ', - # unknown domain - '"Name" ', - # notification server domain - '"Name" ' - ], [ - ('customer.test@custom_domain.com', '"Custom Server Name" ', custom_server), - (other_default_from, '"Name" ', custom_server), - (self.default_bounce_address, '"Name" ', self.mail_server_domain), - ], - ): - 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.assertSMTPEmailsSent( - smtp_from=expected_smtp_from, - smtp_to_list=['dest@xn--example--i1a.com'], - message_from=expected_msg_from, - mail_server=expected_mail_server, - ) - @mute_logger('odoo.models.unlink') @patch.dict(config.options, { "from_filter": "dummy@example.com, test.mycompany.com, dummy2@example.com", @@ -477,15 +338,13 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): '"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, '"test" '), - (self.default_bounce_address, '"Formatted Name" '), + ('specific_user@test.mycompany.com', 'specific_user@test.mycompany.com'), + ('specific_user@test.mycompany.com', '"Formatted Name" '), + ('specific_user@test.MYCOMPANY.com', '"Formatted Name" '), + ('SPECIFIC_USER@test.mycompany.com', '"Formatted Name" '), + # outside "from_filter" domain: spoofing, as fallback email can be found + ('test@unknown_domain.com', 'test@unknown_domain.com'), + ('test@unknown_domain.com', '"Formatted Name" '), ] ): for provide_smtp in [False, True]: # providing smtp session should ont impact test