IMAPClient 4.0.0__tar.gz → 4.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {imapclient-4.0.0 → imapclient-4.1.0}/AUTHORS.rst +1 -1
- {imapclient-4.0.0 → imapclient-4.1.0/IMAPClient.egg-info}/PKG-INFO +1 -2
- {imapclient-4.0.0 → imapclient-4.1.0}/IMAPClient.egg-info/SOURCES.txt +2 -0
- {imapclient-4.0.0/IMAPClient.egg-info → imapclient-4.1.0}/PKG-INFO +1 -2
- {imapclient-4.0.0 → imapclient-4.1.0}/README.rst +0 -1
- {imapclient-4.0.0 → imapclient-4.1.0}/imapclient/imapclient.py +8 -4
- {imapclient-4.0.0 → imapclient-4.1.0}/imapclient/tls.py +9 -3
- {imapclient-4.0.0 → imapclient-4.1.0}/pyproject.toml +1 -1
- {imapclient-4.0.0 → imapclient-4.1.0}/tests/test_starttls.py +15 -18
- imapclient-4.1.0/tests/test_starttls_integration.py +162 -0
- imapclient-4.1.0/tests/test_tls_integration.py +133 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/COPYING +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/IMAPClient.egg-info/dependency_links.txt +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/IMAPClient.egg-info/entry_points.txt +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/IMAPClient.egg-info/top_level.txt +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/imapclient/__init__.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/imapclient/config.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/imapclient/datetime_util.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/imapclient/exceptions.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/imapclient/fixed_offset.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/imapclient/imap4.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/imapclient/imap_utf7.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/imapclient/interact.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/imapclient/livetest.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/imapclient/response_lexer.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/imapclient/response_parser.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/imapclient/response_types.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/imapclient/testable_imapclient.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/imapclient/typing_imapclient.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/imapclient/util.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/imapclient/version.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/setup.cfg +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/tests/test_auth.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/tests/test_datetime_util.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/tests/test_enable.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/tests/test_fixed_offset.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/tests/test_folder_status.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/tests/test_imap_utf7.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/tests/test_imapclient.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/tests/test_init.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/tests/test_response_lexer.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/tests/test_response_parser.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/tests/test_search.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/tests/test_sort.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/tests/test_store.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/tests/test_thread.py +0 -0
- {imapclient-4.0.0 → imapclient-4.1.0}/tests/test_util_functions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: IMAPClient
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.1.0
|
|
4
4
|
Summary: Easy-to-use, Pythonic and complete IMAP client library
|
|
5
5
|
Author-email: Menno Finlay-Smits <hello@menno.io>
|
|
6
6
|
Maintainer-email: IMAPClient Maintainers <imapclient@groups.io>
|
|
@@ -28,7 +28,6 @@ IMAPClient is an easy-to-use, Pythonic and complete IMAP client
|
|
|
28
28
|
library.
|
|
29
29
|
|
|
30
30
|
========================= ========================================
|
|
31
|
-
Current version 3.0.1
|
|
32
31
|
Supported Python versions 3.8 - 3.14
|
|
33
32
|
License New BSD
|
|
34
33
|
Project home https://github.com/mjs/imapclient/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: IMAPClient
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.1.0
|
|
4
4
|
Summary: Easy-to-use, Pythonic and complete IMAP client library
|
|
5
5
|
Author-email: Menno Finlay-Smits <hello@menno.io>
|
|
6
6
|
Maintainer-email: IMAPClient Maintainers <imapclient@groups.io>
|
|
@@ -28,7 +28,6 @@ IMAPClient is an easy-to-use, Pythonic and complete IMAP client
|
|
|
28
28
|
library.
|
|
29
29
|
|
|
30
30
|
========================= ========================================
|
|
31
|
-
Current version 3.0.1
|
|
32
31
|
Supported Python versions 3.8 - 3.14
|
|
33
32
|
License New BSD
|
|
34
33
|
Project home https://github.com/mjs/imapclient/
|
|
@@ -4,7 +4,6 @@ IMAPClient is an easy-to-use, Pythonic and complete IMAP client
|
|
|
4
4
|
library.
|
|
5
5
|
|
|
6
6
|
========================= ========================================
|
|
7
|
-
Current version 3.0.1
|
|
8
7
|
Supported Python versions 3.8 - 3.14
|
|
9
8
|
License New BSD
|
|
10
9
|
Project home https://github.com/mjs/imapclient/
|
|
@@ -378,13 +378,17 @@ class IMAPClient:
|
|
|
378
378
|
if self.ssl or self._starttls_done:
|
|
379
379
|
raise exceptions.IMAPClientAbortError("TLS session already established")
|
|
380
380
|
|
|
381
|
-
|
|
381
|
+
# Delegate the socket wrap to imaplib: it manages its own read buffer
|
|
382
|
+
# since Python 3.14 (where IMAP4.file became a read-only property).
|
|
383
|
+
# imaplib's own None-fallback context does not verify certificates, so
|
|
384
|
+
# pass IMAPClient's verifying default instead.
|
|
385
|
+
if ssl_context is None:
|
|
386
|
+
ssl_context = tls.create_default_context()
|
|
387
|
+
|
|
388
|
+
typ, data = self._imap.starttls(ssl_context=ssl_context)
|
|
382
389
|
self._checkok("starttls", typ, data)
|
|
383
390
|
|
|
384
391
|
self._starttls_done = True
|
|
385
|
-
|
|
386
|
-
self._imap.sock = tls.wrap_socket(self._imap.sock, ssl_context, self.host)
|
|
387
|
-
self._imap.file = self._imap.sock.makefile("rb")
|
|
388
392
|
return data[0]
|
|
389
393
|
|
|
390
394
|
def login(self, username: str, password: str):
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c)
|
|
1
|
+
# Copyright (c) 2026, Menno Smits
|
|
2
2
|
# Released subject to the New BSD License
|
|
3
3
|
# Please see http://en.wikipedia.org/wiki/BSD_licenses
|
|
4
4
|
|
|
@@ -13,11 +13,15 @@ import ssl
|
|
|
13
13
|
from typing import Optional
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def create_default_context() -> ssl.SSLContext:
|
|
17
|
+
return ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH)
|
|
18
|
+
|
|
19
|
+
|
|
16
20
|
def wrap_socket(
|
|
17
21
|
sock: socket.socket, ssl_context: Optional[ssl.SSLContext], host: str
|
|
18
22
|
) -> socket.socket:
|
|
19
23
|
if ssl_context is None:
|
|
20
|
-
ssl_context =
|
|
24
|
+
ssl_context = create_default_context()
|
|
21
25
|
|
|
22
26
|
return ssl_context.wrap_socket(sock, server_hostname=host)
|
|
23
27
|
|
|
@@ -39,7 +43,9 @@ class IMAP4_TLS(imaplib.IMAP4):
|
|
|
39
43
|
self._timeout = timeout
|
|
40
44
|
super().__init__(host, port)
|
|
41
45
|
|
|
42
|
-
def _create_socket(self, timeout: Optional[float]) -> socket.socket:
|
|
46
|
+
def _create_socket(self, timeout: Optional[float] = None) -> socket.socket:
|
|
47
|
+
if timeout is None:
|
|
48
|
+
timeout = self._timeout
|
|
43
49
|
sock = socket.create_connection((self.host, self.port), timeout=timeout)
|
|
44
50
|
|
|
45
51
|
return wrap_socket(sock, self.ssl_context, self.host)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# Released subject to the New BSD License
|
|
3
3
|
# Please see http://en.wikipedia.org/wiki/BSD_licenses
|
|
4
4
|
|
|
5
|
-
from unittest.mock import
|
|
5
|
+
from unittest.mock import patch, sentinel
|
|
6
6
|
|
|
7
7
|
from imapclient.exceptions import IMAPClientError
|
|
8
8
|
from imapclient.imapclient import IMAPClient
|
|
@@ -18,34 +18,31 @@ class TestStarttls(IMAPClientTest):
|
|
|
18
18
|
self.tls = patcher.start()
|
|
19
19
|
self.addCleanup(patcher.stop)
|
|
20
20
|
|
|
21
|
-
self.client._imap.sock = sentinel.old_sock
|
|
22
|
-
|
|
23
|
-
self.new_sock = Mock()
|
|
24
|
-
self.new_sock.makefile.return_value = sentinel.file
|
|
25
|
-
self.tls.wrap_socket.return_value = self.new_sock
|
|
26
|
-
|
|
27
|
-
self.client.host = sentinel.host
|
|
28
21
|
self.client.ssl = False
|
|
29
22
|
self.client._starttls_done = False
|
|
30
|
-
self.client._imap.
|
|
31
|
-
b"start TLS negotiation"
|
|
32
|
-
]
|
|
23
|
+
self.client._imap.starttls.return_value = "OK", [b"start TLS negotiation"]
|
|
33
24
|
self.client._cached_capabilities = [b"STARTTLS"]
|
|
34
25
|
|
|
35
26
|
def test_works(self):
|
|
36
27
|
resp = self.client.starttls(sentinel.ssl_context)
|
|
37
28
|
|
|
38
|
-
self.
|
|
39
|
-
sentinel.
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
self.client._imap.starttls.assert_called_once_with(
|
|
30
|
+
ssl_context=sentinel.ssl_context
|
|
31
|
+
)
|
|
32
|
+
self.tls.create_default_context.assert_not_called()
|
|
33
|
+
self.assertTrue(self.client._starttls_done)
|
|
34
|
+
self.assertEqual(resp, b"start TLS negotiation")
|
|
35
|
+
|
|
36
|
+
def test_default_context_is_used(self):
|
|
37
|
+
resp = self.client.starttls()
|
|
38
|
+
|
|
39
|
+
self.client._imap.starttls.assert_called_once_with(
|
|
40
|
+
ssl_context=self.tls.create_default_context.return_value
|
|
42
41
|
)
|
|
43
|
-
self.new_sock.makefile.assert_called_once_with("rb")
|
|
44
|
-
self.assertEqual(self.client._imap.file, sentinel.file)
|
|
45
42
|
self.assertEqual(resp, b"start TLS negotiation")
|
|
46
43
|
|
|
47
44
|
def test_command_fails(self):
|
|
48
|
-
self.client._imap.
|
|
45
|
+
self.client._imap.starttls.return_value = "NO", [b"sorry"]
|
|
49
46
|
|
|
50
47
|
with self.assertRaises(IMAPClientError) as raised:
|
|
51
48
|
self.client.starttls(sentinel.ssl_context)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Copyright (c) 2025, Menno Smits
|
|
2
|
+
# Released subject to the New BSD License
|
|
3
|
+
# Please see http://en.wikipedia.org/wiki/BSD_licenses
|
|
4
|
+
|
|
5
|
+
"""Integration tests for starttls() over a real socket pair.
|
|
6
|
+
|
|
7
|
+
These exercise the actual imaplib starttls code path (socket wrap plus
|
|
8
|
+
read-buffer refresh), which unit tests with mocked IMAP4 objects cannot
|
|
9
|
+
cover. This is the path that broke on Python 3.14, where IMAP4.file
|
|
10
|
+
became a read-only property.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import socket
|
|
15
|
+
import ssl
|
|
16
|
+
import sys
|
|
17
|
+
import threading
|
|
18
|
+
import unittest
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from imapclient.exceptions import IMAPClientError
|
|
22
|
+
from imapclient.imapclient import IMAPClient
|
|
23
|
+
|
|
24
|
+
CERT_DIR = Path(__file__).parent / "certs"
|
|
25
|
+
CA_FILE = CERT_DIR / "ca.pem"
|
|
26
|
+
SERVER_CERT = CERT_DIR / "server.pem"
|
|
27
|
+
SERVER_KEY = CERT_DIR / "server.key"
|
|
28
|
+
|
|
29
|
+
PRE_TLS_CAPS = b"IMAP4rev1 STARTTLS AUTH=PLAIN"
|
|
30
|
+
POST_TLS_CAPS = b"IMAP4rev1 AUTH=PLAIN"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FakeSTARTTLSServer:
|
|
34
|
+
"""Minimal IMAP4 server that upgrades to TLS when told STARTTLS.
|
|
35
|
+
|
|
36
|
+
Serves one connection per instance. Post-TLS CAPABILITY responses omit
|
|
37
|
+
STARTTLS, mirroring what real servers advertise once the connection is
|
|
38
|
+
secured.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self):
|
|
42
|
+
self.refuse_tls = False
|
|
43
|
+
self.error = None
|
|
44
|
+
self.ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
45
|
+
self.ctx.load_cert_chain(str(SERVER_CERT), str(SERVER_KEY))
|
|
46
|
+
self.sock = socket.socket()
|
|
47
|
+
self.sock.bind(("127.0.0.1", 0))
|
|
48
|
+
self.sock.listen(1)
|
|
49
|
+
self.port = self.sock.getsockname()[1]
|
|
50
|
+
self.thread = threading.Thread(target=self._serve, daemon=True)
|
|
51
|
+
self.thread.start()
|
|
52
|
+
|
|
53
|
+
def _reply_caps(self, conn, tag, caps):
|
|
54
|
+
conn.sendall(b"* CAPABILITY " + caps + b"\r\n")
|
|
55
|
+
conn.sendall(tag + b" OK CAPABILITY completed\r\n")
|
|
56
|
+
|
|
57
|
+
def _serve(self):
|
|
58
|
+
try:
|
|
59
|
+
conn, _ = self.sock.accept()
|
|
60
|
+
conn.sendall(
|
|
61
|
+
b"* OK [CAPABILITY " + PRE_TLS_CAPS + b"] fake server ready\r\n"
|
|
62
|
+
)
|
|
63
|
+
f = conn.makefile("rb")
|
|
64
|
+
while True:
|
|
65
|
+
line = f.readline()
|
|
66
|
+
if not line:
|
|
67
|
+
break
|
|
68
|
+
parts = line.split(None, 1)
|
|
69
|
+
tag = parts[0]
|
|
70
|
+
cmd = parts[1].strip().upper() if len(parts) > 1 else b""
|
|
71
|
+
if cmd.startswith(b"CAPABILITY"):
|
|
72
|
+
self._reply_caps(conn, tag, PRE_TLS_CAPS)
|
|
73
|
+
elif cmd.startswith(b"STARTTLS"):
|
|
74
|
+
if self.refuse_tls:
|
|
75
|
+
conn.sendall(tag + b" NO TLS refused\r\n")
|
|
76
|
+
continue
|
|
77
|
+
conn.sendall(tag + b" OK Begin TLS now\r\n")
|
|
78
|
+
self._serve_tls(self.ctx.wrap_socket(conn, server_side=True))
|
|
79
|
+
return
|
|
80
|
+
elif cmd.startswith(b"LOGOUT"):
|
|
81
|
+
conn.sendall(b"* BYE fake server signing off\r\n")
|
|
82
|
+
conn.sendall(tag + b" OK LOGOUT completed\r\n")
|
|
83
|
+
conn.close()
|
|
84
|
+
return
|
|
85
|
+
else:
|
|
86
|
+
conn.sendall(tag + b" OK done\r\n")
|
|
87
|
+
except Exception as e:
|
|
88
|
+
self.error = e
|
|
89
|
+
|
|
90
|
+
def _serve_tls(self, conn):
|
|
91
|
+
f = conn.makefile("rb")
|
|
92
|
+
while True:
|
|
93
|
+
line = f.readline()
|
|
94
|
+
if not line:
|
|
95
|
+
break
|
|
96
|
+
parts = line.split(None, 1)
|
|
97
|
+
tag = parts[0]
|
|
98
|
+
cmd = parts[1].strip().upper() if len(parts) > 1 else b""
|
|
99
|
+
if cmd.startswith(b"CAPABILITY"):
|
|
100
|
+
self._reply_caps(conn, tag, POST_TLS_CAPS)
|
|
101
|
+
elif cmd.startswith(b"LOGOUT"):
|
|
102
|
+
conn.sendall(b"* BYE fake server signing off\r\n")
|
|
103
|
+
conn.sendall(tag + b" OK LOGOUT completed\r\n")
|
|
104
|
+
conn.close()
|
|
105
|
+
return
|
|
106
|
+
else:
|
|
107
|
+
conn.sendall(tag + b" OK done\r\n")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@unittest.skipIf(
|
|
111
|
+
sys.version_info < (3, 9),
|
|
112
|
+
"imaplib.IMAP4 gained the timeout parameter in Python 3.9",
|
|
113
|
+
)
|
|
114
|
+
class StarttlsSocketTests(unittest.TestCase):
|
|
115
|
+
def setUp(self):
|
|
116
|
+
self.server = FakeSTARTTLSServer()
|
|
117
|
+
self._saved_cert_file = os.environ.pop("SSL_CERT_FILE", None)
|
|
118
|
+
os.environ["SSL_CERT_FILE"] = str(CA_FILE)
|
|
119
|
+
|
|
120
|
+
def tearDown(self):
|
|
121
|
+
if self._saved_cert_file is not None:
|
|
122
|
+
os.environ["SSL_CERT_FILE"] = self._saved_cert_file
|
|
123
|
+
self.server.sock.close()
|
|
124
|
+
self.assertIsNone(self.server.error)
|
|
125
|
+
|
|
126
|
+
def _client(self):
|
|
127
|
+
return IMAPClient("localhost", self.server.port, ssl=False)
|
|
128
|
+
|
|
129
|
+
def test_starttls_with_default_context(self):
|
|
130
|
+
client = self._client()
|
|
131
|
+
|
|
132
|
+
client.starttls()
|
|
133
|
+
|
|
134
|
+
sock = client._imap.sock
|
|
135
|
+
self.assertIsInstance(sock, ssl.SSLSocket)
|
|
136
|
+
# IMAPClient's documented default: hostname checking and certificate
|
|
137
|
+
# verification ON. imaplib's own None-fallback is unverified.
|
|
138
|
+
self.assertTrue(sock.context.check_hostname)
|
|
139
|
+
self.assertEqual(sock.context.verify_mode, ssl.CERT_REQUIRED)
|
|
140
|
+
|
|
141
|
+
self.assertNotIn(b"STARTTLS", client.capabilities())
|
|
142
|
+
client.login("user", "pass")
|
|
143
|
+
client.logout()
|
|
144
|
+
|
|
145
|
+
def test_starttls_with_custom_context(self):
|
|
146
|
+
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
147
|
+
context.check_hostname = False
|
|
148
|
+
context.verify_mode = ssl.CERT_NONE
|
|
149
|
+
|
|
150
|
+
client = self._client()
|
|
151
|
+
client.starttls(context)
|
|
152
|
+
|
|
153
|
+
self.assertIs(client._imap.sock.context, context)
|
|
154
|
+
client.login("user", "pass")
|
|
155
|
+
client.logout()
|
|
156
|
+
|
|
157
|
+
def test_starttls_refused_raises_imapclient_error(self):
|
|
158
|
+
self.server.refuse_tls = True
|
|
159
|
+
client = self._client()
|
|
160
|
+
|
|
161
|
+
with self.assertRaises(IMAPClientError):
|
|
162
|
+
client.starttls()
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Copyright (c) 2025, Menno Smits
|
|
2
|
+
# Released subject to the New BSD License
|
|
3
|
+
# Please see http://en.wikipedia.org/wiki/BSD_licenses
|
|
4
|
+
|
|
5
|
+
"""Integration tests for IMAPClient(..., ssl=True) over a real socket pair.
|
|
6
|
+
|
|
7
|
+
These exercise the actual TLS-from-connect code path (tls.IMAP4_TLS socket
|
|
8
|
+
wrap), which unit tests with mocked IMAP4 objects cannot cover. They mirror
|
|
9
|
+
tests/test_starttls_integration.py, but with TLS established at connection
|
|
10
|
+
time instead of via the STARTTLS command.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import socket
|
|
15
|
+
import ssl
|
|
16
|
+
import sys
|
|
17
|
+
import threading
|
|
18
|
+
import unittest
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from imapclient.imapclient import IMAPClient
|
|
22
|
+
|
|
23
|
+
CERT_DIR = Path(__file__).parent / "certs"
|
|
24
|
+
CA_FILE = CERT_DIR / "ca.pem"
|
|
25
|
+
SERVER_CERT = CERT_DIR / "server.pem"
|
|
26
|
+
SERVER_KEY = CERT_DIR / "server.key"
|
|
27
|
+
|
|
28
|
+
CAPS = b"IMAP4rev1 AUTH=PLAIN"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FakeTLSServer:
|
|
32
|
+
"""Minimal IMAP4 server that speaks TLS from the moment it accepts.
|
|
33
|
+
|
|
34
|
+
Serves one connection per instance. TLS handshake failures (expected
|
|
35
|
+
when the client rejects the server certificate) are recorded in
|
|
36
|
+
tls_error rather than error so tearDown can ignore them.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self):
|
|
40
|
+
self.error = None
|
|
41
|
+
self.tls_error = None
|
|
42
|
+
self.ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
43
|
+
self.ctx.load_cert_chain(str(SERVER_CERT), str(SERVER_KEY))
|
|
44
|
+
self.sock = socket.socket()
|
|
45
|
+
self.sock.bind(("127.0.0.1", 0))
|
|
46
|
+
self.sock.listen(1)
|
|
47
|
+
self.port = self.sock.getsockname()[1]
|
|
48
|
+
self.thread = threading.Thread(target=self._serve, daemon=True)
|
|
49
|
+
self.thread.start()
|
|
50
|
+
|
|
51
|
+
def _serve(self):
|
|
52
|
+
try:
|
|
53
|
+
conn, _ = self.sock.accept()
|
|
54
|
+
try:
|
|
55
|
+
conn = self.ctx.wrap_socket(conn, server_side=True)
|
|
56
|
+
except ssl.SSLError as e:
|
|
57
|
+
self.tls_error = e
|
|
58
|
+
conn.close()
|
|
59
|
+
return
|
|
60
|
+
conn.sendall(b"* OK [CAPABILITY " + CAPS + b"] fake server ready\r\n")
|
|
61
|
+
f = conn.makefile("rb")
|
|
62
|
+
while True:
|
|
63
|
+
line = f.readline()
|
|
64
|
+
if not line:
|
|
65
|
+
break
|
|
66
|
+
parts = line.split(None, 1)
|
|
67
|
+
tag = parts[0]
|
|
68
|
+
cmd = parts[1].strip().upper() if len(parts) > 1 else b""
|
|
69
|
+
if cmd.startswith(b"CAPABILITY"):
|
|
70
|
+
conn.sendall(b"* CAPABILITY " + CAPS + b"\r\n")
|
|
71
|
+
conn.sendall(tag + b" OK CAPABILITY completed\r\n")
|
|
72
|
+
elif cmd.startswith(b"LOGOUT"):
|
|
73
|
+
conn.sendall(b"* BYE fake server signing off\r\n")
|
|
74
|
+
conn.sendall(tag + b" OK LOGOUT completed\r\n")
|
|
75
|
+
conn.close()
|
|
76
|
+
return
|
|
77
|
+
else:
|
|
78
|
+
conn.sendall(tag + b" OK done\r\n")
|
|
79
|
+
except Exception as e:
|
|
80
|
+
self.error = e
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@unittest.skipIf(
|
|
84
|
+
sys.version_info < (3, 9),
|
|
85
|
+
"imaplib.IMAP4 gained the timeout parameter in Python 3.9",
|
|
86
|
+
)
|
|
87
|
+
class TlsSocketTests(unittest.TestCase):
|
|
88
|
+
def setUp(self):
|
|
89
|
+
self.server = FakeTLSServer()
|
|
90
|
+
self._saved_cert_file = os.environ.pop("SSL_CERT_FILE", None)
|
|
91
|
+
os.environ["SSL_CERT_FILE"] = str(CA_FILE)
|
|
92
|
+
|
|
93
|
+
def tearDown(self):
|
|
94
|
+
if self._saved_cert_file is not None:
|
|
95
|
+
os.environ["SSL_CERT_FILE"] = self._saved_cert_file
|
|
96
|
+
self.server.sock.close()
|
|
97
|
+
self.assertIsNone(self.server.error)
|
|
98
|
+
|
|
99
|
+
def _client(self, **kwargs):
|
|
100
|
+
return IMAPClient("localhost", self.server.port, ssl=True, **kwargs)
|
|
101
|
+
|
|
102
|
+
def test_ssl_connect_with_default_context(self):
|
|
103
|
+
client = self._client()
|
|
104
|
+
|
|
105
|
+
sock = client._imap.sock
|
|
106
|
+
self.assertIsInstance(sock, ssl.SSLSocket)
|
|
107
|
+
# IMAPClient's documented default: hostname checking and certificate
|
|
108
|
+
# verification ON. imaplib's own None-fallback is unverified.
|
|
109
|
+
self.assertTrue(sock.context.check_hostname)
|
|
110
|
+
self.assertEqual(sock.context.verify_mode, ssl.CERT_REQUIRED)
|
|
111
|
+
|
|
112
|
+
self.assertIn(b"IMAP4REV1", client.capabilities())
|
|
113
|
+
client.login("user", "pass")
|
|
114
|
+
client.logout()
|
|
115
|
+
|
|
116
|
+
def test_ssl_connect_with_custom_context(self):
|
|
117
|
+
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
118
|
+
context.check_hostname = False
|
|
119
|
+
context.verify_mode = ssl.CERT_NONE
|
|
120
|
+
|
|
121
|
+
client = self._client(ssl_context=context)
|
|
122
|
+
|
|
123
|
+
self.assertIs(client._imap.sock.context, context)
|
|
124
|
+
client.login("user", "pass")
|
|
125
|
+
client.logout()
|
|
126
|
+
|
|
127
|
+
def test_ssl_connect_untrusted_cert_fails(self):
|
|
128
|
+
# Without the test CA in the verification path, the default context
|
|
129
|
+
# must reject the server's self-signed certificate.
|
|
130
|
+
del os.environ["SSL_CERT_FILE"]
|
|
131
|
+
|
|
132
|
+
with self.assertRaises(ssl.SSLError):
|
|
133
|
+
self._client()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|