[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:
@@ -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})
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 """
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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'"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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 [
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user