net-imap 0.4.4 → 0.4.7

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,75 @@ 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 (quirky_SP?; 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
+
992
+ # Used when servers erroneously send an extra SP.
993
+ #
994
+ # As of 2023-11-28, Outlook.com (still) sends SP
995
+ # between +address+ in <tt>env-*</tt> lists.
996
+ alias quirky_SP? SP?
997
+
805
998
  # date-time = DQUOTE date-day-fixed "-" date-month "-" date-year
806
999
  # SP time SP zone DQUOTE
807
1000
  alias date_time quoted
@@ -1070,6 +1263,13 @@ module Net
1070
1263
  str << rbra
1071
1264
  end
1072
1265
 
1266
+ # section-binary = "[" [section-part] "]"
1267
+ def section_binary
1268
+ str = +lbra
1269
+ str << section_part unless peek_rbra?
1270
+ str << rbra
1271
+ end
1272
+
1073
1273
  # section-spec = section-msgtext / (section-part ["." section-text])
1074
1274
  # section-msgtext = "HEADER" /
1075
1275
  # "HEADER.FIELDS" [".NOT"] SP header-list /
@@ -1100,6 +1300,11 @@ module Net
1100
1300
  str << rpar
1101
1301
  end
1102
1302
 
1303
+ # section-part = nz-number *("." nz-number)
1304
+ # ; body part reference.
1305
+ # ; Allows for accessing nested body parts.
1306
+ alias section_part atom
1307
+
1103
1308
  # RFC3501 & RFC9051:
1104
1309
  # header-fld-name = astring
1105
1310
  #
@@ -1148,18 +1353,17 @@ module Net
1148
1353
  alias mailbox_data__lsub mailbox_data__list
1149
1354
  alias mailbox_data__xlist mailbox_data__list
1150
1355
 
1356
+ # mailbox-list = "(" [mbx-list-flags] ")" SP
1357
+ # (DQUOTE QUOTED-CHAR DQUOTE / nil) SP mailbox
1358
+ # [SP mbox-list-extended]
1359
+ # ; This is the list information pointed to by the ABNF
1360
+ # ; item "mailbox-data", which is defined above
1151
1361
  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)
1362
+ lpar; attr = peek_rpar? ? [] : mbx_list_flags; rpar
1363
+ SP!; delim = nquoted
1364
+ SP!; name = mailbox
1365
+ # TODO: mbox-list-extended
1366
+ MailboxList.new(attr, delim, name)
1163
1367
  end
1164
1368
 
1165
1369
  def getquota_response
@@ -1254,124 +1458,143 @@ module Net
1254
1458
  # mailbox-data = obsolete-search-response / ...
1255
1459
  # obsolete-search-response = "SEARCH" *(SP nz-number)
1256
1460
  def mailbox_data__search
1257
- token = match(T_ATOM)
1258
- name = token.value.upcase
1259
- token = lookahead
1260
- if token.symbol == T_SPACE
1261
- shift_token
1262
- data = []
1263
- while true
1264
- token = lookahead
1265
- case token.symbol
1266
- when T_CRLF
1267
- break
1268
- when T_SPACE
1269
- shift_token
1270
- when T_NUMBER
1271
- data.push(number)
1272
- when T_LPAR
1273
- # TODO: include the MODSEQ value in a response
1274
- shift_token
1275
- match(T_ATOM)
1276
- match(T_SPACE)
1277
- match(T_NUMBER)
1278
- match(T_RPAR)
1279
- end
1280
- end
1281
- else
1282
- data = []
1461
+ name = label_in("SEARCH", "SORT")
1462
+ data = []
1463
+ while _ = SP? && nz_number? do data << _ end
1464
+ if lpar?
1465
+ label("MODSEQ"); SP!
1466
+ mod_sequence_value
1467
+ rpar
1283
1468
  end
1284
- return UntaggedResponse.new(name, data, @str)
1469
+ UntaggedResponse.new(name, data, @str)
1285
1470
  end
1286
1471
  alias sort_data mailbox_data__search
1287
1472
 
1473
+ # RFC5256: THREAD
1474
+ # thread-data = "THREAD" [SP 1*thread-list]
1288
1475
  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 = []
1476
+ name = label("THREAD")
1477
+ threads = []
1478
+ if SP?
1479
+ threads << thread_list while lookahead_thread_list?
1310
1480
  end
1311
-
1312
- return UntaggedResponse.new(name, threads, @str)
1481
+ UntaggedResponse.new(name, threads, @str)
1313
1482
  end
1314
1483
 
1315
- def thread_branch(token)
1316
- rootmember = nil
1317
- lastmember = nil
1484
+ alias lookahead_thread_list? lookahead_lpar?
1485
+ alias lookahead_thread_nested? lookahead_thread_list?
1318
1486
 
