diff --git a/Doc/library/email.compat32-message.rst b/Doc/library/email.compat32-message.rst index 5754c2b65b239f9..53d05e7f1da8399 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 641fb2e944d4311..b0ab7412bdcbcc7 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 19555d87085e176..950e4d3d35e04fa 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 000000000000000..e5f07fee66d616d --- /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``.