synapse 2.219.0__py311-none-any.whl → 2.220.0__py311-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.
Potentially problematic release.
This version of synapse might be problematic. Click here for more details.
- synapse/data/__init__.py +4 -0
- synapse/data/lark/__init__.py +0 -0
- synapse/data/lark/imap.lark +8 -0
- synapse/exc.py +2 -0
- synapse/lib/json.py +6 -5
- synapse/lib/link.py +49 -50
- synapse/lib/parser.py +3 -5
- synapse/lib/storm.py +1 -0
- synapse/lib/stormlib/imap.py +476 -35
- synapse/lib/stormtypes.py +39 -0
- synapse/lib/version.py +2 -2
- synapse/tests/test_lib_grammar.py +2 -4
- synapse/tests/test_lib_json.py +29 -0
- synapse/tests/test_lib_storm.py +5 -1
- synapse/tests/test_lib_stormlib_imap.py +1307 -230
- synapse/tests/test_lib_stormtypes.py +32 -0
- synapse/utils/stormcov/plugin.py +2 -5
- {synapse-2.219.0.dist-info → synapse-2.220.0.dist-info}/METADATA +1 -2
- {synapse-2.219.0.dist-info → synapse-2.220.0.dist-info}/RECORD +23 -21
- /synapse/{lib → data/lark}/storm.lark +0 -0
- {synapse-2.219.0.dist-info → synapse-2.220.0.dist-info}/WHEEL +0 -0
- {synapse-2.219.0.dist-info → synapse-2.220.0.dist-info}/licenses/LICENSE +0 -0
- {synapse-2.219.0.dist-info → synapse-2.220.0.dist-info}/top_level.txt +0 -0
|
@@ -1,282 +1,1359 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import time
|
|
2
|
+
import fnmatch
|
|
3
|
+
import imaplib
|
|
4
|
+
import logging
|
|
5
|
+
import textwrap
|
|
6
|
+
import contextlib
|
|
3
7
|
|
|
4
|
-
|
|
8
|
+
import regex
|
|
5
9
|
|
|
6
|
-
import
|
|
10
|
+
from unittest import mock
|
|
7
11
|
|
|
12
|
+
import synapse.exc as s_exc
|
|
8
13
|
import synapse.common as s_common
|
|
9
14
|
|
|
15
|
+
import synapse.lib.link as s_link
|
|
16
|
+
import synapse.lib.stormlib.imap as s_imap
|
|
17
|
+
|
|
10
18
|
import synapse.tests.utils as s_test
|
|
11
19
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
imap_srv_rgx = regex.compile(
|
|
23
|
+
br'''
|
|
24
|
+
^
|
|
25
|
+
(?P<tag>\*|\+|[0-9a-pA-P]+)\s?
|
|
26
|
+
(?P<command>[A-Z]{2,})\s?
|
|
27
|
+
(?P<data>.*?)?
|
|
28
|
+
$
|
|
29
|
+
''',
|
|
30
|
+
flags=regex.VERBOSE
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
email = {
|
|
34
|
+
'headers': textwrap.dedent(
|
|
35
|
+
'''\
|
|
36
|
+
Date: Wed, 17 Jul 1996 02:23:25 -0700 (PDT)\r
|
|
37
|
+
From: Terry Gray <gray@cac.washington.edu>\r
|
|
38
|
+
Subject: IMAP4rev2 WG mtg summary and minutes\r
|
|
39
|
+
To: imap@cac.washington.edu\r
|
|
40
|
+
cc: minutes@CNRI.Reston.VA.US, John Klensin <KLENSIN@MIT.EDU>\r
|
|
41
|
+
Message-Id: <B27397-0100000@cac.washington.edu>\r
|
|
42
|
+
MIME-Version: 1.0\r
|
|
43
|
+
Content-Type: TEXT/PLAIN; CHARSET=US-ASCII\r
|
|
44
|
+
'''
|
|
45
|
+
),
|
|
46
|
+
|
|
47
|
+
'body': textwrap.dedent(
|
|
48
|
+
'''\
|
|
49
|
+
The meeting minutes are attached.
|
|
50
|
+
|
|
51
|
+
Thanks,
|
|
52
|
+
Terry
|
|
53
|
+
'''
|
|
54
|
+
),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class IMAPServer(s_imap.IMAPBase):
|
|
58
|
+
'''
|
|
59
|
+
This is an extremely naive IMAP server implementation used only for testing.
|
|
60
|
+
'''
|
|
61
|
+
|
|
62
|
+
async def postAnit(self):
|
|
63
|
+
self.mail = {
|
|
64
|
+
'user00@vertex.link': {
|
|
65
|
+
'password': 'pass00',
|
|
66
|
+
'mailboxes': {
|
|
67
|
+
'inbox': {
|
|
68
|
+
'parent': None,
|
|
69
|
+
'flags': ['\\HasChildren'],
|
|
70
|
+
},
|
|
71
|
+
'drafts': {
|
|
72
|
+
'parent': None,
|
|
73
|
+
'flags': ['\\HasNoChildren', '\\Drafts'],
|
|
74
|
+
},
|
|
75
|
+
'sent': {
|
|
76
|
+
'parent': None,
|
|
77
|
+
'flags': ['\\HasNoChildren', '\\Sent'],
|
|
78
|
+
},
|
|
79
|
+
'deleted': {
|
|
80
|
+
'parent': None,
|
|
81
|
+
'flags': ['\\HasNoChildren', '\\Trash'],
|
|
82
|
+
},
|
|
83
|
+
'retain': {
|
|
84
|
+
'parent': 'inbox',
|
|
85
|
+
'flags': ['\\HasNoChildren'],
|
|
86
|
+
},
|
|
87
|
+
'status reports': {
|
|
88
|
+
'parent': 'inbox',
|
|
89
|
+
'flags': ['\\HasNoChildren'],
|
|
90
|
+
},
|
|
91
|
+
'"important"': {
|
|
92
|
+
'parent': 'inbox',
|
|
93
|
+
'flags': ['\\HasNoChildren'],
|
|
94
|
+
},
|
|
95
|
+
'"junk mail"': {
|
|
96
|
+
'parent': 'inbox',
|
|
97
|
+
'flags': ['\\HasNoChildren'],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
'messages': {
|
|
101
|
+
1: {
|
|
102
|
+
'mailbox': 'inbox',
|
|
103
|
+
'flags': ['\\Answered', '\\Recent', '\\Seen'],
|
|
104
|
+
'data': email,
|
|
105
|
+
},
|
|
106
|
+
6: {
|
|
107
|
+
'mailbox': 'inbox',
|
|
108
|
+
'flags': [],
|
|
109
|
+
'body': '',
|
|
110
|
+
},
|
|
111
|
+
7: {
|
|
112
|
+
'mailbox': 'inbox',
|
|
113
|
+
'flags': [],
|
|
114
|
+
'body': '',
|
|
115
|
+
},
|
|
116
|
+
8: {
|
|
117
|
+
'mailbox': 'inbox',
|
|
118
|
+
'flags': [],
|
|
119
|
+
'body': '',
|
|
120
|
+
},
|
|
121
|
+
2: {
|
|
122
|
+
'mailbox': 'drafts',
|
|
123
|
+
'flags': ['\\Draft'],
|
|
124
|
+
'body': '',
|
|
125
|
+
},
|
|
126
|
+
3: {
|
|
127
|
+
'mailbox': 'deleted',
|
|
128
|
+
'flags': ['\\Deleted', '\\Seen'],
|
|
129
|
+
'body': '',
|
|
130
|
+
},
|
|
131
|
+
4: {
|
|
132
|
+
'mailbox': 'retain',
|
|
133
|
+
'flags': ['\\Seen'],
|
|
134
|
+
'body': '',
|
|
135
|
+
},
|
|
136
|
+
5: {
|
|
137
|
+
'mailbox': 'status reports',
|
|
138
|
+
'flags': [],
|
|
139
|
+
'body': '',
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
'user01@vertex.link': {
|
|
145
|
+
'password': 'spaces lol',
|
|
146
|
+
'mailboxes': {
|
|
147
|
+
'inbox': {
|
|
148
|
+
'readonly': True,
|
|
149
|
+
'parent': None,
|
|
150
|
+
'flags': ['\\HasChildren'],
|
|
151
|
+
},
|
|
152
|
+
'drafts': {
|
|
153
|
+
'parent': None,
|
|
154
|
+
'flags': ['\\HasNoChildren', '\\Drafts'],
|
|
155
|
+
},
|
|
156
|
+
'sent': {
|
|
157
|
+
'parent': None,
|
|
158
|
+
'flags': ['\\HasNoChildren', '\\Sent'],
|
|
159
|
+
},
|
|
160
|
+
'deleted': {
|
|
161
|
+
'parent': None,
|
|
162
|
+
'flags': ['\\HasNoChildren', '\\Trash'],
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
'messages': {
|
|
166
|
+
1: {
|
|
167
|
+
'mailbox': 'inbox',
|
|
168
|
+
'flags': ['\\Answered', '\\Recent', '\\Seen'],
|
|
169
|
+
'data': email,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
self.user = None
|
|
176
|
+
self.selected = None
|
|
177
|
+
self.validity = int(time.time())
|
|
178
|
+
|
|
179
|
+
self.state = 'NONAUTH'
|
|
180
|
+
|
|
181
|
+
self.capabilities = ['IMAP4rev1', 'AUTH=PLAIN']
|
|
182
|
+
|
|
183
|
+
def _parseLine(self, line):
|
|
184
|
+
match = imap_srv_rgx.match(line)
|
|
185
|
+
if match is None:
|
|
186
|
+
mesg = 'Unable to parse response from client.'
|
|
187
|
+
raise s_exc.ImapError(mesg=mesg, data=line)
|
|
188
|
+
|
|
189
|
+
mesg = match.groupdict()
|
|
190
|
+
|
|
191
|
+
tag = mesg.get('tag').decode()
|
|
192
|
+
mesg['tag'] = tag
|
|
193
|
+
mesg['command'] = mesg.get('command').decode()
|
|
194
|
+
|
|
195
|
+
logger.debug('%s SEND: %s', self.__class__.__name__, mesg)
|
|
196
|
+
|
|
197
|
+
return mesg
|
|
198
|
+
|
|
199
|
+
async def pack(self, mesg):
|
|
200
|
+
(tag, response, data, uid, code, size) = mesg
|
|
201
|
+
|
|
202
|
+
if uid is None:
|
|
203
|
+
uid = ''
|
|
204
|
+
else:
|
|
205
|
+
uid = f' {uid}'
|
|
206
|
+
|
|
207
|
+
if code is None:
|
|
208
|
+
code = ''
|
|
209
|
+
else:
|
|
210
|
+
code = f' [{code}]'
|
|
211
|
+
|
|
212
|
+
if size is None:
|
|
213
|
+
size = ''
|
|
214
|
+
else:
|
|
215
|
+
size = f' {size}'
|
|
216
|
+
|
|
217
|
+
return f'{tag}{uid} {response}{code} {data}{size}\r\n'.encode()
|
|
218
|
+
|
|
219
|
+
async def sendMesg(self, tag, response, data, uid=None, code=None, size=None):
|
|
220
|
+
await self.tx((tag, response, data, uid, code, size))
|
|
221
|
+
|
|
222
|
+
async def greet(self):
|
|
223
|
+
await self.sendMesg(s_imap.UNTAGGED, 'OK', 'SynImap ready.')
|
|
224
|
+
|
|
225
|
+
async def capability(self, mesg):
|
|
226
|
+
tag = mesg.get('tag')
|
|
227
|
+
await self.sendMesg(s_imap.UNTAGGED, 'CAPABILITY', ' '.join(self.capabilities))
|
|
228
|
+
await self.sendMesg(tag, 'OK', 'CAPABILITY completed')
|
|
229
|
+
|
|
230
|
+
async def login(self, mesg):
|
|
231
|
+
tag = mesg.get('tag')
|
|
232
|
+
try:
|
|
233
|
+
username, passwd = s_imap.qsplit(mesg.get('data').decode())
|
|
234
|
+
except ValueError:
|
|
235
|
+
return await self.sendMesg(tag, 'BAD', 'Invalid arguments for LOGIN.')
|
|
236
|
+
|
|
237
|
+
if (user := self.mail.get(username)) is None or user.get('password') != passwd:
|
|
238
|
+
return await self.sendMesg(tag, 'NO', 'Invalid credentials.', code='AUTHENTICATIONFAILED')
|
|
239
|
+
|
|
240
|
+
self.state = 'AUTH'
|
|
241
|
+
self.user = username
|
|
242
|
+
await self.sendMesg(tag, 'OK', 'LOGIN completed')
|
|
243
|
+
|
|
244
|
+
async def logout(self, mesg):
|
|
245
|
+
tag = mesg.get('tag')
|
|
246
|
+
await self.sendMesg(s_imap.UNTAGGED, 'BYE', f'bye, {self.user}!')
|
|
247
|
+
await self.sendMesg(tag, 'OK', 'LOGOUT completed')
|
|
248
|
+
self.user = None
|
|
249
|
+
await self.fini()
|
|
250
|
+
|
|
251
|
+
async def list(self, mesg):
|
|
252
|
+
tag = mesg.get('tag')
|
|
253
|
+
try:
|
|
254
|
+
refname, mboxname = s_imap.qsplit(mesg.get('data').decode())
|
|
255
|
+
except ValueError:
|
|
256
|
+
return await self.sendMesg(tag, 'BAD', 'Invalid arguments for LIST.')
|
|
257
|
+
|
|
258
|
+
parent = None
|
|
259
|
+
if refname:
|
|
260
|
+
parent = refname
|
|
261
|
+
|
|
262
|
+
mailboxes = self.mail.get(self.user).get('mailboxes')
|
|
263
|
+
|
|
264
|
+
matches = [k for k in mailboxes.items() if k[1].get('parent') == parent]
|
|
265
|
+
|
|
266
|
+
if mboxname:
|
|
267
|
+
matches = [k for k in matches if fnmatch.fnmatch(k[0], mboxname)]
|
|
268
|
+
|
|
269
|
+
matches = sorted(matches, key=lambda k: k[0])
|
|
270
|
+
|
|
271
|
+
for match in matches:
|
|
272
|
+
name = match[0]
|
|
273
|
+
if parent:
|
|
274
|
+
name = f'{parent}/{name}'
|
|
275
|
+
|
|
276
|
+
name = s_imap.quote(name)
|
|
277
|
+
await self.sendMesg(s_imap.UNTAGGED, 'LIST', f'() "/" {name}')
|
|
278
|
+
|
|
279
|
+
await self.sendMesg(tag, 'OK', 'LIST completed')
|
|
280
|
+
|
|
281
|
+
async def select(self, mesg):
|
|
282
|
+
tag = mesg.get('tag')
|
|
283
|
+
|
|
284
|
+
mboxname = s_imap.qsplit(mesg.get('data').decode())[0].lower()
|
|
285
|
+
if (mailbox := self.mail[self.user]) is None or mboxname not in mailbox.get('mailboxes'):
|
|
286
|
+
return await self.sendMesg(tag, 'NO', f'No such mailbox: {mboxname}.')
|
|
287
|
+
|
|
288
|
+
flags = []
|
|
289
|
+
exists = 0
|
|
290
|
+
recent = 0
|
|
291
|
+
uidnext = 0
|
|
292
|
+
|
|
293
|
+
for uid, message in mailbox.get('messages').items():
|
|
294
|
+
exists += 1
|
|
295
|
+
|
|
296
|
+
mflags = message.get('flags')
|
|
297
|
+
flags.extend(mflags)
|
|
298
|
+
|
|
299
|
+
if '\\Recent' in mflags:
|
|
300
|
+
recent += 1
|
|
301
|
+
|
|
302
|
+
uidnext = max(uid, uidnext)
|
|
303
|
+
|
|
304
|
+
flags = ' '.join(sorted(set(flags)))
|
|
305
|
+
|
|
306
|
+
self.state = 'SELECTED'
|
|
307
|
+
self.selected = mboxname.lower()
|
|
308
|
+
await self.sendMesg(s_imap.UNTAGGED, 'FLAGS', f'({flags})')
|
|
309
|
+
await self.sendMesg(s_imap.UNTAGGED, 'OK', 'Flags permitted.',
|
|
310
|
+
code='PERMANENTFLAGS (\\Deleted \\Seen \\*)')
|
|
311
|
+
await self.sendMesg(s_imap.UNTAGGED, 'OK', 'UIDs valid.', code=f'UIDVALIDITY {self.validity}')
|
|
312
|
+
await self.sendMesg(s_imap.UNTAGGED, f'{exists} EXISTS', '')
|
|
313
|
+
await self.sendMesg(s_imap.UNTAGGED, f'{recent} RECENT', '')
|
|
314
|
+
await self.sendMesg(s_imap.UNTAGGED, 'OK', 'Predicted next UID.', code=f'UIDNEXT {uidnext + 1}')
|
|
315
|
+
|
|
316
|
+
code = 'READ-WRITE'
|
|
317
|
+
if mailbox['mailboxes'][mboxname].get('readonly', False):
|
|
318
|
+
code = 'READ-ONLY'
|
|
319
|
+
|
|
320
|
+
await self.sendMesg(tag, 'OK', 'SELECT completed', code=code)
|
|
321
|
+
|
|
322
|
+
async def expunge(self, mesg):
|
|
323
|
+
tag = mesg.get('tag')
|
|
324
|
+
|
|
325
|
+
mailbox = self.mail[self.user]
|
|
326
|
+
|
|
327
|
+
messages = {
|
|
328
|
+
k: v for k, v in mailbox['messages'].items()
|
|
329
|
+
if v['mailbox'] == self.selected
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
uids = []
|
|
333
|
+
for uid, message in messages.items():
|
|
334
|
+
if '\\Deleted' in message.get('flags'):
|
|
335
|
+
uids.append(uid)
|
|
336
|
+
|
|
337
|
+
for uid in uids:
|
|
338
|
+
mailbox['messages'].pop(uid)
|
|
339
|
+
|
|
340
|
+
await self.sendMesg(tag, 'OK', 'EXPUNGE completed')
|
|
341
|
+
|
|
342
|
+
async def uid(self, mesg):
|
|
343
|
+
tag = mesg.get('tag')
|
|
344
|
+
data = mesg.get('data').decode()
|
|
345
|
+
cmdname, cmdargs = data.split(' ', maxsplit=1)
|
|
346
|
+
|
|
347
|
+
mailbox = self.mail[self.user]
|
|
348
|
+
messages = {
|
|
349
|
+
k: v for k, v in mailbox['messages'].items()
|
|
350
|
+
if v['mailbox'] == self.selected
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if cmdname == 'STORE':
|
|
354
|
+
uidset, dataname, datavalu = cmdargs.split(' ')
|
|
355
|
+
|
|
356
|
+
if ':' in uidset:
|
|
357
|
+
start, end = uidset.split(':')
|
|
358
|
+
|
|
359
|
+
if end == '*':
|
|
360
|
+
keys = messages.keys()
|
|
361
|
+
end = max(keys)
|
|
362
|
+
|
|
363
|
+
else:
|
|
364
|
+
start = end = uidset
|
|
365
|
+
|
|
366
|
+
datavalu = set(datavalu.strip('()').split(' '))
|
|
367
|
+
|
|
368
|
+
for uid in range(int(start), int(end) + 1):
|
|
369
|
+
if uid not in messages:
|
|
370
|
+
continue
|
|
371
|
+
|
|
372
|
+
curv = set(messages[uid]['flags'])
|
|
373
|
+
if dataname.startswith('+'):
|
|
374
|
+
curv |= datavalu
|
|
375
|
+
elif dataname.startswith('-'):
|
|
376
|
+
curv ^= datavalu
|
|
377
|
+
else:
|
|
378
|
+
curv = datavalu
|
|
379
|
+
|
|
380
|
+
mailbox['messages'][uid]['flags'] = list(curv)
|
|
381
|
+
|
|
382
|
+
return await self.sendMesg(tag, 'OK', 'STORE completed')
|
|
383
|
+
|
|
384
|
+
elif cmdname == 'SEARCH':
|
|
385
|
+
# We only support a couple of search terms for ease of implementation
|
|
386
|
+
# https://www.rfc-editor.org/rfc/rfc3501#section-6.4.4
|
|
387
|
+
supported = ('Answered', 'Deleted', 'Draft', 'Recent', 'Seen', 'Unanswered', 'Undeleted', 'Unseen')
|
|
388
|
+
cmdargs = s_imap.qsplit(cmdargs)
|
|
389
|
+
if cmdargs[0] == 'CHARSET':
|
|
390
|
+
cmdargs = cmdargs[2:]
|
|
391
|
+
|
|
392
|
+
if cmdargs[0].title() not in supported:
|
|
393
|
+
return await self.sendMesg(tag, 'BAD', f'Search term not supported: {cmdargs[0]}.')
|
|
394
|
+
|
|
395
|
+
uids = ' '.join(
|
|
396
|
+
[str(k[0]) for k in messages.items() if f'\\{cmdargs[0].title()}' in k[1].get('flags')]
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
await self.sendMesg(s_imap.UNTAGGED, 'SEARCH', uids)
|
|
400
|
+
return await self.sendMesg(tag, 'OK', 'SEARCH completed')
|
|
401
|
+
|
|
402
|
+
elif cmdname == 'FETCH':
|
|
403
|
+
uid, datanames = cmdargs.split(' ', maxsplit=1)
|
|
404
|
+
items = datanames.strip('()').split(' ')
|
|
405
|
+
|
|
406
|
+
uid = int(uid)
|
|
407
|
+
|
|
408
|
+
if uid not in messages:
|
|
409
|
+
return await self.sendMesg(tag, 'OK', 'FETCH completed')
|
|
410
|
+
|
|
411
|
+
message = messages[uid]['data']
|
|
412
|
+
|
|
413
|
+
data = []
|
|
414
|
+
for item in items:
|
|
415
|
+
if item == 'RFC822':
|
|
416
|
+
data.append((item, ''.join((message.get('headers'), message.get('body')))))
|
|
417
|
+
elif item == 'BODY[HEADER]':
|
|
418
|
+
data.append((item, message.get('headers')))
|
|
419
|
+
|
|
420
|
+
# Send the first requested data item
|
|
421
|
+
name, msgdata = data[0]
|
|
422
|
+
size = len(msgdata)
|
|
423
|
+
await self.sendMesg(s_imap.UNTAGGED, 'FETCH', f'(UID {uid} {name} {{{size}}}', uid=uid)
|
|
424
|
+
await self.send(msgdata.encode())
|
|
425
|
+
|
|
426
|
+
# Send subsequent requested data items
|
|
427
|
+
for (name, msgdata) in data[1:]:
|
|
428
|
+
size = len(msgdata)
|
|
429
|
+
await self.send(f' {name} {{{size}}}\r\n'.encode())
|
|
430
|
+
await self.send(msgdata.encode())
|
|
431
|
+
|
|
432
|
+
# Close response
|
|
433
|
+
await self.send(b')\r\n')
|
|
434
|
+
|
|
435
|
+
return await self.sendMesg(tag, 'OK', 'FETCH completed')
|
|
436
|
+
|
|
437
|
+
else:
|
|
438
|
+
raise s_exc.ImapError(mesg=f'Unsupported command: {cmdname}')
|
|
123
439
|
|
|
124
440
|
class ImapTest(s_test.SynTest):
|
|
441
|
+
async def _imapserv(self, link):
|
|
442
|
+
self.imap = link
|
|
125
443
|
|
|
126
|
-
|
|
444
|
+
await link.greet()
|
|
127
445
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
client_args.append((args, kwargs))
|
|
131
|
-
return mock_create_client(*args, **kwargs)
|
|
446
|
+
while not link.isfini:
|
|
447
|
+
mesg = await link.rx()
|
|
132
448
|
|
|
133
|
-
|
|
134
|
-
|
|
449
|
+
# Receive commands from client
|
|
450
|
+
command = mesg.get('command')
|
|
135
451
|
|
|
136
|
-
|
|
452
|
+
# Check server state
|
|
453
|
+
if link.state not in imaplib.Commands.get(command.upper()):
|
|
454
|
+
mesg = f'{command} not allowed in the {link.state} state.'
|
|
455
|
+
raise s_exc.ImapError(mesg=mesg, state=link.state, command=command)
|
|
137
456
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return($server.list())
|
|
143
|
-
'''
|
|
144
|
-
retn = await core.callStorm(scmd)
|
|
145
|
-
self.eq((True, ('INBOX',)), retn)
|
|
146
|
-
ctx = self.nn(client_args[-1][0][5]) # type: ssl.SSLContext
|
|
147
|
-
self.eq(ctx.verify_mode, ssl.CERT_REQUIRED)
|
|
457
|
+
# Get command handler
|
|
458
|
+
handler = getattr(link, command.lower(), None)
|
|
459
|
+
if handler is None:
|
|
460
|
+
raise NotImplementedError(f'No handler for command: {command}')
|
|
148
461
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
$server = $lib.inet.imap.connect(hello, ssl_verify=(false))
|
|
152
|
-
$server.login("vtx@email.com", "secret")
|
|
153
|
-
$server.select("INBOX")
|
|
154
|
-
return($server.search("FROM", "foo@mail.com"))
|
|
155
|
-
'''
|
|
156
|
-
retn = await core.callStorm(scmd)
|
|
157
|
-
self.eq((True, ('8181', '8192', '8194')), retn)
|
|
158
|
-
ctx = self.nn(client_args[-1][0][5]) # type: ssl.SSLContext
|
|
159
|
-
self.eq(ctx.verify_mode, ssl.CERT_NONE)
|
|
462
|
+
# Process command
|
|
463
|
+
await handler(mesg)
|
|
160
464
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
465
|
+
await link.waitfini()
|
|
466
|
+
|
|
467
|
+
@contextlib.asynccontextmanager
|
|
468
|
+
async def getTestCoreAndImapPort(self, *args, **kwargs):
|
|
469
|
+
coro = s_link.listen('127.0.0.1', 0, self._imapserv, linkcls=IMAPServer)
|
|
470
|
+
with contextlib.closing(await coro) as server:
|
|
471
|
+
|
|
472
|
+
port = server.sockets[0].getsockname()[1]
|
|
473
|
+
|
|
474
|
+
async with self.getTestCore(*args, **kwargs) as core:
|
|
475
|
+
yield core, port
|
|
476
|
+
|
|
477
|
+
async def test_storm_imap_basic(self):
|
|
478
|
+
|
|
479
|
+
async with self.getTestCoreAndImapPort() as (core, port):
|
|
480
|
+
user = 'user00@vertex.link'
|
|
481
|
+
opts = {'vars': {'port': port, 'user': user}}
|
|
482
|
+
|
|
483
|
+
# list mailboxes
|
|
484
|
+
scmd = '''
|
|
485
|
+
$server = $lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))
|
|
486
|
+
$server.login($user, "pass00")
|
|
487
|
+
return($server.list())
|
|
488
|
+
'''
|
|
489
|
+
retn = await core.callStorm(scmd, opts=opts)
|
|
490
|
+
mailboxes = sorted(
|
|
491
|
+
[
|
|
492
|
+
k[0] for k in self.imap.mail[user]['mailboxes'].items()
|
|
493
|
+
if k[1]['parent'] is None
|
|
494
|
+
]
|
|
495
|
+
)
|
|
496
|
+
self.eq((True, mailboxes), retn)
|
|
497
|
+
|
|
498
|
+
# search for UIDs
|
|
499
|
+
scmd = '''
|
|
500
|
+
$server = $lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))
|
|
501
|
+
$server.login($user, "pass00")
|
|
165
502
|
$server.select("INBOX")
|
|
166
|
-
return($server.search("
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
503
|
+
return($server.search("SEEN", charset="utf-8"))
|
|
504
|
+
'''
|
|
505
|
+
retn = await core.callStorm(scmd, opts=opts)
|
|
506
|
+
seen = sorted(
|
|
507
|
+
[
|
|
508
|
+
str(k[0]) for k in self.imap.mail[user]['messages'].items()
|
|
509
|
+
if k[1]['mailbox'] == 'inbox' and '\\Seen' in k[1]['flags']
|
|
510
|
+
]
|
|
511
|
+
)
|
|
512
|
+
self.eq((True, seen), retn)
|
|
170
513
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
$server = $lib.inet.imap.connect(
|
|
174
|
-
$server.login(
|
|
514
|
+
# mark seen
|
|
515
|
+
scmd = '''
|
|
516
|
+
$server = $lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))
|
|
517
|
+
$server.login($user, "pass00")
|
|
175
518
|
$server.select("INBOX")
|
|
176
|
-
return($server.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
519
|
+
return($server.markSeen("1:7"))
|
|
520
|
+
'''
|
|
521
|
+
retn = await core.callStorm(scmd, opts=opts)
|
|
522
|
+
self.eq((True, None), retn)
|
|
523
|
+
self.eq(
|
|
524
|
+
['1', '6', '7'],
|
|
525
|
+
sorted(
|
|
526
|
+
[
|
|
527
|
+
str(k[0]) for k in self.imap.mail[user]['messages'].items()
|
|
528
|
+
if k[1]['mailbox'] == 'inbox' and '\\Seen' in k[1]['flags']
|
|
529
|
+
]
|
|
530
|
+
)
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
# delete
|
|
534
|
+
scmd = '''
|
|
535
|
+
$server = $lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))
|
|
536
|
+
$server.login($user, "pass00")
|
|
537
|
+
$server.select("INBOX")
|
|
538
|
+
return($server.delete("1:7"))
|
|
539
|
+
'''
|
|
540
|
+
retn = await core.callStorm(scmd, opts=opts)
|
|
541
|
+
messages = self.imap.mail[user]['messages']
|
|
542
|
+
self.notin(1, messages)
|
|
543
|
+
self.notin(6, messages)
|
|
544
|
+
self.notin(7, messages)
|
|
545
|
+
self.isin(2, messages)
|
|
546
|
+
self.isin(3, messages)
|
|
547
|
+
self.isin(4, messages)
|
|
548
|
+
self.isin(5, messages)
|
|
549
|
+
self.isin(8, messages)
|
|
550
|
+
self.eq((True, None), retn)
|
|
551
|
+
|
|
552
|
+
# fetch and save a message
|
|
553
|
+
scmd = '''
|
|
554
|
+
$server = $lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))
|
|
555
|
+
$server.login($user, "pass00")
|
|
556
|
+
$server.select("INBOX")
|
|
557
|
+
yield $server.fetch("1")
|
|
558
|
+
'''
|
|
559
|
+
nodes = await core.nodes(scmd, opts=opts)
|
|
560
|
+
self.len(1, nodes)
|
|
561
|
+
self.eq('file:bytes', nodes[0].ndef[0])
|
|
562
|
+
self.true(all(nodes[0].get(p) for p in ('sha512', 'sha256', 'sha1', 'md5', 'size')))
|
|
563
|
+
self.eq('message/rfc822', nodes[0].get('mime'))
|
|
564
|
+
|
|
565
|
+
byts = b''.join([byts async for byts in core.axon.get(s_common.uhex(nodes[0].get('sha256')))])
|
|
566
|
+
data = ''.join((email.get('headers'), email.get('body'))).encode()
|
|
567
|
+
self.eq(data, byts)
|
|
568
|
+
|
|
569
|
+
# fetch must only be for a single message
|
|
570
|
+
scmd = '''
|
|
571
|
+
$server = $lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))
|
|
572
|
+
$server.login($user, "pass00")
|
|
573
|
+
$server.select("INBOX")
|
|
574
|
+
$server.fetch("1:*")
|
|
575
|
+
'''
|
|
576
|
+
mesgs = await core.stormlist(scmd, opts=opts)
|
|
577
|
+
self.stormIsInErr('Failed to make an integer', mesgs)
|
|
578
|
+
|
|
579
|
+
scmd = '''
|
|
580
|
+
$server = $lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))
|
|
581
|
+
$server.login($user, "pass00")
|
|
582
|
+
$server.select("INBOX")
|
|
583
|
+
return($server.fetch(10))
|
|
584
|
+
'''
|
|
585
|
+
ret = await core.callStorm(scmd, opts=opts)
|
|
586
|
+
self.eq(ret, (False, 'No data received from fetch request for uid 10.'))
|
|
587
|
+
|
|
588
|
+
# make sure we can pass around the server object
|
|
589
|
+
scmd = '''
|
|
590
|
+
function foo(s) {
|
|
591
|
+
return($s.login($user, "pass00"))
|
|
592
|
+
}
|
|
593
|
+
$server = $lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))
|
|
594
|
+
$ret00 = $foo($server)
|
|
595
|
+
$ret01 = $server.list()
|
|
596
|
+
return(($ret00, $ret01))
|
|
597
|
+
'''
|
|
598
|
+
retn = await core.callStorm(scmd, opts=opts)
|
|
599
|
+
self.eq(((True, None), (True, ('deleted', 'drafts', 'inbox', 'sent'))), retn)
|
|
180
600
|
|
|
601
|
+
async def test_storm_imap_greet(self):
|
|
602
|
+
async with self.getTestCoreAndImapPort() as (core, port):
|
|
603
|
+
user = 'user00@vertex.link'
|
|
604
|
+
opts = {'vars': {'port': port, 'user': user}}
|
|
605
|
+
|
|
606
|
+
# Normal greeting
|
|
607
|
+
scmd = '''
|
|
608
|
+
$server = $lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))
|
|
609
|
+
$server.select("INBOX")
|
|
610
|
+
'''
|
|
611
|
+
mesgs = await core.stormlist(scmd, opts=opts)
|
|
612
|
+
self.stormIsInErr('SELECT not allowed in the NONAUTH state.', mesgs)
|
|
613
|
+
|
|
614
|
+
# PREAUTH greeting
|
|
615
|
+
async def greet_preauth(self):
|
|
616
|
+
await self.sendMesg(s_imap.UNTAGGED, 'PREAUTH', 'SynImap ready.')
|
|
617
|
+
self.user = 'user00@vertex.link'
|
|
618
|
+
self.state = 'AUTH'
|
|
619
|
+
|
|
620
|
+
with mock.patch.object(IMAPServer, 'greet', greet_preauth):
|
|
621
|
+
mesgs = await core.stormlist(scmd, opts=opts)
|
|
622
|
+
self.stormHasNoWarnErr(mesgs)
|
|
623
|
+
|
|
624
|
+
# BYE greeting
|
|
625
|
+
async def greet_bye(self):
|
|
626
|
+
await self.sendMesg(s_imap.UNTAGGED, 'BYE', 'SynImap not ready.')
|
|
627
|
+
|
|
628
|
+
with mock.patch.object(IMAPServer, 'greet', greet_bye):
|
|
629
|
+
mesgs = await core.stormlist(scmd, opts=opts)
|
|
630
|
+
self.stormIsInErr('SynImap not ready.', mesgs)
|
|
631
|
+
|
|
632
|
+
# Greeting includes capabilities
|
|
633
|
+
async def greet_capabilities(self):
|
|
634
|
+
await self.sendMesg(s_imap.UNTAGGED, 'OK', 'SynImap ready.', code='CAPABILITY IMAP4rev1')
|
|
635
|
+
|
|
636
|
+
with mock.patch.object(IMAPServer, 'greet', greet_capabilities):
|
|
181
637
|
scmd = '''
|
|
182
|
-
$server = $lib.inet.imap.connect(
|
|
183
|
-
$server.login(
|
|
184
|
-
$server.select("INBOX")
|
|
185
|
-
return($server.search("FROM", "newp@mail.com"))
|
|
638
|
+
$server = $lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))
|
|
639
|
+
$server.login($user, pass00)
|
|
186
640
|
'''
|
|
187
|
-
|
|
188
|
-
self.
|
|
641
|
+
mesgs = await core.stormlist(scmd, opts=opts)
|
|
642
|
+
self.stormIsInErr('Plain authentication not available on server.', mesgs)
|
|
643
|
+
|
|
644
|
+
# Greet timeout
|
|
645
|
+
async def greet_timeout(self):
|
|
646
|
+
pass
|
|
647
|
+
|
|
648
|
+
with mock.patch.object(IMAPServer, 'greet', greet_timeout):
|
|
649
|
+
mesgs = await core.stormlist('$lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false), timeout=(1))', opts=opts)
|
|
650
|
+
self.stormIsInErr('Timed out waiting for IMAP server hello', mesgs)
|
|
651
|
+
|
|
652
|
+
async def test_storm_imap_capability(self):
|
|
653
|
+
|
|
654
|
+
async with self.getTestCoreAndImapPort() as (core, port):
|
|
655
|
+
user = 'user00@vertex.link'
|
|
656
|
+
opts = {'vars': {'port': port, 'user': user}}
|
|
657
|
+
|
|
658
|
+
# Capability NO
|
|
659
|
+
async def capability_no(self, mesg):
|
|
660
|
+
tag = mesg.get('tag')
|
|
661
|
+
await self.sendMesg(tag, 'NO', 'No capabilities for you.')
|
|
189
662
|
|
|
190
|
-
|
|
663
|
+
with mock.patch.object(IMAPServer, 'capability', capability_no):
|
|
664
|
+
mesgs = await core.stormlist('$lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))', opts=opts)
|
|
665
|
+
self.stormIsInErr('No capabilities for you.', mesgs)
|
|
666
|
+
|
|
667
|
+
# Invalid capability response (no untagged message)
|
|
668
|
+
async def capability_invalid(self, mesg):
|
|
669
|
+
tag = mesg.get('tag')
|
|
670
|
+
await self.sendMesg(tag, 'OK', 'CAPABILITY completed')
|
|
671
|
+
|
|
672
|
+
with mock.patch.object(IMAPServer, 'capability', capability_invalid):
|
|
673
|
+
mesgs = await core.stormlist('$lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))', opts=opts)
|
|
674
|
+
self.stormIsInErr('Invalid server response.', mesgs)
|
|
675
|
+
|
|
676
|
+
async def test_storm_imap_login(self):
|
|
677
|
+
|
|
678
|
+
async with self.getTestCoreAndImapPort() as (core, port):
|
|
679
|
+
user = 'user00@vertex.link'
|
|
680
|
+
opts = {'vars': {'port': port, 'user': user}}
|
|
681
|
+
|
|
682
|
+
async def login_w_capability(self, mesg):
|
|
683
|
+
tag = mesg.get('tag')
|
|
684
|
+
await self.sendMesg(tag, 'OK', 'LOGIN completed', code='CAPABILITY IMAP4rev1')
|
|
685
|
+
|
|
686
|
+
with mock.patch.object(IMAPServer, 'login', login_w_capability):
|
|
191
687
|
scmd = '''
|
|
192
|
-
$server = $lib.inet.imap.connect(
|
|
193
|
-
$server.login(
|
|
194
|
-
$server.select("INBOX")
|
|
195
|
-
return($server.markSeen("1:4"))
|
|
688
|
+
$server = $lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))
|
|
689
|
+
$server.login($user, pass00)
|
|
196
690
|
'''
|
|
197
|
-
|
|
198
|
-
self.
|
|
199
|
-
|
|
691
|
+
mesgs = await core.stormlist(scmd, opts=opts)
|
|
692
|
+
self.stormHasNoWarnErr(mesgs)
|
|
693
|
+
|
|
694
|
+
capability = IMAPServer.capability
|
|
200
695
|
|
|
201
|
-
|
|
696
|
+
# No auth=plain capability
|
|
697
|
+
async def capability_noauth(self, mesg):
|
|
698
|
+
self.capabilities = ['IMAP4rev1']
|
|
699
|
+
return await capability(self, mesg)
|
|
700
|
+
|
|
701
|
+
with mock.patch.object(IMAPServer, 'capability', capability_noauth):
|
|
202
702
|
scmd = '''
|
|
203
|
-
$server = $lib.inet.imap.connect(
|
|
204
|
-
$server.login(
|
|
205
|
-
$server.select("INBOX")
|
|
206
|
-
return($server.delete("1:4"))
|
|
703
|
+
$server = $lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))
|
|
704
|
+
$server.login($user, pass00)
|
|
207
705
|
'''
|
|
208
|
-
|
|
209
|
-
self.
|
|
706
|
+
mesgs = await core.stormlist(scmd, opts=opts)
|
|
707
|
+
self.stormIsInErr('Plain authentication not available on server.', mesgs)
|
|
708
|
+
|
|
709
|
+
# Login disabled
|
|
710
|
+
async def capability_login_disabled(self, mesg):
|
|
711
|
+
self.capabilities.append('LOGINDISABLED')
|
|
712
|
+
return await capability(self, mesg)
|
|
210
713
|
|
|
211
|
-
|
|
714
|
+
with mock.patch.object(IMAPServer, 'capability', capability_login_disabled):
|
|
212
715
|
scmd = '''
|
|
213
|
-
$server = $lib.inet.imap.connect(
|
|
214
|
-
$server.login(
|
|
215
|
-
$server.select("INBOX")
|
|
216
|
-
yield $server.fetch("1")
|
|
716
|
+
$server = $lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))
|
|
717
|
+
$server.login($user, pass00)
|
|
217
718
|
'''
|
|
218
|
-
|
|
219
|
-
self.
|
|
220
|
-
self.eq('file:bytes', nodes[0].ndef[0])
|
|
221
|
-
self.true(all(nodes[0].get(p) for p in ('sha512', 'sha256', 'sha1', 'md5', 'size')))
|
|
222
|
-
self.eq('message/rfc822', nodes[0].get('mime'))
|
|
719
|
+
mesgs = await core.stormlist(scmd, opts=opts)
|
|
720
|
+
self.stormIsInErr('Login disabled on server.', mesgs)
|
|
223
721
|
|
|
224
|
-
|
|
225
|
-
|
|
722
|
+
# Login command returns non-OK
|
|
723
|
+
async def login_no(self, mesg):
|
|
724
|
+
tag = mesg.get('tag')
|
|
725
|
+
return await self.sendMesg(tag, 'BAD', 'Bad login request.')
|
|
226
726
|
|
|
227
|
-
|
|
727
|
+
with mock.patch.object(IMAPServer, 'login', login_no):
|
|
228
728
|
scmd = '''
|
|
229
|
-
$server = $lib.inet.imap.connect(
|
|
230
|
-
$server.login(
|
|
231
|
-
$server.select("INBOX")
|
|
232
|
-
$server.fetch("1:*")
|
|
729
|
+
$server = $lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))
|
|
730
|
+
$server.login($user, pass00)
|
|
233
731
|
'''
|
|
234
|
-
mesgs = await core.stormlist(scmd)
|
|
235
|
-
self.stormIsInErr('
|
|
732
|
+
mesgs = await core.stormlist(scmd, opts=opts)
|
|
733
|
+
self.stormIsInErr('Bad login request.', mesgs)
|
|
734
|
+
|
|
735
|
+
# Bad creds
|
|
736
|
+
scmd = '''
|
|
737
|
+
$server = $lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))
|
|
738
|
+
$server.login($user, "secret")
|
|
739
|
+
'''
|
|
740
|
+
mesgs = await core.stormlist(scmd, opts=opts)
|
|
741
|
+
self.stormIsInErr('Invalid credentials.', mesgs)
|
|
236
742
|
|
|
237
|
-
|
|
743
|
+
# Login timeout
|
|
744
|
+
async def login_timeout(self, mesg):
|
|
745
|
+
pass
|
|
746
|
+
|
|
747
|
+
with mock.patch.object(IMAPServer, 'login', login_timeout):
|
|
238
748
|
scmd = '''
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
$server = $lib.inet.imap.connect(hello)
|
|
243
|
-
$ret00 = $foo($server)
|
|
244
|
-
$ret01 = $server.list()
|
|
245
|
-
return(($ret00, $ret01))
|
|
749
|
+
$server = $lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false), timeout=(1))
|
|
750
|
+
$server.login($user, "secret")
|
|
246
751
|
'''
|
|
247
|
-
|
|
248
|
-
self.
|
|
752
|
+
mesgs = await core.stormlist(scmd, opts=opts)
|
|
753
|
+
self.stormIsInErr('Timed out waiting for IMAP server response', mesgs)
|
|
249
754
|
|
|
250
|
-
|
|
755
|
+
async def test_storm_imap_select(self):
|
|
251
756
|
|
|
252
|
-
|
|
253
|
-
|
|
757
|
+
async with self.getTestCoreAndImapPort() as (core, port):
|
|
758
|
+
user = 'user01@vertex.link'
|
|
759
|
+
opts = {'vars': {'port': port, 'user': user}}
|
|
254
760
|
|
|
761
|
+
scmd = '''
|
|
762
|
+
$server = $lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))
|
|
763
|
+
$server.login(user00@vertex.link, pass00)
|
|
764
|
+
$server.select("status reports")
|
|
765
|
+
'''
|
|
766
|
+
mesgs = await core.stormlist(scmd, opts=opts)
|
|
767
|
+
self.stormHasNoWarnErr(mesgs)
|
|
768
|
+
|
|
769
|
+
# Non-OK select response
|
|
770
|
+
async def select_no(self, mesg):
|
|
771
|
+
tag = mesg.get('tag')
|
|
772
|
+
await self.sendMesg(tag, 'NO', 'Cannot select mailbox.')
|
|
773
|
+
|
|
774
|
+
with mock.patch.object(IMAPServer, 'select', select_no):
|
|
255
775
|
scmd = '''
|
|
256
|
-
$server = $lib.inet.imap.connect(
|
|
257
|
-
$server.login(
|
|
776
|
+
$server = $lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))
|
|
777
|
+
$server.login($user, 'spaces lol')
|
|
778
|
+
$server.select(INBOX)
|
|
258
779
|
'''
|
|
259
|
-
mesgs = await core.stormlist(scmd)
|
|
260
|
-
self.stormIsInErr('
|
|
780
|
+
mesgs = await core.stormlist(scmd, opts=opts)
|
|
781
|
+
self.stormIsInErr('Cannot select mailbox.', mesgs)
|
|
782
|
+
|
|
783
|
+
# Readonly mailbox
|
|
784
|
+
scmd = '''
|
|
785
|
+
$server = $lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))
|
|
786
|
+
$server.login($user, 'spaces lol')
|
|
787
|
+
$server.select(INBOX)
|
|
788
|
+
$server.delete(1)
|
|
789
|
+
'''
|
|
790
|
+
mesgs = await core.stormlist(scmd, opts=opts)
|
|
791
|
+
self.stormIsInErr('Selected mailbox is read-only.', mesgs)
|
|
792
|
+
|
|
793
|
+
async def test_storm_imap_list(self):
|
|
794
|
+
|
|
795
|
+
async with self.getTestCoreAndImapPort() as (core, port):
|
|
796
|
+
user = 'user01@vertex.link'
|
|
797
|
+
opts = {'vars': {'port': port, 'user': user}}
|
|
798
|
+
|
|
799
|
+
# Non-OK list response
|
|
800
|
+
async def list_no(self, mesg):
|
|
801
|
+
tag = mesg.get('tag')
|
|
802
|
+
await self.sendMesg(tag, 'NO', 'Cannot list mailbox.')
|
|
261
803
|
|
|
804
|
+
with mock.patch.object(IMAPServer, 'list', list_no):
|
|
262
805
|
scmd = '''
|
|
263
|
-
$server = $lib.inet.imap.connect(
|
|
264
|
-
$server.login(
|
|
806
|
+
$server = $lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))
|
|
807
|
+
$server.login($user, 'spaces lol')
|
|
808
|
+
$server.select(INBOX)
|
|
809
|
+
$server.list()
|
|
265
810
|
'''
|
|
266
|
-
mesgs = await core.stormlist(scmd)
|
|
267
|
-
self.stormIsInErr('
|
|
811
|
+
mesgs = await core.stormlist(scmd, opts=opts)
|
|
812
|
+
self.stormIsInErr('Cannot list mailbox.', mesgs)
|
|
813
|
+
|
|
814
|
+
async def test_storm_imap_uid(self):
|
|
815
|
+
|
|
816
|
+
async with self.getTestCoreAndImapPort() as (core, port):
|
|
817
|
+
user = 'user00@vertex.link'
|
|
818
|
+
opts = {'vars': {'port': port, 'user': user}}
|
|
268
819
|
|
|
820
|
+
# Non-OK uid response
|
|
821
|
+
async def uid_no(self, mesg):
|
|
822
|
+
tag = mesg.get('tag')
|
|
823
|
+
await self.sendMesg(tag, 'NO', 'Cannot process UID command.')
|
|
824
|
+
|
|
825
|
+
with mock.patch.object(IMAPServer, 'uid', uid_no):
|
|
269
826
|
scmd = '''
|
|
270
|
-
$server = $lib.inet.imap.connect(
|
|
271
|
-
$server.login(
|
|
827
|
+
$server = $lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))
|
|
828
|
+
$server.login($user, pass00)
|
|
829
|
+
$server.select(INBOX)
|
|
830
|
+
$server.delete(1)
|
|
272
831
|
'''
|
|
273
|
-
mesgs = await core.stormlist(scmd)
|
|
274
|
-
self.stormIsInErr('
|
|
832
|
+
mesgs = await core.stormlist(scmd, opts=opts)
|
|
833
|
+
self.stormIsInErr('Cannot process UID command.', mesgs)
|
|
834
|
+
|
|
835
|
+
async def test_storm_imap_expunge(self):
|
|
836
|
+
|
|
837
|
+
async with self.getTestCoreAndImapPort() as (core, port):
|
|
838
|
+
user = 'user00@vertex.link'
|
|
839
|
+
opts = {'vars': {'port': port, 'user': user}}
|
|
275
840
|
|
|
841
|
+
# Non-OK expunge response
|
|
842
|
+
async def expunge_no(self, mesg):
|
|
843
|
+
tag = mesg.get('tag')
|
|
844
|
+
await self.sendMesg(tag, 'NO', 'Cannot process EXPUNGE command.')
|
|
845
|
+
|
|
846
|
+
with mock.patch.object(IMAPServer, 'expunge', expunge_no):
|
|
276
847
|
scmd = '''
|
|
277
|
-
$server = $lib.inet.imap.connect(
|
|
278
|
-
$server.login(
|
|
279
|
-
$server.
|
|
848
|
+
$server = $lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))
|
|
849
|
+
$server.login($user, pass00)
|
|
850
|
+
$server.select(INBOX)
|
|
851
|
+
$server.delete(1)
|
|
280
852
|
'''
|
|
281
|
-
mesgs = await core.stormlist(scmd)
|
|
282
|
-
self.stormIsInErr('
|
|
853
|
+
mesgs = await core.stormlist(scmd, opts=opts)
|
|
854
|
+
self.stormIsInErr('Cannot process EXPUNGE command.', mesgs)
|
|
855
|
+
|
|
856
|
+
imap = await s_link.connect('127.0.0.1', port, linkcls=s_imap.IMAPClient)
|
|
857
|
+
await imap.login('user01@vertex.link', 'spaces lol')
|
|
858
|
+
await imap.select('INBOX')
|
|
859
|
+
self.eq(
|
|
860
|
+
await imap.expunge(),
|
|
861
|
+
(False, (b'Selected mailbox is read-only.',))
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
async def test_storm_imap_fetch(self):
|
|
865
|
+
|
|
866
|
+
async with self.getTestCoreAndImapPort() as (core, port):
|
|
867
|
+
user = 'user00@vertex.link'
|
|
868
|
+
|
|
869
|
+
# Normal response
|
|
870
|
+
imap = await s_link.connect('127.0.0.1', port, linkcls=s_imap.IMAPClient)
|
|
871
|
+
await imap.login(user, 'pass00')
|
|
872
|
+
await imap.select('INBOX')
|
|
873
|
+
ret = await imap.uid_fetch('1', '(RFC822 BODY[HEADER])')
|
|
874
|
+
|
|
875
|
+
rfc822 = ''.join((email.get('headers'), email.get('body'))).encode()
|
|
876
|
+
header = email.get('headers').encode()
|
|
877
|
+
|
|
878
|
+
self.eq(ret, (True, (rfc822, header, b'(UID 1 RFC822 BODY[HEADER])')))
|
|
879
|
+
|
|
880
|
+
async def test_storm_imap_logout(self):
|
|
881
|
+
|
|
882
|
+
async with self.getTestCoreAndImapPort() as (core, port):
|
|
883
|
+
user = 'user00@vertex.link'
|
|
884
|
+
|
|
885
|
+
# Normal response
|
|
886
|
+
imap = await s_link.connect('127.0.0.1', port, linkcls=s_imap.IMAPClient)
|
|
887
|
+
await imap.login(user, 'pass00')
|
|
888
|
+
self.eq(
|
|
889
|
+
await imap.logout(),
|
|
890
|
+
(True, (b'LOGOUT completed',))
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
# Non-OK logout response
|
|
894
|
+
async def logout_no(self, mesg):
|
|
895
|
+
tag = mesg.get('tag')
|
|
896
|
+
await self.sendMesg(tag, 'NO', 'Cannot logout.')
|
|
897
|
+
|
|
898
|
+
with mock.patch.object(IMAPServer, 'logout', logout_no):
|
|
899
|
+
imap = await s_link.connect('127.0.0.1', port, linkcls=s_imap.IMAPClient)
|
|
900
|
+
await imap.login(user, 'pass00')
|
|
901
|
+
self.eq(
|
|
902
|
+
await imap.logout(),
|
|
903
|
+
(False, (b'Cannot logout.',))
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
# Logout without BYE response
|
|
907
|
+
async def logout_nobye(self, mesg):
|
|
908
|
+
tag = mesg.get('tag')
|
|
909
|
+
await self.sendMesg(tag, 'OK', 'LOGOUT completed')
|
|
910
|
+
|
|
911
|
+
with mock.patch.object(IMAPServer, 'logout', logout_nobye):
|
|
912
|
+
imap = await s_link.connect('127.0.0.1', port, linkcls=s_imap.IMAPClient)
|
|
913
|
+
await imap.login(user, 'pass00')
|
|
914
|
+
self.eq(
|
|
915
|
+
await imap.logout(),
|
|
916
|
+
(False, (b'Server failed to send expected BYE response.',))
|
|
917
|
+
)
|
|
918
|
+
|
|
919
|
+
async def test_storm_imap_errors(self):
|
|
920
|
+
|
|
921
|
+
async with self.getTestCoreAndImapPort() as (core, port):
|
|
922
|
+
user = 'user00@vertex.link'
|
|
923
|
+
opts = {'vars': {'port': port, 'user': user}}
|
|
924
|
+
|
|
925
|
+
# Check state tracking
|
|
926
|
+
scmd = '''
|
|
927
|
+
$server = $lib.inet.imap.connect(127.0.0.1, port=$port, ssl=(false))
|
|
928
|
+
$server.select("INBOX")
|
|
929
|
+
'''
|
|
930
|
+
mesgs = await core.stormlist(scmd, opts=opts)
|
|
931
|
+
self.stormIsInErr('SELECT not allowed in the NONAUTH state.', mesgs)
|
|
932
|
+
|
|
933
|
+
# Check command validation
|
|
934
|
+
imap = await s_link.connect('127.0.0.1', port, linkcls=s_imap.IMAPClient)
|
|
935
|
+
with self.raises(s_exc.ImapError) as exc:
|
|
936
|
+
tag = imap._genTag()
|
|
937
|
+
await imap._command(tag, 'NEWP')
|
|
938
|
+
self.eq(exc.exception.get('mesg'), 'Unsupported command: NEWP.')
|
|
939
|
+
|
|
940
|
+
async def test_storm_imap_parseLine(self):
|
|
941
|
+
|
|
942
|
+
def parseLine(line):
|
|
943
|
+
return s_imap.IMAPClient._parseLine(None, line)
|
|
944
|
+
|
|
945
|
+
with self.raises(s_exc.ImapError) as exc:
|
|
946
|
+
# + is not a valid tag character
|
|
947
|
+
line = b'abc+ OK CAPABILITY completed'
|
|
948
|
+
parseLine(line)
|
|
949
|
+
self.eq(exc.exception.get('mesg'), 'Unable to parse response from server.')
|
|
950
|
+
|
|
951
|
+
with self.raises(s_exc.ImapError) as exc:
|
|
952
|
+
# % is not a valid tag character
|
|
953
|
+
line = b'a%cd OK CAPABILITY completed'
|
|
954
|
+
parseLine(line)
|
|
955
|
+
self.eq(exc.exception.get('mesg'), 'Unable to parse response from server.')
|
|
956
|
+
|
|
957
|
+
# NB: Most of the examples in this test are taken from RFC9051
|
|
958
|
+
|
|
959
|
+
# Server greetings
|
|
960
|
+
line = b'* OK IMAP4rev2 server ready'
|
|
961
|
+
mesg = parseLine(line)
|
|
962
|
+
self.eq(mesg, {
|
|
963
|
+
'tag': '*',
|
|
964
|
+
'response': 'OK',
|
|
965
|
+
'data': b'IMAP4rev2 server ready',
|
|
966
|
+
'code': None, 'uid': None, 'size': None,
|
|
967
|
+
'attachments': [],
|
|
968
|
+
})
|
|
969
|
+
|
|
970
|
+
line = b'* OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE AUTH=PLAIN AUTH=LOGIN] Dovecot ready.'
|
|
971
|
+
mesg = parseLine(line)
|
|
972
|
+
self.eq(mesg, {
|
|
973
|
+
'tag': '*',
|
|
974
|
+
'response': 'OK',
|
|
975
|
+
'data': b'Dovecot ready.',
|
|
976
|
+
'code': 'CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE AUTH=PLAIN AUTH=LOGIN',
|
|
977
|
+
'uid': None, 'size': None,
|
|
978
|
+
'attachments': [],
|
|
979
|
+
})
|
|
980
|
+
|
|
981
|
+
line = b'* PREAUTH IMAP4rev2 server logged in as Smith'
|
|
982
|
+
mesg = parseLine(line)
|
|
983
|
+
self.eq(mesg, {
|
|
984
|
+
'tag': '*',
|
|
985
|
+
'response': 'PREAUTH',
|
|
986
|
+
'data': b'IMAP4rev2 server logged in as Smith',
|
|
987
|
+
'code': None, 'uid': None, 'size': None,
|
|
988
|
+
'attachments': [],
|
|
989
|
+
})
|
|
990
|
+
|
|
991
|
+
line = b'* BYE Autologout; idle for too long'
|
|
992
|
+
mesg = parseLine(line)
|
|
993
|
+
self.eq(mesg, {
|
|
994
|
+
'tag': '*',
|
|
995
|
+
'response': 'BYE',
|
|
996
|
+
'data': b'Autologout; idle for too long',
|
|
997
|
+
'code': None, 'uid': None, 'size': None,
|
|
998
|
+
'attachments': [],
|
|
999
|
+
})
|
|
1000
|
+
|
|
1001
|
+
# Capability response
|
|
1002
|
+
line = b'* CAPABILITY IMAP4rev2 STARTTLS AUTH=GSSAPI LOGINDISABLED'
|
|
1003
|
+
mesg = parseLine(line)
|
|
1004
|
+
self.eq(mesg, {
|
|
1005
|
+
'tag': '*',
|
|
1006
|
+
'response': 'CAPABILITY',
|
|
1007
|
+
'data': b'IMAP4rev2 STARTTLS AUTH=GSSAPI LOGINDISABLED',
|
|
1008
|
+
'code': None, 'uid': None, 'size': None,
|
|
1009
|
+
'attachments': [],
|
|
1010
|
+
})
|
|
1011
|
+
|
|
1012
|
+
line = b'abcd OK CAPABILITY completed'
|
|
1013
|
+
mesg = parseLine(line)
|
|
1014
|
+
self.eq(mesg, {
|
|
1015
|
+
'tag': 'abcd',
|
|
1016
|
+
'response': 'OK',
|
|
1017
|
+
'data': b'CAPABILITY completed',
|
|
1018
|
+
'code': None, 'uid': None, 'size': None,
|
|
1019
|
+
'attachments': [],
|
|
1020
|
+
})
|
|
1021
|
+
|
|
1022
|
+
# Login responses
|
|
1023
|
+
line = b'a001 OK LOGIN completed'
|
|
1024
|
+
mesg = parseLine(line)
|
|
1025
|
+
self.eq(mesg, {
|
|
1026
|
+
'tag': 'a001',
|
|
1027
|
+
'response': 'OK',
|
|
1028
|
+
'data': b'LOGIN completed',
|
|
1029
|
+
'code': None, 'uid': None, 'size': None,
|
|
1030
|
+
'attachments': [],
|
|
1031
|
+
})
|
|
1032
|
+
|
|
1033
|
+
# Select responses
|
|
1034
|
+
line = b'* 172 EXISTS'
|
|
1035
|
+
mesg = parseLine(line)
|
|
1036
|
+
self.eq(mesg, {
|
|
1037
|
+
'tag': '*',
|
|
1038
|
+
'response': 'EXISTS',
|
|
1039
|
+
'data': b'',
|
|
1040
|
+
'code': None,
|
|
1041
|
+
'uid': 172,
|
|
1042
|
+
'size': None,
|
|
1043
|
+
'attachments': [],
|
|
1044
|
+
})
|
|
1045
|
+
|
|
1046
|
+
line = b'* OK [UIDVALIDITY 3857529045] UIDs valid'
|
|
1047
|
+
mesg = parseLine(line)
|
|
1048
|
+
self.eq(mesg, {
|
|
1049
|
+
'tag': '*',
|
|
1050
|
+
'response': 'OK',
|
|
1051
|
+
'data': b'UIDs valid',
|
|
1052
|
+
'code': 'UIDVALIDITY 3857529045',
|
|
1053
|
+
'uid': None, 'size': None,
|
|
1054
|
+
'attachments': [],
|
|
1055
|
+
})
|
|
1056
|
+
|
|
1057
|
+
line = b'* OK [UIDNEXT 4392] Predicted next UID'
|
|
1058
|
+
mesg = parseLine(line)
|
|
1059
|
+
self.eq(mesg, {
|
|
1060
|
+
'tag': '*',
|
|
1061
|
+
'response': 'OK',
|
|
1062
|
+
'data': b'Predicted next UID',
|
|
1063
|
+
'code': 'UIDNEXT 4392',
|
|
1064
|
+
'uid': None, 'size': None,
|
|
1065
|
+
'attachments': [],
|
|
1066
|
+
})
|
|
1067
|
+
|
|
1068
|
+
line = br'* FLAGS (\Answered \Flagged \Deleted \Seen \Draft)'
|
|
1069
|
+
mesg = parseLine(line)
|
|
1070
|
+
self.eq(mesg, {
|
|
1071
|
+
'tag': '*',
|
|
1072
|
+
'response': 'FLAGS',
|
|
1073
|
+
'data': br'(\Answered \Flagged \Deleted \Seen \Draft)',
|
|
1074
|
+
'code': None, 'uid': None, 'size': None,
|
|
1075
|
+
'attachments': [],
|
|
1076
|
+
})
|
|
1077
|
+
|
|
1078
|
+
line = br'* OK [PERMANENTFLAGS (\Deleted \Seen \*)] Limited'
|
|
1079
|
+
mesg = parseLine(line)
|
|
1080
|
+
self.eq(mesg, {
|
|
1081
|
+
'tag': '*',
|
|
1082
|
+
'response': 'OK',
|
|
1083
|
+
'data': br'Limited',
|
|
1084
|
+
'code': r'PERMANENTFLAGS (\Deleted \Seen \*)',
|
|
1085
|
+
'uid': None, 'size': None,
|
|
1086
|
+
'attachments': [],
|
|
1087
|
+
})
|
|
1088
|
+
|
|
1089
|
+
line = b'* LIST () "/" INBOX'
|
|
1090
|
+
mesg = parseLine(line)
|
|
1091
|
+
self.eq(mesg, {
|
|
1092
|
+
'tag': '*',
|
|
1093
|
+
'response': 'LIST',
|
|
1094
|
+
'data': b'() "/" INBOX',
|
|
1095
|
+
'code': None, 'uid': None, 'size': None,
|
|
1096
|
+
'attachments': [],
|
|
1097
|
+
})
|
|
1098
|
+
|
|
1099
|
+
line = b'A142 OK [READ-WRITE] SELECT completed'
|
|
1100
|
+
mesg = parseLine(line)
|
|
1101
|
+
self.eq(mesg, {
|
|
1102
|
+
'tag': 'A142',
|
|
1103
|
+
'response': 'OK',
|
|
1104
|
+
'data': b'SELECT completed',
|
|
1105
|
+
'code': 'READ-WRITE',
|
|
1106
|
+
'uid': None, 'size': None,
|
|
1107
|
+
'attachments': [],
|
|
1108
|
+
})
|
|
1109
|
+
|
|
1110
|
+
# List responses
|
|
1111
|
+
line = br'* LIST (\Noselect) "/" ""'
|
|
1112
|
+
mesg = parseLine(line)
|
|
1113
|
+
self.eq(mesg, {
|
|
1114
|
+
'tag': '*',
|
|
1115
|
+
'response': 'LIST',
|
|
1116
|
+
'data': br'(\Noselect) "/" ""',
|
|
1117
|
+
'code': None, 'uid': None, 'size': None,
|
|
1118
|
+
'attachments': [],
|
|
1119
|
+
})
|
|
1120
|
+
|
|
1121
|
+
line = br'* LIST (\Noselect) "." #news.'
|
|
1122
|
+
mesg = parseLine(line)
|
|
1123
|
+
self.eq(mesg, {
|
|
1124
|
+
'tag': '*',
|
|
1125
|
+
'response': 'LIST',
|
|
1126
|
+
'data': br'(\Noselect) "." #news.',
|
|
1127
|
+
'code': None, 'uid': None, 'size': None,
|
|
1128
|
+
'attachments': [],
|
|
1129
|
+
})
|
|
1130
|
+
|
|
1131
|
+
line = br'* LIST (\Noselect) "/" /'
|
|
1132
|
+
mesg = parseLine(line)
|
|
1133
|
+
self.eq(mesg, {
|
|
1134
|
+
'tag': '*',
|
|
1135
|
+
'response': 'LIST',
|
|
1136
|
+
'data': br'(\Noselect) "/" /',
|
|
1137
|
+
'code': None, 'uid': None, 'size': None,
|
|
1138
|
+
'attachments': [],
|
|
1139
|
+
})
|
|
1140
|
+
|
|
1141
|
+
line = br'* LIST (\Noselect) "/" ~/Mail/foo'
|
|
1142
|
+
mesg = parseLine(line)
|
|
1143
|
+
self.eq(mesg, {
|
|
1144
|
+
'tag': '*',
|
|
1145
|
+
'response': 'LIST',
|
|
1146
|
+
'data': br'(\Noselect) "/" ~/Mail/foo',
|
|
1147
|
+
'code': None, 'uid': None, 'size': None,
|
|
1148
|
+
'attachments': [],
|
|
1149
|
+
})
|
|
1150
|
+
|
|
1151
|
+
line = b'* LIST () "/" ~/Mail/meetings'
|
|
1152
|
+
mesg = parseLine(line)
|
|
1153
|
+
self.eq(mesg, {
|
|
1154
|
+
'tag': '*',
|
|
1155
|
+
'response': 'LIST',
|
|
1156
|
+
'data': br'() "/" ~/Mail/meetings',
|
|
1157
|
+
'code': None, 'uid': None, 'size': None,
|
|
1158
|
+
'attachments': [],
|
|
1159
|
+
})
|
|
1160
|
+
|
|
1161
|
+
line = br'* LIST (\Marked \NoInferiors) "/" "inbox"'
|
|
1162
|
+
mesg = parseLine(line)
|
|
1163
|
+
self.eq(mesg, {
|
|
1164
|
+
'tag': '*',
|
|
1165
|
+
'response': 'LIST',
|
|
1166
|
+
'data': br'(\Marked \NoInferiors) "/" "inbox"',
|
|
1167
|
+
'code': None, 'uid': None, 'size': None,
|
|
1168
|
+
'attachments': [],
|
|
1169
|
+
})
|
|
1170
|
+
|
|
1171
|
+
line = b'* LIST () "/" "Fruit"'
|
|
1172
|
+
mesg = parseLine(line)
|
|
1173
|
+
self.eq(mesg, {
|
|
1174
|
+
'tag': '*',
|
|
1175
|
+
'response': 'LIST',
|
|
1176
|
+
'data': b'() "/" "Fruit"',
|
|
1177
|
+
'code': None, 'uid': None, 'size': None,
|
|
1178
|
+
'attachments': [],
|
|
1179
|
+
})
|
|
1180
|
+
|
|
1181
|
+
line = b'* LIST () "/" "Fruit/Apple"'
|
|
1182
|
+
mesg = parseLine(line)
|
|
1183
|
+
self.eq(mesg, {
|
|
1184
|
+
'tag': '*',
|
|
1185
|
+
'response': 'LIST',
|
|
1186
|
+
'data': br'() "/" "Fruit/Apple"',
|
|
1187
|
+
'code': None, 'uid': None, 'size': None,
|
|
1188
|
+
'attachments': [],
|
|
1189
|
+
})
|
|
1190
|
+
|
|
1191
|
+
line = b'A101 OK LIST Completed'
|
|
1192
|
+
mesg = parseLine(line)
|
|
1193
|
+
self.eq(mesg, {
|
|
1194
|
+
'tag': 'A101',
|
|
1195
|
+
'response': 'OK',
|
|
1196
|
+
'data': b'LIST Completed',
|
|
1197
|
+
'code': None, 'uid': None, 'size': None,
|
|
1198
|
+
'attachments': [],
|
|
1199
|
+
})
|
|
1200
|
+
|
|
1201
|
+
# UID responses
|
|
1202
|
+
line = b'* 3 EXPUNGE'
|
|
1203
|
+
mesg = parseLine(line)
|
|
1204
|
+
self.eq(mesg, {
|
|
1205
|
+
'tag': '*',
|
|
1206
|
+
'response': 'EXPUNGE',
|
|
1207
|
+
'data': b'',
|
|
1208
|
+
'code': None,
|
|
1209
|
+
'uid': 3,
|
|
1210
|
+
'size': None,
|
|
1211
|
+
'attachments': [],
|
|
1212
|
+
})
|
|
1213
|
+
|
|
1214
|
+
line = b'A003 OK UID EXPUNGE completed'
|
|
1215
|
+
mesg = parseLine(line)
|
|
1216
|
+
self.eq(mesg, {
|
|
1217
|
+
'tag': 'A003',
|
|
1218
|
+
'response': 'OK',
|
|
1219
|
+
'data': b'UID EXPUNGE completed',
|
|
1220
|
+
'code': None, 'uid': None, 'size': None,
|
|
1221
|
+
'attachments': [],
|
|
1222
|
+
})
|
|
1223
|
+
|
|
1224
|
+
line = br'* 25 FETCH (FLAGS (\Seen) UID 4828442)'
|
|
1225
|
+
mesg = parseLine(line)
|
|
1226
|
+
self.eq(mesg, {
|
|
1227
|
+
'tag': '*',
|
|
1228
|
+
'response': 'FETCH',
|
|
1229
|
+
'data': br'(FLAGS (\Seen) UID 4828442)',
|
|
1230
|
+
'code': None,
|
|
1231
|
+
'uid': 25,
|
|
1232
|
+
'size': None,
|
|
1233
|
+
'attachments': [],
|
|
1234
|
+
})
|
|
1235
|
+
|
|
1236
|
+
line = b'* 12 FETCH (BODY[HEADER] {342}'
|
|
1237
|
+
mesg = parseLine(line)
|
|
1238
|
+
self.eq(mesg, {
|
|
1239
|
+
'tag': '*',
|
|
1240
|
+
'response': 'FETCH',
|
|
1241
|
+
'data': b'(BODY[HEADER]',
|
|
1242
|
+
'code': None,
|
|
1243
|
+
'uid': 12,
|
|
1244
|
+
'size': 342,
|
|
1245
|
+
'attachments': [],
|
|
1246
|
+
})
|
|
1247
|
+
|
|
1248
|
+
line = b'A999 OK UID FETCH completed'
|
|
1249
|
+
mesg = parseLine(line)
|
|
1250
|
+
self.eq(mesg, {
|
|
1251
|
+
'tag': 'A999',
|
|
1252
|
+
'response': 'OK',
|
|
1253
|
+
'data': b'UID FETCH completed',
|
|
1254
|
+
'code': None, 'uid': None, 'size': None,
|
|
1255
|
+
'attachments': [],
|
|
1256
|
+
})
|
|
1257
|
+
|
|
1258
|
+
# Expunge responses
|
|
1259
|
+
line = b'* 8 EXPUNGE'
|
|
1260
|
+
mesg = parseLine(line)
|
|
1261
|
+
self.eq(mesg, {
|
|
1262
|
+
'tag': '*',
|
|
1263
|
+
'response': 'EXPUNGE',
|
|
1264
|
+
'data': b'',
|
|
1265
|
+
'code': None,
|
|
1266
|
+
'uid': 8,
|
|
1267
|
+
'size': None,
|
|
1268
|
+
'attachments': [],
|
|
1269
|
+
})
|
|
1270
|
+
|
|
1271
|
+
line = b'A202 OK EXPUNGE completed'
|
|
1272
|
+
mesg = parseLine(line)
|
|
1273
|
+
self.eq(mesg, {
|
|
1274
|
+
'tag': 'A202',
|
|
1275
|
+
'response': 'OK',
|
|
1276
|
+
'data': b'EXPUNGE completed',
|
|
1277
|
+
'code': None, 'uid': None, 'size': None,
|
|
1278
|
+
'attachments': [],
|
|
1279
|
+
})
|
|
1280
|
+
|
|
1281
|
+
# Logout responses
|
|
1282
|
+
line = b'* BYE IMAP4rev2 Server logging out'
|
|
1283
|
+
mesg = parseLine(line)
|
|
1284
|
+
self.eq(mesg, {
|
|
1285
|
+
'tag': '*',
|
|
1286
|
+
'response': 'BYE',
|
|
1287
|
+
'data': b'IMAP4rev2 Server logging out',
|
|
1288
|
+
'code': None, 'uid': None, 'size': None,
|
|
1289
|
+
'attachments': [],
|
|
1290
|
+
})
|
|
1291
|
+
|
|
1292
|
+
line = b'A023 OK LOGOUT completed'
|
|
1293
|
+
mesg = parseLine(line)
|
|
1294
|
+
self.eq(mesg, {
|
|
1295
|
+
'tag': 'A023',
|
|
1296
|
+
'response': 'OK',
|
|
1297
|
+
'data': b'LOGOUT completed',
|
|
1298
|
+
'code': None, 'uid': None, 'size': None,
|
|
1299
|
+
'attachments': [],
|
|
1300
|
+
})
|
|
1301
|
+
|
|
1302
|
+
async def test_stormlib_imap_quote(self):
|
|
1303
|
+
self.eq(s_imap.quote('""'), '""')
|
|
1304
|
+
self.eq(s_imap.quote('foobar'), 'foobar')
|
|
1305
|
+
self.eq(s_imap.quote('foo"bar'), '"foo\\"bar"')
|
|
1306
|
+
self.eq(s_imap.quote('foo bar'), '"foo bar"')
|
|
1307
|
+
self.eq(s_imap.quote('foo\\bar'), '"foo\\\\bar"')
|
|
1308
|
+
self.eq(s_imap.quote('foo bar\\'), '"foo bar\\\\"')
|
|
1309
|
+
self.eq(s_imap.quote('"foo bar"'), '"\\"foo bar\\""')
|
|
1310
|
+
self.eq(s_imap.quote('foo "bar"'), '"foo \\"bar\\""')
|
|
1311
|
+
|
|
1312
|
+
async def test_stormlib_imap_qsplit(self):
|
|
1313
|
+
self.eq(s_imap.qsplit('"" bar'), ['', 'bar'])
|
|
1314
|
+
self.eq(s_imap.qsplit('"foo bar"'), ['foo bar'])
|
|
1315
|
+
self.eq(s_imap.qsplit('foo bar'), ['foo', 'bar'])
|
|
1316
|
+
self.eq(s_imap.qsplit('"foobar"'), ['foobar'])
|
|
1317
|
+
self.eq(s_imap.qsplit('"foo bar"'), ['foo bar'])
|
|
1318
|
+
self.eq(s_imap.qsplit('foo bar "foo bar"'), ['foo', 'bar', 'foo bar'])
|
|
1319
|
+
self.eq(s_imap.qsplit('foo bar "\\"\\"" "\\"foo\\""'), ['foo', 'bar', '""', '"foo"'])
|
|
1320
|
+
self.eq(s_imap.qsplit('foo bar "\\"" "\\"" "\\"foo\\""'), ['foo', 'bar', '"', '"', '"foo"'])
|
|
1321
|
+
self.eq(s_imap.qsplit('foo bar "foo\\\\"'), ['foo', 'bar', 'foo\\'])
|
|
1322
|
+
self.eq(s_imap.qsplit('foo bar "foo\\\\\\""'), ['foo', 'bar', 'foo\\"'])
|
|
1323
|
+
self.eq(s_imap.qsplit('(\\HasNoChildren) "/" "\\"foobar\\""'), [r'(\HasNoChildren)', '/', '"foobar"'])
|
|
1324
|
+
self.eq(s_imap.qsplit('foo \\bar foo'), ['foo', '\\bar', 'foo'])
|
|
1325
|
+
|
|
1326
|
+
with self.raises(s_exc.BadDataValu) as exc:
|
|
1327
|
+
s_imap.qsplit('foo bar "\\bfoo"')
|
|
1328
|
+
self.eq(exc.exception.get('mesg'), 'Invalid data: b cannot be escaped.')
|
|
1329
|
+
self.eq(exc.exception.get('data'), r'foo bar "\bfoo"')
|
|
1330
|
+
|
|
1331
|
+
with self.raises(s_exc.BadDataValu) as exc:
|
|
1332
|
+
s_imap.qsplit('foo bar "\\')
|
|
1333
|
+
self.eq(exc.exception.get('mesg'), 'Unable to parse IMAP response data.')
|
|
1334
|
+
self.eq(exc.exception.get('data'), 'foo bar "\\')
|
|
1335
|
+
|
|
1336
|
+
with self.raises(s_exc.BadDataValu) as exc:
|
|
1337
|
+
s_imap.qsplit(r'foo bar \"foo\""')
|
|
1338
|
+
self.eq(exc.exception.get('mesg'), 'Invalid data: " cannot be escaped.')
|
|
1339
|
+
self.eq(exc.exception.get('data'), r'foo bar \"foo\""')
|
|
1340
|
+
|
|
1341
|
+
with self.raises(s_exc.BadDataValu) as exc:
|
|
1342
|
+
s_imap.qsplit(r'foo bar \"foo\"')
|
|
1343
|
+
self.eq(exc.exception.get('mesg'), 'Invalid data: " cannot be escaped.')
|
|
1344
|
+
self.eq(exc.exception.get('data'), r'foo bar \"foo\"')
|
|
1345
|
+
|
|
1346
|
+
with self.raises(s_exc.BadDataValu) as exc:
|
|
1347
|
+
s_imap.qsplit('foo bar"')
|
|
1348
|
+
self.eq(exc.exception.get('mesg'), 'Quoted strings must be preceded and followed by a space.')
|
|
1349
|
+
self.eq(exc.exception.get('data'), 'foo bar"')
|
|
1350
|
+
|
|
1351
|
+
with self.raises(s_exc.BadDataValu) as exc:
|
|
1352
|
+
s_imap.qsplit('foo \\bar "foo')
|
|
1353
|
+
self.eq(exc.exception.get('mesg'), 'Unclosed quotes in text.')
|
|
1354
|
+
self.eq(exc.exception.get('data'), 'foo \\bar "foo')
|
|
1355
|
+
|
|
1356
|
+
with self.raises(s_exc.BadDataValu) as exc:
|
|
1357
|
+
s_imap.qsplit('foo bar "foo')
|
|
1358
|
+
self.eq(exc.exception.get('mesg'), 'Unclosed quotes in text.')
|
|
1359
|
+
self.eq(exc.exception.get('data'), 'foo bar "foo')
|