Skip to content
Open
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
13 changes: 13 additions & 0 deletions Doc/library/email.compat32-message.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
15 changes: 15 additions & 0 deletions Lib/email/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
26 changes: 26 additions & 0 deletions Lib/test/test_email/test_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Loading