net-imap 0.4.2 → 0.4.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -54,10 +54,26 @@ module Net
54
54
  T_STAR = :STAR # atom special; list wildcard
55
55
  T_PERCENT = :PERCENT # atom special; list wildcard
56
56
  T_LITERAL = :LITERAL # starts with atom special
57
+ T_LITERAL8 = :LITERAL8 # starts with atom char "~"
57
58
  T_CRLF = :CRLF # atom special; text special; quoted special
58
59
  T_TEXT = :TEXT # any char except CRLF
59
60
  T_EOF = :EOF # end of response string
60
61
 
62
+ module ResponseConditions
63
+ OK = "OK"
64
+ NO = "NO"
65
+ BAD = "BAD"
66
+ BYE = "BYE"
67
+ PREAUTH = "PREAUTH"
68
+
69
+ RESP_COND_STATES = [OK, NO, BAD ].freeze
70
+ RESP_DATA_CONDS = [OK, NO, BAD, BYE, ].freeze
71
+ AUTH_CONDS = [OK, PREAUTH].freeze
72
+ GREETING_CONDS = [OK, BYE, PREAUTH].freeze
73
+ RESP_CONDS = [OK, NO, BAD, BYE, PREAUTH].freeze
74
+ end
75
+ include ResponseConditions
76
+
61
77
  module Patterns
62
78
 
63
79
  module CharClassSubtraction
@@ -170,6 +186,54 @@ module Net
170
186
  CODE_TEXT_CHAR = TEXT_CHAR - RESP_SPECIALS
171
187
  CODE_TEXT = /#{CODE_TEXT_CHAR}+/n
172
188
 
