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.
- imapclient/__init__.py +12 -0
- imapclient/config.py +219 -0
- imapclient/datetime_util.py +70 -0
- imapclient/exceptions.py +43 -0
- imapclient/fixed_offset.py +45 -0
- imapclient/imap4.py +18 -0
- imapclient/imap_utf7.py +108 -0
- imapclient/imapclient.py +2080 -0
- imapclient/interact.py +155 -0
- imapclient/livetest.py +1207 -0
- imapclient/response_lexer.py +174 -0
- imapclient/response_parser.py +287 -0
- imapclient/response_types.py +154 -0
- imapclient/testable_imapclient.py +41 -0
- imapclient/tls.py +45 -0
- imapclient/typing_imapclient.py +4 -0
- imapclient/util.py +63 -0
- imapclient/version.py +17 -0
- imapclient-4.0.0.dist-info/METADATA +196 -0
- imapclient-4.0.0.dist-info/RECORD +25 -0
- imapclient-4.0.0.dist-info/WHEEL +5 -0
- imapclient-4.0.0.dist-info/entry_points.txt +3 -0
- imapclient-4.0.0.dist-info/licenses/AUTHORS.rst +51 -0
- imapclient-4.0.0.dist-info/licenses/COPYING +24 -0
- imapclient-4.0.0.dist-info/top_level.txt +1 -0
|
@@ -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)
|