net-imap 0.4.4 → 0.4.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -54,6 +54,7 @@ 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
@@ -197,6 +198,7 @@ module Net
197
198
  # ; revisions of this specification.
198
199
  # flag-keyword = "$MDNSent" / "$Forwarded" / "$Junk" /
199
200
  # "$NotJunk" / "$Phishing" / atom
201
+ #
200
202
  # flag-perm = flag / "\*"
201
203
  #
202
204
  # Not checking for max one mbx-list-sflag in the parser.
@@ -219,19 +221,15 @@ module Net
219
221
  MBX_FLAG = FLAG_EXTENSION
220
222
 
221
223
  # flag-list = "(" [flag *(SP flag)] ")"
222
- #
223
- # part of resp-text-code:
224
- # >>>
225
- # "PERMANENTFLAGS" SP "(" [flag-perm *(SP flag-perm)] ")"
226
- #
227
- # parens from mailbox-list are included in the regexp:
228
- # >>>
229
- # mbx-list-flags = *(mbx-list-oflag SP) mbx-list-sflag
230
- # *(SP mbx-list-oflag) /
231
- # mbx-list-oflag *(SP mbx-list-oflag)
232
- FLAG_LIST = /\G\((#{FLAG }(?:#{SP}#{FLAG })*|)\)/ni
233
- FLAG_PERM_LIST = /\G\((#{FLAG_PERM}(?:#{SP}#{FLAG_PERM})*|)\)/ni
234
- MBX_LIST_FLAGS = /\G\((#{MBX_FLAG }(?:#{SP}#{MBX_FLAG })*|)\)/ni
224
+ # resp-text-code =/ "PERMANENTFLAGS" SP
225
+ # "(" [flag-perm *(SP flag-perm)] ")"
226
+ # mbx-list-flags = *(mbx-list-oflag SP) mbx-list-sflag
227
+ # *(SP mbx-list-oflag) /
228
+ # mbx-list-oflag *(SP mbx-list-oflag)
229
+ # (Not checking for max one mbx-list-sflag in the parser.)
230
+ FLAG_LIST = /\G\((#{FLAG }(?:#{SP}#{FLAG })*|)\)/ni
231
+ FLAG_PERM_LIST = /\G\((#{FLAG_PERM}(?:#{SP}#{FLAG_PERM})*|)\)/ni
232
+ MBX_LIST_FLAGS = /\G (#{MBX_FLAG }(?:#{SP}#{MBX_FLAG })*) /nix
235
233
 
236
234
  # RFC3501:
237
235
  # QUOTED-CHAR = <any TEXT-CHAR except quoted-specials> /
@@ -266,6 +264,56 @@ module Net
266
264
  # ; Is a valid RFC 3501 "atom".
267
265
  TAGGED_EXT_LABEL = /#{TAGGED_LABEL_FCHAR}#{TAGGED_LABEL_CHAR}*/n
268
266
 
267
+ # nz-number = digit-nz *DIGIT
268
+ # ; Non-zero unsigned 32-bit integer
269
+ # ; (0 < n < 4,294,967,296)
270
+ NZ_NUMBER = /[1-9]\d*/n
271
+
272
+ # seq-number = nz-number / "*"
273
+ # ; message sequence number (COPY, FETCH, STORE
274
+ # ; commands) or unique identifier (UID COPY,
275
+ # ; UID FETCH, UID STORE commands).
276
+ # ; * represents the largest number in use. In
277
+ # ; the case of message sequence numbers, it is
278
+ # ; the number of messages in a non-empty mailbox.
279
+ # ; In the case of unique identifiers, it is the
280
+ # ; unique identifier of the last message in the
281
+ # ; mailbox or, if the mailbox is empty, the
282
+ # ; mailbox's current UIDNEXT value.
283
+ # ; The server should respond with a tagged BAD
284
+ # ; response to a command that uses a message
285
+ # ; sequence number greater than the number of
286
+ # ; messages in the selected mailbox. This
287
+ # ; includes "*" if the selected mailbox is empty.
288
+ SEQ_NUMBER = /#{NZ_NUMBER}|\*/n
289
+
290
+ # seq-range = seq-number ":" seq-number
291
+ # ; two seq-number values and all values between
292
+ # ; these two regardless of order.
293
+ # ; Example: 2:4 and 4:2 are equivalent and
294
+ # ; indicate values 2, 3, and 4.
295
+ # ; Example: a unique identifier sequence range of
296
+ # ; 3291:* includes the UID of the last message in
297
+ # ; the mailbox, even if that value is less than
298
+ # ; 3291.
299
+ SEQ_RANGE = /#{SEQ_NUMBER}:#{SEQ_NUMBER}/n
300
+
301
+ # sequence-set = (seq-number / seq-range) ["," sequence-set]
302
+ # ; set of seq-number values, regardless of order.
303
+ # ; Servers MAY coalesce overlaps and/or execute
304
+ # ; the sequence in any order.
305
+ # ; Example: a message sequence number set of
306
+ # ; 2,4:7,9,12:* for a mailbox with 15 messages is
307
+ # ; equivalent to 2,4,5,6,7,9,12,13,14,15
308
+ # ; Example: a message sequence number set of
309
+ # ; *:4,5:7 for a mailbox with 10 messages is
310
+ # ; equivalent to 10,9,8,7,6,5,4,5,6,7 and MAY
311
+ # ; be reordered and overlap coalesced to be
312
+ # ; 4,5,6,7,8,9,10.
313
+ SEQUENCE_SET_ITEM = /#{SEQ_NUMBER}|#{SEQ_RANGE}/n
314
+ SEQUENCE_SET = /#{SEQUENCE_SET_ITEM}(?:,#{SEQUENCE_SET_ITEM})*/n
315
+ SEQUENCE_SET_STR = /\A#{SEQUENCE_SET}\z/n
316
+
269
317
  # RFC3501:
270
318
  # literal = "{" number "}" CRLF *CHAR8
271
319
  # ; Number represents the number of CHAR8s
@@ -279,6 +327,16 @@ module Net
279
327
  # ; sent from server to the client.
280
328
  LITERAL = /\{(\d+)\}\r\n/n
281
329
 
330
+ # RFC3516 (BINARY):
331
+ # literal8 = "~{" number "}" CRLF *OCTET
332
+ # ; <number> represents the number of OCTETs
333
+ # ; in the response string.
334
+ # RFC9051:
335
+ # literal8 = "~{" number64 "}" CRLF *OCTET
336
+ # ; <number64> represents the number of OCTETs
337
+ # ; in the response string.
338
+ LITERAL8 = /~\{(\d+)\}\r\n/n
339
+
282
340
  module_function
283
341
 
284
342
  def unescape_quoted!(quoted)
@@ -298,27 +356,28 @@ module Net
298
356
  # the default, used in most places
299
357
  BEG_REGEXP = /\G(?:\
300
358
  (?# 1: SPACE )( )|\
301
- (?# 2: ATOM prefixed with a compatible subtype)\
359
+ (?# 2: LITERAL8)#{Patterns::LITERAL8}|\
360
+ (?# 3: ATOM prefixed with a compatible subtype)\
302
361
  ((?:\
303
- (?# 3: NIL )(NIL)|\
304
- (?# 4: NUMBER )(\d+)|\
305
- (?# 5: PLUS )(\+))\
306
- (?# 6: ATOM remaining after prefix )(#{Patterns::ATOMISH})?\
362
+ (?# 4: NIL )(NIL)|\
363
+ (?# 5: NUMBER )(\d+)|\
364
+ (?# 6: PLUS )(\+))\
365
+ (?# 7: ATOM remaining after prefix )(#{Patterns::ATOMISH})?\
307
366
  (?# This enables greedy alternation without lookahead, in linear time.)\
308
367
  )|\
309
368
  (?# Also need to check for ATOM without a subtype prefix.)\
310
- (?# 7: ATOM )(#{Patterns::ATOMISH})|\
311
- (?# 8: QUOTED )#{Patterns::QUOTED_rev2}|\
312
- (?# 9: LPAR )(\()|\
313
- (?# 10: RPAR )(\))|\
314
- (?# 11: BSLASH )(\\)|\
315
- (?# 12: STAR )(\*)|\
316
- (?# 13: LBRA )(\[)|\
317
- (?# 14: RBRA )(\])|\
318
- (?# 15: LITERAL )#{Patterns::LITERAL}|\
319
- (?# 16: PERCENT )(%)|\
320
- (?# 17: CRLF )(\r\n)|\
321
- (?# 18: EOF )(\z))/ni
369
+ (?# 8: ATOM )(#{Patterns::ATOMISH})|\
370
+ (?# 9: QUOTED )#{Patterns::QUOTED_rev2}|\
371
+ (?# 10: LPAR )(\()|\
372
+ (?# 11: RPAR )(\))|\
373
+ (?# 12: BSLASH )(\\)|\
374
+ (?# 13: STAR )(\*)|\
375
+ (?# 14: LBRA )(\[)|\
376
+ (?# 15: RBRA )(\])|\
377
+ (?# 16: LITERAL )#{Patterns::LITERAL}|\
378
+ (?# 17: PERCENT )(%)|\
379
+ (?# 18: CRLF )(\r\n)|\
380
+ (?# 19: EOF )(\z))/ni
322
381
 
323
382
  # envelope, body(structure), namespaces
324
383
  DATA_REGEXP = /\G(?:\
@@ -359,6 +418,9 @@ module Net
359
418
  # string = quoted / literal
360
419
  def_token_matchers :string, T_QUOTED, T_LITERAL
361
420
 
421
+ # used by nstring8 = nstring / literal8
422
+ def_token_matchers :string8, T_QUOTED, T_LITERAL, T_LITERAL8
423
+
362
424
  # use where string represents "LABEL" values
363
425
  def_token_matchers :case_insensitive__string,
364
426
  T_QUOTED, T_LITERAL,
@@ -390,6 +452,24 @@ module Net
390
452
  # ATOM-CHAR = <any CHAR except atom-specials>
391
453
  ATOM_TOKENS = [T_ATOM, T_NUMBER, T_NIL, T_LBRA, T_PLUS]
392
454
 
455
+ SEQUENCE_SET_TOKENS = [T_ATOM, T_NUMBER, T_STAR]
456
+
457
+ # sequence-set = (seq-number / seq-range) ["," sequence-set]
458
+ # sequence-set =/ seq-last-command
459
+ # ; Allow for "result of the last command"
460
+ # ; indicator.
461
+ # seq-last-command = "$"
462
+ #
463
+ # *note*: doesn't match seq-last-command
464
+ def sequence_set
465
+ str = combine_adjacent(*SEQUENCE_SET_TOKENS)
466
+ if Patterns::SEQUENCE_SET_STR.match?(str)
467
+ SequenceSet.new(str)
468
+ else
469
+ parse_error("unexpected atom %p, expected sequence-set", str)
470
+ end
471
+ end
472
+
393
473
  # ASTRING-CHAR = ATOM-CHAR / resp-specials
394
474
  # resp-specials = "]"
395
475
  ASTRING_CHARS_TOKENS = [*ATOM_TOKENS, T_RBRA].freeze
@@ -460,6 +540,10 @@ module Net
460
540
  NIL? ? nil : string
461
541
  end
462
542
 
543
+ def nstring8
544
+ NIL? ? nil : string8
545
+ end
546
+
463
547
  def nquoted
464
548
  NIL? ? nil : quoted
465
549
  end
@@ -469,6 +553,60 @@ module Net
469
553
  NIL? ? nil : case_insensitive__string
470
554
  end
471
555
 
556
+ # tagged-ext-comp = astring /
557
+ # tagged-ext-comp *(SP tagged-ext-comp) /
558
+ # "(" tagged-ext-comp ")"
559
+ # ; Extensions that follow this general
560
+ # ; syntax should use nstring instead of
561
+ # ; astring when appropriate in the context
562
+ # ; of the extension.
563
+ # ; Note that a message set or a "number"
564
+ # ; can always be represented as an "atom".
565
+ # ; A URL should be represented as
566
+ # ; a "quoted" string.
567
+ def tagged_ext_comp
568
+ vals = []
569
+ while true
570
+ vals << case lookahead!(*ASTRING_TOKENS, T_LPAR).symbol
571
+ when T_LPAR then lpar; ary = tagged_ext_comp; rpar; ary
572
+ when T_NUMBER then number
573
+ else astring
574
+ end
575
+ SP? or break
576
+ end
577
+ vals
578
+ end
579
+
580
+ # tagged-ext-simple is a subset of atom
581
+ # TODO: recognize sequence-set in the lexer
582
+ #
583
+ # tagged-ext-simple = sequence-set / number / number64
584
+ def tagged_ext_simple
585
+ number? || sequence_set
586
+ end
587
+
588
+ # tagged-ext-val = tagged-ext-simple /
589
+ # "(" [tagged-ext-comp] ")"
590
+ def tagged_ext_val
591
+ if lpar?
592
+ _ = peek_rpar? ? [] : tagged_ext_comp
593
+ rpar
594
+ _
595
+ else
596
+ tagged_ext_simple
597
+ end
598
+ end
599
+
600
+ # mailbox = "INBOX" / astring
601
+ # ; INBOX is case-insensitive. All case variants of
602
+ # ; INBOX (e.g., "iNbOx") MUST be interpreted as INBOX
603
+ # ; not as an astring. An astring which consists of
604
+ # ; the case-insensitive sequence "I" "N" "B" "O" "X"
605
+ # ; is considered to be INBOX and not an astring.
606
+ # ; Refer to section 5.1 for further
607
+ # ; semantic details of mailbox names.
608
+ alias mailbox astring
609
+
472
610
  # valid number ranges are not enforced by parser
473
611
  # number64 = 1*DIGIT
474
612
  # ; Unsigned 63-bit integer
@@ -494,6 +632,12 @@ module Net
494
632
  # ; Strictly ascending
495
633
  alias uniqueid nz_number
496
634
 
635
+ # valid number ranges are not enforced by parser
636
+ #
637
+ # a 64-bit unsigned integer and is the decimal equivalent for the ID hex
638
+ # string used in the web interface and the Gmail API.
639
+ alias x_gm_id number
640
+
497
641
  # [RFC3501 & RFC9051:]
498
642
  # response = *(continue-req / response-data) response-done
499
643
  #
@@ -630,34 +774,47 @@ module Net
630
774
 
631
775
  # RFC3501 & RFC9051:
632
776
  # response-tagged = tag SP resp-cond-state CRLF
633
- #
634
- # resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text
635
- # ; Status condition
636
- #
637
- # tag = 1*<any ASTRING-CHAR except "+">
638
777
  def response_tagged
639
- tag = tag(); SP!
640
- name = resp_cond_state__name; SP!
641
- TaggedResponse.new(tag, name, resp_text, @str)
778
+ TaggedResponse.new(tag, *(SP!; resp_cond_state), @str)
642
779
  end
643
780
 
644
781
  # RFC3501 & RFC9051:
645
782
  # resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text
783
+ #
784
+ # NOTE: In the spirit of RFC9051 Appx E 23 (and to workaround existing
785
+ # servers), we don't require a final SP and instead parse this as:
786
+ #
787
+ # resp-cond-state = ("OK" / "NO" / "BAD") [SP resp-text]
788
+ def resp_cond_state
789
+ [resp_cond_state__name, SP? ? resp_text : ResponseText::EMPTY]
790
+ end
791
+
646
792
  def resp_cond_state__untagged
647
- name = resp_cond_state__name; SP!
648
- UntaggedResponse.new(name, resp_text, @str)
793
+ UntaggedResponse.new(*resp_cond_state, @str)
649
794
  end
650
795
 
651
796
  # resp-cond-auth = ("OK" / "PREAUTH") SP resp-text
797
+ #
798
+ # NOTE: In the spirit of RFC9051 Appx E 23 (and to workaround existing
799
+ # servers), we don't require a final SP and instead parse this as:
800
+ #
801
+ # resp-cond-auth = ("OK" / "PREAUTH") [SP resp-text]
652
802
  def resp_cond_auth
653
- name = resp_cond_auth__name; SP!
654
- UntaggedResponse.new(name, resp_text, @str)
803
+ UntaggedResponse.new(resp_cond_auth__name,
804
+ SP? ? resp_text : ResponseText::EMPTY,
805
+ @str)
655
806
  end
656
807
 
657
808
  # resp-cond-bye = "BYE" SP resp-text
809
+ #
810
+ # NOTE: In the spirit of RFC9051 Appx E 23 (and to workaround existing
811
+ # servers), we don't require a final SP and instead parse this as:
812
+ #
813
+ # resp-cond-bye = "BYE" [SP resp-text]
658
814
  def resp_cond_bye
659
- name = label(BYE); SP!
660
- UntaggedResponse.new(name, resp_text, @str)
815
+ UntaggedResponse.new(label(BYE),
816
+ SP? ? resp_text : ResponseText::EMPTY,
817
+ @str)
661
818
  end
662
819
 
663
820
  # message-data = nz-number SP ("EXPUNGE" / ("FETCH" SP msg-att))
@@ -740,10 +897,17 @@ module Net
740
897
  when "ENVELOPE" then envelope
741
898
  when "INTERNALDATE" then date_time
742
899
  when "RFC822.SIZE" then number64
900
+ when /\ABINARY\[/ni then nstring8 # BINARY, IMAP4rev2
901
+ when /\ABINARY\.SIZE\[/ni then number # BINARY, IMAP4rev2
743
902
  when "RFC822" then nstring # not in rev2
744
903
  when "RFC822.HEADER" then nstring # not in rev2
745
904
  when "RFC822.TEXT" then nstring # not in rev2
746
905
  when "MODSEQ" then parens__modseq # CONDSTORE
906
+ when "EMAILID" then parens__objectid # OBJECTID
907
+ when "THREADID" then nparens__objectid # OBJECTID
908
+ when "X-GM-MSGID" then x_gm_id # GMail
909
+ when "X-GM-THRID" then x_gm_id # GMail
910
+ when "X-GM-LABELS" then x_gm_labels # GMail
747
911
  else parse_error("unknown attribute `%s' for {%d}", name, n)
748
912
  end
749
913
  attr[name] = val
@@ -762,46 +926,69 @@ module Net
762
926
  lbra? and rbra
763
927
  when "BODY"
764
928
  peek_lbra? and name << section and
765
- peek_str?("<") and name << atom # partial
929
+ peek_str?("<") and name << gt__number__lt # partial
930
+ when "BINARY", "BINARY.SIZE"
931
+ name << section_binary
932
+ # see https://www.rfc-editor.org/errata/eid7246 and the note above
933
+ peek_str?("<") and name << gt__number__lt # partial
766
934
  end
767
935
  name
768
936
  end
769
937
 
938
+ # this represents the partial size for BODY or BINARY
939
+ alias gt__number__lt atom
940
+
941
+ # RFC3501 & RFC9051:
942
+ # envelope = "(" env-date SP env-subject SP env-from SP
943
+ # env-sender SP env-reply-to SP env-to SP env-cc SP
944
+ # env-bcc SP env-in-reply-to SP env-message-id ")"
770
945
  def envelope
771
946
  @lex_state = EXPR_DATA
772
- token = lookahead
773
- if token.symbol == T_NIL
774
- shift_token
775
- result = nil
776
- else
777
- match(T_LPAR)
778
- date = nstring
779
- match(T_SPACE)
780
- subject = nstring
781
- match(T_SPACE)
782
- from = address_list
783
- match(T_SPACE)
784
- sender = address_list
785
- match(T_SPACE)
786
- reply_to = address_list
787
- match(T_SPACE)
788
- to = address_list
789
- match(T_SPACE)
790
- cc = address_list
791
- match(T_SPACE)
792
- bcc = address_list
793
- match(T_SPACE)
794
- in_reply_to = nstring
795
- match(T_SPACE)
796
- message_id = nstring
797
- match(T_RPAR)
798
- result = Envelope.new(date, subject, from, sender, reply_to,
799
- to, cc, bcc, in_reply_to, message_id)
800
- end
947
+ lpar; date = env_date
948
+ SP!; subject = env_subject
949
+ SP!; from = env_from
950
+ SP!; sender = env_sender
951
+ SP!; reply_to = env_reply_to
952
+ SP!; to = env_to
953
+ SP!; cc = env_cc
954
+ SP!; bcc = env_bcc
955
+ SP!; in_reply_to = env_in_reply_to
956
+ SP!; message_id = env_message_id
957
+ rpar
958
+ Envelope.new(date, subject, from, sender, reply_to,
959
+ to, cc, bcc, in_reply_to, message_id)
960
+ ensure
801
961
  @lex_state = EXPR_BEG
802
- return result
803
962
  end
804
963
 
964
+ # env-date = nstring
965
+ # env-subject = nstring
966
+ # env-in-reply-to = nstring
967
+ # env-message-id = nstring
968
+ alias env_date nstring
969
+ alias env_subject nstring
970
+ alias env_in_reply_to nstring
971
+ alias env_message_id nstring
972
+
973
+ # env-from = "(" 1*address ")" / nil
974
+ # env-sender = "(" 1*address ")" / nil
975
+ # env-reply-to = "(" 1*address ")" / nil
976
+ # env-to = "(" 1*address ")" / nil
977
+ # env-cc = "(" 1*address ")" / nil
978
+ # env-bcc = "(" 1*address ")" / nil
979
+ def nlist__address
980
+ return if NIL?
981
+ lpar; list = [address]; list << address until rpar?
982
+ list
983
+ end
984
+
985
+ alias env_from nlist__address
986
+ alias env_sender nlist__address
987
+ alias env_reply_to nlist__address
988
+ alias env_to nlist__address
989
+ alias env_cc nlist__address
990
+ alias env_bcc nlist__address
991
+
805
992
  # date-time = DQUOTE date-day-fixed "-" date-month "-" date-year
806
993
  # SP time SP zone DQUOTE
807
994
  alias date_time quoted
@@ -1070,6 +1257,13 @@ module Net
1070
1257
  str << rbra
1071
1258
  end
1072
1259
 
1260
+ # section-binary = "[" [section-part] "]"
1261
+ def section_binary
1262
+ str = +lbra
1263
+ str << section_part unless peek_rbra?
1264
+ str << rbra
1265
+ end
1266
+
1073
1267
  # section-spec = section-msgtext / (section-part ["." section-text])
1074
1268
  # section-msgtext = "HEADER" /
1075
1269
  # "HEADER.FIELDS" [".NOT"] SP header-list /
@@ -1100,6 +1294,11 @@ module Net
1100
1294
  str << rpar
1101
1295
  end
1102
1296
 
1297
+ # section-part = nz-number *("." nz-number)
1298
+ # ; body part reference.
1299
+ # ; Allows for accessing nested body parts.
1300
+ alias section_part atom
1301
+
1103
1302
  # RFC3501 & RFC9051:
1104
1303
  # header-fld-name = astring
1105
1304
  #
@@ -1148,18 +1347,17 @@ module Net
1148
1347
  alias mailbox_data__lsub mailbox_data__list
1149
1348
  alias mailbox_data__xlist mailbox_data__list
1150
1349
 
1350
+ # mailbox-list = "(" [mbx-list-flags] ")" SP
1351
+ # (DQUOTE QUOTED-CHAR DQUOTE / nil) SP mailbox
1352
+ # [SP mbox-list-extended]
1353
+ # ; This is the list information pointed to by the ABNF
1354
+ # ; item "mailbox-data", which is defined above
1151
1355
  def mailbox_list
1152
- attr = flag_list
1153
- match(T_SPACE)
1154
- token = match(T_QUOTED, T_NIL)
1155
- if token.symbol == T_NIL
1156
- delim = nil
1157
- else
1158
- delim = token.value
1159
- end
1160
- match(T_SPACE)
1161
- name = astring
1162
- return MailboxList.new(attr, delim, name)
1356
+ lpar; attr = peek_rpar? ? [] : mbx_list_flags; rpar
1357
+ SP!; delim = nquoted
1358
+ SP!; name = mailbox
1359
+ # TODO: mbox-list-extended
1360
+ MailboxList.new(attr, delim, name)
1163
1361
  end
1164
1362
 
1165
1363
  def getquota_response
@@ -1285,93 +1483,131 @@ module Net
1285
1483
  end
1286
1484
  alias sort_data mailbox_data__search
1287
1485
 
1486
+ # RFC5256: THREAD
1487
+ # thread-data = "THREAD" [SP 1*thread-list]
1288
1488
  def thread_data
1289
- token = match(T_ATOM)
1290
- name = token.value.upcase
1291
- token = lookahead
1292
-
1293
- if token.symbol == T_SPACE
1294
- threads = []
1295
-
1296
- while true
1297
- shift_token
1298
- token = lookahead
1299
-
1300
- case token.symbol
1301
- when T_LPAR
1302
- threads << thread_branch(token)
1303
- when T_CRLF
1304
- break
1305
- end
1306
- end
1307
- else
1308
- # no member
1309
- threads = []
1489
+ name = label("THREAD")
1490
+ threads = []
1491
+ if SP?
1492
+ threads << thread_list while lookahead_thread_list?
1310
1493
  end
1311
-
1312
- return UntaggedResponse.new(name, threads, @str)
1494
+ UntaggedResponse.new(name, threads, @str)
1313
1495
  end
1314
1496
 
1315
- def thread_branch(token)
1316
- rootmember = nil
1317
- lastmember = nil
1318
-
1319
- while true
1320
- shift_token # ignore first T_LPAR
1321
- token = lookahead
1497
+ alias lookahead_thread_list? lookahead_lpar?
1498
+ alias lookahead_thread_nested? lookahead_thread_list?
1322
1499
 
1323
- case token.symbol
1324
- when T_NUMBER
1325
- # new member
1326
- newmember = ThreadMember.new(number, [])
1327
- if rootmember.nil?
1328
- rootmember = newmember
1329
- else
1330
- lastmember.children << newmember
1331
- end
1332
- lastmember = newmember
1333
- when T_SPACE
1334
- # do nothing
1335
- when T_LPAR
1336
- if rootmember.nil?
1337
- # dummy member
1338
- lastmember = rootmember = ThreadMember.new(nil, [])
1339
- end
1500
+ # RFC5256: THREAD
1501
+ # thread-list = "(" (thread-members / thread-nested) ")"
1502
+ def thread_list
1503
+ lpar
1504
+ thread = if lookahead_thread_nested?
1505
+ ThreadMember.new(nil, thread_nested)
1506
+ else
1507
+ thread_members
1508
+ end
1509
+ rpar
1510
+ thread
1511
+ end
1340
1512
 
1341
- lastmember.children << thread_branch(token)
1342
- when T_RPAR
1343
- break
1513
+ # RFC5256: THREAD
1514
+ # thread-members = nz-number *(SP nz-number) [SP thread-nested]
1515
+ def thread_members
1516
+ members = []
1517
+ members << nz_number # thread root
1518
+ while SP?
1519
+ case lookahead!(T_NUMBER, T_LPAR).symbol
1520
+ when T_NUMBER then members << nz_number
1521
+ else nested = thread_nested; break
1344
1522
  end
1345
1523
  end
1524
+ members.reverse.inject(nested || []) {|subthreads, number|
1525
+ [ThreadMember.new(number, subthreads)]
1526
+ }.first
1527
+ end
1346
1528
 
1347
- return rootmember
1529
+ # RFC5256: THREAD
1530
+ # thread-nested = 2*thread-list
1531
+ def thread_nested
1532
+ nested = [thread_list, thread_list]
1533
+ while lookahead_thread_list? do nested << thread_list end
1534
+ nested
1348
1535
  end
1349
1536
 
1537
+ # mailbox-data =/ "STATUS" SP mailbox SP "(" [status-att-list] ")"
1350
1538
  def mailbox_data__status
1351
- token = match(T_ATOM)
1352
- name = token.value.upcase
1353
- match(T_SPACE)
1354
- mailbox = astring
1355
- match(T_SPACE)
1356
- match(T_LPAR)
1357
- attr = {}
1358
- while true
1359
- token = lookahead
1360
- case token.symbol
1361
- when T_RPAR
1362
- shift_token
1363
- break
1364
- when T_SPACE
1365
- shift_token
1539
+ resp_name = label("STATUS"); SP!
1540
+ mbox_name = mailbox; SP!
1541
+ lpar; attr = status_att_list; rpar
1542
+ UntaggedResponse.new(resp_name, StatusData.new(mbox_name, attr), @str)
1543
+ end
1544
+
1545
+ # RFC3501
1546
+ # status-att-list = status-att SP number *(SP status-att SP number)
1547
+ # RFC4466, RFC9051, and RFC3501 Errata
1548
+ # status-att-list = status-att-val *(SP status-att-val)
1549
+ def status_att_list
1550
+ attrs = [status_att_val]
1551
+ while SP? do attrs << status_att_val end
1552
+ attrs.to_h
1553
+ end
1554
+
1555
+ # RFC3501 Errata:
1556
+ # status-att-val = ("MESSAGES" SP number) / ("RECENT" SP number) /
1557
+ # ("UIDNEXT" SP nz-number) / ("UIDVALIDITY" SP nz-number) /
1558
+ # ("UNSEEN" SP number)
1559
+ # RFC4466:
1560
+ # status-att-val = ("MESSAGES" SP number) /
1561
+ # ("RECENT" SP number) /
1562
+ # ("UIDNEXT" SP nz-number) /
1563
+ # ("UIDVALIDITY" SP nz-number) /
1564
+ # ("UNSEEN" SP number)
1565
+ # ;; Extensions to the STATUS responses
1566
+ # ;; should extend this production.
1567
+ # ;; Extensions should use the generic
1568
+ # ;; syntax defined by tagged-ext.
1569
+ # RFC9051:
1570
+ # status-att-val = ("MESSAGES" SP number) /
1571
+ # ("UIDNEXT" SP nz-number) /
1572
+ # ("UIDVALIDITY" SP nz-number) /
1573
+ # ("UNSEEN" SP number) /
1574
+ # ("DELETED" SP number) /
1575
+ # ("SIZE" SP number64)
1576
+ # ; Extensions to the STATUS responses
1577
+ # ; should extend this production.
1578
+ # ; Extensions should use the generic
1579
+ # ; syntax defined by tagged-ext.
1580
+ # RFC7162:
1581
+ # status-att-val =/ "HIGHESTMODSEQ" SP mod-sequence-valzer
1582
+ # ;; Extends non-terminal defined in [RFC4466].
1583
+ # ;; Value 0 denotes that the mailbox doesn't
1584
+ # ;; support persistent mod-sequences
1585
+ # ;; as described in Section 3.1.2.2.
1586
+ # RFC7889:
1587
+ # status-att-val =/ "APPENDLIMIT" SP (number / nil)
1588
+ # ;; status-att-val is defined in RFC 4466
1589
+ # RFC8438:
1590
+ # status-att-val =/ "SIZE" SP number64
1591
+ # RFC8474:
1592
+ # status-att-val =/ "MAILBOXID" SP "(" objectid ")"
1593
+ # ; follows tagged-ext production from [RFC4466]
1594
+ def status_att_val
1595
+ key = tagged_ext_label
1596
+ SP!
1597
+ val =
1598
+ case key
1599
+ when "MESSAGES" then number # RFC3501, RFC9051
1600
+ when "UNSEEN" then number # RFC3501, RFC9051
1601
+ when "DELETED" then number # RFC3501, RFC9051
1602
+ when "UIDNEXT" then nz_number # RFC3501, RFC9051
1603
+ when "UIDVALIDITY" then nz_number # RFC3501, RFC9051
1604
+ when "RECENT" then number # RFC3501 (obsolete)
1605
+ when "SIZE" then number64 # RFC8483, RFC9051
1606
+ when "MAILBOXID" then parens__objectid # RFC8474
1607
+ else
1608
+ number? || ExtensionData.new(tagged_ext_val)
1366
1609
  end
1367
- token = match(T_ATOM)
1368
- key = token.value.upcase
1369
- match(T_SPACE)
1370
- val = number
1371
- attr[key] = val
1372
- end
1373
- data = StatusData.new(mailbox, attr)
1374
- return UntaggedResponse.new(name, data, @str)
1610
+ [key, val]
1375
1611
  end
1376
1612
 
1377
1613
  # The presence of "IMAP4rev1" or "IMAP4rev2" is unenforced here.
@@ -1573,6 +1809,9 @@ module Net
1573
1809
  # resp-text-code =/ "HIGHESTMODSEQ" SP mod-sequence-value /
1574
1810
  # "NOMODSEQ" /
1575
1811
  # "MODIFIED" SP sequence-set
1812
+ #
1813
+ # RFC8474: OBJECTID
1814
+ # resp-text-code =/ "MAILBOXID" SP "(" objectid ")"
1576
1815
  def resp_text_code
1577
1816
  name = resp_text_code__name
1578
1817
  data =
@@ -1592,6 +1831,7 @@ module Net
1592
1831
  "LIMIT", "OVERQUOTA", "ALREADYEXISTS", "NONEXISTENT", "CLOSED",
1593
1832
  "NOTSAVED", "UIDNOTSTICKY", "UNKNOWN-CTE", "HASCHILDREN"
1594
1833
  when "NOMODSEQ" # CONDSTORE
1834
+ when "MAILBOXID" then SP!; parens__objectid # RFC8474: OBJECTID
1595
1835
  else
1596
1836
  SP? and text_chars_except_rbra
1597
1837
  end
@@ -1638,61 +1878,40 @@ module Net
1638
1878
  UIDPlusData.new(validity, src_uids, dst_uids)
1639
1879
  end
1640
1880
 
1641
- def address_list
1642
- token = lookahead
1643
- if token.symbol == T_NIL
1644
- shift_token
1645
- return nil
1646
- else
1647
- result = []
1648
- match(T_LPAR)
1649
- while true
1650
- token = lookahead
1651
- case token.symbol
1652
- when T_RPAR
1653
- shift_token
1654
- break
1655
- when T_SPACE
1656
- shift_token
1657
- end
1658
- result.push(address)
1659
- end
1660
- return result
1661
- end
1662
- end
1663
-
1664
- ADDRESS_REGEXP = /\G\
1665
- (?# 1: NAME )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
1666
- (?# 2: ROUTE )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
1667
- (?# 3: MAILBOX )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
1668
- (?# 4: HOST )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)")\
1669
- \)/ni
1670
-
1881
+ ADDRESS_REGEXP = /\G
1882
+ \( (?: NIL | #{Patterns::QUOTED_rev2} ) # 1: NAME
1883
+ \s (?: NIL | #{Patterns::QUOTED_rev2} ) # 2: ROUTE
1884
+ \s (?: NIL | #{Patterns::QUOTED_rev2} ) # 3: MAILBOX
1885
+ \s (?: NIL | #{Patterns::QUOTED_rev2} ) # 4: HOST
1886
+ \)
1887
+ /nix
1888
+
1889
+ # address = "(" addr-name SP addr-adl SP addr-mailbox SP
1890
+ # addr-host ")"
1891
+ # addr-adl = nstring
1892
+ # addr-host = nstring
1893
+ # addr-mailbox = nstring
1894
+ # addr-name = nstring
1671
1895
  def address
1672
- match(T_LPAR)
1673
- if @str.index(ADDRESS_REGEXP, @pos)
1674
- # address does not include literal.
1675
- @pos = $~.end(0)
1676
- name = $1
1677
- route = $2
1678
- mailbox = $3
1679
- host = $4
1680
- for s in [name, route, mailbox, host]
1681
- Patterns.unescape_quoted! s
1682
- end
1683
- else
1684
- name = nstring
1685
- match(T_SPACE)
1686
- route = nstring
1687
- match(T_SPACE)
1688
- mailbox = nstring
1689
- match(T_SPACE)
1690
- host = nstring
1691
- match(T_RPAR)
1896
+ if (match = accept_re(ADDRESS_REGEXP))
1897
+ # note that "NIL" isn't captured by the regexp
1898
+ name, route, mailbox, host = match.captures
1899
+ .map { Patterns.unescape_quoted _1 }
1900
+ else # address may include literals
1901
+ lpar; name = addr_name
1902
+ SP!; route = addr_adl
1903
+ SP!; mailbox = addr_mailbox
1904
+ SP!; host = addr_host
1905
+ rpar
1692
1906
  end
1693
- return Address.new(name, route, mailbox, host)
1907
+ Address.new(name, route, mailbox, host)
1694
1908
  end
1695
1909
 
1910
+ alias addr_adl nstring
1911
+ alias addr_host nstring
1912
+ alias addr_mailbox nstring
1913
+ alias addr_name nstring
1914
+
1696
1915
  # flag-list = "(" [flag *(SP flag)] ")"
1697
1916
  def flag_list
1698
1917
  match_re(Patterns::FLAG_LIST, "flag-list")[1]
@@ -1707,22 +1926,23 @@ module Net
1707
1926
  .map! { _1.start_with?("\\") ? _1[1..].capitalize.to_sym : _1 }
1708
1927
  end
1709
1928
 
1710
- # Not checking for max one mbx-list-sflag in the parser.
1711
- # >>>
1712
- # mbx-list-flags = *(mbx-list-oflag SP) mbx-list-sflag
1713
- # *(SP mbx-list-oflag) /
1714
- # mbx-list-oflag *(SP mbx-list-oflag)
1715
- # mbx-list-oflag = "\Noinferiors" / child-mbox-flag /
1716
- # "\Subscribed" / "\Remote" / flag-extension
1717
- # ; Other flags; multiple from this list are
1718
- # ; possible per LIST response, but each flag
1719
- # ; can only appear once per LIST response
1720
- # mbx-list-sflag = "\NonExistent" / "\Noselect" / "\Marked" /
1721
- # "\Unmarked"
1722
- # ; Selectability flags; only one per LIST response
1723
- def parens__mbx_list_flags
1929
+ # See Patterns::MBX_LIST_FLAGS
1930
+ def mbx_list_flags
1724
1931
  match_re(Patterns::MBX_LIST_FLAGS, "mbx-list-flags")[1]
1725
- .split(nil).map! { _1.capitalize.to_sym }
1932
+ .split(nil).map! { _1[1..].capitalize.to_sym }
1933
+ end
1934
+
1935
+ # See https://developers.google.com/gmail/imap/imap-extensions
1936
+ def x_gm_label; accept(T_BSLASH) ? atom.capitalize.to_sym : astring end
1937
+
1938
+ # See https://developers.google.com/gmail/imap/imap-extensions
1939
+ def x_gm_labels
1940
+ lpar; return [] if rpar?
1941
+ labels = []
1942
+ labels << x_gm_label
1943
+ labels << x_gm_label while SP?
1944
+ rpar
1945
+ labels
1726
1946
  end
1727
1947
 
1728
1948
  # See https://www.rfc-editor.org/errata/rfc3501
@@ -1744,6 +1964,15 @@ module Net
1744
1964
 
1745
1965
  def parens__modseq; lpar; _ = permsg_modsequence; rpar; _ end
1746
1966
 
1967
+ # RFC8474:
1968
+ # objectid = 1*255(ALPHA / DIGIT / "_" / "-")
1969
+ # ; characters in object identifiers are case
1970
+ # ; significant
1971
+ alias objectid atom
1972
+
1973
+ def parens__objectid; lpar; _ = objectid; rpar; _ end
1974
+ def nparens__objectid; NIL? ? nil : parens__objectid end
1975
+
1747
1976
  # RFC-4315 (UIDPLUS) or RFC9051 (IMAP4rev2):
1748
1977
  # uid-set = (uniqueid / uid-range) *("," uid-set)
1749
1978
  # uid-range = (uniqueid ":" uniqueid)
@@ -1789,42 +2018,47 @@ module Net
1789
2018
  @pos = $~.end(0)
1790
2019
  if $1
1791
2020
  return Token.new(T_SPACE, $+)
1792
- elsif $2 && $6
2021
+ elsif $2
2022
+ len = $+.to_i
2023
+ val = @str[@pos, len]
2024
+ @pos += len
2025
+ return Token.new(T_LITERAL8, val)
2026
+ elsif $3 && $7
1793
2027
  # greedily match ATOM, prefixed with NUMBER, NIL, or PLUS.
1794
- return Token.new(T_ATOM, $2)
1795
- elsif $3
1796
- return Token.new(T_NIL, $+)
2028
+ return Token.new(T_ATOM, $3)
1797
2029
  elsif $4
1798
- return Token.new(T_NUMBER, $+)
2030
+ return Token.new(T_NIL, $+)
1799
2031
  elsif $5
2032
+ return Token.new(T_NUMBER, $+)
2033
+ elsif $6
1800
2034
  return Token.new(T_PLUS, $+)
1801
- elsif $7
2035
+ elsif $8
1802
2036
  # match ATOM, without a NUMBER, NIL, or PLUS prefix
1803
2037
  return Token.new(T_ATOM, $+)
1804
- elsif $8
1805
- return Token.new(T_QUOTED, Patterns.unescape_quoted($+))
1806
2038
  elsif $9
1807
- return Token.new(T_LPAR, $+)
2039
+ return Token.new(T_QUOTED, Patterns.unescape_quoted($+))
1808
2040
  elsif $10
1809
- return Token.new(T_RPAR, $+)
2041
+ return Token.new(T_LPAR, $+)
1810
2042
  elsif $11
1811
- return Token.new(T_BSLASH, $+)
2043
+ return Token.new(T_RPAR, $+)
1812
2044
  elsif $12
1813
- return Token.new(T_STAR, $+)
2045
+ return Token.new(T_BSLASH, $+)
1814
2046
  elsif $13
1815
- return Token.new(T_LBRA, $+)
2047
+ return Token.new(T_STAR, $+)
1816
2048
  elsif $14
1817
- return Token.new(T_RBRA, $+)
2049
+ return Token.new(T_LBRA, $+)
1818
2050
  elsif $15
2051
+ return Token.new(T_RBRA, $+)
2052
+ elsif $16
1819
2053
  len = $+.to_i
1820
2054
  val = @str[@pos, len]
1821
2055
  @pos += len
1822
2056
  return Token.new(T_LITERAL, val)
1823
- elsif $16
1824
- return Token.new(T_PERCENT, $+)
1825
2057
  elsif $17
1826
- return Token.new(T_CRLF, $+)
2058
+ return Token.new(T_PERCENT, $+)
1827
2059
  elsif $18
2060
+ return Token.new(T_CRLF, $+)
2061
+ elsif $19
1828
2062
  return Token.new(T_EOF, $+)
1829
2063
  else
1830
2064
  parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid")