From bd2bdb5a1a9f969b1709c596c8577b51b1bae7ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 27 Jul 2023 12:07:39 +0200 Subject: [PATCH] [IMP] mail: allow to search on alias complete email 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 Currently alias search is done only on 'alias_name' field. Now that alias domains can be multiple we have to be able to search on complete alias email definition e.g. when checking destination aliases of incoming emails in mail gateway. We add a new 'alias_full_name' field that is computed based on alias_name and alias_domain_id.name. It is stored so that search is possible on it. Allow to search on 'alias_full_name' from the 'mail.alias.mixin(.optional)' by making 'alias_email' field searchable, based on 'alias_full_name'. Task-36879 (Mail: Support Multi Domains Aliases) Part-of: odoo/odoo#76734 --- addons/mail/models/mail_alias.py | 14 +++ .../mail/models/mail_alias_mixin_optional.py | 5 +- addons/test_mail/tests/test_mail_alias.py | 89 +++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/addons/mail/models/mail_alias.py b/addons/mail/models/mail_alias.py index 91288442b06..590b86a7dd0 100644 --- a/addons/mail/models/mail_alias.py +++ b/addons/mail/models/mail_alias.py @@ -38,6 +38,8 @@ class Alias(models.Model): alias_name = fields.Char( 'Alias Name', copy=False, help="The name of the email alias, e.g. 'jobs' if you want to catch emails for ") + alias_full_name = fields.Char('Alias Email', compute='_compute_alias_full_name', store=True) + display_name = fields.Char(string='Display Name', compute='_compute_display_name') alias_domain_id = fields.Many2one( 'mail.alias.domain', string='Alias Domain', ondelete='restrict', default=lambda self: self.env.company.alias_domain_id) @@ -210,6 +212,18 @@ class Alias(models.Model): alias_names=', '.join(failing.mapped('display_name'))) ) + @api.depends('alias_domain_id.name', 'alias_name') + def _compute_alias_full_name(self): + """ A bit like display_name, but without the 'inactive alias' UI display. + Moreover it is stored, allowing to search on it. """ + for record in self: + if record.alias_domain_id and record.alias_name: + record.alias_full_name = f"{record.alias_name}@{record.alias_domain_id.name}" + elif record.alias_name: + record.alias_full_name = record.alias_name + else: + record.alias_full_name = False + @api.depends('alias_domain', 'alias_name') def _compute_display_name(self): """ Return the mail alias display alias_name, including the catchall diff --git a/addons/mail/models/mail_alias_mixin_optional.py b/addons/mail/models/mail_alias_mixin_optional.py index 9d18e05c613..fd8e383dd0e 100644 --- a/addons/mail/models/mail_alias_mixin_optional.py +++ b/addons/mail/models/mail_alias_mixin_optional.py @@ -25,7 +25,7 @@ class AliasMixinOptional(models.AbstractModel): related='alias_id.alias_domain_id', readonly=False) alias_domain = fields.Char('Alias Domain Name', related='alias_id.alias_domain') alias_defaults = fields.Text(related='alias_id.alias_defaults') - alias_email = fields.Char('Email Alias', compute='_compute_alias_email') + alias_email = fields.Char('Email Alias', compute='_compute_alias_email', search='_search_alias_email') @api.depends('alias_domain', 'alias_name') def _compute_alias_email(self): @@ -36,6 +36,9 @@ class AliasMixinOptional(models.AbstractModel): for record in self.filtered(lambda rec: rec.alias_name and rec.alias_domain): record.alias_email = f"{record.alias_name}@{record.alias_domain}" + def _search_alias_email(self, operator, operand): + return [('alias_id.alias_full_name', operator, operand)] + # -------------------------------------------------- # CRUD # -------------------------------------------------- diff --git a/addons/test_mail/tests/test_mail_alias.py b/addons/test_mail/tests/test_mail_alias.py index 7a43359aca7..13116ff19fb 100644 --- a/addons/test_mail/tests/test_mail_alias.py +++ b/addons/test_mail/tests/test_mail_alias.py @@ -348,6 +348,69 @@ class TestMailAlias(TestMailAliasCommon): for alias in aliases: self.assertEqual(alias.alias_name, False) + @users('admin') + def test_search(self): + """ Test search on aliases, notably searching on display_name which should + be split on searching on alias_name and alias_domain_id. """ + # ensure existing aliases to ease future asserts + existing = self.env['mail.alias'].search([('alias_domain_id', '!=', False)]) + self.assertEqual(existing.alias_domain_id, self.mail_alias_domain) + existing.write({'alias_name': False}) # don't be annoyed by existing aliases + + mail_alias_domain = self.mail_alias_domain.with_env(self.env) + mail_alias_domain_c2 = self.mail_alias_domain_c2.with_env(self.env) + self.assertEqual(mail_alias_domain.name, 'test.mycompany.com') + self.assertEqual(mail_alias_domain_c2.name, 'test.mycompany2.com') + + aliases = self.env['mail.alias'].create([ + { + 'alias_model_id': self.env['ir.model']._get('mail.test.container.mc').id, + 'alias_name': f'test.search.{idx}', + 'alias_domain_id': domain.id, + } + for idx in range(5) + for domain in (mail_alias_domain + mail_alias_domain_c2) + ]) + aliases_d1 = aliases.filtered(lambda a: a.alias_domain_id == mail_alias_domain) + aliases_d2 = aliases.filtered(lambda a: a.alias_domain_id == mail_alias_domain_c2) + + # search on alias_name: classic search + self.assertEqual( + self.env['mail.alias'].search([('alias_name', 'ilike', 'test.search')]), + aliases + ) + + # search on alias_fullname: search on aggregated of {name}@{domain} + for search_term, expected, msg in [ + ('mycompany', aliases, + 'Match all aliases on both domains as "mycompany" is contained in those two'), + (mail_alias_domain.name, aliases_d1, + 'Exact match on domain 1: should find all aliases in that domain'), + (mail_alias_domain_c2.name, aliases_d2, + 'Exact match on domain 2: should find all aliases in that domain'), + ('search.0@test.mycompany', aliases.filtered(lambda a: a.alias_name == 'test.search.0'), + 'Match in both domains'), + ('search.0@test.mycompany.com', aliases.filtered(lambda a: a.alias_name == 'test.search.0' and a.alias_domain_id == mail_alias_domain), + 'Match only in domain 1'), + ('search@test.mycompany.com', self.env['mail.alias'], + 'Does not match even as ilike'), + ]: + with self.subTest(search_term=search_term): + self.assertEqual( + self.env['mail.alias'].search([('alias_full_name', 'ilike', search_term)]), + expected, msg + ) + + # search using IN operator + for search_term, expected, msg in [ + (['mycompany'], self.env['mail.alias'], 'mycompany is too vague: does not match a left- and right- part (!= ilike)'), + ([mail_alias_domain.name], self.env['mail.alias'], 'Match only right-part of aliases emails'), + ]: + with self.subTest(search_term=search_term): + self.assertEqual(self.env['mail.alias'].search([('alias_full_name', 'in', search_term)]), + expected, msg + ) + @users('admin') def test_alias_setup(self): """ Test various constraints / configuration of alias model""" @@ -679,6 +742,32 @@ class TestMailAliasMixin(TestMailAliasCommon): with self.assertRaises(exceptions.ValidationError): record.write({'alias_defaults': "{'custom_field': brokendict"}) + @users('erp_manager') + def test_alias_mixin_alias_email(self): + """ Test 'alias_email' mixin field computation and search capability """ + Model = self.env['mail.test.container.mc'] + records = Model.create([ + { + 'alias_name': f'alias.email.{idx}', # will be present in all companies + 'company_id': company.id, + 'name': f'Test {company.id} {idx}', + } + for company in (self.company_admin, self.company_2) + for idx in range(5) + ]) + self.assertEqual( + Model.search([('alias_email', 'ilike', 'alias.email')]), records, + 'Search: partial search: any domain, matching all left parts') + self.assertEqual( + Model.search([('alias_email', 'ilike', 'alias.email.0')]), records[0] + records[5], + 'Search: partial search: any domain, matching some left parts') + self.assertEqual( + Model.search([('alias_email', '=', self.mail_alias_domain.name)]), Model, + 'Search: partial search: does not match any complete email') + self.assertEqual( + Model.search([('alias_email', '=', f'alias.email.1@{self.mail_alias_domain.name}')]), records[1], + 'Search: both part search: search on name + domain') + @users('employee') @mute_logger('odoo.addons.base.models.ir_model') def test_alias_mixin_alias_id_management(self):