net-imap 0.4.2 → 0.4.3
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/.gitignore +1 -0
- data/Gemfile +2 -0
- data/lib/net/imap/errors.rb +20 -0
- data/lib/net/imap/response_data.rb +46 -6
- data/lib/net/imap/response_parser/parser_utils.rb +10 -0
- data/lib/net/imap/response_parser.rb +608 -352
- data/lib/net/imap/sasl/authenticators.rb +2 -2
- data/lib/net/imap/sasl/xoauth2_authenticator.rb +1 -1
- data/lib/net/imap.rb +22 -24
- data/net-imap.gemspec +3 -2
- data/rakelib/benchmarks.rake +98 -0
- metadata +3 -6
- data/benchmarks/generate_parser_benchmarks +0 -52
- data/benchmarks/parser.yml +0 -578
- data/benchmarks/stringprep.yml +0 -65
- data/benchmarks/table-regexps.yml +0 -39
@@ -58,6 +58,21 @@ module Net
|
|
58
58
|
T_TEXT = :TEXT # any char except CRLF
|
59
59
|
T_EOF = :EOF # end of response string
|
60
60
|
|
61
|
+
module ResponseConditions
|
62
|
+
OK = "OK"
|
63
|
+
NO = "NO"
|
64
|
+
BAD = "BAD"
|
65
|
+
BYE = "BYE"
|
66
|
+
PREAUTH = "PREAUTH"
|
67
|
+
|
68
|
+
RESP_COND_STATES = [OK, NO, BAD ].freeze
|
69
|
+
RESP_DATA_CONDS = [OK, NO, BAD, BYE, ].freeze
|
70
|
+
AUTH_CONDS = [OK, PREAUTH].freeze
|
71
|
+
GREETING_CONDS = [OK, BYE, PREAUTH].freeze
|
72
|
+
RESP_CONDS = [OK, NO, BAD, BYE, PREAUTH].freeze
|
73
|
+
end
|
74
|
+
include ResponseConditions
|
75
|
+
|
61
76
|
module Patterns
|
62
77
|
|
63
78
|
module CharClassSubtraction
|
@@ -170,6 +185,54 @@ module Net
|
|
170
185
|
CODE_TEXT_CHAR = TEXT_CHAR - RESP_SPECIALS
|
171
186
|
CODE_TEXT = /#{CODE_TEXT_CHAR}+/n
|
172
187
|
|
188
|
+
# flag = "\Answered" / "\Flagged" / "\Deleted" /
|
189
|
+
# "\Seen" / "\Draft" / flag-keyword / flag-extension
|
190
|
+
# ; Does not include "\Recent"
|
191
|
+
# flag-extension = "\" atom
|
192
|
+
# ; Future expansion. Client implementations
|
193
|
+
# ; MUST accept flag-extension flags. Server
|
194
|
+
# ; implementations MUST NOT generate
|
195
|
+
# ; flag-extension flags except as defined by
|
196
|
+
# ; a future Standard or Standards Track
|
197
|
+
# ; revisions of this specification.
|
198
|
+
# flag-keyword = "$MDNSent" / "$Forwarded" / "$Junk" /
|
199
|
+
# "$NotJunk" / "$Phishing" / atom
|
200
|
+
# flag-perm = flag / "\*"
|
201
|
+
#
|
202
|
+
# Not checking for max one mbx-list-sflag in the parser.
|
203
|
+
# >>>
|
204
|
+
# mbx-list-oflag = "\Noinferiors" / child-mbox-flag /
|
205
|
+
# "\Subscribed" / "\Remote" / flag-extension
|
206
|
+
# ; Other flags; multiple from this list are
|
207
|
+
# ; possible per LIST response, but each flag
|
208
|
+
# ; can only appear once per LIST response
|
209
|
+
# mbx-list-sflag = "\NonExistent" / "\Noselect" / "\Marked" /
|
210
|
+
# "\Unmarked"
|
211
|
+
# ; Selectability flags; only one per LIST response
|
212
|
+
# child-mbox-flag = "\HasChildren" / "\HasNoChildren"
|
213
|
+
# ; attributes for the CHILDREN return option, at most
|
214
|
+
# ; one possible per LIST response
|
215
|
+
FLAG = /\\?#{ATOM}/n
|
216
|
+
FLAG_EXTENSION = /\\#{ATOM}/n
|
217
|
+
FLAG_KEYWORD = ATOM
|
218
|
+
FLAG_PERM = Regexp.union(FLAG, "\\*")
|
219
|
+
MBX_FLAG = FLAG_EXTENSION
|
220
|
+
|
221
|
+
# flag-list = "(" [flag *(SP flag)] ")"
|
222
|
+
#
|
223
|
+
# part of resp-text-code:
|
224
|
+
# >>>
|
225
|
+
# "PERMANENTFLAGS" SP "(" [flag-perm *(SP flag-perm)] ")"
|
226
|
+
#
|
227
|
+
# parens from mailbox-list are included in the regexp:
|
228
|
+
# >>>
|
229
|
+
# mbx-list-flags = *(mbx-list-oflag SP) mbx-list-sflag
|
230
|
+
# *(SP mbx-list-oflag) /
|
231
|
+
# mbx-list-oflag *(SP mbx-list-oflag)
|
232
|
+
FLAG_LIST = /\G\((#{FLAG }(?:#{SP}#{FLAG })*|)\)/ni
|
233
|
+
FLAG_PERM_LIST = /\G\((#{FLAG_PERM}(?:#{SP}#{FLAG_PERM})*|)\)/ni
|
234
|
+
MBX_LIST_FLAGS = /\G\((#{MBX_FLAG }(?:#{SP}#{MBX_FLAG })*|)\)/ni
|
235
|
+
|
173
236
|
# RFC3501:
|
174
237
|
# QUOTED-CHAR = <any TEXT-CHAR except quoted-specials> /
|
175
238
|
# "\" quoted-specials
|
@@ -195,6 +258,14 @@ module Net
|
|
195
258
|
TEXT_rev1 = /#{TEXT_CHAR}+/
|
196
259
|
TEXT_rev2 = /#{Regexp.union TEXT_CHAR, UTF8_2, UTF8_3, UTF8_4}+/
|
197
260
|
|
261
|
+
# tagged-label-fchar = ALPHA / "-" / "_" / "."
|
262
|
+
TAGGED_LABEL_FCHAR = /[a-zA-Z\-_.]/n
|
263
|
+
# tagged-label-char = tagged-label-fchar / DIGIT / ":"
|
264
|
+
TAGGED_LABEL_CHAR = /[a-zA-Z\-_.0-9:]*/n
|
265
|
+
# tagged-ext-label = tagged-label-fchar *tagged-label-char
|
266
|
+
# ; Is a valid RFC 3501 "atom".
|
267
|
+
TAGGED_EXT_LABEL = /#{TAGGED_LABEL_FCHAR}#{TAGGED_LABEL_CHAR}*/n
|
268
|
+
|
198
269
|
# RFC3501:
|
199
270
|
# literal = "{" number "}" CRLF *CHAR8
|
200
271
|
# ; Number represents the number of CHAR8s
|
@@ -268,6 +339,8 @@ module Net
|
|
268
339
|
Token = Struct.new(:symbol, :value)
|
269
340
|
|
270
341
|
def_char_matchers :SP, " ", :T_SPACE
|
342
|
+
def_char_matchers :PLUS, "+", :T_PLUS
|
343
|
+
def_char_matchers :STAR, "*", :T_STAR
|
271
344
|
|
272
345
|
def_char_matchers :lpar, "(", :T_LPAR
|
273
346
|
def_char_matchers :rpar, ")", :T_RPAR
|
@@ -310,6 +383,9 @@ module Net
|
|
310
383
|
# TODO: add to lexer and only match tagged-ext-label
|
311
384
|
def_token_matchers :tagged_ext_label, T_ATOM, T_NIL, send: :upcase
|
312
385
|
|
386
|
+
def_token_matchers :CRLF, T_CRLF
|
387
|
+
def_token_matchers :EOF, T_EOF
|
388
|
+
|
313
389
|
# atom = 1*ATOM-CHAR
|
314
390
|
# ATOM-CHAR = <any CHAR except atom-specials>
|
315
391
|
ATOM_TOKENS = [T_ATOM, T_NUMBER, T_NIL, T_LBRA, T_PLUS]
|
@@ -320,10 +396,13 @@ module Net
|
|
320
396
|
|
321
397
|
ASTRING_TOKENS = [T_QUOTED, *ASTRING_CHARS_TOKENS, T_LITERAL].freeze
|
322
398
|
|
323
|
-
#
|
324
|
-
|
325
|
-
|
326
|
-
|
399
|
+
# tag = 1*<any ASTRING-CHAR except "+">
|
400
|
+
TAG_TOKENS = (ASTRING_CHARS_TOKENS - [T_PLUS]).freeze
|
401
|
+
|
402
|
+
# TODO: handle atom, astring_chars, and tag entirely inside the lexer
|
403
|
+
def atom; combine_adjacent(*ATOM_TOKENS) end
|
404
|
+
def astring_chars; combine_adjacent(*ASTRING_CHARS_TOKENS) end
|
405
|
+
def tag; combine_adjacent(*TAG_TOKENS) end
|
327
406
|
|
328
407
|
# the #accept version of #atom
|
329
408
|
def atom?; -combine_adjacent(*ATOM_TOKENS) if lookahead?(*ATOM_TOKENS) end
|
@@ -336,11 +415,6 @@ module Net
|
|
336
415
|
-combine_adjacent(*ATOM_TOKENS).upcase if lookahead?(*ATOM_TOKENS)
|
337
416
|
end
|
338
417
|
|
339
|
-
# TODO: handle astring_chars entirely inside the lexer
|
340
|
-
def astring_chars
|
341
|
-
combine_adjacent(*ASTRING_CHARS_TOKENS)
|
342
|
-
end
|
343
|
-
|
344
418
|
# astring = 1*ASTRING-CHAR / string
|
345
419
|
def astring
|
346
420
|
lookahead?(*ASTRING_CHARS_TOKENS) ? astring_chars : string
|
@@ -357,6 +431,30 @@ module Net
|
|
357
431
|
parse_error("unexpected atom %p, expected %p instead", val, word)
|
358
432
|
end
|
359
433
|
|
434
|
+
# Use #label or #label_in to assert specific known labels
|
435
|
+
# (+tagged-ext-label+ only, not +atom+).
|
436
|
+
def label_in(*labels)
|
437
|
+
lbl = tagged_ext_label and labels.include?(lbl) and return lbl
|
438
|
+
parse_error("unexpected atom %p, expected one of %s instead",
|
439
|
+
lbl, labels.join(" or "))
|
440
|
+
end
|
441
|
+
|
442
|
+
# expects "OK" or "PREAUTH" and raises InvalidResponseError on failure
|
443
|
+
def resp_cond_auth__name
|
444
|
+
lbl = tagged_ext_label and AUTH_CONDS.include? lbl and return lbl
|
445
|
+
raise InvalidResponseError, "bad response type %p, expected %s" % [
|
446
|
+
lbl, AUTH_CONDS.join(" or ")
|
447
|
+
]
|
448
|
+
end
|
449
|
+
|
450
|
+
# expects "OK" or "NO" or "BAD" and raises InvalidResponseError on failure
|
451
|
+
def resp_cond_state__name
|
452
|
+
lbl = tagged_ext_label and RESP_COND_STATES.include? lbl and return lbl
|
453
|
+
raise InvalidResponseError, "bad response type %p, expected %s" % [
|
454
|
+
lbl, RESP_COND_STATES.join(" or ")
|
455
|
+
]
|
456
|
+
end
|
457
|
+
|
360
458
|
# nstring = string / nil
|
361
459
|
def nstring
|
362
460
|
NIL? ? nil : string
|
@@ -378,155 +476,295 @@ module Net
|
|
378
476
|
alias number64 number
|
379
477
|
alias number64? number?
|
380
478
|
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
shift_token
|
394
|
-
end
|
395
|
-
match(T_CRLF)
|
396
|
-
match(T_EOF)
|
397
|
-
return result
|
398
|
-
end
|
479
|
+
# valid number ranges are not enforced by parser
|
480
|
+
# nz-number = digit-nz *DIGIT
|
481
|
+
# ; Non-zero unsigned 32-bit integer
|
482
|
+
# ; (0 < n < 4,294,967,296)
|
483
|
+
alias nz_number number
|
484
|
+
alias nz_number? number?
|
485
|
+
|
486
|
+
# valid number ranges are not enforced by parser
|
487
|
+
# nz-number64 = digit-nz *DIGIT
|
488
|
+
# ; Unsigned 63-bit integer
|
489
|
+
# ; (0 < n <= 9,223,372,036,854,775,807)
|
490
|
+
alias nz_number64 nz_number
|
399
491
|
|
492
|
+
# valid number ranges are not enforced by parser
|
493
|
+
# uniqueid = nz-number
|
494
|
+
# ; Strictly ascending
|
495
|
+
alias uniqueid nz_number
|
496
|
+
|
497
|
+
# [RFC3501 & RFC9051:]
|
498
|
+
# response = *(continue-req / response-data) response-done
|
499
|
+
#
|
500
|
+
# For simplicity, response isn't interpreted as the combination of the
|
501
|
+
# three response types, but instead represents any individual server
|
502
|
+
# response. Our simplified interpretation is defined as:
|
503
|
+
# response = continue-req | response_data | response-tagged
|
504
|
+
#
|
505
|
+
# n.b: our "response-tagged" definition parses "greeting" too.
|
506
|
+
def response
|
507
|
+
resp = case lookahead!(T_PLUS, T_STAR, *TAG_TOKENS).symbol
|
508
|
+
when T_PLUS then continue_req
|
509
|
+
when T_STAR then response_data
|
510
|
+
else response_tagged
|
511
|
+
end
|
512
|
+
accept_spaces # QUIRKY: Ignore trailing space (MS Exchange Server?)
|
513
|
+
CRLF!
|
514
|
+
EOF!
|
515
|
+
resp
|
516
|
+
end
|
517
|
+
|
518
|
+
# RFC3501 & RFC9051:
|
519
|
+
# continue-req = "+" SP (resp-text / base64) CRLF
|
520
|
+
#
|
521
|
+
# n.b: base64 is valid resp-text. And in the spirit of RFC9051 Appx E 23
|
522
|
+
# (and to workaround existing servers), we use the following grammar:
|
523
|
+
#
|
524
|
+
# continue-req = "+" (SP (resp-text)) CRLF
|
400
525
|
def continue_req
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
526
|
+
PLUS!
|
527
|
+
ContinuationRequest.new(SP? ? resp_text : ResponseText::EMPTY, @str)
|
528
|
+
end
|
529
|
+
|
530
|
+
RE_RESPONSE_TYPE = /\G(?:\d+ )?(?<type>#{Patterns::TAGGED_EXT_LABEL})/n
|
531
|
+
|
532
|
+
# [RFC3501:]
|
533
|
+
# response-data = "*" SP (resp-cond-state / resp-cond-bye /
|
534
|
+
# mailbox-data / message-data / capability-data) CRLF
|
535
|
+
# [RFC4466:]
|
536
|
+
# response-data = "*" SP response-payload CRLF
|
537
|
+
# response-payload = resp-cond-state / resp-cond-bye /
|
538
|
+
# mailbox-data / message-data / capability-data
|
539
|
+
# RFC5161 (ENABLE capability):
|
540
|
+
# response-data =/ "*" SP enable-data CRLF
|
541
|
+
# RFC5255 (LANGUAGE capability)
|
542
|
+
# response-payload =/ language-data
|
543
|
+
# RFC5255 (I18NLEVEL=1 and I18NLEVEL=2 capabilities)
|
544
|
+
# response-payload =/ comparator-data
|
545
|
+
# [RFC9051:]
|
546
|
+
# response-data = "*" SP (resp-cond-state / resp-cond-bye /
|
547
|
+
# mailbox-data / message-data / capability-data /
|
548
|
+
# enable-data) CRLF
|
549
|
+
#
|
550
|
+
# [merging in greeting and response-fatal:]
|
551
|
+
# greeting = "*" SP (resp-cond-auth / resp-cond-bye) CRLF
|
552
|
+
# response-fatal = "*" SP resp-cond-bye CRLF
|
553
|
+
# response-data =/ "*" SP (resp-cond-auth / resp-cond-bye) CRLF
|
554
|
+
# [removing duplicates, this is simply]
|
555
|
+
# response-payload =/ resp-cond-auth
|
556
|
+
#
|
557
|
+
# TODO: remove resp-cond-auth and handle greeting separately
|
558
|
+
def response_data
|
559
|
+
STAR!; SP!
|
560
|
+
m = peek_re(RE_RESPONSE_TYPE) or parse_error("unparsable response")
|
561
|
+
case m["type"].upcase
|
562
|
+
when "OK" then resp_cond_state__untagged # RFC3501, RFC9051
|
563
|
+
when "FETCH" then message_data__fetch # RFC3501, RFC9051
|
564
|
+
when "EXPUNGE" then message_data__expunge # RFC3501, RFC9051
|
565
|
+
when "EXISTS" then mailbox_data__exists # RFC3501, RFC9051
|
566
|
+
when "ESEARCH" then esearch_response # RFC4731, RFC9051, etc
|
567
|
+
when "VANISHED" then expunged_resp # RFC7162
|
568
|
+
when "UIDFETCH" then uidfetch_resp # (draft) UIDONLY
|
569
|
+
when "SEARCH" then mailbox_data__search # RFC3501 (obsolete)
|
570
|
+
when "CAPABILITY" then capability_data__untagged # RFC3501, RFC9051
|
571
|
+
when "FLAGS" then mailbox_data__flags # RFC3501, RFC9051
|
572
|
+
when "LIST" then mailbox_data__list # RFC3501, RFC9051
|
573
|
+
when "STATUS" then mailbox_data__status # RFC3501, RFC9051
|
574
|
+
when "NAMESPACE" then namespace_response # RFC2342, RFC9051
|
575
|
+
when "ENABLED" then enable_data # RFC5161, RFC9051
|
576
|
+
when "BAD" then resp_cond_state__untagged # RFC3501, RFC9051
|
577
|
+
when "NO" then resp_cond_state__untagged # RFC3501, RFC9051
|
578
|
+
when "PREAUTH" then resp_cond_auth # RFC3501, RFC9051
|
579
|
+
when "BYE" then resp_cond_bye # RFC3501, RFC9051
|
580
|
+
when "RECENT" then mailbox_data__recent # RFC3501 (obsolete)
|
581
|
+
when "SORT" then sort_data # RFC5256, RFC7162
|
582
|
+
when "THREAD" then thread_data # RFC5256
|
583
|
+
when "QUOTA" then quota_response # RFC2087, RFC9208
|
584
|
+
when "QUOTAROOT" then quotaroot_response # RFC2087, RFC9208
|
585
|
+
when "ID" then id_response # RFC2971
|
586
|
+
when "ACL" then acl_data # RFC4314
|
587
|
+
when "LISTRIGHTS" then listrights_data # RFC4314
|
588
|
+
when "MYRIGHTS" then myrights_data # RFC4314
|
589
|
+
when "METADATA" then metadata_resp # RFC5464
|
590
|
+
when "LANGUAGE" then language_data # RFC5255
|
591
|
+
when "COMPARATOR" then comparator_data # RFC5255
|
592
|
+
when "CONVERTED" then message_data__converted # RFC5259
|
593
|
+
when "LSUB" then mailbox_data__lsub # RFC3501 (obsolete)
|
594
|
+
when "XLIST" then mailbox_data__xlist # deprecated
|
595
|
+
when "NOOP" then response_data__noop
|
596
|
+
else response_data__unhandled
|
408
597
|
end
|
409
598
|
end
|
410
599
|
|
411
|
-
def
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
when /\A(?:OK|NO|BAD|BYE|PREAUTH)\z/ni
|
420
|
-
return response_cond
|
421
|
-
when /\A(?:FLAGS)\z/ni
|
422
|
-
return flags_response
|
423
|
-
when /\A(?:ID)\z/ni
|
424
|
-
return id_response
|
425
|
-
when /\A(?:LIST|LSUB|XLIST)\z/ni
|
426
|
-
return list_response
|
427
|
-
when /\A(?:NAMESPACE)\z/ni
|
428
|
-
return namespace_response
|
429
|
-
when /\A(?:QUOTA)\z/ni
|
430
|
-
return getquota_response
|
431
|
-
when /\A(?:QUOTAROOT)\z/ni
|
432
|
-
return getquotaroot_response
|
433
|
-
when /\A(?:ACL)\z/ni
|
434
|
-
return getacl_response
|
435
|
-
when /\A(?:SEARCH|SORT)\z/ni
|
436
|
-
return search_response
|
437
|
-
when /\A(?:THREAD)\z/ni
|
438
|
-
return thread_response
|
439
|
-
when /\A(?:STATUS)\z/ni
|
440
|
-
return status_response
|
441
|
-
when /\A(?:CAPABILITY)\z/ni
|
442
|
-
return capability_data__untagged
|
443
|
-
when /\A(?:NOOP)\z/ni
|
444
|
-
return ignored_response
|
445
|
-
when /\A(?:ENABLED)\z/ni
|
446
|
-
return enable_data
|
447
|
-
else
|
448
|
-
return text_response
|
600
|
+
def response_data__unhandled(klass = UntaggedResponse)
|
601
|
+
num = number?; SP?
|
602
|
+
type = tagged_ext_label; SP?
|
603
|
+
text = remaining_unparsed
|
604
|
+
data =
|
605
|
+
if num && text then UnparsedNumericResponseData.new(num, text)
|
606
|
+
elsif text then UnparsedData.new(text)
|
607
|
+
else num
|
449
608
|
end
|
450
|
-
|
451
|
-
|
452
|
-
|
609
|
+
klass.new(type, data, @str)
|
610
|
+
end
|
611
|
+
|
612
|
+
# reads all the way up until CRLF
|
613
|
+
def remaining_unparsed
|
614
|
+
str = @str[@pos...-2] and @pos += str.bytesize
|
615
|
+
str&.empty? ? nil : str
|
453
616
|
end
|
454
617
|
|
618
|
+
def response_data__ignored; response_data__unhandled(IgnoredResponse) end
|
619
|
+
alias response_data__noop response_data__ignored
|
620
|
+
|
621
|
+
alias esearch_response response_data__unhandled
|
622
|
+
alias expunged_resp response_data__unhandled
|
623
|
+
alias uidfetch_resp response_data__unhandled
|
624
|
+
alias listrights_data response_data__unhandled
|
625
|
+
alias myrights_data response_data__unhandled
|
626
|
+
alias metadata_resp response_data__unhandled
|
627
|
+
alias language_data response_data__unhandled
|
628
|
+
alias comparator_data response_data__unhandled
|
629
|
+
alias message_data__converted response_data__unhandled
|
630
|
+
|
631
|
+
# RFC3501 & RFC9051:
|
632
|
+
# response-tagged = tag SP resp-cond-state CRLF
|
633
|
+
#
|
634
|
+
# resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text
|
635
|
+
# ; Status condition
|
636
|
+
#
|
637
|
+
# tag = 1*<any ASTRING-CHAR except "+">
|
455
638
|
def response_tagged
|
456
|
-
tag
|
457
|
-
|
458
|
-
|
459
|
-
name = token.value.upcase
|
460
|
-
match(T_SPACE)
|
461
|
-
return TaggedResponse.new(tag, name, resp_text, @str)
|
639
|
+
tag = tag(); SP!
|
640
|
+
name = resp_cond_state__name; SP!
|
641
|
+
TaggedResponse.new(tag, name, resp_text, @str)
|
462
642
|
end
|
463
643
|
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
644
|
+
# RFC3501 & RFC9051:
|
645
|
+
# resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text
|
646
|
+
def resp_cond_state__untagged
|
647
|
+
name = resp_cond_state__name; SP!
|
648
|
+
UntaggedResponse.new(name, resp_text, @str)
|
469
649
|
end
|
470
650
|
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
name = token.value.upcase
|
476
|
-
case name
|
477
|
-
when "EXISTS", "RECENT", "EXPUNGE"
|
478
|
-
return UntaggedResponse.new(name, n, @str)
|
479
|
-
when "FETCH"
|
480
|
-
shift_token
|
481
|
-
match(T_SPACE)
|
482
|
-
data = FetchData.new(n, msg_att(n))
|
483
|
-
return UntaggedResponse.new(name, data, @str)
|
484
|
-
end
|
651
|
+
# resp-cond-auth = ("OK" / "PREAUTH") SP resp-text
|
652
|
+
def resp_cond_auth
|
653
|
+
name = resp_cond_auth__name; SP!
|
654
|
+
UntaggedResponse.new(name, resp_text, @str)
|
485
655
|
end
|
486
656
|
|
657
|
+
# resp-cond-bye = "BYE" SP resp-text
|
658
|
+
def resp_cond_bye
|
659
|
+
name = label(BYE); SP!
|
660
|
+
UntaggedResponse.new(name, resp_text, @str)
|
661
|
+
end
|
662
|
+
|
663
|
+
# message-data = nz-number SP ("EXPUNGE" / ("FETCH" SP msg-att))
|
664
|
+
def message_data__fetch
|
665
|
+
seq = nz_number; SP!
|
666
|
+
name = label "FETCH"; SP!
|
667
|
+
data = FetchData.new(seq, msg_att(seq))
|
668
|
+
UntaggedResponse.new(name, data, @str)
|
669
|
+
end
|
670
|
+
|
671
|
+
def response_data__simple_numeric
|
672
|
+
data = nz_number; SP!
|
673
|
+
name = tagged_ext_label
|
674
|
+
UntaggedResponse.new(name, data, @str)
|
675
|
+
end
|
676
|
+
|
677
|
+
alias message_data__expunge response_data__simple_numeric
|
678
|
+
alias mailbox_data__exists response_data__simple_numeric
|
679
|
+
alias mailbox_data__recent response_data__simple_numeric
|
680
|
+
|
681
|
+
# RFC3501 & RFC9051:
|
682
|
+
# msg-att = "(" (msg-att-dynamic / msg-att-static)
|
683
|
+
# *(SP (msg-att-dynamic / msg-att-static)) ")"
|
684
|
+
#
|
685
|
+
# msg-att-dynamic = "FLAGS" SP "(" [flag-fetch *(SP flag-fetch)] ")"
|
686
|
+
# RFC5257 (ANNOTATE extension):
|
687
|
+
# msg-att-dynamic =/ "ANNOTATION" SP
|
688
|
+
# ( "(" entry-att *(SP entry-att) ")" /
|
689
|
+
# "(" entry *(SP entry) ")" )
|
690
|
+
# RFC7162 (CONDSTORE extension):
|
691
|
+
# msg-att-dynamic =/ fetch-mod-resp
|
692
|
+
# fetch-mod-resp = "MODSEQ" SP "(" permsg-modsequence ")"
|
693
|
+
# RFC8970 (PREVIEW extension):
|
694
|
+
# msg-att-dynamic =/ "PREVIEW" SP nstring
|
695
|
+
#
|
696
|
+
# RFC3501:
|
697
|
+
# msg-att-static = "ENVELOPE" SP envelope /
|
698
|
+
# "INTERNALDATE" SP date-time /
|
699
|
+
# "RFC822" [".HEADER" / ".TEXT"] SP nstring /
|
700
|
+
# "RFC822.SIZE" SP number /
|
701
|
+
# "BODY" ["STRUCTURE"] SP body /
|
702
|
+
# "BODY" section ["<" number ">"] SP nstring /
|
703
|
+
# "UID" SP uniqueid
|
704
|
+
# RFC3516 (BINARY extension):
|
705
|
+
# msg-att-static =/ "BINARY" section-binary SP (nstring / literal8)
|
706
|
+
# / "BINARY.SIZE" section-binary SP number
|
707
|
+
# RFC8514 (SAVEDATE extension):
|
708
|
+
# msg-att-static =/ "SAVEDATE" SP (date-time / nil)
|
709
|
+
# RFC8474 (OBJECTID extension):
|
710
|
+
# msg-att-static =/ fetch-emailid-resp / fetch-threadid-resp
|
711
|
+
# fetch-emailid-resp = "EMAILID" SP "(" objectid ")"
|
712
|
+
# fetch-threadid-resp = "THREADID" SP ( "(" objectid ")" / nil )
|
713
|
+
# RFC9051:
|
714
|
+
# msg-att-static = "ENVELOPE" SP envelope /
|
715
|
+
# "INTERNALDATE" SP date-time /
|
716
|
+
# "RFC822.SIZE" SP number64 /
|
717
|
+
# "BODY" ["STRUCTURE"] SP body /
|
718
|
+
# "BODY" section ["<" number ">"] SP nstring /
|
719
|
+
# "BINARY" section-binary SP (nstring / literal8) /
|
720
|
+
# "BINARY.SIZE" section-binary SP number /
|
721
|
+
# "UID" SP uniqueid
|
722
|
+
#
|
723
|
+
# Re https://www.rfc-editor.org/errata/eid7246, I'm adding "offset" to the
|
724
|
+
# official "BINARY" ABNF, like so:
|
725
|
+
#
|
726
|
+
# msg-att-static =/ "BINARY" section-binary ["<" number ">"] SP
|
727
|
+
# (nstring / literal8)
|
487
728
|
def msg_att(n)
|
488
|
-
|
729
|
+
lpar
|
489
730
|
attr = {}
|
490
731
|
while true
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
name,
|
507
|
-
|
508
|
-
name, val = rfc822_text
|
509
|
-
when /\A(?:RFC822\.SIZE)\z/ni
|
510
|
-
name, val = rfc822_size
|
511
|
-
when /\A(?:BODY(?:STRUCTURE)?)\z/ni
|
512
|
-
name, val = body_data
|
513
|
-
when /\A(?:UID)\z/ni
|
514
|
-
name, val = uid_data
|
515
|
-
when /\A(?:MODSEQ)\z/ni
|
516
|
-
name, val = modseq_data
|
517
|
-
else
|
518
|
-
parse_error("unknown attribute `%s' for {%d}", token.value, n)
|
519
|
-
end
|
732
|
+
name = msg_att__label; SP!
|
733
|
+
val =
|
734
|
+
case name
|
735
|
+
when "UID" then uniqueid
|
736
|
+
when "FLAGS" then flag_list
|
737
|
+
when "BODY" then body
|
738
|
+
when /\ABODY\[/ni then nstring
|
739
|
+
when "BODYSTRUCTURE" then body
|
740
|
+
when "ENVELOPE" then envelope
|
741
|
+
when "INTERNALDATE" then date_time
|
742
|
+
when "RFC822.SIZE" then number64
|
743
|
+
when "RFC822" then nstring # not in rev2
|
744
|
+
when "RFC822.HEADER" then nstring # not in rev2
|
745
|
+
when "RFC822.TEXT" then nstring # not in rev2
|
746
|
+
when "MODSEQ" then parens__modseq # CONDSTORE
|
747
|
+
else parse_error("unknown attribute `%s' for {%d}", name, n)
|
748
|
+
end
|
520
749
|
attr[name] = val
|
750
|
+
break unless SP?
|
751
|
+
break if lookahead_rpar?
|
521
752
|
end
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
753
|
+
rpar
|
754
|
+
attr
|
755
|
+
end
|
756
|
+
|
757
|
+
# appends "[section]" and "<partial>" to the base label
|
758
|
+
def msg_att__label
|
759
|
+
case (name = tagged_ext_label)
|
760
|
+
when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni
|
761
|
+
# ignoring "[]" fixes https://bugs.ruby-lang.org/issues/5620
|
762
|
+
lbra? and rbra
|
763
|
+
when "BODY"
|
764
|
+
peek_lbra? and name << section and
|
765
|
+
peek_str?("<") and name << atom # partial
|
766
|
+
end
|
767
|
+
name
|
530
768
|
end
|
531
769
|
|
532
770
|
def envelope
|
@@ -564,58 +802,10 @@ module Net
|
|
564
802
|
return result
|
565
803
|
end
|
566
804
|
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
return name, flag_list
|
572
|
-
end
|
573
|
-
|
574
|
-
def internaldate_data
|
575
|
-
token = match(T_ATOM)
|
576
|
-
name = token.value.upcase
|
577
|
-
match(T_SPACE)
|
578
|
-
token = match(T_QUOTED)
|
579
|
-
return name, token.value
|
580
|
-
end
|
581
|
-
|
582
|
-
def rfc822_text
|
583
|
-
token = match(T_ATOM)
|
584
|
-
name = token.value.upcase
|
585
|
-
token = lookahead
|
586
|
-
if token.symbol == T_LBRA
|
587
|
-
shift_token
|
588
|
-
match(T_RBRA)
|
589
|
-
end
|
590
|
-
match(T_SPACE)
|
591
|
-
return name, nstring
|
592
|
-
end
|
593
|
-
|
594
|
-
def rfc822_size
|
595
|
-
token = match(T_ATOM)
|
596
|
-
name = token.value.upcase
|
597
|
-
match(T_SPACE)
|
598
|
-
return name, number
|
599
|
-
end
|
600
|
-
|
601
|
-
def body_data
|
602
|
-
token = match(T_ATOM)
|
603
|
-
name = token.value.upcase
|
604
|
-
token = lookahead
|
605
|
-
if token.symbol == T_SPACE
|
606
|
-
shift_token
|
607
|
-
return name, body
|
608
|
-
end
|
609
|
-
name.concat(section)
|
610
|
-
token = lookahead
|
611
|
-
if token.symbol == T_ATOM
|
612
|
-
name.concat(token.value)
|
613
|
-
shift_token
|
614
|
-
end
|
615
|
-
match(T_SPACE)
|
616
|
-
data = nstring
|
617
|
-
return name, data
|
618
|
-
end
|
805
|
+
# date-time = DQUOTE date-day-fixed "-" date-month "-" date-year
|
806
|
+
# SP time SP zone DQUOTE
|
807
|
+
alias date_time quoted
|
808
|
+
alias ndatetime nquoted
|
619
809
|
|
620
810
|
# RFC-3501 & RFC-9051:
|
621
811
|
# body = "(" (body-type-1part / body-type-mpart) ")"
|
@@ -844,6 +1034,7 @@ module Net
|
|
844
1034
|
if lpar?
|
845
1035
|
result = [case_insensitive__string]
|
846
1036
|
result << case_insensitive__string while SP?
|
1037
|
+
rpar
|
847
1038
|
result
|
848
1039
|
else
|
849
1040
|
case_insensitive__nstring
|
@@ -872,48 +1063,78 @@ module Net
|
|
872
1063
|
end
|
873
1064
|
end
|
874
1065
|
|
1066
|
+
# section = "[" [section-spec] "]"
|
875
1067
|
def section
|
876
|
-
str =
|
877
|
-
|
878
|
-
str
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
str
|
907
|
-
|
1068
|
+
str = +lbra
|
1069
|
+
str << section_spec unless peek_rbra?
|
1070
|
+
str << rbra
|
1071
|
+
end
|
1072
|
+
|
1073
|
+
# section-spec = section-msgtext / (section-part ["." section-text])
|
1074
|
+
# section-msgtext = "HEADER" /
|
1075
|
+
# "HEADER.FIELDS" [".NOT"] SP header-list /
|
1076
|
+
# "TEXT"
|
1077
|
+
# ; top-level or MESSAGE/RFC822 or
|
1078
|
+
# ; MESSAGE/GLOBAL part
|
1079
|
+
# section-part = nz-number *("." nz-number)
|
1080
|
+
# ; body part reference.
|
1081
|
+
# ; Allows for accessing nested body parts.
|
1082
|
+
# section-text = section-msgtext / "MIME"
|
1083
|
+
# ; text other than actual body part (headers,
|
1084
|
+
# ; etc.)
|
1085
|
+
#
|
1086
|
+
# n.b: we could "cheat" here and just grab all text inside the brackets,
|
1087
|
+
# but literals would need special treatment.
|
1088
|
+
def section_spec
|
1089
|
+
str = "".b
|
1090
|
+
str << atom # grabs everything up to "SP header-list" or "]"
|
1091
|
+
str << " " << header_list if SP?
|
1092
|
+
str
|
1093
|
+
end
|
1094
|
+
|
1095
|
+
# header-list = "(" header-fld-name *(SP header-fld-name) ")"
|
1096
|
+
def header_list
|
1097
|
+
str = +""
|
1098
|
+
str << lpar << header_fld_name
|
1099
|
+
str << " " << header_fld_name while SP?
|
1100
|
+
str << rpar
|
908
1101
|
end
|
909
1102
|
|
910
|
-
|
911
|
-
|
1103
|
+
# RFC3501 & RFC9051:
|
1104
|
+
# header-fld-name = astring
|
1105
|
+
#
|
1106
|
+
# Although RFC3501 allows any astring, RFC5322-valid header names are one
|
1107
|
+
# or more of the printable US-ASCII characters, except SP and colon. So
|
1108
|
+
# empty string isn't valid, and literals aren't needed and should not be
|
1109
|
+
# used. This syntax is unchanged by [I18N-HDRS] (RFC6532).
|
1110
|
+
#
|
1111
|
+
# RFC5233:
|
1112
|
+
# optional-field = field-name ":" unstructured CRLF
|
1113
|
+
# field-name = 1*ftext
|
1114
|
+
# ftext = %d33-57 / ; Printable US-ASCII
|
1115
|
+
# %d59-126 ; characters not including
|
1116
|
+
# ; ":".
|
1117
|
+
#
|
1118
|
+
# Atom and quoted should be sufficient.
|
1119
|
+
#
|
1120
|
+
# TODO: Use original source string, rather than decode and re-encode.
|
1121
|
+
# TODO: or at least, DRY up this code with the send_command formatting.
|
1122
|
+
def header_fld_name
|
1123
|
+
case (str = astring)
|
912
1124
|
when ""
|
1125
|
+
warn '%s header-fld-name is an invalid RFC5322 field-name: ""' %
|
1126
|
+
[self.class]
|
913
1127
|
return '""'
|
914
1128
|
when /[\x80-\xff\r\n]/n
|
1129
|
+
warn "%s header-fld-name %p has invalid RFC5322 field-name char: %p" %
|
1130
|
+
[self.class, str, $&]
|
915
1131
|
# literal
|
916
1132
|
return "{" + str.bytesize.to_s + "}" + CRLF + str
|
1133
|
+
when /[^\x21-\x39\x3b-\xfe]/n
|
1134
|
+
warn "%s header-fld-name %p has invalid RFC5322 field-name char: %p" %
|
1135
|
+
[self.class, str, $&]
|
1136
|
+
# invalid quoted string
|
1137
|
+
return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"'
|
917
1138
|
when /[(){ \x00-\x1f\x7f%*"\\]/n
|
918
1139
|
# quoted string
|
919
1140
|
return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"'
|
@@ -923,50 +1144,24 @@ module Net
|
|
923
1144
|
end
|
924
1145
|
end
|
925
1146
|
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
return name, number
|
931
|
-
end
|
932
|
-
|
933
|
-
def modseq_data
|
934
|
-
token = match(T_ATOM)
|
935
|
-
name = token.value.upcase
|
936
|
-
match(T_SPACE)
|
937
|
-
match(T_LPAR)
|
938
|
-
modseq = number
|
939
|
-
match(T_RPAR)
|
940
|
-
return name, modseq
|
941
|
-
end
|
942
|
-
|
943
|
-
def ignored_response
|
944
|
-
while lookahead.symbol != T_CRLF
|
945
|
-
shift_token
|
946
|
-
end
|
947
|
-
return IgnoredResponse.new(@str)
|
948
|
-
end
|
949
|
-
|
950
|
-
def text_response
|
951
|
-
token = match(T_ATOM)
|
952
|
-
name = token.value.upcase
|
953
|
-
match(T_SPACE)
|
954
|
-
return UntaggedResponse.new(name, text)
|
955
|
-
end
|
1147
|
+
# mailbox-data = "FLAGS" SP flag-list / "LIST" SP mailbox-list /
|
1148
|
+
# "LSUB" SP mailbox-list / "SEARCH" *(SP nz-number) /
|
1149
|
+
# "STATUS" SP mailbox SP "(" [status-att-list] ")" /
|
1150
|
+
# number SP "EXISTS" / number SP "RECENT"
|
956
1151
|
|
957
|
-
def
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
return UntaggedResponse.new(name, flag_list, @str)
|
1152
|
+
def mailbox_data__flags
|
1153
|
+
name = label("FLAGS")
|
1154
|
+
SP!
|
1155
|
+
UntaggedResponse.new(name, flag_list, @str)
|
962
1156
|
end
|
963
1157
|
|
964
|
-
def
|
965
|
-
|
966
|
-
|
967
|
-
|
968
|
-
return UntaggedResponse.new(name, mailbox_list, @str)
|
1158
|
+
def mailbox_data__list
|
1159
|
+
name = label_in("LIST", "LSUB", "XLIST")
|
1160
|
+
SP!
|
1161
|
+
UntaggedResponse.new(name, mailbox_list, @str)
|
969
1162
|
end
|
1163
|
+
alias mailbox_data__lsub mailbox_data__list
|
1164
|
+
alias mailbox_data__xlist mailbox_data__list
|
970
1165
|
|
971
1166
|
def mailbox_list
|
972
1167
|
attr = flag_list
|
@@ -1032,7 +1227,8 @@ module Net
|
|
1032
1227
|
return UntaggedResponse.new(name, data, @str)
|
1033
1228
|
end
|
1034
1229
|
|
1035
|
-
|
1230
|
+
# acl-data = "ACL" SP mailbox *(SP identifier SP rights)
|
1231
|
+
def acl_data
|
1036
1232
|
token = match(T_ATOM)
|
1037
1233
|
name = token.value.upcase
|
1038
1234
|
match(T_SPACE)
|
@@ -1058,7 +1254,21 @@ module Net
|
|
1058
1254
|
return UntaggedResponse.new(name, data, @str)
|
1059
1255
|
end
|
1060
1256
|
|
1061
|
-
|
1257
|
+
# RFC3501:
|
1258
|
+
# mailbox-data = "SEARCH" *(SP nz-number) / ...
|
1259
|
+
# RFC5256: SORT
|
1260
|
+
# sort-data = "SORT" *(SP nz-number)
|
1261
|
+
# RFC7162: CONDSTORE, QRESYNC
|
1262
|
+
# mailbox-data =/ "SEARCH" [1*(SP nz-number) SP
|
1263
|
+
# search-sort-mod-seq]
|
1264
|
+
# sort-data = "SORT" [1*(SP nz-number) SP
|
1265
|
+
# search-sort-mod-seq]
|
1266
|
+
# ; Updates the SORT response from RFC 5256.
|
1267
|
+
# search-sort-mod-seq = "(" "MODSEQ" SP mod-sequence-value ")"
|
1268
|
+
# RFC9051:
|
1269
|
+
# mailbox-data = obsolete-search-response / ...
|
1270
|
+
# obsolete-search-response = "SEARCH" *(SP nz-number)
|
1271
|
+
def mailbox_data__search
|
1062
1272
|
token = match(T_ATOM)
|
1063
1273
|
name = token.value.upcase
|
1064
1274
|
token = lookahead
|
@@ -1088,8 +1298,9 @@ module Net
|
|
1088
1298
|
end
|
1089
1299
|
return UntaggedResponse.new(name, data, @str)
|
1090
1300
|
end
|
1301
|
+
alias sort_data mailbox_data__search
|
1091
1302
|
|
1092
|
-
def
|
1303
|
+
def thread_data
|
1093
1304
|
token = match(T_ATOM)
|
1094
1305
|
name = token.value.upcase
|
1095
1306
|
token = lookahead
|
@@ -1151,7 +1362,7 @@ module Net
|
|
1151
1362
|
return rootmember
|
1152
1363
|
end
|
1153
1364
|
|
1154
|
-
def
|
1365
|
+
def mailbox_data__status
|
1155
1366
|
token = match(T_ATOM)
|
1156
1367
|
name = token.value.upcase
|
1157
1368
|
match(T_SPACE)
|
@@ -1198,11 +1409,13 @@ module Net
|
|
1198
1409
|
end
|
1199
1410
|
|
1200
1411
|
# As a workaround for buggy servers, allow a trailing SP:
|
1201
|
-
# *(SP
|
1412
|
+
# *(SP capability) [SP]
|
1202
1413
|
def capability__list
|
1203
|
-
|
1414
|
+
list = []; while SP? && (capa = capability?) do list << capa end; list
|
1204
1415
|
end
|
1205
1416
|
|
1417
|
+
alias resp_code__capability capability__list
|
1418
|
+
|
1206
1419
|
# capability = ("AUTH=" auth-type) / atom
|
1207
1420
|
# ; New capabilities MUST begin with "X" or be
|
1208
1421
|
# ; registered with IANA as standard or
|
@@ -1325,68 +1538,91 @@ module Net
|
|
1325
1538
|
end
|
1326
1539
|
end
|
1327
1540
|
|
1328
|
-
# See https://www.rfc-editor.org/errata/rfc3501
|
1541
|
+
# RFC3501 (See https://www.rfc-editor.org/errata/rfc3501):
|
1542
|
+
# resp-text-code = "ALERT" /
|
1543
|
+
# "BADCHARSET" [SP "(" charset *(SP charset) ")" ] /
|
1544
|
+
# capability-data / "PARSE" /
|
1545
|
+
# "PERMANENTFLAGS" SP "(" [flag-perm *(SP flag-perm)] ")" /
|
1546
|
+
# "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
|
1547
|
+
# "UIDNEXT" SP nz-number / "UIDVALIDITY" SP nz-number /
|
1548
|
+
# "UNSEEN" SP nz-number /
|
1549
|
+
# atom [SP 1*<any TEXT-CHAR except "]">]
|
1550
|
+
# capability-data = "CAPABILITY" *(SP capability) SP "IMAP4rev1"
|
1551
|
+
# *(SP capability)
|
1329
1552
|
#
|
1330
|
-
#
|
1331
|
-
#
|
1332
|
-
#
|
1333
|
-
#
|
1334
|
-
#
|
1335
|
-
#
|
1336
|
-
#
|
1337
|
-
#
|
1338
|
-
#
|
1553
|
+
# RFC5530:
|
1554
|
+
# resp-text-code =/ "UNAVAILABLE" / "AUTHENTICATIONFAILED" /
|
1555
|
+
# "AUTHORIZATIONFAILED" / "EXPIRED" /
|
1556
|
+
# "PRIVACYREQUIRED" / "CONTACTADMIN" / "NOPERM" /
|
1557
|
+
# "INUSE" / "EXPUNGEISSUED" / "CORRUPTION" /
|
1558
|
+
# "SERVERBUG" / "CLIENTBUG" / "CANNOT" /
|
1559
|
+
# "LIMIT" / "OVERQUOTA" / "ALREADYEXISTS" /
|
1560
|
+
# "NONEXISTENT"
|
1561
|
+
# RFC9051:
|
1562
|
+
# resp-text-code = "ALERT" /
|
1563
|
+
# "BADCHARSET" [SP "(" charset *(SP charset) ")" ] /
|
1564
|
+
# capability-data / "PARSE" /
|
1565
|
+
# "PERMANENTFLAGS" SP "(" [flag-perm *(SP flag-perm)] ")" /
|
1566
|
+
# "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
|
1567
|
+
# "UIDNEXT" SP nz-number / "UIDVALIDITY" SP nz-number /
|
1568
|
+
# resp-code-apnd / resp-code-copy / "UIDNOTSTICKY" /
|
1569
|
+
# "UNAVAILABLE" / "AUTHENTICATIONFAILED" /
|
1570
|
+
# "AUTHORIZATIONFAILED" / "EXPIRED" /
|
1571
|
+
# "PRIVACYREQUIRED" / "CONTACTADMIN" / "NOPERM" /
|
1572
|
+
# "INUSE" / "EXPUNGEISSUED" / "CORRUPTION" /
|
1573
|
+
# "SERVERBUG" / "CLIENTBUG" / "CANNOT" /
|
1574
|
+
# "LIMIT" / "OVERQUOTA" / "ALREADYEXISTS" /
|
1575
|
+
# "NONEXISTENT" / "NOTSAVED" / "HASCHILDREN" /
|
1576
|
+
# "CLOSED" /
|
1577
|
+
# "UNKNOWN-CTE" /
|
1578
|
+
# atom [SP 1*<any TEXT-CHAR except "]">]
|
1579
|
+
# capability-data = "CAPABILITY" *(SP capability) SP "IMAP4rev2"
|
1580
|
+
# *(SP capability)
|
1339
1581
|
#
|
1340
|
-
#
|
1341
|
-
# resp-
|
1582
|
+
# RFC4315 (UIDPLUS), RFC9051 (IMAP4rev2):
|
1583
|
+
# resp-code-apnd = "APPENDUID" SP nz-number SP append-uid
|
1584
|
+
# resp-code-copy = "COPYUID" SP nz-number SP uid-set SP uid-set
|
1585
|
+
# resp-text-code =/ resp-code-apnd / resp-code-copy / "UIDNOTSTICKY"
|
1586
|
+
#
|
1587
|
+
# RFC7162 (CONDSTORE):
|
1588
|
+
# resp-text-code =/ "HIGHESTMODSEQ" SP mod-sequence-value /
|
1589
|
+
# "NOMODSEQ" /
|
1590
|
+
# "MODIFIED" SP sequence-set
|
1342
1591
|
def resp_text_code
|
1343
|
-
|
1344
|
-
|
1345
|
-
|
1346
|
-
|
1347
|
-
|
1348
|
-
|
1349
|
-
|
1350
|
-
|
1351
|
-
|
1352
|
-
|
1353
|
-
|
1354
|
-
|
1355
|
-
|
1356
|
-
|
1357
|
-
|
1358
|
-
|
1359
|
-
|
1360
|
-
|
1361
|
-
result = ResponseCode.new(name, resp_code_copy__data)
|
1362
|
-
else
|
1363
|
-
token = lookahead
|
1364
|
-
if token.symbol == T_SPACE
|
1365
|
-
shift_token
|
1366
|
-
result = ResponseCode.new(name, text_chars_except_rbra)
|
1592
|
+
name = resp_text_code__name
|
1593
|
+
data =
|
1594
|
+
case name
|
1595
|
+
when "CAPABILITY" then resp_code__capability
|
1596
|
+
when "PERMANENTFLAGS" then SP? ? flag_perm__list : []
|
1597
|
+
when "UIDNEXT" then SP!; nz_number
|
1598
|
+
when "UIDVALIDITY" then SP!; nz_number
|
1599
|
+
when "UNSEEN" then SP!; nz_number # rev1 only
|
1600
|
+
when "APPENDUID" then SP!; resp_code_apnd__data # rev2, UIDPLUS
|
1601
|
+
when "COPYUID" then SP!; resp_code_copy__data # rev2, UIDPLUS
|
1602
|
+
when "BADCHARSET" then SP? ? charset__list : []
|
1603
|
+
when "ALERT", "PARSE", "READ-ONLY", "READ-WRITE", "TRYCREATE",
|
1604
|
+
"UNAVAILABLE", "AUTHENTICATIONFAILED", "AUTHORIZATIONFAILED",
|
1605
|
+
"EXPIRED", "PRIVACYREQUIRED", "CONTACTADMIN", "NOPERM", "INUSE",
|
1606
|
+
"EXPUNGEISSUED", "CORRUPTION", "SERVERBUG", "CLIENTBUG", "CANNOT",
|
1607
|
+
"LIMIT", "OVERQUOTA", "ALREADYEXISTS", "NONEXISTENT", "CLOSED",
|
1608
|
+
"NOTSAVED", "UIDNOTSTICKY", "UNKNOWN-CTE", "HASCHILDREN"
|
1609
|
+
when "NOMODSEQ" # CONDSTORE
|
1367
1610
|
else
|
1368
|
-
|
1611
|
+
SP? and text_chars_except_rbra
|
1369
1612
|
end
|
1370
|
-
|
1371
|
-
return result
|
1613
|
+
ResponseCode.new(name, data)
|
1372
1614
|
end
|
1373
1615
|
|
1616
|
+
alias resp_text_code__name case_insensitive__atom
|
1617
|
+
|
1374
1618
|
# 1*<any TEXT-CHAR except "]">
|
1375
1619
|
def text_chars_except_rbra
|
1376
1620
|
match_re(CTEXT_REGEXP, '1*<any TEXT-CHAR except "]">')[0]
|
1377
1621
|
end
|
1378
1622
|
|
1379
|
-
|
1380
|
-
|
1381
|
-
|
1382
|
-
match(T_LPAR)
|
1383
|
-
result << charset
|
1384
|
-
while accept(T_SPACE)
|
1385
|
-
result << charset
|
1386
|
-
end
|
1387
|
-
match(T_RPAR)
|
1388
|
-
end
|
1389
|
-
result
|
1623
|
+
# "(" charset *(SP charset) ")"
|
1624
|
+
def charset__list
|
1625
|
+
lpar; list = [charset]; while SP? do list << charset end; rpar; list
|
1390
1626
|
end
|
1391
1627
|
|
1392
1628
|
# already matched: "APPENDUID"
|
@@ -1402,8 +1638,8 @@ module Net
|
|
1402
1638
|
# match uid_set even if that returns a single-member array.
|
1403
1639
|
#
|
1404
1640
|
def resp_code_apnd__data
|
1405
|
-
|
1406
|
-
|
1641
|
+
validity = number; SP!
|
1642
|
+
dst_uids = uid_set # uniqueid ⊂ uid-set
|
1407
1643
|
UIDPlusData.new(validity, nil, dst_uids)
|
1408
1644
|
end
|
1409
1645
|
|
@@ -1411,9 +1647,9 @@ module Net
|
|
1411
1647
|
#
|
1412
1648
|
# resp-code-copy = "COPYUID" SP nz-number SP uid-set SP uid-set
|
1413
1649
|
def resp_code_copy__data
|
1414
|
-
|
1415
|
-
|
1416
|
-
|
1650
|
+
validity = number; SP!
|
1651
|
+
src_uids = uid_set; SP!
|
1652
|
+
dst_uids = uid_set
|
1417
1653
|
UIDPlusData.new(validity, src_uids, dst_uids)
|
1418
1654
|
end
|
1419
1655
|
|
@@ -1472,36 +1708,56 @@ module Net
|
|
1472
1708
|
return Address.new(name, route, mailbox, host)
|
1473
1709
|
end
|
1474
1710
|
|
1475
|
-
|
1476
|
-
(?# FLAG )\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)|\
|
1477
|
-
(?# ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\]+)/n
|
1478
|
-
|
1711
|
+
# flag-list = "(" [flag *(SP flag)] ")"
|
1479
1712
|
def flag_list
|
1480
|
-
|
1481
|
-
|
1482
|
-
|
1483
|
-
|
1484
|
-
|
1485
|
-
|
1486
|
-
|
1487
|
-
|
1488
|
-
|
1489
|
-
|
1490
|
-
|
1491
|
-
|
1713
|
+
match_re(Patterns::FLAG_LIST, "flag-list")[1]
|
1714
|
+
.split(nil)
|
1715
|
+
.map! { _1.start_with?("\\") ? _1[1..].capitalize.to_sym : _1 }
|
1716
|
+
end
|
1717
|
+
|
1718
|
+
# "(" [flag-perm *(SP flag-perm)] ")"
|
1719
|
+
def flag_perm__list
|
1720
|
+
match_re(Patterns::FLAG_PERM_LIST, "PERMANENTFLAGS flag-perm list")[1]
|
1721
|
+
.split(nil)
|
1722
|
+
.map! { _1.start_with?("\\") ? _1[1..].capitalize.to_sym : _1 }
|
1723
|
+
end
|
1724
|
+
|
1725
|
+
# Not checking for max one mbx-list-sflag in the parser.
|
1726
|
+
# >>>
|
1727
|
+
# mbx-list-flags = *(mbx-list-oflag SP) mbx-list-sflag
|
1728
|
+
# *(SP mbx-list-oflag) /
|
1729
|
+
# mbx-list-oflag *(SP mbx-list-oflag)
|
1730
|
+
# mbx-list-oflag = "\Noinferiors" / child-mbox-flag /
|
1731
|
+
# "\Subscribed" / "\Remote" / flag-extension
|
1732
|
+
# ; Other flags; multiple from this list are
|
1733
|
+
# ; possible per LIST response, but each flag
|
1734
|
+
# ; can only appear once per LIST response
|
1735
|
+
# mbx-list-sflag = "\NonExistent" / "\Noselect" / "\Marked" /
|
1736
|
+
# "\Unmarked"
|
1737
|
+
# ; Selectability flags; only one per LIST response
|
1738
|
+
def parens__mbx_list_flags
|
1739
|
+
match_re(Patterns::MBX_LIST_FLAGS, "mbx-list-flags")[1]
|
1740
|
+
.split(nil).map! { _1.capitalize.to_sym }
|
1492
1741
|
end
|
1493
1742
|
|
1494
|
-
|
1495
1743
|
# See https://www.rfc-editor.org/errata/rfc3501
|
1496
1744
|
#
|
1497
1745
|
# charset = atom / quoted
|
1498
|
-
def charset
|
1499
|
-
|
1500
|
-
|
1501
|
-
|
1502
|
-
|
1503
|
-
|
1504
|
-
|
1746
|
+
def charset; quoted? || atom end
|
1747
|
+
|
1748
|
+
# RFC7162:
|
1749
|
+
# mod-sequence-value = 1*DIGIT
|
1750
|
+
# ;; Positive unsigned 63-bit integer
|
1751
|
+
# ;; (mod-sequence)
|
1752
|
+
# ;; (1 <= n <= 9,223,372,036,854,775,807).
|
1753
|
+
alias mod_sequence_value nz_number64
|
1754
|
+
|
1755
|
+
# RFC7162:
|
1756
|
+
# permsg-modsequence = mod-sequence-value
|
1757
|
+
# ;; Per-message mod-sequence.
|
1758
|
+
alias permsg_modsequence mod_sequence_value
|
1759
|
+
|
1760
|
+
def parens__modseq; lpar; _ = permsg_modsequence; rpar; _ end
|
1505
1761
|
|
1506
1762
|
# RFC-4315 (UIDPLUS) or RFC9051 (IMAP4rev2):
|
1507
1763
|
# uid-set = (uniqueid / uid-range) *("," uid-set)
|
@@ -1535,10 +1791,10 @@ module Net
|
|
1535
1791
|
#
|
1536
1792
|
# This advances @pos directly so it's safe before changing @lex_state.
|
1537
1793
|
def accept_spaces
|
1538
|
-
|
1539
|
-
|
1794
|
+
return false unless SP?
|
1795
|
+
@str.index(SPACES_REGEXP, @pos) and
|
1540
1796
|
@pos = $~.end(0)
|
1541
|
-
|
1797
|
+
true
|
1542
1798
|
end
|
1543
1799
|
|
1544
1800
|
def next_token
|