[IMP] mail: respect alias domains in mail gateway

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

Make mail gateway support alias domains instead of relying on configuration
parameters. This implies the following changes

  * destination alias check is now based on full email by default. Previously
    only left-part of aliases were checked. Optionally an allowed list of
    domains could be additionally checked. Default from now on is to check
    the complete email e.g. 'sales@mydomain.com' != 'sales@mydomain.in';
  * detection of direct write to catchall implies checking all domains
    catchall emails;
  * detection of write to bounce implies checking all domains bounce emails;
  * when having to send bounce emails using the bounce alias as mailer-daemon,
    find the bounce email from the relevant company;

However we have to ease transition from the old ICP-based model used since
ages to the new domain-based model. Notably a common usage of mail gateways
is to do mail forwarding e.g. forward mail from domainA to domainB without
rewriting destination. It means that e.g. sales@mail.domainA should be
considered as a valid alias equivalent to sales@mail.domainB. This was
working due to left-part only check of destination aliases. In order to
keep this setup working after migration a flag is added on aliases allowing
to keep the detection of those aliases based only on local parts.

In summary: When searching for aliases, mailgateway now either checks for
exact email, either for matching local parts when the flag is active. This
is not the default behavior, as we want a stricter comparison of emails by
default but it will be the default behavior at **migration time**.

The 'mail.catchall.domain.allowed' configuration parameter is kept. It is
used only for left-part check aliases, allowing to limit the scope of the
match.

Task-36879 (Mail: Support Multi Domains Aliases)

