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.

@@ -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