net-imap 0.4.1 → 0.4.4

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.

@@ -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,101 +1063,90 @@ 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
908
- end
909
-
910
- def format_string(str)
911
- case str
912
- when ""
913
- return '""'
914
- when /[\x80-\xff\r\n]/n
915
- # literal
916
- return "{" + str.bytesize.to_s + "}" + CRLF + str
917
- when /[(){ \x00-\x1f\x7f%*"\\]/n
918
- # quoted string
919
- return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"'
920
- else
921
- # atom
922
- return str
923
- end
924
- end
925
-
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)
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
948
1093
  end
949
1094
 
950
- def text_response
951
- token = match(T_ATOM)
952
- name = token.value.upcase
953
- match(T_SPACE)
954
- return UntaggedResponse.new(name, text)
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
955
1101
  end
956
1102
 
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)
962
- end
963
-
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)
969
- end
1103
+ # RFC3501 & RFC9051:
1104
+ # header-fld-name = astring
1105
+ #
1106
+ # NOTE: Previously, Net::IMAP recreated the raw original source string.
1107
+ # Now, it grabs the raw encoded value using @str and @pos. A future
1108
+ # version may simply return the decoded astring value. Although that is
1109
+ # technically incompatible, it should almost never make a difference: all
1110
+ # standard header field names are valid atoms:
1111
+ #
1112
+ # https://www.iana.org/assignments/message-headers/message-headers.xhtml
1113
+ #
1114
+ # Although RFC3501 allows any astring, RFC5322-valid header names are one
1115
+ # or more of the printable US-ASCII characters, except SP and colon. So
1116
+ # empty string isn't valid, and literals aren't needed and should not be
1117
+ # used. This is explicitly unchanged by [I18N-HDRS] (RFC6532).
1118
+ #
1119
+ # RFC5233:
1120
+ # optional-field = field-name ":" unstructured CRLF
1121
+ # field-name = 1*ftext
1122
+ # ftext = %d33-57 / ; Printable US-ASCII
1123
+ # %d59-126 ; characters not including
1124
+ # ; ":".
1125
+ def header_fld_name
1126
+ assert_no_lookahead
1127
+ start = @pos
1128
+ astring
1129
+ @str[start...@pos - 1]
1130
+ end
1131
+
1132
+ # mailbox-data = "FLAGS" SP flag-list / "LIST" SP mailbox-list /
1133
+ # "LSUB" SP mailbox-list / "SEARCH" *(SP nz-number) /
1134
+ # "STATUS" SP mailbox SP "(" [status-att-list] ")" /
1135
+ # number SP "EXISTS" / number SP "RECENT"
1136
+
1137
+ def mailbox_data__flags
1138
+ name = label("FLAGS")
1139
+ SP!
1140
+ UntaggedResponse.new(name, flag_list, @str)
1141
+ end
1142
+
1143
+ def mailbox_data__list
1144
+ name = label_in("LIST", "LSUB", "XLIST")
1145
+ SP!
1146
+ UntaggedResponse.new(name, mailbox_list, @str)
1147
+ end
1148
+ alias mailbox_data__lsub mailbox_data__list
1149
+ alias mailbox_data__xlist mailbox_data__list
970
1150
 
971
1151
  def mailbox_list
972
1152
  attr = flag_list
@@ -1032,7 +1212,8 @@ module Net
1032
1212
  return UntaggedResponse.new(name, data, @str)
1033
1213
  end
1034
1214
 
1035
- def getacl_response
1215
+ # acl-data = "ACL" SP mailbox *(SP identifier SP rights)
1216
+ def acl_data
1036
1217
  token = match(T_ATOM)
1037
1218
  name = token.value.upcase
1038
1219
  match(T_SPACE)
@@ -1058,7 +1239,21 @@ module Net
1058
1239
  return UntaggedResponse.new(name, data, @str)
1059
1240
  end
1060
1241
 
1061
- def search_response
1242
+ # RFC3501:
1243
+ # mailbox-data = "SEARCH" *(SP nz-number) / ...
1244
+ # RFC5256: SORT
1245
+ # sort-data = "SORT" *(SP nz-number)
1246
+ # RFC7162: CONDSTORE, QRESYNC
1247
+ # mailbox-data =/ "SEARCH" [1*(SP nz-number) SP
1248
+ # search-sort-mod-seq]
1249
+ # sort-data = "SORT" [1*(SP nz-number) SP
1250
+ # search-sort-mod-seq]
1251
+ # ; Updates the SORT response from RFC 5256.
1252
+ # search-sort-mod-seq = "(" "MODSEQ" SP mod-sequence-value ")"
1253
+ # RFC9051:
1254
+ # mailbox-data = obsolete-search-response / ...
1255
+ # obsolete-search-response = "SEARCH" *(SP nz-number)
1256
+ def mailbox_data__search
1062
1257
  token = match(T_ATOM)
