IMAPClient 4.0.1__tar.gz → 4.2.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.1 → imapclient-4.2.0}/AUTHORS.rst +1 -1
- {imapclient-4.0.1 → imapclient-4.2.0/IMAPClient.egg-info}/PKG-INFO +2 -1
- {imapclient-4.0.1 → imapclient-4.2.0}/IMAPClient.egg-info/SOURCES.txt +2 -0
- {imapclient-4.0.1/IMAPClient.egg-info → imapclient-4.2.0}/PKG-INFO +2 -1
- {imapclient-4.0.1 → imapclient-4.2.0}/imapclient/imapclient.py +49 -11
- {imapclient-4.0.1 → imapclient-4.2.0}/imapclient/tls.py +9 -3
- {imapclient-4.0.1 → imapclient-4.2.0}/pyproject.toml +3 -1
- {imapclient-4.0.1 → imapclient-4.2.0}/tests/test_imapclient.py +81 -6
- {imapclient-4.0.1 → imapclient-4.2.0}/tests/test_init.py +13 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/tests/test_starttls.py +15 -18
- imapclient-4.2.0/tests/test_starttls_integration.py +162 -0
- imapclient-4.2.0/tests/test_tls_integration.py +133 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/tests/test_util_functions.py +5 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/COPYING +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/IMAPClient.egg-info/dependency_links.txt +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/IMAPClient.egg-info/entry_points.txt +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/IMAPClient.egg-info/top_level.txt +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/README.rst +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/imapclient/__init__.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/imapclient/config.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/imapclient/datetime_util.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/imapclient/exceptions.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/imapclient/fixed_offset.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/imapclient/imap4.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/imapclient/imap_utf7.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/imapclient/interact.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/imapclient/livetest.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/imapclient/response_lexer.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/imapclient/response_parser.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/imapclient/response_types.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/imapclient/testable_imapclient.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/imapclient/typing_imapclient.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/imapclient/util.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/imapclient/version.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/setup.cfg +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/tests/test_auth.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/tests/test_datetime_util.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/tests/test_enable.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/tests/test_fixed_offset.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/tests/test_folder_status.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/tests/test_imap_utf7.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/tests/test_response_lexer.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/tests/test_response_parser.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/tests/test_search.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/tests/test_sort.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/tests/test_store.py +0 -0
- {imapclient-4.0.1 → imapclient-4.2.0}/tests/test_thread.py +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: IMAPClient
|
|
3
|
-
Version: 4.0
|
|
3
|
+
Version: 4.2.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>
|
|
7
|
+
License: BSD-3-Clause
|
|
7
8
|
Project-URL: Homepage, https://github.com/mjs/imapclient/
|
|
8
9
|
Keywords: imap,client,email,mail
|
|
9
10
|
Classifier: Development Status :: 5 - Production/Stable
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: IMAPClient
|
|
3
|
-
Version: 4.0
|
|
3
|
+
Version: 4.2.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>
|
|
7
|
+
License: BSD-3-Clause
|
|
7
8
|
Project-URL: Homepage, https://github.com/mjs/imapclient/
|
|
8
9
|
Keywords: imap,client,email,mail
|
|
9
10
|
Classifier: Development Status :: 5 - Production/Stable
|
|
@@ -42,6 +42,7 @@ __all__ = [
|
|
|
42
42
|
"FLAGGED",
|
|
43
43
|
"DRAFT",
|
|
44
44
|
"RECENT",
|
|
45
|
+
"literal",
|
|
45
46
|
]
|
|
46
47
|
|
|
47
48
|
|
|
@@ -321,6 +322,21 @@ class IMAPClient:
|
|
|
321
322
|
|
|
322
323
|
connect_timeout = getattr(self._timeout, "connect", None)
|
|
323
324
|
|
|
325
|
+
if sys.version_info < (3, 9):
|
|
326
|
+
# Support for Python 3.8
|
|
327
|
+
if connect_timeout:
|
|
328
|
+
raise ValueError(
|
|
329
|
+
"connection timeout is not supported with this version of Python"
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
if self.ssl:
|
|
333
|
+
return tls.IMAP4_TLS(
|
|
334
|
+
self.host,
|
|
335
|
+
self.port,
|
|
336
|
+
self.ssl_context,
|
|
337
|
+
)
|
|
338
|
+
return imap4.IMAP4WithTimeout(self.host, self.port)
|
|
339
|
+
|
|
324
340
|
if self.ssl:
|
|
325
341
|
return tls.IMAP4_TLS(
|
|
326
342
|
self.host,
|
|
@@ -328,7 +344,6 @@ class IMAPClient:
|
|
|
328
344
|
self.ssl_context,
|
|
329
345
|
connect_timeout,
|
|
330
346
|
)
|
|
331
|
-
|
|
332
347
|
return imap4.IMAP4WithTimeout(self.host, self.port, connect_timeout)
|
|
333
348
|
|
|
334
349
|
def _set_read_timeout(self):
|
|
@@ -378,13 +393,17 @@ class IMAPClient:
|
|
|
378
393
|
if self.ssl or self._starttls_done:
|
|
379
394
|
raise exceptions.IMAPClientAbortError("TLS session already established")
|
|
380
395
|
|
|
381
|
-
|
|
396
|
+
# Delegate the socket wrap to imaplib: it manages its own read buffer
|
|
397
|
+
# since Python 3.14 (where IMAP4.file became a read-only property).
|
|
398
|
+
# imaplib's own None-fallback context does not verify certificates, so
|
|
399
|
+
# pass IMAPClient's verifying default instead.
|
|
400
|
+
if ssl_context is None:
|
|
401
|
+
ssl_context = tls.create_default_context()
|
|
402
|
+
|
|
403
|
+
typ, data = self._imap.starttls(ssl_context=ssl_context)
|
|
382
404
|
self._checkok("starttls", typ, data)
|
|
383
405
|
|
|
384
406
|
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
407
|
return data[0]
|
|
389
408
|
|
|
390
409
|
def login(self, username: str, password: str):
|
|
@@ -1457,9 +1476,9 @@ class IMAPClient:
|
|
|
1457
1476
|
yield to_bytes(seq_to_parenstr(m["flags"]))
|
|
1458
1477
|
if "date" in m:
|
|
1459
1478
|
yield to_bytes('"%s"' % datetime_to_INTERNALDATE(m["date"]))
|
|
1460
|
-
yield
|
|
1479
|
+
yield literal(to_bytes(m["msg"]))
|
|
1461
1480
|
else:
|
|
1462
|
-
yield
|
|
1481
|
+
yield literal(to_bytes(m))
|
|
1463
1482
|
|
|
1464
1483
|
msgs = list(chunks())
|
|
1465
1484
|
|
|
@@ -1705,11 +1724,25 @@ class IMAPClient:
|
|
|
1705
1724
|
prefix.append(b"UID")
|
|
1706
1725
|
prefix.append(command)
|
|
1707
1726
|
|
|
1708
|
-
|
|
1709
|
-
|
|
1727
|
+
# Check every argument before anything goes on the wire: a bad
|
|
1728
|
+
# argument found after a literal was sent would leave the server
|
|
1729
|
+
# waiting inside a half-sent command.
|
|
1730
|
+
for item in itertools.chain(prefix, args):
|
|
1710
1731
|
if not isinstance(item, bytes):
|
|
1711
1732
|
raise ValueError("command args must be passed as bytes")
|
|
1733
|
+
if b"\x00" in item:
|
|
1734
|
+
raise ValueError("NUL is not allowed in command arguments")
|
|
1735
|
+
if not _is8bit(item) and (b"\r" in item or b"\n" in item):
|
|
1736
|
+
# CR and LF end the command line on the wire, so an argument
|
|
1737
|
+
# carrying them would be run as extra commands. A literal can
|
|
1738
|
+
# hold them, which is what the 8-bit path already sends.
|
|
1739
|
+
raise ValueError(
|
|
1740
|
+
"CR and LF are not allowed in command arguments; "
|
|
1741
|
+
"wrap the value in imapclient.literal to send it as a literal"
|
|
1742
|
+
)
|
|
1712
1743
|
|
|
1744
|
+
line = []
|
|
1745
|
+
for item, is_last in _iter_with_last(prefix + args):
|
|
1713
1746
|
if _is8bit(item):
|
|
1714
1747
|
# If a line was already started send it
|
|
1715
1748
|
if line:
|
|
@@ -1868,6 +1901,8 @@ def _normalise_search_criteria(criteria, charset=None):
|
|
|
1868
1901
|
inner[0] = b"(" + inner[0]
|
|
1869
1902
|
inner[-1] = inner[-1] + b")"
|
|
1870
1903
|
out.extend(inner) # flatten
|
|
1904
|
+
elif isinstance(item, literal):
|
|
1905
|
+
out.append(item)
|
|
1871
1906
|
else:
|
|
1872
1907
|
out.append(_quoted.maybe(to_bytes(item, charset)))
|
|
1873
1908
|
return out
|
|
@@ -1879,10 +1914,13 @@ def _normalise_sort_criteria(criteria, charset=None):
|
|
|
1879
1914
|
return b"(" + b" ".join(to_bytes(item).upper() for item in criteria) + b")"
|
|
1880
1915
|
|
|
1881
1916
|
|
|
1882
|
-
class
|
|
1917
|
+
class literal(bytes):
|
|
1883
1918
|
"""Hold message data that should always be sent as a literal."""
|
|
1884
1919
|
|
|
1885
1920
|
|
|
1921
|
+
_literal = literal # old private name, kept for callers that imported it
|
|
1922
|
+
|
|
1923
|
+
|
|
1886
1924
|
class _quoted(bytes):
|
|
1887
1925
|
"""
|
|
1888
1926
|
This class holds a quoted bytes value which provides access to the
|
|
@@ -1975,7 +2013,7 @@ def as_triplets(items):
|
|
|
1975
2013
|
|
|
1976
2014
|
|
|
1977
2015
|
def _is8bit(data):
|
|
1978
|
-
return isinstance(data,
|
|
2016
|
+
return isinstance(data, literal) or any(b > 127 for b in data)
|
|
1979
2017
|
|
|
1980
2018
|
|
|
1981
2019
|
def _iter_with_last(items):
|
|
@@ -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)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "IMAPClient"
|
|
3
3
|
description = "Easy-to-use, Pythonic and complete IMAP client library"
|
|
4
|
-
version = "4.0
|
|
4
|
+
version = "4.2.0"
|
|
5
|
+
license = {text = "BSD-3-Clause"}
|
|
5
6
|
authors = [
|
|
6
7
|
{name = "Menno Finlay-Smits", email = "hello@menno.io"},
|
|
7
8
|
]
|
|
@@ -46,6 +47,7 @@ requires = ["setuptools>=61.0"]
|
|
|
46
47
|
build-backend = "setuptools.build_meta"
|
|
47
48
|
|
|
48
49
|
[tool.setuptools]
|
|
50
|
+
license-files = ["COPYING", "AUTHORS.rst"]
|
|
49
51
|
packages = ["imapclient"]
|
|
50
52
|
|
|
51
53
|
[tool.setuptools.package-data]
|
|
@@ -15,18 +15,59 @@ from unittest.mock import Mock, patch, sentinel
|
|
|
15
15
|
from imapclient.exceptions import CapabilityError, IMAPClientError, ProtocolError
|
|
16
16
|
from imapclient.fixed_offset import FixedOffset
|
|
17
17
|
from imapclient.imapclient import (
|
|
18
|
-
_literal,
|
|
19
18
|
_parse_quota,
|
|
19
|
+
)
|
|
20
|
+
from imapclient.imapclient import IMAPClient as RealIMAPClient
|
|
21
|
+
from imapclient.imapclient import (
|
|
20
22
|
IMAPlibLoggerAdapter,
|
|
23
|
+
literal,
|
|
21
24
|
MailboxQuotaRoots,
|
|
22
25
|
Quota,
|
|
23
26
|
require_capability,
|
|
27
|
+
SocketTimeout,
|
|
24
28
|
)
|
|
25
29
|
from imapclient.testable_imapclient import TestableIMAPClient as IMAPClient
|
|
26
30
|
|
|
27
31
|
from .imapclient_test import IMAPClientTest
|
|
28
32
|
|
|
29
33
|
|
|
34
|
+
class TestCreateIMAP4(IMAPClientTest):
|
|
35
|
+
@patch("imapclient.imapclient.sys.version_info", (3, 8, 0))
|
|
36
|
+
@patch("imapclient.imapclient.tls.IMAP4_TLS")
|
|
37
|
+
@patch("imapclient.imapclient.imap4.IMAP4WithTimeout")
|
|
38
|
+
def test_python_38_does_not_pass_connect_timeout(self, plain_imap, tls_imap):
|
|
39
|
+
self.client._timeout = SocketTimeout(None, 30)
|
|
40
|
+
self.client.ssl_context = sentinel.ssl_context
|
|
41
|
+
|
|
42
|
+
self.client.ssl = True
|
|
43
|
+
self.assertIs(RealIMAPClient._create_IMAP4(self.client), tls_imap.return_value)
|
|
44
|
+
tls_imap.assert_called_once_with("somehost", 993, sentinel.ssl_context)
|
|
45
|
+
plain_imap.assert_not_called()
|
|
46
|
+
|
|
47
|
+
self.client.ssl = False
|
|
48
|
+
self.assertIs(
|
|
49
|
+
RealIMAPClient._create_IMAP4(self.client), plain_imap.return_value
|
|
50
|
+
)
|
|
51
|
+
plain_imap.assert_called_once_with("somehost", 993)
|
|
52
|
+
|
|
53
|
+
@patch("imapclient.imapclient.sys.version_info", (3, 8, 0))
|
|
54
|
+
@patch("imapclient.imapclient.tls.IMAP4_TLS")
|
|
55
|
+
@patch("imapclient.imapclient.imap4.IMAP4WithTimeout")
|
|
56
|
+
def test_python_38_rejects_connect_timeout(self, plain_imap, tls_imap):
|
|
57
|
+
self.client._timeout = SocketTimeout(10, 30)
|
|
58
|
+
|
|
59
|
+
for use_ssl in (True, False):
|
|
60
|
+
with self.subTest(ssl=use_ssl):
|
|
61
|
+
self.client.ssl = use_ssl
|
|
62
|
+
with self.assertRaisesRegex(
|
|
63
|
+
ValueError, "connection timeout is not supported"
|
|
64
|
+
):
|
|
65
|
+
RealIMAPClient._create_IMAP4(self.client)
|
|
66
|
+
|
|
67
|
+
tls_imap.assert_not_called()
|
|
68
|
+
plain_imap.assert_not_called()
|
|
69
|
+
|
|
70
|
+
|
|
30
71
|
class TestListFolders(IMAPClientTest):
|
|
31
72
|
def test_list_folders(self):
|
|
32
73
|
self.client._imap._simple_command.return_value = ("OK", [b"something"])
|
|
@@ -416,11 +457,11 @@ class TestAppend(IMAPClientTest):
|
|
|
416
457
|
b'"foobar"',
|
|
417
458
|
b"(FLAG WAVE)",
|
|
418
459
|
b'"05-Apr-2009 11:00:05 +0200"',
|
|
419
|
-
|
|
460
|
+
literal(b"msg1"),
|
|
420
461
|
b"(FLAG WAVE)",
|
|
421
|
-
|
|
462
|
+
literal(b"msg2"),
|
|
422
463
|
b'"05-Apr-2009 11:00:05 +0200"',
|
|
423
|
-
|
|
464
|
+
literal(b"msg3"),
|
|
424
465
|
],
|
|
425
466
|
uid=False,
|
|
426
467
|
)
|
|
@@ -749,6 +790,40 @@ class TestIdleAndNoop(IMAPClientTest):
|
|
|
749
790
|
self.assertListEqual([(99, b"EXISTS")], responses)
|
|
750
791
|
|
|
751
792
|
|
|
793
|
+
class TestRawCommandRejectsControlCharacters(IMAPClientTest):
|
|
794
|
+
def setUp(self):
|
|
795
|
+
super().setUp()
|
|
796
|
+
self.client._imap.send = Mock()
|
|
797
|
+
self.client._cached_capabilities = (b"IMAP4REV1",)
|
|
798
|
+
|
|
799
|
+
def test_crlf_in_a_line_argument_is_rejected_before_sending(self):
|
|
800
|
+
criterion = b'x"\r\nX1 STORE 1:* +FLAGS (\\Deleted)\r\nX2 EXPUNGE'
|
|
801
|
+
|
|
802
|
+
with self.assertRaises(ValueError):
|
|
803
|
+
self.client._raw_command(b"SEARCH", [b"HEADER", b"Message-ID", criterion])
|
|
804
|
+
|
|
805
|
+
self.client._imap.send.assert_not_called()
|
|
806
|
+
|
|
807
|
+
def test_crlf_via_search_criteria_is_rejected_before_sending(self):
|
|
808
|
+
with self.assertRaises(ValueError):
|
|
809
|
+
self.client.search(["HEADER", "Message-ID", 'x"\r\nX1 NOOP'])
|
|
810
|
+
|
|
811
|
+
self.client._imap.send.assert_not_called()
|
|
812
|
+
|
|
813
|
+
def test_nul_is_rejected_even_inside_a_literal(self):
|
|
814
|
+
with self.assertRaises(ValueError):
|
|
815
|
+
self.client._raw_command(b"SEARCH", [b"TEXT", literal(b"a\x00b")])
|
|
816
|
+
|
|
817
|
+
self.client._imap.send.assert_not_called()
|
|
818
|
+
|
|
819
|
+
def test_crlf_inside_a_literal_is_allowed(self):
|
|
820
|
+
self.client._send_literal = Mock()
|
|
821
|
+
|
|
822
|
+
self.client._raw_command(b"SEARCH", [b"TEXT", literal(b"line one\r\nline two")])
|
|
823
|
+
|
|
824
|
+
self.client._send_literal.assert_called_once()
|
|
825
|
+
|
|
826
|
+
|
|
752
827
|
class TestDebugLogging(IMAPClientTest):
|
|
753
828
|
def test_IMAP_is_patched(self):
|
|
754
829
|
# Remove all logging handlers so that the order of tests does not
|
|
@@ -1006,7 +1081,7 @@ class TestRawCommand(IMAPClientTest):
|
|
|
1006
1081
|
self.client._cached_capabilities = (b"LITERAL+",)
|
|
1007
1082
|
|
|
1008
1083
|
typ, data = self.client._raw_command(
|
|
1009
|
-
b"APPEND", [b"\xff",
|
|
1084
|
+
b"APPEND", [b"\xff", literal(b"hello")], uid=False
|
|
1010
1085
|
)
|
|
1011
1086
|
self.assertEqual(typ, "OK")
|
|
1012
1087
|
self.assertEqual(data, ["done"])
|
|
@@ -1020,7 +1095,7 @@ class TestRawCommand(IMAPClientTest):
|
|
|
1020
1095
|
|
|
1021
1096
|
typ, data = self.client._raw_command(
|
|
1022
1097
|
b"APPEND",
|
|
1023
|
-
[b"\xff",
|
|
1098
|
+
[b"\xff", literal(b"hello"), b"TEXT", literal(b"test")],
|
|
1024
1099
|
uid=False,
|
|
1025
1100
|
)
|
|
1026
1101
|
self.assertEqual(typ, "OK")
|
|
@@ -2,6 +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
|
+
import sys
|
|
5
6
|
import unittest
|
|
6
7
|
from unittest.mock import Mock, patch, sentinel
|
|
7
8
|
|
|
@@ -22,6 +23,9 @@ class TestInit(unittest.TestCase):
|
|
|
22
23
|
self.imaplib = patcher.start()
|
|
23
24
|
self.addCleanup(patcher.stop)
|
|
24
25
|
|
|
26
|
+
@unittest.skipIf(
|
|
27
|
+
sys.version_info < (3, 9), "connection timeouts require Python 3.9+"
|
|
28
|
+
)
|
|
25
29
|
def test_plain(self):
|
|
26
30
|
fakeIMAP4 = Mock()
|
|
27
31
|
self.imap4.IMAP4WithTimeout.return_value = fakeIMAP4
|
|
@@ -36,6 +40,9 @@ class TestInit(unittest.TestCase):
|
|
|
36
40
|
self.assertEqual(imap.ssl_context, None)
|
|
37
41
|
self.assertEqual(imap.stream, False)
|
|
38
42
|
|
|
43
|
+
@unittest.skipIf(
|
|
44
|
+
sys.version_info < (3, 9), "connection timeouts require Python 3.9+"
|
|
45
|
+
)
|
|
39
46
|
def test_plain_SocketTimeout(self):
|
|
40
47
|
fakeIMAP4 = Mock()
|
|
41
48
|
self.imap4.IMAP4WithTimeout.return_value = fakeIMAP4
|
|
@@ -51,6 +58,9 @@ class TestInit(unittest.TestCase):
|
|
|
51
58
|
"1.2.3.4", 143, sentinel.connect_timeout
|
|
52
59
|
)
|
|
53
60
|
|
|
61
|
+
@unittest.skipIf(
|
|
62
|
+
sys.version_info < (3, 9), "connection timeouts require Python 3.9+"
|
|
63
|
+
)
|
|
54
64
|
def test_SSL(self):
|
|
55
65
|
fakeIMAP4_TLS = Mock()
|
|
56
66
|
self.tls.IMAP4_TLS.return_value = fakeIMAP4_TLS
|
|
@@ -69,6 +79,9 @@ class TestInit(unittest.TestCase):
|
|
|
69
79
|
self.assertEqual(imap.ssl_context, sentinel.context)
|
|
70
80
|
self.assertEqual(imap.stream, False)
|
|
71
81
|
|
|
82
|
+
@unittest.skipIf(
|
|
83
|
+
sys.version_info < (3, 9), "connection timeouts require Python 3.9+"
|
|
84
|
+
)
|
|
72
85
|
def test_SSL_SocketTimeout(self):
|
|
73
86
|
fakeIMAP4_TLS = Mock()
|
|
74
87
|
self.tls.IMAP4_TLS.return_value = fakeIMAP4_TLS
|
|
@@ -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()
|
|
@@ -9,6 +9,7 @@ from imapclient.imapclient import (
|
|
|
9
9
|
_normalise_search_criteria,
|
|
10
10
|
_quoted,
|
|
11
11
|
join_message_ids,
|
|
12
|
+
literal,
|
|
12
13
|
normalise_text_list,
|
|
13
14
|
seq_to_parenstr,
|
|
14
15
|
seq_to_parenstr_upper,
|
|
@@ -139,6 +140,10 @@ class Test_normalise_search_criteria(unittest.TestCase):
|
|
|
139
140
|
def test_quoting(self):
|
|
140
141
|
self.check(["foo bar"], None, [_quoted(b'"foo bar"')])
|
|
141
142
|
|
|
143
|
+
def test_literal_is_not_quoted(self):
|
|
144
|
+
value = literal(b"line one\r\nline two")
|
|
145
|
+
self.check(["TEXT", value], None, [b"TEXT", value])
|
|
146
|
+
|
|
142
147
|
def test_ints(self):
|
|
143
148
|
self.check(["modseq", 500], None, [b"modseq", b"500"])
|
|
144
149
|
|
|
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
|