Skip to content
Merged
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
32 changes: 31 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,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
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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
Expand Down
47 changes: 47 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@
"""

import base64
import email.utils
import inspect
import os
import tempfile
import unittest
from unittest.mock import Mock, patch

from mailparser.exceptions import MailParserOSError, MailParserReceivedParsingError
from mailparser.utils import (
_GETADDRESSES_SUPPORTS_STRICT,
decode_header_part,
find_between,
get_addresses,
Expand Down Expand Up @@ -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 <plain@example.com>")

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