Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f39884b

Browse files
committed
#15222: Insert blank line after each message in mbox mailboxes
1 parent 4680919 commit f39884b

3 files changed

Lines changed: 63 additions & 6 deletions

File tree

Lib/mailbox.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,9 @@ def _string_to_bytes(self, message):
208208
raise ValueError("String input must be ASCII-only; "
209209
"use bytes or a Message instead")
210210

211+
# Whether each message must end in a newline
212+
_append_newline = False
213+
211214
def _dump_message(self, message, target, mangle_from_=False):
212215
# This assumes the target file is open in binary mode.
213216
"""Dump message contents to target file."""
@@ -219,6 +222,9 @@ def _dump_message(self, message, target, mangle_from_=False):
219222
data = buffer.read()
220223
data = data.replace(b'\n', linesep)
221224
target.write(data)
225+
if self._append_newline and not data.endswith(linesep):
226+
# Make sure the message ends with a newline
227+
target.write(linesep)
222228
elif isinstance(message, (str, bytes, io.StringIO)):
223229
if isinstance(message, io.StringIO):
224230
warnings.warn("Use of StringIO input is deprecated, "
@@ -230,11 +236,15 @@ def _dump_message(self, message, target, mangle_from_=False):
230236
message = message.replace(b'\nFrom ', b'\n>From ')
231237
message = message.replace(b'\n', linesep)
232238
target.write(message)
239+
if self._append_newline and not message.endswith(linesep):
240+
# Make sure the message ends with a newline
241+
target.write(linesep)
233242
elif hasattr(message, 'read'):
234243
if hasattr(message, 'buffer'):
235244
warnings.warn("Use of text mode files is deprecated, "
236245
"use a binary mode file instead", DeprecationWarning, 3)
237246
message = message.buffer
247+
lastline = None
238248
while True:
239249
line = message.readline()
240250
# Universal newline support.
@@ -248,6 +258,10 @@ def _dump_message(self, message, target, mangle_from_=False):
248258
line = b'>From ' + line[5:]
249259
line = line.replace(b'\n', linesep)
250260
target.write(line)
261+
lastline = line
262+
if self._append_newline and lastline and not lastline.endswith(linesep):
263+
# Make sure the message ends with a newline
264+
target.write(linesep)
251265
else:
252266
raise TypeError('Invalid message type: %s' % type(message))
253267

@@ -833,30 +847,48 @@ class mbox(_mboxMMDF):
833847

834848
_mangle_from_ = True
835849

850+
# All messages must end in a newline character, and
851+
# _post_message_hooks outputs an empty line between messages.
852+
_append_newline = True
853+
836854
def __init__(self, path, factory=None, create=True):
837855
"""Initialize an mbox mailbox."""
838856
self._message_factory = mboxMessage
839857
_mboxMMDF.__init__(self, path, factory, create)
840858

841-
def _pre_message_hook(self, f):
842-
"""Called before writing each message to file f."""
843-
if f.tell() != 0:
844-
f.write(linesep)
859+
def _post_message_hook(self, f):
860+
"""Called after writing each message to file f."""
861+
f.write(linesep)
845862

846863
def _generate_toc(self):
847864
"""Generate key-to-(start, stop) table of contents."""
848865
starts, stops = [], []
866+
last_was_empty = False
849867
self._file.seek(0)
850868
while True:
851869
line_pos = self._file.tell()
852870
line = self._file.readline()
853871
if line.startswith(b'From '):
854872
if len(stops) < len(starts):
855-
stops.append(line_pos - len(linesep))
873+
if last_was_empty:
874+
stops.append(line_pos - len(linesep))
875+
else:
876+
# The last line before the "From " line wasn't
877+
# blank, but we consider it a start of a
878+
# message anyway.
879+
stops.append(line_pos)
856880
starts.append(line_pos)
881+
last_was_empty = False
857882
elif not line:
858-
stops.append(line_pos)
883+
if last_was_empty:
884+
stops.append(line_pos - len(linesep))
885+
else:
886+
stops.append(line_pos)
859887
break
888+
elif line == linesep:
889+
last_was_empty = True
890+
else:
891+
last_was_empty = False
860892
self._toc = dict(enumerate(zip(starts, stops)))
861893
self._next_key = len(self._toc)
862894
self._file_length = self._file.tell()

Lib/test/test_mailbox.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1113,6 +1113,29 @@ def test_file_perms(self):
11131113
perms = st.st_mode
11141114
self.assertFalse((perms & 0o111)) # Execute bits should all be off.
11151115

1116+
def test_terminating_newline(self):
1117+
message = email.message.Message()
1118+
message['From'] = '[email protected]'
1119+
message.set_payload('No newline at the end')
1120+
i = self._box.add(message)
1121+
1122+
# A newline should have been appended to the payload
1123+
message = self._box.get(i)
1124+
self.assertEqual(message.get_payload(), 'No newline at the end\n')
1125+
1126+
def test_message_separator(self):
1127+
# Check there's always a single blank line after each message
1128+
self._box.add('From: foo\n\n0') # No newline at the end
1129+
with open(self._path) as f:
1130+
data = f.read()
1131+
self.assertEqual(data[-3:], '0\n\n')
1132+
1133+
self._box.add('From: foo\n\n0\n') # Newline at the end
1134+
with open(self._path) as f:
1135+
data = f.read()
1136+
self.assertEqual(data[-3:], '0\n\n')
1137+
1138+
11161139
class TestMMDF(_TestMboxMMDF, unittest.TestCase):
11171140

11181141
_factory = lambda self, path, factory=None: mailbox.MMDF(path, factory)

Misc/NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ Core and Builtins
123123
Library
124124
-------
125125

126+
- Issue #15222: Insert blank line after each message in mbox mailboxes
127+
126128
- Issue #16013: Fix CSV Reader parsing issue with ending quote characters.
127129
Patch by Serhiy Storchaka.
128130

0 commit comments

Comments
 (0)