189
+ # flag = "\Answered" / "\Flagged" / "\Deleted" /
190
+ # "\Seen" / "\Draft" / flag-keyword / flag-extension
191
+ # ; Does not include "\Recent"
192
+ # flag-extension = "\" atom
193
+ # ; Future expansion. Client implementations
194
+ # ; MUST accept flag-extension flags. Server
195
+ # ; implementations MUST NOT generate
196
+ # ; flag-extension flags except as defined by
197
+ # ; a future Standard or Standards Track
198
+ # ; revisions of this specification.
199
+ # flag-keyword = "$MDNSent" / "$Forwarded" / "$Junk" /
200
+ # "$NotJunk" / "$Phishing" / atom
201
+ # flag-perm = flag / "\*"
202
+ #
203
+ # Not checking for max one mbx-list-sflag in the parser.
204
+ # >>>
205
+ # mbx-list-oflag = "\Noinferiors" / child-mbox-flag /
206
+ # "\Subscribed" / "\Remote" / flag-extension
207
+ # ; Other flags; multiple from this list are
208
+ # ; possible per LIST response, but each flag
209
+ # ; can only appear once per LIST response
210
+ # mbx-list-sflag = "\NonExistent" / "\Noselect" / "\Marked" /
211
+ # "\Unmarked"
212
+ # ; Selectability flags; only one per LIST response
213
+ # child-mbox-flag = "\HasChildren" / "\HasNoChildren"
214
+ # ; attributes for the CHILDREN return option, at most
215
+ # ; one possible per LIST response
216
+ FLAG = /\\?#{ATOM}/n
217
+ FLAG_EXTENSION = /\\#{ATOM}/n
218
+ FLAG_KEYWORD = ATOM
219
+ FLAG_PERM = Regexp.union(FLAG, "\\*")
220
+ MBX_FLAG = FLAG_EXTENSION
221
+
222
+ # flag-list = "(" [flag *(SP flag)] ")"
223
+ #
224
+ # part of resp-text-code:
225
+ # >>>
226
+ # "PERMANENTFLAGS" SP "(" [flag-perm *(SP flag-perm)] ")"
227
+ #
228
+ # parens from mailbox-list are included in the regexp:
229
+ # >>>
230
+ # mbx-list-flags = *(mbx-list-oflag SP) mbx-list-sflag
231
+ # *(SP mbx-list-oflag) /
232
+ # mbx-list-oflag *(SP mbx-list-oflag)
233
+ FLAG_LIST = /\G\((#{FLAG }(?:#{SP}#{FLAG })*|)\)/ni
234
+ FLAG_PERM_LIST = /\G\((#{FLAG_PERM}(?:#{SP}#{FLAG_PERM})*|)\)/ni
235
+ MBX_LIST_FLAGS = /\G\((#{MBX_FLAG }(?:#{SP}#{MBX_FLAG })*|)\)/ni
236
+
173
237
  # RFC3501:
174
238
  # QUOTED-CHAR = <any TEXT-CHAR except quoted-specials> /
175
239
  # "\" quoted-specials
@@ -195,6 +259,64 @@ module Net
195
259
  TEXT_rev1 = /#{TEXT_CHAR}+/
196
260
  TEXT_rev2 = /#{Regexp.union TEXT_CHAR, UTF8_2, UTF8_3, UTF8_4}+/
197
261
 
262
+ # tagged-label-fchar = ALPHA / "-" / "_" / "."
263
+ TAGGED_LABEL_FCHAR = /[a-zA-Z\-_.]/n
264
+ # tagged-label-char = tagged-label-fchar / DIGIT / ":"
265
+ TAGGED_LABEL_CHAR = /[a-zA-Z\-_.0-9:]*/n
266
+ # tagged-ext-label = tagged-label-fchar *tagged-label-char
267
+ # ; Is a valid RFC 3501 "atom".
268
+ TAGGED_EXT_LABEL = /#{TAGGED_LABEL_FCHAR}#{TAGGED_LABEL_CHAR}*/n
269
+
270
+ # nz-number = digit-nz *DIGIT
271
+ # ; Non-zero unsigned 32-bit integer
272
+ # ; (0 < n < 4,294,967,296)
273
+ NZ_NUMBER = /[1-9]\d*/n
274
+
275
+ # seq-number = nz-number / "*"
276
+ # ; message sequence number (COPY, FETCH, STORE
277
+ # ; commands) or unique identifier (UID COPY,
278
+ # ; UID FETCH, UID STORE commands).
279
+ # ; * represents the largest number in use. In
280
+ # ; the case of message sequence numbers, it is
281
+ # ; the number of messages in a non-empty mailbox.
282
+ # ; In the case of unique identifiers, it is the
283
+ # ; unique identifier of the last message in the
284
+ # ; mailbox or, if the mailbox is empty, the
285
+ # ; mailbox's current UIDNEXT value.
286
+ # ; The server should respond with a tagged BAD
287
+ # ; response to a command that uses a message
288
+ # ; sequence number greater than the number of
289
+ # ; messages in the selected mailbox. This
290
+ # ; includes "*" if the selected mailbox is empty.
291
+ SEQ_NUMBER = /#{NZ_NUMBER}|\*/n
292
+
293
+ # seq-range = seq-number ":" seq-number
294
+ # ; two seq-number values and all values between
295
+ # ; these two regardless of order.
296
+ # ; Example: 2:4 and 4:2 are equivalent and
297
+ # ; indicate values 2, 3, and 4.
298
+ # ; Example: a unique identifier sequence range of
299
+ # ; 3291:* includes the UID of the last message in
300
+ # ; the mailbox, even if that value is less than
301
+ # ; 3291.
302
+ SEQ_RANGE = /#{SEQ_NUMBER}:#{SEQ_NUMBER}/n
303
+
304
+ # sequence-set = (seq-number / seq-range) ["," sequence-set]
305
+ # ; set of seq-number values, regardless of order.
306
+ # ; Servers MAY coalesce overlaps and/or execute
307
+ # ; the sequence in any order.
308
+ # ; Example: a message sequence number set of
309
+ # ; 2,4:7,9,12:* for a mailbox with 15 messages is
310
+ # ; equivalent to 2,4,5,6,7,9,12,13,14,15
311
+ # ; Example: a message sequence number set of
312
+ # ; *:4,5:7 for a mailbox with 10 messages is
313
+ # ; equivalent to 10,9,8,7,6,5,4,5,6,7 and MAY
314
+ # ; be reordered and overlap coalesced to be
315
+ # ; 4,5,6,7,8,9,10.
316
+ SEQUENCE_SET_ITEM = /#{SEQ_NUMBER}|#{SEQ_RANGE}/n
317
+ SEQUENCE_SET = /#{SEQUENCE_SET_ITEM}(?:,#{SEQUENCE_SET_ITEM})*/n
318
+ SEQUENCE_SET_STR = /\A#{SEQUENCE_SET}\z/n
319
+
198
320
  # RFC3501:
199
321
  # literal = "{" number "}" CRLF *CHAR8
200
322
  # ; Number represents the number of CHAR8s
@@ -208,6 +330,16 @@ module Net
208
330
  # ; sent from server to the client.
209
331
  LITERAL = /\{(\d+)\}\r\n/n
210
332
 
333
+ # RFC3516 (BINARY):
334
+ # literal8 = "~{" number "}" CRLF *OCTET
335
+ # ; <number> represents the number of OCTETs
336
+ # ; in the response string.
337
+ # RFC9051:
338
+ # literal8 = "~{" number64 "}" CRLF *OCTET
339
+ # ; <number64> represents the number of OCTETs
340
+ # ; in the response string.
341
+ LITERAL8 = /~\{(\d+)\}\r\n/n
342
+
211
343
  module_function
212
344
 
213
345
  def unescape_quoted!(quoted)
@@ -227,27 +359,28 @@ module Net
227
359
  # the default, used in most places
228
360
  BEG_REGEXP = /\G(?:\
229
361
  (?# 1: SPACE )( )|\
230
- (?# 2: ATOM prefixed with a compatible subtype)\
362
+ (?# 2: LITERAL8)#{Patterns::LITERAL8}|\
363
+ (?# 3: ATOM prefixed with a compatible subtype)\
231
364
  ((?:\
232
- (?# 3: NIL )(NIL)|\
233
- (?# 4: NUMBER )(\d+)|\
234
- (?# 5: PLUS )(\+))\
235
- (?# 6: ATOM remaining after prefix )(#{Patterns::ATOMISH})?\
365
+ (?# 4: NIL )(NIL)|\
366
+ (?# 5: NUMBER )(\d+)|\
367
+ (?# 6: PLUS )(\+))\
368
+ (?# 7: ATOM remaining after prefix )(#{Patterns::ATOMISH})?\
236
369
  (?# This enables greedy alternation without lookahead, in linear time.)\
237
370
  )|\
238
371
  (?# Also need to check for ATOM without a subtype prefix.)\
239
- (?# 7: ATOM )(#{Patterns::ATOMISH})|\
240
- (?# 8: QUOTED )#{Patterns::QUOTED_rev2}|\
241
- (?# 9: LPAR )(\()|\
242
- (?# 10: RPAR )(\))|\
243
- (?# 11: BSLASH )(\\)|\
244
- (?# 12: STAR )(\*)|\
245
- (?# 13: LBRA )(\[)|\
246
- (?# 14: RBRA )(\])|\
247
- (?# 15: LITERAL )#{Patterns::LITERAL}|\
248
- (?# 16: PERCENT )(%)|\
249
- (?# 17: CRLF )(\r\n)|\
250
- (?# 18: EOF )(\z))/ni
372
+ (?# 8: ATOM )(#{Patterns::ATOMISH})|\
373
+ (?# 9: QUOTED )#{Patterns::QUOTED_rev2}|\
374
+ (?# 10: LPAR )(\()|\
375
+ (?# 11: RPAR )(\))|\
376
+ (?# 12: BSLASH )(\\)|\
377
+ (?# 13: STAR )(\*)|\
378
+ (?# 14: LBRA )(\[)|\
379
+ (?# 15: RBRA )(\])|\
380
+ (?# 16: LITERAL )#{Patterns::LITERAL}|\
381
+ (?# 17: PERCENT )(%)|\
382
+ (?# 18: CRLF )(\r\n)|\
383
+ (?# 19: EOF )(\z))/ni
251
384
 
252
385
  # envelope, body(structure), namespaces
253
386
  DATA_REGEXP = /\G(?:\
@@ -268,6 +401,8 @@ module Net
268
401
  Token = Struct.new(:symbol, :value)
269
402
 
270
403
  def_char_matchers :SP, " ", :T_SPACE
404
+ def_char_matchers :PLUS, "+", :T_PLUS
405
+ def_char_matchers :STAR, "*", :T_STAR
271
406
 
272
407
  def_char_matchers :lpar, "(", :T_LPAR
273
408
  def_char_matchers :rpar, ")", :T_RPAR
@@ -286,6 +421,9 @@ module Net
286
421
  # string = quoted / literal
287
422
  def_token_matchers :string, T_QUOTED, T_LITERAL
288
423
 
424
+ # used by nstring8 = nstring / literal8
425
+ def_token_matchers :string8, T_QUOTED, T_LITERAL, T_LITERAL8
426
+
289
427
  # use where string represents "LABEL" values
290
428
  def_token_matchers :case_insensitive__string,
291
429
  T_QUOTED, T_LITERAL,
@@ -310,20 +448,44 @@ module Net
310
448
  # TODO: add to lexer and only match tagged-ext-label
311
449
  def_token_matchers :tagged_ext_label, T_ATOM, T_NIL, send: :upcase
312
450
 
451
+ def_token_matchers :CRLF, T_CRLF
452
+ def_token_matchers :EOF, T_EOF
453
+
313
454
  # atom = 1*ATOM-CHAR
314
455
  # ATOM-CHAR = <any CHAR except atom-specials>
315
456
  ATOM_TOKENS = [T_ATOM, T_NUMBER, T_NIL, T_LBRA, T_PLUS]
316
457
 
458
+ SEQUENCE_SET_TOKENS = [T_ATOM, T_NUMBER, T_STAR]
459
+
460
+ # sequence-set = (seq-number / seq-range) ["," sequence-set]
461
+ # sequence-set =/ seq-last-command
462
+ # ; Allow for "result of the last command"
463
+ # ; indicator.
464
+ # seq-last-command = "$"
465
+ #
466
+ # *note*: doesn't match seq-last-command
467
+ def sequence_set
468
+ str = combine_adjacent(*SEQUENCE_SET_TOKENS)
469
+ if Patterns::SEQUENCE_SET_STR.match?(str)
470
+ SequenceSet.new(str)
471
+ else
472
+ parse_error("unexpected atom %p, expected sequence-set", str)
473
+ end
474
+ end
475
+
317
476
  # ASTRING-CHAR = ATOM-CHAR / resp-specials
318
477
  # resp-specials = "]"
319
478
  ASTRING_CHARS_TOKENS = [*ATOM_TOKENS, T_RBRA].freeze
320
479
 
321
480
  ASTRING_TOKENS = [T_QUOTED, *ASTRING_CHARS_TOKENS, T_LITERAL].freeze
322
481
 
323
- # atom = 1*ATOM-CHAR
324
- #
325
- # TODO: match atom entirely by regexp (in the "lexer")
326
- def atom; -combine_adjacent(*ATOM_TOKENS) end
482
+ # tag = 1*<any ASTRING-CHAR except "+">
483
+ TAG_TOKENS = (ASTRING_CHARS_TOKENS - [T_PLUS]).freeze
484
+
485
+ # TODO: handle atom, astring_chars, and tag entirely inside the lexer
486
+ def atom; combine_adjacent(*ATOM_TOKENS) end
487
+ def astring_chars; combine_adjacent(*ASTRING_CHARS_TOKENS) end
488
+ def tag; combine_adjacent(*TAG_TOKENS) end
327
489
 
328
490
  # the #accept version of #atom
329
491
  def atom?; -combine_adjacent(*ATOM_TOKENS) if lookahead?(*ATOM_TOKENS) end
@@ -336,11 +498,6 @@ module Net
336
498
  -combine_adjacent(*ATOM_TOKENS).upcase if lookahead?(*ATOM_TOKENS)
337
499
  end
338
500
 
339
- # TODO: handle astring_chars entirely inside the lexer
340
- def astring_chars
341
- combine_adjacent(*ASTRING_CHARS_TOKENS)
342
- end
343
-
344
501
  # astring = 1*ASTRING-CHAR / string
345
502
  def astring
346
503
  lookahead?(*ASTRING_CHARS_TOKENS) ? astring_chars : string
@@ -357,11 +514,39 @@ module Net
357
514
  parse_error("unexpected atom %p, expected %p instead", val, word)
358
515
  end
359
516
 
517
+ # Use #label or #label_in to assert specific known labels
518
+ # (+tagged-ext-label+ only, not +atom+).
519
+ def label_in(*labels)
520
+ lbl = tagged_ext_label and labels.include?(lbl) and return lbl
521
+ parse_error("unexpected atom %p, expected one of %s instead",
522
+ lbl, labels.join(" or "))
523
+ end
524
+
525
+ # expects "OK" or "PREAUTH" and raises InvalidResponseError on failure
526
+ def resp_cond_auth__name
527
+ lbl = tagged_ext_label and AUTH_CONDS.include? lbl and return lbl
528
+ raise InvalidResponseError, "bad response type %p, expected %s" % [
529
+ lbl, AUTH_CONDS.join(" or ")
530
+ ]
531
+ end
532
+
533
+ # expects "OK" or "NO" or "BAD" and raises InvalidResponseError on failure
534
+ def resp_cond_state__name
535
+ lbl = tagged_ext_label and RESP_COND_STATES.include? lbl and return lbl
536
+ raise InvalidResponseError, "bad response type %p, expected %s" % [
537
+ lbl, RESP_COND_STATES.join(" or ")
538
+ ]
539
+ end
540
+
360
541
  # nstring = string / nil
361
542
  def nstring
362
543
  NIL? ? nil : string
363
544
  end
364
545
 
546
+ def nstring8
547
+ NIL? ? nil : string8
548
+ end
549
+
365
550
  def nquoted
366
551
  NIL? ? nil : quoted
367
552
  end
@@ -371,6 +556,60 @@ module Net
371
556
  NIL? ? nil : case_insensitive__string
372
557
  end
373
558
 
559
+ # tagged-ext-comp = astring /
560
+ # tagged-ext-comp *(SP tagged-ext-comp) /
561
+ # "(" tagged-ext-comp ")"
562
+ # ; Extensions that follow this general
563
+ # ; syntax should use nstring instead of
564
+ # ; astring when appropriate in the context
565
+ # ; of the extension.
566
+ # ; Note that a message set or a "number"
567
+ # ; can always be represented as an "atom".
568
+ # ; A URL should be represented as
569
+ # ; a "quoted" string.
570
+ def tagged_ext_comp
571
+ vals = []
572
+ while true
573
+ vals << case lookahead!(*ASTRING_TOKENS, T_LPAR).symbol
574
+ when T_LPAR then lpar; ary = tagged_ext_comp; rpar; ary
575
+ when T_NUMBER then number
576
+ else astring
577
+ end
578
+ SP? or break
579
+ end
580
+ vals
581
+ end
582
+
583
+ # tagged-ext-simple is a subset of atom
584
+ # TODO: recognize sequence-set in the lexer
585
+ #
586
+ # tagged-ext-simple = sequence-set / number / number64
587
+ def tagged_ext_simple
588
+ number? || sequence_set
589
+ end
590
+
591
+ # tagged-ext-val = tagged-ext-simple /
592
+ # "(" [tagged-ext-comp] ")"
593
+ def tagged_ext_val
594
+ if lpar?
595
+ _ = peek_rpar? ? [] : tagged_ext_comp
596
+ rpar
597
+ _
598
+ else
599
+ tagged_ext_simple
600
+ end
601
+ end
602
+
603
+ # mailbox = "INBOX" / astring
604
+ # ; INBOX is case-insensitive. All case variants of
605
+ # ; INBOX (e.g., "iNbOx") MUST be interpreted as INBOX
606
+ # ; not as an astring. An astring which consists of
607
+ # ; the case-insensitive sequence "I" "N" "B" "O" "X"
608
+ # ; is considered to be INBOX and not an astring.
609
+ # ; Refer to section 5.1 for further
610
+ # ; semantic details of mailbox names.
611
+ alias mailbox astring
612
+
374
613
  # valid number ranges are not enforced by parser
375
614
  # number64 = 1*DIGIT
376
615
  # ; Unsigned 63-bit integer
@@ -378,156 +617,316 @@ module Net
378
617
  alias number64 number
379
618
  alias number64? number?
380
619
 
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
620
+ # valid number ranges are not enforced by parser
621
+ # nz-number = digit-nz *DIGIT
622
+ # ; Non-zero unsigned 32-bit integer
623
+ # ; (0 < n < 4,294,967,296)
624
+ alias nz_number number
625
+ alias nz_number? number?
399
626
 
627
+ # valid number ranges are not enforced by parser
628
+ # nz-number64 = digit-nz *DIGIT
629
+ # ; Unsigned 63-bit integer
630
+ # ; (0 < n <= 9,223,372,036,854,775,807)
631
+ alias nz_number64 nz_number
632
+
633
+ # valid number ranges are not enforced by parser
634
+ # uniqueid = nz-number
635
+ # ; Strictly ascending
636
+ alias uniqueid nz_number
637
+
638
+ # valid number ranges are not enforced by parser
639
+ #
640
+ # a 64-bit unsigned integer and is the decimal equivalent for the ID hex
641
+ # string used in the web interface and the Gmail API.
642
+ alias x_gm_id number
643
+
644
+ # [RFC3501 & RFC9051:]
645
+ # response = *(continue-req / response-data) response-done
646
+ #
647
+ # For simplicity, response isn't interpreted as the combination of the
648
+ # three response types, but instead represents any individual server
649
+ # response. Our simplified interpretation is defined as:
650
+ # response = continue-req | response_data | response-tagged
651
+ #
652
+ # n.b: our "response-tagged" definition parses "greeting" too.
653
+ def response
654
+ resp = case lookahead!(T_PLUS, T_STAR, *TAG_TOKENS).symbol
655
+ when T_PLUS then continue_req
656
+ when T_STAR then response_data
657
+ else response_tagged
658
+ end
659
+ accept_spaces # QUIRKY: Ignore trailing space (MS Exchange Server?)
660
+ CRLF!
661
+ EOF!
662
+ resp
663
+ end
664
+
665
+ # RFC3501 & RFC9051:
666
+ # continue-req = "+" SP (resp-text / base64) CRLF
667
+ #
668
+ # n.b: base64 is valid resp-text. And in the spirit of RFC9051 Appx E 23
669
+ # (and to workaround existing servers), we use the following grammar:
670
+ #
671
+ # continue-req = "+" (SP (resp-text)) CRLF
400
672
  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)
673
+ PLUS!
674
+ ContinuationRequest.new(SP? ? resp_text : ResponseText::EMPTY, @str)
675
+ end
676
+
677
+ RE_RESPONSE_TYPE = /\G(?:\d+ )?(?<type>#{Patterns::TAGGED_EXT_LABEL})/n
678
+
679
+ # [RFC3501:]
680
+ # response-data = "*" SP (resp-cond-state / resp-cond-bye /
681
+ # mailbox-data / message-data / capability-data) CRLF
682
+ # [RFC4466:]
683
+ # response-data = "*" SP response-payload CRLF
684
+ # response-payload = resp-cond-state / resp-cond-bye /
685
+ # mailbox-data / message-data / capability-data
686
+ # RFC5161 (ENABLE capability):
687
+ # response-data =/ "*" SP enable-data CRLF
688
+ # RFC5255 (LANGUAGE capability)
689
+ # response-payload =/ language-data
690
+ # RFC5255 (I18NLEVEL=1 and I18NLEVEL=2 capabilities)
691
+ # response-payload =/ comparator-data
692
+ # [RFC9051:]
693
+ # response-data = "*" SP (resp-cond-state / resp-cond-bye /
694
+ # mailbox-data / message-data / capability-data /
695
+ # enable-data) CRLF
696
+ #
697
+ # [merging in greeting and response-fatal:]
698
+ # greeting = "*" SP (resp-cond-auth / resp-cond-bye) CRLF
699
+ # response-fatal = "*" SP resp-cond-bye CRLF
700
+ # response-data =/ "*" SP (resp-cond-auth / resp-cond-bye) CRLF
701
+ # [removing duplicates, this is simply]
702
+ # response-payload =/ resp-cond-auth
703
+ #
704
+ # TODO: remove resp-cond-auth and handle greeting separately
705
+ def response_data
706
+ STAR!; SP!
707
+ m = peek_re(RE_RESPONSE_TYPE) or parse_error("unparsable response")
708
+ case m["type"].upcase
709
+ when "OK" then resp_cond_state__untagged # RFC3501, RFC9051
710
+ when "FETCH" then message_data__fetch # RFC3501, RFC9051
711
+ when "EXPUNGE" then message_data__expunge # RFC3501, RFC9051
712
+ when "EXISTS" then mailbox_data__exists # RFC3501, RFC9051
713
+ when "ESEARCH" then esearch_response # RFC4731, RFC9051, etc
714
+ when "VANISHED" then expunged_resp # RFC7162
715
+ when "UIDFETCH" then uidfetch_resp # (draft) UIDONLY
716
+ when "SEARCH" then mailbox_data__search # RFC3501 (obsolete)
717
+ when "CAPABILITY" then capability_data__untagged # RFC3501, RFC9051
718
+ when "FLAGS" then mailbox_data__flags # RFC3501, RFC9051
719
+ when "LIST" then mailbox_data__list # RFC3501, RFC9051
720
+ when "STATUS" then mailbox_data__status # RFC3501, RFC9051
721
+ when "NAMESPACE" then namespace_response # RFC2342, RFC9051
722
+ when "ENABLED" then enable_data # RFC5161, RFC9051
723
+ when "BAD" then resp_cond_state__untagged # RFC3501, RFC9051
724
+ when "NO" then resp_cond_state__untagged # RFC3501, RFC9051
725
+ when "PREAUTH" then resp_cond_auth # RFC3501, RFC9051
726
+ when "BYE" then resp_cond_bye # RFC3501, RFC9051
727
+ when "RECENT" then mailbox_data__recent # RFC3501 (obsolete)
728
+ when "SORT" then sort_data # RFC5256, RFC7162
729
+ when "THREAD" then thread_data # RFC5256
730
+ when "QUOTA" then quota_response # RFC2087, RFC9208
731
+ when "QUOTAROOT" then quotaroot_response # RFC2087, RFC9208
732
+ when "ID" then id_response # RFC2971
733
+ when "ACL" then acl_data # RFC4314
734
+ when "LISTRIGHTS" then listrights_data # RFC4314
735
+ when "MYRIGHTS" then myrights_data # RFC4314
736
+ when "METADATA" then metadata_resp # RFC5464
737
+ when "LANGUAGE" then language_data # RFC5255
738
+ when "COMPARATOR" then comparator_data # RFC5255
739
+ when "CONVERTED" then message_data__converted # RFC5259
740
+ when "LSUB" then mailbox_data__lsub # RFC3501 (obsolete)
741
+ when "XLIST" then mailbox_data__xlist # deprecated
742
+ when "NOOP" then response_data__noop
743
+ else response_data__unhandled
408
744
  end
409
745
  end
410
746
 
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
747
+ def response_data__unhandled(klass = UntaggedResponse)
748
+ num = number?; SP?
749
+ type = tagged_ext_label; SP?
750
+ text = remaining_unparsed
751
+ data =
752
+ if num && text then UnparsedNumericResponseData.new(num, text)
753
+ elsif text then UnparsedData.new(text)
754
+ else num
449
755
  end
450
- else
451
- parse_error("unexpected token %s", token.symbol)
452
- end
756
+ klass.new(type, data, @str)
453
757
  end
454
758
 
759
+ # reads all the way up until CRLF
760
+ def remaining_unparsed
761
+ str = @str[@pos...-2] and @pos += str.bytesize
762
+ str&.empty? ? nil : str
763
+ end
764
+
765
+ def response_data__ignored; response_data__unhandled(IgnoredResponse) end
766
+ alias response_data__noop response_data__ignored
767
+
768
+ alias esearch_response response_data__unhandled
769
+ alias expunged_resp response_data__unhandled
770
+ alias uidfetch_resp response_data__unhandled
771
+ alias listrights_data response_data__unhandled
772
+ alias myrights_data response_data__unhandled
773
+ alias metadata_resp response_data__unhandled
774
+ alias language_data response_data__unhandled
775
+ alias comparator_data response_data__unhandled
776
+ alias message_data__converted response_data__unhandled
777
+
778
+ # RFC3501 & RFC9051:
779
+ # response-tagged = tag SP resp-cond-state CRLF
780
+ #
781
+ # resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text
782
+ # ; Status condition
783
+ #
784
+ # tag = 1*<any ASTRING-CHAR except "+">
455
785
  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)
786
+ tag = tag(); SP!
787
+ name = resp_cond_state__name; SP!
788
+ TaggedResponse.new(tag, name, resp_text, @str)
462
789
  end
463
790
 
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)
791
+ # RFC3501 & RFC9051:
792
+ # resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text
793
+ def resp_cond_state__untagged
794
+ name = resp_cond_state__name; SP!
795
+ UntaggedResponse.new(name, resp_text, @str)
469
796
  end
470
797
 
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
798
+ # resp-cond-auth = ("OK" / "PREAUTH") SP resp-text
799
+ def resp_cond_auth
800
+ name = resp_cond_auth__name; SP!
801
+ UntaggedResponse.new(name, resp_text, @str)
802
+ end
803
+
804
+ # resp-cond-bye = "BYE" SP resp-text
805
+ def resp_cond_bye
806
+ name = label(BYE); SP!
807
+ UntaggedResponse.new(name, resp_text, @str)
808
+ end
809
+
810
+ # message-data = nz-number SP ("EXPUNGE" / ("FETCH" SP msg-att))
811
+ def message_data__fetch
812
+ seq = nz_number; SP!
813
+ name = label "FETCH"; SP!
814
+ data = FetchData.new(seq, msg_att(seq))
815
+ UntaggedResponse.new(name, data, @str)
816
+ end
817
+
818
+ def response_data__simple_numeric
819
+ data = nz_number; SP!
820
+ name = tagged_ext_label
821
+ UntaggedResponse.new(name, data, @str)
485
822
  end
486
823
 
824
+ alias message_data__expunge response_data__simple_numeric
825
+ alias mailbox_data__exists response_data__simple_numeric
826
+ alias mailbox_data__recent response_data__simple_numeric
827
+
828
+ # RFC3501 & RFC9051:
829
+ # msg-att = "(" (msg-att-dynamic / msg-att-static)
830
+ # *(SP (msg-att-dynamic / msg-att-static)) ")"
831
+ #
832
+ # msg-att-dynamic = "FLAGS" SP "(" [flag-fetch *(SP flag-fetch)] ")"
833
+ # RFC5257 (ANNOTATE extension):
834
+ # msg-att-dynamic =/ "ANNOTATION" SP
835
+ # ( "(" entry-att *(SP entry-att) ")" /
836
+ # "(" entry *(SP entry) ")" )
837
+ # RFC7162 (CONDSTORE extension):
838
+ # msg-att-dynamic =/ fetch-mod-resp
839
+ # fetch-mod-resp = "MODSEQ" SP "(" permsg-modsequence ")"
840
+ # RFC8970 (PREVIEW extension):
841
+ # msg-att-dynamic =/ "PREVIEW" SP nstring
842
+ #
843
+ # RFC3501:
844
+ # msg-att-static = "ENVELOPE" SP envelope /
845
+ # "INTERNALDATE" SP date-time /
846
+ # "RFC822" [".HEADER" / ".TEXT"] SP nstring /
847
+ # "RFC822.SIZE" SP number /
848
+ # "BODY" ["STRUCTURE"] SP body /
849
+ # "BODY" section ["<" number ">"] SP nstring /
850
+ # "UID" SP uniqueid
851
+ # RFC3516 (BINARY extension):
852
+ # msg-att-static =/ "BINARY" section-binary SP (nstring / literal8)
853
+ # / "BINARY.SIZE" section-binary SP number
854
+ # RFC8514 (SAVEDATE extension):
855
+ # msg-att-static =/ "SAVEDATE" SP (date-time / nil)
856
+ # RFC8474 (OBJECTID extension):
857
+ # msg-att-static =/ fetch-emailid-resp / fetch-threadid-resp
858
+ # fetch-emailid-resp = "EMAILID" SP "(" objectid ")"
859
+ # fetch-threadid-resp = "THREADID" SP ( "(" objectid ")" / nil )
860
+ # RFC9051:
861
+ # msg-att-static = "ENVELOPE" SP envelope /
862
+ # "INTERNALDATE" SP date-time /
863
+ # "RFC822.SIZE" SP number64 /
864
+ # "BODY" ["STRUCTURE"] SP body /
865
+ # "BODY" section ["<" number ">"] SP nstring /
866
+ # "BINARY" section-binary SP (nstring / literal8) /
867
+ # "BINARY.SIZE" section-binary SP number /
868
+ # "UID" SP uniqueid
869
+ #
870
+ # Re https://www.rfc-editor.org/errata/eid7246, I'm adding "offset" to the
871
+ # official "BINARY" ABNF, like so:
872
+ #
873
+ # msg-att-static =/ "BINARY" section-binary ["<" number ">"] SP
874
+ # (nstring / literal8)
487
875
  def msg_att(n)
488
- match(T_LPAR)
876
+ lpar
489
877
  attr = {}
490
878
  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
879
+ name = msg_att__label; SP!
880
+ val =
881
+ case name
882
+ when "UID" then uniqueid
883
+ when "FLAGS" then flag_list
884
+ when "BODY" then body
885
+ when /\ABODY\[/ni then nstring
886
+ when "BODYSTRUCTURE" then body
887
+ when "ENVELOPE" then envelope
888
+ when "INTERNALDATE" then date_time
889
+ when "RFC822.SIZE" then number64
890
+ when /\ABINARY\[/ni then nstring8 # BINARY, IMAP4rev2
891
+ when /\ABINARY\.SIZE\[/ni then number # BINARY, IMAP4rev2
892
+ when "RFC822" then nstring # not in rev2
893
+ when "RFC822.HEADER" then nstring # not in rev2
894
+ when "RFC822.TEXT" then nstring # not in rev2
895
+ when "MODSEQ" then parens__modseq # CONDSTORE
896
+ when "EMAILID" then parens__objectid # OBJECTID
897
+ when "THREADID" then nparens__objectid # OBJECTID
898
+ when "X-GM-MSGID" then x_gm_id # GMail
899
+ when "X-GM-THRID" then x_gm_id # GMail
900
+ when "X-GM-LABELS" then x_gm_labels # GMail
901
+ else parse_error("unknown attribute `%s' for {%d}", name, n)
902
+ end
520
903
  attr[name] = val
904
+ break unless SP?
905
+ break if lookahead_rpar?
906
+ end
907
+ rpar
908
+ attr
909
+ end
910
+
911
+ # appends "[section]" and "<partial>" to the base label
912
+ def msg_att__label
913
+ case (name = tagged_ext_label)
914
+ when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni
915
+ # ignoring "[]" fixes https://bugs.ruby-lang.org/issues/5620
916
+ lbra? and rbra
917
+ when "BODY"
918
+ peek_lbra? and name << section and
919
+ peek_str?("<") and name << gt__number__lt # partial
920
+ when "BINARY", "BINARY.SIZE"
921
+ name << section_binary
922
+ # see https://www.rfc-editor.org/errata/eid7246 and the note above
923
+ peek_str?("<") and name << gt__number__lt # partial
521
924
  end
522
- return attr
925
+ name
523
926
  end
524
927
 
525
- def envelope_data
526
- token = match(T_ATOM)
527
- name = token.value.upcase
528
- match(T_SPACE)
529
- return name, envelope
530
- end
928
+ # this represents the partial size for BODY or BINARY
929
+ alias gt__number__lt atom
531
930
 
532
931
  def envelope
533
932
  @lex_state = EXPR_DATA
@@ -564,58 +963,10 @@ module Net
564
963
  return result
565
964
  end
566
965
 
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
966
+ # date-time = DQUOTE date-day-fixed "-" date-month "-" date-year
967
+ # SP time SP zone DQUOTE
968
+ alias date_time quoted
969
+ alias ndatetime nquoted
619
970
 
620
971
  # RFC-3501 & RFC-9051:
621
972
  # body = "(" (body-type-1part / body-type-mpart) ")"
@@ -844,6 +1195,7 @@ module Net
844
1195
  if lpar?
845
1196
  result = [case_insensitive__string]
846
1197
  result << case_insensitive__string while SP?
1198
+ rpar
847
1199
  result
848
1200
  else
849
1201
  case_insensitive__nstring
@@ -872,101 +1224,102 @@ module Net
872
1224
  end
873
1225
  end
874
1226
 
1227
+ # section = "[" [section-spec] "]"
875
1228
  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)
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
956
-
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
1229
+ str = +lbra
1230
+ str << section_spec unless peek_rbra?
1231
+ str << rbra
1232
+ end
1233
+
1234
+ # section-binary = "[" [section-part] "]"
1235
+ def section_binary
1236
+ str = +lbra
1237
+ str << section_part unless peek_rbra?
1238
+ str << rbra
1239
+ end
1240
+
1241
+ # section-spec = section-msgtext / (section-part ["." section-text])
1242
+ # section-msgtext = "HEADER" /
1243
+ # "HEADER.FIELDS" [".NOT"] SP header-list /
1244
+ # "TEXT"
1245
+ # ; top-level or MESSAGE/RFC822 or
1246
+ # ; MESSAGE/GLOBAL part
1247
+ # section-part = nz-number *("." nz-number)
1248
+ # ; body part reference.
1249
+ # ; Allows for accessing nested body parts.
1250
+ # section-text = section-msgtext / "MIME"
1251
+ # ; text other than actual body part (headers,
1252
+ # ; etc.)
1253
+ #
1254
+ # n.b: we could "cheat" here and just grab all text inside the brackets,
1255
+ # but literals would need special treatment.
1256
+ def section_spec
1257
+ str = "".b
1258
+ str << atom # grabs everything up to "SP header-list" or "]"
1259
+ str << " " << header_list if SP?
1260
+ str
1261
+ end
1262
+
1263
+ # header-list = "(" header-fld-name *(SP header-fld-name) ")"
1264
+ def header_list
1265
+ str = +""
1266
+ str << lpar << header_fld_name
1267
+ str << " " << header_fld_name while SP?
1268
+ str << rpar
1269
+ end
1270
+
1271
+ # section-part = nz-number *("." nz-number)
1272
+ # ; body part reference.
1273
+ # ; Allows for accessing nested body parts.
1274
+ alias section_part atom
1275
+
1276
+ # RFC3501 & RFC9051:
1277
+ # header-fld-name = astring
1278
+ #
1279
+ # NOTE: Previously, Net::IMAP recreated the raw original source string.
1280
+ # Now, it grabs the raw encoded value using @str and @pos. A future
1281
+ # version may simply return the decoded astring value. Although that is
1282
+ # technically incompatible, it should almost never make a difference: all
1283
+ # standard header field names are valid atoms:
1284
+ #
1285
+ # https://www.iana.org/assignments/message-headers/message-headers.xhtml
1286
+ #
1287
+ # Although RFC3501 allows any astring, RFC5322-valid header names are one
1288
+ # or more of the printable US-ASCII characters, except SP and colon. So
1289
+ # empty string isn't valid, and literals aren't needed and should not be
1290
+ # used. This is explicitly unchanged by [I18N-HDRS] (RFC6532).
1291
+ #
1292
+ # RFC5233:
1293
+ # optional-field = field-name ":" unstructured CRLF
1294
+ # field-name = 1*ftext
1295
+ # ftext = %d33-57 / ; Printable US-ASCII
1296
+ # %d59-126 ; characters not including
1297
+ # ; ":".
1298
+ def header_fld_name
1299
+ assert_no_lookahead
1300
+ start = @pos
1301
+ astring
1302
+ @str[start...@pos - 1]
1303
+ end
1304
+
1305
+ # mailbox-data = "FLAGS" SP flag-list / "LIST" SP mailbox-list /
1306
+ # "LSUB" SP mailbox-list / "SEARCH" *(SP nz-number) /
1307
+ # "STATUS" SP mailbox SP "(" [status-att-list] ")" /
1308
+ # number SP "EXISTS" / number SP "RECENT"
1309
+
1310
+ def mailbox_data__flags
1311
+ name = label("FLAGS")
1312
+ SP!
1313
+ UntaggedResponse.new(name, flag_list, @str)
1314
+ end
1315
+
1316
+ def mailbox_data__list
1317
+ name = label_in("LIST", "LSUB", "XLIST")
1318
+ SP!
1319
+ UntaggedResponse.new(name, mailbox_list, @str)
1320
+ end
1321
+ alias mailbox_data__lsub mailbox_data__list
1322
+ alias mailbox_data__xlist mailbox_data__list
970
1323
 
971
1324
  def mailbox_list
972
1325
  attr = flag_list
@@ -1032,7 +1385,8 @@ module Net
1032
1385
  return UntaggedResponse.new(name, data, @str)
1033
1386
  end
1034
1387
 
1035
- def getacl_response
1388
+ # acl-data = "ACL" SP mailbox *(SP identifier SP rights)
1389
+ def acl_data
1036
1390
  token = match(T_ATOM)
1037
1391
  name = token.value.upcase
1038
1392
  match(T_SPACE)
@@ -1058,7 +1412,21 @@ module Net
1058
1412
  return UntaggedResponse.new(name, data, @str)
1059
1413
  end
1060
1414
 
1061
- def search_response
1415
+ # RFC3501:
1416
+ # mailbox-data = "SEARCH" *(SP nz-number) / ...
1417
+ # RFC5256: SORT
1418
+ # sort-data = "SORT" *(SP nz-number)
1419
+ # RFC7162: CONDSTORE, QRESYNC
1420
+ # mailbox-data =/ "SEARCH" [1*(SP nz-number) SP
1421
+ # search-sort-mod-seq]
1422
+ # sort-data = "SORT" [1*(SP nz-number) SP
1423
+ # search-sort-mod-seq]
1424
+ # ; Updates the SORT response from RFC 5256.
1425
+ # search-sort-mod-seq = "(" "MODSEQ" SP mod-sequence-value ")"
1426
+ # RFC9051:
1427
+ # mailbox-data = obsolete-search-response / ...
1428
+ # obsolete-search-response = "SEARCH" *(SP nz-number)
1429
+ def mailbox_data__search
1062
1430
  token = match(T_ATOM)
1063
1431
  name = token.value.upcase
1064
1432
  token = lookahead
@@ -1088,8 +1456,9 @@ module Net
1088
1456
  end
1089
1457
  return UntaggedResponse.new(name, data, @str)
1090
1458
  end
1459
+ alias sort_data mailbox_data__search
1091
1460
 
1092
- def thread_response
1461
+ def thread_data
1093
1462
  token = match(T_ATOM)
1094
1463
  name = token.value.upcase
1095
1464
  token = lookahead
@@ -1151,31 +1520,80 @@ module Net
1151
1520
  return rootmember
1152
1521
  end
1153
1522
 
1154
- def status_response
1155
- token = match(T_ATOM)
1156
- name = token.value.upcase
1157
- match(T_SPACE)
1158
- mailbox = astring
1159
- match(T_SPACE)
1160
- match(T_LPAR)
1161
- attr = {}
1162
- while true
1163
- token = lookahead
1164
- case token.symbol
1165
- when T_RPAR
1166
- shift_token
1167
- break
1168
- when T_SPACE
1169
- shift_token
1523
+ # mailbox-data =/ "STATUS" SP mailbox SP "(" [status-att-list] ")"
1524
+ def mailbox_data__status
1525
+ resp_name = label("STATUS"); SP!
1526
+ mbox_name = mailbox; SP!
1527
+ lpar; attr = status_att_list; rpar
1528
+ UntaggedResponse.new(resp_name, StatusData.new(mbox_name, attr), @str)
1529
+ end
1530
+
1531
+ # RFC3501
1532
+ # status-att-list = status-att SP number *(SP status-att SP number)
1533
+ # RFC4466, RFC9051, and RFC3501 Errata
1534
+ # status-att-list = status-att-val *(SP status-att-val)
1535
+ def status_att_list
1536
+ attrs = [status_att_val]
1537
+ while SP? do attrs << status_att_val end
1538
+ attrs.to_h
1539
+ end
1540
+
1541
+ # RFC3501 Errata:
1542
+ # status-att-val = ("MESSAGES" SP number) / ("RECENT" SP number) /
1543
+ # ("UIDNEXT" SP nz-number) / ("UIDVALIDITY" SP nz-number) /
1544
+ # ("UNSEEN" SP number)
1545
+ # RFC4466:
1546
+ # status-att-val = ("MESSAGES" SP number) /
1547
+ # ("RECENT" SP number) /
1548
+ # ("UIDNEXT" SP nz-number) /
1549
+ # ("UIDVALIDITY" SP nz-number) /
1550
+ # ("UNSEEN" SP number)
1551
+ # ;; Extensions to the STATUS responses
1552
+ # ;; should extend this production.
1553
+ # ;; Extensions should use the generic
1554
+ # ;; syntax defined by tagged-ext.
1555
+ # RFC9051:
1556
+ # status-att-val = ("MESSAGES" SP number) /
1557
+ # ("UIDNEXT" SP nz-number) /
1558
+ # ("UIDVALIDITY" SP nz-number) /
1559
+ # ("UNSEEN" SP number) /
1560
+ # ("DELETED" SP number) /
1561
+ # ("SIZE" SP number64)
1562
+ # ; Extensions to the STATUS responses
1563
+ # ; should extend this production.
1564
+ # ; Extensions should use the generic
1565
+ # ; syntax defined by tagged-ext.
1566
+ # RFC7162:
1567
+ # status-att-val =/ "HIGHESTMODSEQ" SP mod-sequence-valzer
1568
+ # ;; Extends non-terminal defined in [RFC4466].
1569
+ # ;; Value 0 denotes that the mailbox doesn't
1570
+ # ;; support persistent mod-sequences
1571
+ # ;; as described in Section 3.1.2.2.
1572
+ # RFC7889:
1573
+ # status-att-val =/ "APPENDLIMIT" SP (number / nil)
1574
+ # ;; status-att-val is defined in RFC 4466
1575
+ # RFC8438:
1576
+ # status-att-val =/ "SIZE" SP number64
1577
+ # RFC8474:
1578
+ # status-att-val =/ "MAILBOXID" SP "(" objectid ")"
1579
+ # ; follows tagged-ext production from [RFC4466]
1580
+ def status_att_val
1581
+ key = tagged_ext_label
1582
+ SP!
1583
+ val =
1584
+ case key
1585
+ when "MESSAGES" then number # RFC3501, RFC9051
1586
+ when "UNSEEN" then number # RFC3501, RFC9051
1587
+ when "DELETED" then number # RFC3501, RFC9051
1588
+ when "UIDNEXT" then nz_number # RFC3501, RFC9051
1589
+ when "UIDVALIDITY" then nz_number # RFC3501, RFC9051
1590
+ when "RECENT" then number # RFC3501 (obsolete)
1591
+ when "SIZE" then number64 # RFC8483, RFC9051
1592
+ when "MAILBOXID" then parens__objectid # RFC8474
1593
+ else
1594
+ number? || ExtensionData.new(tagged_ext_val)
1170
1595
  end
1171
- token = match(T_ATOM)
1172
- key = token.value.upcase
1173
- match(T_SPACE)
1174
- val = number
1175
- attr[key] = val
1176
- end
1177
- data = StatusData.new(mailbox, attr)
1178
- return UntaggedResponse.new(name, data, @str)
1596
+ [key, val]
1179
1597
  end
1180
1598
 
1181
1599
  # The presence of "IMAP4rev1" or "IMAP4rev2" is unenforced here.
@@ -1198,11 +1616,13 @@ module Net
1198
1616
  end
1199
1617
 
1200
1618
  # As a workaround for buggy servers, allow a trailing SP:
1201
- # *(SP capapility) [SP]
1619
+ # *(SP capability) [SP]
1202
1620
  def capability__list
1203
- data = []; while _ = SP? && capability? do data << _ end; data
1621
+ list = []; while SP? && (capa = capability?) do list << capa end; list
1204
1622
  end
1205
1623
 
1624
+ alias resp_code__capability capability__list
1625
+
1206
1626
  # capability = ("AUTH=" auth-type) / atom
1207
1627
  # ; New capabilities MUST begin with "X" or be
1208
1628
  # ; registered with IANA as standard or
@@ -1325,68 +1745,95 @@ module Net
1325
1745
  end
1326
1746
  end
1327
1747
 
1328
- # See https://www.rfc-editor.org/errata/rfc3501
1748
+ # RFC3501 (See https://www.rfc-editor.org/errata/rfc3501):
1749
+ # resp-text-code = "ALERT" /
1750
+ # "BADCHARSET" [SP "(" charset *(SP charset) ")" ] /
1751
+ # capability-data / "PARSE" /
1752
+ # "PERMANENTFLAGS" SP "(" [flag-perm *(SP flag-perm)] ")" /
1753
+ # "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
1754
+ # "UIDNEXT" SP nz-number / "UIDVALIDITY" SP nz-number /
1755
+ # "UNSEEN" SP nz-number /
1756
+ # atom [SP 1*<any TEXT-CHAR except "]">]
1757
+ # capability-data = "CAPABILITY" *(SP capability) SP "IMAP4rev1"
1758
+ # *(SP capability)
1329
1759
  #
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 "]">]
1760
+ # RFC5530:
1761
+ # resp-text-code =/ "UNAVAILABLE" / "AUTHENTICATIONFAILED" /
1762
+ # "AUTHORIZATIONFAILED" / "EXPIRED" /
1763
+ # "PRIVACYREQUIRED" / "CONTACTADMIN" / "NOPERM" /
1764
+ # "INUSE" / "EXPUNGEISSUED" / "CORRUPTION" /
1765
+ # "SERVERBUG" / "CLIENTBUG" / "CANNOT" /
1766
+ # "LIMIT" / "OVERQUOTA" / "ALREADYEXISTS" /
1767
+ # "NONEXISTENT"
1768
+ # RFC9051:
1769
+ # resp-text-code = "ALERT" /
1770
+ # "BADCHARSET" [SP "(" charset *(SP charset) ")" ] /
1771
+ # capability-data / "PARSE" /
1772
+ # "PERMANENTFLAGS" SP "(" [flag-perm *(SP flag-perm)] ")" /
1773
+ # "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
1774
+ # "UIDNEXT" SP nz-number / "UIDVALIDITY" SP nz-number /
1775
+ # resp-code-apnd / resp-code-copy / "UIDNOTSTICKY" /
1776
+ # "UNAVAILABLE" / "AUTHENTICATIONFAILED" /
1777
+ # "AUTHORIZATIONFAILED" / "EXPIRED" /
1778
+ # "PRIVACYREQUIRED" / "CONTACTADMIN" / "NOPERM" /
1779
+ # "INUSE" / "EXPUNGEISSUED" / "CORRUPTION" /
1780
+ # "SERVERBUG" / "CLIENTBUG" / "CANNOT" /
1781
+ # "LIMIT" / "OVERQUOTA" / "ALREADYEXISTS" /
1782
+ # "NONEXISTENT" / "NOTSAVED" / "HASCHILDREN" /
1783
+ # "CLOSED" /
1784
+ # "UNKNOWN-CTE" /
1785
+ # atom [SP 1*<any TEXT-CHAR except "]">]
1786
+ # capability-data = "CAPABILITY" *(SP capability) SP "IMAP4rev2"
1787
+ # *(SP capability)
1339
1788
  #
1340
- # +UIDPLUS+ ABNF:: https://www.rfc-editor.org/rfc/rfc4315.html#section-4
1341
- # resp-text-code =/ resp-code-apnd / resp-code-copy / "UIDNOTSTICKY"
1789
+ # RFC4315 (UIDPLUS), RFC9051 (IMAP4rev2):
1790
+ # resp-code-apnd = "APPENDUID" SP nz-number SP append-uid
1791
+ # resp-code-copy = "COPYUID" SP nz-number SP uid-set SP uid-set
1792
+ # resp-text-code =/ resp-code-apnd / resp-code-copy / "UIDNOTSTICKY"
1793
+ #
1794
+ # RFC7162 (CONDSTORE):
1795
+ # resp-text-code =/ "HIGHESTMODSEQ" SP mod-sequence-value /
1796
+ # "NOMODSEQ" /
1797
+ # "MODIFIED" SP sequence-set
1798
+ #
1799
+ # RFC8474: OBJECTID
1800
+ # resp-text-code =/ "MAILBOXID" SP "(" objectid ")"
1342
1801
  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)
1802
+ name = resp_text_code__name
1803
+ data =
1804
+ case name
1805
+ when "CAPABILITY" then resp_code__capability
1806
+ when "PERMANENTFLAGS" then SP? ? flag_perm__list : []
1807
+ when "UIDNEXT" then SP!; nz_number
1808
+ when "UIDVALIDITY" then SP!; nz_number
1809
+ when "UNSEEN" then SP!; nz_number # rev1 only
1810
+ when "APPENDUID" then SP!; resp_code_apnd__data # rev2, UIDPLUS
1811
+ when "COPYUID" then SP!; resp_code_copy__data # rev2, UIDPLUS
1812
+ when "BADCHARSET" then SP? ? charset__list : []
1813
+ when "ALERT", "PARSE", "READ-ONLY", "READ-WRITE", "TRYCREATE",
1814
+ "UNAVAILABLE", "AUTHENTICATIONFAILED", "AUTHORIZATIONFAILED",
1815
+ "EXPIRED", "PRIVACYREQUIRED", "CONTACTADMIN", "NOPERM", "INUSE",
1816
+ "EXPUNGEISSUED", "CORRUPTION", "SERVERBUG", "CLIENTBUG", "CANNOT",
1817
+ "LIMIT", "OVERQUOTA", "ALREADYEXISTS", "NONEXISTENT", "CLOSED",
1818
+ "NOTSAVED", "UIDNOTSTICKY", "UNKNOWN-CTE", "HASCHILDREN"
1819
+ when "NOMODSEQ" # CONDSTORE
1820
+ when "MAILBOXID" then SP!; parens__objectid # RFC8474: OBJECTID
1367
1821
  else
1368
- result = ResponseCode.new(name, nil)
1822
+ SP? and text_chars_except_rbra
1369
1823
  end
1370
- end
1371
- return result
1824
+ ResponseCode.new(name, data)
1372
1825
  end
1373
1826
 
1827
+ alias resp_text_code__name case_insensitive__atom
1828
+
1374
1829
  # 1*<any TEXT-CHAR except "]">
1375
1830
  def text_chars_except_rbra
1376
1831
  match_re(CTEXT_REGEXP, '1*<any TEXT-CHAR except "]">')[0]
1377
1832
  end
1378
1833
 
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
1834
+ # "(" charset *(SP charset) ")"
1835
+ def charset__list
1836
+ lpar; list = [charset]; while SP? do list << charset end; rpar; list
1390
1837
  end
1391
1838
 
1392
1839
  # already matched: "APPENDUID"
@@ -1402,8 +1849,8 @@ module Net
1402
1849
  # match uid_set even if that returns a single-member array.
1403
1850
  #
1404
1851
  def resp_code_apnd__data
1405
- match(T_SPACE); validity = number
1406
- match(T_SPACE); dst_uids = uid_set # uniqueid ⊂ uid-set
1852
+ validity = number; SP!
1853
+ dst_uids = uid_set # uniqueid ⊂ uid-set
1407
1854
  UIDPlusData.new(validity, nil, dst_uids)
1408
1855
  end
1409
1856
 
@@ -1411,9 +1858,9 @@ module Net
1411
1858
  #
1412
1859
  # resp-code-copy = "COPYUID" SP nz-number SP uid-set SP uid-set
1413
1860
  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
1861
+ validity = number; SP!
1862
+ src_uids = uid_set; SP!
1863
+ dst_uids = uid_set
1417
1864
  UIDPlusData.new(validity, src_uids, dst_uids)
1418
1865
  end
1419
1866
 
@@ -1472,36 +1919,78 @@ module Net
1472
1919
  return Address.new(name, route, mailbox, host)
1473
1920
  end
1474
1921
 
1475
- FLAG_REGEXP = /\
1476
- (?# FLAG )\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)|\
1477
- (?# ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\]+)/n
1478
-
1922
+ # flag-list = "(" [flag *(SP flag)] ")"
1479
1923
  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
1924
+ match_re(Patterns::FLAG_LIST, "flag-list")[1]
1925
+ .split(nil)
1926
+ .map! { _1.start_with?("\\") ? _1[1..].capitalize.to_sym : _1 }
1927
+ end
1928
+
1929
+ # "(" [flag-perm *(SP flag-perm)] ")"
1930
+ def flag_perm__list
1931
+ match_re(Patterns::FLAG_PERM_LIST, "PERMANENTFLAGS flag-perm list")[1]
1932
+ .split(nil)
1933
+ .map! { _1.start_with?("\\") ? _1[1..].capitalize.to_sym : _1 }
1934
+ end
1935
+
1936
+ # Not checking for max one mbx-list-sflag in the parser.
1937
+ # >>>
1938
+ # mbx-list-flags = *(mbx-list-oflag SP) mbx-list-sflag
1939
+ # *(SP mbx-list-oflag) /
1940
+ # mbx-list-oflag *(SP mbx-list-oflag)
1941
+ # mbx-list-oflag = "\Noinferiors" / child-mbox-flag /
1942
+ # "\Subscribed" / "\Remote" / flag-extension
1943
+ # ; Other flags; multiple from this list are
1944
+ # ; possible per LIST response, but each flag
1945
+ # ; can only appear once per LIST response
1946
+ # mbx-list-sflag = "\NonExistent" / "\Noselect" / "\Marked" /
1947
+ # "\Unmarked"
1948
+ # ; Selectability flags; only one per LIST response
1949
+ def parens__mbx_list_flags
1950
+ match_re(Patterns::MBX_LIST_FLAGS, "mbx-list-flags")[1]
1951
+ .split(nil).map! { _1.capitalize.to_sym }
1952
+ end
1953
+
1954
+ # See https://developers.google.com/gmail/imap/imap-extensions
1955
+ def x_gm_label; accept(T_BSLASH) ? atom.capitalize.to_sym : astring end
1956
+
1957
+ # See https://developers.google.com/gmail/imap/imap-extensions
1958
+ def x_gm_labels
1959
+ lpar; return [] if rpar?
1960
+ labels = []
1961
+ labels << x_gm_label
1962
+ labels << x_gm_label while SP?
1963
+ rpar
1964
+ labels
1492
1965
  end
1493
1966
 
1494
-
1495
1967
  # See https://www.rfc-editor.org/errata/rfc3501
1496
1968
  #
1497
1969
  # charset = atom / quoted
1498
- def charset
1499
- if token = accept(T_QUOTED)
1500
- token.value
1501
- else
1502
- atom
1503
- end
1504
- end
1970
+ def charset; quoted? || atom end
1971
+
1972
+ # RFC7162:
1973
+ # mod-sequence-value = 1*DIGIT
1974
+ # ;; Positive unsigned 63-bit integer
1975
+ # ;; (mod-sequence)
1976
+ # ;; (1 <= n <= 9,223,372,036,854,775,807).
1977
+ alias mod_sequence_value nz_number64
1978
+
1979
+ # RFC7162:
1980
+ # permsg-modsequence = mod-sequence-value
1981
+ # ;; Per-message mod-sequence.
1982
+ alias permsg_modsequence mod_sequence_value
1983
+
1984
+ def parens__modseq; lpar; _ = permsg_modsequence; rpar; _ end
1985
+
1986
+ # RFC8474:
1987
+ # objectid = 1*255(ALPHA / DIGIT / "_" / "-")
1988
+ # ; characters in object identifiers are case
1989
+ # ; significant
1990
+ alias objectid atom
1991
+
1992
+ def parens__objectid; lpar; _ = objectid; rpar; _ end
1993
+ def nparens__objectid; NIL? ? nil : parens__objectid end
1505
1994
 
1506
1995
  # RFC-4315 (UIDPLUS) or RFC9051 (IMAP4rev2):
1507
1996
  # uid-set = (uniqueid / uid-range) *("," uid-set)
@@ -1535,10 +2024,10 @@ module Net
1535
2024
  #
1536
2025
  # This advances @pos directly so it's safe before changing @lex_state.
1537
2026
  def accept_spaces
1538
- shift_token if @token&.symbol == T_SPACE
1539
- if @str.index(SPACES_REGEXP, @pos)
2027
+ return false unless SP?
2028
+ @str.index(SPACES_REGEXP, @pos) and
1540
2029
  @pos = $~.end(0)
1541
- end
2030
+ true
1542
2031
  end
1543
2032
 
1544
2033
  def next_token
@@ -1548,42 +2037,47 @@ module Net
1548
2037
  @pos = $~.end(0)
1549
2038
  if $1
1550
2039
  return Token.new(T_SPACE, $+)
1551
- elsif $2 && $6
2040
+ elsif $2
2041
+ len = $+.to_i
2042
+ val = @str[@pos, len]
2043
+ @pos += len
2044
+ return Token.new(T_LITERAL8, val)
2045
+ elsif $3 && $7
1552
2046
  # greedily match ATOM, prefixed with NUMBER, NIL, or PLUS.
1553
- return Token.new(T_ATOM, $2)
1554
- elsif $3
1555
- return Token.new(T_NIL, $+)
2047
+ return Token.new(T_ATOM, $3)
1556
2048
  elsif $4
1557
- return Token.new(T_NUMBER, $+)
2049
+ return Token.new(T_NIL, $+)
1558
2050
  elsif $5
2051
+ return Token.new(T_NUMBER, $+)
2052
+ elsif $6
1559
2053
  return Token.new(T_PLUS, $+)
1560
- elsif $7
2054
+ elsif $8
1561
2055
  # match ATOM, without a NUMBER, NIL, or PLUS prefix
1562
2056
  return Token.new(T_ATOM, $+)
1563
- elsif $8
1564
- return Token.new(T_QUOTED, Patterns.unescape_quoted($+))
1565
2057
  elsif $9
1566
- return Token.new(T_LPAR, $+)
2058
+ return Token.new(T_QUOTED, Patterns.unescape_quoted($+))
1567
2059
  elsif $10
1568
- return Token.new(T_RPAR, $+)
2060
+ return Token.new(T_LPAR, $+)
1569
2061
  elsif $11
1570
- return Token.new(T_BSLASH, $+)
2062
+ return Token.new(T_RPAR, $+)
1571
2063
  elsif $12
1572
- return Token.new(T_STAR, $+)
2064
+ return Token.new(T_BSLASH, $+)
1573
2065
  elsif $13
1574
- return Token.new(T_LBRA, $+)
2066
+ return Token.new(T_STAR, $+)
1575
2067
  elsif $14
1576
- return Token.new(T_RBRA, $+)
2068
+ return Token.new(T_LBRA, $+)
1577
2069
  elsif $15
2070
+ return Token.new(T_RBRA, $+)
2071
+ elsif $16
1578
2072
  len = $+.to_i
1579
2073
  val = @str[@pos, len]
1580
2074
  @pos += len
1581
2075
  return Token.new(T_LITERAL, val)
1582
- elsif $16
1583
- return Token.new(T_PERCENT, $+)
1584
2076
  elsif $17
1585
- return Token.new(T_CRLF, $+)
2077
+ return Token.new(T_PERCENT, $+)
1586
2078
  elsif $18
2079
+ return Token.new(T_CRLF, $+)
2080
+ elsif $19
1587
2081
  return Token.new(T_EOF, $+)
1588
2082
  else
1589
2083
  parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid")