From f3b001f966d8e16662241e2ed7c7f345cd7051db Mon Sep 17 00:00:00 2001 From: Antoine Pitrou Date: Fri, 12 Nov 2010 18:49:16 +0000 Subject: Issue #4471: Add the IMAP.starttls() method to enable encryption on standard IMAP4 connections. Original patch by Lorenzo M. Catucci. --- Doc/library/imaplib.rst | 11 +++++++++++ Lib/imaplib.py | 41 ++++++++++++++++++++++++++++++++++++----- Lib/test/test_imaplib.py | 16 +++++++++++++--- Misc/NEWS | 3 +++ 4 files changed, 63 insertions(+), 8 deletions(-) diff --git a/Doc/library/imaplib.rst b/Doc/library/imaplib.rst index b8ac03d..dc94beb 100644 --- a/Doc/library/imaplib.rst +++ b/Doc/library/imaplib.rst @@ -56,6 +56,7 @@ Three exceptions are defined as attributes of the :class:`IMAP4` class: write permission, and the mailbox will need to be re-opened to re-obtain write permission. + There's also a subclass for secure connections: @@ -68,6 +69,7 @@ There's also a subclass for secure connections: and *certfile* are also optional - they can contain a PEM formatted private key and certificate chain file for the SSL connection. + The second subclass allows for connections created by a child process: @@ -406,6 +408,15 @@ An :class:`IMAP4` instance has the following methods: This is an ``IMAP4rev1`` extension command. +.. method:: IMAP4.starttls(ssl_context=None) + + Send a ``STARTTLS`` command. The *ssl_context* argument is optional + and should be a :class:`ssl.SSLContext` object. This will enable + encryption on the IMAP connection. + + .. versionadded:: 3.2 + + .. method:: IMAP4.status(mailbox, names) Request named status conditions for *mailbox*. diff --git a/Lib/imaplib.py b/Lib/imaplib.py index 77806db..421cb97 100644 --- a/Lib/imaplib.py +++ b/Lib/imaplib.py @@ -24,6 +24,12 @@ __version__ = "2.58" import binascii, errno, random, re, socket, subprocess, sys, time +try: + import ssl + HAVE_SSL = True +except ImportError: + HAVE_SSL = False + __all__ = ["IMAP4", "IMAP4_stream", "Internaldate2tuple", "Int2AP", "ParseFlags", "Time2Internaldate"] @@ -71,6 +77,7 @@ Commands = { 'SETANNOTATION':('AUTH', 'SELECTED'), 'SETQUOTA': ('AUTH', 'SELECTED'), 'SORT': ('SELECTED',), + 'STARTTLS': ('NONAUTH',), 'STATUS': ('AUTH', 'SELECTED'), 'STORE': ('SELECTED',), 'SUBSCRIBE': ('AUTH', 'SELECTED'), @@ -156,6 +163,7 @@ class IMAP4: self.continuation_response = '' # Last continuation response self.is_readonly = False # READ-ONLY desired state self.tagnum = 0 + self._tls_established = False # Open socket to server. @@ -711,6 +719,33 @@ class IMAP4: return self._untagged_response(typ, dat, name) + def starttls(self, ssl_context=None): + name = 'STARTTLS' + if not HAVE_SSL: + raise self.error('SSL support missing') + if self._tls_established: + raise self.abort('TLS session already established') + if name not in self.capabilities: + raise self.abort('TLS not supported by server') + # Generate a default SSL context if none was passed. + if ssl_context is None: + ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + # SSLv2 considered harmful. + ssl_context.options |= ssl.OP_NO_SSLv2 + typ, dat = self._simple_command(name) + if typ == 'OK': + self.sock = ssl_context.wrap_socket(self.sock) + self.file = self.sock.makefile('rb') + self._tls_established = True + typ, dat = self.capability() + if dat == [None]: + raise self.error('no CAPABILITY response from server') + self.capabilities = tuple(dat[-1].upper().split()) + else: + raise self.error("Couldn't establish TLS session") + return self._untagged_response(typ, dat, name) + + def status(self, mailbox, names): """Request named status conditions for mailbox. @@ -1125,12 +1160,8 @@ class IMAP4: n -= 1 +if HAVE_SSL: -try: - import ssl -except ImportError: - pass -else: class IMAP4_SSL(IMAP4): """IMAP4 client class over SSL connection diff --git a/Lib/test/test_imaplib.py b/Lib/test/test_imaplib.py index 351659d..9bae65a 100644 --- a/Lib/test/test_imaplib.py +++ b/Lib/test/test_imaplib.py @@ -209,8 +209,6 @@ class RemoteIMAPTest(unittest.TestCase): def test_logincapa(self): self.assertTrue('LOGINDISABLED' in self.server.capabilities) - - def test_anonlogin(self): self.assertTrue('AUTH=ANONYMOUS' in self.server.capabilities) rs = self.server.login(self.username, self.password) self.assertEqual(rs[0], 'OK') @@ -222,6 +220,18 @@ class RemoteIMAPTest(unittest.TestCase): @unittest.skipUnless(ssl, "SSL not available") +class RemoteIMAP_STARTTLSTest(RemoteIMAPTest): + + def setUp(self): + super().setUp() + rs = self.server.starttls() + self.assertEqual(rs[0], 'OK') + + def test_logincapa(self): + self.assertFalse('LOGINDISABLED' in self.server.capabilities) + + +@unittest.skipUnless(ssl, "SSL not available") class RemoteIMAP_SSLTest(RemoteIMAPTest): port = 993 imap_class = IMAP4_SSL @@ -243,7 +253,7 @@ def test_main(): raise support.TestFailed("Can't read certificate files!") tests.extend([ ThreadedNetworkedTests, ThreadedNetworkedTestsSSL, - RemoteIMAPTest, RemoteIMAP_SSLTest, + RemoteIMAPTest, RemoteIMAP_SSLTest, RemoteIMAP_STARTTLSTest, ]) support.run_unittest(*tests) diff --git a/Misc/NEWS b/Misc/NEWS index 2ff7247..e522ed1 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -63,6 +63,9 @@ Core and Builtins Library ------- +- Issue #4471: Add the IMAP.starttls() method to enable encryption on + standard IMAP4 connections. Original patch by Lorenzo M. Catucci. + - Issue #1466065: Add 'validate' option to base64.b64decode to raise an error if there are non-base64 alphabet characters in the input. -- cgit v0.12