1063
1258
  name = token.value.upcase
1064
1259
  token = lookahead
@@ -1088,8 +1283,9 @@ module Net
1088
1283
  end
1089
1284
  return UntaggedResponse.new(name, data, @str)
1090
1285
  end
1286
+ alias sort_data mailbox_data__search
1091
1287
 
1092
- def thread_response
1288
+ def thread_data
1093
1289
  token = match(T_ATOM)
1094
1290
  name = token.value.upcase
1095
1291
  token = lookahead
@@ -1151,7 +1347,7 @@ module Net
1151
1347
  return rootmember
1152
1348
  end
1153
1349
 
1154
- def status_response
1350
+ def mailbox_data__status
1155
1351
  token = match(T_ATOM)
1156
1352
  name = token.value.upcase
1157
1353
  match(T_SPACE)
@@ -1198,11 +1394,13 @@ module Net
1198
1394
  end
1199
1395
 
1200
1396
  # As a workaround for buggy servers, allow a trailing SP:
1201
- # *(SP capapility) [SP]
1397
+ # *(SP capability) [SP]
1202
1398
  def capability__list
1203
- data = []; while _ = SP? && capability? do data << _ end; data
1399
+ list = []; while SP? && (capa = capability?) do list << capa end; list
1204
1400
  end
1205
1401
 
1402
+ alias resp_code__capability capability__list
1403
+
1206
1404
  # capability = ("AUTH=" auth-type) / atom
1207
1405
  # ; New capabilities MUST begin with "X" or be
1208
1406
  # ; registered with IANA as standard or
@@ -1325,68 +1523,91 @@ module Net
1325
1523
  end
1326
1524
  end
1327
1525
 
1328
- # See https://www.rfc-editor.org/errata/rfc3501
1526
+ # RFC3501 (See https://www.rfc-editor.org/errata/rfc3501):
1527
+ # resp-text-code = "ALERT" /
1528
+ # "BADCHARSET" [SP "(" charset *(SP charset) ")" ] /
1529
+ # capability-data / "PARSE" /
1530
+ # "PERMANENTFLAGS" SP "(" [flag-perm *(SP flag-perm)] ")" /
1531
+ # "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
1532
+ # "UIDNEXT" SP nz-number / "UIDVALIDITY" SP nz-number /
1533
+ # "UNSEEN" SP nz-number /
1534
+ # atom [SP 1*<any TEXT-CHAR except "]">]
1535
+ # capability-data = "CAPABILITY" *(SP capability) SP "IMAP4rev1"
1536
+ # *(SP capability)
1329
1537
  #
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 "]">]
1538
+ # RFC5530:
1539
+ # resp-text-code =/ "UNAVAILABLE" / "AUTHENTICATIONFAILED" /
1540
+ # "AUTHORIZATIONFAILED" / "EXPIRED" /
1541
+ # "PRIVACYREQUIRED" / "CONTACTADMIN" / "NOPERM" /
1542
+ # "INUSE" / "EXPUNGEISSUED" / "CORRUPTION" /
1543
+ # "SERVERBUG" / "CLIENTBUG" / "CANNOT" /
1544
+ # "LIMIT" / "OVERQUOTA" / "ALREADYEXISTS" /
1545
+ # "NONEXISTENT"
1546
+ # RFC9051:
1547
+ # resp-text-code = "ALERT" /
1548
+ # "BADCHARSET" [SP "(" charset *(SP charset) ")" ] /
1549
+ # capability-data / "PARSE" /
1550
+ # "PERMANENTFLAGS" SP "(" [flag-perm *(SP flag-perm)] ")" /
1551
+ # "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
1552
+ # "UIDNEXT" SP nz-number / "UIDVALIDITY" SP nz-number /
1553
+ # resp-code-apnd / resp-code-copy / "UIDNOTSTICKY" /
1554
+ # "UNAVAILABLE" / "AUTHENTICATIONFAILED" /
1555
+ # "AUTHORIZATIONFAILED" / "EXPIRED" /
1556
+ # "PRIVACYREQUIRED" / "CONTACTADMIN" / "NOPERM" /
1557
+ # "INUSE" / "EXPUNGEISSUED" / "CORRUPTION" /
1558
+ # "SERVERBUG" / "CLIENTBUG" / "CANNOT" /
1559
+ # "LIMIT" / "OVERQUOTA" / "ALREADYEXISTS" /
1560
+ # "NONEXISTENT" / "NOTSAVED" / "HASCHILDREN" /
1561
+ # "CLOSED" /
1562
+ # "UNKNOWN-CTE" /
1563
+ # atom [SP 1*<any TEXT-CHAR except "]">]
1564
+ # capability-data = "CAPABILITY" *(SP capability) SP "IMAP4rev2"
1565
+ # *(SP capability)
1339
1566
  #
