Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8618d74

Browse files
committed
Issue #4473: Add a POP3.stls() to switch a clear-text POP3 session into an encrypted POP3 session, on supported servers.
Patch by Lorenzo Catucci.
1 parent 25cee19 commit 8618d74

4 files changed

Lines changed: 176 additions & 39 deletions

File tree

Doc/library/poplib.rst

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@
1313
--------------
1414

1515
This module defines a class, :class:`POP3`, which encapsulates a connection to a
16-
POP3 server and implements the protocol as defined in :rfc:`1725`. The
17-
:class:`POP3` class supports both the minimal and optional command sets.
16+
POP3 server and implements the protocol as defined in :rfc:`1939`. The
17+
:class:`POP3` class supports both the minimal and optional command sets from
18+
:rfc:`1939`. The :class:`POP3` class also supports the `STLS` command introduced
19+
in :rfc:`2595` to enable encrypted communication on an already established connection.
20+
1821
Additionally, this module provides a class :class:`POP3_SSL`, which provides
1922
support for connecting to POP3 servers that use SSL as an underlying protocol
2023
layer.
@@ -184,6 +187,18 @@ An :class:`POP3` instance has the following methods:
184187
the unique id for that message in the form ``'response mesgnum uid``, otherwise
185188
result is list ``(response, ['mesgnum uid', ...], octets)``.
186189

190+
.. method:: POP3.stls(context=None)
191+
192+
Start a TLS session on the active connection as specified in :rfc:`2595`.
193+
This is only allowed before user authentication
194+
195+
*context* parameter is a :class:`ssl.SSLContext` object which allows
196+
bundling SSL configuration options, certificates and private keys into
197+
a single (potentially long-lived) structure.
198+
199+
.. versionadded:: 3.4
200+
201+
187202
Instances of :class:`POP3_SSL` have no additional methods. The interface of this
188203
subclass is identical to its parent.
189204

Lib/poplib.py

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515

1616
import re, socket
1717

18+
try:
19+
import ssl
20+
HAVE_SSL = True
21+
except ImportError:
22+
HAVE_SSL = False
23+
1824
__all__ = ["POP3","error_proto"]
1925

2026
# Exception raised when an error or invalid response is received:
@@ -56,6 +62,7 @@ class POP3:
5662
TOP msg n top(msg, n)
5763
UIDL [msg] uidl(msg = None)
5864
CAPA capa()
65+
STLS stls()
5966
6067
Raises one exception: 'error_proto'.
6168
@@ -82,6 +89,7 @@ def __init__(self, host, port=POP3_PORT,
8289
timeout=socket._GLOBAL_DEFAULT_TIMEOUT):
8390
self.host = host
8491
self.port = port
92+
self._tls_established = False
8593
self.sock = self._create_socket(timeout)
8694
self.file = self.sock.makefile('rb')
8795
self._debugging = 0
@@ -352,21 +360,42 @@ def _parsecap(line):
352360
raise error_proto('-ERR CAPA not supported by server')
353361
return caps
354362

355-
try:
356-
import ssl
357-
except ImportError:
358-
pass
359-
else:
363+
364+
def stls(self, context=None):
365+
"""Start a TLS session on the active connection as specified in RFC 2595.
366+
367+
context - a ssl.SSLContext
368+
"""
369+
if not HAVE_SSL:
370+
raise error_proto('-ERR TLS support missing')
371+
if self._tls_established:
372+
raise error_proto('-ERR TLS session already established')
373+
caps = self.capa()
374+
if not 'STLS' in caps:
375+
raise error_proto('-ERR STLS not supported by server')
376+
if context is None:
377+
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
378+
context.options |= ssl.OP_NO_SSLv2
379+
resp = self._shortcmd('STLS')
380+
self.sock = context.wrap_socket(self.sock)
381+
self.file = self.sock.makefile('rb')
382+
self._tls_established = True
383+
return resp
384+
385+
386+
if HAVE_SSL:
360387

361388
class POP3_SSL(POP3):
362389
"""POP3 client class over SSL connection
363390
364-
Instantiate with: POP3_SSL(hostname, port=995, keyfile=None, certfile=None)
391+
Instantiate with: POP3_SSL(hostname, port=995, keyfile=None, certfile=None,
392+
context=None)
365393
366394
hostname - the hostname of the pop3 over ssl server
367395
port - port number
368396
keyfile - PEM formatted file that countains your private key
369397
certfile - PEM formatted certificate chain file
398+
context - a ssl.SSLContext
370399
371400
See the methods of the parent class POP3 for more documentation.
372401
"""
@@ -392,6 +421,13 @@ def _create_socket(self, timeout):
392421
sock = ssl.wrap_socket(sock, self.keyfile, self.certfile)
393422
return sock
394423

