diff --git a/src/mailparser/utils.py b/src/mailparser/utils.py index be6e4d5..9b71315 100644 --- a/src/mailparser/utils.py +++ b/src/mailparser/utils.py @@ -25,6 +25,7 @@ import email.utils import functools import hashlib +import inspect import json import logging import os @@ -52,6 +53,30 @@ log = logging.getLogger(__name__) + +def _getaddresses(fieldvalues: list[str]) -> list[tuple[str, str]]: + """Call ``email.utils.getaddresses`` with strict parsing when available. + + The ``strict`` keyword was added to ``email.utils.getaddresses`` in + Python 3.13 (and backported only to later security patch releases of + 3.9-3.12, e.g. 3.11.10). mail-parser supports ``requires-python + >=3.9,<3.15``, so on an earlier patch release the keyword is absent and + passing it raises ``TypeError: getaddresses() got an unexpected keyword + argument 'strict'``. Feature-detect it before passing ``strict=True`` so + older interpreters keep working (parsedmarc #808). + """ + try: + supports_strict = ( + "strict" in inspect.signature(email.utils.getaddresses).parameters + ) + except (TypeError, ValueError): # pragma: no cover - defensive + supports_strict = False + + if supports_strict: + return email.utils.getaddresses(fieldvalues, strict=True) + return email.utils.getaddresses(fieldvalues) + + # --------------------------------------------------------------------------- # RFC 5322 address parsing — fallback for non-compliant display names # --------------------------------------------------------------------------- @@ -134,7 +159,7 @@ def get_addresses( elif not isinstance(raw_header, str): raw_header = str(raw_header) - parsed = email.utils.getaddresses([raw_header], strict=True) + parsed = _getaddresses([raw_header]) # If every result from the strict parser has an empty address — while the # raw header is non-empty — fall back to regex extraction so that the diff --git a/tests/test_utils.py b/tests/test_utils.py index 734900d..0aecb27 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -771,6 +771,43 @@ def test_get_addresses_fallback_regex_no_matches(self): result = get_addresses("not an email address at all") self.assertEqual(result, [("", "")]) + def test_get_addresses_without_strict_parameter(self): + """ + Regression for parsedmarc #808: get_addresses must not pass the + ``strict`` keyword unconditionally to email.utils.getaddresses. + + The ``strict`` parameter was added to ``email.utils.getaddresses`` in + Python 3.13 (and backported only to later security patch releases of + 3.9-3.12). mail-parser targets ``requires-python >=3.9,<3.15``, so on + an earlier patch release (e.g. CPython 3.11.3) the call raised:: + + TypeError: getaddresses() got an unexpected keyword argument 'strict' + + Here we simulate a pre-3.13 ``getaddresses`` (no ``strict`` parameter) + and assert that get_addresses parses the address instead of crashing. + """ + + import email.utils + + real_getaddresses = email.utils.getaddresses + + def legacy_getaddresses(fieldvalues): + """Mimic the pre-3.13 signature that lacks ``strict``. + + Patched in as a real function (not a Mock) so that the feature + detection in get_addresses observes a signature without + ``strict`` and does not attempt to pass the keyword. + """ + return real_getaddresses(fieldvalues) + + with patch( + "mailparser.utils.email.utils.getaddresses", + new=legacy_getaddresses, + ): + result = get_addresses("Plain Name ") + + self.assertEqual(result, [("Plain Name", "plain@example.com")]) + def test_parse_received_sendgrid_date(self): """parse_received extracts SendGrid non-standard date (utils.py:389-390)""" received = (