diff --git a/odoo/addons/base/models/ir_mail_server.py b/odoo/addons/base/models/ir_mail_server.py index ca06f8795e0..c6546290f8d 100644 --- a/odoo/addons/base/models/ir_mail_server.py +++ b/odoo/addons/base/models/ir_mail_server.py @@ -96,7 +96,7 @@ class IrMailServer(models.Model): name = fields.Char(string='Name', required=True, index=True) from_filter = fields.Char( "FROM Filtering", - help='Define for which email address or domain this server can be used.\n' + help='Comma-separated list of addresses or domains for which this server can be used.\n' 'e.g.: "notification@odoo.com" or "odoo.com"') smtp_host = fields.Char(string='SMTP Server', help="Hostname or IP of SMTP server") smtp_port = fields.Integer(string='SMTP Port', default=25, help="SMTP Port. Usually 465 for SSL, and 25 or 587 for other cases.") @@ -204,20 +204,21 @@ class IrMailServer(models.Model): self.ensure_one() email_to = "noreply@odoo.com" if self.from_filter: - if "@" in self.from_filter: + from_filter_parts = [part.strip() for part in self.from_filter.split(",")] + 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 self.from_filter, email_to + return mail_from, email_to # 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}@{self.from_filter}", email_to + return f"{default_from}@{from_filter_parts[0]}", email_to 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, email_to # the from_filter is configured for a domain different that the one # of the full email configured in mail.default.from - return f"noreply@{self.from_filter}", email_to + return f"noreply@{from_filter_parts[0]}", email_to # Fallback to current user email if there's no from filter email_from = self.env.user.email if not email_from: @@ -733,30 +734,33 @@ class IrMailServer(models.Model): if mail_servers is None: mail_servers = self.sudo().search([], order='sequence') - # 1. Try to find a mail server for the right mail from - mail_server = mail_servers.filtered(lambda m: email_normalize(m.from_filter) == email_from_normalized) - if mail_server: - return mail_server[0], email_from + def first_match(target, normalize_method): + for mail_server in mail_servers: + if mail_server.from_filter and any( + normalize_method(email.strip()) == target + for email in mail_server.from_filter.split(',') + ): + return mail_server - mail_server = mail_servers.filtered(lambda m: email_domain_normalize(m.from_filter) == email_from_domain) - if mail_server: - return mail_server[0], email_from + # 1. Try to find a mail server for the right mail from + if mail_server := first_match(email_from_normalized, email_normalize): + return mail_server, email_from + + if mail_server := first_match(email_from_domain, email_domain_normalize): + return mail_server, email_from # 2. Try to find a mail server for if notifications_email: - mail_server = mail_servers.filtered(lambda m: email_normalize(m.from_filter) == notifications_email) - if mail_server: - return mail_server[0], notifications_email + if mail_server := first_match(notifications_email, email_normalize): + return mail_server, notifications_email - mail_server = mail_servers.filtered(lambda m: email_domain_normalize(m.from_filter) == notifications_domain) - if mail_server: - return mail_server[0], notifications_email + if mail_server := first_match(notifications_domain, email_domain_normalize): + return mail_server, notifications_email # 3. Take the first mail server without "from_filter" because # nothing else has been found... Will spoof the FROM because # we have no other choices - mail_server = mail_servers.filtered(lambda m: not m.from_filter) - if mail_server: + if mail_server := mail_servers.filtered(lambda m: not m.from_filter): return mail_server[0], email_from # 4. Return the first mail server even if it was configured for another domain @@ -786,10 +790,15 @@ class IrMailServer(models.Model): return True normalized_mail_from = email_normalize(email_from) - if '@' in from_filter: - return email_normalize(from_filter) == normalized_mail_from + normalized_domain = email_domain_extract(normalized_mail_from) - return email_domain_extract(normalized_mail_from) == email_domain_normalize(from_filter) + for email_filter in from_filter.split(','): + email_filter = email_filter.strip() + if '@' in email_filter and email_normalize(email_filter) == normalized_mail_from: + return True + if '@' not in email_filter and email_domain_normalize(email_filter) == normalized_domain: + return True + return False @api.onchange('smtp_encryption') def _onchange_encryption(self): diff --git a/odoo/addons/base/tests/test_ir_mail_server.py b/odoo/addons/base/tests/test_ir_mail_server.py index 4d6329f1549..47cdcc6cf2d 100644 --- a/odoo/addons/base/tests/test_ir_mail_server.py +++ b/odoo/addons/base/tests/test_ir_mail_server.py @@ -41,6 +41,7 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): ('admin@mail.example.com', False), ('"fake@test.com" ', 'mail.example.com'), ('"fake@test.com" ', 'mail.example.com'), + ('"fake@test.com" ', 'test.com, mail.example.com, test2.com'), ] for email, from_filter in tests: self.assertTrue(match_from_filter(email, from_filter)) @@ -53,6 +54,7 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): ('admin@mmail.example.com', 'mail.example.com'), ('admin@mail.example.com', 'mmail.example.com'), ('"admin@mail.example.com" ', 'mail.example.com'), + ('"fake@test.com" ', 'test.com, wrong.mail.example.com, test3.com'), ] for email, from_filter in tests: self.assertFalse(match_from_filter(email, from_filter)) @@ -138,6 +140,23 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): self.assertEqual(mail_server, self.server_notification, 'Should take the notification email') self.assertEqual(mail_from, 'notifications@test.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.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" ') + # 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) @@ -391,7 +410,10 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): ) @mute_logger('odoo.models.unlink') - @patch.dict(config.options, {"from_filter": "test.com", "smtp_server": "example.com"}) + @patch.dict(config.options, { + "from_filter": "dummy@example.com, test.com, dummy2@example.com", + "smtp_server": "example.com", + }) def test_mail_server_binary_arguments_domain_smtp_session(self): """Test the configuration provided in the odoo-bin arguments. @@ -415,7 +437,7 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): self.assert_email_sent_smtp( smtp_from=default_bounce_adress, message_from='specific_user@test.com', - from_filter='test.com', + from_filter='dummy@example.com, test.com, dummy2@example.com', ) # Use an email outside of the domain of the "from_filter" @@ -430,12 +452,12 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): self.assert_email_sent_smtp( smtp_from=default_bounce_adress, message_from='"test" ', - from_filter='test.com', + from_filter='dummy@example.com, test.com, dummy2@example.com', ) def test_mail_server_get_email_addresses(self): """Test the email used to test the mail server connection.""" - self.server_notification.from_filter = 'example_2.com' + self.server_notification.from_filter = 'example_2.com, example_3.com' self.env['ir.config_parameter'].set_param('mail.default.from', 'notifications@example.com') email_from = self.server_notification._get_test_email_addresses()[0] @@ -445,7 +467,7 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): email_from = self.server_notification._get_test_email_addresses()[0] self.assertEqual(email_from, 'notifications@example_2.com') - self.server_notification.from_filter = 'full_email@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] @@ -477,7 +499,7 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): 'smtp_host': 'smtp_host', 'smtp_encryption': 'none', 'smtp_authentication': 'cli', - 'from_filter': 'cli_example.com', + 'from_filter': 'dummy@example.com, cli_example.com, dummy2@example.com', }]) IrMailServer = self.env['ir.mail_server'] @@ -491,7 +513,7 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): self.assert_email_sent_smtp( smtp_from='test@cli_example.com', message_from='test@cli_example.com', - from_filter='cli_example.com', + from_filter='dummy@example.com, cli_example.com, dummy2@example.com', ) # other mail server still work diff --git a/odoo/addons/base/views/ir_mail_server_views.xml b/odoo/addons/base/views/ir_mail_server_views.xml index 26c53f10e3b..2478e32a4c8 100644 --- a/odoo/addons/base/views/ir_mail_server_views.xml +++ b/odoo/addons/base/views/ir_mail_server_views.xml @@ -15,7 +15,7 @@ - +