From 05077a34b7d973efd4bf2161230c280642ae8a61 Mon Sep 17 00:00:00 2001 From: Jakub Wiselka Date: Fri, 17 Mar 2023 15:52:22 +0100 Subject: [PATCH 1/7] innit mail_alias_with_domain module --- mail_alias_with_domain/__init__.py | 3 + mail_alias_with_domain/__manifest__.py | 16 ++ mail_alias_with_domain/models/__init__.py | 4 + mail_alias_with_domain/models/mail_alias.py | 58 ++++++++ mail_alias_with_domain/models/mail_thread.py | 44 ++++++ .../static/description/icon.png | Bin 0 -> 9455 bytes mail_alias_with_domain/tests/__init__.py | 3 + .../tests/test_mail_thread.py | 137 ++++++++++++++++++ .../views/mail_alias_views.xml | 50 +++++++ 9 files changed, 315 insertions(+) create mode 100644 mail_alias_with_domain/__init__.py create mode 100644 mail_alias_with_domain/__manifest__.py create mode 100644 mail_alias_with_domain/models/__init__.py create mode 100644 mail_alias_with_domain/models/mail_alias.py create mode 100644 mail_alias_with_domain/models/mail_thread.py create mode 100644 mail_alias_with_domain/static/description/icon.png create mode 100644 mail_alias_with_domain/tests/__init__.py create mode 100644 mail_alias_with_domain/tests/test_mail_thread.py create mode 100644 mail_alias_with_domain/views/mail_alias_views.xml diff --git a/mail_alias_with_domain/__init__.py b/mail_alias_with_domain/__init__.py new file mode 100644 index 0000000000..b66e6bd134 --- /dev/null +++ b/mail_alias_with_domain/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2023 Solvti sp. z o.o. (https://solvti.pl) + +from . import models diff --git a/mail_alias_with_domain/__manifest__.py b/mail_alias_with_domain/__manifest__.py new file mode 100644 index 0000000000..8f4a4a07d5 --- /dev/null +++ b/mail_alias_with_domain/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2023 Solvti sp. z o.o. (https://solvti.pl) + +{ + "name": "Mail Alias With Domain", + "summary": """ + Extend alias fnctionality by giving possibility + to setup alias with custom domain""", + "author": "Solvti, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/social", + "version": "13.0.1.0.0", + "license": "AGPL-3", + "application": False, + "installable": True, + "depends": ["mail"], + "data": ["views/mail_alias_views.xml"], +} diff --git a/mail_alias_with_domain/models/__init__.py b/mail_alias_with_domain/models/__init__.py new file mode 100644 index 0000000000..7e891a0d4d --- /dev/null +++ b/mail_alias_with_domain/models/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2023 Solvti sp. z o.o. (https://solvti.pl) + +from . import mail_alias +from . import mail_thread diff --git a/mail_alias_with_domain/models/mail_alias.py b/mail_alias_with_domain/models/mail_alias.py new file mode 100644 index 0000000000..4c71110647 --- /dev/null +++ b/mail_alias_with_domain/models/mail_alias.py @@ -0,0 +1,58 @@ +# Copyright 2023 Solvti sp. z o.o. (https://solvti.pl) + +import hashlib + +from odoo import api, fields, models + + +def generate_hash(value): + mail_hash = hashlib.md5(value.encode("utf-8")).hexdigest() + return mail_hash + + +class Alias(models.Model): + _inherit = "mail.alias" + + alias_domain = fields.Char(inverse="_inverse_alias_domain", store=True,) + alias_display_name = fields.Char() + alias_name = fields.Char(compute="_compute_alias_name_and_hash", store=True) + alias_hash = fields.Char(compute="_compute_alias_name_and_hash", store=True) + check_domain = fields.Boolean( + help=( + "Determines whether alias should be processed together with domain.\n" + "If checked domain will be taken into account during mail processing.\n\n" + "Alias name is genereted as follow: 'HASH+alias_name'\n\n" + "*HASH = hash(alias_display_name + alias_domain)" + ), + ) + + def _inverse_alias_domain(self): + pass + + @api.depends("alias_domain", "alias_display_name", "check_domain", "alias_name") + def _compute_alias_name_and_hash(self): + for rec in self: + if rec.check_domain and rec.alias_display_name and rec.alias_domain: + alias_hash = generate_hash(rec.alias_display_name + rec.alias_domain) + rec.alias_hash = alias_hash + rec.alias_name = alias_hash + "+" + rec.alias_display_name + elif rec.alias_name or rec.alias_display_name: + name = rec.alias_display_name or rec.alias_name + rec.alias_hash = False + rec.alias_name = rec.alias_display_name = rec._clean_and_make_unique( + name, alias_ids=rec.ids + ) + else: + rec.alias_hash = rec.alias_name = False + + def write(self, vals): + name = vals.get("alias_name") or vals.get("alias_display_name") + if name and not vals.get("check_domain") and not self.check_domain: + vals["alias_name"] = vals[ + "alias_display_name" + ] = self._clean_and_make_unique(name, alias_ids=self.ids) + if vals.get("check_domain") is False: + vals["alias_domain"] = ( + self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain") + ) + return super().write(vals) diff --git a/mail_alias_with_domain/models/mail_thread.py b/mail_alias_with_domain/models/mail_thread.py new file mode 100644 index 0000000000..0945cbbd88 --- /dev/null +++ b/mail_alias_with_domain/models/mail_thread.py @@ -0,0 +1,44 @@ +# Copyright 2023 Solvti sp. z o.o. (https://solvti.pl) + +import logging + +from odoo import api, models, tools + +from .mail_alias import generate_hash + +_logger = logging.getLogger(__name__) + + +class MailThread(models.AbstractModel): + _inherit = "mail.thread" + + @api.model + def message_route( + self, message, message_dict, model=None, thread_id=None, custom_values=None + ): + """ Prepare message_dict by extending recipients + with found aliases mails base on alias and domain + """ + try: + maching_aliases = self._find_alias_with_domain(message_dict) + if maching_aliases: + recipients = ( + f"{message_dict['recipients']}," + f"{','.join(maching_aliases.mapped('display_name'))}" + ) + message_dict["recipients"] = recipients + except Exception as e: + _logger.error(f"Unexpected error during processing alias with domain: {e}") + return super().message_route( + message, message_dict, model, thread_id, custom_values + ) + + def _find_alias_with_domain(self, message_dict): + emails = {email for email in (tools.email_split(message_dict["recipients"]))} + hash_list = list( + map(generate_hash, [email.replace("@", "") for email in emails]) + ) + match = self.env["mail.alias"].search( + [("check_domain", "=", True), ("alias_hash", "in", hash_list)] + ) + return match diff --git a/mail_alias_with_domain/static/description/icon.png b/mail_alias_with_domain/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/mail_alias_with_domain/tests/__init__.py b/mail_alias_with_domain/tests/__init__.py new file mode 100644 index 0000000000..8e07c10614 --- /dev/null +++ b/mail_alias_with_domain/tests/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2023 Solvti sp. z o.o. (https://solvti.pl) + +from . import test_mail_thread diff --git a/mail_alias_with_domain/tests/test_mail_thread.py b/mail_alias_with_domain/tests/test_mail_thread.py new file mode 100644 index 0000000000..17ae5e40fd --- /dev/null +++ b/mail_alias_with_domain/tests/test_mail_thread.py @@ -0,0 +1,137 @@ +# Copyright 2023 Solvti sp. z o.o. (https://solvti.pl) + +from email.message import EmailMessage + +from odoo.tests import TransactionCase + + +class TestMailThread(TransactionCase): + def setUp(self): + super(TestMailThread, self).setUp() + + self.contact_model = self.env["ir.model"].search( + [("model", "=", "res.partner")] + ) + self.mail_alias_with_domain = self.env["mail.alias"].create( + { + "check_domain": True, + "alias_display_name": "test_alias_hash", + "alias_domain": "example.com", + "alias_model_id": self.contact_model.id, + "alias_defaults": "{'name': 'Test Alias Hash'}", + } + ) + self.mail_alias = self.env["mail.alias"].create( + { + "alias_name": "test_alias", + "alias_model_id": self.contact_model.id, + "alias_defaults": "{'name': 'Test Alias'}", + } + ) + + message = EmailMessage() + message.add_header("Subject", "New Alias Test") + message.add_header("From", "test.user@example.com") + message.set_default_type("text/plain") + message.set_content("Please Create New Contact!") + self.message = message + + self.message_dict = { + "message_type": "email", + "message_id": "", + "subject": "New Contact", + "email_from": '"test.user@company.com" ', + "from": '"test.user@company.com" ', + "cc": "", + "partner_ids": [], + "references": "", + "in_reply_to": "", + "date": "2021-09-23 09:03:13", + "body": "Hello, Please create new contact", + "attachments": [], + "bounced_email": False, + "bounced_partner": "", + "bounced_msg_id": False, + "bounced_message": "", + } + + def test_create_alias_by_alias_name_and_alias_display_name(self): + self.assertTrue(self.mail_alias_with_domain.alias_name) + self.assertTrue(self.mail_alias.alias_display_name) + + def test_generate_hash(self): + alias = self.mail_alias_with_domain + self.assertEqual(alias.alias_hash, alias.alias_name.split("+")[0]) + + def test_message_route_include_hash_alias(self): + email_to = "test_alias_hash@example.com" + self.message.add_header("To", email_to) + self.message_dict.update( + { + "recipients": f'"{email_to}" <{email_to}>', + "to": f""" + "{email_to}" <{email_to}>, "someone@test-fake.com" + """, + } + ) + routes = self.env["mail.thread"].message_route( + self.message, + self.message_dict, + model=self.contact_model.model, + thread_id=None, + custom_values=None, + ) + self.assertTrue( + any( + self.mail_alias_with_domain == alias for alias in [r[4] for r in routes] + ) + ) + + def test_message_route_differend_domain_for_hash_alias(self): + email_to = "test_alias_hash@something_else.com" + self.message.add_header("To", email_to) + self.message_dict.update( + { + "recipients": f'"{email_to}" <{email_to}>', + "to": ( + f'"{email_to}" <{email_to}>, ' + '"someone@test-fake.com" ' + ), + } + ) + routes = self.env["mail.thread"].message_route( + self.message, + self.message_dict, + model=self.contact_model.model, + thread_id=None, + custom_values=None, + ) + self.assertFalse( + any( + self.mail_alias_with_domain == alias for alias in [r[4] for r in routes] + ) + ) + + def test_message_route_two_types_of_aliases_at_once(self): + email_to_1 = "test_alias_hash@example.com" + email_to_2 = "test_alias@example_mail.com" + self.message.add_header("To", f"{email_to_1}, {email_to_2}") + self.message_dict.update( + { + "recipients": f'"{email_to_1}" <{email_to_1}>, "{email_to_2}" <{email_to_2}>', + "to": ( + f'"{email_to_1}" <{email_to_1}>, "{email_to_2}" <{email_to_2}>,' + '"abc@abc.com" ' + ), + } + ) + routes = self.env["mail.thread"].message_route( + self.message, + self.message_dict, + model=self.contact_model.model, + thread_id=None, + custom_values=None, + ) + self.assertTrue( + {r[4] for r in routes} == {self.mail_alias_with_domain, self.mail_alias} + ) diff --git a/mail_alias_with_domain/views/mail_alias_views.xml b/mail_alias_with_domain/views/mail_alias_views.xml new file mode 100644 index 0000000000..144dcab22c --- /dev/null +++ b/mail_alias_with_domain/views/mail_alias_views.xml @@ -0,0 +1,50 @@ + + + + + + mail.alias.form.inherit + mail.alias + + + + + + + + 1 + + + + + + + + + + + {'readonly': [('check_domain','=',False)], 'required': [('check_domain','=',True)]} + + + + + + mail.alias.tree.inherit + mail.alias + + + + + + + + + From 0a1252c0ee5b1a8da5f3c56bef7f411a2e5f2ff5 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Wed, 26 Mar 2025 10:14:35 +0100 Subject: [PATCH 2/7] [IMP] mail_alias_with_domain: pre-commit stuff --- mail_alias_with_domain/models/mail_alias.py | 5 ++++- mail_alias_with_domain/models/mail_thread.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/mail_alias_with_domain/models/mail_alias.py b/mail_alias_with_domain/models/mail_alias.py index 4c71110647..aea75ce408 100644 --- a/mail_alias_with_domain/models/mail_alias.py +++ b/mail_alias_with_domain/models/mail_alias.py @@ -13,7 +13,10 @@ def generate_hash(value): class Alias(models.Model): _inherit = "mail.alias" - alias_domain = fields.Char(inverse="_inverse_alias_domain", store=True,) + alias_domain = fields.Char( + inverse="_inverse_alias_domain", + store=True, + ) alias_display_name = fields.Char() alias_name = fields.Char(compute="_compute_alias_name_and_hash", store=True) alias_hash = fields.Char(compute="_compute_alias_name_and_hash", store=True) diff --git a/mail_alias_with_domain/models/mail_thread.py b/mail_alias_with_domain/models/mail_thread.py index 0945cbbd88..9d17795784 100644 --- a/mail_alias_with_domain/models/mail_thread.py +++ b/mail_alias_with_domain/models/mail_thread.py @@ -16,8 +16,8 @@ class MailThread(models.AbstractModel): def message_route( self, message, message_dict, model=None, thread_id=None, custom_values=None ): - """ Prepare message_dict by extending recipients - with found aliases mails base on alias and domain + """Prepare message_dict by extending recipients + with found aliases mails base on alias and domain """ try: maching_aliases = self._find_alias_with_domain(message_dict) From da8d655844b7d1c5f3bad47a07d23ca76fbb50bd Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Wed, 26 Mar 2025 11:22:36 +0100 Subject: [PATCH 3/7] [MIG] mail_alias_with_domain: migrate to 16.0 --- mail_alias_with_domain/README.rst | 131 +++++ mail_alias_with_domain/__init__.py | 3 +- mail_alias_with_domain/__manifest__.py | 11 +- mail_alias_with_domain/models/__init__.py | 2 +- mail_alias_with_domain/models/mail_alias.py | 138 ++++-- mail_alias_with_domain/models/mail_thread.py | 59 ++- mail_alias_with_domain/post_init_hook.py | 10 + .../readme/CONTRIBUTORS.rst | 7 + mail_alias_with_domain/readme/DESCRIPTION.rst | 41 ++ mail_alias_with_domain/readme/USAGE.rst | 4 + .../static/description/index.html | 466 ++++++++++++++++++ mail_alias_with_domain/tests/__init__.py | 2 +- .../tests/test_mail_thread.py | 152 ++++-- .../views/mail_alias_views.xml | 69 +-- 14 files changed, 923 insertions(+), 172 deletions(-) create mode 100644 mail_alias_with_domain/README.rst create mode 100644 mail_alias_with_domain/post_init_hook.py create mode 100644 mail_alias_with_domain/readme/CONTRIBUTORS.rst create mode 100644 mail_alias_with_domain/readme/DESCRIPTION.rst create mode 100644 mail_alias_with_domain/readme/USAGE.rst create mode 100644 mail_alias_with_domain/static/description/index.html diff --git a/mail_alias_with_domain/README.rst b/mail_alias_with_domain/README.rst new file mode 100644 index 0000000000..d8b60734e9 --- /dev/null +++ b/mail_alias_with_domain/README.rst @@ -0,0 +1,131 @@ +====================== +Mail Alias With Domain +====================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:3c7166c28331e6e457d77bd71f2af2420004c8b92ad4b8d745f80ad1e6884603 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsocial-lightgray.png?logo=github + :target: https://github.com/OCA/social/tree/16.0/mail_alias_with_domain + :alt: OCA/social +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/social-16-0/social-16-0-mail_alias_with_domain + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/social&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds possibility to process aliases together with domain. + +For example, suppose we have 3 companies in odoo. +Each company wants to have an alias where customers can send the bills. +invoice@company1.com +invoice@company2.com +invoice@company3.com + +In odoo, aliases are unique, and this module extends this functionality in +such a way that you can have many of the same aliases but with different domains. + +Note that when an incoming mail can be linked to an alias with a domain, +this will be the only alias used. However when an incoming mail can be +linked to multiple aliasses that have a domain, it is possible to have +multiple used. + +FOR DEVELOPERS + +In the default alias system, only the local part of an email address (the part +before the @) is used to link an incoming email to an alias. This happens in the +message_route method of the mail.thread model. + +Aliasses in standard Odoo store the alias_name field without domain. + +To still be able to use a domain name, we need a trick. What we will do is: + +* Replace the alias_name in the user interface with an alias_entry field, where a + complete email address can be entered. + +* If an alias is entered as a complete email address, this will be stored in the + alias_name as __at__. For instance alex__at__example.com. + alias_name is therefore changed from a writable field to a stored computed field. + +* The computation of alias_domain will be enhanced to take full email addresses into + account. + +* If an incoming mail can be linked to a full email address alias, we will write a + context key pointing to this alias. The search method of mail.alias will be overriden + to check for this key, and then not search at all, but just return the alias + requested. + + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To use this module, you need to: + +Got to the mail aliasses and check which aliasses you want to link to a specific +domain. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Solvti +* Therp BV + +Contributors +~~~~~~~~~~~~ + +* `Solvti sp. z o.o. `_: + + * Jakub Wiselka + +* `Therp `_: + + * Ronald Portier (ronald@therp.nl) + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/social `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/mail_alias_with_domain/__init__.py b/mail_alias_with_domain/__init__.py index b66e6bd134..53d63dae48 100644 --- a/mail_alias_with_domain/__init__.py +++ b/mail_alias_with_domain/__init__.py @@ -1,3 +1,4 @@ -# Copyright 2023 Solvti sp. z o.o. (https://solvti.pl) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from . import models +from .post_init_hook import init_alias_entry diff --git a/mail_alias_with_domain/__manifest__.py b/mail_alias_with_domain/__manifest__.py index 8f4a4a07d5..715bc8b151 100644 --- a/mail_alias_with_domain/__manifest__.py +++ b/mail_alias_with_domain/__manifest__.py @@ -1,16 +1,17 @@ # Copyright 2023 Solvti sp. z o.o. (https://solvti.pl) +# Copyright 2025 Therp BV (https://therp.nl) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). { "name": "Mail Alias With Domain", - "summary": """ - Extend alias fnctionality by giving possibility - to setup alias with custom domain""", - "author": "Solvti, Odoo Community Association (OCA)", + "summary": "Allow simple mail alias to be combined with a mail domain", + "author": "Solvti, Therp BV, Odoo Community Association (OCA)", "website": "https://github.com/OCA/social", - "version": "13.0.1.0.0", + "version": "16.0.1.0.0", "license": "AGPL-3", "application": False, "installable": True, + "post_init_hook": "init_alias_entry", "depends": ["mail"], "data": ["views/mail_alias_views.xml"], } diff --git a/mail_alias_with_domain/models/__init__.py b/mail_alias_with_domain/models/__init__.py index 7e891a0d4d..06aaa650a8 100644 --- a/mail_alias_with_domain/models/__init__.py +++ b/mail_alias_with_domain/models/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2023 Solvti sp. z o.o. (https://solvti.pl) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from . import mail_alias from . import mail_thread diff --git a/mail_alias_with_domain/models/mail_alias.py b/mail_alias_with_domain/models/mail_alias.py index aea75ce408..63b48cccb2 100644 --- a/mail_alias_with_domain/models/mail_alias.py +++ b/mail_alias_with_domain/models/mail_alias.py @@ -1,61 +1,101 @@ -# Copyright 2023 Solvti sp. z o.o. (https://solvti.pl) - -import hashlib +# Copyright 2023 Solvti sp. z o.o. (https://solvti.pl). +# Copyright 2025 Therp BV (https://therp.nl). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import api, fields, models -def generate_hash(value): - mail_hash = hashlib.md5(value.encode("utf-8")).hexdigest() - return mail_hash - - class Alias(models.Model): _inherit = "mail.alias" - alias_domain = fields.Char( - inverse="_inverse_alias_domain", - store=True, - ) - alias_display_name = fields.Char() - alias_name = fields.Char(compute="_compute_alias_name_and_hash", store=True) - alias_hash = fields.Char(compute="_compute_alias_name_and_hash", store=True) - check_domain = fields.Boolean( - help=( - "Determines whether alias should be processed together with domain.\n" - "If checked domain will be taken into account during mail processing.\n\n" - "Alias name is genereted as follow: 'HASH+alias_name'\n\n" - "*HASH = hash(alias_display_name + alias_domain)" - ), + @api.depends("alias_name") + def _compute_alias_domain(self): + alias_with_domain = self.filtered( + lambda r: r.alias_name and "__at__" in r.alias_name + ) + for alias in alias_with_domain: + alias.alias_domain = alias.alias_name.split("__at__")[1] + alias_without_domain = self - alias_with_domain + if alias_without_domain: + super(Alias, alias_without_domain)._compute_alias_domain() + return None + + alias_entry = fields.Char( + help="This will be used to enter an email, complete with domain", ) - def _inverse_alias_domain(self): - pass - - @api.depends("alias_domain", "alias_display_name", "check_domain", "alias_name") - def _compute_alias_name_and_hash(self): - for rec in self: - if rec.check_domain and rec.alias_display_name and rec.alias_domain: - alias_hash = generate_hash(rec.alias_display_name + rec.alias_domain) - rec.alias_hash = alias_hash - rec.alias_name = alias_hash + "+" + rec.alias_display_name - elif rec.alias_name or rec.alias_display_name: - name = rec.alias_display_name or rec.alias_name - rec.alias_hash = False - rec.alias_name = rec.alias_display_name = rec._clean_and_make_unique( - name, alias_ids=rec.ids - ) - else: - rec.alias_hash = rec.alias_name = False + @api.model + def search(self, domain, **kwargs): + """If mail alias in context, return this as result.""" + matching_alias = self.env.context.get("matching_alias", False) + if matching_alias: + return matching_alias + return super().search(domain, **kwargs) + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + self._patch_alias_vals(vals) + records = super().create(vals_list) + records._synchronize_alias_entry_with_name() + return records def write(self, vals): - name = vals.get("alias_name") or vals.get("alias_display_name") - if name and not vals.get("check_domain") and not self.check_domain: - vals["alias_name"] = vals[ - "alias_display_name" - ] = self._clean_and_make_unique(name, alias_ids=self.ids) - if vals.get("check_domain") is False: - vals["alias_domain"] = ( - self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain") + self._patch_alias_vals(vals) + result = super().write(vals) + self._synchronize_alias_entry_with_name() + return result + + def _synchronize_alias_entry_with_name(self): + """In case alias created/written without alias_entry, complete entry field.""" + for this in self: + if not this.alias_name: + alias_entry = False + elif "__at__" in this.alias_name: + alias_entry = this.alias_name.replace("__at__", "@") + else: + alias_entry = this.alias_name + if this.alias_entry != alias_entry: + super(Alias, this).write({"alias_entry": alias_entry}) + return None + + @api.model + def _patch_alias_vals(self, vals): + """If vals contains alias_entry, add corresponding alias_name.""" + alias_entry = vals.get("alias_entry", False) + if alias_entry: + default_domain = self._get_default_domain() + if "@" not in alias_entry: + alias_name = alias_entry + elif default_domain and default_domain in alias_entry: + alias_name = alias_entry.split("@")[0] + else: + alias_name = alias_entry.replace("@", "__at__") + vals["alias_name"] = alias_name + + @api.model + def _get_default_domain(self): + """get default domain.""" + ICP = self.env["ir.config_parameter"].sudo() + return ICP.get_param("mail.catchall.domain") + + @api.model + def get_clean_email(self, email): + """Users tend to pollute emails with extra info. get just the email.""" + # In Odoo 17.0 there is a new method parse_contact_from_email in + # odoo/tools/mail.py that we could use for this purpose. + if email: + # 1. Replace special characters with spaces. + cleaned = ( + email.replace('"', " ") + .replace("<", " ") + .replace(">", " ") + .replace(",", " ") ) - return super().write(vals) + # 2. Split on whitespace + parts = cleaned.split() + # 3. Find the part with an '@' if any and assume it is the real email. + for part in parts: + if "@" in part: + return part.lower() + return False # Else module partner_email_check would raise ValidationError. diff --git a/mail_alias_with_domain/models/mail_thread.py b/mail_alias_with_domain/models/mail_thread.py index 9d17795784..1954b577ba 100644 --- a/mail_alias_with_domain/models/mail_thread.py +++ b/mail_alias_with_domain/models/mail_thread.py @@ -1,13 +1,8 @@ # Copyright 2023 Solvti sp. z o.o. (https://solvti.pl) - -import logging - +# Copyright 2025 Therp BV (https://therp.nl) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import api, models, tools -from .mail_alias import generate_hash - -_logger = logging.getLogger(__name__) - class MailThread(models.AbstractModel): _inherit = "mail.thread" @@ -16,29 +11,39 @@ class MailThread(models.AbstractModel): def message_route( self, message, message_dict, model=None, thread_id=None, custom_values=None ): - """Prepare message_dict by extending recipients - with found aliases mails base on alias and domain - """ - try: - maching_aliases = self._find_alias_with_domain(message_dict) - if maching_aliases: - recipients = ( - f"{message_dict['recipients']}," - f"{','.join(maching_aliases.mapped('display_name'))}" + """Check for a recipient that can be linked to a full domain alias.""" + if not self.env.context.get("matching_alias", False): + matching_alias = self._find_alias_with_domain(message_dict) + if matching_alias: + # Call super with extra context. + return ( + super() + .with_context(matching_alias=matching_alias) + .message_route( + message, + message_dict, + model=model, + thread_id=thread_id, + custom_values=custom_values, + ) ) - message_dict["recipients"] = recipients - except Exception as e: - _logger.error(f"Unexpected error during processing alias with domain: {e}") return super().message_route( - message, message_dict, model, thread_id, custom_values + message, + message_dict, + model=model, + thread_id=thread_id, + custom_values=custom_values, ) def _find_alias_with_domain(self, message_dict): + """Find all aliasses that match.""" + Alias = self.env["mail.alias"] emails = {email for email in (tools.email_split(message_dict["recipients"]))} - hash_list = list( - map(generate_hash, [email.replace("@", "") for email in emails]) - ) - match = self.env["mail.alias"].search( - [("check_domain", "=", True), ("alias_hash", "in", hash_list)] - ) - return match + alias_names = [] + for email in emails: + clean_email = Alias.get_clean_email(email) + if not clean_email: + continue + alias_name = clean_email.replace("@", "__at__") + alias_names.append(alias_name) + return Alias.search([("alias_name", "in", alias_names)]) diff --git a/mail_alias_with_domain/post_init_hook.py b/mail_alias_with_domain/post_init_hook.py new file mode 100644 index 0000000000..dbd35d0b6a --- /dev/null +++ b/mail_alias_with_domain/post_init_hook.py @@ -0,0 +1,10 @@ +# Copyright 2025 Therp BV (https://therp.nl) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + + +def init_alias_entry(cr, registry): + cr.execute( + "UPDATE mail_alias" + " SET alias_entry = alias_name" + " WHERE alias_entry IS NULL AND NOT alias_name IS NULL" + ) diff --git a/mail_alias_with_domain/readme/CONTRIBUTORS.rst b/mail_alias_with_domain/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..167a32718c --- /dev/null +++ b/mail_alias_with_domain/readme/CONTRIBUTORS.rst @@ -0,0 +1,7 @@ +* `Solvti sp. z o.o. `_: + + * Jakub Wiselka + +* `Therp `_: + + * Ronald Portier (ronald@therp.nl) diff --git a/mail_alias_with_domain/readme/DESCRIPTION.rst b/mail_alias_with_domain/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..cc5a4e2ede --- /dev/null +++ b/mail_alias_with_domain/readme/DESCRIPTION.rst @@ -0,0 +1,41 @@ +This module adds possibility to process aliases together with domain. + +For example, suppose we have 3 companies in odoo. +Each company wants to have an alias where customers can send the bills. +invoice@company1.com +invoice@company2.com +invoice@company3.com + +In odoo, aliases are unique, and this module extends this functionality in +such a way that you can have many of the same aliases but with different domains. + +Note that when an incoming mail can be linked to an alias with a domain, +this will be the only alias used. However when an incoming mail can be +linked to multiple aliasses that have a domain, it is possible to have +multiple used. + +FOR DEVELOPERS + +In the default alias system, only the local part of an email address (the part +before the @) is used to link an incoming email to an alias. This happens in the +message_route method of the mail.thread model. + +Aliasses in standard Odoo store the alias_name field without domain. + +To still be able to use a domain name, we need a trick. What we will do is: + +* Replace the alias_name in the user interface with an alias_entry field, where a + complete email address can be entered. + +* If an alias is entered as a complete email address, this will be stored in the + alias_name as __at__. For instance alex__at__example.com. + alias_name is therefore changed from a writable field to a stored computed field. + +* The computation of alias_domain will be enhanced to take full email addresses into + account. + +* If an incoming mail can be linked to a full email address alias, we will write a + context key pointing to this alias. The search method of mail.alias will be overriden + to check for this key, and then not search at all, but just return the alias + requested. + diff --git a/mail_alias_with_domain/readme/USAGE.rst b/mail_alias_with_domain/readme/USAGE.rst new file mode 100644 index 0000000000..93d8c7603e --- /dev/null +++ b/mail_alias_with_domain/readme/USAGE.rst @@ -0,0 +1,4 @@ +To use this module, you need to: + +Got to the mail aliasses and check which aliasses you want to link to a specific +domain. diff --git a/mail_alias_with_domain/static/description/index.html b/mail_alias_with_domain/static/description/index.html new file mode 100644 index 0000000000..62251c8583 --- /dev/null +++ b/mail_alias_with_domain/static/description/index.html @@ -0,0 +1,466 @@ + + + + + + +Mail Alias With Domain + + + +
+

Mail Alias With Domain

+ + +

Beta License: AGPL-3 OCA/social Translate me on Weblate Try me on Runboat

+

This module adds possibility to process aliases together with domain.

+

For example, suppose we have 3 companies in odoo. +Each company wants to have an alias where customers can send the bills. +invoice@company1.com +invoice@company2.com +invoice@company3.com

+

In odoo, aliases are unique, and this module extends this functionality in +such a way that you can have many of the same aliases but with different domains.

+

Note that when an incoming mail can be linked to an alias with a domain, +this will be the only alias used. However when an incoming mail can be +linked to multiple aliasses that have a domain, it is possible to have +multiple used.

+

FOR DEVELOPERS

+

In the default alias system, only the local part of an email address (the part +before the @) is used to link an incoming email to an alias. This happens in the +message_route method of the mail.thread model.

+

Aliasses in standard Odoo store the alias_name field without domain.

+

To still be able to use a domain name, we need a trick. What we will do is:

+
    +
  • Replace the alias_name in the user interface with an alias_entry field, where a +complete email address can be entered.
  • +
  • If an alias is entered as a complete email address, this will be stored in the +alias_name as <localpart>__at__<domain>. For instance alex__at__example.com. +alias_name is therefore changed from a writable field to a stored computed field.
  • +
  • The computation of alias_domain will be enhanced to take full email addresses into +account.
  • +
  • If an incoming mail can be linked to a full email address alias, we will write a +context key pointing to this alias. The search method of mail.alias will be overriden +to check for this key, and then not search at all, but just return the alias +requested.
  • +
+

Table of contents

+ +
+

Usage

+

To use this module, you need to:

+

Got to the mail aliasses and check which aliasses you want to link to a specific +domain.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Solvti
  • +
  • Therp BV
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/social project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/mail_alias_with_domain/tests/__init__.py b/mail_alias_with_domain/tests/__init__.py index 8e07c10614..61a091079d 100644 --- a/mail_alias_with_domain/tests/__init__.py +++ b/mail_alias_with_domain/tests/__init__.py @@ -1,3 +1,3 @@ -# Copyright 2023 Solvti sp. z o.o. (https://solvti.pl) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from . import test_mail_thread diff --git a/mail_alias_with_domain/tests/test_mail_thread.py b/mail_alias_with_domain/tests/test_mail_thread.py index 17ae5e40fd..a1f5c90b16 100644 --- a/mail_alias_with_domain/tests/test_mail_thread.py +++ b/mail_alias_with_domain/tests/test_mail_thread.py @@ -1,4 +1,6 @@ # Copyright 2023 Solvti sp. z o.o. (https://solvti.pl) +# Copyright 2025 Therp BV (https://therp.nl) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from email.message import EmailMessage @@ -6,37 +8,38 @@ class TestMailThread(TransactionCase): - def setUp(self): - super(TestMailThread, self).setUp() + @classmethod + def setUpClass(cls): + super().setUpClass() - self.contact_model = self.env["ir.model"].search( - [("model", "=", "res.partner")] - ) - self.mail_alias_with_domain = self.env["mail.alias"].create( + ICP = cls.env["ir.config_parameter"].sudo() + ICP.set_param("mail.catchall.domain", "fsf.org") + + cls.contact_model = cls.env["ir.model"].search([("model", "=", "res.partner")]) + cls.Alias = cls.env["mail.alias"] + cls.mail_alias_with_domain = cls.Alias.create( { - "check_domain": True, - "alias_display_name": "test_alias_hash", - "alias_domain": "example.com", - "alias_model_id": self.contact_model.id, - "alias_defaults": "{'name': 'Test Alias Hash'}", + "alias_entry": "test_alias_entry@example.com", + "alias_model_id": cls.contact_model.id, + "alias_defaults": "{'name': 'Test Alias With Domain'}", } ) - self.mail_alias = self.env["mail.alias"].create( + cls.mail_alias_no_domain = cls.Alias.create( { - "alias_name": "test_alias", - "alias_model_id": self.contact_model.id, - "alias_defaults": "{'name': 'Test Alias'}", + "alias_entry": "test_alias", + "alias_model_id": cls.contact_model.id, + "alias_defaults": "{'name': 'Test Alias No Domain'}", } ) - message = EmailMessage() message.add_header("Subject", "New Alias Test") message.add_header("From", "test.user@example.com") + message.add_header("To", "info@fsf.org") message.set_default_type("text/plain") message.set_content("Please Create New Contact!") - self.message = message + cls.message = message - self.message_dict = { + cls.message_dict = { "message_type": "email", "message_id": "", "subject": "New Contact", @@ -55,17 +58,65 @@ def setUp(self): "bounced_message": "", } - def test_create_alias_by_alias_name_and_alias_display_name(self): - self.assertTrue(self.mail_alias_with_domain.alias_name) - self.assertTrue(self.mail_alias.alias_display_name) + def test_patch_alias_vals(self): + # Non default domain in alias_entry. + vals = {"alias_entry": "test_patch@example.com"} + self.Alias._patch_alias_vals(vals) + self.assertEqual(vals["alias_name"], "test_patch__at__example.com") + # Default domain in alias_entry. + vals = {"alias_entry": "test_patch@fsf.org"} + self.Alias._patch_alias_vals(vals) + self.assertEqual(vals["alias_name"], "test_patch") + # No domain in alias_entry. + vals = {"alias_entry": "test_patch"} + self.Alias._patch_alias_vals(vals) + self.assertEqual(vals["alias_name"], "test_patch") + + def test_create_alias_by_alias_entry(self): + self.assertEqual( + self.mail_alias_with_domain.alias_name, "test_alias_entry__at__example.com" + ) + self.assertEqual(self.mail_alias_with_domain.alias_domain, "example.com") + self.assertEqual(self.mail_alias_no_domain.alias_name, "test_alias") + self.assertEqual(self.mail_alias_no_domain.alias_domain, "fsf.org") + + def test_create_alias_by_alias_name(self): + alias_with_domain = self.Alias.create( + { + "alias_name": "test_alias_name__at__example.com", + "alias_model_id": self.contact_model.id, + "alias_defaults": "{'name': 'Test Alias Name'}", + } + ) + self.assertEqual(alias_with_domain.alias_entry, "test_alias_name@example.com") + self.assertEqual(alias_with_domain.alias_domain, "example.com") + alias_no_domain = self.Alias.create( + { + "alias_name": "test_alias_no_domain", + "alias_model_id": self.contact_model.id, + "alias_defaults": "{'name': 'Test Alias'}", + } + ) + self.assertEqual(alias_no_domain.alias_entry, "test_alias_no_domain") + self.assertEqual(alias_no_domain.alias_domain, "fsf.org") - def test_generate_hash(self): - alias = self.mail_alias_with_domain - self.assertEqual(alias.alias_hash, alias.alias_name.split("+")[0]) + def test_find_alias_with_domain(self): + email_to = "test_alias_entry@example.com" + self.message_dict.update( + { + "recipients": f'"{email_to}" <{email_to}>', + "to": f""" + "{email_to}" <{email_to}>, "someone@test-fake.com" + """, + } + ) + Thread = self.env["mail.thread"] + matching_alias = Thread._find_alias_with_domain(self.message_dict) + self.assertEqual(matching_alias, self.mail_alias_with_domain) - def test_message_route_include_hash_alias(self): - email_to = "test_alias_hash@example.com" - self.message.add_header("To", email_to) + def test_message_route_include_domain_alias(self): + email_to = "test_alias_entry@example.com" + self.message.replace_header("To", email_to) self.message_dict.update( { "recipients": f'"{email_to}" <{email_to}>', @@ -81,15 +132,12 @@ def test_message_route_include_hash_alias(self): thread_id=None, custom_values=None, ) - self.assertTrue( - any( - self.mail_alias_with_domain == alias for alias in [r[4] for r in routes] - ) - ) + self.assertEqual(len(routes), 1) # Will only use route with domain. + self.assertEqual(routes[0][4], self.mail_alias_with_domain) - def test_message_route_differend_domain_for_hash_alias(self): - email_to = "test_alias_hash@something_else.com" - self.message.add_header("To", email_to) + def test_message_route_different_domain_alias(self): + email_to = "test_alias_entry@something_else.com" + self.message.replace_header("To", email_to) self.message_dict.update( { "recipients": f'"{email_to}" <{email_to}>', @@ -106,16 +154,14 @@ def test_message_route_differend_domain_for_hash_alias(self): thread_id=None, custom_values=None, ) - self.assertFalse( - any( - self.mail_alias_with_domain == alias for alias in [r[4] for r in routes] - ) - ) + self.assertEqual(len(routes), 1) # Should be default route + self.assertEqual(routes[0][0], self.contact_model.model) + self.assertEqual(routes[0][4], None) def test_message_route_two_types_of_aliases_at_once(self): - email_to_1 = "test_alias_hash@example.com" + email_to_1 = "test_alias_entry@example.com" email_to_2 = "test_alias@example_mail.com" - self.message.add_header("To", f"{email_to_1}, {email_to_2}") + self.message.replace_header("To", f"{email_to_1}, {email_to_2}") self.message_dict.update( { "recipients": f'"{email_to_1}" <{email_to_1}>, "{email_to_2}" <{email_to_2}>', @@ -132,6 +178,24 @@ def test_message_route_two_types_of_aliases_at_once(self): thread_id=None, custom_values=None, ) - self.assertTrue( - {r[4] for r in routes} == {self.mail_alias_with_domain, self.mail_alias} + self.assertEqual(len(routes), 1) # Will only use route with domain. + self.assertEqual(routes[0][4], self.mail_alias_with_domain) + + def test_message_route_no_domain_alias(self): + email_to = "test_alias@example_mail.com" + self.message.replace_header("To", f"{email_to}") + self.message_dict.update( + { + "recipients": f'"{email_to}" <{email_to}>', + "to": f'"{email_to}" <{email_to}>, "abc@abc.com" ', + } + ) + routes = self.env["mail.thread"].message_route( + self.message, + self.message_dict, + model=self.contact_model.model, + thread_id=None, + custom_values=None, ) + self.assertEqual(len(routes), 1) # Will only use route without domain. + self.assertEqual(routes[0][4], self.mail_alias_no_domain) diff --git a/mail_alias_with_domain/views/mail_alias_views.xml b/mail_alias_with_domain/views/mail_alias_views.xml index 144dcab22c..9034460038 100644 --- a/mail_alias_with_domain/views/mail_alias_views.xml +++ b/mail_alias_with_domain/views/mail_alias_views.xml @@ -1,50 +1,31 @@ + - - - mail.alias.form.inherit - mail.alias - - - - - - - - 1 - - - - - - - - - - - {'readonly': [('check_domain','=',False)], 'required': [('check_domain','=',True)]} - - + + mail.alias.form - mail_alias_with_domain + mail.alias + + + + 1 - - - mail.alias.tree.inherit - mail.alias - - - - - + + - - + + + + mail.alias.tree.inherit - mail_alias_with_domain + mail.alias + + + + 1 + + + + + + From afcc799527fcb5076f2eb6749b42054f6fb74443 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Wed, 21 Jan 2026 12:17:22 +0100 Subject: [PATCH 4/7] [FIX] mail_alias_with_domain: review remarks --- mail_alias_with_domain/__manifest__.py | 2 +- mail_alias_with_domain/models/mail_alias.py | 6 +++++- mail_alias_with_domain/models/mail_thread.py | 15 ++++++--------- .../static/description/index.html | 1 - 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/mail_alias_with_domain/__manifest__.py b/mail_alias_with_domain/__manifest__.py index 715bc8b151..d172cd1d7d 100644 --- a/mail_alias_with_domain/__manifest__.py +++ b/mail_alias_with_domain/__manifest__.py @@ -1,5 +1,5 @@ # Copyright 2023 Solvti sp. z o.o. (https://solvti.pl) -# Copyright 2025 Therp BV (https://therp.nl) +# Copyright 2025-2026 Therp BV (https://therp.nl) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). { diff --git a/mail_alias_with_domain/models/mail_alias.py b/mail_alias_with_domain/models/mail_alias.py index 63b48cccb2..d1ca65237c 100644 --- a/mail_alias_with_domain/models/mail_alias.py +++ b/mail_alias_with_domain/models/mail_alias.py @@ -1,5 +1,5 @@ # Copyright 2023 Solvti sp. z o.o. (https://solvti.pl). -# Copyright 2025 Therp BV (https://therp.nl). +# Copyright 2025-2026 Therp BV (https://therp.nl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import api, fields, models @@ -24,6 +24,10 @@ def _compute_alias_domain(self): help="This will be used to enter an email, complete with domain", ) + _sql_constraints = [ + ("unique_alias_entry", "UNIQUE(alias_entry)", "Alias entry must be unique!") + ] + @api.model def search(self, domain, **kwargs): """If mail alias in context, return this as result.""" diff --git a/mail_alias_with_domain/models/mail_thread.py b/mail_alias_with_domain/models/mail_thread.py index 1954b577ba..8e2f2e68b0 100644 --- a/mail_alias_with_domain/models/mail_thread.py +++ b/mail_alias_with_domain/models/mail_thread.py @@ -1,5 +1,5 @@ # Copyright 2023 Solvti sp. z o.o. (https://solvti.pl) -# Copyright 2025 Therp BV (https://therp.nl) +# Copyright 2025-2026 Therp BV (https://therp.nl) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import api, models, tools @@ -38,12 +38,9 @@ def message_route( def _find_alias_with_domain(self, message_dict): """Find all aliasses that match.""" Alias = self.env["mail.alias"] - emails = {email for email in (tools.email_split(message_dict["recipients"]))} - alias_names = [] - for email in emails: - clean_email = Alias.get_clean_email(email) - if not clean_email: - continue - alias_name = clean_email.replace("@", "__at__") - alias_names.append(alias_name) + alias_names = [ + email.replace("@", "__at__") + # tools.email_split only returns clean email addresses (no names). + for email in tools.email_split(message_dict["recipients"]) + ] return Alias.search([("alias_name", "in", alias_names)]) diff --git a/mail_alias_with_domain/static/description/index.html b/mail_alias_with_domain/static/description/index.html index 62251c8583..678f500545 100644 --- a/mail_alias_with_domain/static/description/index.html +++ b/mail_alias_with_domain/static/description/index.html @@ -1,4 +1,3 @@ - From a439aba1d4d536a5f7bb61dd23bb6761c6cda656 Mon Sep 17 00:00:00 2001 From: Nikos Tsirintanis Date: Thu, 22 Jan 2026 14:59:39 +0100 Subject: [PATCH 5/7] [IMP] mail_alias_with_domain: pre-commit stuff --- mail_alias_with_domain/README.rst | 10 +++++----- .../static/description/index.html | 17 ++++++++++------- .../odoo/addons/mail_alias_with_domain | 1 + setup/mail_alias_with_domain/setup.py | 6 ++++++ 4 files changed, 22 insertions(+), 12 deletions(-) create mode 120000 setup/mail_alias_with_domain/odoo/addons/mail_alias_with_domain create mode 100644 setup/mail_alias_with_domain/setup.py diff --git a/mail_alias_with_domain/README.rst b/mail_alias_with_domain/README.rst index d8b60734e9..5e31e26abc 100644 --- a/mail_alias_with_domain/README.rst +++ b/mail_alias_with_domain/README.rst @@ -17,13 +17,13 @@ Mail Alias With Domain :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsocial-lightgray.png?logo=github - :target: https://github.com/OCA/social/tree/16.0/mail_alias_with_domain + :target: https://github.com/OCA/social/tree/14.0/mail_alias_with_domain :alt: OCA/social .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/social-16-0/social-16-0-mail_alias_with_domain + :target: https://translation.odoo-community.org/projects/social-14-0/social-14-0-mail_alias_with_domain :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png - :target: https://runboat.odoo-community.org/builds?repo=OCA/social&target_branch=16.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/social&target_branch=14.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -89,7 +89,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -126,6 +126,6 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. -This module is part of the `OCA/social `_ project on GitHub. +This module is part of the `OCA/social `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/mail_alias_with_domain/static/description/index.html b/mail_alias_with_domain/static/description/index.html index 678f500545..373bec966c 100644 --- a/mail_alias_with_domain/static/description/index.html +++ b/mail_alias_with_domain/static/description/index.html @@ -8,10 +8,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -274,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -300,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { @@ -368,7 +369,7 @@

Mail Alias With Domain

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:3c7166c28331e6e457d77bd71f2af2420004c8b92ad4b8d745f80ad1e6884603 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Beta License: AGPL-3 OCA/social Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/social Translate me on Weblate Try me on Runboat

This module adds possibility to process aliases together with domain.

For example, suppose we have 3 companies in odoo. Each company wants to have an alias where customers can send the bills. @@ -424,7 +425,7 @@

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -feedback.

+feedback.

Do not contact contributors directly about support or help with technical issues.

@@ -452,11 +453,13 @@

Contributors

Maintainers

This module is maintained by the OCA.

-Odoo Community Association + +Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

-

This module is part of the OCA/social project on GitHub.

+

This module is part of the OCA/social project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

diff --git a/setup/mail_alias_with_domain/odoo/addons/mail_alias_with_domain b/setup/mail_alias_with_domain/odoo/addons/mail_alias_with_domain new file mode 120000 index 0000000000..24eaa858ad --- /dev/null +++ b/setup/mail_alias_with_domain/odoo/addons/mail_alias_with_domain @@ -0,0 +1 @@ +../../../../mail_alias_with_domain \ No newline at end of file diff --git a/setup/mail_alias_with_domain/setup.py b/setup/mail_alias_with_domain/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/mail_alias_with_domain/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 978c7b26dd75008da104eeffccb72a3ab72ee295 Mon Sep 17 00:00:00 2001 From: Nikos Tsirintanis Date: Thu, 22 Jan 2026 15:23:49 +0100 Subject: [PATCH 6/7] [14.0][MIG] mail_alias_with_domain: Backport to 14.0 [FIX] mail_alias_with_domain: review remarks --- mail_alias_with_domain/__manifest__.py | 2 +- mail_alias_with_domain/tests/test_mail_thread.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/mail_alias_with_domain/__manifest__.py b/mail_alias_with_domain/__manifest__.py index d172cd1d7d..28d0321546 100644 --- a/mail_alias_with_domain/__manifest__.py +++ b/mail_alias_with_domain/__manifest__.py @@ -7,7 +7,7 @@ "summary": "Allow simple mail alias to be combined with a mail domain", "author": "Solvti, Therp BV, Odoo Community Association (OCA)", "website": "https://github.com/OCA/social", - "version": "16.0.1.0.0", + "version": "14.0.1.0.0", "license": "AGPL-3", "application": False, "installable": True, diff --git a/mail_alias_with_domain/tests/test_mail_thread.py b/mail_alias_with_domain/tests/test_mail_thread.py index a1f5c90b16..a4d964851a 100644 --- a/mail_alias_with_domain/tests/test_mail_thread.py +++ b/mail_alias_with_domain/tests/test_mail_thread.py @@ -1,13 +1,14 @@ +# flake8: noqa: E222,E231,E711 # Copyright 2023 Solvti sp. z o.o. (https://solvti.pl) # Copyright 2025 Therp BV (https://therp.nl) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from email.message import EmailMessage -from odoo.tests import TransactionCase +from odoo.tests.common import SavepointCase -class TestMailThread(TransactionCase): +class TestMailThread(SavepointCase): @classmethod def setUpClass(cls): super().setUpClass() @@ -164,7 +165,9 @@ def test_message_route_two_types_of_aliases_at_once(self): self.message.replace_header("To", f"{email_to_1}, {email_to_2}") self.message_dict.update( { - "recipients": f'"{email_to_1}" <{email_to_1}>, "{email_to_2}" <{email_to_2}>', + "recipients": ( + f'"{email_to_1}" <{email_to_1}>, "{email_to_2}" <{email_to_2}>' + ), "to": ( f'"{email_to_1}" <{email_to_1}>, "{email_to_2}" <{email_to_2}>,' '"abc@abc.com" ' From 11d76149fd144944ae25661f33032c6e0e5cc3df Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Wed, 21 Jan 2026 12:17:22 +0100 Subject: [PATCH 7/7] [FIX] mail_alias_with_domain: improvements - Add test for write on mail.alias; - No unneeded call to clean already clean email; - Add constraint to mail.alias. --- .../tests/test_mail_thread.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/mail_alias_with_domain/tests/test_mail_thread.py b/mail_alias_with_domain/tests/test_mail_thread.py index a4d964851a..91030410cd 100644 --- a/mail_alias_with_domain/tests/test_mail_thread.py +++ b/mail_alias_with_domain/tests/test_mail_thread.py @@ -81,6 +81,27 @@ def test_create_alias_by_alias_entry(self): self.assertEqual(self.mail_alias_no_domain.alias_name, "test_alias") self.assertEqual(self.mail_alias_no_domain.alias_domain, "fsf.org") + def test_write_alias_by_alias_entry(self): + self.assertEqual( + self.mail_alias_with_domain.alias_name, "test_alias_entry__at__example.com" + ) + self.mail_alias_with_domain.write({"alias_entry": "test_new_name@example.org"}) + self.assertEqual( + self.mail_alias_with_domain.alias_name, "test_new_name__at__example.org" + ) + # New domain should have been computed as well. + self.assertEqual(self.mail_alias_with_domain.alias_domain, "example.org") + + def test_clear_alias_name(self): + # Clearing the alias_name should also clear the alias_entry. + self.assertEqual( + self.mail_alias_with_domain.alias_name, "test_alias_entry__at__example.com" + ) + self.mail_alias_with_domain.write({"alias_name": False}) + self.assertEqual(self.mail_alias_with_domain.alias_entry, False) + # Domain should have been reset to default. + self.assertEqual(self.mail_alias_with_domain.alias_domain, "fsf.org") + def test_create_alias_by_alias_name(self): alias_with_domain = self.Alias.create( {