IMAPClient 4.0.0__py3-none-any.whl

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.
@@ -0,0 +1,174 @@
1
+ # Copyright (c) 2014, Menno Smits
2
+ # Released subject to the New BSD License
3
+ # Please see http://en.wikipedia.org/wiki/BSD_licenses
4
+
5
+ """
6
+ A lexical analyzer class for IMAP responses.
7
+
8
+ Although Lexer does all the work, TokenSource is the class to use for
9
+ external callers.
10
+ """
11
+
12
+ from typing import Iterator, List, Optional, Tuple, TYPE_CHECKING, Union
13
+
14
+ from .util import assert_imap_protocol
15
+
16
+ __all__ = ["TokenSource"]
17
+
18
+ CTRL_CHARS = frozenset(c for c in range(32))
19
+ ALL_CHARS = frozenset(c for c in range(256))
20
+ SPECIALS = frozenset(c for c in b' ()%"[')
21
+ NON_SPECIALS = ALL_CHARS - SPECIALS - CTRL_CHARS
22
+ WHITESPACE = frozenset(c for c in b" \t\r\n")
23
+
24
+ BACKSLASH = ord("\\")
25
+ OPEN_SQUARE = ord("[")
26
+ CLOSE_SQUARE = ord("]")
27
+ DOUBLE_QUOTE = ord('"')
28
+
29
+
30
+ class TokenSource:
31
+ """
32
+ A simple iterator for the Lexer class that also provides access to
33
+ the current IMAP literal.
34
+ """
35
+
36
+ def __init__(self, text: List[bytes]):
37
+ self.lex = Lexer(text)
38
+ self.src = iter(self.lex)
39
+
40
+ @property
41
+ def current_literal(self) -> Optional[bytes]:
42
+ if TYPE_CHECKING:
43
+ assert self.lex.current_source is not None
44
+ return self.lex.current_source.literal
45
+
46
+ def __iter__(self) -> Iterator[bytes]:
47
+ return self.src
48
+
49
+
50
+ class Lexer:
51
+ """
52
+ A lexical analyzer class for IMAP
53
+ """
54
+
55
+ def __init__(self, text: List[bytes]):
56
+ self.sources = (LiteralHandlingIter(chunk) for chunk in text)
57
+ self.current_source: Optional[LiteralHandlingIter] = None
58
+
59
+ def read_until(
60
+ self, stream_i: "PushableIterator", end_char: int, escape: bool = True
61
+ ) -> bytearray:
62
+ token = bytearray()
63
+ try:
64
+ for nextchar in stream_i:
65
+ if escape and nextchar == BACKSLASH:
66
+ escaper = nextchar
67
+ nextchar = next(stream_i)
68
+ if nextchar not in (escaper, end_char):
69
+ token.append(escaper) # Don't touch invalid escaping
70
+ elif nextchar == end_char:
71
+ break
72
+ token.append(nextchar)
73
+ else:
74
+ raise ValueError("No closing '%s'" % chr(end_char))
75
+ except StopIteration:
76
+ raise ValueError("No closing '%s'" % chr(end_char))
77
+ token.append(end_char)
78
+ return token
79
+
80
+ def read_token_stream(self, stream_i: "PushableIterator") -> Iterator[bytearray]:
81
+ whitespace = WHITESPACE
82
+ wordchars = NON_SPECIALS
83
+ read_until = self.read_until
84
+
85
+ while True:
86
+ # Whitespace
87
+ for nextchar in stream_i:
88
+ if nextchar not in whitespace:
89
+ stream_i.push(nextchar)
90
+ break # done skipping over the whitespace
91
+
92
+ # Non-whitespace
93
+ token = bytearray()
94
+ for nextchar in stream_i:
95
+ if nextchar in wordchars:
96
+ token.append(nextchar)
97
+ elif nextchar == OPEN_SQUARE:
98
+ token.append(nextchar)
99
+ token.extend(read_until(stream_i, CLOSE_SQUARE, escape=False))
100
+ else:
101
+ if nextchar in whitespace:
102
+ yield token
103
+ elif nextchar == DOUBLE_QUOTE:
104
+ assert_imap_protocol(not token)
105
+ token.append(nextchar)
106
+ token.extend(read_until(stream_i, nextchar))
107
+ yield token
108
+ else:
109
+ # Other punctuation, eg. "(". This ends the current token.
110
+ if token:
111
+ yield token
112
+ yield bytearray([nextchar])
113
+ break
114
+ else:
115
+ if token:
116
+ yield token
117
+ break
118
+
119
+ def __iter__(self) -> Iterator[bytes]:
120
+ for source in self.sources:
121
+ self.current_source = source
122
+ for tok in self.read_token_stream(iter(source)):
123
+ yield bytes(tok)
124
+
125
+
126
+ # imaplib has poor handling of 'literals' - it both fails to remove the
127
+ # {size} marker, and fails to keep responses grouped into the same logical
128
+ # 'line'. What we end up with is a list of response 'records', where each
129
+ # record is either a simple string, or tuple of (str_with_lit, literal) -
130
+ # where str_with_lit is a string with the {xxx} marker at its end. Note
131
+ # that each element of this list does *not* correspond 1:1 with the
132
+ # untagged responses.
133
+ # (http://bugs.python.org/issue5045 also has comments about this)
134
+ # So: we have a special object for each of these records. When a
135
+ # string literal is processed, we peek into this object to grab the
136
+ # literal.
137
+ class LiteralHandlingIter:
138
+ def __init__(self, resp_record: Union[Tuple[bytes, bytes], bytes]):
139
+ self.literal: Optional[bytes]
140
+ if isinstance(resp_record, tuple):
141
+ # A 'record' with a string which includes a literal marker, and
142
+ # the literal itself.
143
+ self.src_text = resp_record[0]
144
+ assert_imap_protocol(self.src_text.endswith(b"}"), self.src_text)
145
+ self.literal = resp_record[1]
146
+ else:
147
+ # just a line with no literals.
148
+ self.src_text = resp_record
149
+ self.literal = None
150
+
151
+ def __iter__(self) -> "PushableIterator":
152
+ return PushableIterator(self.src_text)
153
+
154
+
155
+ class PushableIterator:
156
+ NO_MORE = object()
157
+
158
+ def __init__(self, it: bytes):
159
+ self.it = iter(it)
160
+ self.pushed: List[int] = []
161
+
162
+ def __iter__(self) -> "PushableIterator":
163
+ return self
164
+
165
+ def __next__(self) -> int:
166
+ if self.pushed:
167
+ return self.pushed.pop()
168
+ return next(self.it)
169
+
170
+ # For Python 2 compatibility
171
+ next = __next__
172
+
173
+ def push(self, item: int) -> None:
174
+ self.pushed.append(item)
@@ -0,0 +1,287 @@
1
+ # Copyright (c) 2014, Menno Smits
2
+ # Released subject to the New BSD License
3
+ # Please see http://en.wikipedia.org/wiki/BSD_licenses
4
+
5
+ """
6
+ Parsing for IMAP command responses with focus on FETCH responses as
7
+ returned by imaplib.
8
+
9
+ Initially inspired by http://effbot.org/zone/simple-iterator-parser.htm
10
+ """
11
+
12
+ # TODO more exact error reporting
13
+
14
+ import datetime
15
+ import re
16
+ import sys
17
+ from collections import defaultdict
18
+ from typing import cast, Dict, Iterator, List, Optional, Tuple, TYPE_CHECKING, Union
19
+
20
+ from .datetime_util import parse_to_datetime
21
+ from .exceptions import ProtocolError
22
+ from .response_lexer import TokenSource
23
+ from .response_types import Address, BodyData, Envelope, SearchIds
24
+ from .typing_imapclient import _Atom
25
+
26
+ __all__ = ["parse_response", "parse_message_list"]
27
+
28
+
29
+ def parse_response(data: List[bytes]) -> Tuple[_Atom, ...]:
30
+ """Pull apart IMAP command responses.
31
+
32
+ Returns nested tuples of appropriately typed objects.
33
+ """
34
+ if data == [None]:
35
+ return tuple()
36
+ return tuple(gen_parsed_response(data))
37
+
38
+
39
+ _msg_id_pattern = re.compile(r"(\d+(?: +\d+)*)")
40
+
41
+
42
+ def parse_message_list(data: List[Union[bytes, str]]) -> SearchIds:
43
+ """Parse a list of message ids and return them as a list.
44
+
45
+ parse_response is also capable of doing this but this is
46
+ faster. This also has special handling of the optional MODSEQ part
47
+ of a SEARCH response.
48
+
49
+ The returned list is a SearchIds instance which has a *modseq*
50
+ attribute which contains the MODSEQ response (if returned by the
51
+ server).
52
+ """
53
+ if len(data) != 1:
54
+ raise ValueError("unexpected message list data")
55
+
56
+ message_data = data[0]
57
+ if not message_data:
58
+ return SearchIds()
59
+
60
+ if isinstance(message_data, bytes):
61
+ message_data = message_data.decode("ascii")
62
+
63
+ m = _msg_id_pattern.match(message_data)
64
+ if not m:
65
+ raise ValueError("unexpected message list format")
66
+
67
+ ids = SearchIds(int(n) for n in m.group(1).split())
68
+
69
+ # Parse any non-numeric part on the end using parse_response (this
70
+ # is likely to be the MODSEQ section).
71
+ extra = message_data[m.end(1) :]
72
+ if extra:
73
+ for item in parse_response([extra.encode("ascii")]):
74
+ if (
75
+ isinstance(item, tuple)
76
+ and len(item) == 2
77
+ and cast(bytes, item[0]).lower() == b"modseq"
78
+ ):
79
+ if TYPE_CHECKING:
80
+ assert isinstance(item[1], int)
81
+ ids.modseq = item[1]
82
+ elif isinstance(item, int):
83
+ ids.append(item)
84
+ return ids
85
+
86
+
87
+ def gen_parsed_response(text: List[bytes]) -> Iterator[_Atom]:
88
+ if not text:
89
+ return
90
+ src = TokenSource(text)
91
+
92
+ token = None
93
+ try:
94
+ for token in src:
95
+ yield atom(src, token)
96
+ except ProtocolError:
97
+ raise
98
+ except ValueError:
99
+ _, err, _ = sys.exc_info()
100
+ raise ProtocolError("%s: %r" % (str(err), token))
101
+
102
+
103
+ _ParseFetchResponseInnerDict = Dict[
104
+ bytes, Optional[Union[datetime.datetime, int, BodyData, Envelope, _Atom]]
105
+ ]
106
+
107
+
108
+ def parse_fetch_response(
109
+ text: List[bytes], normalise_times: bool = True, uid_is_key: bool = True
110
+ ) -> "defaultdict[int, _ParseFetchResponseInnerDict]":
111
+ """Pull apart IMAP FETCH responses as returned by imaplib.
112
+
113
+ Returns a dictionary, keyed by message ID. Each value a dictionary
114
+ keyed by FETCH field type (eg."RFC822").
115
+ """
116
+ if text == [None]:
117
+ return defaultdict()
118
+ response = gen_parsed_response(text)
119
+
120
+ parsed_response: "defaultdict[int, _ParseFetchResponseInnerDict]" = defaultdict(
121
+ dict
122
+ )
123
+ while True:
124
+ try:
125
+ msg_id = seq = _int_or_error(next(response), "invalid message ID")
126
+ except StopIteration:
127
+ break
128
+
129
+ try:
130
+ msg_response = next(response)
131
+ except StopIteration:
132
+ raise ProtocolError("unexpected EOF")
133
+
134
+ if not isinstance(msg_response, tuple):
135
+ raise ProtocolError("bad response type: %s" % repr(msg_response))
136
+ if len(msg_response) % 2:
137
+ raise ProtocolError(
138
+ "uneven number of response items: %s" % repr(msg_response)
139
+ )
140
+
141
+ # always return the sequence of the message, so it is available
142
+ # even if we return keyed by UID.
143
+ msg_data: _ParseFetchResponseInnerDict = {b"SEQ": seq}
144
+ for i in range(0, len(msg_response), 2):
145
+ msg_attribute = msg_response[i]
146
+ if TYPE_CHECKING:
147
+ assert isinstance(msg_attribute, bytes)
148
+ word = msg_attribute.upper()
149
+ value = msg_response[i + 1]
150
+
151
+ if word == b"UID":
152
+ uid = _int_or_error(value, "invalid UID")
153
+ if uid_is_key:
154
+ msg_id = uid
155
+ else:
156
+ msg_data[word] = uid
157
+ elif word == b"INTERNALDATE":
158
+ msg_data[word] = _convert_INTERNALDATE(value, normalise_times)
159
+ elif word == b"ENVELOPE":
160
+ msg_data[word] = _convert_ENVELOPE(value, normalise_times)
161
+ elif word in (b"BODY", b"BODYSTRUCTURE"):
162
+ if TYPE_CHECKING:
163
+ assert isinstance(value, tuple)
164
+ msg_data[word] = BodyData.create(value)
165
+ else:
166
+ msg_data[word] = value
167
+
168
+ parsed_response[msg_id].update(msg_data)
169
+
170
+ return parsed_response
171
+
172
+
173
+ def _int_or_error(value: _Atom, error_text: str) -> int:
174
+ try:
175
+ return int(value) # type: ignore[arg-type]
176
+ except (TypeError, ValueError):
177
+ raise ProtocolError("%s: %s" % (error_text, repr(value)))
178
+
179
+
180
+ def _convert_INTERNALDATE(
181
+ date_string: _Atom, normalise_times: bool = True
182
+ ) -> Optional[datetime.datetime]:
183
+ if date_string is None:
184
+ return None
185
+
186
+ try:
187
+ if TYPE_CHECKING:
188
+ assert isinstance(date_string, bytes)
189
+ return parse_to_datetime(date_string, normalise=normalise_times)
190
+ except ValueError:
191
+ return None
192
+
193
+
194
+ def _convert_ENVELOPE(
195
+ envelope_response: _Atom, normalise_times: bool = True
196
+ ) -> Envelope:
197
+ if TYPE_CHECKING:
198
+ assert isinstance(envelope_response, tuple)
199
+ dt = None
200
+ if envelope_response[0]:
201
+ try:
202
+ if TYPE_CHECKING:
203
+ assert isinstance(envelope_response[0], bytes)
204
+ dt = parse_to_datetime(
205
+ envelope_response[0],
206
+ normalise=normalise_times,
207
+ )
208
+ except ValueError:
209
+ pass
210
+
211
+ subject = envelope_response[1]
212
+ in_reply_to = envelope_response[8]
213
+ message_id = envelope_response[9]
214
+ if TYPE_CHECKING:
215
+ assert isinstance(subject, bytes)
216
+ assert isinstance(in_reply_to, bytes)
217
+ assert isinstance(message_id, bytes)
218
+
219
+ # addresses contains a tuple of addresses
220
+ # from, sender, reply_to, to, cc, bcc headers
221
+ addresses: List[Optional[Tuple[Address, ...]]] = []
222
+ for addr_list in envelope_response[2:8]:
223
+ addrs = []
224
+ if addr_list:
225
+ if TYPE_CHECKING:
226
+ assert isinstance(addr_list, tuple)
227
+ for addr_tuple in addr_list:
228
+ if TYPE_CHECKING:
229
+ assert isinstance(addr_tuple, tuple)
230
+ if addr_tuple:
231
+ if TYPE_CHECKING:
232
+ addr_tuple = cast(Tuple[bytes, bytes, bytes, bytes], addr_tuple)
233
+ addrs.append(Address(*addr_tuple))
234
+ addresses.append(tuple(addrs))
235
+ else:
236
+ addresses.append(None)
237
+
238
+ return Envelope(
239
+ date=dt,
240
+ subject=subject,
241
+ from_=addresses[0],
242
+ sender=addresses[1],
243
+ reply_to=addresses[2],
244
+ to=addresses[3],
245
+ cc=addresses[4],
246
+ bcc=addresses[5],
247
+ in_reply_to=in_reply_to,
248
+ message_id=message_id,
249
+ )
250
+
251
+
252
+ def atom(src: TokenSource, token: bytes) -> _Atom:
253
+ if token == b"(":
254
+ return parse_tuple(src)
255
+ if token == b"NIL":
256
+ return None
257
+ if token[:1] == b"{":
258
+ literal_len = int(token[1:-1])
259
+ literal_text = src.current_literal
260
+ if literal_text is None:
261
+ raise ProtocolError("No literal corresponds to %r" % token)
262
+ if len(literal_text) != literal_len:
263
+ raise ProtocolError(
264
+ "Expecting literal of size %d, got %d"
265
+ % (literal_len, len(literal_text))
266
+ )
267
+ return literal_text
268
+ if len(token) >= 2 and (token[:1] == token[-1:] == b'"'):
269
+ return token[1:-1]
270
+ if token.isdigit() and (token[:1] != b"0" or len(token) == 1):
271
+ # this prevents converting items like 0123 to 123
272
+ return int(token)
273
+ return token
274
+
275
+
276
+ def parse_tuple(src: TokenSource) -> _Atom:
277
+ out: List[_Atom] = []
278
+ for token in src:
279
+ if token == b")":
280
+ return tuple(out)
281
+ out.append(atom(src, token))
282
+ # no terminator
283
+ raise ProtocolError('Tuple incomplete before "(%s"' % _fmt_tuple(out))
284
+
285
+
286
+ def _fmt_tuple(t: List[_Atom]) -> str:
287
+ return " ".join(str(item) for item in t)
@@ -0,0 +1,154 @@
1
+ # Copyright (c) 2014, Menno Smits
2
+ # Released subject to the New BSD License
3
+ # Please see http://en.wikipedia.org/wiki/BSD_licenses
4
+
5
+ import dataclasses
6
+ import datetime
7
+ from email.utils import formataddr
8
+ from typing import Any, List, Optional, Tuple, TYPE_CHECKING, Union
9
+
10
+ from .typing_imapclient import _Atom
11
+ from .util import to_unicode
12
+
13
+
14
+ @dataclasses.dataclass
15
+ class Envelope:
16
+ r"""Represents envelope structures of messages. Returned when parsing
17
+ ENVELOPE responses.
18
+
19
+ :ivar date: A datetime instance that represents the "Date" header.
20
+ :ivar subject: A string that contains the "Subject" header.
21
+ :ivar from\_: A tuple of Address objects that represent one or more
22
+ addresses from the "From" header, or None if header does not exist.
23
+ :ivar sender: As for from\_ but represents the "Sender" header.
24
+ :ivar reply_to: As for from\_ but represents the "Reply-To" header.
25
+ :ivar to: As for from\_ but represents the "To" header.
26
+ :ivar cc: As for from\_ but represents the "Cc" header.
27
+ :ivar bcc: As for from\_ but represents the "Bcc" recipients.
28
+ :ivar in_reply_to: A string that contains the "In-Reply-To" header.
29
+ :ivar message_id: A string that contains the "Message-Id" header.
30
+
31
+ A particular issue to watch out for is IMAP's handling of "group
32
+ syntax" in address fields. This is often encountered as a
33
+ recipient header of the form::
34
+
35
+ undisclosed-recipients:;
36
+
37
+ but can also be expressed per this more general example::
38
+
39
+ A group: a@example.com, B <b@example.org>;
40
+
41
+ This example would yield the following Address tuples::
42
+
43
+ Address(name=None, route=None, mailbox=u'A group', host=None)
44
+ Address(name=None, route=None, mailbox=u'a', host=u'example.com')
45
+ Address(name=u'B', route=None, mailbox=u'b', host=u'example.org')
46
+ Address(name=None, route=None, mailbox=None, host=None)
47
+
48
+ The first Address, where ``host`` is ``None``, indicates the start
49
+ of the group. The ``mailbox`` field contains the group name. The
50
+ final Address, where both ``mailbox`` and ``host`` are ``None``,
51
+ indicates the end of the group.
52
+
53
+ See :rfc:`3501#section-7.4.2` and :rfc:`2822` for further details.
54
+
55
+ """
56
+
57
+ date: Optional[datetime.datetime]
58
+ subject: bytes
59
+ from_: Optional[Tuple["Address", ...]]
60
+ sender: Optional[Tuple["Address", ...]]
61
+ reply_to: Optional[Tuple["Address", ...]]
62
+ to: Optional[Tuple["Address", ...]]
63
+ cc: Optional[Tuple["Address", ...]]
64
+ bcc: Optional[Tuple["Address", ...]]
65
+ in_reply_to: bytes
66
+ message_id: bytes
67
+
68
+
69
+ @dataclasses.dataclass
70
+ class Address:
71
+ """Represents electronic mail addresses. Used to store addresses in
72
+ :py:class:`Envelope`.
73
+
74
+ :ivar name: The address "personal name".
75
+ :ivar route: SMTP source route (rarely used).
76
+ :ivar mailbox: Mailbox name (what comes just before the @ sign).
77
+ :ivar host: The host/domain name.
78
+
79
+ As an example, an address header that looks like::
80
+
81
+ Mary Smith <mary@foo.com>
82
+
83
+ would be represented as::
84
+
85
+ Address(name=u'Mary Smith', route=None, mailbox=u'mary', host=u'foo.com')
86
+
87
+ See :rfc:`2822` for more detail.
88
+
89
+ See also :py:class:`Envelope` for information about handling of
90
+ "group syntax".
91
+ """
92
+
93
+ name: bytes
94
+ route: bytes
95
+ mailbox: bytes
96
+ host: bytes
97
+
98
+ def __str__(self) -> str:
99
+ if self.mailbox and self.host:
100
+ address = to_unicode(self.mailbox) + "@" + to_unicode(self.host)
101
+ else:
102
+ address = to_unicode(self.mailbox or self.host)
103
+
104
+ return formataddr((to_unicode(self.name), address))
105
+
106
+
107
+ class SearchIds(List[int]):
108
+ """
109
+ Contains a list of message ids as returned by IMAPClient.search().
110
+
111
+ The *modseq* attribute will contain the MODSEQ value returned by
112
+ the server (only if the SEARCH command sent involved the MODSEQ
113
+ criteria). See :rfc:`4551` for more details.
114
+ """
115
+
116
+ def __init__(self, *args: Any):
117
+ super().__init__(*args)
118
+ self.modseq: Optional[int] = None
119
+
120
+
121
+ _BodyDataType = Tuple[Union[bytes, int, "BodyData"], "_BodyDataType"]
122
+
123
+
124
+ class BodyData(_BodyDataType):
125
+ """
126
+ Returned when parsing BODY and BODYSTRUCTURE responses.
127
+ """
128
+
129
+ @classmethod
130
+ def create(cls, response: Tuple[_Atom, ...]) -> "BodyData":
131
+ # In case of multipart messages we will see at least 2 tuples
132
+ # at the start. Nest these in to a list so that the returned
133
+ # response tuple always has a consistent number of elements
134
+ # regardless of whether the message is multipart or not.
135
+ if isinstance(response[0], tuple):
136
+ # Multipart, find where the message part tuples stop
137
+ parts = []
138
+ for i, part in enumerate(response):
139
+ if isinstance(part, bytes):
140
+ break
141
+ if TYPE_CHECKING:
142
+ assert isinstance(part, tuple)
143
+ parts.append(part)
144
+ # The runtime representation of BodyData mixes converted
145
+ # BodyData/list items with raw, not-yet-converted response
146
+ # atoms. _BodyDataType can't precisely express this shape, so
147
+ # the construction below can't be typed exactly.
148
+ body_parts = ([cls.create(part) for part in parts],) + response[i:]
149
+ return cls(body_parts) # type: ignore[arg-type]
150
+ return cls(response) # type: ignore[arg-type]
151
+
152
+ @property
153
+ def is_multipart(self) -> bool:
154
+ return isinstance(self[0], list)
@@ -0,0 +1,41 @@
1
+ # Copyright (c) 2014, Menno Smits
2
+ # Released subject to the New BSD License
3
+ # Please see http://en.wikipedia.org/wiki/BSD_licenses
4
+
5
+ from typing import Any, Dict, List
6
+ from unittest.mock import Mock
7
+
8
+ from .imapclient import IMAPClient
9
+
10
+
11
+ class TestableIMAPClient(IMAPClient):
12
+ """Wrapper of :py:class:`imapclient.IMAPClient` that mocks all
13
+ interaction with real IMAP server.
14
+
15
+ This class should only be used in tests, where you can safely
16
+ interact with imapclient without running commands on a real
17
+ IMAP account.
18
+ """
19
+
20
+ def __init__(self) -> None:
21
+ super().__init__("somehost")
22
+
23
+ def _create_IMAP4(self) -> "MockIMAP4":
24
+ return MockIMAP4()
25
+
26
+
27
+ class MockIMAP4(Mock):
28
+ def __init__(self, *args: Any, **kwargs: Any):
29
+ super().__init__(*args, **kwargs)
30
+ self.use_uid = True
31
+ self.sent = b"" # Accumulates what was given to send()
32
+ self.tagged_commands: Dict[Any, Any] = {}
33
+ self.untagged_responses: Dict[Any, Any] = {}
34
+ self.capabilities: List[str] = []
35
+ self._starttls_done = False
36
+
37
+ def send(self, data: bytes) -> None:
38
+ self.sent += data
39
+
40
+ def _new_tag(self) -> str:
41
+ return "tag"
imapclient/tls.py ADDED
@@ -0,0 +1,45 @@
1
+ # Copyright (c) 2023, Menno Smits
2
+ # Released subject to the New BSD License
3
+ # Please see http://en.wikipedia.org/wiki/BSD_licenses
4
+
5
+ """
6
+ This module contains IMAPClient's functionality related to Transport
7
+ Layer Security (TLS a.k.a. SSL).
8
+ """
9
+
10
+ import imaplib
11
+ import socket
12
+ import ssl
13
+ from typing import Optional
14
+
15
+
16
+ def wrap_socket(
17
+ sock: socket.socket, ssl_context: Optional[ssl.SSLContext], host: str
18
+ ) -> socket.socket:
19
+ if ssl_context is None:
20
+ ssl_context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH)
21
+
22
+ return ssl_context.wrap_socket(sock, server_hostname=host)
23
+
24
+
25
+ class IMAP4_TLS(imaplib.IMAP4):
26
+ """IMAP4 client class for TLS/SSL connections.
27
+
28
+ Adapted from imaplib.IMAP4_SSL.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ host: str,
34
+ port: int = 993,
35
+ ssl_context: Optional[ssl.SSLContext] = None,
36
+ timeout: Optional[float] = None,
37
+ ):
38
+ self.ssl_context = ssl_context
39
+ self._timeout = timeout
40
+ super().__init__(host, port)
41
+
42
+ def _create_socket(self, timeout: Optional[float]) -> socket.socket:
43
+ sock = socket.create_connection((self.host, self.port), timeout=timeout)
44
+
45
+ return wrap_socket(sock, self.ssl_context, self.host)
@@ -0,0 +1,4 @@
1
+ from typing import Tuple, Union
2
+
3
+ _AtomPart = Union[None, int, bytes]
4
+ _Atom = Union[_AtomPart, Tuple["_Atom", ...]]