net-imap 0.4.4 → 0.4.7

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