1319
- while true
1320
- shift_token # ignore first T_LPAR
1321
- token = lookahead
1322
-
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
1487
+ # RFC5256: THREAD
1488
+ # thread-list = "(" (thread-members / thread-nested) ")"
1489
+ def thread_list
1490
+ lpar
1491
+ thread = if lookahead_thread_nested?
1492
+ ThreadMember.new(nil, thread_nested)
1493
+ else
1494
+ thread_members
1495
+ end
1496
+ rpar
1497
+ thread
1498
+ end
1340
1499
 
1341
- lastmember.children << thread_branch(token)
1342
- when T_RPAR
1343
- break
1500
+ # RFC5256: THREAD
1501
+ # thread-members = nz-number *(SP nz-number) [SP thread-nested]
1502
+ def thread_members
1503
+ members = []
1504
+ members << nz_number # thread root
1505
+ while SP?
1506
+ case lookahead!(T_NUMBER, T_LPAR).symbol
1507
+ when T_NUMBER then members << nz_number
1508
+ else nested = thread_nested; break
1344
1509
  end
1345
1510
  end
1511
+ members.reverse.inject(nested || []) {|subthreads, number|
1512
+ [ThreadMember.new(number, subthreads)]
1513
+ }.first
1514
+ end
1346
1515
 
1347
- return rootmember
1516
+ # RFC5256: THREAD
1517
+ # thread-nested = 2*thread-list
1518
+ def thread_nested
1519
+ nested = [thread_list, thread_list]
1520
+ while lookahead_thread_list? do nested << thread_list end
1521
+ nested
1348
1522
  end
1349
1523
 
1524
+ # mailbox-data =/ "STATUS" SP mailbox SP "(" [status-att-list] ")"
1350
1525
  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
1526
+ resp_name = label("STATUS"); SP!
1527
+ mbox_name = mailbox; SP!
1528
+ lpar; attr = status_att_list; rpar
1529
+ UntaggedResponse.new(resp_name, StatusData.new(mbox_name, attr), @str)
1530
+ end
1531
+
1532
+ # RFC3501
1533
+ # status-att-list = status-att SP number *(SP status-att SP number)
1534
+ # RFC4466, RFC9051, and RFC3501 Errata
1535
+ # status-att-list = status-att-val *(SP status-att-val)
1536
+ def status_att_list
1537
+ attrs = [status_att_val]
1538
+ while SP? do attrs << status_att_val end
1539
+ attrs.to_h
1540
+ end
1541
+
1542
+ # RFC3501 Errata:
1543
+ # status-att-val = ("MESSAGES" SP number) / ("RECENT" SP number) /
1544
+ # ("UIDNEXT" SP nz-number) / ("UIDVALIDITY" SP nz-number) /
1545
+ # ("UNSEEN" SP number)
1546
+ # RFC4466:
1547
+ # status-att-val = ("MESSAGES" SP number) /
1548
+ # ("RECENT" SP number) /
1549
+ # ("UIDNEXT" SP nz-number) /
1550
+ # ("UIDVALIDITY" SP nz-number) /
1551
+ # ("UNSEEN" SP number)
1552
+ # ;; Extensions to the STATUS responses
1553
+ # ;; should extend this production.
1554
+ # ;; Extensions should use the generic
1555
+ # ;; syntax defined by tagged-ext.
1556
+ # RFC9051:
1557
+ # status-att-val = ("MESSAGES" SP number) /
1558
+ # ("UIDNEXT" SP nz-number) /
1559
+ # ("UIDVALIDITY" SP nz-number) /
1560
+ # ("UNSEEN" SP number) /
1561
+ # ("DELETED" SP number) /
1562
+ # ("SIZE" SP number64)
1563
+ # ; Extensions to the STATUS responses
1564
+ # ; should extend this production.
1565
+ # ; Extensions should use the generic
1566
+ # ; syntax defined by tagged-ext.
1567
+ # RFC7162:
1568
+ # status-att-val =/ "HIGHESTMODSEQ" SP mod-sequence-valzer
1569
+ # ;; Extends non-terminal defined in [RFC4466].
1570
+ # ;; Value 0 denotes that the mailbox doesn't
1571
+ # ;; support persistent mod-sequences
1572
+ # ;; as described in Section 3.1.2.2.
1573
+ # RFC7889:
1574
+ # status-att-val =/ "APPENDLIMIT" SP (number / nil)
1575
+ # ;; status-att-val is defined in RFC 4466
1576
+ # RFC8438:
1577
+ # status-att-val =/ "SIZE" SP number64
1578
+ # RFC8474:
1579
+ # status-att-val =/ "MAILBOXID" SP "(" objectid ")"
1580
+ # ; follows tagged-ext production from [RFC4466]
1581
+ def status_att_val
1582
+ key = tagged_ext_label
1583
+ SP!
1584
+ val =
1585
+ case key
1586
+ when "MESSAGES" then number # RFC3501, RFC9051
1587
+ when "UNSEEN" then number # RFC3501, RFC9051
1588
+ when "DELETED" then number # RFC3501, RFC9051
1589
+ when "UIDNEXT" then nz_number # RFC3501, RFC9051
1590
+ when "UIDVALIDITY" then nz_number # RFC3501, RFC9051
1591
+ when "RECENT" then number # RFC3501 (obsolete)
1592
+ when "SIZE" then number64 # RFC8483, RFC9051
1593
+ when "MAILBOXID" then parens__objectid # RFC8474
1594
+ else
1595
+ number? || ExtensionData.new(tagged_ext_val)
1366
1596
  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)
