Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion Doc/library/imaplib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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)

Expand Down
12 changes: 9 additions & 3 deletions Lib/imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)


Expand Down
49 changes: 43 additions & 6 deletions Lib/test/test_imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
from test.support import socket_helper

from contextlib import contextmanager
from email.message import EmailMessage
import imaplib
import os.path
import socketserver
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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Loading