Skip to content

Commit adbdcdb

Browse files
committed
#14925: email now registers a defect for missing header/body separator.
This patch also deprecates the MalformedHeaderDefect. My best guess is that this defect was rendered obsolete by a refactoring of the parser, and the corresponding defect for the new parser (which this patch introduces) was overlooked.
1 parent 2c172d0 commit adbdcdb

6 files changed

Lines changed: 61 additions & 18 deletions

File tree

Doc/library/email.errors.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,18 @@ this class is *not* an exception!
7979
* :class:`MisplacedEnvelopeHeaderDefect` - A "Unix From" header was found in the
8080
middle of a header block.
8181

82+
* :class:`MissingHeaderBodySeparatorDefect` - A line was found while parsing
83+
headers that had no leading white space but contained no ':'. Parsing
84+
continues assuming that the line represents the first line of the body.
85+
86+
.. versionadded: 3.3
87+
8288
* :class:`MalformedHeaderDefect` -- A header was found that was missing a colon,
8389
or was otherwise malformed.
8490

91+
.. deprecated:: 3.3
92+
This defect has not been used for several Python versions.
93+
8594
* :class:`MultipartInvariantViolationDefect` -- A message claimed to be a
8695
:mimetype:`multipart`, but no subparts were found. Note that when a message has
8796
this defect, its :meth:`is_multipart` method may return false even though its

Lib/email/errors.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,10 @@ class FirstHeaderLineIsContinuationDefect(MessageDefect):
4848
class MisplacedEnvelopeHeaderDefect(MessageDefect):
4949
"""A 'Unix-from' header was found in the middle of a header block."""
5050

51-
class MalformedHeaderDefect(MessageDefect):
52-
"""Found a header that was missing a colon, or was otherwise malformed."""
51+
class MissingHeaderBodySeparatorDefect(MessageDefect):
52+
"""Found line with no leading whitespace and no colon before blank line."""
53+
# XXX: backward compatibility, just in case (it was never emitted).
54+
MalformedHeaderDefect = MissingHeaderBodySeparatorDefect
5355

5456
class MultipartInvariantViolationDefect(MessageDefect):
5557
"""A message claimed to be a multipart but no subparts were found."""

Lib/email/feedparser.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,8 @@ def _parsegen(self):
219219
# (i.e. newline), just throw it away. Otherwise the line is
220220
# part of the body so push it back.
221221
if not NLCRE.match(line):
222+
defect = errors.MissingHeaderBodySeparatorDefect()
223+
self.policy.handle_defect(self._cur, defect)
222224
self._input.unreadline(line)
223225
break
224226
headers.append(line)
@@ -488,12 +490,10 @@ def _parse_headers(self, lines):
488490
self._cur.defects.append(defect)
489491
continue
490492
# Split the line on the colon separating field name from value.
493+
# There will always be a colon, because if there wasn't the part of
494+
# the parser that calls us would have started parsing the body.
491495
i = line.find(':')
492-
if i < 0:
493-
defect = errors.MalformedHeaderDefect(line)
494-
# XXX: fixme (defect not going through policy)
495-
self._cur.defects.append(defect)
496-
continue
496+
assert i>0, "_parse_headers fed line with no : and no leading WS"
497497
lastheader = line[:i]
498498
lastvalue = [line]
499499
# Done with all the lines, so handle the last header.

Lib/test/test_email/test_email.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1960,15 +1960,27 @@ def test_missing_start_boundary(self):
19601960
# test_parser.TestMessageDefectDetectionBase
19611961
def test_first_line_is_continuation_header(self):
19621962
eq = self.assertEqual
1963-
m = ' Line 1\nLine 2\nLine 3'
1963+
m = ' Line 1\nSubject: test\n\nbody'
19641964
msg = email.message_from_string(m)
1965-
eq(msg.keys(), [])
1966-
eq(msg.get_payload(), 'Line 2\nLine 3')
1965+
eq(msg.keys(), ['Subject'])
1966+
eq(msg.get_payload(), 'body')
19671967
eq(len(msg.defects), 1)
1968-
self.assertTrue(isinstance(msg.defects[0],
1969-
errors.FirstHeaderLineIsContinuationDefect))
1968+
self.assertDefectsEqual(msg.defects,
1969+
[errors.FirstHeaderLineIsContinuationDefect])
19701970
eq(msg.defects[0].line, ' Line 1\n')
19711971

