IMAPClient 4.1.0__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.
Files changed (47) hide show
  1. {imapclient-4.1.0 → imapclient-4.2.0/IMAPClient.egg-info}/PKG-INFO +2 -1
  2. {imapclient-4.1.0/IMAPClient.egg-info → imapclient-4.2.0}/PKG-INFO +2 -1
  3. {imapclient-4.1.0 → imapclient-4.2.0}/imapclient/imapclient.py +41 -7
  4. {imapclient-4.1.0 → imapclient-4.2.0}/pyproject.toml +3 -1
  5. {imapclient-4.1.0 → imapclient-4.2.0}/tests/test_imapclient.py +81 -6
  6. {imapclient-4.1.0 → imapclient-4.2.0}/tests/test_init.py +13 -0
  7. {imapclient-4.1.0 → imapclient-4.2.0}/tests/test_util_functions.py +5 -0
  8. {imapclient-4.1.0 → imapclient-4.2.0}/AUTHORS.rst +0 -0
  9. {imapclient-4.1.0 → imapclient-4.2.0}/COPYING +0 -0
  10. {imapclient-4.1.0 → imapclient-4.2.0}/IMAPClient.egg-info/SOURCES.txt +0 -0
  11. {imapclient-4.1.0 → imapclient-4.2.0}/IMAPClient.egg-info/dependency_links.txt +0 -0
  12. {imapclient-4.1.0 → imapclient-4.2.0}/IMAPClient.egg-info/entry_points.txt +0 -0
  13. {imapclient-4.1.0 → imapclient-4.2.0}/IMAPClient.egg-info/top_level.txt +0 -0
  14. {imapclient-4.1.0 → imapclient-4.2.0}/README.rst +0 -0
  15. {imapclient-4.1.0 → imapclient-4.2.0}/imapclient/__init__.py +0 -0
  16. {imapclient-4.1.0 → imapclient-4.2.0}/imapclient/config.py +0 -0
  17. {imapclient-4.1.0 → imapclient-4.2.0}/imapclient/datetime_util.py +0 -0
  18. {imapclient-4.1.0 → imapclient-4.2.0}/imapclient/exceptions.py +0 -0
  19. {imapclient-4.1.0 → imapclient-4.2.0}/imapclient/fixed_offset.py +0 -0
  20. {imapclient-4.1.0 → imapclient-4.2.0}/imapclient/imap4.py +0 -0
  21. {imapclient-4.1.0 → imapclient-4.2.0}/imapclient/imap_utf7.py +0 -0
  22. {imapclient-4.1.0 → imapclient-4.2.0}/imapclient/interact.py +0 -0
  23. {imapclient-4.1.0 → imapclient-4.2.0}/imapclient/livetest.py +0 -0
  24. {imapclient-4.1.0 → imapclient-4.2.0}/imapclient/response_lexer.py +0 -0
  25. {imapclient-4.1.0 → imapclient-4.2.0}/imapclient/response_parser.py +0 -0
  26. {imapclient-4.1.0 → imapclient-4.2.0}/imapclient/response_types.py +0 -0
  27. {imapclient-4.1.0 → imapclient-4.2.0}/imapclient/testable_imapclient.py +0 -0
  28. {imapclient-4.1.0 → imapclient-4.2.0}/imapclient/tls.py +0 -0
  29. {imapclient-4.1.0 → imapclient-4.2.0}/imapclient/typing_imapclient.py +0 -0
  30. {imapclient-4.1.0 → imapclient-4.2.0}/imapclient/util.py +0 -0
  31. {imapclient-4.1.0 → imapclient-4.2.0}/imapclient/version.py +0 -0
  32. {imapclient-4.1.0 → imapclient-4.2.0}/setup.cfg +0 -0
  33. {imapclient-4.1.0 → imapclient-4.2.0}/tests/test_auth.py +0 -0
  34. {imapclient-4.1.0 → imapclient-4.2.0}/tests/test_datetime_util.py +0 -0
  35. {imapclient-4.1.0 → imapclient-4.2.0}/tests/test_enable.py +0 -0
  36. {imapclient-4.1.0 → imapclient-4.2.0}/tests/test_fixed_offset.py +0 -0
  37. {imapclient-4.1.0 → imapclient-4.2.0}/tests/test_folder_status.py +0 -0
  38. {imapclient-4.1.0 → imapclient-4.2.0}/tests/test_imap_utf7.py +0 -0
  39. {imapclient-4.1.0 → imapclient-4.2.0}/tests/test_response_lexer.py +0 -0
  40. {imapclient-4.1.0 → imapclient-4.2.0}/tests/test_response_parser.py +0 -0
  41. {imapclient-4.1.0 → imapclient-4.2.0}/tests/test_search.py +0 -0
  42. {imapclient-4.1.0 → imapclient-4.2.0}/tests/test_sort.py +0 -0
  43. {imapclient-4.1.0 → imapclient-4.2.0}/tests/test_starttls.py +0 -0
  44. {imapclient-4.1.0 → imapclient-4.2.0}/tests/test_starttls_integration.py +0 -0
  45. {imapclient-4.1.0 → imapclient-4.2.0}/tests/test_store.py +0 -0
  46. {imapclient-4.1.0 → imapclient-4.2.0}/tests/test_thread.py +0 -0
  47. {imapclient-4.1.0 → imapclient-4.2.0}/tests/test_tls_integration.py +0 -0
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: IMAPClient
3
- Version: 4.1.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.1.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):
@@ -1461,9 +1476,9 @@ class IMAPClient:
1461
1476
  yield to_bytes(seq_to_parenstr(m["flags"]))
