diff --git a/Lib/imaplib.py b/Lib/imaplib.py index 497b5a60cecb083..26773fb0ee083be 100644 --- a/Lib/imaplib.py +++ b/Lib/imaplib.py @@ -255,7 +255,7 @@ def _connect(self): else: raise self.error(self.welcome) - self._get_capabilities() + self._refresh_capabilities() if __debug__: if self.debug >= 3: self._mesg('CAPABILITIES: %r' % (self.capabilities,)) @@ -533,6 +533,7 @@ def authenticate(self, mechanism, authobject): if typ != 'OK': raise self.error(dat[-1].decode('utf-8', 'replace')) self.state = 'AUTH' + self._refresh_capabilities() return typ, dat @@ -714,6 +715,7 @@ def login(self, user, password): if typ != 'OK': raise self.error(dat[-1].decode('UTF-8', 'replace')) self.state = 'AUTH' + self._refresh_capabilities() return typ, dat @@ -1201,6 +1203,15 @@ def _get_capabilities(self): self.capabilities = tuple(dat.split()) + def _refresh_capabilities(self): + # Use a CAPABILITY response sent by the server, or ask for it. + if 'CAPABILITY' in self.untagged_responses: + dat = self.untagged_responses.pop('CAPABILITY')[-1] + self.capabilities = tuple(str(dat, self._encoding).upper().split()) + else: + self._get_capabilities() + + def _get_response(self, start_timeout=False): # Read response and store. diff --git a/Lib/test/test_imaplib.py b/Lib/test/test_imaplib.py index fb256fb7cbcd344..fa23d09806e6fb1 100644 --- a/Lib/test/test_imaplib.py +++ b/Lib/test/test_imaplib.py @@ -136,9 +136,11 @@ def _send_textline(self, message): def _send_tagged(self, tag, code, message): self._send_textline(' '.join((tag, code, message))) + welcome = '* OK IMAP4rev1' + def handle(self): # Send a welcome message. - self._send_textline('* OK IMAP4rev1') + self._send_textline(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 @@ -634,6 +636,71 @@ def test_login(self): self.assertEqual(data[0], b'LOGIN completed') self.assertEqual(client.state, 'AUTH') + def test_login_capabilities(self): + # A server may advertise new capabilities after login (as an + # untagged CAPABILITY response); imaplib must refresh its cached + # capability list (gh-63121, gh-103451). + class CapabilityLoginHandler(SimpleIMAPHandler): + def cmd_LOGIN(self, tag, args): + self.server.logged = args[0] + self._send_textline('* CAPABILITY IMAP4rev1 ENABLE UTF8=ACCEPT') + self._send_tagged(tag, 'OK', 'LOGIN completed') + def cmd_ENABLE(self, tag, args): + self._send_tagged(tag, 'OK', 'ENABLE completed') + + client, _ = self._setup(CapabilityLoginHandler) + self.assertNotIn('ENABLE', client.capabilities) + client.login('user', 'pass') + self.assertIn('ENABLE', client.capabilities) + self.assertIn('UTF8=ACCEPT', client.capabilities) + typ, _ = client.enable('UTF8=ACCEPT') + self.assertEqual(typ, 'OK') + + def test_authenticate_capabilities(self): + # Capabilities are also refreshed after AUTHENTICATE, here from a + # CAPABILITY response code in the tagged OK response. + class CapabilityAuthHandler(SimpleIMAPHandler): + def cmd_AUTHENTICATE(self, tag, args): + self._send_textline('+') + self.server.response = yield + self._send_tagged( + tag, 'OK', + '[CAPABILITY IMAP4rev1 ENABLE] AUTHENTICATE completed') + + client, _ = self._setup(CapabilityAuthHandler) + self.assertNotIn('ENABLE', client.capabilities) + client.authenticate('MYAUTH', lambda x: b'fake') + self.assertIn('ENABLE', client.capabilities) + + def test_greeting_capabilities(self): + # Capabilities advertised in the greeting are used directly, + # without sending a separate CAPABILITY command. + class GreetingHandler(SimpleIMAPHandler): + welcome = '* OK [CAPABILITY IMAP4rev1 ENABLE] Server ready' + def cmd_CAPABILITY(self, tag, args): + self.server.capability_queried = True + super().cmd_CAPABILITY(tag, args) + + client, server = self._setup(GreetingHandler) + self.assertEqual(client.capabilities, ('IMAP4REV1', 'ENABLE')) + self.assertFalse(getattr(server, 'capability_queried', False)) + + def test_login_requery_capabilities(self): + # If the server does not advertise capabilities after login, + # imaplib re-queries them (as it does after STARTTLS), so a + # capability that becomes available only after authentication is + # still recognized (gh-63121). + class RequeryHandler(SimpleIMAPHandler): + def cmd_CAPABILITY(self, tag, args): + caps = 'IMAP4rev1 ENABLE' if self.server.logged else 'IMAP4rev1' + self._send_textline('* CAPABILITY ' + caps) + self._send_tagged(tag, 'OK', 'CAPABILITY completed') + + client, _ = self._setup(RequeryHandler) + self.assertNotIn('ENABLE', client.capabilities) + client.login('user', 'pass') + self.assertIn('ENABLE', client.capabilities) + def test_logout(self): client, _ = self._setup(SimpleIMAPHandler) typ, data = client.login('user', 'pass') diff --git a/Misc/NEWS.d/next/Library/2026-07-01-10-30-00.gh-issue-63121.Rm4tZp.rst b/Misc/NEWS.d/next/Library/2026-07-01-10-30-00.gh-issue-63121.Rm4tZp.rst new file mode 100644 index 000000000000000..a63b164ad7a627b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-01-10-30-00.gh-issue-63121.Rm4tZp.rst @@ -0,0 +1,7 @@ +:mod:`imaplib` now refreshes the cached capability list after a successful +:meth:`~imaplib.IMAP4.login` or :meth:`~imaplib.IMAP4.authenticate`, using +the ``CAPABILITY`` response sent by the server or, if none was sent, by +querying it, so that capabilities that become available only after +authentication (such as ``ENABLE`` on Gmail) are recognized. Capabilities +advertised in the server greeting are now also used, avoiding a redundant +``CAPABILITY`` command.