net-imap 0.4.4 → 0.4.6

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