https://github.com/python/cpython/commit/ddb6539c4976b18895bb5c5cd37d197c60fb459d commit: ddb6539c4976b18895bb5c5cd37d197c60fb459d branch: main author: Serhiy Storchaka <[email protected]> committer: serhiy-storchaka <[email protected]> date: 2026-07-02T11:17:37Z summary:
gh-49680: Add translate_line_endings parameter to imaplib.IMAP4.append (GH-152775) Pass False to send the message as an exact sequence of octets, without rewriting bare CR or LF to CRLF. Also add tests for the APPEND command, including an email message round-trip. Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> Co-authored-by: harjoth <[email protected]> files: A Misc/NEWS.d/next/Library/2026-07-01-14-00-00.gh-issue-49680.Lm5rQv.rst M Doc/library/imaplib.rst M Lib/imaplib.py M Lib/test/test_imaplib.py diff --git a/Doc/library/imaplib.rst b/Doc/library/imaplib.rst index 4a714652ecf5d2..db17f6b79a7c50 100644 --- a/Doc/library/imaplib.rst +++ b/Doc/library/imaplib.rst @@ -202,7 +202,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. @@ -211,6 +211,17 @@ 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. + 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. + .. method:: IMAP4.authenticate(mechanism, authobject) diff --git a/Lib/imaplib.py b/Lib/imaplib.py index ca2ae40726b2d1..239218bc96aeb4 100644 --- a/Lib/imaplib.py +++ b/Lib/imaplib.py @@ -487,12 +487,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]) = <instance>.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: @@ -506,8 +511,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 e797a625014603..3a3f62ef1ea6ab 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 @@ -89,6 +91,18 @@ class Handler(SimpleIMAPHandler): return Handler +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): @@ -474,10 +488,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') @@ -737,6 +749,33 @@ 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 are normalized to CRLF; False sends the + # literal exactly (gh-49680). + class AppendHandler(SimpleIMAPHandler): + def cmd_APPEND(self, tag, args): + self.server.response = _read_literal(self, args[-1]) + yield # read the trailer line + self._send_tagged(tag, 'OK', 'APPEND completed') + 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_login_capabilities(self): # A server may advertise new capabilities after login (as an # untagged CAPABILITY response); imaplib must refresh its cached @@ -1673,10 +1712,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') 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 00000000000000..6371df69e69077 --- /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. _______________________________________________ Python-checkins mailing list -- [email protected] To unsubscribe send an email to [email protected] https://mail.python.org/mailman3//lists/python-checkins.python.org Member address: [email protected]
