PURPOSE Allow alias domains to be multiple, notably to be used in a multi company environment where each company has its own alias domain. SPECIFICATIONS Add 'mail.alias.domain' information on alias model. Aliases do not use global configuration parameters anymore. Instead they are linked to an alias domain e.g. 'sales' linked to 'mycompany.com' alias domain: 'sales@mycompany.com'. It is now considered as a different alias compared to 'sales@mycompany.in' which has the same alias_name but a different alias domain. Constraints and checks are added, as * uniqueness of aliases is now checked inside a given domain; * an alias name must not clash with its domain bounce or catchall; * a combination of (alias_name, alias_domain_id) must be unique; Crm multi-company environment is also updated to match the new alias domain behavior. Task-36879 (Mail: Support Multi Domains Aliases) Part-of: odoo/odoo#76734
764 lines
36 KiB
Python
764 lines
36 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import psycopg2
|
|
|
|
from ast import literal_eval
|
|
|
|
from odoo import exceptions
|
|
from odoo.addons.mail.tests.common import MailCommon
|
|
from odoo.tests import tagged
|
|
from odoo.tests.common import users
|
|
from odoo.tools import formataddr, mute_logger
|
|
|
|
|
|
class TestMailAliasCommon(MailCommon):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
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',
|
|
})
|
|
|
|
|
|
@tagged('mail_gateway', 'mail_alias', 'multi_company')
|
|
class TestMailAlias(TestMailAliasCommon):
|
|
""" Test alias model features, constraints and behavior. """
|
|
|
|
@users('admin')
|
|
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):
|
|
self.env['ir.config_parameter'].set_param('mail.catchall.domain.allowed', value)
|
|
|
|
for value, expected in [
|
|
('', False),
|
|
('hello.com', 'hello.com'),
|
|
('hello.com,,', 'hello.com'),
|
|
('hello.com,bonjour.com', 'hello.com,bonjour.com'),
|
|
('hello.COM, BONJOUR.com', 'hello.com,bonjour.com'),
|
|
]:
|
|
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)
|
|
|
|
# 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'))
|
|
|
|
# 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')
|
|
|
|
@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 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
|
|
|
|
new_mail_alias = self.env['mail.alias'].create({
|
|
'alias_model_id': alias_model_id,
|
|
'alias_name': 'unused.test.alias',
|
|
})
|
|
other_alias = self.env['mail.alias'].create({
|
|
'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 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():
|
|
new_mail_alias.write({'alias_name': mail_alias_domain.catchall_alias})
|
|
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
|
|
new_mail_alias.write({'alias_name': mail_alias_domain.bounce_alias})
|
|
|
|
# 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,
|
|
'alias_name': 'unused.test.alias',
|
|
})
|
|
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
|
|
self.env['mail.alias'].create([
|
|
{
|
|
'alias_model_id': alias_model_id,
|
|
'alias_name': alias_name,
|
|
}
|
|
for alias_name in ('new.alias.1', 'new.alias.2', 'new.alias.1')
|
|
])
|
|
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
|
|
other_alias.write({'alias_name': 'unused.test.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['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['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,
|
|
'alias_name': 'unused.test.alias'
|
|
})
|
|
|
|
with mute_logger('odoo.sql_db'), self.assertRaises(psycopg2.errors.UniqueViolation), self.cr.savepoint():
|
|
new_mail_alias.copy({'alias_name': 'unused.test.alias'})
|
|
|
|
# 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):
|
|
(copy_1 + copy_2).write({'alias_name': 'test.alias.other'})
|
|
|
|
@users('admin')
|
|
@mute_logger('odoo.models.unlink')
|
|
def test_alias_name_sanitize(self):
|
|
""" Check sanitizer, at both create, copy and write on alias name. """
|
|
alias_names = [
|
|
'bidule...inc.',
|
|
'b4r+=*R3wl_#_-$€{}[]()~|\\/!?&%^\'"`~',
|
|
'hélène.prôvâïder',
|
|
'😊',
|
|
'Déboulonneur 😊',
|
|
'ぁ',
|
|
]
|
|
expected_names = [
|
|
'bidule.inc',
|
|
'b4r+=*r3wl_#_-$-{}-~|-/!?&%^\'-`~',
|
|
'helene.provaider',
|
|
'-',
|
|
'deboulonneur-',
|
|
'?',
|
|
]
|
|
msgs = [
|
|
'Emails cannot start or end with a dot, there cannot be a sequence of dots.',
|
|
'Disallowed chars should be replaced by hyphens',
|
|
'Email alias should be unaccented',
|
|
'Only a subset of unaccented latin chars are valid, others are replaced',
|
|
'Only a subset of unaccented latin chars are valid, others are replaced',
|
|
'Only a subset of unaccented latin chars are valid, others are replaced',
|
|
]
|
|
for alias_name, expected, msg in zip(alias_names, expected_names, msgs):
|
|
with self.subTest(alias_name=alias_name):
|
|
alias = self.env['mail.alias'].create({
|
|
'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
|
|
'alias_name': alias_name,
|
|
})
|
|
self.assertEqual(alias.alias_name, expected, msg)
|
|
alias.unlink()
|
|
|
|
alias = self.env['mail.alias'].create({
|
|
'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
|
|
})
|
|
# check at write
|
|
for alias_name, expected, msg in zip(alias_names, expected_names, msgs):
|
|
with self.subTest(alias_name=alias_name):
|
|
alias.write({'alias_name': alias_name})
|
|
self.assertEqual(alias.alias_name, expected, msg)
|
|
|
|
@users('admin')
|
|
def test_alias_name_sanitize_false(self):
|
|
""" Check empty-like aliases are forced to False, as otherwise unique
|
|
constraint might fail with empty strings. """
|
|
aliases = self.env['mail.alias'].create([
|
|
{
|
|
'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
|
|
'alias_name': falsy_name,
|
|
}
|
|
# '.' -> not allowed to start with a "." hence False
|
|
for falsy_name in [False, None, '', ' ', '.']
|
|
])
|
|
for alias in aliases:
|
|
with self.subTest(alias_name=alias.alias_name):
|
|
self.assertFalse(alias.alias_name, 'Void values should resolve to False')
|
|
|
|
# try to reset names in batch: should work
|
|
for idx, alias in enumerate(aliases):
|
|
alias.write({'alias_name': f'unique-{idx}'})
|
|
aliases.write({'alias_name': ''})
|
|
for alias in aliases:
|
|
self.assertEqual(alias.alias_name, False)
|
|
|
|
@users('admin')
|
|
def test_alias_setup(self):
|
|
""" Test various constraints / configuration of alias model"""
|
|
alias = self.env['mail.alias'].create({
|
|
'alias_model_id': self.env['ir.model']._get('mail.test.container.mc').id,
|
|
'alias_name': 'unused.test.alias'
|
|
})
|
|
self.assertEqual(alias.alias_status, 'not_tested')
|
|
|
|
# validation of alias_defaults
|
|
with self.assertRaises(exceptions.ValidationError):
|
|
alias.write({'alias_defaults': "{'custom_field': brokendict"})
|
|
alias.write({'alias_defaults': "{'custom_field': 'validdict'}"})
|
|
|
|
|
|
@tagged('mail_alias', 'multi_company')
|
|
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.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_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.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_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):
|
|
""" Test alias domain configuration when creating new companies """
|
|
company = self.env['res.company'].create({
|
|
'email': '"Super Company" <super.company@test3.mycompany.com>',
|
|
'name': 'Super Company',
|
|
})
|
|
company.flush_recordset()
|
|
self.assertEqual(
|
|
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" <yet.another.company@test.embed.mycompany.com>',
|
|
'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, 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'))
|
|
self.assertEqual(record.alias_id.alias_parent_thread_id, record.id)
|
|
self.assertEqual(record.alias_id.alias_name, 'alias.test')
|
|
self.assertEqual(record.alias_id.alias_contact, 'followers')
|
|
|
|
record.write({
|
|
'alias_name': 'better.alias.test',
|
|
'alias_defaults': "{'default_name': 'defaults'}"
|
|
})
|
|
self.assertEqual(record.alias_id.alias_name, 'better.alias.test')
|
|
self.assertEqual(record.alias_id.alias_defaults, "{'default_name': 'defaults'}")
|
|
|
|
with self.assertRaises(exceptions.AccessError):
|
|
record.write({
|
|
'alias_domain_id': self.mail_alias_domain_c2.id,
|
|
})
|
|
|
|
with self.assertRaises(exceptions.AccessError):
|
|
record.write({
|
|
'alias_force_thread_id': 0,
|
|
})
|
|
|
|
with self.assertRaises(exceptions.AccessError):
|
|
record.write({
|
|
'alias_model_id': self.env['ir.model']._get('mail.test.gateway').id,
|
|
})
|
|
|
|
with self.assertRaises(exceptions.ValidationError):
|
|
record.write({'alias_defaults': "{'custom_field': brokendict"})
|
|
|
|
@users('employee')
|
|
@mute_logger('odoo.addons.base.models.ir_model')
|
|
def test_alias_mixin_alias_id_management(self):
|
|
""" Test alias_id being not mandatory """
|
|
record_wo_alias, record_w_alias = self.env['mail.test.alias.optional'].create([
|
|
{
|
|
'name': 'Test WoAlias Name',
|
|
}, {
|
|
'alias_name': 'Alias Name',
|
|
'name': 'Test WoAlias Name',
|
|
}
|
|
])
|
|
self.assertFalse(record_wo_alias.alias_id, 'Alias record not created if not necessary (no alias_name)')
|
|
self.assertFalse(record_wo_alias.alias_id.alias_name)
|
|
self.assertFalse(record_wo_alias.alias_id.alias_defaults)
|
|
self.assertFalse(record_wo_alias.alias_name)
|
|
self.assertTrue(record_w_alias.alias_id, 'Alias record created as alias_name was given')
|
|
self.assertEqual(record_w_alias.alias_id.alias_name, 'alias-name', 'Alias name should go through sanitize')
|
|
self.assertEqual(
|
|
literal_eval(record_w_alias.alias_id.alias_defaults),
|
|
{'company_id': self.env.company.id}
|
|
)
|
|
self.assertEqual(record_w_alias.alias_name, 'alias-name', 'Alias name should go through sanitize')
|
|
self.assertEqual(
|
|
literal_eval(record_w_alias.alias_defaults),
|
|
{'company_id': self.env.company.id}
|
|
)
|
|
|
|
# update existing alias
|
|
record_w_alias.write({'alias_contact': 'followers', 'alias_name': 'Updated Alias Name'})
|
|
self.assertEqual(record_w_alias.alias_id.alias_contact, 'followers')
|
|
self.assertEqual(record_w_alias.alias_id.alias_name, 'updated-alias-name')
|
|
self.assertEqual(record_w_alias.alias_name, 'updated-alias-name')
|
|
|
|
# update non existing alias -> creates alias
|
|
record_wo_alias.write({'alias_name': 'trying a name'})
|
|
self.assertTrue(record_wo_alias.alias_id, 'Alias record should have been created to store the name')
|
|
self.assertEqual(record_wo_alias.alias_id.alias_name, 'trying-a-name')
|
|
self.assertEqual(
|
|
literal_eval(record_wo_alias.alias_id.alias_defaults),
|
|
{'company_id': self.env.company.id}
|
|
)
|
|
self.assertEqual(record_wo_alias.alias_name, 'trying-a-name')
|
|
self.assertEqual(
|
|
literal_eval(record_wo_alias.alias_defaults),
|
|
{'company_id': self.env.company.id}
|
|
)
|
|
|
|
# reset alias -> keep the alias as void, don't remove it
|
|
existing_aliases = record_wo_alias.alias_id + record_w_alias.alias_id
|
|
(record_wo_alias + record_w_alias).write({'alias_name': False})
|
|
self.assertEqual((record_wo_alias + record_w_alias).alias_id, existing_aliases)
|
|
self.assertFalse(list(filter(None, existing_aliases.mapped('alias_name'))))
|
|
|
|
@users('employee')
|
|
def test_copy_content(self):
|
|
self.assertFalse(
|
|
self.env.user.has_group('base.group_system'),
|
|
'Test user should not have Administrator access')
|
|
|
|
record = self.env['mail.test.container'].create({
|
|
'name': 'Test Record',
|
|
'alias_name': 'test.record',
|
|
'alias_contact': 'followers',
|
|
'alias_bounced_content': False,
|
|
})
|
|
record_alias = record.alias_id
|
|
self.assertFalse(record.alias_bounced_content)
|
|
record_copy = record.copy()
|
|
record_alias_copy = record_copy.alias_id
|
|
self.assertNotEqual(record_alias, record_alias_copy)
|
|
self.assertEqual(record_alias.alias_force_thread_id, record.id)
|
|
self.assertEqual(record_alias_copy.alias_force_thread_id, record_copy.id)
|
|
self.assertFalse(record_copy.alias_bounced_content)
|
|
self.assertEqual(record_copy.alias_contact, record.alias_contact)
|
|
self.assertFalse(record_copy.alias_name, 'Copy should not duplicate name')
|
|
|
|
new_content = '<p>Bounced Content</p>'
|
|
record_copy.write({'alias_bounced_content': new_content})
|
|
self.assertEqual(record_copy.alias_bounced_content, new_content)
|
|
record_copy2 = record_copy.copy()
|
|
self.assertEqual(record_copy2.alias_bounced_content, new_content)
|
|
|
|
@users('erp_manager')
|
|
def test_multi_company_setup(self):
|
|
""" 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)
|
|
|
|
# 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)
|
|
|
|
# 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)
|