From dcb7d2567b6f7df15bf4de9322a962e8ee592606 Mon Sep 17 00:00:00 2001 From: harjoth Date: Sat, 13 Jun 2026 15:06:31 -0700 Subject: [PATCH] gh-151454: Accept Header values in email.message.Message.add_header Message.items() can yield (name, Header) pairs under the compat32 policy (for a header carrying 8-bit data), and Message[name] = value accepts such Header values, but add_header() raised "TypeError: sequence item 0: expected str instance, Header found" because it assembled the value with SEMISPACE.join(). When no extra parameters are given, add_header() now mirrors __setitem__ and stores the value (Header or str) unmodified, fixing the reported header-copy loop. None is still coerced to '' to preserve add_header()'s historical behavior. The parameterized branch is unchanged: it still builds the header as a string and so continues to require a str value. Co-Authored-By: Claude Opus 4.8 --- Doc/library/email.compat32-message.rst | 9 +++++++++ Lib/email/message.py | 7 +++++++ Lib/test/test_email/test_email.py | 17 +++++++++++++++++ ...26-06-13-15-00-38.gh-issue-151454.BQQ7up.rst | 5 +++++ 4 files changed, 38 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-06-13-15-00-38.gh-issue-151454.BQQ7up.rst diff --git a/Doc/library/email.compat32-message.rst b/Doc/library/email.compat32-message.rst index 5754c2b65b239f..53d05e7f1da839 100644 --- a/Doc/library/email.compat32-message.rst +++ b/Doc/library/email.compat32-message.rst @@ -419,6 +419,15 @@ Here are the methods of the :class:`Message` class: Content-Disposition: attachment; filename*="iso-8859-1''Fu%DFballer.ppt" + When called without any *_params*, this method stores *_value* the same + way :meth:`__setitem__` does, so *_value* may also be an + :class:`~email.header.Header` instance, such as those returned by + :meth:`items` under a ``compat32`` policy. + + .. versionchanged:: next + When no parameters are given, an :class:`~email.header.Header` + *_value* is now accepted, consistent with :meth:`__setitem__`. + .. method:: replace_header(_name, _value) diff --git a/Lib/email/message.py b/Lib/email/message.py index 641fb2e944d431..b0ab7412bdcbcc 100644 --- a/Lib/email/message.py +++ b/Lib/email/message.py @@ -576,6 +576,13 @@ def add_header(self, _name, _value, **_params): msg.add_header('content-disposition', 'attachment', filename='Fußballer.ppt')) """ + if not _params: + # With no parameters, mirror __setitem__ so add_header() accepts + # the same values, e.g. the Header instances that items() returns + # under the compat32 policy. None is coerced to '' to preserve + # add_header()'s historical behavior (__setitem__ would store None). + self[_name] = '' if _value is None else _value + return parts = [] for k, v in _params.items(): if v is None: diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py index 19555d87085e17..950e4d3d35e04f 100644 --- a/Lib/test/test_email/test_email.py +++ b/Lib/test/test_email/test_email.py @@ -839,6 +839,23 @@ def test_add_header_with_no_value(self): msg.add_header('X-Status', None) self.assertEqual('', msg['X-Status']) + def test_add_header_copies_Header_returned_by_items(self): + # gh-151454: items() returns (name, Header) under the compat32 policy + # for a header carrying 8-bit data. Copying such headers with the + # bug report's loop (newmsg.add_header(h, v)) used to raise + # "TypeError: sequence item 0: expected str instance, Header found". + old = email.message_from_bytes(b'X-Ham-Report: spam \xff report\n\n') + name, value = old.items()[0] + self.assertIsInstance(value, Header) + msg = Message() + msg.add_header(name, value) + # The Header is stored unmodified, exactly as __setitem__ does (rather + # than stringified, which would lose the 8-bit bytes to U+FFFD). + self.assertIs(next(msg.raw_items())[1], value) + ref = Message() + ref[name] = value + self.assertEqual(msg.as_bytes(), ref.as_bytes()) + # Issue 5871: reject an attempt to embed a header inside a header value # (header injection attack). def test_embedded_header_via_Header_rejected(self): diff --git a/Misc/NEWS.d/next/Library/2026-06-13-15-00-38.gh-issue-151454.BQQ7up.rst b/Misc/NEWS.d/next/Library/2026-06-13-15-00-38.gh-issue-151454.BQQ7up.rst new file mode 100644 index 00000000000000..e5f07fee66d616 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-13-15-00-38.gh-issue-151454.BQQ7up.rst @@ -0,0 +1,5 @@ +When called without extra parameters, :meth:`email.message.Message.add_header` +now stores the value the same way ``msg[name] = value`` does, so it accepts the +:class:`~email.header.Header` instances that :meth:`~email.message.Message.items` +returns under the ``compat32`` policy. Previously it raised ``TypeError: +sequence item 0: expected str instance, Header found``.