Skip to content
Open
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
34 changes: 22 additions & 12 deletions Doc/library/imaplib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@

This module defines three classes, :class:`IMAP4`, :class:`IMAP4_SSL` and
:class:`IMAP4_stream`, which encapsulate a connection to an IMAP4 server and
implement a large subset of the IMAP4rev1 client protocol as defined in
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.

Retain the mention to RFC 2060 and add mention to RFC 9051 for rev2.

:rfc:`2060`. It is backward compatible with IMAP4 (:rfc:`1730`) servers, but
note that the ``STATUS`` command is not supported in IMAP4.
implement IMAP4rev1 and IMAP4rev2 client protocol features. It is backward
compatible with IMAP4 (:rfc:`1730`) servers, but note that the ``STATUS``
command is not supported in IMAP4.

.. include:: ../includes/wasm-notavail.rst

Expand All @@ -28,7 +28,7 @@ base class:
.. class:: IMAP4(host='', port=IMAP4_PORT, timeout=None)

This class implements the actual IMAP4 protocol. The connection is created and
protocol version (IMAP4 or IMAP4rev1) is determined when the instance is
protocol version (IMAP4, IMAP4rev1, or IMAP4rev2) is determined when the instance is
initialized. If *host* is not specified, ``''`` (the local host) is used. If
*port* is omitted, the standard IMAP4 port (143) is used. The optional *timeout*
parameter specifies a timeout in seconds for the connection attempt.
Expand Down Expand Up @@ -169,8 +169,8 @@ example of usage.
IMAP4 Objects
-------------

All IMAP4rev1 commands are represented by methods of the same name, either
uppercase or lowercase.
All IMAP4rev2 and IMAP4rev1 commands are represented by methods of the
same name, either uppercase or lowercase.

All arguments to commands are converted to strings, except for ``AUTHENTICATE``,
and the last argument to ``APPEND`` which is passed as an IMAP4 literal. If
Expand Down Expand Up @@ -260,8 +260,10 @@ An :class:`IMAP4` instance has the following methods:
.. method:: IMAP4.enable(capability)

Enable *capability* (see :rfc:`5161`). Most capabilities do not need to be
enabled. Currently only the ``UTF8=ACCEPT`` capability is supported
(see :RFC:`6855`).
enabled. ``UTF8=ACCEPT`` has special client-side handling
(see :RFC:`6855`). On servers that advertise both ``IMAP4rev1`` and
``IMAP4rev2``, ``ENABLE IMAP4REV2`` switches the connection from its
initial IMAP4rev1-compatible mode into IMAP4rev2 mode.

.. versionadded:: 3.5
The :meth:`enable` method itself, and :RFC:`6855` support.
Expand Down Expand Up @@ -507,7 +509,9 @@ An :class:`IMAP4` instance has the following methods:
protocol requires that at least one criterion be specified; an exception will be
raised when the server returns an error. *charset* must be ``None`` if
the ``UTF8=ACCEPT`` capability was enabled using the :meth:`enable`
command.
command; :rfc:`6855` requires this once UTF-8 support has been enabled.
In IMAP4rev2 mode (:rfc:`9051`), UTF-8 is already in use, so specifying an explicit
``CHARSET`` such as ``UTF-8`` is redundant, which implies that *charset* need not be ``None``.

Example::

Expand All @@ -524,6 +528,11 @@ An :class:`IMAP4` instance has the following methods:
(``EXISTS`` response). The default *mailbox* is ``'INBOX'``. If the *readonly*
flag is set, modifications to the mailbox are not allowed.

With the :rfc:`6855` ``UTF8=ACCEPT`` extension, the server can open
mailboxes containing internationalized messages with ``SELECT`` and
``EXAMINE``. In IMAP4rev2 mode, UTF-8 support is part of the protocol
instead of being enabled separately.


.. method:: IMAP4.send(data)

Expand Down Expand Up @@ -681,8 +690,9 @@ The following attributes are defined on instances of :class:`IMAP4`:

.. attribute:: IMAP4.PROTOCOL_VERSION

The most recent supported protocol in the ``CAPABILITY`` response from the
server.
The protocol version currently in use for the connection. If the server
advertises both ``IMAP4rev1`` and ``IMAP4rev2``, the connection starts in
``IMAP4rev1`` mode until ``ENABLE IMAP4REV2`` is issued successfully.


.. attribute:: IMAP4.debug
Expand All @@ -695,7 +705,7 @@ The following attributes are defined on instances of :class:`IMAP4`:

Boolean value that is normally ``False``, but is set to ``True`` if an
:meth:`enable` command is successfully issued for the ``UTF8=ACCEPT``
capability.
capability, and also when the connection enters IMAP4rev2 mode.

.. versionadded:: 3.5

Expand Down
69 changes: 60 additions & 9 deletions Lib/imaplib.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""IMAP4 client.
Based on RFC 2060.
Based on RFC 2060 and updated for RFC 9051.
Public class: IMAP4
Public variable: Debug
Expand Down Expand Up @@ -40,7 +40,11 @@
Debug = 0
IMAP4_PORT = 143
IMAP4_SSL_PORT = 993
AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
AllowedVersions = ('IMAP4REV2', 'IMAP4REV1', 'IMAP4') # Most recent first
IMAP4REV2_BUILTIN_COMMANDS = (
'NAMESPACE', 'UNSELECT', 'UIDPLUS', 'ESEARCH', 'SEARCHRES',
'ENABLE', 'IDLE', 'SASL-IR', 'LIST-EXTENDED', 'LIST-STATUS',
'MOVE', 'LITERAL+') # RFC 9051 Appendix E.2

# Maximal line length when calling readline(). This is to prevent
# reading arbitrary length lines. RFC 3501 and 2060 (IMAP 4rev1)
Expand Down Expand Up @@ -144,7 +148,7 @@ class IMAP4:
If timeout is not given or is None,
the global default socket timeout is used
All IMAP4rev1 commands are supported by methods of the same
All IMAP4rev2 and IMAP4rev1 commands are supported by methods of the same
name (in lowercase).
All arguments to commands are converted to strings, except for
Expand Down Expand Up @@ -198,6 +202,7 @@ def __init__(self, host='', port=IMAP4_PORT, timeout=None):
self.is_readonly = False # READ-ONLY desired state
self.tagnum = 0
self._tls_established = False
self.PROTOCOL_VERSION = None
self._mode_ascii()
self._readbuf = []

Expand Down Expand Up @@ -260,14 +265,59 @@ def _connect(self):
if self.debug >= 3:
self._mesg('CAPABILITIES: %r' % (self.capabilities,))

self._set_protocol_version()


def _set_protocol_version(self):

if self._should_use_rev1_mode_for_dual_support():
self._activate_rev1_mode()
return

for version in AllowedVersions:
if not version in self.capabilities:
continue
self.PROTOCOL_VERSION = version
if version == 'IMAP4REV2':
self._activate_rev2_mode()
else:
self.PROTOCOL_VERSION = version
return

raise self.error('server not IMAP4 compliant')

def _supports_rev2(self):
return 'IMAP4REV2' in self.capabilities

def _supports_dual_rev1_rev2(self):
return self._supports_rev2() and 'IMAP4REV1' in self.capabilities

def _is_using_rev2(self):
return self.PROTOCOL_VERSION == 'IMAP4REV2'

def _is_capability_available(self, capability):
capability = capability.upper()
if self._is_using_rev2() and capability in IMAP4REV2_BUILTIN_COMMANDS:
return True
return capability in self.capabilities

def _should_use_rev1_mode_for_dual_support(self):
return self._supports_dual_rev1_rev2()

def _activate_rev1_mode(self):
self.PROTOCOL_VERSION = 'IMAP4REV1'
self._mode_ascii()

def _activate_rev2_mode(self):
self.PROTOCOL_VERSION = 'IMAP4REV2'
self._mode_utf8()

def _handle_enable_success(self, capability):
capability = capability.upper()
if 'UTF8=ACCEPT' in capability:
self._mode_utf8()
if 'IMAP4REV2' in capability:
self._activate_rev2_mode()


def __getattr__(self, attr):
# Allow UPPERCASE variants of IMAP4 command methods.
Expand Down Expand Up @@ -603,11 +653,11 @@ def enable(self, capability):
(typ, [data]) = <instance>.enable(capability)
"""
if 'ENABLE' not in self.capabilities:
if not self._is_capability_available('ENABLE'):
raise IMAP4.error("Server does not support ENABLE")
typ, data = self._simple_command('ENABLE', capability)
if typ == 'OK' and 'UTF8=ACCEPT' in capability.upper():
self._mode_utf8()
if typ == 'OK':
self._handle_enable_success(capability)
return typ, data

def expunge(self):
Expand Down Expand Up @@ -838,7 +888,7 @@ def search(self, charset, *criteria):
"""
name = 'SEARCH'
if charset:
if self.utf8_enabled:
if self.utf8_enabled and not self._is_using_rev2():
raise IMAP4.error("Non-None charset not valid in UTF8 mode")
typ, dat = self._simple_command(name, 'CHARSET', charset, *criteria)
else:
Expand Down Expand Up @@ -936,6 +986,7 @@ def starttls(self, ssl_context=None):
self._imaplib_file = self.sock.makefile('rb')
self._tls_established = True
self._get_capabilities()
self._set_protocol_version()
else:
raise self.error("Couldn't establish TLS session")
return self._untagged_response(typ, dat, name)
Expand Down Expand Up @@ -1439,7 +1490,7 @@ class Idler:
"""

def __init__(self, imap, duration=None):
if 'IDLE' not in imap.capabilities:
if not imap._is_capability_available('IDLE'):
raise imap.error("Server does not support IMAP4 IDLE")
if duration is not None and not imap.sock:
# IMAP4_stream pipes don't support timeouts
Expand Down
62 changes: 57 additions & 5 deletions Lib/test/test_imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ class SimpleIMAPHandler(socketserver.StreamRequestHandler):
timeout = support.LOOPBACK_TIMEOUT
continuation = None
capabilities = ''
welcome = 'IMAP4rev1'
protocol_capabilities = 'IMAP4rev1'

def setup(self):
super().setup()
Expand All @@ -136,9 +138,9 @@ def _send_textline(self, message):
def _send_tagged(self, tag, code, message):
self._send_textline(' '.join((tag, code, message)))

def handle(self):
def handle(self):
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.

Revert this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

sorry, will fix this. I was lot on this function after creating tests to understand how handle of the server calls the commands in test cases and was doing find on it so accidentally added space.

# Send a welcome message.
self._send_textline('* OK IMAP4rev1')
self._send_textline(f'* OK {self.welcome}')
while 1:
# Gather up input until we receive a line terminator or we timeout.
# Accumulate read(1) because it's simpler to handle the differences
Expand Down Expand Up @@ -179,9 +181,7 @@ def handle(self):
self._send_tagged(tag, 'BAD', cmd + ' unknown')

def cmd_CAPABILITY(self, tag, args):
caps = ('IMAP4rev1 ' + self.capabilities
if self.capabilities
else 'IMAP4rev1')
caps = ' '.join(filter(None, (self.protocol_capabilities, self.capabilities)))
self._send_textline('* CAPABILITY ' + caps)
self._send_tagged(tag, 'OK', 'CAPABILITY completed')

Expand Down Expand Up @@ -213,6 +213,11 @@ def cmd_IDLE(self, tag, args):
self._send_tagged(tag, 'NO', 'IDLE is not allowed at this time')


class SimpleIMAPRev2Handler(SimpleIMAPHandler):
welcome = 'IMAP4rev2'
protocol_capabilities = 'IMAP4rev2'


class IdleCmdHandler(SimpleIMAPHandler):
capabilities = 'IDLE'
def cmd_IDLE(self, tag, args):
Expand Down Expand Up @@ -395,6 +400,53 @@ def cmd_APPEND(self, tag, args):
'(~{25}', ('%s\r\n' % msg_string).encode('utf-8'),
b')\r\n' ])

def test_imap4rev2_enables_utf8_mode(self):
client, _ = self._setup(SimpleIMAPRev2Handler)
self.assertEqual(client.PROTOCOL_VERSION, 'IMAP4REV2')
self.assertTrue(client.utf8_enabled)
self.assertEqual(client._encoding, 'utf-8')


def test_imap4rev2_enable_works_without_ENABLE_capability(self):
class IMAP4Rev2EnableServer(SimpleIMAPRev2Handler):
capabilities = ''
def cmd_ENABLE(self, tag, args):
self.server.response = args
self._send_textline('* ENABLED ' + ' '.join(args))
self._send_tagged(tag, 'OK', 'ENABLE successful')
client, server = self._setup(IMAP4Rev2EnableServer)
typ, _ = client.login('user', 'pass')
self.assertEqual(typ, 'OK')
typ, _ = client.enable('CONDSTORE')
self.assertEqual(typ, 'OK')
self.assertEqual(server.response, ['CONDSTORE'])

def test_imap4rev2_idle_works_without_IDLE_capability(self):
class IMAP4Rev2IdleServer(SimpleIMAPRev2Handler):
capabilities = ''
def cmd_IDLE(self, tag, args):
self._send_textline('+ idling')
r = yield
if r == b'DONE\r\n':
self._send_tagged(tag, 'OK', 'IDLE completed')
else:
self._send_tagged(tag, 'BAD', 'Expected DONE')
client, _ = self._setup(IMAP4Rev2IdleServer)
typ, _ = client.login('user', 'pass')
self.assertEqual(typ, 'OK')
with client.idle(duration=0.01):
pass

def test_dual_imap4rev1_imap4rev2_starts_in_rev1_mode(self):
class DualProtocolServer(SimpleIMAPHandler):
welcome = 'IMAP4rev2'
protocol_capabilities = 'IMAP4rev2 IMAP4rev1'

client, _ = self._setup(DualProtocolServer)
self.assertEqual(client.PROTOCOL_VERSION, 'IMAP4REV1')
self.assertFalse(client.utf8_enabled)
self.assertEqual(client._encoding, 'ascii')

def test_search_disallows_charset_in_utf8_mode(self):
class UTF8Server(SimpleIMAPHandler):
capabilities = 'AUTH ENABLE UTF8=ACCEPT'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Update the :mod:`imaplib` module to support IMAP4rev2 following :rfc:`9501`, with backward compatibility with IMAP4rev1
(outlined in :rfc:`9051#appendix-A`) and IMAP4rev1 extensions folded into IMAP4rev2 (outlined in :rfc:`9051#appendix-E`).
Loading