Skip to content
Closed
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
10 changes: 10 additions & 0 deletions Doc/library/imaplib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,21 @@ An :class:`IMAP4` instance has the following methods:

Append *message* to named mailbox.

*message* is sent verbatim, without any line-ending conversion.
:rfc:`3501` requires messages to use CRLF line endings, so callers must
ensure this themselves; for example, serialize :mod:`email` messages using
:class:`email.policy.SMTP` (which emits CRLF) rather than the default
policy.

*flags* may be ``None`` or a string of IMAP flag tokens. Multiple
flags are separated by spaces, for example ``r'\Seen \Answered'``.
If *flags* is not already enclosed in parentheses, parentheses are
added automatically.

.. versionchanged:: 3.16

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.. versionchanged:: 3.16
.. versionchanged:: next

*message* is now sent verbatim. Previously, bare CR or LF characters
were rewritten to CRLF.


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

Expand Down
3 changes: 1 addition & 2 deletions Lib/imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,8 +502,7 @@ 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
self.literal = message
return self._simple_command(name, mailbox, flags, date_time)


Expand Down
110 changes: 94 additions & 16 deletions Lib/test/test_imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,6 +28,25 @@
CAFILE = os.path.join(os.path.dirname(__file__) or os.curdir, "certdata", "pycacert.pem")


def _read_append_literal(handler, args):
literal = args[-1]
trailer = b'\r\n'
if literal.startswith('('):
literal = literal[1:]
trailer = b')\r\n'
if literal.startswith('~'):
literal = literal[1:]
match = re.fullmatch(r'\{(\d+)\}', literal)
if match is None:
raise AssertionError(f"unexpected APPEND literal marker: {args[-1]!r}")
literal_size = int(match.group(1))

handler._send_textline('+')
payload = handler.rfile.read(literal_size)
received_trailer = handler.rfile.read(len(trailer))
return payload, received_trailer


class TestImaplib(unittest.TestCase):

def test_Internaldate2tuple(self):
Expand Down Expand Up @@ -371,12 +391,9 @@ 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)
literal = yield
self.server.response.append(literal)
payload, trailer = _read_append_literal(self, args)
self.server.response.extend([payload, trailer])
self._send_tagged(tag, 'OK', 'okay')
client, server = self._setup(UTF8AppendServer)
self.assertEqual(client._encoding, 'ascii')
Expand All @@ -387,14 +404,45 @@ def cmd_APPEND(self, tag, args):
self.assertEqual(code, 'OK')
self.assertEqual(client._encoding, 'utf-8')
msg_string = 'Subject: üñí©öðé'
typ, data = client.append(
None, None, None, (msg_string + '\n').encode('utf-8'))
msg = (msg_string + '\n').encode('utf-8')
self.assertEqual(len(msg), 24)
typ, data = client.append(None, None, None, msg)
self.assertEqual(typ, 'OK')
self.assertEqual(server.response,
['INBOX', 'UTF8',
'(~{25}', ('%s\r\n' % msg_string).encode('utf-8'),
'(~{24}', msg,
b')\r\n' ])

def test_append_preserves_message_line_endings(self):
class AppendServer(SimpleIMAPHandler):
def cmd_APPEND(self, tag, args):
payload, trailer = _read_append_literal(self, args)
self.server.responses.append(args + [payload, trailer])
self._send_tagged(tag, 'OK', 'okay')

client, server = self._setup(AppendServer)
server.responses = []
typ, _ = client.login('user', 'pass')
self.assertEqual(typ, 'OK')

msg = b"one\ntwo\rthree\r\nfour"
self.assertEqual(len(msg), 19)
typ, _ = client.append(None, None, None, msg)
self.assertEqual(typ, 'OK')
self.assertEqual(server.responses[-1],
['INBOX', '{19}', msg, b'\r\n'])

email = EmailMessage()
email['Subject'] = 'line endings'
email.set_content('body line\n')
msg = email.as_bytes()
self.assertIn(b'\n', msg)
self.assertNotIn(b'\r\n', msg)
typ, _ = client.append(None, None, None, msg)
self.assertEqual(typ, 'OK')
self.assertEqual(server.responses[-1],
['INBOX', f'{{{len(msg)}}}', msg, b'\r\n'])

def test_search_disallows_charset_in_utf8_mode(self):
class UTF8Server(SimpleIMAPHandler):
capabilities = 'AUTH ENABLE UTF8=ACCEPT'
Expand Down Expand Up @@ -925,12 +973,9 @@ 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)
literal = yield
self.server.response.append(literal)
payload, trailer = _read_append_literal(self, args)
self.server.response.extend([payload, trailer])
self._send_tagged(tag, 'OK', 'okay')

with self.reaped_pair(UTF8AppendServer) as (server, client):
Expand All @@ -943,14 +988,47 @@ def cmd_APPEND(self, tag, args):
self.assertEqual(code, 'OK')
self.assertEqual(client._encoding, 'utf-8')
msg_string = 'Subject: üñí©öðé'
typ, data = client.append(
None, None, None, (msg_string + '\n').encode('utf-8'))
msg = (msg_string + '\n').encode('utf-8')
self.assertEqual(len(msg), 24)
typ, data = client.append(None, None, None, msg)
self.assertEqual(typ, 'OK')
self.assertEqual(server.response,
['INBOX', 'UTF8',
'(~{25}', ('%s\r\n' % msg_string).encode('utf-8'),
'(~{24}', msg,
b')\r\n' ])

@threading_helper.reap_threads
def test_append_preserves_message_line_endings(self):

class AppendServer(SimpleIMAPHandler):
def cmd_APPEND(self, tag, args):
payload, trailer = _read_append_literal(self, args)
self.server.responses.append(args + [payload, trailer])
self._send_tagged(tag, 'OK', 'okay')

with self.reaped_pair(AppendServer) as (server, client):
server.responses = []
typ, _ = client.login('user', 'pass')
self.assertEqual(typ, 'OK')

msg = b"one\ntwo\rthree\r\nfour"
self.assertEqual(len(msg), 19)
typ, _ = client.append(None, None, None, msg)
self.assertEqual(typ, 'OK')
self.assertEqual(server.responses[-1],
['INBOX', '{19}', msg, b'\r\n'])

email = EmailMessage()
email['Subject'] = 'line endings'
email.set_content('body line\n')
msg = email.as_bytes()
self.assertIn(b'\n', msg)
self.assertNotIn(b'\r\n', msg)
typ, _ = client.append(None, None, None, msg)
self.assertEqual(typ, 'OK')
self.assertEqual(server.responses[-1],
['INBOX', f'{{{len(msg)}}}', msg, b'\r\n'])

# XXX also need a test that makes sure that the Literal and Untagged_status
# regexes uses unicode in UTF8 mode instead of the default ASCII.

Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,7 @@ Robert Kern
Jim Kerr
Magnus Kessler
Lawrence Kesteloot
Harjoth Khara
Garvit Khatri
Vivek Khera
Dhiru Kholia
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
:meth:`imaplib.IMAP4.append` now sends the message verbatim instead of
rewriting bare CR or LF bytes to CRLF. Since :rfc:`3501` requires messages to
use CRLF line endings, callers must now supply them themselves; for example,
serialize :mod:`email` messages with :class:`email.policy.SMTP` (which emits
CRLF) rather than the default policy.
Loading