diff --git a/src/mailparser/utils.py b/src/mailparser/utils.py index be6e4d5..1bb03ac 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,35 @@ log = logging.getLogger(__name__) + +# 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'`` +# (parsedmarc #808). The signature is fixed for the running interpreter, so +# detect support once at import time. +_GETADDRESSES_SUPPORTS_STRICT = ( + "strict" in inspect.signature(email.utils.getaddresses).parameters +) + + +def _getaddresses(fieldvalues: list[str]) -> list[tuple[str, str]]: + """ + Call ``email.utils.getaddresses`` with strict parsing when available. + + Args: + fieldvalues (list[str]): raw address header values to parse. + + Returns: + list[tuple[str, str]]: list of ``(display_name, email_addr)`` tuples, + as returned by ``email.utils.getaddresses``. + """ + if _GETADDRESSES_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 +164,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..e56283e 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -17,6 +17,8 @@ """ import base64 +import email.utils +import inspect import os import tempfile import unittest @@ -24,6 +26,7 @@ from mailparser.exceptions import MailParserOSError, MailParserReceivedParsingError from mailparser.utils import ( + _GETADDRESSES_SUPPORTS_STRICT, decode_header_part, find_between, get_addresses, @@ -771,6 +774,50 @@ 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 interpreter: the feature-detection flag + is forced off and ``getaddresses`` is replaced by a function whose + signature lacks ``strict``, so passing the keyword would raise the + reported TypeError. The address must still be parsed. + """ + real_getaddresses = email.utils.getaddresses + + def legacy_getaddresses(fieldvalues): + """Mimic the pre-3.13 signature that lacks ``strict``.""" + return real_getaddresses(fieldvalues) + + with ( + patch("mailparser.utils._GETADDRESSES_SUPPORTS_STRICT", False), + patch( + "mailparser.utils.email.utils.getaddresses", + new=legacy_getaddresses, + ), + ): + result = get_addresses("Plain Name ") + + self.assertEqual(result, [("Plain Name", "plain@example.com")]) + + def test_get_addresses_strict_detection_matches_interpreter(self): + """ + The feature-detection flag must reflect the running interpreter, so + that ``strict=True`` is still used wherever it is available. + """ + self.assertEqual( + _GETADDRESSES_SUPPORTS_STRICT, + "strict" in inspect.signature(email.utils.getaddresses).parameters, + ) + def test_parse_received_sendgrid_date(self): """parse_received extracts SendGrid non-standard date (utils.py:389-390)""" received = (