Skip to content

Commit 9f4a944

Browse files
miss-islingtonserhiy-storchakaclaude
authored
[3.14] gh-63121: Refresh imaplib capabilities on state changes (GH-152752) (GH-152855)
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 c89b72a) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7449390 commit 9f4a944

3 files changed

Lines changed: 87 additions & 2 deletions

File tree

Lib/imaplib.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ def _connect(self):
258258
else:
259259
raise self.error(self.welcome)
260260

261-
self._get_capabilities()
261+
self._refresh_capabilities()
262262
if __debug__:
263263
if self.debug >= 3:
264264
self._mesg('CAPABILITIES: %r' % (self.capabilities,))
@@ -527,6 +527,7 @@ def authenticate(self, mechanism, authobject):
527527
if typ != 'OK':
528528
raise self.error(dat[-1].decode('utf-8', 'replace'))
529529
self.state = 'AUTH'
530+
self._refresh_capabilities()
530531
return typ, dat
531532

532533

@@ -708,6 +709,7 @@ def login(self, user, password):
708709
if typ != 'OK':
709710
raise self.error(dat[-1].decode('UTF-8', 'replace'))
710711
self.state = 'AUTH'
712+
self._refresh_capabilities()
711713
return typ, dat
712714

713715

@@ -1192,6 +1194,15 @@ def _get_capabilities(self):
11921194
self.capabilities = tuple(dat.split())
11931195

11941196

1197+
def _refresh_capabilities(self):
1198+
# Use a CAPABILITY response sent by the server, or ask for it.
1199+
if 'CAPABILITY' in self.untagged_responses:
1200+
dat = self.untagged_responses.pop('CAPABILITY')[-1]
1201+
self.capabilities = tuple(str(dat, self._encoding).upper().split())
1202+
else:
1203+
self._get_capabilities()
1204+
1205+
11951206
def _get_response(self, start_timeout=False):
11961207

11971208
# Read response and store.

Lib/test/test_imaplib.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,11 @@ def _send_textline(self, message):
137137
def _send_tagged(self, tag, code, message):
138138
self._send_textline(' '.join((tag, code, message)))
139139

140+
welcome = '* OK IMAP4rev1'
141+
140142
def handle(self):
141143
# Send a welcome message.
142-
self._send_textline('* OK IMAP4rev1')
144+
self._send_textline(self.welcome)
143145
while 1:
144146
# Gather up input until we receive a line terminator or we timeout.
145147
# Accumulate read(1) because it's simpler to handle the differences
@@ -640,6 +642,71 @@ def test_login(self):
640642
self.assertEqual(data[0], b'LOGIN completed')
641643
self.assertEqual(client.state, 'AUTH')
642644

645+
def test_login_capabilities(self):
646+
# A server may advertise new capabilities after login (as an
647+
# untagged CAPABILITY response); imaplib must refresh its cached
648+
# capability list (gh-63121, gh-103451).
649+
class CapabilityLoginHandler(SimpleIMAPHandler):
650+
def cmd_LOGIN(self, tag, args):
651+
self.server.logged = args[0]
652+
self._send_textline('* CAPABILITY IMAP4rev1 ENABLE UTF8=ACCEPT')
653+
self._send_tagged(tag, 'OK', 'LOGIN completed')
654+
def cmd_ENABLE(self, tag, args):
655+
self._send_tagged(tag, 'OK', 'ENABLE completed')
656+
657+
client, _ = self._setup(CapabilityLoginHandler)
658+
self.assertNotIn('ENABLE', client.capabilities)
659+
client.login('user', 'pass')
660+
self.assertIn('ENABLE', client.capabilities)
661+
self.assertIn('UTF8=ACCEPT', client.capabilities)
662+
typ, _ = client.enable('UTF8=ACCEPT')
663+
self.assertEqual(typ, 'OK')
664+
665+
def test_authenticate_capabilities(self):
666+
# Capabilities are also refreshed after AUTHENTICATE, here from a
667+
# CAPABILITY response code in the tagged OK response.
668+
class CapabilityAuthHandler(SimpleIMAPHandler):
669+
def cmd_AUTHENTICATE(self, tag, args):
670+
self._send_textline('+')
671+
self.server.response = yield
672+
self._send_tagged(
673+
tag, 'OK',
674+
'[CAPABILITY IMAP4rev1 ENABLE] AUTHENTICATE completed')
675+
676+
client, _ = self._setup(CapabilityAuthHandler)
677+
self.assertNotIn('ENABLE', client.capabilities)
678+
client.authenticate('MYAUTH', lambda x: b'fake')
679+
self.assertIn('ENABLE', client.capabilities)
680+
681+
def test_greeting_capabilities(self):
682+
# Capabilities advertised in the greeting are used directly,
683+
# without sending a separate CAPABILITY command.
684+
class GreetingHandler(SimpleIMAPHandler):
685+
welcome = '* OK [CAPABILITY IMAP4rev1 ENABLE] Server ready'
686+
def cmd_CAPABILITY(self, tag, args):
687+
self.server.capability_queried = True
688+
super().cmd_CAPABILITY(tag, args)
689+
690+
client, server = self._setup(GreetingHandler)
691+
self.assertEqual(client.capabilities, ('IMAP4REV1', 'ENABLE'))
692+
self.assertFalse(getattr(server, 'capability_queried', False))
693+
694+
def test_login_requery_capabilities(self):
695+
# If the server does not advertise capabilities after login,
696+
# imaplib re-queries them (as it does after STARTTLS), so a
697+
# capability that becomes available only after authentication is
698+
# still recognized (gh-63121).
699+
class RequeryHandler(SimpleIMAPHandler):
700+
def cmd_CAPABILITY(self, tag, args):
701+
caps = 'IMAP4rev1 ENABLE' if self.server.logged else 'IMAP4rev1'
702+
self._send_textline('* CAPABILITY ' + caps)
703+
self._send_tagged(tag, 'OK', 'CAPABILITY completed')
704+
705+
client, _ = self._setup(RequeryHandler)
706+
self.assertNotIn('ENABLE', client.capabilities)
707+
client.login('user', 'pass')
708+
self.assertIn('ENABLE', client.capabilities)
709+
643710
def test_logout(self):
644711
client, _ = self._setup(SimpleIMAPHandler)
645712
typ, data = client.login('user', 'pass')
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
:mod:`imaplib` now refreshes the cached capability list after a successful
2+
:meth:`~imaplib.IMAP4.login` or :meth:`~imaplib.IMAP4.authenticate`, using
3+
the ``CAPABILITY`` response sent by the server or, if none was sent, by
4+
querying it, so that capabilities that become available only after
5+
authentication (such as ``ENABLE`` on Gmail) are recognized. Capabilities
6+
advertised in the server greeting are now also used, avoiding a redundant
7+
``CAPABILITY`` command.

0 commit comments

Comments
 (0)