Commits

Anonymous committed 8d65169

Issue #4471: Add the IMAP.starttls() method to enable encryption on
standard IMAP4 connections. Original patch by Lorenzo M. Catucci.

Comments (0)

Files changed (4)

Doc/library/imaplib.rst

    write permission, and the mailbox will need to be re-opened to re-obtain write
    permission.
 
+
 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:
 
 
    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*.
 
 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"]
 
         'SETANNOTATION':('AUTH', 'SELECTED'),
         'SETQUOTA':     ('AUTH', 'SELECTED'),
         'SORT':         ('SELECTED',),
+        'STARTTLS':     ('NONAUTH',),
         'STATUS':       ('AUTH', 'SELECTED'),
         'STORE':        ('SELECTED',),
         'SUBSCRIBE':    ('AUTH', 'SELECTED'),
         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.
 
         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.
 
                 n -= 1
 
 
+if HAVE_SSL:
 
-try:
-    import ssl
-except ImportError:
-    pass
-else:
     class IMAP4_SSL(IMAP4):
 
         """IMAP4 client class over SSL connection

Lib/test/test_imaplib.py

 
     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')
 
 
 @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
                 raise support.TestFailed("Can't read certificate files!")
         tests.extend([
             ThreadedNetworkedTests, ThreadedNetworkedTestsSSL,
-            RemoteIMAPTest, RemoteIMAP_SSLTest,
+            RemoteIMAPTest, RemoteIMAP_SSLTest, RemoteIMAP_STARTTLSTest,
         ])
 
     support.run_unittest(*tests)
 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.