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
14 changes: 14 additions & 0 deletions Doc/library/imaplib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,20 @@ An :class:`IMAP4` instance has the following methods:

The following attributes are defined on instances of :class:`IMAP4`:

.. attribute:: IMAP4.capabilities

A tuple of the capabilities advertised by the server, in upper case.

It is set when the connection is established,
and refreshed after a successful :meth:`~IMAP4.login`,
:meth:`~IMAP4.authenticate` or :meth:`~IMAP4.starttls`,
because the server can advertise different capabilities
in different connection states.

.. versionchanged:: 3.14.7
Refreshed after :meth:`~IMAP4.login` and :meth:`~IMAP4.authenticate`.


.. attribute:: IMAP4.PROTOCOL_VERSION

The most recent supported protocol in the ``CAPABILITY`` response from the
Expand Down
11 changes: 8 additions & 3 deletions Lib/imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,8 @@ def _connect(self):
self._encoding, 'replace')
raise self.error('invalid greeting: ' + greeting)

self._refresh_capabilities()
# The greeting is not a response to a command.
self._refresh_capabilities(consume=True)
if __debug__:
if self.debug >= 3:
self._mesg('CAPABILITIES: %r' % (self.capabilities,))
Expand Down Expand Up @@ -1509,10 +1510,14 @@ def _get_capabilities(self):
self.capabilities = tuple(dat.split())


def _refresh_capabilities(self):
def _refresh_capabilities(self, consume=False):
# Use a CAPABILITY response sent by the server, or ask for it.
# Unless it is consumed, the response can still be read with
# response('CAPABILITY').
if 'CAPABILITY' in self.untagged_responses:
dat = self.untagged_responses.pop('CAPABILITY')[-1]
dat = self.untagged_responses['CAPABILITY'][-1]
if consume:
del self.untagged_responses['CAPABILITY']
self.capabilities = tuple(str(dat, self._encoding).upper().split())
else:
self._get_capabilities()
Expand Down
6 changes: 6 additions & 0 deletions Lib/test/test_imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,8 @@ def cmd_ENABLE(self, tag, args):
client.login('user', 'pass')
self.assertIn('ENABLE', client.capabilities)
self.assertIn('UTF8=ACCEPT', client.capabilities)
self.assertEqual(client.response('CAPABILITY'),
('CAPABILITY', [b'IMAP4rev1 ENABLE UTF8=ACCEPT']))
typ, _ = client.enable('UTF8=ACCEPT')
self.assertEqual(typ, 'OK')

Expand All @@ -1087,6 +1089,8 @@ def cmd_AUTHENTICATE(self, tag, args):
self.assertNotIn('ENABLE', client.capabilities)
client.authenticate('MYAUTH', lambda x: b'fake')
self.assertIn('ENABLE', client.capabilities)
self.assertEqual(client.response('CAPABILITY'),
('CAPABILITY', [b'IMAP4rev1 ENABLE']))

def test_greeting_capabilities(self):
# Capabilities advertised in the greeting are used directly,
Expand All @@ -1100,6 +1104,8 @@ def cmd_CAPABILITY(self, tag, args):
client, server = self._setup(GreetingHandler)
self.assertEqual(client.capabilities, ('IMAP4REV1', 'ENABLE'))
self.assertFalse(getattr(server, 'capability_queried', False))
# The greeting is not a response to a command, so it is consumed.
self.assertEqual(client.response('CAPABILITY'), ('CAPABILITY', [None]))

def test_login_requery_capabilities(self):
# If the server does not advertise capabilities after login,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix a regression in :mod:`imaplib` introduced in the fix for :gh:`63121`:
refreshing the capabilities after a successful :meth:`~imaplib.IMAP4.login`
or :meth:`~imaplib.IMAP4.authenticate` consumed the ``CAPABILITY`` response,
so it could no longer be read with :meth:`~imaplib.IMAP4.response`.
Loading