From: Serhiy Storchaka Date: Thu, 2 Jul 2026 11:17:37 +0000 (+0300) Subject: gh-49680: Add translate_line_endings parameter to imaplib.IMAP4.append (GH-152775) X-Git-Url: http://git.ipfire.org/gitweb/index.cgi?a=commitdiff_plain;h=ddb6539c4976b18895bb5c5cd37d197c60fb459d;p=thirdparty%2FPython%2Fcpython.git 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) Co-authored-by: harjoth --- diff --git a/Doc/library/imaplib.rst b/Doc/library/imaplib.rst index 4a714652ecf5..db17f6b79a7c 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 ca2ae40726b2..239218bc96ae 100644 --- a/Lib/imaplib.py +++ b/Lib/imaplib.py @@ -487,12 +487,17 @@ class IMAP4: # 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: @@ -506,8 +511,9 @@ class IMAP4: 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 e797a6250146..3a3f62ef1ea6 100644 --- a/Lib/test/test_imaplib.py +++ b/Lib/test/test_imaplib.py @@ -2,6 +2,7 @@ from test import support 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 time 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 @@ def make_simple_handler(command, untagged_response=(), 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 @@ class NewIMAPTestsMixin: 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 @@ class NewIMAPTestsMixin: 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 @@ class ThreadedNetworkedTests(unittest.TestCase): 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 000000000000..6371df69e690 --- /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.