diff --git a/Doc/library/imaplib.rst b/Doc/library/imaplib.rst index 910b0f00c0e7de..745683673ccf50 100644 --- a/Doc/library/imaplib.rst +++ b/Doc/library/imaplib.rst @@ -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 diff --git a/Lib/imaplib.py b/Lib/imaplib.py index 24d3a27f21d2d1..977d6bdb1c3d86 100644 --- a/Lib/imaplib.py +++ b/Lib/imaplib.py @@ -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,)) @@ -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() diff --git a/Lib/test/test_imaplib.py b/Lib/test/test_imaplib.py index d97da398803681..da63da54e8ef73 100644 --- a/Lib/test/test_imaplib.py +++ b/Lib/test/test_imaplib.py @@ -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') @@ -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, @@ -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, diff --git a/Misc/NEWS.d/next/Library/2026-08-14-14-10-00.gh-issue-155775.Kq3vTx.rst b/Misc/NEWS.d/next/Library/2026-08-14-14-10-00.gh-issue-155775.Kq3vTx.rst new file mode 100644 index 00000000000000..309421ae08240f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-14-14-10-00.gh-issue-155775.Kq3vTx.rst @@ -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`.