Part-of: odoo/odoo#76734
This commit is contained in:
Thibault Delavallée
2023-10-24 19:24:50 +00:00
parent 1e40fb6247
commit 3a0da2278f
12 changed files with 181 additions and 100 deletions
@@ -12,8 +12,6 @@ class TestAccountIncomingSupplierInvoice(AccountTestInvoicingCommon):
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.env['ir.config_parameter'].sudo().set_param('mail.catchall.domain', 'test.mycompany.com')
cls.internal_user = cls.env['res.users'].create({
'name': 'Internal User',
'login': 'internal.user@test.odoo.com',
@@ -1345,7 +1345,6 @@ class TestSyncGoogle2Odoo(TestSyncGoogle):
@patch_api
def test_alias_email_sync_recurrence(self):
self.env['ir.config_parameter'].sudo().set_param('mail.catchall.domain', 'test.mycompany.com')
alias_model = self.env['ir.model'].search([('model', '=', 'calendar.event')])
mail_alias = self.env['mail.alias'].create({'alias_name': 'sale', 'alias_model_id': alias_model.id})
+1 -1
View File
@@ -25,6 +25,6 @@ class IrConfigParameter(models.Model):
value = self.env['mail.alias']._sanitize_alias_name(value)
# sanitize and normalize allowed catchall domains
elif key == 'mail.catchall.domain.allowed' and value:
value = self.env['mail.alias']._clean_and_check_mail_catchall_allowed_domains(value)
value = self.env['mail.alias']._sanitize_allowed_domains(value)
return super().set_param(key, value)
+15 -12
View File
@@ -81,6 +81,7 @@ class Alias(models.Model):
"- everyone: everyone can post\n"
"- partners: only authenticated partners\n"
"- followers: only followers of the related document or members of following channels\n")
alias_incoming_local = fields.Boolean('Local-part based incoming detection', default=False)
alias_bounced_content = fields.Html(
"Custom Bounced Message", translate=True,
help="If set, this content will automatically be sent out to unauthorized users instead of the default message.")
@@ -288,18 +289,6 @@ class Alias(models.Model):
return super().write(vals)
def _clean_and_check_mail_catchall_allowed_domains(self, value):
""" The purpose of this system parameter is to avoid the creation
of records from incoming emails with a domain != alias_domain
but that have a pattern matching an internal mail.alias . """
value = [domain.strip().lower() for domain in value.split(',') if domain.strip()]
if not value:
raise ValidationError(
_("Value for `mail.catchall.domain.allowed` cannot be validated.\n"
"It should be a comma separated list of domains e.g. example.com,example.org.")
)
return ",".join(value)
def _check_unique(self, alias_names, alias_domains):
""" Check unicity constraint won't be raised, otherwise raise a UserError
with a complete error message. Also check unicity against alias config
@@ -359,6 +348,20 @@ class Alias(models.Model):
msg_end = _('Choose another value or change it on the other document.')
raise UserError(f'{msg_begin} {msg_end}')
@api.model
def _sanitize_allowed_domains(self, allowed_domains):
""" When having aliases checked on email left-part only we may define
an allowed list for right-part filtering, allowing more fine-grain than
either alias domain, either everything. This method sanitized its value. """
value = [domain.strip().lower() for domain in allowed_domains.split(',') if domain.strip()]
if not value:
raise ValidationError(_(
"Value %(allowed_domains)s for `mail.catchall.domain.allowed` cannot be validated.\n"
"It should be a comma separated list of domains e.g. example.com,example.org.",
allowed_domains=allowed_domains
))
return ",".join(value)
@api.model
def _sanitize_alias_name(self, name):
""" Cleans and sanitizes the alias name """
+86 -69
View File
@@ -345,22 +345,23 @@ class MailThread(models.AbstractModel):
that adds alias information. """
model = self._context.get('empty_list_help_model')
res_id = self._context.get('empty_list_help_id')
catchall_domain = self.env['ir.config_parameter'].sudo().get_param("mail.catchall.domain")
document_name = self._context.get('empty_list_help_document_name', _('document'))
nothing_here = is_html_empty(help_message)
alias = None
if catchall_domain and model and res_id: # specific res_id -> find its alias (i.e. section_id specified)
# specific res_id -> find its alias (i.e. section_id specified)
if model and res_id:
record = self.env[model].sudo().browse(res_id)
# check that the alias effectively creates new records
if record.alias_id and record.alias_id.alias_name and \
record.alias_id.alias_model_id and \
record.alias_id.alias_model_id.model == self._name and \
record.alias_id.alias_force_thread_id == 0:
if ('alias_id' in record and record.alias_id and
record.alias_id.alias_name and record.alias_id.alias_domain and
record.alias_id.alias_model_id.model == self._name and
record.alias_id.alias_force_thread_id == 0):
alias = record.alias_id
if not alias and catchall_domain and model: # no res_id or res_id not linked to an alias -> generic help message, take a generic alias of the model
Alias = self.env['mail.alias']
aliases = Alias.search([
# no res_id or res_id not linked to an alias -> generic help message, take a generic alias of the model
if not alias and model and self.env.company.alias_domain_id:
aliases = self.env['mail.alias'].search([
("alias_domain_id", "=", self.env.company.alias_domain_id.id),
("alias_parent_model_id.model", "=", model),
("alias_name", "!=", False),
('alias_force_thread_id', '=', False),
@@ -695,13 +696,19 @@ class MailThread(models.AbstractModel):
'email_to': bounce_to,
'auto_delete': True,
}
bounce_from = tools.email_normalize(self.env['ir.mail_server']._get_default_bounce_address() or '')
if bounce_from:
bounce_mail_values['email_from'] = tools.formataddr(('MAILER-DAEMON', bounce_from))
elif self.env['ir.config_parameter'].sudo().get_param("mail.catchall.alias") not in message['To']:
bounce_mail_values['email_from'] = tools.decode_message_header(message, 'To')
else:
bounce_mail_values['email_from'] = tools.formataddr(('MAILER-DAEMON', self.env.user.email_normalized))
# find an email_from for the bounce email
email_from = False
if bounce_from := self.env.company.bounce_email:
email_from = tools.formataddr(('MAILER-DAEMON', bounce_from))
if not email_from:
catchall_aliases = self.env['mail.alias.domain'].search([]).mapped('catchall_email')
if not any(catchall_email in message['To'] for catchall_email in catchall_aliases):
email_from = tools.decode_message_header(message, 'To')
if not email_from:
email_from = tools.formataddr(('MAILER-DAEMON', self.env.user.email_normalized))
bounce_mail_values['email_from'] = email_from
bounce_mail_values.update(mail_values)
self.env['mail.mail'].sudo().create(bounce_mail_values).send()
@@ -885,13 +892,12 @@ class MailThread(models.AbstractModel):
we also need to verify if the message come from "mailer-daemon"
"""
# detection based on email_to
bounce_alias = self.env['ir.config_parameter'].sudo().get_param("mail.bounce.alias")
email_to = message_dict['to']
email_to_localparts = [
e.split('@', 1)[0].lower()
for e in (tools.email_split(email_to) or [''])
bounce_aliases = self.env['mail.alias.domain'].search([]).mapped('bounce_email')
email_to_list = [
tools.email_normalize(e) or e
for e in (tools.email_split(message_dict['to']) or [''])
]
if bounce_alias and bounce_alias in email_to_localparts:
if bounce_aliases and any(email in bounce_aliases for email in email_to_list):
return True
email_from = message_dict['email_from']
@@ -1001,16 +1007,15 @@ class MailThread(models.AbstractModel):
@api.model
def _detect_write_to_catchall(self, msg_dict):
"""Return True if directly contacts catchall."""
catchall_alias = self.env['ir.config_parameter'].sudo().get_param("mail.catchall.alias")
email_to = msg_dict['to']
email_to_localparts = [
e.split('@', 1)[0].lower()
for e in (tools.email_split(email_to) or [''])
catchall_aliases = self.env['mail.alias.domain'].search([]).mapped('catchall_email')
email_to_list = [
tools.email_normalize(e) or e
for e in (tools.email_split(msg_dict['to']) or [''])
]
# check it does not directly contact catchall
return (
catchall_alias and email_to_localparts and
all(email_localpart == catchall_alias for email_localpart in email_to_localparts)
catchall_aliases and email_to_list and
all(email_to in catchall_aliases for email_to in email_to_list)
)
@api.model
@@ -1050,10 +1055,19 @@ class MailThread(models.AbstractModel):
"""
if not isinstance(message, EmailMessage):
raise TypeError('message must be an email.message.EmailMessage at this point')
catchall_domain_lowered = self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain", "").strip().lower()
catchall_domains_allowed = self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain.allowed")
if catchall_domain_lowered and catchall_domains_allowed:
catchall_domains_allowed = catchall_domains_allowed.split(',') + [catchall_domain_lowered]
catchall_domains_allowed = list(filter(None, (self.env["ir.config_parameter"].sudo().get_param(
"mail.catchall.domain.allowed") or '').split(',')))
if catchall_domains_allowed:
catchall_domains_allowed += self.env['mail.alias.domain'].search([]).mapped('name')
def _filter_excluded_local_part(email):
left, _at, domain = email.partition('@')
if not domain:
return False
if catchall_domains_allowed and domain not in catchall_domains_allowed:
return False
return left
fallback_model = model
# handle bounce: verify whether this is a bounced email and use it to
@@ -1080,19 +1094,13 @@ class MailThread(models.AbstractModel):
# author and recipients
email_from = message_dict['email_from']
email_to = message_dict['to']
email_to_localparts = [
e.split('@', 1)[0].lower()
for e in (tools.email_split(email_to) or [''])
]
email_to_list = [e.lower() for e in (tools.email_split(message_dict['to']) or [''])]
email_to_localparts = list(filter(None, (_filter_excluded_local_part(email_to) for email_to in email_to_list)))
# Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values
# for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value.
rcpt_tos_localparts = []
for recipient in tools.email_split(message_dict['recipients']):
to_local, to_domain = recipient.split('@', maxsplit=1)
if not catchall_domains_allowed or to_domain.lower() in catchall_domains_allowed:
rcpt_tos_localparts.append(to_local.lower())
rcpt_tos_valid_localparts = [to for to in rcpt_tos_localparts]
rcpt_tos_list = [e.lower() for e in (tools.email_split(message_dict['recipients']) or [''])]
rcpt_tos_localparts = list(filter(None, (_filter_excluded_local_part(email_to) for email_to in rcpt_tos_list)))
rcpt_tos_valid_list = list(rcpt_tos_list)
# 1. Handle reply
# if destination = alias with different model -> consider it is a forward and not a reply
@@ -1100,20 +1108,32 @@ class MailThread(models.AbstractModel):
if reply_model and reply_thread_id:
reply_model_id = self.env['ir.model']._get_id(reply_model)
other_model_aliases = self.env['mail.alias'].search([
'&', '&',
('alias_name', '!=', False),
('alias_name', 'in', email_to_localparts),
'&',
('alias_model_id', '!=', reply_model_id),
'|',
('alias_full_name', 'in', email_to_list),
'&', ('alias_name', 'in', email_to_localparts), ('alias_incoming_local', '=', True),
])
if other_model_aliases:
is_a_reply, reply_model, reply_thread_id = False, False, False
rcpt_tos_valid_localparts = [to for to in rcpt_tos_valid_localparts if to in other_model_aliases.mapped('alias_name')]
rcpt_tos_valid_list = [
to
for to in rcpt_tos_valid_list
if (
to in other_model_aliases.mapped('alias_full_name')
or to.split('@', 1)[0] in other_model_aliases.filtered('alias_incoming_local').mapped('alias_name')
)
]
rcpt_tos_valid_localparts = list(filter(None, (_filter_excluded_local_part(email_to) for email_to in rcpt_tos_valid_list)))
if is_a_reply and reply_model:
reply_model_id = self.env['ir.model']._get_id(reply_model)
dest_aliases = self.env['mail.alias'].search([
('alias_name', 'in', rcpt_tos_localparts),
('alias_model_id', '=', reply_model_id)
'&',
('alias_model_id', '=', reply_model_id),
'|',
('alias_full_name', 'in', rcpt_tos_list),
'&', ('alias_name', 'in', rcpt_tos_localparts), ('alias_incoming_local', '=', True),
], limit=1)
user_id = self._mail_find_user_for_gateway(email_from, alias=dest_aliases).id or self._uid
@@ -1124,27 +1144,31 @@ class MailThread(models.AbstractModel):
if route:
_logger.info(
'Routing mail from %s to %s with Message-Id %s: direct reply to msg: model: %s, thread_id: %s, custom_values: %s, uid: %s',
email_from, email_to, message_id, reply_model, reply_thread_id, custom_values, self._uid)
email_from, message_dict['to'], message_id, reply_model, reply_thread_id, custom_values, self._uid)
return [route]
if route is False:
return []
# 2. Handle new incoming email by checking aliases and applying their settings
if rcpt_tos_localparts:
if rcpt_tos_list:
# no route found for a matching reference (or reply), so parent is invalid
message_dict.pop('parent_id', None)
# check it does not directly contact catchall
if self._detect_write_to_catchall(message_dict):
_logger.info('Routing mail from %s to %s with Message-Id %s: direct write to catchall, bounce',
email_from, email_to, message_id)
email_from, message_dict['to'], message_id)
body = self.env['ir.qweb']._render('mail.mail_bounce_catchall', {
'message': message,
})
self._routing_create_bounce_email(email_from, body, message, references=message_id, reply_to=self.env.company.email)
return []
dest_aliases = self.env['mail.alias'].search([('alias_name', 'in', rcpt_tos_valid_localparts)])
dest_aliases = self.env['mail.alias'].search([
'|',
('alias_full_name', 'in', rcpt_tos_valid_list),
'&', ('alias_name', 'in', rcpt_tos_valid_localparts), ('alias_incoming_local', '=', True),
])
if dest_aliases:
routes = []
for alias in dest_aliases:
@@ -1154,7 +1178,7 @@ class MailThread(models.AbstractModel):
if route:
_logger.info(
'Routing mail from %s to %s with Message-Id %s: direct alias match: %r',
email_from, email_to, message_id, route)
email_from, message_dict['to'], message_id, route)
routes.append(route)
return routes
@@ -1170,14 +1194,14 @@ class MailThread(models.AbstractModel):
if route:
_logger.info(
'Routing mail from %s to %s with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s',
email_from, email_to, message_id, fallback_model, thread_id, custom_values, user_id)
email_from, message_dict['to'], message_id, fallback_model, thread_id, custom_values, user_id)
return [route]
# ValueError if no routes found and if no bounce occurred
raise ValueError(
'No possible route found for incoming message from %s to %s (Message-Id %s:). '
'Create an appropriate mail.alias or force the destination model.' %
(email_from, email_to, message_id)
(email_from, message_dict['to'], message_id)
)
@api.model
@@ -1842,12 +1866,8 @@ class MailThread(models.AbstractModel):
if not normalized_email:
return self.env['res.users']
catchall_domain = self.env['ir.config_parameter'].sudo().get_param("mail.catchall.domain")
if catchall_domain:
left_part = normalized_email.split('@')[0] if normalized_email.split('@')[1] == catchall_domain.lower() else False
if left_part:
if self.env['mail.alias'].sudo().search_count([('alias_name', '=', left_part)]):
return self.env['res.users']
if self.env['mail.alias'].sudo().search_count([('alias_full_name', '=', email_value)]):
return self.env['res.users']
if alias and alias.alias_parent_model_id and alias.alias_parent_thread_id:
followers = self.env['mail.followers'].search([
@@ -1895,7 +1915,6 @@ class MailThread(models.AbstractModel):
followers = records.mapped('message_partner_ids')
else:
followers = self.env['res.partner']
catchall_domain = self.env['ir.config_parameter'].sudo().get_param("mail.catchall.domain")
# first, build a normalized email list and remove those linked to aliases
# to avoid adding aliases as partners. In case of multi-email input, use
@@ -1904,11 +1923,9 @@ class MailThread(models.AbstractModel):
for email_normalized in (tools.email_normalize(contact, strict=False) for contact in emails)
if email_normalized
]
if catchall_domain:
domain_left_parts = [email.split('@')[0] for email in normalized_emails if email and email.split('@')[1] == catchall_domain.lower()]
if domain_left_parts:
found_alias_names = self.env['mail.alias'].sudo().search([('alias_name', 'in', domain_left_parts)]).mapped('alias_name')
normalized_emails = [email for email in normalized_emails if email.split('@')[0] not in found_alias_names]
matching_aliases = self.env['mail.alias'].sudo().search([('alias_full_name', 'in', normalized_emails)])
if matching_aliases:
normalized_emails = [email for email in normalized_emails if email not in matching_aliases.mapped('alias_full_name')]
done_partners = [follower for follower in followers if follower.email_normalized in normalized_emails]
remaining = [email for email in normalized_emails if email not in [partner.email_normalized for partner in done_partners]]
+2
View File
@@ -42,6 +42,7 @@
<field name="alias_force_thread_id"/>
<field name="alias_defaults"/>
<field name="alias_contact"/>
<field name="alias_incoming_local"/>
<field name="alias_parent_model_id"/>
<field name="alias_parent_thread_id"/>
</group>
@@ -66,6 +67,7 @@
<field name="alias_parent_thread_id" optional="hide"/>
<field name="alias_defaults" optional="hide"/>
<field name="alias_contact"/>
<field name="alias_incoming_local" optional="hide"/>
<field name="alias_status" widget="badge"
decoration-success="alias_status == 'valid'"
decoration-warning="alias_status == 'not_tested'"
+4 -4
View File
@@ -45,7 +45,7 @@ class TestProjectFlow(TestProjectCommon, MailCommon):
def test_task_process_without_stage(self):
# Do: incoming mail from an unknown partner on an alias creates a new task 'Frogs'
task = self.format_and_process(
EMAIL_TPL, to='project+pigs@mydomain.com, valid.lelitre@agrolait.com', cc='valid.other@gmail.com',
EMAIL_TPL, to=f'project+pigs@{self.alias_domain}, valid.lelitre@agrolait.com', cc='valid.other@gmail.com',
email_from='%s' % self.user_projectuser.email,
subject='Frogs', msg_id='<1198923581.41972151344608186760.JavaMail@agrolait.com>',
target_model='project.task')
@@ -72,7 +72,7 @@ class TestProjectFlow(TestProjectCommon, MailCommon):
def test_task_process_with_stages(self):
# Do: incoming mail from an unknown partner on an alias creates a new task 'Cats'
task = self.format_and_process(
EMAIL_TPL, to='project+goats@mydomain.com, valid.lelitre@agrolait.com', cc='valid.other@gmail.com',
EMAIL_TPL, to=f'project+goats@{self.alias_domain}, valid.lelitre@agrolait.com', cc='valid.other@gmail.com',
email_from='%s' % self.user_projectuser.email,
subject='Cats', msg_id='<1198923581.41972151344608186760.JavaMail@agrolait.com>',
target_model='project.task')
@@ -99,7 +99,7 @@ class TestProjectFlow(TestProjectCommon, MailCommon):
def test_task_from_email_alias(self):
# Do: incoming mail from a known partner email on an alias creates a new task 'Super Frog'
task = self.format_and_process(
EMAIL_TPL, to='project+goats@mydomain.com, valid.lelitre@agrolait.com', cc='valid.other@gmail.com',
EMAIL_TPL, to=f'project+goats@{self.alias_domain}, valid.lelitre@agrolait.com', cc='valid.other@gmail.com',
email_from='%s' % self.user_portal.email,
subject='Super Frog', msg_id='<1198923581.41972151344608186760.JavaMail@agrolait.com>',
target_model='project.task')
@@ -131,7 +131,7 @@ class TestProjectFlow(TestProjectCommon, MailCommon):
self.assertFalse(new_partner)
task = self.format_and_process(
EMAIL_TPL, to='project+pigs@mydomain.com, valid.lelitre@agrolait.com',
EMAIL_TPL, to=f'project+pigs@{self.alias_domain}, valid.lelitre@agrolait.com',
cc='valid.other@gmail.com',
email_from=email,
subject='subject',
@@ -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. """
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
11 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
12 access_mail_test_gateway_portal mail.test.gateway.portal model_mail_test_gateway base.group_portal 1 0 0 0
13 access_mail_test_gateway_user mail.test.gateway.user model_mail_test_gateway base.group_user 1 1 1 1
14 access_mail_test_gateway_company_user mail.test.gateway.company.user model_mail_test_gateway_company base.group_user 1 1 1 1
15 access_mail_test_gateway_groups_portal mail.test.gateway.groups.portal model_mail_test_gateway_groups base.group_portal 1 0 0 0
16 access_mail_test_gateway_groups_user mail.test.gateway.groups.user model_mail_test_gateway_groups base.group_user 1 1 1 1
17 access_mail_test_track_portal mail.test.track.portal model_mail_test_track base.group_portal 0 0 0 0
+1 -2
View File
@@ -33,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 [
+61 -8
View File
@@ -153,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" <test.sylvie.lelitre@agrolait.com>'
cls.test_record = cls.env['mail.test.gateway'].with_context(cls._test_context).create({
@@ -164,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')
@@ -380,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
})
@@ -520,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',
@@ -792,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 """
@@ -806,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',
@@ -832,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',
@@ -861,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',
@@ -886,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',
@@ -907,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',
})
@@ -917,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', ""),
@@ -926,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)
@@ -1437,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',
@@ -1457,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',
@@ -1476,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',
@@ -1549,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": "{}",
@@ -1572,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'}",
@@ -1698,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',
@@ -1828,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,
})
@@ -1852,6 +1904,7 @@ class TestMailThreadCC(MailCommon):
cls.email_from = 'Sylvie Lelitre <test.sylvie.lelitre@agrolait.com>'
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',
})
@@ -294,7 +294,6 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase):
@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)