1340
- # +UIDPLUS+ ABNF:: https://www.rfc-editor.org/rfc/rfc4315.html#section-4
1341
- # resp-text-code =/ resp-code-apnd / resp-code-copy / "UIDNOTSTICKY"
1567
+ # RFC4315 (UIDPLUS), RFC9051 (IMAP4rev2):
1568
+ # resp-code-apnd = "APPENDUID" SP nz-number SP append-uid
1569
+ # resp-code-copy = "COPYUID" SP nz-number SP uid-set SP uid-set
1570
+ # resp-text-code =/ resp-code-apnd / resp-code-copy / "UIDNOTSTICKY"
1571
+ #
1572
+ # RFC7162 (CONDSTORE):
1573
+ # resp-text-code =/ "HIGHESTMODSEQ" SP mod-sequence-value /
1574
+ # "NOMODSEQ" /
1575
+ # "MODIFIED" SP sequence-set
1342
1576
  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)
1577
+ name = resp_text_code__name
1578
+ data =
1579
+ case name
1580
+ when "CAPABILITY" then resp_code__capability
1581
+ when "PERMANENTFLAGS" then SP? ? flag_perm__list : []
1582
+ when "UIDNEXT" then SP!; nz_number
1583
+ when "UIDVALIDITY" then SP!; nz_number
1584
+ when "UNSEEN" then SP!; nz_number # rev1 only
1585
+ when "APPENDUID" then SP!; resp_code_apnd__data # rev2, UIDPLUS
1586
+ when "COPYUID" then SP!; resp_code_copy__data # rev2, UIDPLUS
1587
+ when "BADCHARSET" then SP? ? charset__list : []
1588
+ when "ALERT", "PARSE", "READ-ONLY", "READ-WRITE", "TRYCREATE",
1589
+ "UNAVAILABLE", "AUTHENTICATIONFAILED", "AUTHORIZATIONFAILED",
1590
+ "EXPIRED", "PRIVACYREQUIRED", "CONTACTADMIN", "NOPERM", "INUSE",
1591
+ "EXPUNGEISSUED", "CORRUPTION", "SERVERBUG", "CLIENTBUG", "CANNOT",
1592
+ "LIMIT", "OVERQUOTA", "ALREADYEXISTS", "NONEXISTENT", "CLOSED",
1593
+ "NOTSAVED", "UIDNOTSTICKY", "UNKNOWN-CTE", "HASCHILDREN"
1594
+ when "NOMODSEQ" # CONDSTORE
1367
1595
  else
1368
- result = ResponseCode.new(name, nil)
1596
+ SP? and text_chars_except_rbra
1369
1597
  end
1370
- end
1371
- return result
1598
+ ResponseCode.new(name, data)
1372
1599
  end
1373
1600
 
1601
+ alias resp_text_code__name case_insensitive__atom
1602
+
1374
1603
  # 1*<any TEXT-CHAR except "]">
1375
1604
  def text_chars_except_rbra
1376
1605
  match_re(CTEXT_REGEXP, '1*<any TEXT-CHAR except "]">')[0]
1377
1606
  end
1378
1607
 
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
1608
+ # "(" charset *(SP charset) ")"
1609
+ def charset__list
1610
+ lpar; list = [charset]; while SP? do list << charset end; rpar; list
1390
1611
  end
1391
1612
 
1392
1613
  # already matched: "APPENDUID"
@@ -1402,8 +1623,8 @@ module Net
1402
1623
  # match uid_set even if that returns a single-member array.
1403
1624
  #
1404
1625
  def resp_code_apnd__data
1405
- match(T_SPACE); validity = number
1406
- match(T_SPACE); dst_uids = uid_set # uniqueid ⊂ uid-set
1626
+ validity = number; SP!
1627
+ dst_uids = uid_set # uniqueid ⊂ uid-set
1407
1628
  UIDPlusData.new(validity, nil, dst_uids)
