+
-
- @
+ @
+
@@ -392,8 +394,6 @@
-
-
diff --git a/addons/test_mail/models/test_mail_models.py b/addons/test_mail/models/test_mail_models.py
index 3152dc601eb..186460aa041 100644
--- a/addons/test_mail/models/test_mail_models.py
+++ b/addons/test_mail/models/test_mail_models.py
@@ -25,8 +25,8 @@ class MailTestSimple(models.Model):
_a = super()._notify_by_email_get_final_mail_values(*args, **kwargs)
return _a
- def _notify_by_email_get_headers(self):
- headers = super()._notify_by_email_get_headers()
+ def _notify_by_email_get_headers(self, headers=None):
+ headers = super()._notify_by_email_get_headers(headers=headers)
headers['X-Custom'] = 'Done'
return headers
@@ -95,6 +95,16 @@ class MailTestGateway(models.Model):
return super().message_new(msg_dict, custom_values=defaults)
+class MailTestGatewayCompany(models.Model):
+ """ A very simple model only inheriting from mail.thread to test pure mass
+ mailing features and base performances, with a company field. """
+ _description = 'Simple Chatter Model for Mail Gateway with company'
+ _name = 'mail.test.gateway.company'
+ _inherit = ['mail.test.gateway']
+
+ company_id = fields.Many2one('res.company', 'Company')
+
+
class MailTestGatewayGroups(models.Model):
""" A model looking like discussion channels / groups (flat thread and
alias). Used notably for advanced gatewxay tests. """
diff --git a/addons/test_mail/security/ir.model.access.csv b/addons/test_mail/security/ir.model.access.csv
index 28a9b72143d..be96b530ae1 100644
--- a/addons/test_mail/security/ir.model.access.csv
+++ b/addons/test_mail/security/ir.model.access.csv
@@ -11,6 +11,7 @@ access_mail_test_simple_main_attachment_portal,mail.test.simple.main.attachment.
access_mail_test_simple_main_attachment_user,mail.test.simple.main.attachment.user,model_mail_test_simple_main_attachment,base.group_user,1,1,1,1
access_mail_test_gateway_portal,mail.test.gateway.portal,model_mail_test_gateway,base.group_portal,1,0,0,0
access_mail_test_gateway_user,mail.test.gateway.user,model_mail_test_gateway,base.group_user,1,1,1,1
+access_mail_test_gateway_company_user,mail.test.gateway.company.user,model_mail_test_gateway_company,base.group_user,1,1,1,1
access_mail_test_gateway_groups_portal,mail.test.gateway.groups.portal,model_mail_test_gateway_groups,base.group_portal,1,0,0,0
access_mail_test_gateway_groups_user,mail.test.gateway.groups.user,model_mail_test_gateway_groups,base.group_user,1,1,1,1
access_mail_test_track_portal,mail.test.track.portal,model_mail_test_track,base.group_portal,0,0,0,0
diff --git a/addons/test_mail/tests/test_mail_alias.py b/addons/test_mail/tests/test_mail_alias.py
index 5f4e2002827..af639075c6a 100644
--- a/addons/test_mail/tests/test_mail_alias.py
+++ b/addons/test_mail/tests/test_mail_alias.py
@@ -19,6 +19,7 @@ class TestMailAliasCommon(MailCommon):
super().setUpClass()
cls.test_alias_mc = cls.env['mail.alias'].create({
+ 'alias_domain_id': cls.mail_alias_domain.id,
'alias_model_id': cls.env['ir.model']._get('mail.test.container.mc').id,
'alias_name': 'test.alias',
})
@@ -32,8 +33,7 @@ class TestMailAlias(TestMailAliasCommon):
def test_alias_domain_allowed_validation(self):
""" Check the validation of `mail.catchall.domain.allowed` system parameter"""
for value in [',', ',,', ', ,']:
- with self.assertRaises(exceptions.ValidationError,
- msg="The value '%s' should not be allowed" % value):
+ with self.assertRaises(exceptions.ValidationError):
self.env['ir.config_parameter'].set_param('mail.catchall.domain.allowed', value)
for value, expected in [
@@ -46,56 +46,72 @@ class TestMailAlias(TestMailAliasCommon):
self.env['ir.config_parameter'].set_param('mail.catchall.domain.allowed', value)
self.assertEqual(self.env['ir.config_parameter'].get_param('mail.catchall.domain.allowed'), expected)
- @users('admin')
- @mute_logger('odoo.models.unlink')
- def test_alias_domain_parameters(self):
- """ Check the validation of ``mail.bounce.alias`` and ``mail.catchall.alias``
- parameters. """
- ICP = self.env['ir.config_parameter']
- # sanitization
- for (bounce_value, catchall_value), (expected_bounce, expected_catchall) in zip(
- [
- ('bounce+b4r=*R3wl_#_-$€{}[]()~|\\/!?&%^\'"`~', 'catchall+b4r=*R3wl_#_-$€{}[]()~|\\/!?&%^\'"`~'),
- ('bounce+😊', 'catchall+😊'),
- ('Bouncâïde 😊', 'Catchôïee 😊'),
- ('ぁ', 'ぁぁ'),
- ],
- [
- ('bounce+b4r=*r3wl_#_-$-{}-~|-/!?&%^\'-`~', 'catchall+b4r=*r3wl_#_-$-{}-~|-/!?&%^\'-`~'),
- ('bounce+-', 'catchall+-'),
- ('bouncaide-', 'catchoiee-'),
- ('?', '??'),
- ]
- ):
- with self.subTest(bounce_value=bounce_value):
- ICP.set_param('mail.bounce.alias', bounce_value)
- self.assertEqual(ICP.get_param('mail.bounce.alias'), expected_bounce)
- with self.subTest(catchall_value=catchall_value):
- ICP.set_param('mail.catchall.alias', catchall_value)
- self.assertEqual(ICP.get_param('mail.catchall.alias'), expected_catchall)
+ @users('erp_manager')
+ def test_alias_domain_company_check(self):
+ """ Check constraint trying to avoid ill-defined company setup aka
+ having an alias domain on parent record / record to update that does
+ not match the alias domain. """
+ misc_alias_domain = self.env['mail.alias.domain'].create({'name': 'misc.com'})
+ record_mc_c1, record_mc_c2 = self.env['mail.test.container.mc'].create([
+ {
+ 'alias_name': 'Test1',
+ 'company_id': self.company_admin.id,
+ }, {
+ 'alias_name': 'Test2',
+ 'company_id': self.company_2.id,
+ }
+ ])
+ alias_mc_c1, alias_mc_c2 = record_mc_c1.alias_id, record_mc_c2.alias_id
+ self.assertEqual(
+ (alias_mc_c1 + alias_mc_c2).alias_parent_model_id,
+ self.env['ir.model']._get('mail.test.container.mc'))
+ self.assertEqual(
+ (alias_mc_c1 + alias_mc_c2).mapped('alias_parent_thread_id'),
+ (record_mc_c1 + record_mc_c2).ids)
+ self.assertEqual(alias_mc_c1.alias_domain_id, self.mail_alias_domain)
+ self.assertEqual(alias_mc_c2.alias_domain_id, self.mail_alias_domain_c2)
- # falsy values
- for config_value in [False, None, '', ' ']:
- with self.subTest(config_value=config_value):
- ICP.set_param('mail.bounce.alias', config_value)
- self.assertFalse(ICP.get_param('mail.bounce.alias'))
- ICP.set_param('mail.catchall.alias', config_value)
- self.assertFalse(ICP.get_param('mail.catchall.alias'))
+ # mail_alias_domain_c2 is linked to a conflicting company
+ with self.assertRaises(exceptions.ValidationError):
+ record_mc_c1.alias_domain_id = self.mail_alias_domain_c2
+ with self.assertRaises(exceptions.ValidationError):
+ alias_mc_c1.sudo().alias_domain_id = self.mail_alias_domain_c2
+ # misc_alias_domain is not linked to any company, therefore ok
+ record_mc_c1.alias_domain_id = misc_alias_domain
- # check successive param set, should not raise for unicity against itself
- for _ in range(2):
- ICP.set_param('mail.bounce.alias', 'bounce+double.test')
- ICP.set_param('mail.catchall.alias', 'catchall+double.test')
- self.assertEqual(ICP.get_param('mail.bounce.alias'), 'bounce+double.test')
- self.assertEqual(ICP.get_param('mail.catchall.alias'), 'catchall+double.test')
+ # alias updating records
+ record_upd_c1, record_upd_c2 = self.env['mail.test.alias.optional'].sudo().create([
+ {
+ 'alias_name': 'Update C1',
+ 'company_id': self.company_admin.id,
+ }, {
+ 'alias_name': 'Update C2',
+ 'company_id': self.company_2.id,
+ }
+ ])
+ alias_update_c1, alias_update_c2 = record_upd_c1.alias_id, record_upd_c2.alias_id
+ self.assertEqual(
+ (alias_update_c1 + alias_update_c2).mapped('alias_force_thread_id'),
+ (record_upd_c1 + record_upd_c2).ids)
+ self.assertEqual(alias_update_c1.alias_domain_id, self.mail_alias_domain)
+ self.assertEqual(alias_update_c2.alias_domain_id, self.mail_alias_domain_c2)
+
+ # mail_alias_domain_c2 is linked to a conflicting company
+ with self.assertRaises(exceptions.ValidationError):
+ record_upd_c1.alias_domain_id = self.mail_alias_domain_c2
+ with self.assertRaises(exceptions.ValidationError):
+ alias_update_c1.sudo().alias_domain_id = self.mail_alias_domain_c2
+ # misc_alias_domain is not linked to any company, therefore ok
+ record_upd_c1.alias_domain_id = misc_alias_domain
@users('admin')
def test_alias_name_unique(self):
""" Check uniqueness constraint on alias names, at create and update.
- Also check conflict management with bounce / catchall aliases. """
+ Also check conflict management with bounce / catchall defined on
+ alias domains. """
+ mail_alias_domain = self.mail_alias_domain.with_env(self.env)
+ mail_alias_domain_c2 = self.mail_alias_domain_c2.with_env(self.env)
alias_model_id = self.env['ir.model']._get('mail.test.gateway').id
- catchall_alias = self.env['ir.config_parameter'].sudo().get_param('mail.catchall.alias')
- bounce_alias = self.env['ir.config_parameter'].sudo().get_param('mail.bounce.alias')
new_mail_alias = self.env['mail.alias'].create({
'alias_model_id': alias_model_id,
@@ -105,18 +121,40 @@ class TestMailAlias(TestMailAliasCommon):
'alias_model_id': alias_model_id,
'alias_name': 'other.test.alias',
})
+ self.assertEqual((new_mail_alias + other_alias).alias_domain_id, mail_alias_domain)
- # test you cannot create or update aliases matching bounce / catchall
+ # test you cannot create or update aliases matching bounce / catchall of same alias domain
+ with self.assertRaises(exceptions.ValidationError), self.cr.savepoint():
+ self.env['mail.alias'].create({
+ 'alias_model_id': alias_model_id,
+ 'alias_name': mail_alias_domain.catchall_alias,
+ })
+ with self.assertRaises(exceptions.ValidationError), self.cr.savepoint():
+ self.env['mail.alias'].create({
+ 'alias_model_id': alias_model_id,
+ 'alias_name': mail_alias_domain.bounce_alias,
+ })
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
- self.env['mail.alias'].create({'alias_model_id': alias_model_id, 'alias_name': catchall_alias})
+ new_mail_alias.write({'alias_name': mail_alias_domain.catchall_alias})
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
- self.env['mail.alias'].create({'alias_model_id': alias_model_id, 'alias_name': bounce_alias})
- with self.assertRaises(exceptions.UserError), self.cr.savepoint():
- new_mail_alias.write({'alias_name': catchall_alias})
- with self.assertRaises(exceptions.UserError), self.cr.savepoint():
- new_mail_alias.write({'alias_name': bounce_alias})
+ new_mail_alias.write({'alias_name': mail_alias_domain.bounce_alias})
- # test that alias names should be unique
+ # other domains bounce / catchall do not prevent
+ new_aliases = self.env['mail.alias'].create([
+ {'alias_model_id': alias_model_id, 'alias_name': self.alias_catchall_c2},
+ {'alias_model_id': alias_model_id, 'alias_name': self.alias_bounce_c2},
+ ])
+ self.assertEqual(new_aliases.alias_domain_id, mail_alias_domain)
+ new_aliases.unlink()
+ # bounce/catchall of another domain is ok
+ new_mail_alias.write({'alias_name': mail_alias_domain_c2.bounce_alias})
+ other_alias.write({'alias_name': mail_alias_domain_c2.catchall_alias})
+ # changing domain would clash with existing catchall
+ with self.assertRaises(exceptions.UserError), self.cr.savepoint():
+ new_mail_alias.write({'alias_domain_id': mail_alias_domain_c2.id,})
+
+ new_mail_alias.write({'alias_name': 'unused.test.alias'})
+ # test that alias {name, alias_domain_id} should be unique
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
self.env['mail.alias'].create({
'alias_model_id': alias_model_id,
@@ -133,15 +171,48 @@ class TestMailAlias(TestMailAliasCommon):
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
other_alias.write({'alias_name': 'unused.test.alias'})
- # cannot set catchall / bounce to used alias
+ # also valid for void domain
+ nodom_alias = self.env['mail.alias'].create({
+ 'alias_domain_id': False,
+ 'alias_model_id': alias_model_id,
+ 'alias_name': 'no.domain',
+ })
+ self.assertFalse(nodom_alias.alias_domain_id)
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
- self.env['ir.config_parameter'].sudo().set_param('mail.catchall.alias', new_mail_alias.alias_name)
+ self.env['mail.alias'].create({
+ 'alias_domain_id': False,
+ 'alias_model_id': alias_model_id,
+ 'alias_name': 'no.domain',
+ })
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
- self.env['ir.config_parameter'].sudo().set_param('mail.bounce.alias', new_mail_alias.alias_name)
+ self.env['mail.alias'].create([
+ {
+ 'alias_domain_id': False,
+ 'alias_model_id': alias_model_id,
+ 'alias_name': 'dupes.wo.domain',
+ } for _x in range(2)
+ ])
+ with self.assertRaises(exceptions.UserError), self.cr.savepoint():
+ other_alias.write({
+ 'alias_domain_id': False,
+ 'alias_name': 'no.domain',
+ })
+
+ # test that alias name can be duplicated in case of different alias domains
+ other_domain_alias = self.env['mail.alias'].create({
+ 'alias_domain_id': mail_alias_domain_c2.id,
+ 'alias_model_id': alias_model_id,
+ 'alias_name': 'unused.test.alias'
+ })
+ self.assertEqual(other_domain_alias.alias_domain_id, mail_alias_domain_c2)
+ # changing domain would violate uniqueness
+ with self.assertRaises(exceptions.UserError), self.cr.savepoint():
+ other_domain_alias.write({'alias_domain_id': mail_alias_domain.id})
@users('admin')
def test_alias_name_unique_copy(self):
""" Check uniqueness constraint check when copying aliases """
+ mail_alias_domain = self.mail_alias_domain.with_env(self.env)
alias_model_id = self.env['ir.model']._get('mail.test.gateway').id
new_mail_alias = self.env['mail.alias'].create({
'alias_model_id': alias_model_id,
@@ -154,9 +225,11 @@ class TestMailAlias(TestMailAliasCommon):
# test that duplicating an alias should have blank name
copy_1 = new_mail_alias.copy()
self.assertFalse(copy_1.alias_name)
+ self.assertEqual(copy_1.alias_domain_id, mail_alias_domain)
# test sanitize of copy with new name
copy_2 = new_mail_alias.copy({'alias_name': 'test.alias.2.éè#'})
self.assertEqual(copy_2.alias_name, 'test.alias.2.ee#')
+ self.assertEqual(copy_2.alias_domain_id, mail_alias_domain)
# cannot batch update, would create duplicates
with self.assertRaises(exceptions.UserError):
@@ -231,6 +304,69 @@ class TestMailAlias(TestMailAliasCommon):
for alias in aliases:
self.assertEqual(alias.alias_name, False)
+ @users('admin')
+ def test_search(self):
+ """ Test search on aliases, notably searching on display_name which should
+ be split on searching on alias_name and alias_domain_id. """
+ # ensure existing aliases to ease future asserts
+ existing = self.env['mail.alias'].search([('alias_domain_id', '!=', False)])
+ self.assertEqual(existing.alias_domain_id, self.mail_alias_domain)
+ existing.write({'alias_name': False}) # don't be annoyed by existing aliases
+
+ mail_alias_domain = self.mail_alias_domain.with_env(self.env)
+ mail_alias_domain_c2 = self.mail_alias_domain_c2.with_env(self.env)
+ self.assertEqual(mail_alias_domain.name, 'test.mycompany.com')
+ self.assertEqual(mail_alias_domain_c2.name, 'test.mycompany2.com')
+
+ aliases = self.env['mail.alias'].create([
+ {
+ 'alias_model_id': self.env['ir.model']._get('mail.test.container.mc').id,
+ 'alias_name': f'test.search.{idx}',
+ 'alias_domain_id': domain.id,
+ }
+ for idx in range(5)
+ for domain in (mail_alias_domain + mail_alias_domain_c2)
+ ])
+ aliases_d1 = aliases.filtered(lambda a: a.alias_domain_id == mail_alias_domain)
+ aliases_d2 = aliases.filtered(lambda a: a.alias_domain_id == mail_alias_domain_c2)
+
+ # search on alias_name: classic search
+ self.assertEqual(
+ self.env['mail.alias'].search([('alias_name', 'ilike', 'test.search')]),
+ aliases
+ )
+
+ # search on alias_fullname: search on aggregated of {name}@{domain}
+ for search_term, expected, msg in [
+ ('mycompany', aliases,
+ 'Match all aliases on both domains as "mycompany" is contained in those two'),
+ (mail_alias_domain.name, aliases_d1,
+ 'Exact match on domain 1: should find all aliases in that domain'),
+ (mail_alias_domain_c2.name, aliases_d2,
+ 'Exact match on domain 2: should find all aliases in that domain'),
+ ('search.0@test.mycompany', aliases.filtered(lambda a: a.alias_name == 'test.search.0'),
+ 'Match in both domains'),
+ ('search.0@test.mycompany.com', aliases.filtered(lambda a: a.alias_name == 'test.search.0' and a.alias_domain_id == mail_alias_domain),
+ 'Match only in domain 1'),
+ ('search@test.mycompany.com', self.env['mail.alias'],
+ 'Does not match even as ilike'),
+ ]:
+ with self.subTest(search_term=search_term):
+ self.assertEqual(
+ self.env['mail.alias'].search([('alias_full_name', 'ilike', search_term)]),
+ expected, msg
+ )
+
+ # search using IN operator
+ for search_term, expected, msg in [
+ (['mycompany'], self.env['mail.alias'], 'mycompany is too vague: does not match a left- and right- part (!= ilike)'),
+ ([mail_alias_domain.name], self.env['mail.alias'], 'Match only right-part of aliases emails'),
+ ]:
+ with self.subTest(search_term=search_term):
+ self.assertEqual(self.env['mail.alias'].search([('alias_full_name', 'in', search_term)]),
+ expected, msg
+ )
+
@users('admin')
def test_alias_setup(self):
""" Test various constraints / configuration of alias model"""
@@ -250,26 +386,103 @@ class TestMailAlias(TestMailAliasCommon):
class TestAliasCompany(TestMailAliasCommon):
""" Test company / alias domain and configuration synchronization """
+ @mute_logger('odoo.models.unlink')
+ @users('erp_manager')
+ def test_alias_domain_setup(self):
+ """ Test synchronization of alias domain with companies when adding /
+ updating / removing alias domains """
+ mail_alias_domain = self.mail_alias_domain.with_env(self.env)
+ mail_alias_domain_c2 = self.mail_alias_domain_c2.with_env(self.env)
+
+ self.assertEqual(self.company_admin.alias_domain_id, mail_alias_domain)
+ self.assertEqual(self.company_2.alias_domain_id, mail_alias_domain_c2)
+
+ # cannot unlink alias domain as there are aliases linked to it
+ with self.assertRaises(psycopg2.errors.ForeignKeyViolation), self.cr.savepoint(), mute_logger('odoo.sql_db'):
+ mail_alias_domain.unlink()
+
+ # eject linked aliases then remove alias domain of first company; should
+ # not impact second company
+ self.env['mail.alias'].sudo().search([]).write({'alias_domain_id': False})
+ mail_alias_domain.unlink()
+ self.assertFalse(self.company_admin.alias_domain_id)
+ self.assertEqual(self.company_2.alias_domain_id, mail_alias_domain_c2)
+ self.assertFalse(self.test_alias_mc.alias_domain_id)
+
+ # remove all alias domains
+ self.env['mail.alias.domain'].search([]).unlink()
+ self.assertFalse(self.company_2.alias_domain_id)
+ self.assertEqual(self.company_2.bounce_email, '')
+ self.assertEqual(self.company_2.bounce_formatted, '')
+ self.assertEqual(self.company_2.catchall_email, '')
+ self.assertEqual(self.company_2.catchall_formatted, '')
+ self.assertFalse(self.company_2.default_from_email, '')
+ self.assertFalse(self.company_3.alias_domain_id)
+
+ # create a new alias domain -> consider as re-init, populate all companies
+ alias_domain_new = self.env['mail.alias.domain'].create({
+ 'bounce_alias': 'bounce.new',
+ 'catchall_alias': 'catchall.new',
+ 'name': 'test.global.bitnurk.com',
+ })
+ self.assertEqual(self.company_admin.alias_domain_id, alias_domain_new,
+ 'MC Alias: first domain should populate void companies')
+ self.assertEqual(self.company_2.alias_domain_id, alias_domain_new,
+ 'MC Alias: should take alias domain with lower sequence')
+ self.assertEqual(self.company_3.alias_domain_id, alias_domain_new,
+ 'MC Alias: should take alias domain with lower sequence')
+ self.assertEqual(self.test_alias_mc.alias_domain_id, alias_domain_new,
+ 'MC Alias: first domain should populate void aliases')
+
+ # manual update
+ self.company_2.alias_domain_id = alias_domain_new.id
+ self.assertEqual(self.company_2.alias_domain_id, alias_domain_new)
+ self.assertEqual(self.company_2.bounce_email, 'bounce.new@test.global.bitnurk.com')
+ self.assertEqual(self.company_2.catchall_email, 'catchall.new@test.global.bitnurk.com')
+
def test_assert_initial_values(self):
""" Test initial setup values: currently all companies share the same
alias configuration as it is unique. """
+ self.assertEqual(self.test_alias_mc.alias_domain_id, self.mail_alias_domain)
+
+ self.assertEqual(self.company_admin.alias_domain_id, self.mail_alias_domain)
+ self.assertEqual(self.company_admin.bounce_email, f'{self.alias_bounce}@{self.alias_domain}')
+ self.assertEqual(
+ self.company_admin.bounce_formatted,
+ formataddr((self.company_admin.name, f'{self.alias_bounce}@{self.alias_domain}'))
+ )
self.assertEqual(self.company_admin.catchall_email, f'{self.alias_catchall}@{self.alias_domain}')
self.assertEqual(
self.company_admin.catchall_formatted,
formataddr((self.company_admin.name, f'{self.alias_catchall}@{self.alias_domain}'))
)
+ self.assertEqual(self.company_admin.default_from_email, f'{self.default_from}@{self.alias_domain}')
- self.assertEqual(self.company_2.catchall_email, f'{self.alias_catchall}@{self.alias_domain}')
+ self.assertEqual(self.company_2.alias_domain_id, self.mail_alias_domain_c2)
+ self.assertEqual(self.company_2.bounce_email, f'{self.alias_bounce_c2}@{self.alias_domain_c2_name}')
+ self.assertEqual(
+ self.company_2.bounce_formatted,
+ formataddr((self.company_2.name, f'{self.alias_bounce_c2}@{self.alias_domain_c2_name}'))
+ )
+ self.assertEqual(self.company_2.catchall_email, f'{self.alias_catchall_c2}@{self.alias_domain_c2_name}')
self.assertEqual(
self.company_2.catchall_formatted,
- formataddr((self.company_2.name, f'{self.alias_catchall}@{self.alias_domain}'))
+ formataddr((self.company_2.name, f'{self.alias_catchall_c2}@{self.alias_domain_c2_name}'))
)
+ self.assertEqual(self.company_2.default_from_email, f'{self.alias_default_from_c2}@{self.alias_domain_c2_name}')
- self.assertEqual(self.company_3.catchall_email, f'{self.alias_catchall}@{self.alias_domain}')
+ self.assertEqual(self.company_3.alias_domain_id, self.mail_alias_domain_c3)
+ self.assertEqual(self.company_3.bounce_email, f'{self.alias_bounce_c3}@{self.alias_domain_c3_name}')
+ self.assertEqual(
+ self.company_3.bounce_formatted,
+ formataddr((self.company_3.name, f'{self.alias_bounce_c3}@{self.alias_domain_c3_name}'))
+ )
+ self.assertEqual(self.company_3.catchall_email, f'{self.alias_catchall_c3}@{self.alias_domain_c3_name}')
self.assertEqual(
self.company_3.catchall_formatted,
- formataddr((self.company_3.name, f'{self.alias_catchall}@{self.alias_domain}'))
+ formataddr((self.company_3.name, f'{self.alias_catchall_c3}@{self.alias_domain_c3_name}'))
)
+ self.assertEqual(self.company_3.default_from_email, f'{self.alias_default_from_c3}@{self.alias_domain_c3_name}')
@users('erp_manager')
def test_res_company_creation_alias_domain(self):
@@ -280,25 +493,182 @@ class TestAliasCompany(TestMailAliasCommon):
})
company.flush_recordset()
self.assertEqual(
- company.catchall_formatted,
- formataddr((company.name, f'{self.alias_catchall}@{self.alias_domain}'))
- )
+ company.alias_domain_id, self.mail_alias_domain,
+ 'Default alias domain: sequence based')
+
+ # respect forced value
+ company = self.env['res.company'].create({
+ 'alias_domain_id': self.mail_alias_domain_c2.id,
+ 'email': '"Yet Another Company"
',
+ 'name': 'Yet Another Company',
+ })
+ self.assertEqual(company.alias_domain_id, self.mail_alias_domain_c2)
@tagged('mail_gateway', 'mail_alias', 'multi_company')
+class TestMailAliasDomain(TestMailAliasCommon):
+
+ @users('admin')
+ def test_alias_domain_config_alias_clash(self):
+ """ Domain names are not unique e.g. owning multiple gmail.com accounts.
+ However bounce / catchall should not clash with aliases. """
+ alias_domain = self.mail_alias_domain.with_env(self.env)
+
+ for domain_config in {'bounce_alias', 'catchall_alias'}:
+ with self.subTest(domain_config=domain_config):
+ with self.assertRaises(exceptions.ValidationError):
+ self.env['mail.alias.domain'].create({
+ domain_config: self.test_alias_mc.alias_name,
+ 'name': self.test_alias_mc.alias_domain_id.name,
+ })
+ # left-part should not clech
+ self.env['mail.alias.domain'].create({
+ domain_config: self.test_alias_mc.alias_name,
+ 'name': 'another.domain.name.com',
+ })
+
+ # should not clash with existing aliases, to avoid valid aliases be
+ # considered as bounce / catchall
+ with self.assertRaises(exceptions.UserError), self.cr.savepoint():
+ alias_domain.write({'bounce_alias': self.test_alias_mc.alias_name})
+ with self.assertRaises(exceptions.UserError), self.cr.savepoint():
+ alias_domain.write({'catchall_alias': self.test_alias_mc.alias_name})
+
+ @users('admin')
+ def test_alias_domain_config_unique(self):
+ """ Domain names are not unique e.g. owning multiple gmail.com accounts.
+ However bounce / catchall should be unique. """
+ alias_domain = self.mail_alias_domain.with_env(self.env)
+
+ # copying directly would duplicate bounce / catchall emails
+ with mute_logger('odoo.sql_db'), self.assertRaises(psycopg2.errors.UniqueViolation), self.cr.savepoint():
+ new_alias_domain = alias_domain.copy()
+
+ # same domain name is authorized if bounce and catchall are different
+ new_alias_domain = alias_domain.copy({
+ 'bounce_alias': 'new.bounce',
+ 'catchall_alias': 'new.catchall',
+ })
+ self.assertEqual(new_alias_domain.bounce_email, f'new.bounce@{alias_domain.name}')
+ self.assertEqual(new_alias_domain.catchall_email, f'new.catchall@{alias_domain.name}')
+ self.assertEqual(new_alias_domain.name, alias_domain.name)
+
+ # check bounce / catchall are unique at create
+ self.env['mail.alias.domain'].create({
+ 'bounce_alias': 'unique.bounce',
+ 'catchall_alias': 'unique.catchall',
+ 'name': alias_domain.name,
+ })
+ # any not unique should raise UniqueViolation (SQL constraint fired after check)
+ with mute_logger('odoo.sql_db'), self.assertRaises(psycopg2.errors.UniqueViolation), self.cr.savepoint():
+ self.env['mail.alias.domain'].create({
+ 'bounce_alias': alias_domain.bounce_alias,
+ 'name': alias_domain.name,
+ })
+ with mute_logger('odoo.sql_db'), self.assertRaises(psycopg2.errors.UniqueViolation), self.cr.savepoint():
+ self.env['mail.alias.domain'].create({
+ 'catchall_alias': alias_domain.catchall_alias,
+ 'name': alias_domain.name,
+ })
+
+ # also check write operation
+ with self.assertRaises(exceptions.ValidationError):
+ new_alias_domain.write({'bounce_alias': alias_domain.bounce_alias})
+ with self.assertRaises(exceptions.ValidationError):
+ new_alias_domain.write({'catchall_alias': alias_domain.catchall_alias})
+
+ @users('admin')
+ def test_alias_domain_parameters_validation(self):
+ """ Test validation of bounce and catchall fields of alias domain as
+ well as sanitization. """
+ alias_domain = self.mail_alias_domain.with_env(self.env)
+
+ # sanitization of name (both create and write)
+ for failing_name in [
+ 'outlook.fr, gmail.com',
+ # accents
+ 'provaïder',
+ 'provaïder.cöm',
+ # fail
+ '', ' ',
+ ]:
+ with self.subTest(failing_name=failing_name):
+ with self.assertRaises(exceptions.ValidationError):
+ _new_domain = self.env['mail.alias.domain'].create({'name': failing_name})
+
+ with self.assertRaises(exceptions.ValidationError):
+ alias_domain.write({'name': failing_name})
+
+ # sanitization of bounce / catchall
+ for (bounce_alias, catchall_alias, default_from), (exp_bounce, exp_catchall, exp_default_from) in zip(
+ [
+ (
+ 'bounce+b4r=*R3wl_#_-$€{}[]()~|\\/!?&%^\'"`~',
+ 'catchall+b4r=*R3wl_#_-$€{}[]()~|\\/!?&%^\'"`~',
+ 'notifications+b4r=*R3wl_#_-$€{}[]()~|\\/!?&%^\'"`~',
+ ),
+ ('bounce+😊', 'catchall+😊', 'notifications+😊'),
+ ('Bouncâïde 😊', 'Catchôïee 😊', 'Notificâtïons 😊'),
+ ('ぁ', 'ぁぁ', 'ぁぁぁ'),
+ ],
+ [
+ (
+ 'bounce+b4r=*r3wl_#_-$-{}-~|-/!?&%^\'-`~',
+ 'catchall+b4r=*r3wl_#_-$-{}-~|-/!?&%^\'-`~',
+ 'notifications+b4r=*r3wl_#_-$-{}-~|-/!?&%^\'-`~',
+ ),
+ ('bounce+-', 'catchall+-', 'notifications+-'),
+ ('bouncaide-', 'catchoiee-', 'notifications-'),
+ ('?', '??', '???'),
+ ]
+ ):
+ with self.subTest(bounce_alias=bounce_alias):
+ alias_domain.write({'bounce_alias': bounce_alias})
+ self.assertEqual(alias_domain.bounce_alias, exp_bounce)
+ with self.subTest(catchall_alias=catchall_alias):
+ alias_domain.write({'catchall_alias': catchall_alias})
+ self.assertEqual(alias_domain.catchall_alias, exp_catchall)
+ with self.subTest(default_from=default_from):
+ alias_domain.write({'default_from': default_from})
+ self.assertEqual(alias_domain.default_from, exp_default_from)
+
+ # falsy values
+ for config_value in [False, None, '', ' ']:
+ with self.subTest(config_value=config_value):
+ alias_domain.write({'bounce_alias': config_value})
+ self.assertFalse(alias_domain.bounce_alias)
+ alias_domain.write({'catchall_alias': config_value})
+ self.assertFalse(alias_domain.catchall_alias)
+ alias_domain.write({'default_from': config_value})
+ self.assertFalse(alias_domain.default_from)
+
+ # check successive param set, should not raise for unicity against itself
+ for _ in range(2):
+ alias_domain.write({
+ 'bounce_alias': 'bounce+double.test',
+ 'catchall_alias': 'catchall+double.test',
+ })
+ self.assertEqual(alias_domain.bounce_alias, 'bounce+double.test')
+ self.assertEqual(alias_domain.catchall_alias, 'catchall+double.test')
+
+
+@tagged('mail_gateway', 'mail_alias', 'mail_alias_mixin', 'multi_company')
class TestMailAliasMixin(TestMailAliasCommon):
- """ Test alias mixin implementation, synchornization of alias records
+ """ Test alias mixin implementation, synchronization of alias records
based on owner records. """
@users('employee')
@mute_logger('odoo.addons.base.models.ir_model')
def test_alias_mixin(self):
""" Various base checks on alias mixin behavior """
+ self.assertEqual(self.env.company.alias_domain_id, self.mail_alias_domain)
+
record = self.env['mail.test.container'].create({
'name': 'Test Record',
'alias_name': 'alias.test',
'alias_contact': 'followers',
})
+ self.assertEqual(record.alias_id.alias_domain_id, self.mail_alias_domain)
self.assertEqual(record.alias_id.alias_model_id, self.env['ir.model']._get('mail.test.container'))
self.assertEqual(record.alias_id.alias_force_thread_id, record.id)
self.assertEqual(record.alias_id.alias_parent_model_id, self.env['ir.model']._get('mail.test.container'))
@@ -307,9 +677,11 @@ class TestMailAliasMixin(TestMailAliasCommon):
self.assertEqual(record.alias_id.alias_contact, 'followers')
record.write({
+ 'alias_domain_id': self.mail_alias_domain_c2.id,
'alias_name': 'better.alias.test',
'alias_defaults': "{'default_name': 'defaults'}"
})
+ self.assertEqual(record.alias_domain, self.mail_alias_domain_c2.name)
self.assertEqual(record.alias_id.alias_name, 'better.alias.test')
self.assertEqual(record.alias_id.alias_defaults, "{'default_name': 'defaults'}")
@@ -326,6 +698,32 @@ class TestMailAliasMixin(TestMailAliasCommon):
with self.assertRaises(exceptions.ValidationError):
record.write({'alias_defaults': "{'custom_field': brokendict"})
+ @users('erp_manager')
+ def test_alias_mixin_alias_email(self):
+ """ Test 'alias_email' mixin field computation and search capability """
+ Model = self.env['mail.test.container.mc']
+ records = Model.create([
+ {
+ 'alias_name': f'alias.email.{idx}', # will be present in all companies
+ 'company_id': company.id,
+ 'name': f'Test {company.id} {idx}',
+ }
+ for company in (self.company_admin, self.company_2)
+ for idx in range(5)
+ ])
+ self.assertEqual(
+ Model.search([('alias_email', 'ilike', 'alias.email')]), records,
+ 'Search: partial search: any domain, matching all left parts')
+ self.assertEqual(
+ Model.search([('alias_email', 'ilike', 'alias.email.0')]), records[0] + records[5],
+ 'Search: partial search: any domain, matching some left parts')
+ self.assertEqual(
+ Model.search([('alias_email', '=', self.mail_alias_domain.name)]), Model,
+ 'Search: partial search: does not match any complete email')
+ self.assertEqual(
+ Model.search([('alias_email', '=', f'alias.email.1@{self.mail_alias_domain.name}')]), records[1],
+ 'Search: both part search: search on name + domain')
+
@users('employee')
@mute_logger('odoo.addons.base.models.ir_model')
def test_alias_mixin_alias_id_management(self):
@@ -411,20 +809,55 @@ class TestMailAliasMixin(TestMailAliasCommon):
@users('erp_manager')
def test_multi_company_setup(self):
- """ Test company change does not impact anything at alias domain level """
- record = self.env['mail.test.container.mc'].create({
- 'name': 'Test Record',
- 'alias_name': 'alias.test',
- 'alias_contact': 'followers',
- 'company_id': self.env.user.company_id.id,
- })
- self.assertEqual(record.alias_domain, self.alias_domain)
- self.assertEqual(record.company_id, self.company_2)
+ """ Test company impact on alias domains when creating or updating
+ records in a MC environment. """
+ counter = 0
+ for create_cid, exp_company, exp_alias_domain in [
+ (None, self.company_2, self.mail_alias_domain_c2),
+ (False, self.env['res.company'], self.mail_alias_domain_c2),
+ (self.env.user.company_id.id, self.company_2, self.mail_alias_domain_c2),
+ (self.company_admin.id, self.company_admin, self.mail_alias_domain),
+ ]:
+ with self.subTest(create_cid=create_cid, exp_company=exp_company, exp_alias_domain=exp_alias_domain):
+ counter += 1
+ base_values = {
+ 'name': f'Test Record {counter}',
+ 'alias_name': f'alias.test.{counter}',
+ 'alias_contact': 'followers',
+ }
+ if create_cid is not None:
+ base_values['company_id'] = create_cid
+ record = self.env['mail.test.container.mc'].create(base_values)
+ self.assertEqual(record.alias_domain_id, exp_alias_domain)
+ self.assertEqual(record.company_id, exp_company)
- record.write({'company_id': self.company_admin.id})
- self.assertEqual(record.alias_domain, self.alias_domain)
- self.assertEqual(record.company_id, self.company_admin)
+ # copy: keep company
+ record_copy = record.copy(
+ default={
+ 'alias_name': f'alias.copy.{counter}',
+ 'name': f'Copy of {record.name}',
+ }
+ )
+ self.assertEqual(record_copy.alias_domain_id, exp_alias_domain)
+ self.assertEqual(record_copy.company_id, record.company_id)
- record.write({'company_id': False})
- self.assertEqual(record.alias_domain, self.alias_domain)
- self.assertFalse(record.company_id)
+ # copy: force company
+ record_copy_2 = record.copy(
+ default={
+ 'alias_name': f'alias.copy.{counter}.2',
+ 'company_id': self.company_admin.id,
+ 'name': f'Copy 2 of {record.name}',
+ }
+ )
+ self.assertEqual(record_copy_2.alias_domain_id, self.mail_alias_domain)
+ self.assertEqual(record_copy_2.company_id, self.company_admin)
+
+ # updating company: force same alias domain
+ record.write({'company_id': self.company_admin.id})
+ self.assertEqual(record.alias_domain_id, self.mail_alias_domain)
+ self.assertEqual(record.company_id, self.company_admin)
+
+ # reset company: should not impact alias_domain if set
+ record.write({'company_id': False})
+ self.assertEqual(record.alias_domain_id, self.mail_alias_domain)
+ self.assertFalse(record.company_id)
diff --git a/addons/test_mail/tests/test_mail_composer.py b/addons/test_mail/tests/test_mail_composer.py
index d81162fdab4..f683689ee04 100644
--- a/addons/test_mail/tests/test_mail_composer.py
+++ b/addons/test_mail/tests/test_mail_composer.py
@@ -56,32 +56,32 @@ class TestMailComposer(MailCommon, TestRecipients):
)
cls.env.ref('mail.group_mail_template_editor').users -= cls.user_rendering_restricted
- cls.test_record = cls.env['mail.test.ticket'].with_context(cls._test_context).create({
+ cls.test_record = cls.env['mail.test.ticket.mc'].with_context(cls._test_context).create({
'name': 'TestRecord',
'customer_id': cls.partner_1.id,
'user_id': cls.user_employee_2.id,
})
cls.test_records, cls.test_partners = cls._create_records_for_batch(
- 'mail.test.ticket', 2,
+ 'mail.test.ticket.mc', 2,
additional_values={'user_id': cls.user_employee_2.id},
)
cls.test_report, cls.test_report_2, cls.test_report_3 = cls.env['ir.actions.report'].create([
{
'name': 'Test Report on Mail Test Ticket',
- 'model': 'mail.test.ticket',
+ 'model': 'mail.test.ticket.mc',
'print_report_name': "'TestReport for %s' % object.name",
'report_type': 'qweb-pdf',
'report_name': 'test_mail.mail_test_ticket_test_template',
}, {
'name': 'Test Report 2 on Mail Test Ticket',
- 'model': 'mail.test.ticket',
+ 'model': 'mail.test.ticket.mc',
'print_report_name': "'TestReport2 for %s' % object.name",
'report_type': 'qweb-pdf',
'report_name': 'test_mail.mail_test_ticket_test_template_2',
}, {
'name': 'Test Report 3 with variable data on Mail Test Ticket',
- 'model': 'mail.test.ticket',
+ 'model': 'mail.test.ticket.mc',
'print_report_name': "'TestReport3 for %s' % object.name",
'report_type': 'qweb-pdf',
'report_name': 'test_mail.mail_test_ticket_test_variable_template',
@@ -100,7 +100,7 @@ class TestMailComposer(MailCommon, TestRecipients):
'email_from': '{{ (object.user_id.email_formatted or user.email_formatted) }}',
'lang': '{{ object.customer_id.lang }}',
'mail_server_id': cls.mail_server_domain.id,
- 'model_id': cls.env['ir.model']._get('mail.test.ticket').id,
+ 'model_id': cls.env['ir.model']._get('mail.test.ticket.mc').id,
'reply_to': '{{ ctx.get("custom_reply_to") or "info@test.example.com" }}',
'scheduled_date': '{{ (object.create_date or datetime.datetime(2022, 12, 26, 18, 0, 0)) + datetime.timedelta(days=2) }}',
})
@@ -171,6 +171,8 @@ class TestComposerForm(TestMailComposer):
self.assertFalse(composer_form.mail_server_id)
self.assertEqual(composer_form.model, self.test_record._name)
self.assertFalse(composer_form.partner_ids)
+ self.assertEqual(composer_form.record_alias_domain_id, self.mail_alias_domain)
+ self.assertEqual(composer_form.record_company_id, self.env.company)
self.assertEqual(composer_form.record_name, self.test_record.name, 'MailComposer: comment mode should compute record name')
self.assertFalse(composer_form.reply_to)
self.assertFalse(composer_form.reply_to_force_new)
@@ -182,6 +184,32 @@ class TestComposerForm(TestMailComposer):
self.assertEqual(composer_form.subtype_id, self.env.ref('mail.mt_comment'))
self.assertFalse(composer_form.subtype_is_log)
+ @users('employee')
+ def test_mail_composer_comment_mc(self):
+ """ Test values specific to multi-company setup, in comment mode, using
+ the Form tool. """
+ # add access to second company to avoid MC rules on ticket model
+ self.env.user.company_ids = [(4, self.company_2.id)]
+ test_record = self.test_record.with_env(self.env)
+ source_company = [
+ self.company_admin,
+ self.company_2,
+ self.env['res.company'],
+ ]
+ expected = [
+ (self.company_admin, self.mail_alias_domain),
+ (self.company_2, self.mail_alias_domain_c2),
+ (self.company_admin, self.mail_alias_domain), # env company
+ ]
+ for company, (exp_company, exp_alias_domain) in zip(source_company, expected):
+ with self.subTest(cmopany=company):
+ test_record.write({'company_id': company.id})
+ composer_form = Form(self.env['mail.compose.message'].with_context(
+ self._get_web_context(self.test_record, add_web=True)
+ ))
+ self.assertEqual(composer_form.record_alias_domain_id, exp_alias_domain)
+ self.assertEqual(composer_form.record_company_id, exp_company)
+
@users('employee')
def test_mail_composer_comment_attachments(self):
"""Tests that all attachments are added to the composer, static attachments
@@ -251,6 +279,8 @@ class TestComposerForm(TestMailComposer):
self.assertEqual(composer_form.mail_server_id, self.mail_server_domain)
self.assertEqual(composer_form.model, self.test_record._name)
self.assertEqual(composer_form.partner_ids[:], self.partner_1)
+ self.assertEqual(composer_form.record_alias_domain_id, self.mail_alias_domain)
+ self.assertEqual(composer_form.record_company_id, self.env.company)
self.assertEqual(composer_form.record_name, self.test_record.name, 'MailComposer: comment mode should compute record name')
self.assertEqual(composer_form.reply_to, 'info@test.example.com')
self.assertFalse(composer_form.reply_to_force_new)
@@ -283,6 +313,8 @@ class TestComposerForm(TestMailComposer):
self.assertFalse(composer_form.force_send, 'MailComposer: batch record post use email queue for notifications')
self.assertEqual(composer_form.mail_server_id, self.mail_server_domain)
self.assertEqual(composer_form.model, self.test_record._name)
+ self.assertFalse(composer_form.record_alias_domain_id, 'MailComposer: comment in batch mode should have void alias domain')
+ self.assertFalse(composer_form.record_company_id, 'MailComposer: comment in batch mode should have void company')
self.assertFalse(composer_form.record_name, 'MailComposer: comment in batch mode should have void record name')
self.assertEqual(composer_form.reply_to, self.template.reply_to)
self.assertFalse(composer_form.reply_to_force_new)
@@ -315,6 +347,8 @@ class TestComposerForm(TestMailComposer):
self.assertFalse(composer_form.force_send, 'MailComposer: batch record post use email queue for notifications')
self.assertEqual(composer_form.mail_server_id, self.mail_server_domain)
self.assertEqual(composer_form.model, self.test_record._name)
+ self.assertFalse(composer_form.record_alias_domain_id, 'MailComposer: comment in batch mode should have void alias domain')
+ self.assertFalse(composer_form.record_company_id, 'MailComposer: comment in batch mode should have void company')
self.assertFalse(composer_form.record_name, 'MailComposer: comment in batch mode should have void record name')
self.assertEqual(composer_form.reply_to, self.template.reply_to)
self.assertFalse(composer_form.reply_to_force_new)
@@ -332,7 +366,7 @@ class TestComposerForm(TestMailComposer):
when nothing is given as context. """
composer_form = Form(self.env['mail.compose.message'].with_context(
default_composition_mode='comment',
- default_model='mail.test.ticket',
+ default_model='mail.test.ticket.mc',
default_template_id=self.template.id,
))
self.assertTrue(composer_form.auto_delete, 'Should take composer value')
@@ -347,6 +381,8 @@ class TestComposerForm(TestMailComposer):
self.assertEqual(composer_form.mail_server_id, self.mail_server_domain)
self.assertEqual(composer_form.model, self.test_record._name)
self.assertFalse(composer_form.partner_ids[:])
+ self.assertFalse(composer_form.record_alias_domain_id)
+ self.assertFalse(composer_form.record_company_id)
self.assertFalse(composer_form.record_name)
self.assertEqual(composer_form.reply_to, 'info@test.example.com')
self.assertFalse(composer_form.reply_to_force_new)
@@ -375,6 +411,8 @@ class TestComposerForm(TestMailComposer):
self.assertTrue(composer_form.force_send, 'MailComposer: mass mode sends emails right away')
self.assertFalse(composer_form.mail_server_id)
self.assertEqual(composer_form.model, self.test_records._name)
+ self.assertFalse(composer_form.record_alias_domain_id, 'MailComposer: mass mode should have void alias domain')
+ self.assertFalse(composer_form.record_company_id, 'MailComposer: mass mode should have void company')
self.assertFalse(composer_form.record_name, 'MailComposer: mass mode should have void record name')
self.assertFalse(composer_form.reply_to)
self.assertFalse(composer_form.reply_to_force_new)
@@ -404,6 +442,8 @@ class TestComposerForm(TestMailComposer):
self.assertTrue(composer_form.force_send, 'MailComposer: mass mode sends emails right away')
self.assertEqual(composer_form.mail_server_id, self.mail_server_domain)
self.assertEqual(composer_form.model, self.test_records._name)
+ self.assertFalse(composer_form.record_alias_domain_id, 'MailComposer: mass mode should have void alias domain')
+ self.assertFalse(composer_form.record_company_id, 'MailComposer: mass mode should have void company')
self.assertFalse(composer_form.record_name, 'MailComposer: mass mode should have void record name')
self.assertEqual(composer_form.reply_to, self.template.reply_to)
self.assertFalse(composer_form.reply_to_force_new)
@@ -437,6 +477,8 @@ class TestComposerForm(TestMailComposer):
self.assertTrue(composer_form.force_send, 'MailComposer: mass mode sends emails right away')
self.assertEqual(composer_form.mail_server_id, self.mail_server_domain)
self.assertEqual(composer_form.model, self.test_records._name)
+ self.assertFalse(composer_form.record_alias_domain_id, 'MailComposer: mass mode should have void alias domain')
+ self.assertFalse(composer_form.record_company_id, 'MailComposer: mass mode should have void company')
self.assertFalse(composer_form.record_name, 'MailComposer: mass mode should have void record name')
self.assertEqual(composer_form.reply_to, self.template.reply_to)
self.assertFalse(composer_form.reply_to_force_new)
@@ -454,7 +496,7 @@ class TestComposerForm(TestMailComposer):
when nothing is given as context. """
composer_form = Form(self.env['mail.compose.message'].with_context(
default_composition_mode='mass_mail',
- default_model='mail.test.ticket',
+ default_model='mail.test.ticket.mc',
default_template_id=self.template.id,
))
self.assertTrue(composer_form.auto_delete, 'Should take composer value')
@@ -470,6 +512,8 @@ class TestComposerForm(TestMailComposer):
self.assertTrue(composer_form.force_send, 'MailComposer: mass mode sends emails right away')
self.assertEqual(composer_form.mail_server_id, self.mail_server_domain)
self.assertEqual(composer_form.model, self.test_records._name)
+ self.assertFalse(composer_form.record_alias_domain_id, 'MailComposer: mass mode should have void alias domain')
+ self.assertFalse(composer_form.record_company_id, 'MailComposer: mass mode should have void company')
self.assertFalse(composer_form.record_name, 'MailComposer: mass mode should have void record name')
self.assertEqual(composer_form.reply_to, self.template.reply_to)
self.assertFalse(composer_form.reply_to_force_new)
@@ -1253,7 +1297,7 @@ class TestComposerInternals(TestMailComposer):
new_partners.unlink()
-@tagged('mail_composer', 'multi_lang')
+@tagged('mail_composer', 'multi_lang', 'multi_company')
class TestComposerResultsComment(TestMailComposer, CronMixinCase):
""" Test global output of composer used in comment mode. Test notably
notification and emails generated during this process. """
@@ -1262,11 +1306,18 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase):
""" Ensure class initial data to ease understanding """
self.assertTrue(self.template.auto_delete)
+ self.assertEqual(len(self.test_record), 1)
+ self.assertEqual(self.test_record.user_id, self.user_employee_2)
+ self.assertEqual(self.test_record.message_partner_ids, self.partner_employee_2)
+ self.assertEqual(self.test_record[0].customer_id.lang, 'en_US')
+ self.assertEqual(self.test_record.company_id, self.company_admin)
+
self.assertEqual(len(self.test_records), 2)
self.assertEqual(self.test_records.user_id, self.user_employee_2)
self.assertEqual(self.test_records.message_partner_ids, self.partner_employee_2)
self.assertEqual(self.test_records[0].customer_id.lang, 'en_US')
self.assertEqual(self.test_records[1].customer_id.lang, 'en_US')
+ self.assertEqual(self.test_records.company_id, self.company_admin)
self.assertEqual(len(self.test_partners), 2)
@@ -1426,6 +1477,89 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase):
self.assertEqual(message.subtype_id, self.env.ref('mail.mt_comment'))
self.assertEqual(message.partner_ids, self.partner_1 | self.partner_2)
+ @users('employee')
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_mail_composer_server_config(self):
+ """ Test various configuration to check behavior of outgoing mail
+ servers, notifications, .... """
+ # add access to second company to avoid MC rules on ticket model
+ self.env.user.company_ids = [(4, self.company_2.id)]
+
+ # initial data
+ self.assertEqual(self.env.company, self.company_admin)
+ self.assertEqual(self.user_admin.company_id, self.company_admin)
+
+ # update test configuration
+ test_records = self.test_records
+ test_companies = self.company_admin + self.company_2
+ self.company_2.alias_domain_id = self.mail_alias_domain
+ for company, record in zip(test_companies, test_records):
+ record.company_id = company.id
+
+ # various from / servers configuration
+ self.template.write({'mail_server_id': False}) # allow server archive
+ server_other = self.env['ir.mail_server'].sudo().create({
+ 'name': 'Server Other',
+ 'from_filter': 'test.othercompany.com',
+ 'sequence': 4,
+ 'smtp_encryption': 'none',
+ 'smtp_host': 'smtp_host',
+ })
+ servers_all = self.mail_servers + server_other
+ for (emails_from, servers_active, mail_config), (exp_smtp_from_lst, exp_msg_from_lst, exp_from_filter) in zip(
+ [
+ (
+ [f'user.from@{self.alias_domain}', 'user@other.domain.com'],
+ self.env['ir.mail_server'],
+ {'default_from': f'notifications@{self.alias_domain}', 'from_filter': self.alias_domain}
+ ), # odoo-style configuration
+ ], [
+ (
+ [f'{self.alias_bounce}@{self.alias_domain}', f'{self.alias_bounce}@{self.alias_domain}'],
+ [f'"{self.env.user.name}" ', f'"{self.env.user.name}" '],
+ self.alias_domain
+ ), # no spoof
+ ],
+ ):
+ with self.subTest(emails_from=emails_from,
+ servers_active=servers_active):
+ # update servers
+ servers_all.active = False
+ if servers_active:
+ servers_active.active = True
+ # update mail config
+ default_from = mail_config.get('default_from', self.default_from)
+ from_filter = mail_config.get('from_filter', self.default_from_filter)
+ self.mail_alias_domain.default_from = default_from
+ self.env['ir.config_parameter'].sudo().set_param('mail.default.from_filter', from_filter)
+
+ for email_from, exp_smtp_from, exp_msg_from in zip(emails_from, exp_smtp_from_lst, exp_msg_from_lst):
+ self.env.user.email = email_from
+
+ # open a composer and run it in comment mode
+ composer = Form(self.env['mail.compose.message'].with_context(
+ default_composition_mode='comment',
+ default_force_send=True, # force sending emails directly to check SMTP
+ default_model=test_records._name,
+ default_res_ids=test_records.ids,
+ # avoid successive tests issues with followers
+ mail_create_nosubscribe=True,
+ ))
+ composer.body = 'Hello {{ object.name }}'
+ composer.subject = 'My Subject'
+ composer = composer.save()
+ with self.mock_mail_gateway(mail_unlink_sent=False), \
+ self.mock_mail_app():
+ composer._action_send_mail()
+
+ self.assertSMTPEmailsSent(
+ smtp_from=exp_smtp_from,
+ smtp_to_list=[self.partner_employee_2.email_normalized],
+ emails_count=2, # same on both records
+ message_from=exp_msg_from,
+ from_filter=exp_from_filter,
+ )
+
@users('employee')
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail', 'odoo.addons.mail.models.mail_message_schedule')
def test_mail_composer_wtpl_complete(self):
@@ -1761,6 +1895,110 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase):
self.assertEqual(set(message.attachment_ids.mapped('res_id')), set(test_record.ids))
self.assertTrue(all(attach not in message.attachment_ids for attach in attachs), 'Should have copied attachments')
+ @users('employee')
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_mail_composer_wtpl_mc(self):
+ """ Test specific to multi-company environment, notably company propagation
+ or aliases. """
+ # add access to second company to avoid MC rules on ticket model
+ self.env.user.company_ids = [(4, self.company_2.id)]
+
+ # initial data
+ self.assertEqual(self.env.company, self.company_admin)
+ self.assertEqual(self.user_admin.company_id, self.company_admin)
+
+ attachment_data = self._generate_attachments_data(2, self.template._name, self.template.id)
+ email_to_1 = 'test.to.1@test.example.com'
+ self.template.write({
+ 'auto_delete': False, # keep sent emails to check content
+ 'attachment_ids': [(0, 0, a) for a in attachment_data],
+ 'email_from': False, # use current user as author
+ 'email_layout_xmlid': 'mail.test_layout',
+ 'email_to': email_to_1,
+ 'mail_server_id': False, # let it find a server
+ 'partner_to': '%s, {{ object.customer_id.id if object.customer_id else "" }}' % self.partner_admin.id,
+ })
+ attachs = self.env['ir.attachment'].search([('name', 'in', [a['name'] for a in attachment_data])])
+ self.assertEqual(len(attachs), 2)
+
+ for batch, companies, expected_companies, expected_alias_domains in [
+ (False, self.company_admin, self.company_admin, self.mail_alias_domain),
+ (
+ True, self.company_admin + self.company_2, self.company_admin + self.company_2,
+ self.mail_alias_domain + self.mail_alias_domain_c2,
+ ),
+ ]:
+ with self.subTest(batch=batch,
+ companies=companies):
+ # update test configuration
+ test_records = self.test_records if batch else self.test_record
+ for company, record in zip(companies, test_records):
+ record.company_id = company.id
+
+ # open a composer and run it in comment mode
+ composer = Form(self.env['mail.compose.message'].with_context(
+ default_composition_mode='comment',
+ default_force_send=True, # force sending emails directly to check SMTP
+ default_model=test_records._name,
+ default_res_ids=test_records.ids,
+ default_template_id=self.template.id,
+ # avoid successive tests issues with followers
+ mail_create_nosubscribe=True,
+ )).save()
+ with self.mock_mail_gateway(mail_unlink_sent=False), \
+ self.mock_mail_app():
+ composer._action_send_mail()
+
+ new_partner = self.env['res.partner'].search([('email_normalized', '=', 'test.to.1@test.example.com')])
+ self.assertEqual(len(new_partner), 1)
+ # check output, company-specific values mainly for this test
+ for record, exp_company, exp_alias_domain in zip(
+ test_records, expected_companies, expected_alias_domains
+ ):
+ message = record.message_ids[0]
+ for recipient in [self.partner_employee_2, new_partner, record.customer_id]:
+ self.assertMailMail(
+ recipient,
+ 'sent',
+ author=self.partner_employee,
+ mail_message=message,
+ email_values={
+ 'headers': {
+ 'Return-Path': f'{exp_alias_domain.bounce_email}',
+ 'X-Odoo-Objects': f'{record._name}-{record.id}',
+ },
+ 'subject': f'TemplateSubject {record.name}',
+ },
+ fields_values={
+ 'headers': {
+ 'Return-Path': f'{exp_alias_domain.bounce_email}',
+ 'X-Odoo-Objects': f'{record._name}-{record.id}',
+ },
+ 'mail_server_id': self.env['ir.mail_server'],
+ 'record_alias_domain_id': exp_alias_domain,
+ 'record_company_id': exp_company,
+ 'subject': f'TemplateSubject {record.name}',
+ },
+ )
+ # to check behavior of extract_rfc2822_addresses
+ if recipient == new_partner:
+ smtp_to_list = ['"test.to.1@test.example.com"', 'test.to.1@test.example.com']
+ else:
+ smtp_to_list = [recipient.email_normalized]
+ if exp_alias_domain == self.mail_alias_domain:
+ self.assertSMTPEmailsSent(
+ smtp_from=f'{self.default_from}@{self.alias_domain}',
+ smtp_to_list=smtp_to_list,
+ mail_server=self.mail_server_notification,
+ emails_count=1,
+ )
+ else:
+ self.assertSMTPEmailsSent(
+ smtp_from=exp_alias_domain.bounce_email,
+ smtp_to_list=smtp_to_list,
+ emails_count=1,
+ )
+
@users('employee')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mail_composer_wtpl_recipients_email_fields(self):
@@ -2462,6 +2700,111 @@ class TestComposerResultsMass(TestMailComposer):
# self.assertIn(exp_button_es, sent_mail['body'])
self.assertIn(exp_button_en, sent_mail['body'])
+ @users('employee')
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_mail_composer_wtpl_mc(self):
+ """ Test specific to multi-company environment, notably company propagation
+ or aliases. """
+ # add access to second company to avoid MC rules on ticket model
+ self.env.user.company_ids = [(4, self.company_2.id)]
+
+ # initial data
+ self.assertEqual(self.env.company, self.company_admin)
+ self.assertEqual(self.user_admin.company_id, self.company_admin)
+
+ attachment_data = self._generate_attachments_data(2, self.template._name, self.template.id)
+ email_to_1 = 'test.to.1@test.example.com'
+ self.template.write({
+ 'auto_delete': False, # keep sent emails to check content
+ 'attachment_ids': [(0, 0, a) for a in attachment_data],
+ 'email_from': False, # use current user as author
+ 'email_layout_xmlid': 'mail.test_layout',
+ 'email_to': email_to_1,
+ 'mail_server_id': False, # let it find a server
+ 'partner_to': '%s, {{ object.customer_id.id if object.customer_id else "" }}' % self.partner_admin.id,
+ })
+ attachs = self.env['ir.attachment'].search([('name', 'in', [a['name'] for a in attachment_data])])
+ self.assertEqual(len(attachs), 2)
+
+ for companies, expected_companies, expected_alias_domains in [
+ (
+ self.company_admin + self.company_2,
+ self.company_admin + self.company_2,
+ self.mail_alias_domain + self.mail_alias_domain_c2,
+ ),
+ ]:
+ with self.subTest(companies=companies):
+ # update test configuration
+ test_records = self.test_records
+ for company, record in zip(companies, test_records):
+ record.company_id = company.id
+
+ # open a composer and run it in comment mode
+ composer = Form(self.env['mail.compose.message'].with_context(
+ default_composition_mode='mass_mail',
+ default_force_send=True, # force sending emails directly to check SMTP
+ default_model=test_records._name,
+ default_res_ids=test_records.ids,
+ default_template_id=self.template.id,
+ # avoid successive tests issues with followers
+ mail_create_nosubscribe=True,
+ )).save()
+ with self.mock_mail_gateway(mail_unlink_sent=False), \
+ self.mock_mail_app():
+ composer._action_send_mail()
+
+ new_partner = self.env['res.partner'].search([('email_normalized', '=', 'test.to.1@test.example.com')])
+ self.assertEqual(len(new_partner), 1)
+ # check output, company-specific values mainly for this test
+ for record, exp_company, exp_alias_domain in zip(
+ test_records, expected_companies, expected_alias_domains
+ ):
+ # message copy is kept
+ message = record.message_ids[0]
+ recipients = record.customer_id + new_partner + self.partner_admin
+ self.assertMailMail(
+ record.customer_id + new_partner + self.partner_admin,
+ 'sent',
+ author=self.partner_employee,
+ mail_message=message,
+ email_values={
+ 'headers': {
+ 'Return-Path': f'{exp_alias_domain.bounce_email}',
+ 'X-Odoo-Objects': f'{record._name}-{record.id}',
+ },
+ 'subject': f'TemplateSubject {record.name}',
+ },
+ fields_values={
+ 'headers': {
+ 'Return-Path': f'{exp_alias_domain.bounce_email}',
+ 'X-Odoo-Objects': f'{record._name}-{record.id}',
+ },
+ 'mail_server_id': self.env['ir.mail_server'],
+ 'record_alias_domain_id': exp_alias_domain,
+ 'record_company_id': exp_company,
+ 'subject': f'TemplateSubject {record.name}',
+ },
+ )
+ for recipient in recipients:
+ # to check behavior of extract_rfc2822_addresses
+ if recipient == new_partner:
+ smtp_to_list = ['"test.to.1@test.example.com"', 'test.to.1@test.example.com']
+ else:
+ smtp_to_list = [recipient.email_normalized]
+ if exp_alias_domain == self.mail_alias_domain:
+ self.assertSMTPEmailsSent(
+ smtp_from=f'{self.default_from}@{self.alias_domain}',
+ smtp_to_list=smtp_to_list,
+ mail_server=self.mail_server_notification,
+ emails_count=1,
+ )
+ else:
+ self.assertSMTPEmailsSent(
+ smtp_from=exp_alias_domain.bounce_email,
+ smtp_to_list=smtp_to_list,
+ emails_count=1,
+ )
+
@users('employee')
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_mail_composer_wtpl_recipients(self):
@@ -2581,7 +2924,7 @@ class TestComposerResultsMass(TestMailComposer):
# 6: void is void: raise in comment mode, just don't send anything in mass mail mode
composer_form = Form(self.env['mail.compose.message'].with_context(
- default_model='mail.test.ticket',
+ default_model='mail.test.ticket.mc',
default_template_id=self.template.id
))
composer = composer_form.save()
@@ -2592,7 +2935,7 @@ class TestComposerResultsMass(TestMailComposer):
composer_form = Form(self.env['mail.compose.message'].with_context(
default_composition_mode='mass_mail',
- default_model='mail.test.ticket',
+ default_model='mail.test.ticket.mc',
default_template_id=self.template.id
))
composer = composer_form.save()
@@ -2780,9 +3123,12 @@ class TestComposerResultsMass(TestMailComposer):
default_template_id=self.template.id)
))
composer = composer_form.save()
+
+ # remove alias so that _notify_get_reply_to will return the default value instead of alias
+ self.company_admin.write({
+ 'alias_domain_id': False,
+ })
with self.mock_mail_gateway(mail_unlink_sent=False):
- # remove alias so that _notify_get_reply_to will return the default value instead of alias
- self.env['ir.config_parameter'].sudo().set_param("mail.catchall.domain", None)
composer.action_send_mail()
for record in self.test_records:
diff --git a/addons/test_mail/tests/test_mail_gateway.py b/addons/test_mail/tests/test_mail_gateway.py
index 63c8a3444d7..fe2ef3a6484 100644
--- a/addons/test_mail/tests/test_mail_gateway.py
+++ b/addons/test_mail/tests/test_mail_gateway.py
@@ -128,9 +128,7 @@ class TestEmailParsing(MailCommon):
email_to='bounce@xxx.odoo.com',
delivered_to='bounce@xxx.odoo.com'
)
-
- msg_dict = {}
- msg = self.env['mail.thread']._message_parse_extract_bounce(self.from_string(incoming_bounce), msg_dict)
+ msg = self.env['mail.thread'].message_parse(self.from_string(incoming_bounce))
self.assertEqual(msg['bounced_email'], partner.email, "The sender email should be correctly parsed")
self.assertEqual(msg['bounced_partner'], partner, "A partner with this email should exist")
self.assertEqual(msg['bounced_msg_ids'][0], message.message_id, "The sender message-id should correctly parsed")
@@ -155,6 +153,7 @@ class TestMailgateway(MailCommon):
super(TestMailgateway, cls).setUpClass()
cls.mail_test_gateway_model = cls.env['ir.model']._get('mail.test.gateway')
+ cls.mail_test_gateway_company_model = cls.env['ir.model']._get('mail.test.gateway.company')
cls.email_from = '"Sylvie Lelitre" '
cls.test_record = cls.env['mail.test.gateway'].with_context(cls._test_context).create({
@@ -166,12 +165,23 @@ class TestMailgateway(MailCommon):
'name': 'Valid Lelitre',
'email': 'valid.lelitre@agrolait.com',
})
- # groups@.. will cause the creation of new mail.test.gateway
+ # groups@test.mycompany.com will cause the creation of new mail.test.gateway
cls.alias = cls.env['mail.alias'].create({
+ 'alias_domain_id': cls.mail_alias_domain.id,
'alias_contact': 'everyone',
'alias_model_id': cls.mail_test_gateway_model.id,
'alias_name': 'groups',
})
+ # groups@test.mycompany2.com will cause the creation of new mail.test.gateway.company
+ cls.alias_c2 = cls.env['mail.alias'].create({
+ 'alias_defaults': {
+ 'company_id': cls.company_2.id,
+ },
+ 'alias_domain_id': cls.mail_alias_domain_c2.id,
+ 'alias_contact': 'everyone',
+ 'alias_model_id': cls.mail_test_gateway_company_model.id,
+ 'alias_name': 'groups',
+ })
# Set a first message on public group to test update and hierarchy
cls.fake_email = cls._create_gateway_message(cls.test_record, '123456')
@@ -382,9 +392,10 @@ class TestMailgateway(MailCommon):
""" Do not set alias as author to avoid including aliases in discussions """
from_1 = self.env['res.partner'].create({
'name': 'Brice Denisse',
- 'email': f'from.test@{self.alias_domain}',
+ 'email': f'from.test@{self.mail_alias_domain.name}',
})
self.env['mail.alias'].create({
+ 'alias_domain_id': self.mail_alias_domain.id,
'alias_name': 'from.test',
'alias_model_id': self.env['ir.model']._get('mail.test.gateway').id
})
@@ -522,6 +533,7 @@ class TestMailgateway(MailCommon):
test_model_track = self.env['ir.model']._get('mail.test.track')
container_custom = self.env['mail.test.container'].create({})
alias_valid = self.env['mail.alias'].with_user(self.user_admin).create({
+ 'alias_domain_id': self.mail_alias_domain.id,
'alias_name': 'valid',
'alias_model_id': test_model_track.id,
'alias_contact': 'everyone',
@@ -794,13 +806,36 @@ class TestMailgateway(MailCommon):
@mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
def test_message_route_alias_no_domain(self):
- """ Incoming email: write to alias even if no domain set: considered as valid alias """
- self.env['ir.config_parameter'].set_param('mail.catchall.domain', '')
+ """ Incoming email: write to alias with no domain set: not recognized as
+ a valid alias even when local-part only is checked. """
+ self.alias.alias_domain_id = False
- new_record = self.format_and_process(MAIL_TEMPLATE, self.partner_1.email_formatted, 'groups@another.domain.com', subject='Test Subject')
- # Test: one group created
+ for incoming_ok in [True, False]:
+ with self.subTest(incoming_ok=incoming_ok):
+ with self.assertRaises(ValueError):
+ _new_record = self.format_and_process(
+ MAIL_TEMPLATE, self.partner_1.email_formatted, f'groups@{self.alias_domain}',
+ subject='Test Subject'
+ )
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_route_alias_alias_incoming_local(self):
+ """ Incoming email: write to alias using local part only: depends on
+ alias accepting local only flag. """
+ self.alias.alias_incoming_local = True
+ new_record = self.format_and_process(
+ MAIL_TEMPLATE, self.partner_1.email_formatted, 'groups@another.domain.com',
+ subject='Test Subject Global'
+ )
self.assertEqual(len(new_record), 1, 'message_process: a new mail.test.simple should have been created')
+ self.alias.alias_incoming_local = False
+ with self.assertRaises(ValueError):
+ _new_record = self.format_and_process(
+ MAIL_TEMPLATE, self.partner_1.email_formatted, 'groups@another.domain.com',
+ subject='Test Subject Local'
+ )
+
@mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
def test_message_route_alias_forward_bypass_reply_first(self):
""" Incoming email: write to two "new thread" alias, one as a reply, one being another model -> consider as a forward """
@@ -808,6 +843,7 @@ class TestMailgateway(MailCommon):
# test@.. will cause the creation of new mail.test
new_alias_2 = self.env['mail.alias'].create({
+ 'alias_domain_id': self.mail_alias_domain.id,
'alias_name': 'test',
'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
'alias_contact': 'everyone',
@@ -834,6 +870,7 @@ class TestMailgateway(MailCommon):
# test@.. will cause the creation of new mail.test
new_alias_2 = self.env['mail.alias'].create({
+ 'alias_domain_id': self.mail_alias_domain.id,
'alias_name': 'test',
'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
'alias_contact': 'everyone',
@@ -863,6 +900,7 @@ class TestMailgateway(MailCommon):
# test@.. will cause the creation of new mail.test
new_alias_2 = self.env['mail.alias'].create({
+ 'alias_domain_id': self.mail_alias_domain.id,
'alias_name': 'test',
'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
'alias_contact': 'everyone',
@@ -888,6 +926,7 @@ class TestMailgateway(MailCommon):
""" Incoming email: write to two aliases creating records: both should be activated """
# test@.. will cause the creation of new mail.test
new_alias_2 = self.env['mail.alias'].create({
+ 'alias_domain_id': self.mail_alias_domain.id,
'alias_name': 'test',
'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
'alias_contact': 'everyone',
@@ -909,9 +948,11 @@ class TestMailgateway(MailCommon):
""" Incoming email: check that if domains are set in the optional system
parameter `mail.catchall.domain.allowed` only incoming emails from these
domains will generate records."""
- # test@.. will cause the creation of new mail.test
+ # test@.. will cause the creation of new mail.test.container
new_alias_2 = self.env['mail.alias'].create({
'alias_contact': 'everyone',
+ 'alias_domain_id': self.mail_alias_domain_c2.id,
+ 'alias_incoming_local': True,
'alias_model_id': self.env['ir.model']._get_id('mail.test.container.mc'),
'alias_name': 'test',
})
@@ -919,6 +960,8 @@ class TestMailgateway(MailCommon):
test_domain = 'hello.com'
for (alias_right_part, allowed_domain), container_created in zip(
[
+ # Test a valid alias domain, standard case
+ (self.mail_alias_domain_c2.name, ""),
# Test with 'mail.catchall.domain.allowed' not set in system parameters
# and with a domain not allowed
('bonjour.com', ""),
@@ -928,7 +971,7 @@ class TestMailgateway(MailCommon):
# Test with 'mail.catchall.domain.allowed' set in system parameters
# and with a domain allowed
(test_domain, test_domain),
- ], [True, False, True]):
+ ], [True, True, False, True]):
with self.subTest(alias_right_part=alias_right_part, allowed_domain=allowed_domain):
self.env['ir.config_parameter'].set_param('mail.catchall.domain.allowed', allowed_domain)
@@ -1439,6 +1482,7 @@ class TestMailgateway(MailCommon):
def test_message_process_references_forward(self):
""" Incoming email using references but with alias forward should not go into references destination """
self.env['mail.alias'].create({
+ 'alias_domain_id': self.mail_alias_domain.id,
'alias_name': 'test.alias',
'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
'alias_contact': 'everyone',
@@ -1459,6 +1503,7 @@ class TestMailgateway(MailCommon):
def test_message_process_references_forward_same_model(self):
""" Incoming email using references but with alias forward on same model should be considered as a reply """
self.env['mail.alias'].create({
+ 'alias_domain_id': self.mail_alias_domain.id,
'alias_name': 'test.alias',
'alias_model_id': self.env['ir.model']._get('mail.test.gateway').id,
'alias_contact': 'everyone',
@@ -1478,6 +1523,7 @@ class TestMailgateway(MailCommon):
def test_message_process_references_forward_cc(self):
""" Incoming email using references but with alias forward in CC should be considered as a repy (To > Cc) """
self.env['mail.alias'].create({
+ 'alias_domain_id': self.mail_alias_domain.id,
'alias_name': 'test.alias',
'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
'alias_contact': 'everyone',
@@ -1551,6 +1597,7 @@ class TestMailgateway(MailCommon):
"""New record with mail that contains base64 inline image."""
target_model = "mail.test.field.type"
alias = self.env["mail.alias"].create({
+ 'alias_domain_id': self.mail_alias_domain.id,
"alias_name": "base64-lover",
"alias_model_id": self.env["ir.model"]._get(target_model).id,
"alias_defaults": "{}",
@@ -1574,6 +1621,7 @@ class TestMailgateway(MailCommon):
coming from alias."""
target_model = "mail.test.field.type"
alias = self.env["mail.alias"].create({
+ 'alias_domain_id': self.mail_alias_domain.id,
"alias_name": "base64-lover",
"alias_model_id": self.env["ir.model"]._get(target_model).id,
"alias_defaults": "{'type': 'second'}",
@@ -1700,6 +1748,7 @@ class TestMailgateway(MailCommon):
])
alias = self.env['mail.alias'].create({
+ 'alias_domain_id': self.mail_alias_domain.id,
'alias_name': 'test',
'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
'alias_contact': 'everyone',
@@ -1830,6 +1879,7 @@ class TestMailgateway(MailCommon):
})
self.env['mail.alias'].create({
+ 'alias_domain_id': self.mail_alias_domain.id,
'alias_name': 'test',
'alias_model_id': self.env['ir.model']._get('mail.test.gateway').id,
})
@@ -1854,6 +1904,7 @@ class TestMailThreadCC(MailCommon):
cls.email_from = 'Sylvie Lelitre '
cls.alias = cls.env['mail.alias'].create({
'alias_contact': 'everyone',
+ 'alias_domain_id': cls.mail_alias_domain.id,
'alias_model_id': cls.env['ir.model']._get('mail.test.cc').id,
'alias_name': 'cc_record',
})
diff --git a/addons/test_mail/tests/test_mail_mail.py b/addons/test_mail/tests/test_mail_mail.py
index aee64987025..4b050ad9de2 100644
--- a/addons/test_mail/tests/test_mail_mail.py
+++ b/addons/test_mail/tests/test_mail_mail.py
@@ -294,7 +294,7 @@ class TestMailMail(MailCommon):
# MailServer.build_email(): invalid from (missing)
for default_from in [False, '']:
- self.env['ir.config_parameter'].set_param('mail.default.from', default_from)
+ self.mail_alias_domain.default_from = default_from
self._reset_data()
with self.mock_mail_gateway(), mute_logger('odoo.addons.mail.models.mail_mail'):
mail.send(raise_exception=False)
@@ -311,8 +311,9 @@ class TestMailMail(MailCommon):
self.assertEqual(notification.notification_status, 'exception')
# MailServer.send_email(): _prepare_email_message: unexpected ASCII / Malformed 'Return-Path' or 'From' address
- # Force catchall domain to void otherwise bounce is set to postmaster-odoo@domain
- self.env['ir.config_parameter'].set_param('mail.catchall.domain', '')
+ # Force bounce alias to void, will force usage of email_from
+ self.mail_alias_domain.bounce_alias = False
+ self.env.company.invalidate_recordset(fnames={'bounce_email', 'bounce_formatted'})
for email_from in ['strange@example¢¡.com', 'robert']:
self._reset_data()
mail.write({'email_from': email_from})
@@ -326,20 +327,6 @@ class TestMailMail(MailCommon):
self.assertEqual(notification.failure_type, 'mail_from_invalid')
self.assertEqual(notification.notification_status, 'exception')
- # MailServer.send_email(): _prepare_email_message: unexpected ASCII based on catchall domain in bounce
- self.env['ir.config_parameter'].set_param('mail.catchall.domain', 'domain¢¡.com')
- self._reset_data()
- mail.write({'email_from': 'test.user@example.com'})
- with self.mock_mail_gateway():
- mail.send(raise_exception=False)
- self.assertEqual(self._mails[0]['email_from'], 'test.user@example.com')
- self.assertIn("Malformed 'Return-Path' or 'From' address: bounce.test@domain¢¡.com", mail.failure_reason)
- self.assertEqual(mail.failure_type, 'mail_from_invalid')
- self.assertEqual(mail.state, 'exception')
- self.assertEqual(notification.failure_reason, "Malformed 'Return-Path' or 'From' address: bounce.test@domain¢¡.com - It should contain one valid plain ASCII email")
- self.assertEqual(notification.failure_type, 'mail_from_invalid')
- self.assertEqual(notification.notification_status, 'exception')
-
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mail_mail_send_exceptions_recipients_emails(self):
""" Test various use case with exceptions and errors and see how they are
diff --git a/addons/test_mail/tests/test_mail_message.py b/addons/test_mail/tests/test_mail_message.py
index a25f35d9e2a..f54dbff8557 100644
--- a/addons/test_mail/tests/test_mail_message.py
+++ b/addons/test_mail/tests/test_mail_message.py
@@ -221,16 +221,8 @@ class TestMessageValues(MailCommon):
self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
# no alias domain -> author
- self.env['ir.config_parameter'].search([('key', '=', 'mail.catchall.domain')]).unlink()
-
- msg = self.Message.create({})
- self.assertIn('-private', msg.message_id.split('@')[0], 'mail_message: message_id for a void message should be a "private" one')
- self.assertEqual(msg.reply_to, formataddr((self.user_employee.name, self.user_employee.email)))
- self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
-
- # no alias catchall, no alias -> author
- self.env['ir.config_parameter'].set_param('mail.catchall.domain', self.alias_domain)
- self.env['ir.config_parameter'].search([('key', '=', 'mail.catchall.alias')]).unlink()
+ self.env.company.alias_domain_id = False
+ self.assertFalse(self.env.company.catchall_email)
msg = self.Message.create({})
self.assertIn('-private', msg.message_id.split('@')[0], 'mail_message: message_id for a void message should be a "private" one')
@@ -249,8 +241,10 @@ class TestMessageValues(MailCommon):
self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email)))
self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
- # no alias domain -> author
- self.env['ir.config_parameter'].search([('key', '=', 'mail.catchall.domain')]).unlink()
+ # no alias domain, no company catchall -> author
+ self.alias_record.alias_domain_id = False
+ self.env.company.alias_domain_id = False
+ self.assertFalse(self.env.company.catchall_email)
msg = self.Message.create({
'model': 'mail.test.container',
@@ -260,9 +254,8 @@ class TestMessageValues(MailCommon):
self.assertEqual(msg.reply_to, formataddr((self.user_employee.name, self.user_employee.email)))
self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
- # no catchall -> don't care, alias
- self.env['ir.config_parameter'].set_param('mail.catchall.domain', self.alias_domain)
- self.env['ir.config_parameter'].search([('key', '=', 'mail.catchall.alias')]).unlink()
+ # alias wins over company, hence no catchall is not an issue
+ self.alias_record.alias_domain_id = self.mail_alias_domain
msg = self.Message.create({
'model': 'mail.test.container',
@@ -467,7 +460,6 @@ class TestMessageAccess(MailCommon):
def test_mail_message_access_create_reply(self):
# TDE FIXME: should it really work ? not sure - catchall makes crash (aka, post will crash also)
- self.env['ir.config_parameter'].set_param('mail.catchall.domain', False)
self.message.write({'partner_ids': [(4, self.user_employee.partner_id.id)]})
self.env['mail.message'].with_user(self.user_employee).create({'model': 'discuss.channel', 'res_id': self.private_group.id, 'body': 'Test', 'parent_id': self.message.id})
diff --git a/addons/test_mail/tests/test_message_post.py b/addons/test_mail/tests/test_message_post.py
index c5042bf8a10..084b19cff25 100644
--- a/addons/test_mail/tests/test_message_post.py
+++ b/addons/test_mail/tests/test_message_post.py
@@ -290,7 +290,7 @@ class TestMailNotifyAPI(TestMessagePostCommon):
test_record._notify_get_reply_to()[test_record.id],
formataddr((
f"{self.user_employee_c2.company_id.name} {test_record.name}",
- f"{self.alias_catchall}@{self.alias_domain}"))
+ f"{self.alias_catchall_c2}@{self.alias_domain_c2_name}"))
)
test_record_c1 = test_record.with_user(self.user_employee)
self.assertEqual(
@@ -310,11 +310,17 @@ class TestMailNotifyAPI(TestMessagePostCommon):
])
res = test_records._notify_get_reply_to()
for test_record in test_records:
+ company = test_record.company_id
+ if company == self.company_2:
+ alias_domain = self.alias_domain_c2_name
+ alias_catchall = self.alias_catchall_c2
+ else:
+ alias_domain = self.alias_domain
+ alias_catchall = self.alias_catchall
+
self.assertEqual(
res[test_record.id],
- formataddr((
- f"{self.user_employee_c2.company_id.name} {test_record.name}",
- f"{self.alias_catchall}@{self.alias_domain}"))
+ formataddr((f"{company.name} {test_record.name}", f"{alias_catchall}@{alias_domain}"))
)
# Test3: get company from record (company_id field)
@@ -331,7 +337,7 @@ class TestMailNotifyAPI(TestMessagePostCommon):
res[test_record.id],
formataddr((
f"{self.company_3.name} {test_record.name}",
- f"{self.alias_catchall}@{self.alias_domain}"))
+ f"{self.alias_catchall_c3}@{self.alias_domain_c3_name}"))
)
@@ -956,7 +962,10 @@ class TestMessagePost(TestMessagePostCommon, CronMixinCase):
},
])
expected_companies = [self.company_2, self.company_admin, self.company_2]
- for record, expected_company in zip(records, expected_companies):
+ expected_alias_domains = [self.mail_alias_domain_c2, self.mail_alias_domain, self.mail_alias_domain_c2]
+ for record, expected_company, expected_alias_domain in zip(
+ records, expected_companies, expected_alias_domains
+ ):
with self.subTest(record=record):
with self.assertSinglePostNotifications(
[{'partner': self.partner_employee_2, 'type': 'email'}],
@@ -964,11 +973,12 @@ class TestMessagePost(TestMessagePostCommon, CronMixinCase):
'content': 'Body',
'email_values': {
'headers': {
- 'Return-Path': f'{self.alias_bounce}@{self.alias_domain}',
+ 'Return-Path': f'{expected_alias_domain.bounce_alias}@{expected_alias_domain.name}',
},
},
'mail_mail_values': {
'headers': {
+ 'Return-Path': f'{expected_alias_domain.bounce_alias}@{expected_alias_domain.name}',
'X-Odoo-Objects': f'{record._name}-{record.id}',
},
},
@@ -978,7 +988,10 @@ class TestMessagePost(TestMessagePostCommon, CronMixinCase):
'is_internal': False,
'notified_partner_ids': self.partner_employee_2,
'reply_to': formataddr(
- (f'{expected_company.name} {record.name}', f'{self.alias_catchall}@{self.alias_domain}')
+ (
+ f'{expected_company.name} {record.name}',
+ f'{expected_alias_domain.catchall_alias}@{expected_alias_domain.name}'
+ )
),
},
}
@@ -1289,7 +1302,7 @@ class TestMessagePost(TestMessagePostCommon, CronMixinCase):
)
self.assertEqual(msg.subject, '1st line 2nd line')
- @mute_logger('odoo.addons.mail.models.mail_mail')
+ @mute_logger('odoo.addons.base.models.ir_model', 'odoo.addons.mail.models.mail_mail')
def test_portal_acls(self):
self.test_record.message_subscribe((self.partner_1 | self.user_employee.partner_id).ids)
diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py
index 3b0a293f18a..64eca45c41b 100644
--- a/addons/test_mail/tests/test_performance.py
+++ b/addons/test_mail/tests/test_performance.py
@@ -224,19 +224,19 @@ class TestBaseMailPerformance(BaseMailPerformance):
@warmup
def test_create_mail_with_tracking(self):
""" Create records inheriting from 'mail.thread' (with field tracking). """
- with self.assertQueryCount(admin=8, demo=8):
+ with self.assertQueryCount(admin=9, demo=9):
self.env['mail.performance.thread'].create({'name': 'X'})
@users('admin', 'employee')
@warmup
def test_create_mail_simple(self):
- with self.assertQueryCount(admin=7, employee=7):
+ with self.assertQueryCount(admin=8, employee=8):
self.env['mail.test.simple'].create({'name': 'Test'})
@users('admin', 'employee')
@warmup
def test_create_mail_simple_multi(self):
- with self.assertQueryCount(admin=7, employee=7):
+ with self.assertQueryCount(admin=8, employee=8):
self.env['mail.test.simple'].create([{'name': 'Test'}] * 5)
@users('admin', 'employee')
@@ -264,7 +264,7 @@ class TestBaseAPIPerformance(BaseMailPerformance):
def test_adv_activity(self):
model = self.env['mail.test.activity']
- with self.assertQueryCount(admin=7, employee=7):
+ with self.assertQueryCount(admin=8, employee=8):
model.create({'name': 'Test'})
@users('admin', 'employee')
@@ -400,7 +400,7 @@ class TestBaseAPIPerformance(BaseMailPerformance):
composer_form.attachment_ids.add(attachment)
composer = composer_form.save()
- with self.assertQueryCount(admin=54, employee=54): # tm+com 53/53
+ with self.assertQueryCount(admin=55, employee=55): # tm+com 53/53
composer._action_send_mail()
# notifications
@@ -454,7 +454,7 @@ class TestBaseAPIPerformance(BaseMailPerformance):
test_record, test_template = self._create_test_records()
test_template.write({'attachment_ids': [(5, 0)]})
- with self.assertQueryCount(admin=26, employee=26): # tm 15/15 / com 25/25
+ with self.assertQueryCount(admin=27, employee=27): # tm 15/15 / com 25/25
composer = self.env['mail.compose.message'].with_context({
'default_composition_mode': 'comment',
'default_model': test_record._name,
@@ -478,7 +478,7 @@ class TestBaseAPIPerformance(BaseMailPerformance):
def test_mail_composer_w_template_attachments(self):
test_record, test_template = self._create_test_records()
- with self.assertQueryCount(admin=27, employee=27): # tm 16/16 / com 26/26
+ with self.assertQueryCount(admin=28, employee=28): # tm 16/16 / com 26/26
composer = self.env['mail.compose.message'].with_context({
'default_composition_mode': 'comment',
'default_model': test_record._name,
@@ -507,7 +507,7 @@ class TestBaseAPIPerformance(BaseMailPerformance):
test_template.write({'attachment_ids': [(5, 0)]})
customer = self.env['res.partner'].browse(self.customer.ids)
- with self.assertQueryCount(admin=33, employee=33): # tm 22/22 / com 32/32
+ with self.assertQueryCount(admin=34, employee=34): # tm 22/22 / com 32/32
composer_form = Form(
self.env['mail.compose.message'].with_context({
'default_composition_mode': 'comment',
@@ -518,7 +518,7 @@ class TestBaseAPIPerformance(BaseMailPerformance):
)
composer = composer_form.save()
- with self.assertQueryCount(admin=50, employee=50): # com 44/44
+ with self.assertQueryCount(admin=51, employee=51): # com 45/45
composer._action_send_mail()
# notifications
@@ -537,7 +537,7 @@ class TestBaseAPIPerformance(BaseMailPerformance):
test_record, test_template = self._create_test_records()
customer = self.env['res.partner'].browse(self.customer.ids)
- with self.assertQueryCount(admin=33, employee=33): # tm 22/22 / com 32/32
+ with self.assertQueryCount(admin=34, employee=34): # tm 22/22 / com 32/32
composer_form = Form(
self.env['mail.compose.message'].with_context({
'default_composition_mode': 'comment',
@@ -548,7 +548,7 @@ class TestBaseAPIPerformance(BaseMailPerformance):
)
composer = composer_form.save()
- with self.assertQueryCount(admin=69, employee=69): # com 61/61
+ with self.assertQueryCount(admin=70, employee=70): # com 62/62
composer._action_send_mail()
# notifications
@@ -575,7 +575,7 @@ class TestBaseAPIPerformance(BaseMailPerformance):
# use another user already pre-defined with the email notification type,
# so the ormcache is preserved.
record = self.env['mail.test.track'].create({'name': 'Test'})
- with self.assertQueryCount(admin=41, employee=41):
+ with self.assertQueryCount(admin=42, employee=42):
record.write({
'user_id': self.user_test_email.id,
})
@@ -801,7 +801,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
'mail_message_id': message.id,
'recipient_ids': [(4, pid) for pid in self.partners.ids],
})
- with self.assertQueryCount(admin=7, employee=7):
+ with self.assertQueryCount(admin=8, employee=8):
self.env['mail.mail'].sudo().browse(mail.ids).send()
@mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
@@ -840,7 +840,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
unlinked_mails |= set(records.ids)
unlinked_mails = set()
- with self.assertQueryCount(admin=30, employee=30), \
+ with self.assertQueryCount(admin=31, employee=31), \
patch.object(type(self.env['mail.mail']), 'unlink', _patched_unlink):
self.env['mail.mail'].sudo().browse(mails.ids).send()
@@ -860,7 +860,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
record = self.container.with_user(self.env.user)
# about 20 (19?) queries per additional customer group
- with self.assertQueryCount(admin=58, employee=57): # com 55/54
+ with self.assertQueryCount(admin=61, employee=60): # com 55/54
record.message_post(
body=Markup('Test Post Performances
'),
message_type='comment',
@@ -878,7 +878,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
template = self.env.ref('test_mail.mail_test_container_tpl')
# about 20 (19 ?) queries per additional customer group
- with self.assertQueryCount(admin=67, employee=66): # com 62/61
+ with self.assertQueryCount(admin=70, employee=69): # com 62/61
record.message_post_with_source(
template,
message_type='comment',
@@ -968,7 +968,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
})
rec1 = rec.with_context(active_test=False) # to see inactive records
self.assertEqual(rec1.message_partner_ids, self.partners | self.env.user.partner_id)
- with self.assertQueryCount(admin=41, employee=41):
+ with self.assertQueryCount(admin=43, employee=43):
rec.write({'user_id': self.user_portal.id})
self.assertEqual(rec1.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
# write tracking message
@@ -988,7 +988,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
customer_id = self.customer.id
user_id = self.user_portal.id
- with self.assertQueryCount(admin=98, employee=98):
+ with self.assertQueryCount(admin=102, employee=102):
rec = self.env['mail.test.ticket'].create({
'name': 'Test',
'container_id': container_id,
@@ -1017,7 +1017,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
rec1 = rec.with_context(active_test=False) # to see inactive records
self.assertEqual(rec1.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id)
self.assertEqual(len(rec1.message_ids), 1)
- with self.assertQueryCount(admin=61, employee=61):
+ with self.assertQueryCount(admin=63, employee=63):
rec.write({
'name': 'Test2',
'container_id': self.container.id,
@@ -1054,7 +1054,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
rec1 = rec.with_context(active_test=False) # to see inactive records
self.assertEqual(rec1.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id)
- with self.assertQueryCount(admin=67, employee=67):
+ with self.assertQueryCount(admin=70, employee=70):
rec.write({
'name': 'Test2',
'container_id': container_id,
@@ -1087,7 +1087,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
rec1 = rec.with_context(active_test=False) # to see inactive records
self.assertEqual(rec1.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
- with self.assertQueryCount(admin=33, employee=33):
+ with self.assertQueryCount(admin=34, employee=34):
rec.write({
'name': 'Test2',
'customer_id': customer_id,
@@ -1364,7 +1364,7 @@ class TestPerformance(BaseMailPerformance):
attachments = self.env['ir.attachment'].with_user(self.env.user).create(self.test_attachments_vals)
# enable_logging = self.cr._enable_logging() if self.warm else nullcontext()
# with self.assertQueryCount(employee=63), enable_logging:
- with self.assertQueryCount(employee=66): # com 60
+ with self.assertQueryCount(employee=69): # com 63
record_container.with_context({}).message_post(
body=Markup('Test body
'),
subject='Test Subject',
diff --git a/addons/test_mail_full/tests/test_mail_performance.py b/addons/test_mail_full/tests/test_mail_performance.py
index fa05fb676cb..81e95ad9abc 100644
--- a/addons/test_mail_full/tests/test_mail_performance.py
+++ b/addons/test_mail_full/tests/test_mail_performance.py
@@ -91,7 +91,7 @@ class TestMailPerformance(FullBaseMailPerformance):
record_ticket = self.env['mail.test.ticket.mc'].browse(self.record_ticket.ids)
attachments = self.env['ir.attachment'].create(self.test_attachments_vals)
- with self.assertQueryCount(employee=96): # test_mail_full: 95
+ with self.assertQueryCount(employee=101): # test_mail_full: 95
new_message = record_ticket.message_post(
attachment_ids=attachments.ids,
body=Markup('Test Content
'),
diff --git a/addons/test_mail_full/tests/test_rating.py b/addons/test_mail_full/tests/test_rating.py
index 8d2cc0c9498..055f180e3c8 100644
--- a/addons/test_mail_full/tests/test_rating.py
+++ b/addons/test_mail_full/tests/test_rating.py
@@ -172,7 +172,7 @@ class TestRatingPerformance(TestRatingCommon):
@users('employee')
@warmup
def test_rating_last_value_perfs(self):
- with self.assertQueryCount(employee=1616): # tmf 1313 / com 1313
+ with self.assertQueryCount(employee=1617): # tmf 1313 / com 1313
self.create_ratings('mail.test.rating.thread')
with self.assertQueryCount(employee=2101): # tmf 1901
@@ -184,7 +184,7 @@ class TestRatingPerformance(TestRatingCommon):
@users('employee')
@warmup
def test_rating_last_value_perfs_with_rating_mixin(self):
- with self.assertQueryCount(employee=1723): # tmf 1419 / com 1419
+ with self.assertQueryCount(employee=1724): # tmf 1419 / com 1419
self.create_ratings('mail.test.rating')
with self.assertQueryCount(employee=2304): # tmf 2104
diff --git a/addons/test_mail_sms/tests/test_sms_performance.py b/addons/test_mail_sms/tests/test_sms_performance.py
index 061caba1048..83732903b3b 100644
--- a/addons/test_mail_sms/tests/test_sms_performance.py
+++ b/addons/test_mail_sms/tests/test_sms_performance.py
@@ -35,7 +35,7 @@ class TestSMSPerformance(BaseMailPerformance, sms_common.SMSCase):
def test_message_sms_record_1_partner(self):
record = self.test_record.with_user(self.env.user)
pids = self.customer.ids
- with self.subTest("QueryCount"), self.mockSMSGateway(sms_allow_unlink=True), self.assertQueryCount(employee=23):
+ with self.subTest("QueryCount"), self.mockSMSGateway(sms_allow_unlink=True), self.assertQueryCount(employee=27):
messages = record._message_sms(
body='Performance Test',
partner_ids=pids,
@@ -50,7 +50,7 @@ class TestSMSPerformance(BaseMailPerformance, sms_common.SMSCase):
def test_message_sms_record_10_partners(self):
record = self.test_record.with_user(self.env.user)
pids = self.partners.ids
- with self.subTest("QueryCount"), self.mockSMSGateway(sms_allow_unlink=True), self.assertQueryCount(employee=23):
+ with self.subTest("QueryCount"), self.mockSMSGateway(sms_allow_unlink=True), self.assertQueryCount(employee=27):
messages = record._message_sms(
body='Performance Test',
partner_ids=pids,
@@ -64,7 +64,7 @@ class TestSMSPerformance(BaseMailPerformance, sms_common.SMSCase):
@warmup
def test_message_sms_record_default(self):
record = self.test_record.with_user(self.env.user)
- with self.subTest("QueryCount"), self.mockSMSGateway(sms_allow_unlink=True), self.assertQueryCount(employee=25):
+ with self.subTest("QueryCount"), self.mockSMSGateway(sms_allow_unlink=True), self.assertQueryCount(employee=29):
messages = record._message_sms(
body='Performance Test',
)
@@ -133,5 +133,5 @@ class TestSMSMassPerformance(BaseMailPerformance, sms_common.MockSMS):
'mass_keep_log': True,
})
- with self.mockSMSGateway(sms_allow_unlink=True), self.assertQueryCount(employee=57):
+ with self.mockSMSGateway(sms_allow_unlink=True), self.assertQueryCount(employee=58):
composer.action_send_sms()
diff --git a/addons/test_mass_mailing/tests/test_mailing_server.py b/addons/test_mass_mailing/tests/test_mailing_server.py
index fda438bf015..874aed17e90 100644
--- a/addons/test_mass_mailing/tests/test_mailing_server.py
+++ b/addons/test_mass_mailing/tests/test_mailing_server.py
@@ -112,8 +112,8 @@ class TestMassMailingServer(TestMassMailCommon):
('unknow_email@unknow_domain.com', 'unknow_email@unknow_domain.com', self.mail_server_notification),
]:
self.assertSMTPEmailsSent(
- smtp_from=expected_smtp_from,
- message_from=expected_msg_from,
- mail_server=expected_mail_server,
- emails_count=8,
- )
+ smtp_from=expected_smtp_from,
+ message_from=expected_msg_from,
+ mail_server=expected_mail_server,
+ emails_count=8,
+ )
diff --git a/addons/test_mass_mailing/tests/test_performance.py b/addons/test_mass_mailing/tests/test_performance.py
index b726baaa26e..ada1251811a 100644
--- a/addons/test_mass_mailing/tests/test_performance.py
+++ b/addons/test_mass_mailing/tests/test_performance.py
@@ -47,7 +47,7 @@ class TestMassMailPerformance(TestMassMailPerformanceBase):
})
# runbot needs +51 compared to local
- with self.assertQueryCount(__system__=1523, marketing=1524):
+ with self.assertQueryCount(__system__=1622, marketing=1623):
mailing.action_send_mail()
self.assertEqual(mailing.sent, 50)
@@ -90,7 +90,7 @@ class TestMassMailBlPerformance(TestMassMailPerformanceBase):
})
# runbot needs +51 compared to local
- with self.assertQueryCount(__system__=1595, marketing=1596):
+ with self.assertQueryCount(__system__=1694, marketing=1695):
mailing.action_send_mail()
self.assertEqual(mailing.sent, 50)
diff --git a/odoo/addons/base/models/ir_mail_server.py b/odoo/addons/base/models/ir_mail_server.py
index 72389b038f4..6f364ae88ab 100644
--- a/odoo/addons/base/models/ir_mail_server.py
+++ b/odoo/addons/base/models/ir_mail_server.py
@@ -87,7 +87,7 @@ class IrMailServer(models.Model):
"""Represents an SMTP server, able to send outgoing emails, with SSL and TLS capabilities."""
_name = "ir.mail_server"
_description = 'Mail Server'
- _order = 'sequence'
+ _order = 'sequence, id'
NO_VALID_RECIPIENT = ("At least one valid recipient address should be "
"specified for outgoing emails (To/Cc/Bcc)")
@@ -363,8 +363,7 @@ class IrMailServer(models.Model):
if mail_server:
from_filter = mail_server.from_filter
else:
- from_filter = self.env['ir.config_parameter'].sudo().get_param(
- 'mail.default.from_filter', tools.config.get('from_filter'))
+ from_filter = self.env['ir.mail_server']._get_default_from_filter()
smtp_encryption = encryption
if smtp_encryption is None and tools.config.get('smtp_ssl'):
@@ -474,7 +473,7 @@ class IrMailServer(models.Model):
:rtype: email.message.EmailMessage
:return: the new RFC2822 email message
"""
- email_from = email_from or self._get_default_from_address()
+ email_from = email_from or self.env.context.get('domain_notifications_email') or self._get_default_from_address()
assert email_from, self.NO_FOUND_FROM
headers = headers or {} # need valid dict later
@@ -540,6 +539,19 @@ class IrMailServer(models.Model):
"""
return tools.config.get("email_from")
+ @api.model
+ def _get_default_from_filter(self):
+ """ Computes the default from_filter. It is used when no specific
+ ir.mail_server is used when sending emails, hence having no value for
+ from_filter.
+
+ :return str/None: defaults to 'mail.default.from_filter', then
+ ``--from-filter`` CLI/config parameter.
+ """
+ return self.env['ir.config_parameter'].sudo().get_param(
+ 'mail.default.from_filter', tools.config.get('from_filter')
+ )
+
def _prepare_email_message(self, message, smtp_session):
"""Prepare the SMTP information (from, to, message) before sending.
@@ -554,7 +566,9 @@ class IrMailServer(models.Model):
# Use the default bounce address **only if** no Return-Path was
# provided by caller. Caller may be using Variable Envelope Return
# Path (VERP) to detect no-longer valid email addresses.
- bounce_address = message['Return-Path'] or self._get_default_bounce_address() or message['From']
+ # context may force a value, e.g. mail.alias.domain usage
+ bounce_address = self.env.context.get('domain_bounce_address') or message['Return-Path'] or self._get_default_bounce_address() or message['From']
+
smtp_from = message['From'] or bounce_address
assert smtp_from, self.NO_FOUND_SMTP_FROM
@@ -579,11 +593,13 @@ class IrMailServer(models.Model):
del message['To'] # avoid multiple To: headers!
message['To'] = x_forge_to
- # Try to not spoof the mail from headers
+ # Try to not spoof the mail from headers; fetch session-based or contextualized
+ # values for encapsulation computation
from_filter = getattr(smtp_session, 'from_filter', False)
smtp_from = getattr(smtp_session, 'smtp_from', False) or smtp_from
-
- notifications_email = email_normalize(self._get_default_from_address())
+ notifications_email = email_normalize(
+ self.env.context.get('domain_notifications_email') or self._get_default_from_address()
+ )
if notifications_email and smtp_from == notifications_email and message['From'] != notifications_email:
smtp_from = encapsulate_email(message['From'], notifications_email)
@@ -699,7 +715,7 @@ class IrMailServer(models.Model):
"""
email_from_normalized = email_normalize(email_from)
email_from_domain = email_domain_extract(email_from_normalized)
- notifications_email = email_normalize(self._get_default_from_address())
+ notifications_email = self.env.context.get('domain_notifications_email') or email_normalize(self._get_default_from_address())
notifications_domain = email_domain_extract(notifications_email)
if mail_servers is None:
@@ -741,8 +757,7 @@ class IrMailServer(models.Model):
return mail_servers[0], email_from
# 5: SMTP config in odoo-bin arguments
- from_filter = self.env['ir.config_parameter'].sudo().get_param(
- 'mail.default.from_filter', tools.config.get('from_filter'))
+ from_filter = self.env['ir.mail_server']._get_default_from_filter()
if self._match_from_filter(email_from, from_filter):
return None, email_from
diff --git a/odoo/addons/base/tests/common.py b/odoo/addons/base/tests/common.py
index 014a6365bcb..937479ad452 100644
--- a/odoo/addons/base/tests/common.py
+++ b/odoo/addons/base/tests/common.py
@@ -4,7 +4,7 @@
import threading
from contextlib import contextmanager
-from unittest.mock import patch
+from unittest.mock import patch, Mock
from odoo.tests.common import TransactionCase, HttpCase
from odoo import Command
@@ -326,16 +326,27 @@ class MockSmtplibCase:
self.testing_smtp_session = TestingSMTPSession()
IrMailServer = self.env['ir.mail_server']
- connect_origin = IrMailServer.connect
- find_mail_server_origin = IrMailServer._find_mail_server
+ connect_origin = type(IrMailServer).connect
+ find_mail_server_origin = type(IrMailServer)._find_mail_server
+
+ # custom mock to avoid losing context
+ def mock_function(func):
+ mock = Mock()
+
+ def _call(*args, **kwargs):
+ mock(*args[1:], **kwargs)
+ return func(*args, **kwargs)
+
+ _call.mock = mock
+ return _call
with patch('smtplib.SMTP_SSL', side_effect=lambda *args, **kwargs: self.testing_smtp_session), \
patch('smtplib.SMTP', side_effect=lambda *args, **kwargs: self.testing_smtp_session), \
patch.object(type(IrMailServer), '_is_test_mode', lambda self: False), \
- patch.object(type(IrMailServer), 'connect', wraps=IrMailServer, side_effect=connect_origin) as connect_mocked, \
- patch.object(type(IrMailServer), '_find_mail_server', side_effect=find_mail_server_origin) as find_mail_server_mocked:
- self.connect_mocked = connect_mocked
- self.find_mail_server_mocked = find_mail_server_mocked
+ patch.object(type(IrMailServer), 'connect', mock_function(connect_origin)) as connect_mocked, \
+ patch.object(type(IrMailServer), '_find_mail_server', mock_function(find_mail_server_origin)) as find_mail_server_mocked:
+ self.connect_mocked = connect_mocked.mock
+ self.find_mail_server_mocked = find_mail_server_mocked.mock
yield
def _build_email(self, mail_from, return_path=None, **kwargs):
@@ -385,15 +396,21 @@ class MockSmtplibCase:
debug_info = ''
matching_emails_count = len(list(matching_emails))
if matching_emails_count != emails_count:
+ emails_from = []
+ for email in self.emails:
+ from_found = next((
+ line.split('From:')[1].strip() for line in email['message'].splitlines()
+ if line.startswith('From:')), '')
+ emails_from.append(from_found)
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
+ f"SMTP-From: {email['smtp_from']}, SMTP-To: {email['smtp_to_list']}, Msg-From: {email_msg_from}, From_filter: {email['from_filter']})"
+ for email, email_msg_from in zip(self.emails, emails_from)
)
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}'
+ f'\nConditions\nSMTP-From: {smtp_from}, SMTP-To: {smtp_to_list}, Msg-From: {message_from}, From_filter: {from_filter}'
+ f'\nNot found in\n{debug_info}'
)
@classmethod
@@ -413,18 +430,22 @@ class MockSmtplibCase:
{
'name': 'Domain based server',
'from_filter': 'test.mycompany.com',
+ 'sequence': 0,
** ir_mail_server_values,
}, {
'name': 'User specific server',
'from_filter': 'specific_user@test.mycompany.com',
+ 'sequence': 1,
** ir_mail_server_values,
}, {
'name': 'Server Notifications',
'from_filter': 'notifications.test@test.mycompany.com',
+ 'sequence': 2,
** ir_mail_server_values,
}, {
'name': 'Server No From Filter',
'from_filter': False,
+ 'sequence': 3,
** ir_mail_server_values,
},
])
diff --git a/odoo/addons/base/tests/test_ir_mail_server.py b/odoo/addons/base/tests/test_ir_mail_server.py
index 7557421649a..cfc4d986c7f 100644
--- a/odoo/addons/base/tests/test_ir_mail_server.py
+++ b/odoo/addons/base/tests/test_ir_mail_server.py
@@ -51,9 +51,7 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
- # 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.env['ir.config_parameter'].sudo().set_param('mail.default.from_filter', False)
cls._init_mail_servers()
cls.default_bounce_address = 'CACA'
cls.default_from_address = 'PROUT'
@@ -291,10 +289,39 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase):
from_filter=False,
)
+ @mute_logger('odoo.models.unlink')
+ def test_mail_server_send_email_context_force(self):
+ """ Allow to force notifications_email / bounce_address from context
+ to allow higher-level apps to send values until end of mail stack
+ without hacking too much models. """
+ # custom notification / bounce email from context
+ context_server = self.env['ir.mail_server'].create({
+ 'from_filter': 'context.example.com',
+ 'name': 'context',
+ 'smtp_host': 'test',
+ })
+ IrMailServer = self.env["ir.mail_server"].with_context(
+ domain_notifications_email="notification@context.example.com",
+ domain_bounce_address="bounce@context.example.com",
+ )
+ with self.mock_smtplib_connection():
+ mail_server, smtp_from = IrMailServer._find_mail_server(email_from='"Name" ')
+ self.assertEqual(mail_server, context_server)
+ self.assertEqual(smtp_from, "notification@context.example.com")
+ smtp_session = IrMailServer.connect(smtp_from=smtp_from)
+ message = self._build_email(mail_from='"Name" ')
+ IrMailServer.send_email(message, smtp_session=smtp_session)
+
+ self.assertEqual(len(self.emails), 1)
+ self.assertSMTPEmailsSent(
+ smtp_from="bounce@context.example.com",
+ message_from='"Name" ',
+ from_filter=context_server.from_filter,
+ )
+
@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')
self.env['ir.mail_server'].send_email(message)