diff --git a/Doc/library/email.compat32-message.rst b/Doc/library/email.compat32-message.rst index 5754c2b65b239f9..b8710314aa05c7e 100644 --- a/Doc/library/email.compat32-message.rst +++ b/Doc/library/email.compat32-message.rst @@ -419,6 +419,19 @@ 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. Combining a + :class:`~email.header.Header` *_value* with *_params* raises + :exc:`TypeError`, since the parameters have no defined meaning for it. + + .. versionchanged:: next + When no parameters are given, an :class:`~email.header.Header` + *_value* is now accepted, consistent with :meth:`__setitem__`. + Combining a :class:`~email.header.Header` *_value* with parameters + now raises a clear :exc:`TypeError` instead of an internal one. + .. method:: replace_header(_name, _value) diff --git a/Lib/email/message.py b/Lib/email/message.py index 641fb2e944d4311..06a6baedf37c7c3 100644 --- a/Lib/email/message.py +++ b/Lib/email/message.py @@ -576,6 +576,21 @@ 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 + if _value is not None and not isinstance(_value, str): + # Combining a non-str value (e.g. a Header instance) with + # parameters isn't supported: raise a clear error instead of + # failing inside SEMISPACE.join() below. + raise TypeError( + f'add_header() value must be a str when parameters are ' + f'given, not {type(_value).__name__}; pass a str, or omit ' + f'the parameters and set the header with msg[name] = value') 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..0d586f15ece14e5 100644 --- a/Lib/test/test_email/test_email.py +++ b/Lib/test/test_email/test_email.py @@ -839,6 +839,32 @@ 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()) + + def test_add_header_with_Header_value_and_params_raises_clear_error(self): + # gh-151454: a Header combined with params isn't supported (the + # params would have no defined meaning for it), so this should raise + # a clear TypeError rather than fail inside SEMISPACE.join(). + msg = Message() + header = Header('spam \xff report', charset='unknown-8bit') + with self.assertRaisesRegex(TypeError, 'must be a str'): + msg.add_header('X-Ham-Report', header, foo='bar') + # 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..33418824598d641 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-13-15-00-38.gh-issue-151454.BQQ7up.rst @@ -0,0 +1,7 @@ +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``. Combining a +:class:`~email.header.Header` value with parameters now raises a clear +``TypeError`` instead of that same confusing message.