From ee2f9615c92e8010e71e497d3fe2883461d7c6d0 Mon Sep 17 00:00:00 2001 From: Fernando Date: Sat, 16 May 2026 16:03:09 -0400 Subject: [PATCH] [FIX] fetchmail_attach_from_folder: case-insensitive email matching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Emails with uppercase letters were not matching partner records because the search used "=" which is case-sensitive in PostgreSQL. Changes: - Use Odoo's email_normalize() on incoming addresses for consistent, normalized matching (strips display names, lowercases). - Keep "=ilike" operator, but now search values are normalized so there are no wildcards — it functions as a case-insensitive "=". - Added test coverage for partner stored as uppercase and display-name headers ("Name "). Fixes OCA/server-tools#3402 --- .../match_algorithm/email_exact.py | 21 ++++++++++--- .../tests/test_match_algorithms.py | 30 ++++++++++++++++++- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/fetchmail_attach_from_folder/match_algorithm/email_exact.py b/fetchmail_attach_from_folder/match_algorithm/email_exact.py index 899289407da..da941fe664e 100644 --- a/fetchmail_attach_from_folder/match_algorithm/email_exact.py +++ b/fetchmail_attach_from_folder/match_algorithm/email_exact.py @@ -1,11 +1,11 @@ # Copyright - 2013-2024 Therp BV . # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo.tools.mail import email_split +from odoo.tools.mail import email_normalize, email_split from odoo.tools.safe_eval import safe_eval class EmailExact: - """Search for exactly the mailadress as noted in the email""" + """Search for exactly the email address as noted in the email.""" def _get_mailaddresses(self, folder, message_dict): mailaddresses = [] @@ -13,11 +13,24 @@ def _get_mailaddresses(self, folder, message_dict): for field in fields: if field in message_dict: mailaddresses += email_split(message_dict[field]) - return [addr.lower() for addr in mailaddresses] + # Normalize using email_normalize for consistent matching. + # This strips display names, lowercases the address, and handles + # edge cases (e.g. "" or "User "). + return [email_normalize(addr) or addr.lower() for addr in mailaddresses] def _get_mailaddress_search_domain( - self, folder, message_dict, operator="=", values=None + self, folder, message_dict, operator="=ilike", values=None ): + """Build search domain for email matching. + + We use ``=ilike`` (case-insensitive exact match) instead of ``=`` + so that uppercase email variants (e.g. ``Name.SURNAME@Domain.com``) + also match partners whose email is stored in mixed case. + + ``=ilike`` is safe here because there are no ``%`` wildcards in the + search values, so it behaves exactly like a case-insensitive ``=`` + (PostgreSQL: ``LOWER(field) = LOWER(value)``). + """ mailaddresses = values or self._get_mailaddresses(folder, message_dict) if not mailaddresses: return [(0, "=", 1)] diff --git a/fetchmail_attach_from_folder/tests/test_match_algorithms.py b/fetchmail_attach_from_folder/tests/test_match_algorithms.py index 02969a4f4fd..92885dbdd4b 100644 --- a/fetchmail_attach_from_folder/tests/test_match_algorithms.py +++ b/fetchmail_attach_from_folder/tests/test_match_algorithms.py @@ -8,7 +8,7 @@ from odoo.fields import Command from odoo.tests.common import TransactionCase -from ..match_algorithm import email_domain +from ..match_algorithm import email_domain, email_exact TEST_EMAIL = "reynaert@dutchsagas.nl" TEST_SUBJECT = "Test subject" @@ -144,6 +144,34 @@ def setUpClass(cls): } ) + def test_email_exact_case_insensitive(self): + """A message to REYN@dom.ain should match partner with reyn@dom.ain.""" + MAIL_MESSAGE["from"] = TEST_EMAIL.upper() + self.folder.match_algorithm = "email_exact" + matcher = email_exact.EmailExact() + matches = matcher.search_matches(self.folder, MAIL_MESSAGE) + self.assertEqual(matches, self.test_partner) + + def test_email_exact_case_insensitive_partner_uppercase(self): + """A message to reyn@dom.ain should match partner with REYN@dom.ain.""" + self.test_partner.email = TEST_EMAIL.upper() + MAIL_MESSAGE["from"] = TEST_EMAIL + self.folder.match_algorithm = "email_exact" + matcher = email_exact.EmailExact() + matches = matcher.search_matches(self.folder, MAIL_MESSAGE) + self.assertEqual(matches, self.test_partner) + # restore original partner email for subsequent tests + self.test_partner.email = TEST_EMAIL + + def test_email_exact_display_name(self): + """A message in the form 'Name ' should be parsed correctly.""" + TEST_DISPLAY_NAME = "Reynaert de Vos " + MAIL_MESSAGE["from"] = TEST_DISPLAY_NAME + self.folder.match_algorithm = "email_exact" + matcher = email_exact.EmailExact() + matches = matcher.search_matches(self.folder, MAIL_MESSAGE) + self.assertEqual(matches, self.test_partner) + def test_email_exact(self): """A message to ronald@acme.com should be linked to partner with that email.""" MAIL_MESSAGE["from"] = TEST_EMAIL