Skip to content

Commit dcb7d25

Browse files
harjothkharaclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 9ad6ba0 commit dcb7d25

4 files changed

Lines changed: 38 additions & 0 deletions

File tree

Doc/library/email.compat32-message.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,15 @@ Here are the methods of the :class:`Message` class:
419419

420420
Content-Disposition: attachment; filename*="iso-8859-1''Fu%DFballer.ppt"
421421

422+
When called without any *_params*, this method stores *_value* the same
423+
way :meth:`__setitem__` does, so *_value* may also be an
424+
:class:`~email.header.Header` instance, such as those returned by
425+
:meth:`items` under a ``compat32`` policy.
426+
427+
.. versionchanged:: next
428+
When no parameters are given, an :class:`~email.header.Header`
429+
*_value* is now accepted, consistent with :meth:`__setitem__`.
430+
422431

423432
.. method:: replace_header(_name, _value)
424433

Lib/email/message.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,13 @@ def add_header(self, _name, _value, **_params):
576576
msg.add_header('content-disposition', 'attachment',
577577
filename='Fußballer.ppt'))
578578
"""
579+
if not _params:
580+
# With no parameters, mirror __setitem__ so add_header() accepts
581+
# the same values, e.g. the Header instances that items() returns
582+
# under the compat32 policy. None is coerced to '' to preserve
583+
# add_header()'s historical behavior (__setitem__ would store None).
584+
self[_name] = '' if _value is None else _value
585+
return
579586
parts = []
580587
for k, v in _params.items():
581588
if v is None:

Lib/test/test_email/test_email.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,23 @@ def test_add_header_with_no_value(self):
839839
msg.add_header('X-Status', None)
840840
self.assertEqual('', msg['X-Status'])
841841

842+
def test_add_header_copies_Header_returned_by_items(self):
843+
# gh-151454: items() returns (name, Header) under the compat32 policy
844+
# for a header carrying 8-bit data. Copying such headers with the
845+
# bug report's loop (newmsg.add_header(h, v)) used to raise
846+
# "TypeError: sequence item 0: expected str instance, Header found".
847+
old = email.message_from_bytes(b'X-Ham-Report: spam \xff report\n\n')
848+
name, value = old.items()[0]
849+
self.assertIsInstance(value, Header)
850+
msg = Message()
851+
msg.add_header(name, value)
852+
# The Header is stored unmodified, exactly as __setitem__ does (rather
853+
# than stringified, which would lose the 8-bit bytes to U+FFFD).
854+
self.assertIs(next(msg.raw_items())[1], value)
855+
ref = Message()
856+
ref[name] = value
857+
self.assertEqual(msg.as_bytes(), ref.as_bytes())
858+
842859
# Issue 5871: reject an attempt to embed a header inside a header value
843860
# (header injection attack).
844861
def test_embedded_header_via_Header_rejected(self):
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
When called without extra parameters, :meth:`email.message.Message.add_header`
2+
now stores the value the same way ``msg[name] = value`` does, so it accepts the
3+
:class:`~email.header.Header` instances that :meth:`~email.message.Message.items`
4+
returns under the ``compat32`` policy. Previously it raised ``TypeError:
5+
sequence item 0: expected str instance, Header found``.

0 commit comments

Comments
 (0)