diff --git a/Lib/mailbox.py b/Lib/mailbox.py index 5bb9843eba31b4..847bc77176c1d1 100644 --- a/Lib/mailbox.py +++ b/Lib/mailbox.py @@ -1426,9 +1426,9 @@ def _generate_toc(self): if len(stops) < len(starts): stops.append(line_pos - len(linesep)) starts.append(next_pos) - labels = [label.strip() for label - in self._file.readline()[1:].split(b',') - if label.strip()] + labels = [label.strip().decode(errors='surrogateescape') + for label in self._file.readline()[1:].split(b',') + if label.strip()] label_lists.append(labels) elif line == b'\037' or line == b'\037' + linesep: if len(stops) < len(starts): @@ -1447,7 +1447,7 @@ def _pre_mailbox_hook(self, f): babyl = b'BABYL OPTIONS:' + linesep babyl += b'Version: 5' + linesep labels = self.get_labels() - labels = (label.encode() for label in labels) + labels = (label.encode(errors='surrogateescape') for label in labels) babyl += b'Labels:' + b','.join(labels) + linesep babyl += b'\037' f.write(babyl) @@ -1473,10 +1473,12 @@ def _install_message(self, message): labels.append(label) self._file.write(b'1') for label in special_labels: - self._file.write(b', ' + label.encode()) + label = label.encode(errors='surrogateescape') + self._file.write(b', ' + label) self._file.write(b',,') for label in labels: - self._file.write(b' ' + label.encode() + b',') + label = label.encode(errors='surrogateescape') + self._file.write(b' ' + label + b',') self._file.write(linesep) else: self._file.write(b'1,,' + linesep) diff --git a/Lib/test/test_mailbox.py b/Lib/test/test_mailbox.py index f1100f98ab9a06..0fdf0cc7ac42a9 100644 --- a/Lib/test/test_mailbox.py +++ b/Lib/test/test_mailbox.py @@ -1521,6 +1521,38 @@ def test_labels(self): self._box.remove(key1) self.assertEqual(set(self._box.get_labels()), set(['blah'])) + def test_labels_read_from_file(self): + # Labels read back from the file are strings. + msg = mailbox.BabylMessage(self._template % 0) + msg.set_labels(['unseen', 'foo']) + self._box.add(msg) + self._box.add(mailbox.BabylMessage(self._template % 1)) + self._box.close() + self._box = self._factory(self._path) + self.assertEqual(sorted(self._box[0].get_labels()), ['foo', 'unseen']) + self.assertEqual(self._box.get_labels(), ['foo']) + self._box.remove(1) + self._box.close() + self._box = self._factory(self._path) + self.assertEqual(sorted(self._box[0].get_labels()), ['foo', 'unseen']) + self.assertEqual(self._box.get_labels(), ['foo']) + + def test_non_utf8_labels(self): + # Labels that are not valid UTF-8 are kept as they are. + msg = mailbox.BabylMessage(self._template % 0) + msg.set_labels(['caf\udce9']) + self._box.add(msg) + self._box.add(mailbox.BabylMessage(self._template % 1)) + self._box.close() + with open(self._path, 'rb') as f: + self.assertIn(b' caf\xe9,', f.read()) + self._box = self._factory(self._path) + self.assertEqual(self._box[0].get_labels(), ['caf\udce9']) + self._box.remove(1) + self._box.close() + with open(self._path, 'rb') as f: + self.assertIn(b'Labels:caf\xe9', f.read()) + def test_visible_headers(self): # gh-157880: visible headers are written to the file and read back msg = mailbox.BabylMessage(self._template % 0) diff --git a/Misc/NEWS.d/next/Library/2026-10-24-13-39-23.gh-issue-160598.ROamiQ.rst b/Misc/NEWS.d/next/Library/2026-10-24-13-39-23.gh-issue-160598.ROamiQ.rst new file mode 100644 index 00000000000000..4220af1d464e71 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-24-13-39-23.gh-issue-160598.ROamiQ.rst @@ -0,0 +1,3 @@ +Fix :class:`mailbox.Babyl` returning message labels as :class:`bytes` +after reading a mailbox file, which made unseen messages appear as read +when converted to other formats and made rewriting the mailbox fail.