From ae9652f5e29b6f244eda6510ff08f9b52048d5f4 Mon Sep 17 00:00:00 2001 From: Sander Lienaerts Date: Wed, 22 Apr 2026 15:09:27 +0200 Subject: [PATCH] [FIX] mail_notification_custom_subject: read templates with sudo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mail.thread override searches mail.message.custom.subject with sudo and then explicitly drops back to the caller's env with .sudo(False). When iterating the resulting recordset, field reads trigger ACL checks against the caller. Portal and public users are allowed to trigger message_post through portal controllers (which typically call record.sudo().message_post), but they lack read access to mail.message.custom.subject — an internal-only ACL. The resulting AccessError is silently caught by the per-template try/except, but the custom subject is never applied for those users, and the raw AccessError can leak when field access is triggered outside the try block (e.g. via prefetch on related fields). Templates are admin-managed configuration records, akin to mail.template. They should be read with sudo, while rendering still honors the caller's permissions on the business record via res_ids. Add a test that reproduces the portal-user path: runs as a portal user (asserts base.group_portal / not base.group_user), posts via partner.sudo().message_post(..., author_id=portal_user.partner_id) — the exact pattern used by portal controllers. Without the fix the subject falls back to the default "Re: "; with the fix it is rendered from the template. --- mail_notification_custom_subject/README.rst | 4 +++ .../__manifest__.py | 2 +- .../models/mail_thread.py | 9 +++--- .../readme/CONTRIBUTORS.md | 3 ++ .../test_mail_notification_custom_subject.py | 30 +++++++++++++++++++ 5 files changed, 43 insertions(+), 5 deletions(-) diff --git a/mail_notification_custom_subject/README.rst b/mail_notification_custom_subject/README.rst index b237a0672..1a8d9ac51 100644 --- a/mail_notification_custom_subject/README.rst +++ b/mail_notification_custom_subject/README.rst @@ -101,6 +101,10 @@ Contributors - Eduardo de Miguel +- Codeforward + + - Sander Lienaerts + Maintainers ----------- diff --git a/mail_notification_custom_subject/__manifest__.py b/mail_notification_custom_subject/__manifest__.py index 71686ac7b..9f8a0d505 100644 --- a/mail_notification_custom_subject/__manifest__.py +++ b/mail_notification_custom_subject/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Mail Notification Custom Subject", "summary": "Apply a custom subject to mail notifications", - "version": "18.0.1.0.0", + "version": "18.0.1.0.1", "category": "Social Network", "website": "https://github.com/OCA/mail", "author": "Tecnativa, Odoo Community Association (OCA)", diff --git a/mail_notification_custom_subject/models/mail_thread.py b/mail_notification_custom_subject/models/mail_thread.py index c74be0d0a..b88874d58 100644 --- a/mail_notification_custom_subject/models/mail_thread.py +++ b/mail_notification_custom_subject/models/mail_thread.py @@ -46,11 +46,12 @@ def message_post( if subject: # If it already had a defined subject, we must respect it domain += [("position", "!=", "replace")] + # Read templates with sudo: they are admin-managed configuration + # records, akin to mail.template. Portal and public users may + # trigger message_post (e.g. via portal controllers) and must be + # able to iterate the templates without hitting ACL errors. custom_subjects = ( - self.env["mail.message.custom.subject"] - .sudo() - .search(domain) - .sudo(False) + self.env["mail.message.custom.subject"].sudo().search(domain) ) if not subject: record_name = ( diff --git a/mail_notification_custom_subject/readme/CONTRIBUTORS.md b/mail_notification_custom_subject/readme/CONTRIBUTORS.md index a8de95dee..5a26edcb0 100644 --- a/mail_notification_custom_subject/readme/CONTRIBUTORS.md +++ b/mail_notification_custom_subject/readme/CONTRIBUTORS.md @@ -10,3 +10,6 @@ - Moduon \<\> - Eduardo de Miguel + +- Codeforward \<\> + - Sander Lienaerts diff --git a/mail_notification_custom_subject/tests/test_mail_notification_custom_subject.py b/mail_notification_custom_subject/tests/test_mail_notification_custom_subject.py index 3ea48855a..446e0fde1 100644 --- a/mail_notification_custom_subject/tests/test_mail_notification_custom_subject.py +++ b/mail_notification_custom_subject/tests/test_mail_notification_custom_subject.py @@ -20,6 +20,7 @@ def setUpClass(cls): ) cls.admin = new_test_user(cls.env, "boss", "base.group_system") new_test_user(cls.env, "worker_custom_subject") + new_test_user(cls.env, "portal_custom_subject", "base.group_portal") cls.subject_model = cls.env["mail.message.custom.subject"].with_user(cls.admin) @users("worker_custom_subject") @@ -210,3 +211,32 @@ def test_no_template_default_result(self): # Get message and check subject # No exception should be raised but subject should remain as original. self.assertEqual(mail_message_1.subject, "Test partner 1") + + @users("portal_custom_subject") + def test_email_subject_template_portal_user(self): + """Portal users triggering message_post via sudo (e.g. from a portal + controller) must render custom subject templates without hitting ACL + errors on mail.message.custom.subject — templates are admin-managed + configuration records and should be read with sudo.""" + self.subject_model.create( + { + "name": "Test portal template", + "model_id": self.env.ref("base.model_res_partner").id, + "subtype_ids": [(6, 0, [self.env.ref("mail.mt_comment").id])], + "subject_template": "{{object.name or 'n/a'}} - portal", + } + ) + portal_user = self.env.user + self.assertTrue(portal_user.has_group("base.group_portal")) + self.assertFalse(portal_user.has_group("base.group_user")) + # Mimic the portal controller pattern: the caller is the portal user, + # the record is sudo'd for message_post, and author_id is set to the + # portal user's partner. + partner = self.env["res.partner"].browse(self.partner_1.id) + mail_message = partner.sudo().message_post( + body="Test from portal", + subtype_xmlid="mail.mt_comment", + author_id=portal_user.partner_id.id, + ) + self.assertEqual(mail_message.author_id, portal_user.partner_id) + self.assertEqual(mail_message.subject, "Test partner 1 - portal")