Skip to content
Merged
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
13 changes: 12 additions & 1 deletion Lib/imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,))
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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.
Expand Down
69 changes: 68 additions & 1 deletion Lib/test/test_imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Loading