1462
1477
  if "date" in m:
1463
1478
  yield to_bytes('"%s"' % datetime_to_INTERNALDATE(m["date"]))
1464
- yield _literal(to_bytes(m["msg"]))
1479
+ yield literal(to_bytes(m["msg"]))
1465
1480
  else:
1466
- yield _literal(to_bytes(m))
1481
+ yield literal(to_bytes(m))
1467
1482
 
1468
1483
  msgs = list(chunks())
1469
1484
 
@@ -1709,11 +1724,25 @@ class IMAPClient:
1709
1724
  prefix.append(b"UID")
1710
1725
  prefix.append(command)
1711
1726
 
1712
- line = []
1713
- for item, is_last in _iter_with_last(prefix + args):
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):
1714
1731
  if not isinstance(item, bytes):
1715
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
+ )
1716
1743
 
1744
+ line = []
1745
+ for item, is_last in _iter_with_last(prefix + args):
1717
1746
  if _is8bit(item):
1718
1747
  # If a line was already started send it
1719
1748
  if line:
@@ -1872,6 +1901,8 @@ def _normalise_search_criteria(criteria, charset=None):
1872
1901
  inner[0] = b"(" + inner[0]
1873
1902
  inner[-1] = inner[-1] + b")"
1874
1903
  out.extend(inner) # flatten
1904
+ elif isinstance(item, literal):
1905
+ out.append(item)
1875
1906
  else:
1876
1907
  out.append(_quoted.maybe(to_bytes(item, charset)))
1877
1908
  return out
@@ -1883,10 +1914,13 @@ def _normalise_sort_criteria(criteria, charset=None):
1883
1914
  return b"(" + b" ".join(to_bytes(item).upper() for item in criteria) + b")"
1884
1915
 
1885
1916
 
1886
- class _literal(bytes):
1917
+ class literal(bytes):
1887
1918
  """Hold message data that should always be sent as a literal."""
1888
1919
 
1889
1920
 
1921
+ _literal = literal # old private name, kept for callers that imported it
1922
+
1923
+
1890
1924
  class _quoted(bytes):
1891
1925
  """
1892
1926
  This class holds a quoted bytes value which provides access to the
@@ -1979,7 +2013,7 @@ def as_triplets(items):
1979
2013
 
1980
2014
 
1981
2015
  def _is8bit(data):
1982
- return isinstance(data, _literal) or any(b > 127 for b in data)
2016
+ return isinstance(data, literal) or any(b > 127 for b in data)
1983
2017
 
1984
2018
 
1985
2019
  def _iter_with_last(items):
@@ -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.1.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
- _literal(b"msg1"),
460
+ literal(b"msg1"),
420
461
  b"(FLAG WAVE)",
421
- _literal(b"msg2"),
462
+ literal(b"msg2"),
422
463
  b'"05-Apr-2009 11:00:05 +0200"',
423
- _literal(b"msg3"),
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", _literal(b"hello")], uid=False
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", _literal(b"hello"), b"TEXT", _literal(b"test")],
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
@@ -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