]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-49680: Add translate_line_endings parameter to imaplib.IMAP4.append (GH-152775)
authorSerhiy Storchaka <storchaka@gmail.com>
Thu, 2 Jul 2026 11:17:37 +0000 (14:17 +0300)
committerGitHub <noreply@github.com>
Thu, 2 Jul 2026 11:17:37 +0000 (11:17 +0000)
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) <noreply@anthropic.com>
Co-authored-by: harjoth <harjoth.khara@gmail.com>
Doc/library/imaplib.rst
Lib/imaplib.py
Lib/test/test_imaplib.py
Misc/NEWS.d/next/Library/2026-07-01-14-00-00.gh-issue-49680.Lm5rQv.rst [new file with mode: 0644]

index 4a714652ecf5d203a1ec417e555e51e7d32d3ae7..db17f6b79a7c50d509231eea30e2f3b92822fbea 100644 (file)
@@ -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)
 
index ca2ae40726b2d1275031ccaa3d9bac77bafb0dd5..239218bc96aeb4f5ac2916305d4cca372e8e0c95 100644 (file)
@@ -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]) = <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 @@ 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)
 
 
index e797a625014603e7b0e1781332640eb9fc5f4dd7..3a3f62ef1ea6abb35756a048622163e60d2ade9e 100644 (file)
@@ -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 (file)
index 0000000..6371df6
--- /dev/null
@@ -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.