Files
odoo_source/addons/mail/tests/test_ir_mail_server.py
T
Thibault Delavallée 9345060498 [IMP] mail: respect alias domains in 'mail.mail' and outgoing emails
PURPOSE

Allow alias domains to be multiple, notably to be used in a multi company
environment where each company has its own alias domain.

SPECIFICATIONS: MAIL.MAIL

Update MailMail to use alias domains. Notably "Return-Path" headers are
now computed based on alias domain when possible, using the recently added
fields on 'mail.message' model for that purpose. Fallback is to use current
company's bounce email when mail_mail creation is done outside of classic
mail flows or without that information.

SPECIFICATIONS: IR.MAIL.SERVER

Update IrMailServer and low-level stack to use alias domains. This has an
impact notably on default values computation for from and bounce emails

  * '_get_default_bounce_address' is called when there is no 'Return-Path'
    given. Most classic mail flows will set it according to current record
    company / alias domain. Fallback when not set is to fallback on current
    company's bounce email, computed based on its alias domain;
  * '_get_default_from_address' is used in two use cases
    * computing a default 'email_from' for outgoing emails when it is not set.
      In most classic mail flows it is set based on current user's email. If
      not set fallback on current company's notification emails is considered
      as a safe bet, replacing the global configuration parameter;
    * overriding the 'email_from' of emails that are considered spoofing the
      mail server, allowing to wrap the sending into a 'notifications@domain'
      generic sender. For those we should try to keep record's information as
      it may be called in classic mail flows;
  * '_get_default_from_filter' is added in base and overridden in mail to
    either use 'mail.default.from_filter' ICP, or use the one defined on
    the alias domain. Supporting both is still an option, as its behavior
    is implemented for basic email sending, without mail being available.

Those methods are updated to try to support multi domains / multi company
setup. However as those defaults are located ar ir.mail_server level it is
not always easy to have complete environment information, hence fallbacking
on current company's parameters when no better information is provided.

A test about 'mail.default.from' is removed, as it was testing a default_from
outside of catchall domain. It is not possible anymore as default_from is now
part of domain definition. As multi domains is supported, no need to support
exotic configuration like that.

SPECIFICATIONS: FROM MAIL.MAIL TO OUTGOING EMAILS

When sending emails based on MailMail, we now prepares sending groups based
on MailServer, email_from, but also alias domain to which the mail belongs to.
Information about alias domain (e.g. notifications email based on default_from
and bounce email) is propagated to low-level email preparation methods. It
uses the context as it is the easiest way to propagate information to that
level without hacking too much models or calls.

Task-36879 (Mail: Support Multi Domains Aliases)

Part-of: odoo/odoo#76734
2023-10-24 19:24:50 +00:00

295 lines
15 KiB
Python

# -*- 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" <specific_user@test.mycompany.com>',
'"Formatted Name" <specific_user@test.MYCOMPANY.com>',
'"Formatted Name" <SPECIFIC_USER@test.mycompany.com>',
# outside "from_filter" domain
'test@unknown_domain.com',
'"Formatted Name" <test@unknown_domain.com>',
], [
# inside "from_filter" domain: no rewriting
(self.default_bounce_address, 'specific_user@test.mycompany.com'),
(self.default_bounce_address, '"Formatted Name" <specific_user@test.mycompany.com>'),
(self.default_bounce_address, '"Formatted Name" <specific_user@test.MYCOMPANY.com>'),
(self.default_bounce_address, '"Formatted Name" <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, 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" <specific_user@test.mycompany.com>',
# 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" <unknown_email@test.mycompany.com>',
# fallback on notification email
'"Test" <test@unknown_domain.com>',
# mail_server_user multiple from_filter check: can be used for a
# specific email and 2 domain names -> check other domains in filter
'"Example" <test@domain2.com>',
'"Example" <test@domain1.com>',
], [
(self.mail_server_user, 'specific_user@test.mycompany.com'),
(self.mail_server_user, '"Name name@strange.name" <specific_user@test.mycompany.com>'),
(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" <unknown_email@test.mycompany.com>'),
(self.mail_server_notification, f'{self.default_from}@test.mycompany.com'),
# mail_server_user multiple from_filter check
(self.mail_server_user, '"Example" <test@domain2.com>'),
(self.mail_server_user, '"Example" <test@domain1.com>'),
],
):
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>',
'test@unknown_domain.com',
'"Name" <unknown_name@test.mycompany.com>'
], [
# 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" <unknown_name@test.mycompany.com>', 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 <notifications.test@test.mycompany.com> will use the <test.mycompany.com> 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" <test@unknown_domain.com>')
message = self._build_email(mail_from='"Name" <test@unknown_domain.com>')
IrMailServer.send_email(message, smtp_session=smtp_session)
else:
message = self._build_email(mail_from='"Name" <test@unknown_domain.com>')
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,
)