1597
+ [key, val]
1375
1598
  end
1376
1599
 
1377
1600
  # The presence of "IMAP4rev1" or "IMAP4rev2" is unenforced here.
@@ -1573,6 +1796,9 @@ module Net
1573
1796
  # resp-text-code =/ "HIGHESTMODSEQ" SP mod-sequence-value /
1574
1797
  # "NOMODSEQ" /
1575
1798
  # "MODIFIED" SP sequence-set
1799
+ #
1800
+ # RFC8474: OBJECTID
1801
+ # resp-text-code =/ "MAILBOXID" SP "(" objectid ")"
1576
1802
  def resp_text_code
1577
1803
  name = resp_text_code__name
1578
1804
  data =
@@ -1592,6 +1818,7 @@ module Net
1592
1818
  "LIMIT", "OVERQUOTA", "ALREADYEXISTS", "NONEXISTENT", "CLOSED",
1593
1819
  "NOTSAVED", "UIDNOTSTICKY", "UNKNOWN-CTE", "HASCHILDREN"
1594
1820
  when "NOMODSEQ" # CONDSTORE
1821
+ when "MAILBOXID" then SP!; parens__objectid # RFC8474: OBJECTID
1595
1822
  else
1596
1823
  SP? and text_chars_except_rbra
1597
1824
  end
@@ -1638,61 +1865,40 @@ module Net
1638
1865
  UIDPlusData.new(validity, src_uids, dst_uids)
1639
1866
  end
