Files
odoo_source/addons/mail/models/mail_alias_domain.py
T
Thibault Delavallée 50f5bc4a9b [IMP] mail: migrate alias ICP at install if they exist
When adding alias domain in v17 support of configuration parameter
was dropped. We moved from singleton configuration to multi domains
using real models.

This means most of mail support lies in mail while part of it was in base
beforehand. In some cases we want to let people do some basic
configuration using base module then install mail which could migrate
this ICP based configuration into new models. This is notably the case
with odoo.sh where mail is not always automatically installed.

Use case is: install base, do some configuration using ICP allowing notably
to setup website / server domains, main mail configuration, ... then let
people install modules on top of that configuration which generally
installs mail. With this change existing ir.config_parameters matching
pre v17 names are now used to bootstrap alias domain table at mail module
initialization.

Task-3789584

closes odoo/odoo#156654

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
2024-03-07 11:40:23 +00:00

201 lines
9.5 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, exceptions, fields, models, _
from odoo.addons.mail.models.mail_alias import dot_atom_text
class AliasDomain(models.Model):
""" Model alias domains, now company-specific. Alias domains are email
domains used to receive emails through catchall and bounce aliases, as
well as using mail.alias records to redirect email replies.
This replaces ``mail.alias.domain`` configuration parameter use until v16.
"""
_name = 'mail.alias.domain'
_description = "Email Domain"
_order = 'sequence ASC, id ASC'
name = fields.Char(
'Name', required=True,
help="Email domain e.g. 'example.com' in 'odoo@example.com'")
company_ids = fields.One2many(
'res.company', 'alias_domain_id', string='Companies',
help="Companies using this domain as default for sending mails")
sequence = fields.Integer(default=10)
bounce_alias = fields.Char(
'Bounce Alias', default='bounce', required=True,
help="Local-part of email used for Return-Path used when emails bounce e.g. "
"'bounce' in 'bounce@example.com'")
bounce_email = fields.Char('Bounce Email', compute='_compute_bounce_email')
catchall_alias = fields.Char(
'Catchall Alias', default='catchall', required=True,
help="Local-part of email used for Reply-To to catch answers e.g. "
"'catchall' in 'catchall@example.com'")
catchall_email = fields.Char('Catchall Email', compute='_compute_catchall_email')
default_from = fields.Char(
'Default From Alias', default='notifications',
help="Default from when it does not match outgoing server filters. Can be either "
"a local-part e.g. 'notifications' either a complete email address e.g. "
"'notifications@example.com' to override all outgoing emails.")
default_from_email = fields.Char('Default From', compute='_compute_default_from_email')
_sql_constraints = [
(
'bounce_email_uniques',
'UNIQUE(bounce_alias, name)',
'Bounce emails should be unique'
),
(
'catchall_email_uniques',
'UNIQUE(catchall_alias, name)',
'Catchall emails should be unique'
),
]
@api.depends('bounce_alias', 'name')
def _compute_bounce_email(self):
self.bounce_email = ''
for domain in self.filtered('bounce_alias'):
domain.bounce_email = f'{domain.bounce_alias}@{domain.name}'
@api.depends('catchall_alias', 'name')
def _compute_catchall_email(self):
self.catchall_email = ''
for domain in self.filtered('catchall_alias'):
domain.catchall_email = f'{domain.catchall_alias}@{domain.name}'
@api.depends('default_from', 'name')
def _compute_default_from_email(self):
""" Default from may be a valid complete email and not only a left-part
like bounce or catchall aliases. Adding domain name should therefore
be done only if necessary. """
self.default_from_email = ''
for domain in self.filtered('default_from'):
if "@" in domain.default_from:
domain.default_from_email = domain.default_from
else:
domain.default_from_email = f'{domain.default_from}@{domain.name}'
@api.constrains('bounce_alias', 'catchall_alias')
def _check_bounce_catchall_uniqueness(self):
names = self.filtered('bounce_alias').mapped('bounce_alias') + self.filtered('catchall_alias').mapped('catchall_alias')
if not names:
return
similar_domains = self.env['mail.alias.domain'].search([('name', 'in', self.mapped('name'))])
for tocheck in self:
if any(similar.bounce_alias == tocheck.bounce_alias
for similar in similar_domains if similar != tocheck and similar.name == tocheck.name):
raise exceptions.ValidationError(
_('Bounce alias %(bounce)s is already used for another domain with same name. '
'Use another bounce or simply use the other alias domain.',
bounce=tocheck.bounce_email)
)
if any(similar.catchall_alias == tocheck.catchall_alias
for similar in similar_domains if similar != tocheck and similar.name == tocheck.name):
raise exceptions.ValidationError(
_('Catchall alias %(catchall)s is already used for another domain with same name. '
'Use another catchall or simply use the other alias domain.',
catchall=tocheck.catchall_email)
)
# search on left-part only to speedup, then filter on right part
potential_aliases = self.env['mail.alias'].search([
('alias_name', 'in', list(set(names))),
('alias_domain_id', '!=', False)
])
existing = next(
(alias for alias in potential_aliases
if alias.display_name in (self.mapped('bounce_email') + self.mapped('catchall_email'))),
self.env['mail.alias']
)
if existing:
document_name = False
# If owner or target: display document name also in the warning
if existing.alias_parent_model_id and existing.alias_parent_thread_id:
document_name = self.env[existing.alias_parent_model_id.model].sudo().browse(existing.alias_parent_thread_id).display_name
elif existing.alias_model_id and existing.alias_force_thread_id:
document_name = self.env[existing.alias_model_id.model].sudo().browse(existing.alias_force_thread_id).display_name
if document_name:
raise exceptions.ValidationError(
_("Bounce/Catchall '%(matching_alias_name)s' is already used by %(document_name)s. Choose another alias or change it on the other document.",
matching_alias_name=existing.display_name,
document_name=document_name)
)
raise exceptions.ValidationError(
_("Bounce/Catchall '%(matching_alias_name)s' is already used. Choose another alias or change it on the linked model.",
matching_alias_name=existing.display_name)
)
@api.constrains('name')
def _check_name(self):
""" Should match a sanitized version of itself, otherwise raise to warn
user (do not dynamically change it, would be confusing). """
for domain in self:
if not dot_atom_text.match(domain.name):
raise exceptions.ValidationError(
_("You cannot use anything else than unaccented latin characters in the domain name %(domain_name)s.",
domain_name=domain.name)
)
@api.model_create_multi
def create(self, vals_list):
""" Sanitize bounce_alias / catchall_alias / default_from """
for vals in vals_list:
self._sanitize_configuration(vals)
alias_domains = super().create(vals_list)
# alias domain init: populate companies and aliases at first creation
if alias_domains and self.search_count([]) == len(alias_domains):
self.env['res.company'].search(
[('alias_domain_id', '=', False)]
).alias_domain_id = alias_domains[0].id
self.env['mail.alias'].sudo().search(
[('alias_domain_id', '=', False)]
).alias_domain_id = alias_domains[0].id
return alias_domains
def write(self, vals):
""" Sanitize bounce_alias / catchall_alias / default_from """
self._sanitize_configuration(vals)
return super().write(vals)
@api.model
def _sanitize_configuration(self, config_values):
""" Tool sanitizing configuration values for domains """
if config_values.get('bounce_alias'):
config_values['bounce_alias'] = self.env['mail.alias']._sanitize_alias_name(config_values['bounce_alias'])
if config_values.get('catchall_alias'):
config_values['catchall_alias'] = self.env['mail.alias']._sanitize_alias_name(config_values['catchall_alias'])
if config_values.get('default_from'):
config_values['default_from'] = self.env['mail.alias']._sanitize_alias_name(
config_values['default_from'], is_email=True
)
return config_values
@api.model
def _migrate_icp_to_domain(self):
""" Compatibility layer helping going from pre-v17 ICP to alias
domains. Mainly used when base mail configuration is done with 'base'
module only and 'mail' is installed afterwards: configuration should
not be lost (odoo.sh use case). """
Icp = self.env['ir.config_parameter'].sudo()
alias_domain = Icp.get_param('mail.catchall.domain')
if alias_domain:
existing = self.search([('name', '=', alias_domain)])
if existing:
return existing
bounce_alias = Icp.get_param('mail.bounce.alias')
catchall_alias = Icp.get_param('mail.catchall.alias')
default_from = Icp.get_param('mail.default.from')
return self.create({
'bounce_alias': bounce_alias or 'bounce',
'catchall_alias': catchall_alias or 'catchall',
'default_from': default_from or 'notifications',
'name': alias_domain,
})
return self.browse()