diff --git a/fusion_helpdesk/__manifest__.py b/fusion_helpdesk/__manifest__.py index 9f24d860..59190928 100644 --- a/fusion_helpdesk/__manifest__.py +++ b/fusion_helpdesk/__manifest__.py @@ -3,7 +3,7 @@ # License OPL-1 (Odoo Proprietary License v1.0) { 'name': 'Fusion Helpdesk Reporter', - 'version': '19.0.1.4.0', + 'version': '19.0.1.4.1', 'category': 'Productivity', 'summary': 'One-click in-app bug reporting & feature requesting — ' 'auto-creates a helpdesk.ticket on a central Odoo Helpdesk.', diff --git a/fusion_helpdesk/controllers/main.py b/fusion_helpdesk/controllers/main.py index 58612b59..4e92c90a 100644 --- a/fusion_helpdesk/controllers/main.py +++ b/fusion_helpdesk/controllers/main.py @@ -22,7 +22,6 @@ from urllib.parse import urljoin, urlparse from odoo import _, http from odoo.exceptions import UserError from odoo.http import request -from odoo.tools import email_normalize from odoo.addons.fusion_helpdesk.utils import ( build_ticket_vals, @@ -30,6 +29,7 @@ from odoo.addons.fusion_helpdesk.utils import ( is_public_message, compute_unread_count, escape_like, + _norm_email, ) _logger = logging.getLogger(__name__) diff --git a/fusion_helpdesk/tests/test_utils.py b/fusion_helpdesk/tests/test_utils.py index 30fa6d3b..3943b68e 100644 --- a/fusion_helpdesk/tests/test_utils.py +++ b/fusion_helpdesk/tests/test_utils.py @@ -14,6 +14,7 @@ from odoo.addons.fusion_helpdesk.utils import ( build_scope_domain, is_public_message, compute_unread_count, + _norm_email, ) @@ -103,3 +104,28 @@ class TestMessageFilterAndUnread(TransactionCase): def test_unread_count_unseen_ticket_counts(self): tickets = [{'id': 9, 'last_support_msg_id': 4}] self.assertEqual(compute_unread_count(tickets, {}), 1) + + +@tagged('post_install', '-at_install', 'fusion_helpdesk') +class TestNormEmail(TransactionCase): + + def test_valid_email_is_normalised_lowercase(self): + self.assertEqual(_norm_email('John@Entech.COM'), 'john@entech.com') + + def test_first_valid_candidate_wins(self): + # confirmed reply email empty -> fall back to the next valid one + self.assertEqual(_norm_email('', 'not an email', 'jane@x.com'), 'jane@x.com') + + def test_wildcard_is_rejected(self): + # IDOR guard: a self-set '%' must not survive as a scope key + self.assertEqual(_norm_email('%'), '') + + def test_non_email_login_falls_through_to_empty(self): + self.assertEqual(_norm_email('admin', 'also-not-email', ''), '') + + def test_controller_namespace_resolves_norm_email(self): + # Regression: _norm_email was called in controllers/main.py + # (submit + _identity) but never imported/defined -> NameError on + # every inbox endpoint. Guard that the name is resolvable there. + from odoo.addons.fusion_helpdesk.controllers import main + self.assertTrue(hasattr(main, '_norm_email')) diff --git a/fusion_helpdesk/utils.py b/fusion_helpdesk/utils.py index 28b81310..1c0bcc02 100644 --- a/fusion_helpdesk/utils.py +++ b/fusion_helpdesk/utils.py @@ -8,6 +8,7 @@ here is unit-testable in isolation, which is what lets us validate the identity keystone, the server-side scoping boundary, the public-message filter and the unread maths without a live central Odoo to talk to. """ +from odoo.tools import email_normalize # Sentinel used so a missing label/email can never widen a domain to # "match everything". An empty string in `=`/`=ilike` would match rows @@ -29,6 +30,24 @@ def escape_like(value): return (value or '').replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_') +def _norm_email(*candidates): + """Return the first candidate that normalises to a valid email, else ''. + + Used to derive the inbox scope key from a chain of fallbacks (the + confirmed reply email -> ``user.email`` -> ``user.login``). + ``email_normalize`` lowercases the address and returns a falsy value for + anything that is not exactly one valid email — including a self-set + wildcard like ``%`` — so the value fed into ``build_scope_domain`` can + never widen the scope. Pairs with :func:`escape_like` as defense in depth + against the ``partner_email =ilike`` IDOR. + """ + for candidate in candidates: + normalized = email_normalize(candidate or '') + if normalized: + return normalized + return '' + + def build_ticket_vals(kind, subject, body_html, team_id, client_label, reporter_name, reporter_email, company_name): """Construct the `helpdesk.ticket` create vals for a forwarded report.