From 0d7e2987c2f0e0096e2e6f68be218b4e3c5ac8be Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 1 Jul 2026 14:50:17 +0300 Subject: [PATCH 1/2] gh-49680: Add translate_line_endings parameter to imaplib append Pass False to send the message as an exact sequence of octets, without rewriting bare CR or LF to CRLF. Co-Authored-By: Claude Opus 4.8 (1M context) --- Doc/library/imaplib.rst | 10 +++++++++- Lib/imaplib.py | 12 +++++++++--- Lib/test/test_imaplib.py | 19 +++++++++++++++++++ ...6-07-01-14-00-00.gh-issue-49680.Lm5rQv.rst | 4 ++++ 4 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-01-14-00-00.gh-issue-49680.Lm5rQv.rst diff --git a/Doc/library/imaplib.rst b/Doc/library/imaplib.rst index df2468f7124e6d6..4d9900648708c16 100644 --- a/Doc/library/imaplib.rst +++ b/Doc/library/imaplib.rst @@ -195,7 +195,7 @@ upper bound (``'3:*'``). An :class:`IMAP4` instance has the following methods: -.. method:: IMAP4.append(mailbox, flags, date_time, message) +.. method:: IMAP4.append(mailbox, flags, date_time, message, *, translate_line_endings=True) Append *message* to named mailbox. @@ -204,6 +204,14 @@ An :class:`IMAP4` instance has the following methods: If *flags* is not already enclosed in parentheses, parentheses are added automatically. + If *translate_line_endings* is true (the default), + line endings in *message* are translated to CRLF. + Pass ``False`` to send the message literal exactly as given, + which is required to preserve messages that contain bare CR or LF. + + .. versionchanged:: next + Added the *translate_line_endings* parameter. + .. method:: IMAP4.authenticate(mechanism, authobject) diff --git a/Lib/imaplib.py b/Lib/imaplib.py index 497b5a60cecb083..c387c7299eedf77 100644 --- a/Lib/imaplib.py +++ b/Lib/imaplib.py @@ -483,12 +483,17 @@ def response(self, code): # IMAP4 commands - def append(self, mailbox, flags, date_time, message): + def append(self, mailbox, flags, date_time, message, *, + translate_line_endings=True): """Append message to named mailbox. (typ, [data]) = .append(mailbox, flags, date_time, message) All args except 'message' can be None. + + If 'translate_line_endings' is true (the default), line endings in + 'message' are translated to CRLF. Pass false to send the message + literal exactly as given. """ name = 'APPEND' if not mailbox: @@ -502,8 +507,9 @@ def append(self, mailbox, flags, date_time, message): date_time = Time2Internaldate(date_time) else: date_time = None - literal = MapCRLF.sub(CRLF, message) - self.literal = literal + if translate_line_endings: + message = MapCRLF.sub(CRLF, message) + self.literal = message return self._simple_command(name, mailbox, flags, date_time) diff --git a/Lib/test/test_imaplib.py b/Lib/test/test_imaplib.py index fb256fb7cbcd344..68b261b71cb43b2 100644 --- a/Lib/test/test_imaplib.py +++ b/Lib/test/test_imaplib.py @@ -634,6 +634,25 @@ def test_login(self): self.assertEqual(data[0], b'LOGIN completed') self.assertEqual(client.state, 'AUTH') + def test_append_translate_line_endings(self): + # By default line endings in the message are normalized to CRLF; + # translate_line_endings=False sends the literal exactly (gh-49680). + class AppendHandler(SimpleIMAPHandler): + def cmd_APPEND(self, tag, args): + size = int(args[-1].strip('{}')) + self._send_textline('+') + self.server.response = self.rfile.read(size) + self.rfile.readline() # trailing CRLF after the literal + self._send_tagged(tag, 'OK', 'APPEND completed') + message = b'a\rb\nc\r\nd' + client, server = self._setup(AppendHandler) + client.login('user', 'pass') + client.append('INBOX', None, None, message) + self.assertEqual(server.response, b'a\r\nb\r\nc\r\nd') + client.append('INBOX', None, None, message, + translate_line_endings=False) + self.assertEqual(server.response, message) + def test_logout(self): client, _ = self._setup(SimpleIMAPHandler) typ, data = client.login('user', 'pass') diff --git a/Misc/NEWS.d/next/Library/2026-07-01-14-00-00.gh-issue-49680.Lm5rQv.rst b/Misc/NEWS.d/next/Library/2026-07-01-14-00-00.gh-issue-49680.Lm5rQv.rst new file mode 100644 index 000000000000000..6371df69e69077f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-01-14-00-00.gh-issue-49680.Lm5rQv.rst @@ -0,0 +1,4 @@ +Add the *translate_line_endings* parameter to :meth:`imaplib.IMAP4.append`. +By default line endings in the message are translated to CRLF, as before; +passing ``False`` sends the message literal exactly as given, preserving +bare CR or LF octets. From ccf900a6059efdc83e7e970c76152697c5fbe229 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 2 Jul 2026 08:10:08 +0300 Subject: [PATCH 2/2] gh-49680: Test exact APPEND and email message round-trip Read the APPEND literal by its octet count and verify that a serialized email message with bare LF is sent verbatim when translate_line_endings is false. Document using email.policy.SMTP for that case. Co-authored-by: harjoth Co-Authored-By: Claude Opus 4.8 (1M context) --- Doc/library/imaplib.rst | 3 +++ Lib/test/test_imaplib.py | 44 ++++++++++++++++++++++++++++------------ 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/Doc/library/imaplib.rst b/Doc/library/imaplib.rst index 4d9900648708c16..76a69d0c77e342a 100644 --- a/Doc/library/imaplib.rst +++ b/Doc/library/imaplib.rst @@ -208,6 +208,9 @@ An :class:`IMAP4` instance has the following methods: line endings in *message* are translated to CRLF. Pass ``False`` to send the message literal exactly as given, which is required to preserve messages that contain bare CR or LF. + In that case *message* must already use CRLF line endings as required + by :rfc:`3501`; for example, serialize :mod:`email` messages using + :class:`email.policy.SMTP`. .. versionchanged:: next Added the *translate_line_endings* parameter. diff --git a/Lib/test/test_imaplib.py b/Lib/test/test_imaplib.py index 68b261b71cb43b2..bd1b0d230ca2242 100644 --- a/Lib/test/test_imaplib.py +++ b/Lib/test/test_imaplib.py @@ -2,6 +2,7 @@ from test.support import socket_helper from contextlib import contextmanager +from email.message import EmailMessage import imaplib import os.path import socketserver @@ -9,6 +10,7 @@ import calendar import threading import re +import select import socket from test.support import verbose, run_with_tz, run_with_locale, cpython_only @@ -27,6 +29,18 @@ CAFILE = os.path.join(os.path.dirname(__file__) or os.curdir, "certdata", "pycacert.pem") +def _read_literal(handler, marker): + # Read one literal, a raw octet sequence, by its count from the marker + # ('{N}', or '(~{N}' in UTF8 mode). + size = int(re.search(r'\{(\d+)\}', marker).group(1)) + # The client must wait for the continuation, so nothing should be readable. + if select.select([handler.connection], [], [], 0)[0]: + raise AssertionError('client sent the literal before the ' + 'continuation request') + handler._send_textline('+') + return handler.rfile.read(size) + + class TestImaplib(unittest.TestCase): def test_Internaldate2tuple(self): @@ -371,10 +385,8 @@ def cmd_AUTHENTICATE(self, tag, args): self.server.response = yield self._send_tagged(tag, 'OK', 'FAKEAUTH successful') def cmd_APPEND(self, tag, args): - self._send_textline('+') self.server.response = args - literal = yield - self.server.response.append(literal) + self.server.response.append(_read_literal(self, args[-1])) literal = yield self.server.response.append(literal) self._send_tagged(tag, 'OK', 'okay') @@ -635,24 +647,32 @@ def test_login(self): self.assertEqual(client.state, 'AUTH') def test_append_translate_line_endings(self): - # By default line endings in the message are normalized to CRLF; - # translate_line_endings=False sends the literal exactly (gh-49680). + # By default line endings are normalized to CRLF; False sends the + # literal exactly (gh-49680). class AppendHandler(SimpleIMAPHandler): def cmd_APPEND(self, tag, args): - size = int(args[-1].strip('{}')) - self._send_textline('+') - self.server.response = self.rfile.read(size) - self.rfile.readline() # trailing CRLF after the literal + self.server.response = _read_literal(self, args[-1]) + yield # read the trailer line self._send_tagged(tag, 'OK', 'APPEND completed') - message = b'a\rb\nc\r\nd' client, server = self._setup(AppendHandler) client.login('user', 'pass') + message = b'a\rb\nc\r\nd' client.append('INBOX', None, None, message) self.assertEqual(server.response, b'a\r\nb\r\nc\r\nd') client.append('INBOX', None, None, message, translate_line_endings=False) self.assertEqual(server.response, message) + # An email message uses bare LF by default; False sends it verbatim. + message = EmailMessage() + message['Subject'] = 'line endings' + message.set_content('body line\n') + message = message.as_bytes() + self.assertNotIn(b'\r\n', message) + client.append('INBOX', None, None, message, + translate_line_endings=False) + self.assertEqual(server.response, message) + def test_logout(self): client, _ = self._setup(SimpleIMAPHandler) typ, data = client.login('user', 'pass') @@ -944,10 +964,8 @@ def test_enable_UTF8_True_append(self): class UTF8AppendServer(self.UTF8Server): def cmd_APPEND(self, tag, args): - self._send_textline('+') self.server.response = args - literal = yield - self.server.response.append(literal) + self.server.response.append(_read_literal(self, args[-1])) literal = yield self.server.response.append(literal) self._send_tagged(tag, 'OK', 'okay')