1972+
# test_parser.TestMessageDefectDetectionBase
1973+
def test_missing_header_body_separator(self):
1974+
# Our heuristic if we see a line that doesn't look like a header (no
1975+
# leading whitespace but no ':') is to assume that the blank line that
1976+
# separates the header from the body is missing, and to stop parsing
1977+
# headers and start parsing the body.
1978+
msg = self._str_msg('Subject: test\nnot a header\nTo: abc\n\nb\n')
1979+
self.assertEqual(msg.keys(), ['Subject'])
1980+
self.assertEqual(msg.get_payload(), 'not a header\nTo: abc\n\nb\n')
1981+
self.assertDefectsEqual(msg.defects,
1982+
[errors.MissingHeaderBodySeparatorDefect])
1983+
19721984

19731985
# Test RFC 2047 header encoding and decoding
19741986
class TestRFC2047(TestEmailBase):

Lib/test/test_email/test_parser.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -237,17 +237,33 @@ def test_missing_start_boundary_raise_on_defect(self):
237237
policy=self.policy.clone(raise_on_defect=True))
238238

239239
def test_first_line_is_continuation_header(self):
240-
msg = self._str_msg(' Line 1\nLine 2\nLine 3')
241-
self.assertEqual(msg.keys(), [])
242-
self.assertEqual(msg.get_payload(), 'Line 2\nLine 3')
240+
msg = self._str_msg(' Line 1\nSubject: test\n\nbody')
241+
self.assertEqual(msg.keys(), ['Subject'])
242+
self.assertEqual(msg.get_payload(), 'body')
243243
self.assertEqual(len(self.get_defects(msg)), 1)
244-
self.assertTrue(isinstance(self.get_defects(msg)[0],
245-
errors.FirstHeaderLineIsContinuationDefect))
244+
self.assertDefectsEqual(self.get_defects(msg),
245+
[errors.FirstHeaderLineIsContinuationDefect])
246246
self.assertEqual(self.get_defects(msg)[0].line, ' Line 1\n')
247247

248248
def test_first_line_is_continuation_header_raise_on_defect(self):
249249
with self.assertRaises(errors.FirstHeaderLineIsContinuationDefect):
250-
self._str_msg(' Line 1\nLine 2\nLine 3',
250+
self._str_msg(' Line 1\nSubject: test\n\nbody\n',
251+
policy=self.policy.clone(raise_on_defect=True))
252+
253+
def test_missing_header_body_separator(self):
254+
# Our heuristic if we see a line that doesn't look like a header (no
255+
# leading whitespace but no ':') is to assume that the blank line that
256+
# separates the header from the body is missing, and to stop parsing
257+
# headers and start parsing the body.
258+
msg = self._str_msg('Subject: test\nnot a header\nTo: abc\n\nb\n')
259+
self.assertEqual(msg.keys(), ['Subject'])
260+
self.assertEqual(msg.get_payload(), 'not a header\nTo: abc\n\nb\n')
261+
self.assertDefectsEqual(self.get_defects(msg),
262+
[errors.MissingHeaderBodySeparatorDefect])
263+
264+
def test_missing_header_body_separator_raise_on_defect(self):
265+
with self.assertRaises(errors.MissingHeaderBodySeparatorDefect):
266+
self._str_msg('Subject: test\nnot a header\nTo: abc\n\nb\n',
251267
policy=self.policy.clone(raise_on_defect=True))
252268

253269

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ Core and Builtins
4949
Library
5050
-------
5151

52+
- Issue #14925: email now registers a defect when the parser decides that there
53+
is a missing header/body separator line. MalformedHeaderDefect, which the
54+
existing code would never actually generate, is deprecated.
55+
5256
- Issue #10365: File open dialog now works instead of crashing
5357
even when parent window is closed. Patch by Roger Serwy.
5458

0 commit comments

Comments
 (0)