]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.13] gh-63121: Refresh imaplib capabilities on state changes (GH-152752) (GH-152865)
authorSerhiy Storchaka <storchaka@gmail.com>
Sun, 5 Jul 2026 07:02:56 +0000 (10:02 +0300)
committerGitHub <noreply@github.com>
Sun, 5 Jul 2026 07:02:56 +0000 (07:02 +0000)
imaplib fetched the server capabilities only once, at connection time.
They are now also refreshed after a successful LOGIN or AUTHENTICATE,
from the CAPABILITY response the server sent or, if it sent none, by
querying it.  This lets methods such as enable() see capabilities added
after login, for example ENABLE on Gmail (gh-103451).

Capabilities advertised in the server greeting are now used too, saving
a redundant CAPABILITY command.

(cherry picked from commit c89b72abed7393d9d428f1e336e3aa02e7ef6ce8)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lib/imaplib.py
Lib/test/test_imaplib.py
Misc/NEWS.d/next/Library/2026-07-01-10-30-00.gh-issue-63121.Rm4tZp.rst [new file with mode: 0644]

index 22714291d601f28d1c50df54950489a3c663523d..fdb8f718132c2ee59adcf5cb8e483e241d9f0825 100644 (file)
@@ -259,7 +259,7 @@ class IMAP4:
                 self._encoding, 'replace')
             raise self.error('invalid greeting: ' + greeting)
 
-        self._get_capabilities()
+        self._refresh_capabilities()
         if __debug__:
             if self.debug >= 3:
                 self._mesg('CAPABILITIES: %r' % (self.capabilities,))
@@ -454,6 +454,7 @@ class IMAP4:
         if typ != 'OK':
             raise self.error(dat[-1].decode('utf-8', 'replace'))
         self.state = 'AUTH'
+        self._refresh_capabilities()
         return typ, dat
 
 
@@ -622,6 +623,7 @@ class IMAP4:
         if typ != 'OK':
             raise self.error(dat[-1].decode('UTF-8', 'replace'))
         self.state = 'AUTH'
+        self._refresh_capabilities()
         return typ, dat
 
 
@@ -1088,6 +1090,15 @@ class IMAP4:
         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):
 
         # Read response and store.
index 00856f9684edd4511498f86f244e7089bb479b56..2b67c8c3f92513f08adfd5ee17d7a710efd9d4fb 100644 (file)
@@ -221,9 +221,11 @@ class SimpleIMAPHandler(socketserver.StreamRequestHandler):
     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
@@ -659,6 +661,71 @@ class NewIMAPTestsMixin:
         client.append('INBOX', None, None, message)
         self.assertEqual(server.response, b'a\r\nb\r\nc\r\nd')
 
+    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 (file)
index 0000000..a63b164
--- /dev/null
@@ -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.