1640
1867
 
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
-
1868
+ ADDRESS_REGEXP = /\G
1869
+ \( (?: NIL | #{Patterns::QUOTED_rev2} ) # 1: NAME
1870
+ \s (?: NIL | #{Patterns::QUOTED_rev2} ) # 2: ROUTE
1871
+ \s (?: NIL | #{Patterns::QUOTED_rev2} ) # 3: MAILBOX
1872
+ \s (?: NIL | #{Patterns::QUOTED_rev2} ) # 4: HOST
1873
+ \)
1874
+ /nix
1875
+
1876
+ # address = "(" addr-name SP addr-adl SP addr-mailbox SP
1877
+ # addr-host ")"
1878
+ # addr-adl = nstring
1879
+ # addr-host = nstring
1880
+ # addr-mailbox = nstring
1881
+ # addr-name = nstring
1671
1882
  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)
1883
+ if (match = accept_re(ADDRESS_REGEXP))
1884
+ # note that "NIL" isn't captured by the regexp
1885
+ name, route, mailbox, host = match.captures
1886
+ .map { Patterns.unescape_quoted _1 }
1887
+ else # address may include literals
1888
+ lpar; name = addr_name
1889
+ SP!; route = addr_adl
1890
+ SP!; mailbox = addr_mailbox
1891
+ SP!; host = addr_host
1892
+ rpar
1692
1893
  end
1693
- return Address.new(name, route, mailbox, host)
1894
+ Address.new(name, route, mailbox, host)
1694
1895
  end
1695
1896
 
1897
+ alias addr_adl nstring
1898
+ alias addr_host nstring
1899
+ alias addr_mailbox nstring
1900
+ alias addr_name nstring
1901
+
1696
1902
  # flag-list = "(" [flag *(SP flag)] ")"
1697
1903
  def flag_list
1698
1904
  match_re(Patterns::FLAG_LIST, "flag-list")[1]
@@ -1707,22 +1913,23 @@ module Net
1707
1913
  .map! { _1.start_with?("\\") ? _1[1..].capitalize.to_sym : _1 }
1708
1914
  end
1709
1915
 
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
1916
+ # See Patterns::MBX_LIST_FLAGS
1917
+ def mbx_list_flags
1724
1918
  match_re(Patterns::MBX_LIST_FLAGS, "mbx-list-flags")[1]
1725
- .split(nil).map! { _1.capitalize.to_sym }
1919
+ .split(nil).map! { _1[1..].capitalize.to_sym }
1920
+ end
1921
+
1922
+ # See https://developers.google.com/gmail/imap/imap-extensions
1923
+ def x_gm_label; accept(T_BSLASH) ? atom.capitalize.to_sym : astring end
1924
+
1925
+ # See https://developers.google.com/gmail/imap/imap-extensions
1926
+ def x_gm_labels
1927
+ lpar; return [] if rpar?
1928
+ labels = []
1929
+ labels << x_gm_label
1930
+ labels << x_gm_label while SP?
1931
+ rpar
1932
+ labels
1726
1933
  end
1727
1934
 
1728
1935
  # See https://www.rfc-editor.org/errata/rfc3501
@@ -1744,6 +1951,15 @@ module Net
1744
1951
 
1745
1952
  def parens__modseq; lpar; _ = permsg_modsequence; rpar; _ end
1746
1953
 
1954
+ # RFC8474:
1955
+ # objectid = 1*255(ALPHA / DIGIT / "_" / "-")
1956
+ # ; characters in object identifiers are case
1957
+ # ; significant
1958
+ alias objectid atom
1959
+
1960
+ def parens__objectid; lpar; _ = objectid; rpar; _ end
1961
+ def nparens__objectid; NIL? ? nil : parens__objectid end
1962
+
1747
1963
  # RFC-4315 (UIDPLUS) or RFC9051 (IMAP4rev2):
1748
1964
  # uid-set = (uniqueid / uid-range) *("," uid-set)
1749
1965
  # uid-range = (uniqueid ":" uniqueid)
@@ -1789,42 +2005,47 @@ module Net
1789
2005
  @pos = $~.end(0)
1790
2006
  if $1
1791
2007
  return Token.new(T_SPACE, $+)
1792
- elsif $2 && $6
2008
+ elsif $2
2009
+ len = $+.to_i
2010
+ val = @str[@pos, len]
2011
+ @pos += len
2012
+ return Token.new(T_LITERAL8, val)
2013
+ elsif $3 && $7
1793
2014
  # 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, $+)
2015
+ return Token.new(T_ATOM, $3)
1797
2016
  elsif $4
1798
- return Token.new(T_NUMBER, $+)
2017
+ return Token.new(T_NIL, $+)
1799
2018
  elsif $5
2019
+ return Token.new(T_NUMBER, $+)
2020
+ elsif $6
1800
2021
  return Token.new(T_PLUS, $+)
1801
- elsif $7
2022
+ elsif $8
1802
2023
  # match ATOM, without a NUMBER, NIL, or PLUS prefix
1803
2024
  return Token.new(T_ATOM, $+)
1804
- elsif $8
1805
- return Token.new(T_QUOTED, Patterns.unescape_quoted($+))
1806
2025
  elsif $9
1807
- return Token.new(T_LPAR, $+)
2026
+ return Token.new(T_QUOTED, Patterns.unescape_quoted($+))
1808
2027
  elsif $10
1809
- return Token.new(T_RPAR, $+)
2028
+ return Token.new(T_LPAR, $+)
1810
2029
  elsif $11
1811
- return Token.new(T_BSLASH, $+)
2030
+ return Token.new(T_RPAR, $+)
1812
2031
  elsif $12
1813
- return Token.new(T_STAR, $+)
2032
+ return Token.new(T_BSLASH, $+)
1814
2033
  elsif $13
1815
- return Token.new(T_LBRA, $+)
2034
+ return Token.new(T_STAR, $+)
1816
2035
  elsif $14
1817
- return Token.new(T_RBRA, $+)
2036
+ return Token.new(T_LBRA, $+)
1818
2037
  elsif $15
2038
+ return Token.new(T_RBRA, $+)
2039
+ elsif $16
1819
2040
  len = $+.to_i
1820
2041
  val = @str[@pos, len]
1821
2042
  @pos += len
1822
2043
  return Token.new(T_LITERAL, val)
1823
- elsif $16
1824
- return Token.new(T_PERCENT, $+)
1825
2044
  elsif $17
1826
- return Token.new(T_CRLF, $+)
2045
+ return Token.new(T_PERCENT, $+)
1827
2046
  elsif $18
2047
+ return Token.new(T_CRLF, $+)
2048
+ elsif $19
1828
2049
  return Token.new(T_EOF, $+)
1829
2050
  else
1830
2051
  parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid")