From 93e7c1a32436e5b2f6be0d16225e0af54e09c359 Mon Sep 17 00:00:00 2001 From: Stefan Zetzsche Date: Sun, 22 Feb 2026 14:54:23 +0000 Subject: [PATCH 1/6] gh-XXXXX: Fix email.utils.collapse_rfc2231_value crash on non-3-tuples Passing a tuple of length != 3 fell through to unquote(value) which calls .startswith() on the tuple, raising AttributeError. Handle non-3-tuples explicitly before calling unquote. --- Lib/email/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/email/utils.py b/Lib/email/utils.py index d4824dc3601b2d..2a8924e0e3824b 100644 --- a/Lib/email/utils.py +++ b/Lib/email/utils.py @@ -449,6 +449,8 @@ def decode_params(params): def collapse_rfc2231_value(value, errors='replace', fallback_charset='us-ascii'): if not isinstance(value, tuple) or len(value) != 3: + if isinstance(value, tuple): + return str(value) return unquote(value) # While value comes to us as a unicode string, we need it to be a bytes # object. We do not want bytes() normal utf-8 decoder, we want a straight From d85561d68e5a7c609205a21985f17105350d77cf Mon Sep 17 00:00:00 2001 From: Stefan Zetzsche Date: Wed, 11 Mar 2026 14:32:01 +0000 Subject: [PATCH 2/6] Add test for collapse_rfc2231_value with non-3-tuples --- Lib/test/test_email/test_email.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py index 4e6c213510c74c..b761291404f98b 100644 --- a/Lib/test/test_email/test_email.py +++ b/Lib/test/test_email/test_email.py @@ -5789,6 +5789,21 @@ def test_should_not_hang_on_invalid_ew_messages(self): with self.subTest(m=m): msg = email.message_from_string(m) + def test_collapse_rfc2231_value_non_3_tuple(self): + # collapse_rfc2231_value should not crash on tuples + # whose length is not 3. + from email.utils import collapse_rfc2231_value + # Non-3-tuples should return a string, not raise AttributeError. + for val in [(), ('a',), ('a', 'b'), ('a', 'b', 'c', 'd')]: + with self.subTest(val=val): + result = collapse_rfc2231_value(val) + self.assertIsInstance(result, str) + # A proper 3-tuple decodes correctly. + result = collapse_rfc2231_value(('us-ascii', 'en', 'hello')) + self.assertEqual(result, 'hello') + # A plain string passes through unquote. + self.assertEqual(collapse_rfc2231_value('"hello"'), 'hello') + # Tests to ensure that signed parts of an email are completely preserved, as # required by RFC1847 section 2.1. Note that these are incomplete, because the From 972b4896305b178859ed55e40c6c6bb2aa6b2362 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:42:24 +0000 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Library/2026-03-11-14-42-23.gh-issue-145824.HMjVqq.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-03-11-14-42-23.gh-issue-145824.HMjVqq.rst diff --git a/Misc/NEWS.d/next/Library/2026-03-11-14-42-23.gh-issue-145824.HMjVqq.rst b/Misc/NEWS.d/next/Library/2026-03-11-14-42-23.gh-issue-145824.HMjVqq.rst new file mode 100644 index 00000000000000..eae5e839f86d6f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-11-14-42-23.gh-issue-145824.HMjVqq.rst @@ -0,0 +1,3 @@ +Fix :exc:`AttributeError` in :func:`email.utils.collapse_rfc2231_value` when +called with a tuple whose length is not 3. The function now returns a string +representation instead of passing the tuple to :func:`~email.utils.unquote`. From 741d554cd9cb1fee4bb94be7c3fef6404974ba36 Mon Sep 17 00:00:00 2001 From: Stefan Zetzsche Date: Wed, 11 Mar 2026 17:20:35 +0000 Subject: [PATCH 4/6] email.utils: raise TypeError for non-string non-3-tuple, update docs Revised per reviewer feedback: instead of silently handling non-3-tuples, raise TypeError with a clear message. Updated docs to say 'not a 3-tuple' instead of 'not a tuple' to match the code's actual check. --- Doc/library/email.utils.rst | 2 +- Lib/email/utils.py | 6 ++++-- Lib/test/test_email/test_email.py | 11 +++++------ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Doc/library/email.utils.rst b/Doc/library/email.utils.rst index e0d2c19a3b0737..da55341a92c787 100644 --- a/Doc/library/email.utils.rst +++ b/Doc/library/email.utils.rst @@ -212,7 +212,7 @@ of the new API. :rfc:`2231` header is not known by Python; it defaults to ``'us-ascii'``. For convenience, if the *value* passed to :func:`collapse_rfc2231_value` is not - a tuple, it should be a string and it is returned unquoted. + a 3-tuple, it should be a string and it is returned unquoted. .. function:: decode_params(params) diff --git a/Lib/email/utils.py b/Lib/email/utils.py index 2a8924e0e3824b..0c8bf384249bb1 100644 --- a/Lib/email/utils.py +++ b/Lib/email/utils.py @@ -449,8 +449,10 @@ def decode_params(params): def collapse_rfc2231_value(value, errors='replace', fallback_charset='us-ascii'): if not isinstance(value, tuple) or len(value) != 3: - if isinstance(value, tuple): - return str(value) + if not isinstance(value, str): + raise TypeError( + f"expected str or 3-tuple, got {type(value).__name__}" + ) return unquote(value) # While value comes to us as a unicode string, we need it to be a bytes # object. We do not want bytes() normal utf-8 decoder, we want a straight diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py index b761291404f98b..b4c8f45b16a101 100644 --- a/Lib/test/test_email/test_email.py +++ b/Lib/test/test_email/test_email.py @@ -5790,14 +5790,13 @@ def test_should_not_hang_on_invalid_ew_messages(self): msg = email.message_from_string(m) def test_collapse_rfc2231_value_non_3_tuple(self): - # collapse_rfc2231_value should not crash on tuples - # whose length is not 3. + # collapse_rfc2231_value raises TypeError on values that are + # neither a string nor a 3-tuple. from email.utils import collapse_rfc2231_value - # Non-3-tuples should return a string, not raise AttributeError. - for val in [(), ('a',), ('a', 'b'), ('a', 'b', 'c', 'd')]: + for val in [(), ('a',), ('a', 'b'), ('a', 'b', 'c', 'd'), 42, None]: with self.subTest(val=val): - result = collapse_rfc2231_value(val) - self.assertIsInstance(result, str) + with self.assertRaises(TypeError): + collapse_rfc2231_value(val) # A proper 3-tuple decodes correctly. result = collapse_rfc2231_value(('us-ascii', 'en', 'hello')) self.assertEqual(result, 'hello') From 0a7bd9df873a484e5e9a7143be12e8df228e1776 Mon Sep 17 00:00:00 2001 From: Stefan Zetzsche Date: Wed, 11 Mar 2026 17:22:52 +0000 Subject: [PATCH 5/6] Update NEWS entry to reflect TypeError change --- .../Library/2026-03-11-14-42-23.gh-issue-145824.HMjVqq.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2026-03-11-14-42-23.gh-issue-145824.HMjVqq.rst b/Misc/NEWS.d/next/Library/2026-03-11-14-42-23.gh-issue-145824.HMjVqq.rst index eae5e839f86d6f..b6c56519b87a17 100644 --- a/Misc/NEWS.d/next/Library/2026-03-11-14-42-23.gh-issue-145824.HMjVqq.rst +++ b/Misc/NEWS.d/next/Library/2026-03-11-14-42-23.gh-issue-145824.HMjVqq.rst @@ -1,3 +1,3 @@ -Fix :exc:`AttributeError` in :func:`email.utils.collapse_rfc2231_value` when -called with a tuple whose length is not 3. The function now returns a string -representation instead of passing the tuple to :func:`~email.utils.unquote`. +:func:`email.utils.collapse_rfc2231_value` now raises :exc:`TypeError` +instead of :exc:`AttributeError` when called with a value that is neither a +string nor a 3-tuple. From 72f7c752c24299a03743d2e2d80d6eb868d4f081 Mon Sep 17 00:00:00 2001 From: Stefan Zetzsche Date: Thu, 12 Mar 2026 14:09:08 +0000 Subject: [PATCH 6/6] Address review: remove LLM comments, use top-level import, drop redundant success test --- Lib/test/test_email/test_email.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py index b4c8f45b16a101..aa6053002c2977 100644 --- a/Lib/test/test_email/test_email.py +++ b/Lib/test/test_email/test_email.py @@ -5790,18 +5790,10 @@ def test_should_not_hang_on_invalid_ew_messages(self): msg = email.message_from_string(m) def test_collapse_rfc2231_value_non_3_tuple(self): - # collapse_rfc2231_value raises TypeError on values that are - # neither a string nor a 3-tuple. - from email.utils import collapse_rfc2231_value for val in [(), ('a',), ('a', 'b'), ('a', 'b', 'c', 'd'), 42, None]: with self.subTest(val=val): with self.assertRaises(TypeError): - collapse_rfc2231_value(val) - # A proper 3-tuple decodes correctly. - result = collapse_rfc2231_value(('us-ascii', 'en', 'hello')) - self.assertEqual(result, 'hello') - # A plain string passes through unquote. - self.assertEqual(collapse_rfc2231_value('"hello"'), 'hello') + email.utils.collapse_rfc2231_value(val) # Tests to ensure that signed parts of an email are completely preserved, as