Skip to content

Commit ddb6539

Browse files
serhiy-storchakaclaudeharjothkhara
authored
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) <noreply@anthropic.com> Co-authored-by: harjoth <harjoth.khara@gmail.com>
1 parent a50b089 commit ddb6539

4 files changed

Lines changed: 68 additions & 10 deletions

File tree

Doc/library/imaplib.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ upper bound (``'3:*'``).
202202
An :class:`IMAP4` instance has the following methods:
203203

204204

205-
.. method:: IMAP4.append(mailbox, flags, date_time, message)
205+
.. method:: IMAP4.append(mailbox, flags, date_time, message, *, translate_line_endings=True)
206206

207207
Append *message* to named mailbox.
208208

@@ -211,6 +211,17 @@ An :class:`IMAP4` instance has the following methods:
211211
If *flags* is not already enclosed in parentheses, parentheses are
212212
added automatically.
213213

214+
If *translate_line_endings* is true (the default),
215+
line endings in *message* are translated to CRLF.
216+
Pass ``False`` to send the message literal exactly as given,
217+
which is required to preserve messages that contain bare CR or LF.
218+
In that case *message* must already use CRLF line endings as required
219+
by :rfc:`3501`; for example, serialize :mod:`email` messages using
220+
:class:`email.policy.SMTP`.
221+
222+
.. versionchanged:: next
223+
Added the *translate_line_endings* parameter.
224+
214225

215226
.. method:: IMAP4.authenticate(mechanism, authobject)
216227

Lib/imaplib.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -487,12 +487,17 @@ def response(self, code):
487487
# IMAP4 commands
488488

489489

490-
def append(self, mailbox, flags, date_time, message):
490+
def append(self, mailbox, flags, date_time, message, *,
491+
translate_line_endings=True):
491492
"""Append message to named mailbox.
492493
493494
(typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
494495
495496
All args except 'message' can be None.
497+
498+
If 'translate_line_endings' is true (the default), line endings in
499+
'message' are translated to CRLF. Pass false to send the message
500+
literal exactly as given.
496501
"""
497502
name = 'APPEND'
498503
if not mailbox:
@@ -506,8 +511,9 @@ def append(self, mailbox, flags, date_time, message):
506511
date_time = Time2Internaldate(date_time)
507512
else:
508513
date_time = None
509-
literal = MapCRLF.sub(CRLF, message)
510-
self.literal = literal
514+
if translate_line_endings:
515+
message = MapCRLF.sub(CRLF, message)
516+
self.literal = message
511517
return self._simple_command(name, mailbox, flags, date_time)
512518

513519

Lib/test/test_imaplib.py

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
from test.support import socket_helper
33

44
from contextlib import contextmanager
5+
from email.message import EmailMessage
56
import imaplib
67
import os.path
78
import socketserver
89
import time
910
import calendar
1011
import threading
1112
import re
13+
import select
1214
import socket
1315

1416
from test.support import verbose, run_with_tz, run_with_locale, cpython_only
@@ -89,6 +91,18 @@ class Handler(SimpleIMAPHandler):
8991
return Handler
9092

9193

94+
def _read_literal(handler, marker):
95+
# Read one literal, a raw octet sequence, by its count from the marker
96+
# ('{N}', or '(~{N}' in UTF8 mode).
97+
size = int(re.search(r'\{(\d+)\}', marker).group(1))
98+
# The client must wait for the continuation, so nothing should be readable.
99+
if select.select([handler.connection], [], [], 0)[0]:
100+
raise AssertionError('client sent the literal before the '
101+
'continuation request')
102+
handler._send_textline('+')
103+
return handler.rfile.read(size)
104+
105+
92106
class TestImaplib(unittest.TestCase):
93107

94108
def test_Internaldate2tuple(self):
@@ -474,10 +488,8 @@ def cmd_AUTHENTICATE(self, tag, args):
474488
self.server.response = yield
475489
self._send_tagged(tag, 'OK', 'FAKEAUTH successful')
476490
def cmd_APPEND(self, tag, args):
477-
self._send_textline('+')
478491
self.server.response = args
479-
literal = yield
480-
self.server.response.append(literal)
492+
self.server.response.append(_read_literal(self, args[-1]))
481493
literal = yield
482494
self.server.response.append(literal)
483495
self._send_tagged(tag, 'OK', 'okay')
@@ -737,6 +749,33 @@ def test_login(self):
737749
self.assertEqual(data[0], b'LOGIN completed')
738750
self.assertEqual(client.state, 'AUTH')
739751

752+
def test_append_translate_line_endings(self):
753+
# By default line endings are normalized to CRLF; False sends the
754+
# literal exactly (gh-49680).
755+
class AppendHandler(SimpleIMAPHandler):
756+
def cmd_APPEND(self, tag, args):
757+
self.server.response = _read_literal(self, args[-1])
758+
yield # read the trailer line
759+
self._send_tagged(tag, 'OK', 'APPEND completed')
760+
client, server = self._setup(AppendHandler)
761+
client.login('user', 'pass')
762+
message = b'a\rb\nc\r\nd'
763+
client.append('INBOX', None, None, message)
764+
self.assertEqual(server.response, b'a\r\nb\r\nc\r\nd')
765+
client.append('INBOX', None, None, message,
766+
translate_line_endings=False)
767+
self.assertEqual(server.response, message)
768+
769+
# An email message uses bare LF by default; False sends it verbatim.
770+
message = EmailMessage()
771+
message['Subject'] = 'line endings'
772+
message.set_content('body line\n')
773+
message = message.as_bytes()
774+
self.assertNotIn(b'\r\n', message)
775+
client.append('INBOX', None, None, message,
776+
translate_line_endings=False)
777+
self.assertEqual(server.response, message)
778+
740779
def test_login_capabilities(self):
741780
# A server may advertise new capabilities after login (as an
742781
# untagged CAPABILITY response); imaplib must refresh its cached
@@ -1673,10 +1712,8 @@ def test_enable_UTF8_True_append(self):
16731712

16741713
class UTF8AppendServer(self.UTF8Server):
16751714
def cmd_APPEND(self, tag, args):
1676-
self._send_textline('+')
16771715
self.server.response = args
1678-
literal = yield
1679-
self.server.response.append(literal)
1716+
self.server.response.append(_read_literal(self, args[-1]))
16801717
literal = yield
16811718
self.server.response.append(literal)
16821719
self._send_tagged(tag, 'OK', 'okay')
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add the *translate_line_endings* parameter to :meth:`imaplib.IMAP4.append`.
2+
By default line endings in the message are translated to CRLF, as before;
3+
passing ``False`` sends the message literal exactly as given, preserving
4+
bare CR or LF octets.

0 commit comments

Comments
 (0)