1408
1629
  end
1409
1630
 
@@ -1411,9 +1632,9 @@ module Net
1411
1632
  #
1412
1633
  # resp-code-copy = "COPYUID" SP nz-number SP uid-set SP uid-set
1413
1634
  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
1635
+ validity = number; SP!
1636
+ src_uids = uid_set; SP!
1637
+ dst_uids = uid_set
1417
1638
  UIDPlusData.new(validity, src_uids, dst_uids)
1418
1639
  end
1419
1640
 
@@ -1472,36 +1693,56 @@ module Net
1472
1693
  return Address.new(name, route, mailbox, host)
1473
1694
  end
1474
1695
 
1475
- FLAG_REGEXP = /\
1476
- (?# FLAG )\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)|\
1477
- (?# ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\]+)/n
1478
-
1696
+ # flag-list = "(" [flag *(SP flag)] ")"
1479
1697
  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
1698
+ match_re(Patterns::FLAG_LIST, "flag-list")[1]
1699
+ .split(nil)
1700
+ .map! { _1.start_with?("\\") ? _1[1..].capitalize.to_sym : _1 }
1701
+ end
1702
+
1703
+ # "(" [flag-perm *(SP flag-perm)] ")"
1704
+ def flag_perm__list
1705
+ match_re(Patterns::FLAG_PERM_LIST, "PERMANENTFLAGS flag-perm list")[1]
1706
+ .split(nil)
1707
+ .map! { _1.start_with?("\\") ? _1[1..].capitalize.to_sym : _1 }
1708
+ end
1709
+
1710
+ # Not checking for max one mbx-list-sflag in the parser.
1711
+ # >>>
1712
+ # mbx-list-flags = *(mbx-list-oflag SP) mbx-list-sflag
1713
+ # *(SP mbx-list-oflag) /
1714
+ # mbx-list-oflag *(SP mbx-list-oflag)
1715
+ # mbx-list-oflag = "\Noinferiors" / child-mbox-flag /
1716
+ # "\Subscribed" / "\Remote" / flag-extension
1717
+ # ; Other flags; multiple from this list are
1718
+ # ; possible per LIST response, but each flag
1719
+ # ; can only appear once per LIST response
1720
+ # mbx-list-sflag = "\NonExistent" / "\Noselect" / "\Marked" /
1721
+ # "\Unmarked"
1722
+ # ; Selectability flags; only one per LIST response
1723
+ def parens__mbx_list_flags
1724
+ match_re(Patterns::MBX_LIST_FLAGS, "mbx-list-flags")[1]
1725
+ .split(nil).map! { _1.capitalize.to_sym }
1492
1726
  end
1493
1727
 
1494
-
1495
1728
  # See https://www.rfc-editor.org/errata/rfc3501
1496
1729
  #
1497
1730
  # charset = atom / quoted
1498
- def charset
1499
- if token = accept(T_QUOTED)
1500
- token.value
1501
- else
1502
- atom
1503
- end
1504
- end
1731
+ def charset; quoted? || atom end
1732
+
1733
+ # RFC7162:
1734
+ # mod-sequence-value = 1*DIGIT
1735
+ # ;; Positive unsigned 63-bit integer
1736
+ # ;; (mod-sequence)
1737
+ # ;; (1 <= n <= 9,223,372,036,854,775,807).
1738
+ alias mod_sequence_value nz_number64
1739
+
1740
+ # RFC7162:
1741
+ # permsg-modsequence = mod-sequence-value
1742
+ # ;; Per-message mod-sequence.
1743
+ alias permsg_modsequence mod_sequence_value
1744
+
1745
+ def parens__modseq; lpar; _ = permsg_modsequence; rpar; _ end
1505
1746
 
1506
1747
  # RFC-4315 (UIDPLUS) or RFC9051 (IMAP4rev2):
1507
1748
  # uid-set = (uniqueid / uid-range) *("," uid-set)
@@ -1535,10 +1776,10 @@ module Net
1535
1776
  #
1536
1777
  # This advances @pos directly so it's safe before changing @lex_state.
1537
1778
  def accept_spaces
1538
- shift_token if @token&.symbol == T_SPACE
1539
- if @str.index(SPACES_REGEXP, @pos)
1779
+ return false unless SP?
1780
+ @str.index(SPACES_REGEXP, @pos) and
1540
1781
  @pos = $~.end(0)
1541
- end
1782
+ true
1542
1783
  end
1543
1784
 
1544
1785
  def next_token