net-imap 0.4.2 → 0.4.5

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.

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