Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion src/mailparser/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import email.utils
import functools
import hashlib
import inspect
import json
import logging
import os
Expand Down Expand Up @@ -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
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <plain@example.com>")

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 = (
Expand Down