net-imap 0.4.2 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- # atom = 1*ATOM-CHAR
324
- #
325
- # TODO: match atom entirely by regexp (in the "lexer")
326
- def atom; -combine_adjacent(*ATOM_TOKENS) end
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
- def response
382
- token = lookahead
383
- case token.symbol
384
- when T_PLUS
385
- result = continue_req
386
- when T_STAR
387
- result = response_untagged
388
- else
389
- result = response_tagged
390
- end
391
- while lookahead.symbol == T_SPACE
392
- # Ignore trailing space for Microsoft Exchange Server
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
- match(T_PLUS)
402
- token = lookahead
403
- if token.symbol == T_SPACE
404
- shift_token
405
- return ContinuationRequest.new(resp_text, @str)
406
- else
407
- return ContinuationRequest.new(ResponseText.new(nil, ""), @str)
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 response_untagged
412
- match(T_STAR)
413
- match(T_SPACE)
414
- token = lookahead
415
- if token.symbol == T_NUMBER
416
- return numeric_response
417
- elsif token.symbol == T_ATOM
418
- case token.value
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
- else
451
- parse_error("unexpected token %s", token.symbol)
452
- end
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 = astring_chars
457
- match(T_SPACE)
458
- token = match(T_ATOM)
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
- def response_cond
465
- token = match(T_ATOM)
466
- name = token.value.upcase
467
- match(T_SPACE)
468
- return UntaggedResponse.new(name, resp_text, @str)
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
- def numeric_response
472
- n = number
473
- match(T_SPACE)
474
- token = match(T_ATOM)
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
- match(T_LPAR)
729
+ lpar
489
730
  attr = {}
490
731
  while true
491
- token = lookahead
492
- case token.symbol
493
- when T_RPAR
494
- shift_token
495
- break
496
- when T_SPACE
497
- shift_token
498
- next
499
- end
500
- case token.value
501
- when /\A(?:ENVELOPE)\z/ni
502
- name, val = envelope_data
503
- when /\A(?:FLAGS)\z/ni
504
- name, val = flags_data
505
- when /\A(?:INTERNALDATE)\z/ni
506
- name, val = internaldate_data
507
- when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni
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
- return attr
523
- end
524
-
525
- def envelope_data
526
- token = match(T_ATOM)
527
- name = token.value.upcase
528
- match(T_SPACE)
529
- return name, envelope
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
- def flags_data
568
- token = match(T_ATOM)
569
- name = token.value.upcase
570
- match(T_SPACE)
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 = String.new
877
- token = match(T_LBRA)
878
- str.concat(token.value)
879
- token = match(T_ATOM, T_NUMBER, T_RBRA)
880
- if token.symbol == T_RBRA
881
- str.concat(token.value)
882
- return str
883
- end
884
- str.concat(token.value)
885
- token = lookahead
886
- if token.symbol == T_SPACE
887
- shift_token
888
- str.concat(token.value)
889
- token = match(T_LPAR)
890
- str.concat(token.value)
891
- while true
892
- token = lookahead
893
- case token.symbol
894
- when T_RPAR
895
- str.concat(token.value)
896
- shift_token
897
- break
898
- when T_SPACE
899
- shift_token
900
- str.concat(token.value)
901
- end
902
- str.concat(format_string(astring))
903
- end
904
- end
905
- token = match(T_RBRA)
906
- str.concat(token.value)
907
- return str
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
- def format_string(str)
911
- case str
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
- def uid_data
927
- token = match(T_ATOM)
928
- name = token.value.upcase
929
- match(T_SPACE)
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 flags_response
958
- token = match(T_ATOM)
959
- name = token.value.upcase
960
- match(T_SPACE)
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 list_response
965
- token = match(T_ATOM)
966
- name = token.value.upcase
967
- match(T_SPACE)
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
- def getacl_response
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
- def search_response
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 thread_response
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 status_response
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 capapility) [SP]
1412
+ # *(SP capability) [SP]
1202
1413
  def capability__list
1203
- data = []; while _ = SP? && capability? do data << _ end; data
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
- # resp-text-code = "ALERT" /
1331
- # "BADCHARSET" [SP "(" charset *(SP charset) ")" ] /
1332
- # capability-data / "PARSE" /
1333
- # "PERMANENTFLAGS" SP "("
1334
- # [flag-perm *(SP flag-perm)] ")" /
1335
- # "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
1336
- # "UIDNEXT" SP nz-number / "UIDVALIDITY" SP nz-number /
1337
- # "UNSEEN" SP nz-number /
1338
- # atom [SP 1*<any TEXT-CHAR except "]">]
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
- # +UIDPLUS+ ABNF:: https://www.rfc-editor.org/rfc/rfc4315.html#section-4
1341
- # resp-text-code =/ resp-code-apnd / resp-code-copy / "UIDNOTSTICKY"
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
- token = match(T_ATOM)
1344
- name = token.value.upcase
1345
- case name
1346
- when /\A(?:ALERT|PARSE|READ-ONLY|READ-WRITE|TRYCREATE|NOMODSEQ)\z/n
1347
- result = ResponseCode.new(name, nil)
1348
- when /\A(?:BADCHARSET)\z/n
1349
- result = ResponseCode.new(name, charset_list)
1350
- when /\A(?:CAPABILITY)\z/ni
1351
- result = ResponseCode.new(name, capability__list)
1352
- when /\A(?:PERMANENTFLAGS)\z/n
1353
- match(T_SPACE)
1354
- result = ResponseCode.new(name, flag_list)
1355
- when /\A(?:UIDVALIDITY|UIDNEXT|UNSEEN)\z/n
1356
- match(T_SPACE)
1357
- result = ResponseCode.new(name, number)
1358
- when /\A(?:APPENDUID)\z/n
1359
- result = ResponseCode.new(name, resp_code_apnd__data)
1360
- when /\A(?:COPYUID)\z/n
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
- result = ResponseCode.new(name, nil)
1611
+ SP? and text_chars_except_rbra
1369
1612
  end
1370
- end
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
- def charset_list
1380
- result = []
1381
- if accept(T_SPACE)
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
- match(T_SPACE); validity = number
1406
- match(T_SPACE); dst_uids = uid_set # uniqueid ⊂ uid-set
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
- match(T_SPACE); validity = number
1415
- match(T_SPACE); src_uids = uid_set
1416
- match(T_SPACE); dst_uids = uid_set
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
- FLAG_REGEXP = /\
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
- if @str.index(/\(([^)]*)\)/ni, @pos)
1481
- @pos = $~.end(0)
1482
- return $1.scan(FLAG_REGEXP).collect { |flag, atom|
1483
- if atom
1484
- atom
1485
- else
1486
- flag.capitalize.intern
1487
- end
1488
- }
1489
- else
1490
- parse_error("invalid flag list")
1491
- end
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
- if token = accept(T_QUOTED)
1500
- token.value
1501
- else
1502
- atom
1503
- end
1504
- end
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
- shift_token if @token&.symbol == T_SPACE
1539
- if @str.index(SPACES_REGEXP, @pos)
1794
+ return false unless SP?
1795
+ @str.index(SPACES_REGEXP, @pos) and
1540
1796
  @pos = $~.end(0)
1541
- end
1797
+ true
1542
1798
  end
1543
1799
 
1544
1800
  def next_token