net-imap 0.2.1 → 0.2.2
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 net-imap might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +3 -5
- data/.gitignore +1 -0
- data/README.md +1 -1
- data/lib/net/imap.rb +149 -2702
- data/lib/net/imap/authenticators.rb +44 -0
- data/lib/net/imap/authenticators/cram_md5.rb +49 -0
- data/lib/net/imap/authenticators/digest_md5.rb +111 -0
- data/lib/net/imap/authenticators/login.rb +43 -0
- data/lib/net/imap/authenticators/plain.rb +41 -0
- data/lib/net/imap/command_data.rb +301 -0
- data/lib/net/imap/data_encoding.rb +47 -0
- data/lib/net/imap/flags.rb +76 -0
- data/lib/net/imap/response_data.rb +527 -0
- data/lib/net/imap/response_parser.rb +1530 -0
- metadata +12 -3
- data/Gemfile.lock +0 -23
@@ -0,0 +1,1530 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Net
|
4
|
+
class IMAP < Protocol
|
5
|
+
|
6
|
+
class ResponseParser # :nodoc:
|
7
|
+
def initialize
|
8
|
+
@str = nil
|
9
|
+
@pos = nil
|
10
|
+
@lex_state = nil
|
11
|
+
@token = nil
|
12
|
+
@flag_symbols = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse(str)
|
16
|
+
@str = str
|
17
|
+
@pos = 0
|
18
|
+
@lex_state = EXPR_BEG
|
19
|
+
@token = nil
|
20
|
+
return response
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
EXPR_BEG = :EXPR_BEG
|
26
|
+
EXPR_DATA = :EXPR_DATA
|
27
|
+
EXPR_TEXT = :EXPR_TEXT
|
28
|
+
EXPR_RTEXT = :EXPR_RTEXT
|
29
|
+
EXPR_CTEXT = :EXPR_CTEXT
|
30
|
+
|
31
|
+
T_SPACE = :SPACE
|
32
|
+
T_NIL = :NIL
|
33
|
+
T_NUMBER = :NUMBER
|
34
|
+
T_ATOM = :ATOM
|
35
|
+
T_QUOTED = :QUOTED
|
36
|
+
T_LPAR = :LPAR
|
37
|
+
T_RPAR = :RPAR
|
38
|
+
T_BSLASH = :BSLASH
|
39
|
+
T_STAR = :STAR
|
40
|
+
T_LBRA = :LBRA
|
41
|
+
T_RBRA = :RBRA
|
42
|
+
T_LITERAL = :LITERAL
|
43
|
+
T_PLUS = :PLUS
|
44
|
+
T_PERCENT = :PERCENT
|
45
|
+
T_CRLF = :CRLF
|
46
|
+
T_EOF = :EOF
|
47
|
+
T_TEXT = :TEXT
|
48
|
+
|
49
|
+
BEG_REGEXP = /\G(?:\
|
50
|
+
(?# 1: SPACE )( +)|\
|
51
|
+
(?# 2: NIL )(NIL)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
|
52
|
+
(?# 3: NUMBER )(\d+)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
|
53
|
+
(?# 4: ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+]+)|\
|
54
|
+
(?# 5: QUOTED )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\
|
55
|
+
(?# 6: LPAR )(\()|\
|
56
|
+
(?# 7: RPAR )(\))|\
|
57
|
+
(?# 8: BSLASH )(\\)|\
|
58
|
+
(?# 9: STAR )(\*)|\
|
59
|
+
(?# 10: LBRA )(\[)|\
|
60
|
+
(?# 11: RBRA )(\])|\
|
61
|
+
(?# 12: LITERAL )\{(\d+)\}\r\n|\
|
62
|
+
(?# 13: PLUS )(\+)|\
|
63
|
+
(?# 14: PERCENT )(%)|\
|
64
|
+
(?# 15: CRLF )(\r\n)|\
|
65
|
+
(?# 16: EOF )(\z))/ni
|
66
|
+
|
67
|
+
DATA_REGEXP = /\G(?:\
|
68
|
+
(?# 1: SPACE )( )|\
|
69
|
+
(?# 2: NIL )(NIL)|\
|
70
|
+
(?# 3: NUMBER )(\d+)|\
|
71
|
+
(?# 4: QUOTED )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\
|
72
|
+
(?# 5: LITERAL )\{(\d+)\}\r\n|\
|
73
|
+
(?# 6: LPAR )(\()|\
|
74
|
+
(?# 7: RPAR )(\)))/ni
|
75
|
+
|
76
|
+
TEXT_REGEXP = /\G(?:\
|
77
|
+
(?# 1: TEXT )([^\x00\r\n]*))/ni
|
78
|
+
|
79
|
+
RTEXT_REGEXP = /\G(?:\
|
80
|
+
(?# 1: LBRA )(\[)|\
|
81
|
+
(?# 2: TEXT )([^\x00\r\n]*))/ni
|
82
|
+
|
83
|
+
CTEXT_REGEXP = /\G(?:\
|
84
|
+
(?# 1: TEXT )([^\x00\r\n\]]*))/ni
|
85
|
+
|
86
|
+
Token = Struct.new(:symbol, :value)
|
87
|
+
|
88
|
+
def response
|
89
|
+
token = lookahead
|
90
|
+
case token.symbol
|
91
|
+
when T_PLUS
|
92
|
+
result = continue_req
|
93
|
+
when T_STAR
|
94
|
+
result = response_untagged
|
95
|
+
else
|
96
|
+
result = response_tagged
|
97
|
+
end
|
98
|
+
while lookahead.symbol == T_SPACE
|
99
|
+
# Ignore trailing space for Microsoft Exchange Server
|
100
|
+
shift_token
|
101
|
+
end
|
102
|
+
match(T_CRLF)
|
103
|
+
match(T_EOF)
|
104
|
+
return result
|
105
|
+
end
|
106
|
+
|
107
|
+
def continue_req
|
108
|
+
match(T_PLUS)
|
109
|
+
token = lookahead
|
110
|
+
if token.symbol == T_SPACE
|
111
|
+
shift_token
|
112
|
+
return ContinuationRequest.new(resp_text, @str)
|
113
|
+
else
|
114
|
+
return ContinuationRequest.new(ResponseText.new(nil, ""), @str)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def response_untagged
|
119
|
+
match(T_STAR)
|
120
|
+
match(T_SPACE)
|
121
|
+
token = lookahead
|
122
|
+
if token.symbol == T_NUMBER
|
123
|
+
return numeric_response
|
124
|
+
elsif token.symbol == T_ATOM
|
125
|
+
case token.value
|
126
|
+
when /\A(?:OK|NO|BAD|BYE|PREAUTH)\z/ni
|
127
|
+
return response_cond
|
128
|
+
when /\A(?:FLAGS)\z/ni
|
129
|
+
return flags_response
|
130
|
+
when /\A(?:ID)\z/ni
|
131
|
+
return id_response
|
132
|
+
when /\A(?:LIST|LSUB|XLIST)\z/ni
|
133
|
+
return list_response
|
134
|
+
when /\A(?:NAMESPACE)\z/ni
|
135
|
+
return namespace_response
|
136
|
+
when /\A(?:QUOTA)\z/ni
|
137
|
+
return getquota_response
|
138
|
+
when /\A(?:QUOTAROOT)\z/ni
|
139
|
+
return getquotaroot_response
|
140
|
+
when /\A(?:ACL)\z/ni
|
141
|
+
return getacl_response
|
142
|
+
when /\A(?:SEARCH|SORT)\z/ni
|
143
|
+
return search_response
|
144
|
+
when /\A(?:THREAD)\z/ni
|
145
|
+
return thread_response
|
146
|
+
when /\A(?:STATUS)\z/ni
|
147
|
+
return status_response
|
148
|
+
when /\A(?:CAPABILITY)\z/ni
|
149
|
+
return capability_response
|
150
|
+
when /\A(?:NOOP)\z/ni
|
151
|
+
return ignored_response
|
152
|
+
else
|
153
|
+
return text_response
|
154
|
+
end
|
155
|
+
else
|
156
|
+
parse_error("unexpected token %s", token.symbol)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def response_tagged
|
161
|
+
tag = astring_chars
|
162
|
+
match(T_SPACE)
|
163
|
+
token = match(T_ATOM)
|
164
|
+
name = token.value.upcase
|
165
|
+
match(T_SPACE)
|
166
|
+
return TaggedResponse.new(tag, name, resp_text, @str)
|
167
|
+
end
|
168
|
+
|
169
|
+
def response_cond
|
170
|
+
token = match(T_ATOM)
|
171
|
+
name = token.value.upcase
|
172
|
+
match(T_SPACE)
|
173
|
+
return UntaggedResponse.new(name, resp_text, @str)
|
174
|
+
end
|
175
|
+
|
176
|
+
def numeric_response
|
177
|
+
n = number
|
178
|
+
match(T_SPACE)
|
179
|
+
token = match(T_ATOM)
|
180
|
+
name = token.value.upcase
|
181
|
+
case name
|
182
|
+
when "EXISTS", "RECENT", "EXPUNGE"
|
183
|
+
return UntaggedResponse.new(name, n, @str)
|
184
|
+
when "FETCH"
|
185
|
+
shift_token
|
186
|
+
match(T_SPACE)
|
187
|
+
data = FetchData.new(n, msg_att(n))
|
188
|
+
return UntaggedResponse.new(name, data, @str)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def msg_att(n)
|
193
|
+
match(T_LPAR)
|
194
|
+
attr = {}
|
195
|
+
while true
|
196
|
+
token = lookahead
|
197
|
+
case token.symbol
|
198
|
+
when T_RPAR
|
199
|
+
shift_token
|
200
|
+
break
|
201
|
+
when T_SPACE
|
202
|
+
shift_token
|
203
|
+
next
|
204
|
+
end
|
205
|
+
case token.value
|
206
|
+
when /\A(?:ENVELOPE)\z/ni
|
207
|
+
name, val = envelope_data
|
208
|
+
when /\A(?:FLAGS)\z/ni
|
209
|
+
name, val = flags_data
|
210
|
+
when /\A(?:INTERNALDATE)\z/ni
|
211
|
+
name, val = internaldate_data
|
212
|
+
when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni
|
213
|
+
name, val = rfc822_text
|
214
|
+
when /\A(?:RFC822\.SIZE)\z/ni
|
215
|
+
name, val = rfc822_size
|
216
|
+
when /\A(?:BODY(?:STRUCTURE)?)\z/ni
|
217
|
+
name, val = body_data
|
218
|
+
when /\A(?:UID)\z/ni
|
219
|
+
name, val = uid_data
|
220
|
+
when /\A(?:MODSEQ)\z/ni
|
221
|
+
name, val = modseq_data
|
222
|
+
else
|
223
|
+
parse_error("unknown attribute `%s' for {%d}", token.value, n)
|
224
|
+
end
|
225
|
+
attr[name] = val
|
226
|
+
end
|
227
|
+
return attr
|
228
|
+
end
|
229
|
+
|
230
|
+
def envelope_data
|
231
|
+
token = match(T_ATOM)
|
232
|
+
name = token.value.upcase
|
233
|
+
match(T_SPACE)
|
234
|
+
return name, envelope
|
235
|
+
end
|
236
|
+
|
237
|
+
def envelope
|
238
|
+
@lex_state = EXPR_DATA
|
239
|
+
token = lookahead
|
240
|
+
if token.symbol == T_NIL
|
241
|
+
shift_token
|
242
|
+
result = nil
|
243
|
+
else
|
244
|
+
match(T_LPAR)
|
245
|
+
date = nstring
|
246
|
+
match(T_SPACE)
|
247
|
+
subject = nstring
|
248
|
+
match(T_SPACE)
|
249
|
+
from = address_list
|
250
|
+
match(T_SPACE)
|
251
|
+
sender = address_list
|
252
|
+
match(T_SPACE)
|
253
|
+
reply_to = address_list
|
254
|
+
match(T_SPACE)
|
255
|
+
to = address_list
|
256
|
+
match(T_SPACE)
|
257
|
+
cc = address_list
|
258
|
+
match(T_SPACE)
|
259
|
+
bcc = address_list
|
260
|
+
match(T_SPACE)
|
261
|
+
in_reply_to = nstring
|
262
|
+
match(T_SPACE)
|
263
|
+
message_id = nstring
|
264
|
+
match(T_RPAR)
|
265
|
+
result = Envelope.new(date, subject, from, sender, reply_to,
|
266
|
+
to, cc, bcc, in_reply_to, message_id)
|
267
|
+
end
|
268
|
+
@lex_state = EXPR_BEG
|
269
|
+
return result
|
270
|
+
end
|
271
|
+
|
272
|
+
def flags_data
|
273
|
+
token = match(T_ATOM)
|
274
|
+
name = token.value.upcase
|
275
|
+
match(T_SPACE)
|
276
|
+
return name, flag_list
|
277
|
+
end
|
278
|
+
|
279
|
+
def internaldate_data
|
280
|
+
token = match(T_ATOM)
|
281
|
+
name = token.value.upcase
|
282
|
+
match(T_SPACE)
|
283
|
+
token = match(T_QUOTED)
|
284
|
+
return name, token.value
|
285
|
+
end
|
286
|
+
|
287
|
+
def rfc822_text
|
288
|
+
token = match(T_ATOM)
|
289
|
+
name = token.value.upcase
|
290
|
+
token = lookahead
|
291
|
+
if token.symbol == T_LBRA
|
292
|
+
shift_token
|
293
|
+
match(T_RBRA)
|
294
|
+
end
|
295
|
+
match(T_SPACE)
|
296
|
+
return name, nstring
|
297
|
+
end
|
298
|
+
|
299
|
+
def rfc822_size
|
300
|
+
token = match(T_ATOM)
|
301
|
+
name = token.value.upcase
|
302
|
+
match(T_SPACE)
|
303
|
+
return name, number
|
304
|
+
end
|
305
|
+
|
306
|
+
def body_data
|
307
|
+
token = match(T_ATOM)
|
308
|
+
name = token.value.upcase
|
309
|
+
token = lookahead
|
310
|
+
if token.symbol == T_SPACE
|
311
|
+
shift_token
|
312
|
+
return name, body
|
313
|
+
end
|
314
|
+
name.concat(section)
|
315
|
+
token = lookahead
|
316
|
+
if token.symbol == T_ATOM
|
317
|
+
name.concat(token.value)
|
318
|
+
shift_token
|
319
|
+
end
|
320
|
+
match(T_SPACE)
|
321
|
+
data = nstring
|
322
|
+
return name, data
|
323
|
+
end
|
324
|
+
|
325
|
+
def body
|
326
|
+
@lex_state = EXPR_DATA
|
327
|
+
token = lookahead
|
328
|
+
if token.symbol == T_NIL
|
329
|
+
shift_token
|
330
|
+
result = nil
|
331
|
+
else
|
332
|
+
match(T_LPAR)
|
333
|
+
token = lookahead
|
334
|
+
if token.symbol == T_LPAR
|
335
|
+
result = body_type_mpart
|
336
|
+
else
|
337
|
+
result = body_type_1part
|
338
|
+
end
|
339
|
+
match(T_RPAR)
|
340
|
+
end
|
341
|
+
@lex_state = EXPR_BEG
|
342
|
+
return result
|
343
|
+
end
|
344
|
+
|
345
|
+
def body_type_1part
|
346
|
+
token = lookahead
|
347
|
+
case token.value
|
348
|
+
when /\A(?:TEXT)\z/ni
|
349
|
+
return body_type_text
|
350
|
+
when /\A(?:MESSAGE)\z/ni
|
351
|
+
return body_type_msg
|
352
|
+
when /\A(?:ATTACHMENT)\z/ni
|
353
|
+
return body_type_attachment
|
354
|
+
when /\A(?:MIXED)\z/ni
|
355
|
+
return body_type_mixed
|
356
|
+
else
|
357
|
+
return body_type_basic
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
def body_type_basic
|
362
|
+
mtype, msubtype = media_type
|
363
|
+
token = lookahead
|
364
|
+
if token.symbol == T_RPAR
|
365
|
+
return BodyTypeBasic.new(mtype, msubtype)
|
366
|
+
end
|
367
|
+
match(T_SPACE)
|
368
|
+
param, content_id, desc, enc, size = body_fields
|
369
|
+
md5, disposition, language, extension = body_ext_1part
|
370
|
+
return BodyTypeBasic.new(mtype, msubtype,
|
371
|
+
param, content_id,
|
372
|
+
desc, enc, size,
|
373
|
+
md5, disposition, language, extension)
|
374
|
+
end
|
375
|
+
|
376
|
+
def body_type_text
|
377
|
+
mtype, msubtype = media_type
|
378
|
+
match(T_SPACE)
|
379
|
+
param, content_id, desc, enc, size = body_fields
|
380
|
+
match(T_SPACE)
|
381
|
+
lines = number
|
382
|
+
md5, disposition, language, extension = body_ext_1part
|
383
|
+
return BodyTypeText.new(mtype, msubtype,
|
384
|
+
param, content_id,
|
385
|
+
desc, enc, size,
|
386
|
+
lines,
|
387
|
+
md5, disposition, language, extension)
|
388
|
+
end
|
389
|
+
|
390
|
+
def body_type_msg
|
391
|
+
mtype, msubtype = media_type
|
392
|
+
match(T_SPACE)
|
393
|
+
param, content_id, desc, enc, size = body_fields
|
394
|
+
|
395
|
+
token = lookahead
|
396
|
+
if token.symbol == T_RPAR
|
397
|
+
# If this is not message/rfc822, we shouldn't apply the RFC822
|
398
|
+
# spec to it. We should handle anything other than
|
399
|
+
# message/rfc822 using multipart extension data [rfc3501] (i.e.
|
400
|
+
# the data itself won't be returned, we would have to retrieve it
|
401
|
+
# with BODYSTRUCTURE instead of with BODY
|
402
|
+
|
403
|
+
# Also, sometimes a message/rfc822 is included as a large
|
404
|
+
# attachment instead of having all of the other details
|
405
|
+
# (e.g. attaching a .eml file to an email)
|
406
|
+
if msubtype == "RFC822"
|
407
|
+
return BodyTypeMessage.new(mtype, msubtype, param, content_id,
|
408
|
+
desc, enc, size, nil, nil, nil, nil,
|
409
|
+
nil, nil, nil)
|
410
|
+
else
|
411
|
+
return BodyTypeExtension.new(mtype, msubtype,
|
412
|
+
param, content_id,
|
413
|
+
desc, enc, size)
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
match(T_SPACE)
|
418
|
+
env = envelope
|
419
|
+
match(T_SPACE)
|
420
|
+
b = body
|
421
|
+
match(T_SPACE)
|
422
|
+
lines = number
|
423
|
+
md5, disposition, language, extension = body_ext_1part
|
424
|
+
return BodyTypeMessage.new(mtype, msubtype,
|
425
|
+
param, content_id,
|
426
|
+
desc, enc, size,
|
427
|
+
env, b, lines,
|
428
|
+
md5, disposition, language, extension)
|
429
|
+
end
|
430
|
+
|
431
|
+
def body_type_attachment
|
432
|
+
mtype = case_insensitive_string
|
433
|
+
match(T_SPACE)
|
434
|
+
param = body_fld_param
|
435
|
+
return BodyTypeAttachment.new(mtype, nil, param)
|
436
|
+
end
|
437
|
+
|
438
|
+
def body_type_mixed
|
439
|
+
mtype = "MULTIPART"
|
440
|
+
msubtype = case_insensitive_string
|
441
|
+
param, disposition, language, extension = body_ext_mpart
|
442
|
+
return BodyTypeBasic.new(mtype, msubtype, param, nil, nil, nil, nil, nil, disposition, language, extension)
|
443
|
+
end
|
444
|
+
|
445
|
+
def body_type_mpart
|
446
|
+
parts = []
|
447
|
+
while true
|
448
|
+
token = lookahead
|
449
|
+
if token.symbol == T_SPACE
|
450
|
+
shift_token
|
451
|
+
break
|
452
|
+
end
|
453
|
+
parts.push(body)
|
454
|
+
end
|
455
|
+
mtype = "MULTIPART"
|
456
|
+
msubtype = case_insensitive_string
|
457
|
+
param, disposition, language, extension = body_ext_mpart
|
458
|
+
return BodyTypeMultipart.new(mtype, msubtype, parts,
|
459
|
+
param, disposition, language,
|
460
|
+
extension)
|
461
|
+
end
|
462
|
+
|
463
|
+
def media_type
|
464
|
+
mtype = case_insensitive_string
|
465
|
+
token = lookahead
|
466
|
+
if token.symbol != T_SPACE
|
467
|
+
return mtype, nil
|
468
|
+
end
|
469
|
+
match(T_SPACE)
|
470
|
+
msubtype = case_insensitive_string
|
471
|
+
return mtype, msubtype
|
472
|
+
end
|
473
|
+
|
474
|
+
def body_fields
|
475
|
+
param = body_fld_param
|
476
|
+
match(T_SPACE)
|
477
|
+
content_id = nstring
|
478
|
+
match(T_SPACE)
|
479
|
+
desc = nstring
|
480
|
+
match(T_SPACE)
|
481
|
+
enc = case_insensitive_string
|
482
|
+
match(T_SPACE)
|
483
|
+
size = number
|
484
|
+
return param, content_id, desc, enc, size
|
485
|
+
end
|
486
|
+
|
487
|
+
def body_fld_param
|
488
|
+
token = lookahead
|
489
|
+
if token.symbol == T_NIL
|
490
|
+
shift_token
|
491
|
+
return nil
|
492
|
+
end
|
493
|
+
match(T_LPAR)
|
494
|
+
param = {}
|
495
|
+
while true
|
496
|
+
token = lookahead
|
497
|
+
case token.symbol
|
498
|
+
when T_RPAR
|
499
|
+
shift_token
|
500
|
+
break
|
501
|
+
when T_SPACE
|
502
|
+
shift_token
|
503
|
+
end
|
504
|
+
name = case_insensitive_string
|
505
|
+
match(T_SPACE)
|
506
|
+
val = string
|
507
|
+
param[name] = val
|
508
|
+
end
|
509
|
+
return param
|
510
|
+
end
|
511
|
+
|
512
|
+
def body_ext_1part
|
513
|
+
token = lookahead
|
514
|
+
if token.symbol == T_SPACE
|
515
|
+
shift_token
|
516
|
+
else
|
517
|
+
return nil
|
518
|
+
end
|
519
|
+
md5 = nstring
|
520
|
+
|
521
|
+
token = lookahead
|
522
|
+
if token.symbol == T_SPACE
|
523
|
+
shift_token
|
524
|
+
else
|
525
|
+
return md5
|
526
|
+
end
|
527
|
+
disposition = body_fld_dsp
|
528
|
+
|
529
|
+
token = lookahead
|
530
|
+
if token.symbol == T_SPACE
|
531
|
+
shift_token
|
532
|
+
else
|
533
|
+
return md5, disposition
|
534
|
+
end
|
535
|
+
language = body_fld_lang
|
536
|
+
|
537
|
+
token = lookahead
|
538
|
+
if token.symbol == T_SPACE
|
539
|
+
shift_token
|
540
|
+
else
|
541
|
+
return md5, disposition, language
|
542
|
+
end
|
543
|
+
|
544
|
+
extension = body_extensions
|
545
|
+
return md5, disposition, language, extension
|
546
|
+
end
|
547
|
+
|
548
|
+
def body_ext_mpart
|
549
|
+
token = lookahead
|
550
|
+
if token.symbol == T_SPACE
|
551
|
+
shift_token
|
552
|
+
else
|
553
|
+
return nil
|
554
|
+
end
|
555
|
+
param = body_fld_param
|
556
|
+
|
557
|
+
token = lookahead
|
558
|
+
if token.symbol == T_SPACE
|
559
|
+
shift_token
|
560
|
+
else
|
561
|
+
return param
|
562
|
+
end
|
563
|
+
disposition = body_fld_dsp
|
564
|
+
|
565
|
+
token = lookahead
|
566
|
+
if token.symbol == T_SPACE
|
567
|
+
shift_token
|
568
|
+
else
|
569
|
+
return param, disposition
|
570
|
+
end
|
571
|
+
language = body_fld_lang
|
572
|
+
|
573
|
+
token = lookahead
|
574
|
+
if token.symbol == T_SPACE
|
575
|
+
shift_token
|
576
|
+
else
|
577
|
+
return param, disposition, language
|
578
|
+
end
|
579
|
+
|
580
|
+
extension = body_extensions
|
581
|
+
return param, disposition, language, extension
|
582
|
+
end
|
583
|
+
|
584
|
+
def body_fld_dsp
|
585
|
+
token = lookahead
|
586
|
+
if token.symbol == T_NIL
|
587
|
+
shift_token
|
588
|
+
return nil
|
589
|
+
end
|
590
|
+
match(T_LPAR)
|
591
|
+
dsp_type = case_insensitive_string
|
592
|
+
match(T_SPACE)
|
593
|
+
param = body_fld_param
|
594
|
+
match(T_RPAR)
|
595
|
+
return ContentDisposition.new(dsp_type, param)
|
596
|
+
end
|
597
|
+
|
598
|
+
def body_fld_lang
|
599
|
+
token = lookahead
|
600
|
+
if token.symbol == T_LPAR
|
601
|
+
shift_token
|
602
|
+
result = []
|
603
|
+
while true
|
604
|
+
token = lookahead
|
605
|
+
case token.symbol
|
606
|
+
when T_RPAR
|
607
|
+
shift_token
|
608
|
+
return result
|
609
|
+
when T_SPACE
|
610
|
+
shift_token
|
611
|
+
end
|
612
|
+
result.push(case_insensitive_string)
|
613
|
+
end
|
614
|
+
else
|
615
|
+
lang = nstring
|
616
|
+
if lang
|
617
|
+
return lang.upcase
|
618
|
+
else
|
619
|
+
return lang
|
620
|
+
end
|
621
|
+
end
|
622
|
+
end
|
623
|
+
|
624
|
+
def body_extensions
|
625
|
+
result = []
|
626
|
+
while true
|
627
|
+
token = lookahead
|
628
|
+
case token.symbol
|
629
|
+
when T_RPAR
|
630
|
+
return result
|
631
|
+
when T_SPACE
|
632
|
+
shift_token
|
633
|
+
end
|
634
|
+
result.push(body_extension)
|
635
|
+
end
|
636
|
+
end
|
637
|
+
|
638
|
+
def body_extension
|
639
|
+
token = lookahead
|
640
|
+
case token.symbol
|
641
|
+
when T_LPAR
|
642
|
+
shift_token
|
643
|
+
result = body_extensions
|
644
|
+
match(T_RPAR)
|
645
|
+
return result
|
646
|
+
when T_NUMBER
|
647
|
+
return number
|
648
|
+
else
|
649
|
+
return nstring
|
650
|
+
end
|
651
|
+
end
|
652
|
+
|
653
|
+
def section
|
654
|
+
str = String.new
|
655
|
+
token = match(T_LBRA)
|
656
|
+
str.concat(token.value)
|
657
|
+
token = match(T_ATOM, T_NUMBER, T_RBRA)
|
658
|
+
if token.symbol == T_RBRA
|
659
|
+
str.concat(token.value)
|
660
|
+
return str
|
661
|
+
end
|
662
|
+
str.concat(token.value)
|
663
|
+
token = lookahead
|
664
|
+
if token.symbol == T_SPACE
|
665
|
+
shift_token
|
666
|
+
str.concat(token.value)
|
667
|
+
token = match(T_LPAR)
|
668
|
+
str.concat(token.value)
|
669
|
+
while true
|
670
|
+
token = lookahead
|
671
|
+
case token.symbol
|
672
|
+
when T_RPAR
|
673
|
+
str.concat(token.value)
|
674
|
+
shift_token
|
675
|
+
break
|
676
|
+
when T_SPACE
|
677
|
+
shift_token
|
678
|
+
str.concat(token.value)
|
679
|
+
end
|
680
|
+
str.concat(format_string(astring))
|
681
|
+
end
|
682
|
+
end
|
683
|
+
token = match(T_RBRA)
|
684
|
+
str.concat(token.value)
|
685
|
+
return str
|
686
|
+
end
|
687
|
+
|
688
|
+
def format_string(str)
|
689
|
+
case str
|
690
|
+
when ""
|
691
|
+
return '""'
|
692
|
+
when /[\x80-\xff\r\n]/n
|
693
|
+
# literal
|
694
|
+
return "{" + str.bytesize.to_s + "}" + CRLF + str
|
695
|
+
when /[(){ \x00-\x1f\x7f%*"\\]/n
|
696
|
+
# quoted string
|
697
|
+
return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"'
|
698
|
+
else
|
699
|
+
# atom
|
700
|
+
return str
|
701
|
+
end
|
702
|
+
end
|
703
|
+
|
704
|
+
def uid_data
|
705
|
+
token = match(T_ATOM)
|
706
|
+
name = token.value.upcase
|
707
|
+
match(T_SPACE)
|
708
|
+
return name, number
|
709
|
+
end
|
710
|
+
|
711
|
+
def modseq_data
|
712
|
+
token = match(T_ATOM)
|
713
|
+
name = token.value.upcase
|
714
|
+
match(T_SPACE)
|
715
|
+
match(T_LPAR)
|
716
|
+
modseq = number
|
717
|
+
match(T_RPAR)
|
718
|
+
return name, modseq
|
719
|
+
end
|
720
|
+
|
721
|
+
def ignored_response
|
722
|
+
while lookahead.symbol != T_CRLF
|
723
|
+
shift_token
|
724
|
+
end
|
725
|
+
return IgnoredResponse.new(@str)
|
726
|
+
end
|
727
|
+
|
728
|
+
def text_response
|
729
|
+
token = match(T_ATOM)
|
730
|
+
name = token.value.upcase
|
731
|
+
match(T_SPACE)
|
732
|
+
return UntaggedResponse.new(name, text)
|
733
|
+
end
|
734
|
+
|
735
|
+
def flags_response
|
736
|
+
token = match(T_ATOM)
|
737
|
+
name = token.value.upcase
|
738
|
+
match(T_SPACE)
|
739
|
+
return UntaggedResponse.new(name, flag_list, @str)
|
740
|
+
end
|
741
|
+
|
742
|
+
def list_response
|
743
|
+
token = match(T_ATOM)
|
744
|
+
name = token.value.upcase
|
745
|
+
match(T_SPACE)
|
746
|
+
return UntaggedResponse.new(name, mailbox_list, @str)
|
747
|
+
end
|
748
|
+
|
749
|
+
def mailbox_list
|
750
|
+
attr = flag_list
|
751
|
+
match(T_SPACE)
|
752
|
+
token = match(T_QUOTED, T_NIL)
|
753
|
+
if token.symbol == T_NIL
|
754
|
+
delim = nil
|
755
|
+
else
|
756
|
+
delim = token.value
|
757
|
+
end
|
758
|
+
match(T_SPACE)
|
759
|
+
name = astring
|
760
|
+
return MailboxList.new(attr, delim, name)
|
761
|
+
end
|
762
|
+
|
763
|
+
def getquota_response
|
764
|
+
# If quota never established, get back
|
765
|
+
# `NO Quota root does not exist'.
|
766
|
+
# If quota removed, get `()' after the
|
767
|
+
# folder spec with no mention of `STORAGE'.
|
768
|
+
token = match(T_ATOM)
|
769
|
+
name = token.value.upcase
|
770
|
+
match(T_SPACE)
|
771
|
+
mailbox = astring
|
772
|
+
match(T_SPACE)
|
773
|
+
match(T_LPAR)
|
774
|
+
token = lookahead
|
775
|
+
case token.symbol
|
776
|
+
when T_RPAR
|
777
|
+
shift_token
|
778
|
+
data = MailboxQuota.new(mailbox, nil, nil)
|
779
|
+
return UntaggedResponse.new(name, data, @str)
|
780
|
+
when T_ATOM
|
781
|
+
shift_token
|
782
|
+
match(T_SPACE)
|
783
|
+
token = match(T_NUMBER)
|
784
|
+
usage = token.value
|
785
|
+
match(T_SPACE)
|
786
|
+
token = match(T_NUMBER)
|
787
|
+
quota = token.value
|
788
|
+
match(T_RPAR)
|
789
|
+
data = MailboxQuota.new(mailbox, usage, quota)
|
790
|
+
return UntaggedResponse.new(name, data, @str)
|
791
|
+
else
|
792
|
+
parse_error("unexpected token %s", token.symbol)
|
793
|
+
end
|
794
|
+
end
|
795
|
+
|
796
|
+
def getquotaroot_response
|
797
|
+
# Similar to getquota, but only admin can use getquota.
|
798
|
+
token = match(T_ATOM)
|
799
|
+
name = token.value.upcase
|
800
|
+
match(T_SPACE)
|
801
|
+
mailbox = astring
|
802
|
+
quotaroots = []
|
803
|
+
while true
|
804
|
+
token = lookahead
|
805
|
+
break unless token.symbol == T_SPACE
|
806
|
+
shift_token
|
807
|
+
quotaroots.push(astring)
|
808
|
+
end
|
809
|
+
data = MailboxQuotaRoot.new(mailbox, quotaroots)
|
810
|
+
return UntaggedResponse.new(name, data, @str)
|
811
|
+
end
|
812
|
+
|
813
|
+
def getacl_response
|
814
|
+
token = match(T_ATOM)
|
815
|
+
name = token.value.upcase
|
816
|
+
match(T_SPACE)
|
817
|
+
mailbox = astring
|
818
|
+
data = []
|
819
|
+
token = lookahead
|
820
|
+
if token.symbol == T_SPACE
|
821
|
+
shift_token
|
822
|
+
while true
|
823
|
+
token = lookahead
|
824
|
+
case token.symbol
|
825
|
+
when T_CRLF
|
826
|
+
break
|
827
|
+
when T_SPACE
|
828
|
+
shift_token
|
829
|
+
end
|
830
|
+
user = astring
|
831
|
+
match(T_SPACE)
|
832
|
+
rights = astring
|
833
|
+
data.push(MailboxACLItem.new(user, rights, mailbox))
|
834
|
+
end
|
835
|
+
end
|
836
|
+
return UntaggedResponse.new(name, data, @str)
|
837
|
+
end
|
838
|
+
|
839
|
+
def search_response
|
840
|
+
token = match(T_ATOM)
|
841
|
+
name = token.value.upcase
|
842
|
+
token = lookahead
|
843
|
+
if token.symbol == T_SPACE
|
844
|
+
shift_token
|
845
|
+
data = []
|
846
|
+
while true
|
847
|
+
token = lookahead
|
848
|
+
case token.symbol
|
849
|
+
when T_CRLF
|
850
|
+
break
|
851
|
+
when T_SPACE
|
852
|
+
shift_token
|
853
|
+
when T_NUMBER
|
854
|
+
data.push(number)
|
855
|
+
when T_LPAR
|
856
|
+
# TODO: include the MODSEQ value in a response
|
857
|
+
shift_token
|
858
|
+
match(T_ATOM)
|
859
|
+
match(T_SPACE)
|
860
|
+
match(T_NUMBER)
|
861
|
+
match(T_RPAR)
|
862
|
+
end
|
863
|
+
end
|
864
|
+
else
|
865
|
+
data = []
|
866
|
+
end
|
867
|
+
return UntaggedResponse.new(name, data, @str)
|
868
|
+
end
|
869
|
+
|
870
|
+
def thread_response
|
871
|
+
token = match(T_ATOM)
|
872
|
+
name = token.value.upcase
|
873
|
+
token = lookahead
|
874
|
+
|
875
|
+
if token.symbol == T_SPACE
|
876
|
+
threads = []
|
877
|
+
|
878
|
+
while true
|
879
|
+
shift_token
|
880
|
+
token = lookahead
|
881
|
+
|
882
|
+
case token.symbol
|
883
|
+
when T_LPAR
|
884
|
+
threads << thread_branch(token)
|
885
|
+
when T_CRLF
|
886
|
+
break
|
887
|
+
end
|
888
|
+
end
|
889
|
+
else
|
890
|
+
# no member
|
891
|
+
threads = []
|
892
|
+
end
|
893
|
+
|
894
|
+
return UntaggedResponse.new(name, threads, @str)
|
895
|
+
end
|
896
|
+
|
897
|
+
def thread_branch(token)
|
898
|
+
rootmember = nil
|
899
|
+
lastmember = nil
|
900
|
+
|
901
|
+
while true
|
902
|
+
shift_token # ignore first T_LPAR
|
903
|
+
token = lookahead
|
904
|
+
|
905
|
+
case token.symbol
|
906
|
+
when T_NUMBER
|
907
|
+
# new member
|
908
|
+
newmember = ThreadMember.new(number, [])
|
909
|
+
if rootmember.nil?
|
910
|
+
rootmember = newmember
|
911
|
+
else
|
912
|
+
lastmember.children << newmember
|
913
|
+
end
|
914
|
+
lastmember = newmember
|
915
|
+
when T_SPACE
|
916
|
+
# do nothing
|
917
|
+
when T_LPAR
|
918
|
+
if rootmember.nil?
|
919
|
+
# dummy member
|
920
|
+
lastmember = rootmember = ThreadMember.new(nil, [])
|
921
|
+
end
|
922
|
+
|
923
|
+
lastmember.children << thread_branch(token)
|
924
|
+
when T_RPAR
|
925
|
+
break
|
926
|
+
end
|
927
|
+
end
|
928
|
+
|
929
|
+
return rootmember
|
930
|
+
end
|
931
|
+
|
932
|
+
def status_response
|
933
|
+
token = match(T_ATOM)
|
934
|
+
name = token.value.upcase
|
935
|
+
match(T_SPACE)
|
936
|
+
mailbox = astring
|
937
|
+
match(T_SPACE)
|
938
|
+
match(T_LPAR)
|
939
|
+
attr = {}
|
940
|
+
while true
|
941
|
+
token = lookahead
|
942
|
+
case token.symbol
|
943
|
+
when T_RPAR
|
944
|
+
shift_token
|
945
|
+
break
|
946
|
+
when T_SPACE
|
947
|
+
shift_token
|
948
|
+
end
|
949
|
+
token = match(T_ATOM)
|
950
|
+
key = token.value.upcase
|
951
|
+
match(T_SPACE)
|
952
|
+
val = number
|
953
|
+
attr[key] = val
|
954
|
+
end
|
955
|
+
data = StatusData.new(mailbox, attr)
|
956
|
+
return UntaggedResponse.new(name, data, @str)
|
957
|
+
end
|
958
|
+
|
959
|
+
def capability_response
|
960
|
+
token = match(T_ATOM)
|
961
|
+
name = token.value.upcase
|
962
|
+
match(T_SPACE)
|
963
|
+
UntaggedResponse.new(name, capability_data, @str)
|
964
|
+
end
|
965
|
+
|
966
|
+
def capability_data
|
967
|
+
data = []
|
968
|
+
while true
|
969
|
+
token = lookahead
|
970
|
+
case token.symbol
|
971
|
+
when T_CRLF, T_RBRA
|
972
|
+
break
|
973
|
+
when T_SPACE
|
974
|
+
shift_token
|
975
|
+
next
|
976
|
+
end
|
977
|
+
data.push(atom.upcase)
|
978
|
+
end
|
979
|
+
data
|
980
|
+
end
|
981
|
+
|
982
|
+
def id_response
|
983
|
+
token = match(T_ATOM)
|
984
|
+
name = token.value.upcase
|
985
|
+
match(T_SPACE)
|
986
|
+
token = match(T_LPAR, T_NIL)
|
987
|
+
if token.symbol == T_NIL
|
988
|
+
return UntaggedResponse.new(name, nil, @str)
|
989
|
+
else
|
990
|
+
data = {}
|
991
|
+
while true
|
992
|
+
token = lookahead
|
993
|
+
case token.symbol
|
994
|
+
when T_RPAR
|
995
|
+
shift_token
|
996
|
+
break
|
997
|
+
when T_SPACE
|
998
|
+
shift_token
|
999
|
+
next
|
1000
|
+
else
|
1001
|
+
key = string
|
1002
|
+
match(T_SPACE)
|
1003
|
+
val = nstring
|
1004
|
+
data[key] = val
|
1005
|
+
end
|
1006
|
+
end
|
1007
|
+
return UntaggedResponse.new(name, data, @str)
|
1008
|
+
end
|
1009
|
+
end
|
1010
|
+
|
1011
|
+
def namespace_response
|
1012
|
+
@lex_state = EXPR_DATA
|
1013
|
+
token = lookahead
|
1014
|
+
token = match(T_ATOM)
|
1015
|
+
name = token.value.upcase
|
1016
|
+
match(T_SPACE)
|
1017
|
+
personal = namespaces
|
1018
|
+
match(T_SPACE)
|
1019
|
+
other = namespaces
|
1020
|
+
match(T_SPACE)
|
1021
|
+
shared = namespaces
|
1022
|
+
@lex_state = EXPR_BEG
|
1023
|
+
data = Namespaces.new(personal, other, shared)
|
1024
|
+
return UntaggedResponse.new(name, data, @str)
|
1025
|
+
end
|
1026
|
+
|
1027
|
+
def namespaces
|
1028
|
+
token = lookahead
|
1029
|
+
# empty () is not allowed, so nil is functionally identical to empty.
|
1030
|
+
data = []
|
1031
|
+
if token.symbol == T_NIL
|
1032
|
+
shift_token
|
1033
|
+
else
|
1034
|
+
match(T_LPAR)
|
1035
|
+
loop do
|
1036
|
+
data << namespace
|
1037
|
+
break unless lookahead.symbol == T_SPACE
|
1038
|
+
shift_token
|
1039
|
+
end
|
1040
|
+
match(T_RPAR)
|
1041
|
+
end
|
1042
|
+
data
|
1043
|
+
end
|
1044
|
+
|
1045
|
+
def namespace
|
1046
|
+
match(T_LPAR)
|
1047
|
+
prefix = match(T_QUOTED, T_LITERAL).value
|
1048
|
+
match(T_SPACE)
|
1049
|
+
delimiter = string
|
1050
|
+
extensions = namespace_response_extensions
|
1051
|
+
match(T_RPAR)
|
1052
|
+
Namespace.new(prefix, delimiter, extensions)
|
1053
|
+
end
|
1054
|
+
|
1055
|
+
def namespace_response_extensions
|
1056
|
+
data = {}
|
1057
|
+
token = lookahead
|
1058
|
+
if token.symbol == T_SPACE
|
1059
|
+
shift_token
|
1060
|
+
name = match(T_QUOTED, T_LITERAL).value
|
1061
|
+
data[name] ||= []
|
1062
|
+
match(T_SPACE)
|
1063
|
+
match(T_LPAR)
|
1064
|
+
loop do
|
1065
|
+
data[name].push match(T_QUOTED, T_LITERAL).value
|
1066
|
+
break unless lookahead.symbol == T_SPACE
|
1067
|
+
shift_token
|
1068
|
+
end
|
1069
|
+
match(T_RPAR)
|
1070
|
+
end
|
1071
|
+
data
|
1072
|
+
end
|
1073
|
+
|
1074
|
+
# text = 1*TEXT-CHAR
|
1075
|
+
# TEXT-CHAR = <any CHAR except CR and LF>
|
1076
|
+
def text
|
1077
|
+
match(T_TEXT, lex_state: EXPR_TEXT).value
|
1078
|
+
end
|
1079
|
+
|
1080
|
+
# resp-text = ["[" resp-text-code "]" SP] text
|
1081
|
+
def resp_text
|
1082
|
+
token = match(T_LBRA, T_TEXT, lex_state: EXPR_RTEXT)
|
1083
|
+
case token.symbol
|
1084
|
+
when T_LBRA
|
1085
|
+
code = resp_text_code
|
1086
|
+
match(T_RBRA)
|
1087
|
+
accept_space # violating RFC
|
1088
|
+
ResponseText.new(code, text)
|
1089
|
+
when T_TEXT
|
1090
|
+
ResponseText.new(nil, token.value)
|
1091
|
+
end
|
1092
|
+
end
|
1093
|
+
|
1094
|
+
# See https://www.rfc-editor.org/errata/rfc3501
|
1095
|
+
#
|
1096
|
+
# resp-text-code = "ALERT" /
|
1097
|
+
# "BADCHARSET" [SP "(" charset *(SP charset) ")" ] /
|
1098
|
+
# capability-data / "PARSE" /
|
1099
|
+
# "PERMANENTFLAGS" SP "("
|
1100
|
+
# [flag-perm *(SP flag-perm)] ")" /
|
1101
|
+
# "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
|
1102
|
+
# "UIDNEXT" SP nz-number / "UIDVALIDITY" SP nz-number /
|
1103
|
+
# "UNSEEN" SP nz-number /
|
1104
|
+
# atom [SP 1*<any TEXT-CHAR except "]">]
|
1105
|
+
def resp_text_code
|
1106
|
+
token = match(T_ATOM)
|
1107
|
+
name = token.value.upcase
|
1108
|
+
case name
|
1109
|
+
when /\A(?:ALERT|PARSE|READ-ONLY|READ-WRITE|TRYCREATE|NOMODSEQ)\z/n
|
1110
|
+
result = ResponseCode.new(name, nil)
|
1111
|
+
when /\A(?:BADCHARSET)\z/n
|
1112
|
+
result = ResponseCode.new(name, charset_list)
|
1113
|
+
when /\A(?:CAPABILITY)\z/ni
|
1114
|
+
result = ResponseCode.new(name, capability_data)
|
1115
|
+
when /\A(?:PERMANENTFLAGS)\z/n
|
1116
|
+
match(T_SPACE)
|
1117
|
+
result = ResponseCode.new(name, flag_list)
|
1118
|
+
when /\A(?:UIDVALIDITY|UIDNEXT|UNSEEN)\z/n
|
1119
|
+
match(T_SPACE)
|
1120
|
+
result = ResponseCode.new(name, number)
|
1121
|
+
else
|
1122
|
+
token = lookahead
|
1123
|
+
if token.symbol == T_SPACE
|
1124
|
+
shift_token
|
1125
|
+
token = match(T_TEXT, lex_state: EXPR_CTEXT)
|
1126
|
+
result = ResponseCode.new(name, token.value)
|
1127
|
+
else
|
1128
|
+
result = ResponseCode.new(name, nil)
|
1129
|
+
end
|
1130
|
+
end
|
1131
|
+
return result
|
1132
|
+
end
|
1133
|
+
|
1134
|
+
def charset_list
|
1135
|
+
result = []
|
1136
|
+
if accept(T_SPACE)
|
1137
|
+
match(T_LPAR)
|
1138
|
+
result << charset
|
1139
|
+
while accept(T_SPACE)
|
1140
|
+
result << charset
|
1141
|
+
end
|
1142
|
+
match(T_RPAR)
|
1143
|
+
end
|
1144
|
+
result
|
1145
|
+
end
|
1146
|
+
|
1147
|
+
def address_list
|
1148
|
+
token = lookahead
|
1149
|
+
if token.symbol == T_NIL
|
1150
|
+
shift_token
|
1151
|
+
return nil
|
1152
|
+
else
|
1153
|
+
result = []
|
1154
|
+
match(T_LPAR)
|
1155
|
+
while true
|
1156
|
+
token = lookahead
|
1157
|
+
case token.symbol
|
1158
|
+
when T_RPAR
|
1159
|
+
shift_token
|
1160
|
+
break
|
1161
|
+
when T_SPACE
|
1162
|
+
shift_token
|
1163
|
+
end
|
1164
|
+
result.push(address)
|
1165
|
+
end
|
1166
|
+
return result
|
1167
|
+
end
|
1168
|
+
end
|
1169
|
+
|
1170
|
+
ADDRESS_REGEXP = /\G\
|
1171
|
+
(?# 1: NAME )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
|
1172
|
+
(?# 2: ROUTE )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
|
1173
|
+
(?# 3: MAILBOX )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
|
1174
|
+
(?# 4: HOST )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)")\
|
1175
|
+
\)/ni
|
1176
|
+
|
1177
|
+
def address
|
1178
|
+
match(T_LPAR)
|
1179
|
+
if @str.index(ADDRESS_REGEXP, @pos)
|
1180
|
+
# address does not include literal.
|
1181
|
+
@pos = $~.end(0)
|
1182
|
+
name = $1
|
1183
|
+
route = $2
|
1184
|
+
mailbox = $3
|
1185
|
+
host = $4
|
1186
|
+
for s in [name, route, mailbox, host]
|
1187
|
+
if s
|
1188
|
+
s.gsub!(/\\(["\\])/n, "\\1")
|
1189
|
+
end
|
1190
|
+
end
|
1191
|
+
else
|
1192
|
+
name = nstring
|
1193
|
+
match(T_SPACE)
|
1194
|
+
route = nstring
|
1195
|
+
match(T_SPACE)
|
1196
|
+
mailbox = nstring
|
1197
|
+
match(T_SPACE)
|
1198
|
+
host = nstring
|
1199
|
+
match(T_RPAR)
|
1200
|
+
end
|
1201
|
+
return Address.new(name, route, mailbox, host)
|
1202
|
+
end
|
1203
|
+
|
1204
|
+
FLAG_REGEXP = /\
|
1205
|
+
(?# FLAG )\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)|\
|
1206
|
+
(?# ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\]+)/n
|
1207
|
+
|
1208
|
+
def flag_list
|
1209
|
+
if @str.index(/\(([^)]*)\)/ni, @pos)
|
1210
|
+
@pos = $~.end(0)
|
1211
|
+
return $1.scan(FLAG_REGEXP).collect { |flag, atom|
|
1212
|
+
if atom
|
1213
|
+
atom
|
1214
|
+
else
|
1215
|
+
symbol = flag.capitalize.intern
|
1216
|
+
@flag_symbols[symbol] = true
|
1217
|
+
if @flag_symbols.length > IMAP.max_flag_count
|
1218
|
+
raise FlagCountError, "number of flag symbols exceeded"
|
1219
|
+
end
|
1220
|
+
symbol
|
1221
|
+
end
|
1222
|
+
}
|
1223
|
+
else
|
1224
|
+
parse_error("invalid flag list")
|
1225
|
+
end
|
1226
|
+
end
|
1227
|
+
|
1228
|
+
def nstring
|
1229
|
+
token = lookahead
|
1230
|
+
if token.symbol == T_NIL
|
1231
|
+
shift_token
|
1232
|
+
return nil
|
1233
|
+
else
|
1234
|
+
return string
|
1235
|
+
end
|
1236
|
+
end
|
1237
|
+
|
1238
|
+
def astring
|
1239
|
+
token = lookahead
|
1240
|
+
if string_token?(token)
|
1241
|
+
return string
|
1242
|
+
else
|
1243
|
+
return astring_chars
|
1244
|
+
end
|
1245
|
+
end
|
1246
|
+
|
1247
|
+
def string
|
1248
|
+
token = lookahead
|
1249
|
+
if token.symbol == T_NIL
|
1250
|
+
shift_token
|
1251
|
+
return nil
|
1252
|
+
end
|
1253
|
+
token = match(T_QUOTED, T_LITERAL)
|
1254
|
+
return token.value
|
1255
|
+
end
|
1256
|
+
|
1257
|
+
STRING_TOKENS = [T_QUOTED, T_LITERAL, T_NIL]
|
1258
|
+
|
1259
|
+
def string_token?(token)
|
1260
|
+
return STRING_TOKENS.include?(token.symbol)
|
1261
|
+
end
|
1262
|
+
|
1263
|
+
def case_insensitive_string
|
1264
|
+
token = lookahead
|
1265
|
+
if token.symbol == T_NIL
|
1266
|
+
shift_token
|
1267
|
+
return nil
|
1268
|
+
end
|
1269
|
+
token = match(T_QUOTED, T_LITERAL)
|
1270
|
+
return token.value.upcase
|
1271
|
+
end
|
1272
|
+
|
1273
|
+
# atom = 1*ATOM-CHAR
|
1274
|
+
# ATOM-CHAR = <any CHAR except atom-specials>
|
1275
|
+
ATOM_TOKENS = [
|
1276
|
+
T_ATOM,
|
1277
|
+
T_NUMBER,
|
1278
|
+
T_NIL,
|
1279
|
+
T_LBRA,
|
1280
|
+
T_PLUS
|
1281
|
+
]
|
1282
|
+
|
1283
|
+
def atom
|
1284
|
+
-combine_adjacent(*ATOM_TOKENS)
|
1285
|
+
end
|
1286
|
+
|
1287
|
+
# ASTRING-CHAR = ATOM-CHAR / resp-specials
|
1288
|
+
# resp-specials = "]"
|
1289
|
+
ASTRING_CHARS_TOKENS = [*ATOM_TOKENS, T_RBRA]
|
1290
|
+
|
1291
|
+
def astring_chars
|
1292
|
+
combine_adjacent(*ASTRING_CHARS_TOKENS)
|
1293
|
+
end
|
1294
|
+
|
1295
|
+
def combine_adjacent(*tokens)
|
1296
|
+
result = "".b
|
1297
|
+
while token = accept(*tokens)
|
1298
|
+
result << token.value
|
1299
|
+
end
|
1300
|
+
if result.empty?
|
1301
|
+
parse_error('unexpected token %s (expected %s)',
|
1302
|
+
lookahead.symbol, args.join(" or "))
|
1303
|
+
end
|
1304
|
+
result
|
1305
|
+
end
|
1306
|
+
|
1307
|
+
# See https://www.rfc-editor.org/errata/rfc3501
|
1308
|
+
#
|
1309
|
+
# charset = atom / quoted
|
1310
|
+
def charset
|
1311
|
+
if token = accept(T_QUOTED)
|
1312
|
+
token.value
|
1313
|
+
else
|
1314
|
+
atom
|
1315
|
+
end
|
1316
|
+
end
|
1317
|
+
|
1318
|
+
def number
|
1319
|
+
token = lookahead
|
1320
|
+
if token.symbol == T_NIL
|
1321
|
+
shift_token
|
1322
|
+
return nil
|
1323
|
+
end
|
1324
|
+
token = match(T_NUMBER)
|
1325
|
+
return token.value.to_i
|
1326
|
+
end
|
1327
|
+
|
1328
|
+
def nil_atom
|
1329
|
+
match(T_NIL)
|
1330
|
+
return nil
|
1331
|
+
end
|
1332
|
+
|
1333
|
+
SPACES_REGEXP = /\G */n
|
1334
|
+
|
1335
|
+
# This advances @pos directly so it's safe before changing @lex_state.
|
1336
|
+
def accept_space
|
1337
|
+
if @token
|
1338
|
+
shift_token if @token.symbol == T_SPACE
|
1339
|
+
elsif @str[@pos] == " "
|
1340
|
+
@pos += 1
|
1341
|
+
end
|
1342
|
+
end
|
1343
|
+
|
1344
|
+
# The RFC is very strict about this and usually we should be too.
|
1345
|
+
# But skipping spaces is usually a safe workaround for buggy servers.
|
1346
|
+
#
|
1347
|
+
# This advances @pos directly so it's safe before changing @lex_state.
|
1348
|
+
def accept_spaces
|
1349
|
+
shift_token if @token&.symbol == T_SPACE
|
1350
|
+
if @str.index(SPACES_REGEXP, @pos)
|
1351
|
+
@pos = $~.end(0)
|
1352
|
+
end
|
1353
|
+
end
|
1354
|
+
|
1355
|
+
def match(*args, lex_state: @lex_state)
|
1356
|
+
if @token && lex_state != @lex_state
|
1357
|
+
parse_error("invalid lex_state change to %s with unconsumed token",
|
1358
|
+
lex_state)
|
1359
|
+
end
|
1360
|
+
begin
|
1361
|
+
@lex_state, original_lex_state = lex_state, @lex_state
|
1362
|
+
token = lookahead
|
1363
|
+
unless args.include?(token.symbol)
|
1364
|
+
parse_error('unexpected token %s (expected %s)',
|
1365
|
+
token.symbol.id2name,
|
1366
|
+
args.collect {|i| i.id2name}.join(" or "))
|
1367
|
+
end
|
1368
|
+
shift_token
|
1369
|
+
return token
|
1370
|
+
ensure
|
1371
|
+
@lex_state = original_lex_state
|
1372
|
+
end
|
1373
|
+
end
|
1374
|
+
|
1375
|
+
# like match, but does not raise error on failure.
|
1376
|
+
#
|
1377
|
+
# returns and shifts token on successful match
|
1378
|
+
# returns nil and leaves @token unshifted on no match
|
1379
|
+
def accept(*args)
|
1380
|
+
token = lookahead
|
1381
|
+
if args.include?(token.symbol)
|
1382
|
+
shift_token
|
1383
|
+
token
|
1384
|
+
end
|
1385
|
+
end
|
1386
|
+
|
1387
|
+
def lookahead
|
1388
|
+
@token ||= next_token
|
1389
|
+
end
|
1390
|
+
|
1391
|
+
def shift_token
|
1392
|
+
@token = nil
|
1393
|
+
end
|
1394
|
+
|
1395
|
+
def next_token
|
1396
|
+
case @lex_state
|
1397
|
+
when EXPR_BEG
|
1398
|
+
if @str.index(BEG_REGEXP, @pos)
|
1399
|
+
@pos = $~.end(0)
|
1400
|
+
if $1
|
1401
|
+
return Token.new(T_SPACE, $+)
|
1402
|
+
elsif $2
|
1403
|
+
return Token.new(T_NIL, $+)
|
1404
|
+
elsif $3
|
1405
|
+
return Token.new(T_NUMBER, $+)
|
1406
|
+
elsif $4
|
1407
|
+
return Token.new(T_ATOM, $+)
|
1408
|
+
elsif $5
|
1409
|
+
return Token.new(T_QUOTED,
|
1410
|
+
$+.gsub(/\\(["\\])/n, "\\1"))
|
1411
|
+
elsif $6
|
1412
|
+
return Token.new(T_LPAR, $+)
|
1413
|
+
elsif $7
|
1414
|
+
return Token.new(T_RPAR, $+)
|
1415
|
+
elsif $8
|
1416
|
+
return Token.new(T_BSLASH, $+)
|
1417
|
+
elsif $9
|
1418
|
+
return Token.new(T_STAR, $+)
|
1419
|
+
elsif $10
|
1420
|
+
return Token.new(T_LBRA, $+)
|
1421
|
+
elsif $11
|
1422
|
+
return Token.new(T_RBRA, $+)
|
1423
|
+
elsif $12
|
1424
|
+
len = $+.to_i
|
1425
|
+
val = @str[@pos, len]
|
1426
|
+
@pos += len
|
1427
|
+
return Token.new(T_LITERAL, val)
|
1428
|
+
elsif $13
|
1429
|
+
return Token.new(T_PLUS, $+)
|
1430
|
+
elsif $14
|
1431
|
+
return Token.new(T_PERCENT, $+)
|
1432
|
+
elsif $15
|
1433
|
+
return Token.new(T_CRLF, $+)
|
1434
|
+
elsif $16
|
1435
|
+
return Token.new(T_EOF, $+)
|
1436
|
+
else
|
1437
|
+
parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid")
|
1438
|
+
end
|
1439
|
+
else
|
1440
|
+
@str.index(/\S*/n, @pos)
|
1441
|
+
parse_error("unknown token - %s", $&.dump)
|
1442
|
+
end
|
1443
|
+
when EXPR_DATA
|
1444
|
+
if @str.index(DATA_REGEXP, @pos)
|
1445
|
+
@pos = $~.end(0)
|
1446
|
+
if $1
|
1447
|
+
return Token.new(T_SPACE, $+)
|
1448
|
+
elsif $2
|
1449
|
+
return Token.new(T_NIL, $+)
|
1450
|
+
elsif $3
|
1451
|
+
return Token.new(T_NUMBER, $+)
|
1452
|
+
elsif $4
|
1453
|
+
return Token.new(T_QUOTED,
|
1454
|
+
$+.gsub(/\\(["\\])/n, "\\1"))
|
1455
|
+
elsif $5
|
1456
|
+
len = $+.to_i
|
1457
|
+
val = @str[@pos, len]
|
1458
|
+
@pos += len
|
1459
|
+
return Token.new(T_LITERAL, val)
|
1460
|
+
elsif $6
|
1461
|
+
return Token.new(T_LPAR, $+)
|
1462
|
+
elsif $7
|
1463
|
+
return Token.new(T_RPAR, $+)
|
1464
|
+
else
|
1465
|
+
parse_error("[Net::IMAP BUG] DATA_REGEXP is invalid")
|
1466
|
+
end
|
1467
|
+
else
|
1468
|
+
@str.index(/\S*/n, @pos)
|
1469
|
+
parse_error("unknown token - %s", $&.dump)
|
1470
|
+
end
|
1471
|
+
when EXPR_TEXT
|
1472
|
+
if @str.index(TEXT_REGEXP, @pos)
|
1473
|
+
@pos = $~.end(0)
|
1474
|
+
if $1
|
1475
|
+
return Token.new(T_TEXT, $+)
|
1476
|
+
else
|
1477
|
+
parse_error("[Net::IMAP BUG] TEXT_REGEXP is invalid")
|
1478
|
+
end
|
1479
|
+
else
|
1480
|
+
@str.index(/\S*/n, @pos)
|
1481
|
+
parse_error("unknown token - %s", $&.dump)
|
1482
|
+
end
|
1483
|
+
when EXPR_RTEXT
|
1484
|
+
if @str.index(RTEXT_REGEXP, @pos)
|
1485
|
+
@pos = $~.end(0)
|
1486
|
+
if $1
|
1487
|
+
return Token.new(T_LBRA, $+)
|
1488
|
+
elsif $2
|
1489
|
+
return Token.new(T_TEXT, $+)
|
1490
|
+
else
|
1491
|
+
parse_error("[Net::IMAP BUG] RTEXT_REGEXP is invalid")
|
1492
|
+
end
|
1493
|
+
else
|
1494
|
+
@str.index(/\S*/n, @pos)
|
1495
|
+
parse_error("unknown token - %s", $&.dump)
|
1496
|
+
end
|
1497
|
+
when EXPR_CTEXT
|
1498
|
+
if @str.index(CTEXT_REGEXP, @pos)
|
1499
|
+
@pos = $~.end(0)
|
1500
|
+
if $1
|
1501
|
+
return Token.new(T_TEXT, $+)
|
1502
|
+
else
|
1503
|
+
parse_error("[Net::IMAP BUG] CTEXT_REGEXP is invalid")
|
1504
|
+
end
|
1505
|
+
else
|
1506
|
+
@str.index(/\S*/n, @pos) #/
|
1507
|
+
parse_error("unknown token - %s", $&.dump)
|
1508
|
+
end
|
1509
|
+
else
|
1510
|
+
parse_error("invalid @lex_state - %s", @lex_state.inspect)
|
1511
|
+
end
|
1512
|
+
end
|
1513
|
+
|
1514
|
+
def parse_error(fmt, *args)
|
1515
|
+
if IMAP.debug
|
1516
|
+
$stderr.printf("@str: %s\n", @str.dump)
|
1517
|
+
$stderr.printf("@pos: %d\n", @pos)
|
1518
|
+
$stderr.printf("@lex_state: %s\n", @lex_state)
|
1519
|
+
if @token
|
1520
|
+
$stderr.printf("@token.symbol: %s\n", @token.symbol)
|
1521
|
+
$stderr.printf("@token.value: %s\n", @token.value.inspect)
|
1522
|
+
end
|
1523
|
+
end
|
1524
|
+
raise ResponseParseError, format(fmt, *args)
|
1525
|
+
end
|
1526
|
+
end
|
1527
|
+
|
1528
|
+
end
|
1529
|
+
|
1530
|
+
end
|