424+
def stls(self, keyfile=None, certfile=None, context=None):
425+
"""The method unconditionally raises an exception since the
426+
STLS command doesn't make any sense on an already established
427+
SSL/TLS session.
428+
"""
429+
raise error_proto('-ERR TLS session already established')
430+
395431
__all__.append("POP3_SSL")
396432

397433
if __name__ == "__main__":

Lib/test/test_poplib.py

Lines changed: 114 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@
1818
HOST = test_support.HOST
1919
PORT = 0
2020

21+
SUPPORTS_SSL = False
22+
if hasattr(poplib, 'POP3_SSL'):
23+
import ssl
24+
25+
SUPPORTS_SSL = True
26+
CERTFILE = os.path.join(os.path.dirname(__file__) or os.curdir, "keycert.pem")
27+
2128
# the dummy data returned by server when LIST and RETR commands are issued
2229
LIST_RESP = b'1 1\r\n2 2\r\n3 3\r\n4 4\r\n5 5\r\n.\r\n'
2330
RETR_RESP = b"""From: [email protected]\
@@ -40,6 +47,8 @@ def __init__(self, conn):
4047
self.set_terminator(b"\r\n")
4148
self.in_buffer = []
4249
self.push('+OK dummy pop3 server ready. <timestamp>')
50+
self.tls_active = False
51+
self.tls_starting = False
4352

4453
def collect_incoming_data(self, data):
4554
self.in_buffer.append(data)
@@ -114,16 +123,65 @@ def cmd_quit(self, arg):
114123
self.push('+OK closing.')
115124
self.close_when_done()
116125

126+
def _get_capas(self):
127+
_capas = dict(self.CAPAS)
128+
if not self.tls_active and SUPPORTS_SSL:
129+
_capas['STLS'] = []
130+
return _capas
131+
117132
def cmd_capa(self, arg):
118133
self.push('+OK Capability list follows')
119-
if self.CAPAS:
120-
for cap, params in self.CAPAS.items():
134+
if self._get_capas():
135+
for cap, params in self._get_capas().items():
121136
_ln = [cap]
122137
if params:
123138
_ln.extend(params)
124139
self.push(' '.join(_ln))
125140
self.push('.')
126141

142+
if SUPPORTS_SSL:
143+
144+
def cmd_stls(self, arg):
145+
if self.tls_active is False:
146+
self.push('+OK Begin TLS negotiation')
147+
tls_sock = ssl.wrap_socket(self.socket, certfile=CERTFILE,
148+
server_side=True,
149+
do_handshake_on_connect=False,
150+
suppress_ragged_eofs=False)
151+
self.del_channel()
152+
self.set_socket(tls_sock)
153+
self.tls_active = True
154+
self.tls_starting = True
155+
self.in_buffer = []
156+
self._do_tls_handshake()
157+
else:
158+
self.push('-ERR Command not permitted when TLS active')
159+
160+
def _do_tls_handshake(self):
161+
try:
162+
self.socket.do_handshake()
163+
except ssl.SSLError as err:
164+
if err.args[0] in (ssl.SSL_ERROR_WANT_READ,
165+
ssl.SSL_ERROR_WANT_WRITE):
166+
return
167+
elif err.args[0] == ssl.SSL_ERROR_EOF:
168+
return self.handle_close()
169+
raise
170+
except socket.error as err:
171+
if err.args[0] == errno.ECONNABORTED:
172+
return self.handle_close()
173+
else:
174+
self.tls_active = True
175+
self.tls_starting = False
176+
177+
def handle_read(self):
178+
if self.tls_starting:
179+
self._do_tls_handshake()
180+
else:
181+
try:
182+
asynchat.async_chat.handle_read(self)
183+
except ssl.SSLEOFError:
184+
self.handle_close()
127185

128186
class DummyPOP3Server(asyncore.dispatcher, threading.Thread):
129187

@@ -254,13 +312,25 @@ def test_quit(self):
254312
self.assertIsNone(self.client.sock)
255313
self.assertIsNone(self.client.file)
256314

315+
if SUPPORTS_SSL:
257316

258-
SUPPORTS_SSL = False
259-
if hasattr(poplib, 'POP3_SSL'):
260-
import ssl
317+
def test_stls_capa(self):
318+
capa = self.client.capa()
319+
self.assertTrue('STLS' in capa.keys())
261320

262-
SUPPORTS_SSL = True
263-
CERTFILE = os.path.join(os.path.dirname(__file__) or os.curdir, "keycert.pem")
321+
def test_stls(self):
322+
expected = b'+OK Begin TLS negotiation'
323+
resp = self.client.stls()
324+
self.assertEqual(resp, expected)
325+
326+
def test_stls_context(self):
327+
expected = b'+OK Begin TLS negotiation'
328+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
329+
resp = self.client.stls(context=ctx)
330+
self.assertEqual(resp, expected)
331+
332+
333+
if SUPPORTS_SSL:
264334

265335
class DummyPOP3_SSLHandler(DummyPOP3Handler):
266336

@@ -272,34 +342,13 @@ def __init__(self, conn):
272342
self.del_channel()
273343
self.set_socket(ssl_socket)
274344
# Must try handshake before calling push()
275-
self._ssl_accepting = True
276-
self._do_ssl_handshake()
345+
self.tls_active = True
346+
self.tls_starting = True
347+
self._do_tls_handshake()
277348
self.set_terminator(b"\r\n")
278349
self.in_buffer = []
279350
self.push('+OK dummy pop3 server ready. <timestamp>')
280351

281-
def _do_ssl_handshake(self):
282-
try:
283-
self.socket.do_handshake()
284-
except ssl.SSLError as err:
285-
if err.args[0] in (ssl.SSL_ERROR_WANT_READ,
286-
ssl.SSL_ERROR_WANT_WRITE):
287-
return
288-
elif err.args[0] == ssl.SSL_ERROR_EOF:
289-
return self.handle_close()
290-
raise
291-
except socket.error as err:
292-
if err.args[0] == errno.ECONNABORTED:
293-
return self.handle_close()
294-
else:
295-
self._ssl_accepting = False
296-
297-
def handle_read(self):
298-
if self._ssl_accepting:
299-
self._do_ssl_handshake()
300-
else:
301-
DummyPOP3Handler.handle_read(self)
302-
303352

304353
class TestPOP3_SSLClass(TestPOP3Class):
305354
# repeat previous tests by using poplib.POP3_SSL
@@ -330,6 +379,39 @@ def test_context(self):
330379
self.assertIs(self.client.sock.context, ctx)
331380
self.assertTrue(self.client.noop().startswith(b'+OK'))
332381

382+
def test_stls(self):
383+
self.assertRaises(poplib.error_proto, self.client.stls)
384+
385+
test_stls_context = test_stls
386+
387+
def test_stls_capa(self):
388+
capa = self.client.capa()
389+
self.assertFalse('STLS' in capa.keys())
390+
391+
392+
class TestPOP3_TLSClass(TestPOP3Class):
393+
# repeat previous tests by using poplib.POP3.stls()
394+
395+
def setUp(self):
396+
self.server = DummyPOP3Server((HOST, PORT))
397+
self.server.start()
398+
self.client = poplib.POP3(self.server.host, self.server.port, timeout=3)
399+
self.client.stls()
400+
401+
def tearDown(self):
402+
if self.client.file is not None and self.client.sock is not None:
403+
self.client.quit()
404+
self.server.stop()
405+
406+
def test_stls(self):
407+
self.assertRaises(poplib.error_proto, self.client.stls)
408+
409+
test_stls_context = test_stls
410+
411+
def test_stls_capa(self):
412+
capa = self.client.capa()
413+
self.assertFalse(b'STLS' in capa.keys())
414+
333415

334416
class TestTimeouts(TestCase):
335417

@@ -389,6 +471,7 @@ def test_main():
389471
tests = [TestPOP3Class, TestTimeouts]
390472
if SUPPORTS_SSL:
391473
tests.append(TestPOP3_SSLClass)
474+
tests.append(TestPOP3_TLSClass)
392475
thread_info = test_support.threading_setup()
393476
try:
394477
test_support.run_unittest(*tests)

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ Core and Builtins
138138
Library
139139
-------
140140

141+
- Issue #4473: Add a POP3.stls() to switch a clear-text POP3 session into
142+
an encrypted POP3 session, on supported servers. Patch by Lorenzo Catucci.
143+
141144
- Issue #4473: Add a POP3.capa() method to query the capabilities advertised
142145
by the POP3 server. Patch by Lorenzo Catucci.
143146

0 commit comments

Comments
 (0)