net-imap 0.4.4 → 0.4.9

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,19 @@ 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
233
+
234
+ # Gmail allows SP and "]" in flags.......
235
+ QUIRKY_FLAG = Regexp.union(/\\?#{ASTRING_CHARS}/n, "\\*")
236
+ QUIRKY_FLAGS_LIST = /\G\(( [^)]* )\)/nx
235
237
 
236
238
  # RFC3501:
237
239
  # QUOTED-CHAR = <any TEXT-CHAR except quoted-specials> /
@@ -266,6 +268,56 @@ module Net
266
268
  # ; Is a valid RFC 3501 "atom".
267
269
  TAGGED_EXT_LABEL = /#{TAGGED_LABEL_FCHAR}#{TAGGED_LABEL_CHAR}*/n
268
270
 
271
+ # nz-number = digit-nz *DIGIT
272
+ # ; Non-zero unsigned 32-bit integer
273
+ # ; (0 < n < 4,294,967,296)
274
+ NZ_NUMBER = /[1-9]\d*/n
275
+
276
+ # seq-number = nz-number / "*"
277
+ # ; message sequence number (COPY, FETCH, STORE
278
+ # ; commands) or unique identifier (UID COPY,
279
+ # ; UID FETCH, UID STORE commands).
280
+ # ; * represents the largest number in use. In
281
+ # ; the case of message sequence numbers, it is
282
+ # ; the number of messages in a non-empty mailbox.
283
+ # ; In the case of unique identifiers, it is the
284
+ # ; unique identifier of the last message in the
285
+ # ; mailbox or, if the mailbox is empty, the
286
+ # ; mailbox's current UIDNEXT value.
287
+ # ; The server should respond with a tagged BAD
288
+ # ; response to a command that uses a message
289
+ # ; sequence number greater than the number of
290
+ # ; messages in the selected mailbox. This
291
+ # ; includes "*" if the selected mailbox is empty.
292
+ SEQ_NUMBER = /#{NZ_NUMBER}|\*/n
293
+
294
+ # seq-range = seq-number ":" seq-number
295
+ # ; two seq-number values and all values between
296
+ # ; these two regardless of order.
297
+ # ; Example: 2:4 and 4:2 are equivalent and
298
+ # ; indicate values 2, 3, and 4.
299
+ # ; Example: a unique identifier sequence range of
300
+ # ; 3291:* includes the UID of the last message in
301
+ # ; the mailbox, even if that value is less than
302
+ # ; 3291.
303
+ SEQ_RANGE = /#{SEQ_NUMBER}:#{SEQ_NUMBER}/n
304
+
305
+ # sequence-set = (seq-number / seq-range) ["," sequence-set]
306
+ # ; set of seq-number values, regardless of order.
307
+ # ; Servers MAY coalesce overlaps and/or execute
308
+ # ; the sequence in any order.
309
+ # ; Example: a message sequence number set of
310
+ # ; 2,4:7,9,12:* for a mailbox with 15 messages is
311
+ # ; equivalent to 2,4,5,6,7,9,12,13,14,15
312
+ # ; Example: a message sequence number set of
313
+ # ; *:4,5:7 for a mailbox with 10 messages is
314
+ # ; equivalent to 10,9,8,7,6,5,4,5,6,7 and MAY
315
+ # ; be reordered and overlap coalesced to be
316
+ # ; 4,5,6,7,8,9,10.
317
+ SEQUENCE_SET_ITEM = /#{SEQ_NUMBER}|#{SEQ_RANGE}/n
318
+ SEQUENCE_SET = /#{SEQUENCE_SET_ITEM}(?:,#{SEQUENCE_SET_ITEM})*/n
319
+ SEQUENCE_SET_STR = /\A#{SEQUENCE_SET}\z/n
320
+
269
321
  # RFC3501:
270
322
  # literal = "{" number "}" CRLF *CHAR8
271
323
  # ; Number represents the number of CHAR8s
@@ -279,6 +331,16 @@ module Net
279
331
  # ; sent from server to the client.
280
332
  LITERAL = /\{(\d+)\}\r\n/n
281
333
 
334
+ # RFC3516 (BINARY):
335
+ # literal8 = "~{" number "}" CRLF *OCTET
336
+ # ; <number> represents the number of OCTETs
337
+ # ; in the response string.
338
+ # RFC9051:
339
+ # literal8 = "~{" number64 "}" CRLF *OCTET
340
+ # ; <number64> represents the number of OCTETs
341
+ # ; in the response string.
342
+ LITERAL8 = /~\{(\d+)\}\r\n/n
343
+
282
344
  module_function
283
345
 
284
346
  def unescape_quoted!(quoted)
@@ -298,27 +360,28 @@ module Net
298
360
  # the default, used in most places
299
361
  BEG_REGEXP = /\G(?:\
300
362
  (?# 1: SPACE )( )|\
301
- (?# 2: ATOM prefixed with a compatible subtype)\
363
+ (?# 2: LITERAL8)#{Patterns::LITERAL8}|\
364
+ (?# 3: ATOM prefixed with a compatible subtype)\
302
365
  ((?:\
303
- (?# 3: NIL )(NIL)|\
304
- (?# 4: NUMBER )(\d+)|\
305
- (?# 5: PLUS )(\+))\
306
- (?# 6: ATOM remaining after prefix )(#{Patterns::ATOMISH})?\
366
+ (?# 4: NIL )(NIL)|\
367
+ (?# 5: NUMBER )(\d+)|\
368
+ (?# 6: PLUS )(\+))\
369
+ (?# 7: ATOM remaining after prefix )(#{Patterns::ATOMISH})?\
307
370
  (?# This enables greedy alternation without lookahead, in linear time.)\
308
371
  )|\
309
372
  (?# 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
373
+ (?# 8: ATOM )(#{Patterns::ATOMISH})|\
374
+ (?# 9: QUOTED )#{Patterns::QUOTED_rev2}|\
375
+ (?# 10: LPAR )(\()|\
376
+ (?# 11: RPAR )(\))|\
377
+ (?# 12: BSLASH )(\\)|\
378
+ (?# 13: STAR )(\*)|\
379
+ (?# 14: LBRA )(\[)|\
380
+ (?# 15: RBRA )(\])|\
381
+ (?# 16: LITERAL )#{Patterns::LITERAL}|\
382
+ (?# 17: PERCENT )(%)|\
383
+ (?# 18: CRLF )(\r\n)|\
384
+ (?# 19: EOF )(\z))/ni
322
385
 
323
386
  # envelope, body(structure), namespaces
324
387
  DATA_REGEXP = /\G(?:\
@@ -359,6 +422,9 @@ module Net
359
422
  # string = quoted / literal
360
423
  def_token_matchers :string, T_QUOTED, T_LITERAL
361
424
 
425
+ # used by nstring8 = nstring / literal8
426
+ def_token_matchers :string8, T_QUOTED, T_LITERAL, T_LITERAL8
427
+
362
428
  # use where string represents "LABEL" values
363
429
  def_token_matchers :case_insensitive__string,
364
430
  T_QUOTED, T_LITERAL,
@@ -390,6 +456,24 @@ module Net
390
456
  # ATOM-CHAR = <any CHAR except atom-specials>
391
457
  ATOM_TOKENS = [T_ATOM, T_NUMBER, T_NIL, T_LBRA, T_PLUS]
392
458
 
459
+ SEQUENCE_SET_TOKENS = [T_ATOM, T_NUMBER, T_STAR]
460
+
461
+ # sequence-set = (seq-number / seq-range) ["," sequence-set]
462
+ # sequence-set =/ seq-last-command
463
+ # ; Allow for "result of the last command"
464
+ # ; indicator.
465
+ # seq-last-command = "$"
466
+ #
467
+ # *note*: doesn't match seq-last-command
468
+ def sequence_set
469
+ str = combine_adjacent(*SEQUENCE_SET_TOKENS)
470
+ if Patterns::SEQUENCE_SET_STR.match?(str)
471
+ SequenceSet[str]
472
+ else
473
+ parse_error("unexpected atom %p, expected sequence-set", str)
474
+ end
475
+ end
476
+
393
477
  # ASTRING-CHAR = ATOM-CHAR / resp-specials
394
478
  # resp-specials = "]"
395
479
  ASTRING_CHARS_TOKENS = [*ATOM_TOKENS, T_RBRA].freeze
@@ -460,6 +544,10 @@ module Net
460
544
  NIL? ? nil : string
461
545
  end
462
546
 
547
+ def nstring8
548
+ NIL? ? nil : string8
549
+ end
550
+
463
551
  def nquoted
464
552
  NIL? ? nil : quoted
465
553
  end
@@ -469,6 +557,60 @@ module Net
469
557
  NIL? ? nil : case_insensitive__string
470
558
  end
471
559
 
560
+ # tagged-ext-comp = astring /
561
+ # tagged-ext-comp *(SP tagged-ext-comp) /
562
+ # "(" tagged-ext-comp ")"
563
+ # ; Extensions that follow this general
564
+ # ; syntax should use nstring instead of
565
+ # ; astring when appropriate in the context
566
+ # ; of the extension.
567
+ # ; Note that a message set or a "number"
568
+ # ; can always be represented as an "atom".
569
+ # ; A URL should be represented as
570
+ # ; a "quoted" string.
571
+ def tagged_ext_comp
572
+ vals = []
573
+ while true
574
+ vals << case lookahead!(*ASTRING_TOKENS, T_LPAR).symbol
575
+ when T_LPAR then lpar; ary = tagged_ext_comp; rpar; ary
576
+ when T_NUMBER then number
577
+ else astring
578
+ end
579
+ SP? or break
580
+ end
581
+ vals
582
+ end
583
+
584
+ # tagged-ext-simple is a subset of atom
585
+ # TODO: recognize sequence-set in the lexer
586
+ #
587
+ # tagged-ext-simple = sequence-set / number / number64
588
+ def tagged_ext_simple
589
+ number? || sequence_set
590
+ end
591
+
592
+ # tagged-ext-val = tagged-ext-simple /
593
+ # "(" [tagged-ext-comp] ")"
594
+ def tagged_ext_val
595
+ if lpar?
596
+ _ = peek_rpar? ? [] : tagged_ext_comp
597
+ rpar
598
+ _
599
+ else
600
+ tagged_ext_simple
601
+ end
602
+ end
603
+
604
+ # mailbox = "INBOX" / astring
605
+ # ; INBOX is case-insensitive. All case variants of
606
+ # ; INBOX (e.g., "iNbOx") MUST be interpreted as INBOX
607
+ # ; not as an astring. An astring which consists of
608
+ # ; the case-insensitive sequence "I" "N" "B" "O" "X"
609
+ # ; is considered to be INBOX and not an astring.
610
+ # ; Refer to section 5.1 for further
611
+ # ; semantic details of mailbox names.
612
+ alias mailbox astring
613
+
472
614
  # valid number ranges are not enforced by parser
473
615
  # number64 = 1*DIGIT
474
616
  # ; Unsigned 63-bit integer
@@ -494,6 +636,12 @@ module Net
494
636
  # ; Strictly ascending
495
637
  alias uniqueid nz_number
496
638
 
639
+ # valid number ranges are not enforced by parser
640
+ #
641
+ # a 64-bit unsigned integer and is the decimal equivalent for the ID hex
642
+ # string used in the web interface and the Gmail API.
643
+ alias x_gm_id number
644
+
497
645
  # [RFC3501 & RFC9051:]
498
646
  # response = *(continue-req / response-data) response-done
499
647
  #
@@ -630,34 +778,47 @@ module Net
630
778
 
631
779
  # RFC3501 & RFC9051:
632
780
  # 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
781
  def response_tagged
639
- tag = tag(); SP!
640
- name = resp_cond_state__name; SP!
641
- TaggedResponse.new(tag, name, resp_text, @str)
782
+ TaggedResponse.new(tag, *(SP!; resp_cond_state), @str)
642
783
  end
643
784
 
644
785
  # RFC3501 & RFC9051:
645
786
  # resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text
787
+ #
788
+ # NOTE: In the spirit of RFC9051 Appx E 23 (and to workaround existing
789
+ # servers), we don't require a final SP and instead parse this as:
790
+ #
791
+ # resp-cond-state = ("OK" / "NO" / "BAD") [SP resp-text]
792
+ def resp_cond_state
793
+ [resp_cond_state__name, SP? ? resp_text : ResponseText::EMPTY]
794
+ end
795
+
646
796
  def resp_cond_state__untagged
647
- name = resp_cond_state__name; SP!
648
- UntaggedResponse.new(name, resp_text, @str)
797
+ UntaggedResponse.new(*resp_cond_state, @str)
649
798
  end
650
799
 
651
800
  # resp-cond-auth = ("OK" / "PREAUTH") SP resp-text
801
+ #
802
+ # NOTE: In the spirit of RFC9051 Appx E 23 (and to workaround existing
803
+ # servers), we don't require a final SP and instead parse this as:
804
+ #
805
+ # resp-cond-auth = ("OK" / "PREAUTH") [SP resp-text]
652
806
  def resp_cond_auth
653
- name = resp_cond_auth__name; SP!
654
- UntaggedResponse.new(name, resp_text, @str)
807
+ UntaggedResponse.new(resp_cond_auth__name,
808
+ SP? ? resp_text : ResponseText::EMPTY,
809
+ @str)
655
810
  end
656
811
 
657
812
  # resp-cond-bye = "BYE" SP resp-text
813
+ #
814
+ # NOTE: In the spirit of RFC9051 Appx E 23 (and to workaround existing
815
+ # servers), we don't require a final SP and instead parse this as:
816
+ #
817
+ # resp-cond-bye = "BYE" [SP resp-text]
658
818
  def resp_cond_bye
659
- name = label(BYE); SP!
660
- UntaggedResponse.new(name, resp_text, @str)
819
+ UntaggedResponse.new(label(BYE),
820
+ SP? ? resp_text : ResponseText::EMPTY,
821
+ @str)
661
822
  end
662
823
 
663
824
  # message-data = nz-number SP ("EXPUNGE" / ("FETCH" SP msg-att))
@@ -740,10 +901,17 @@ module Net
740
901
  when "ENVELOPE" then envelope
741
902
  when "INTERNALDATE" then date_time
742
903
  when "RFC822.SIZE" then number64
904
+ when /\ABINARY\[/ni then nstring8 # BINARY, IMAP4rev2
905
+ when /\ABINARY\.SIZE\[/ni then number # BINARY, IMAP4rev2
743
906
  when "RFC822" then nstring # not in rev2
744
907
  when "RFC822.HEADER" then nstring # not in rev2
745
908
  when "RFC822.TEXT" then nstring # not in rev2
746
909
  when "MODSEQ" then parens__modseq # CONDSTORE
910
+ when "EMAILID" then parens__objectid # OBJECTID
911
+ when "THREADID" then nparens__objectid # OBJECTID
912
+ when "X-GM-MSGID" then x_gm_id # GMail
913
+ when "X-GM-THRID" then x_gm_id # GMail
914
+ when "X-GM-LABELS" then x_gm_labels # GMail
747
915
  else parse_error("unknown attribute `%s' for {%d}", name, n)
748
916
  end
749
917
  attr[name] = val
@@ -762,46 +930,75 @@ module Net
762
930
  lbra? and rbra
763
931
  when "BODY"
764
932
  peek_lbra? and name << section and
765
- peek_str?("<") and name << atom # partial
933
+ peek_str?("<") and name << gt__number__lt # partial
934
+ when "BINARY", "BINARY.SIZE"
935
+ name << section_binary
936
+ # see https://www.rfc-editor.org/errata/eid7246 and the note above
937
+ peek_str?("<") and name << gt__number__lt # partial
766
938
  end
767
939
  name
768
940
  end
769
941
 
942
+ # this represents the partial size for BODY or BINARY
943
+ alias gt__number__lt atom
944
+
945
+ # RFC3501 & RFC9051:
946
+ # envelope = "(" env-date SP env-subject SP env-from SP
947
+ # env-sender SP env-reply-to SP env-to SP env-cc SP
948
+ # env-bcc SP env-in-reply-to SP env-message-id ")"
770
949
  def envelope
771
950
  @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
951
+ lpar; date = env_date
952
+ SP!; subject = env_subject
953
+ SP!; from = env_from
954
+ SP!; sender = env_sender
955
+ SP!; reply_to = env_reply_to
956
+ SP!; to = env_to
957
+ SP!; cc = env_cc
958
+ SP!; bcc = env_bcc
959
+ SP!; in_reply_to = env_in_reply_to
960
+ SP!; message_id = env_message_id
961
+ rpar
962
+ Envelope.new(date, subject, from, sender, reply_to,
963
+ to, cc, bcc, in_reply_to, message_id)
964
+ ensure
801
965
  @lex_state = EXPR_BEG
802
- return result
803
966
  end
804
967
 
968
+ # env-date = nstring
969
+ # env-subject = nstring
970
+ # env-in-reply-to = nstring
971
+ # env-message-id = nstring
972
+ alias env_date nstring
973
+ alias env_subject nstring
974
+ alias env_in_reply_to nstring
975
+ alias env_message_id nstring
976
+
977
+ # env-from = "(" 1*address ")" / nil
978
+ # env-sender = "(" 1*address ")" / nil
979
+ # env-reply-to = "(" 1*address ")" / nil
980
+ # env-to = "(" 1*address ")" / nil
981
+ # env-cc = "(" 1*address ")" / nil
982
+ # env-bcc = "(" 1*address ")" / nil
983
+ def nlist__address
984
+ return if NIL?
985
+ lpar; list = [address]; list << address until (quirky_SP?; rpar?)
986
+ list
987
+ end
988
+
989
+ alias env_from nlist__address
990
+ alias env_sender nlist__address
991
+ alias env_reply_to nlist__address
992
+ alias env_to nlist__address
993
+ alias env_cc nlist__address
994
+ alias env_bcc nlist__address
995
+
996
+ # Used when servers erroneously send an extra SP.
997
+ #
998
+ # As of 2023-11-28, Outlook.com (still) sends SP
999
+ # between +address+ in <tt>env-*</tt> lists.
1000
+ alias quirky_SP? SP?
1001
+
805
1002
  # date-time = DQUOTE date-day-fixed "-" date-month "-" date-year
806
1003
  # SP time SP zone DQUOTE
807
1004
  alias date_time quoted
@@ -1070,6 +1267,13 @@ module Net
1070
1267
  str << rbra
1071
1268
  end
1072
1269
 
1270
+ # section-binary = "[" [section-part] "]"
1271
+ def section_binary
1272
+ str = +lbra
1273
+ str << section_part unless peek_rbra?
1274
+ str << rbra
1275
+ end
1276
+
1073
1277
  # section-spec = section-msgtext / (section-part ["." section-text])
1074
1278
  # section-msgtext = "HEADER" /
1075
1279
  # "HEADER.FIELDS" [".NOT"] SP header-list /
@@ -1100,6 +1304,11 @@ module Net
1100
1304
  str << rpar
1101
1305
  end
1102
1306
 
1307
+ # section-part = nz-number *("." nz-number)
1308
+ # ; body part reference.
1309
+ # ; Allows for accessing nested body parts.
1310
+ alias section_part atom
1311
+
1103
1312
  # RFC3501 & RFC9051:
1104
1313
  # header-fld-name = astring
1105
1314
  #
@@ -1148,21 +1357,20 @@ module Net
1148
1357
  alias mailbox_data__lsub mailbox_data__list
1149
1358
  alias mailbox_data__xlist mailbox_data__list
1150
1359
 
1360
+ # mailbox-list = "(" [mbx-list-flags] ")" SP
1361
+ # (DQUOTE QUOTED-CHAR DQUOTE / nil) SP mailbox
1362
+ # [SP mbox-list-extended]
1363
+ # ; This is the list information pointed to by the ABNF
1364
+ # ; item "mailbox-data", which is defined above
1151
1365
  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)
1366
+ lpar; attr = peek_rpar? ? [] : mbx_list_flags; rpar
1367
+ SP!; delim = nquoted
1368
+ SP!; name = mailbox
1369
+ # TODO: mbox-list-extended
1370
+ MailboxList.new(attr, delim, name)
1163
1371
  end
1164
1372
 
1165
- def getquota_response
1373
+ def quota_response
1166
1374
  # If quota never established, get back
1167
1375
  # `NO Quota root does not exist'.
1168
1376
  # If quota removed, get `()' after the
@@ -1195,7 +1403,7 @@ module Net
1195
1403
  end
1196
1404
  end
1197
1405
 
1198
- def getquotaroot_response
1406
+ def quotaroot_response
1199
1407
  # Similar to getquota, but only admin can use getquota.
1200
1408
  token = match(T_ATOM)
1201
1409
  name = token.value.upcase
@@ -1254,124 +1462,145 @@ module Net
1254
1462
  # mailbox-data = obsolete-search-response / ...
1255
1463
  # obsolete-search-response = "SEARCH" *(SP nz-number)
1256
1464
  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 = []
1465
+ name = label_in("SEARCH", "SORT")
1466
+ data = []
1467
+ while _ = SP? && nz_number? do data << _ end
1468
+ if lpar?
1469
+ label("MODSEQ"); SP!
1470
+ modseq = mod_sequence_value
1471
+ rpar
1283
1472
  end
1284
- return UntaggedResponse.new(name, data, @str)
1473
+ data = SearchResult.new(data, modseq: modseq)
1474
+ UntaggedResponse.new(name, data, @str)
1285
1475
  end
1286
1476
  alias sort_data mailbox_data__search
1287
1477
 
1478
+ # RFC5256: THREAD
1479
+ # thread-data = "THREAD" [SP 1*thread-list]
1288
1480
  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 = []
1481
+ name = label("THREAD")
1482
+ threads = []
1483
+ if SP?
1484
+ threads << thread_list while lookahead_thread_list?
1310
1485
  end
1311
-
1312
- return UntaggedResponse.new(name, threads, @str)
1486
+ UntaggedResponse.new(name, threads, @str)
1313
1487
  end
1314
1488
 
1315
- def thread_branch(token)
1316
- rootmember = nil
1317
- lastmember = nil
1489
+ alias lookahead_thread_list? lookahead_lpar?
1490
+ alias lookahead_thread_nested? lookahead_thread_list?
1318
1491
 
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
1492
+ # RFC5256: THREAD
1493
+ # thread-list = "(" (thread-members / thread-nested) ")"
1494
+ def thread_list
1495
+ lpar
1496
+ thread = if lookahead_thread_nested?
1497
+ ThreadMember.new(nil, thread_nested)
1498
+ else
1499
+ thread_members
1500
+ end
1501
+ rpar
1502
+ thread
1503
+ end
1340
1504
 
1341
- lastmember.children << thread_branch(token)
1342
- when T_RPAR
1343
- break
1505
+ # RFC5256: THREAD
1506
+ # thread-members = nz-number *(SP nz-number) [SP thread-nested]
1507
+ def thread_members
1508
+ members = []
1509
+ members << nz_number # thread root
1510
+ while SP?
1511
+ case lookahead!(T_NUMBER, T_LPAR).symbol
1512
+ when T_NUMBER then members << nz_number
1513
+ else nested = thread_nested; break
1344
1514
  end
1345
1515
  end
1516
+ members.reverse.inject(nested || []) {|subthreads, number|
1517
+ [ThreadMember.new(number, subthreads)]
1518
+ }.first
1519
+ end
1346
1520
 
1347
- return rootmember
1521
+ # RFC5256: THREAD
1522
+ # thread-nested = 2*thread-list
1523
+ def thread_nested
1524
+ nested = [thread_list, thread_list]
1525
+ while lookahead_thread_list? do nested << thread_list end
1526
+ nested
1348
1527
  end
1349
1528
 
1529
+ # mailbox-data =/ "STATUS" SP mailbox SP "(" [status-att-list] ")"
1350
1530
  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
1531
+ resp_name = label("STATUS"); SP!
1532
+ mbox_name = mailbox; SP!
1533
+ lpar; attr = status_att_list; rpar
1534
+ UntaggedResponse.new(resp_name, StatusData.new(mbox_name, attr), @str)
1535
+ end
1536
+
1537
+ # RFC3501
1538
+ # status-att-list = status-att SP number *(SP status-att SP number)
1539
+ # RFC4466, RFC9051, and RFC3501 Errata
1540
+ # status-att-list = status-att-val *(SP status-att-val)
1541
+ def status_att_list
1542
+ attrs = [status_att_val]
1543
+ while SP? do attrs << status_att_val end
1544
+ attrs.to_h
1545
+ end
1546
+
1547
+ # RFC3501 Errata:
1548
+ # status-att-val = ("MESSAGES" SP number) / ("RECENT" SP number) /
1549
+ # ("UIDNEXT" SP nz-number) / ("UIDVALIDITY" SP nz-number) /
1550
+ # ("UNSEEN" SP number)
1551
+ # RFC4466:
1552
+ # status-att-val = ("MESSAGES" SP number) /
1553
+ # ("RECENT" SP number) /
1554
+ # ("UIDNEXT" SP nz-number) /
1555
+ # ("UIDVALIDITY" SP nz-number) /
1556
+ # ("UNSEEN" SP number)
1557
+ # ;; Extensions to the STATUS responses
1558
+ # ;; should extend this production.
1559
+ # ;; Extensions should use the generic
1560
+ # ;; syntax defined by tagged-ext.
1561
+ # RFC9051:
1562
+ # status-att-val = ("MESSAGES" SP number) /
1563
+ # ("UIDNEXT" SP nz-number) /
1564
+ # ("UIDVALIDITY" SP nz-number) /
1565
+ # ("UNSEEN" SP number) /
1566
+ # ("DELETED" SP number) /
1567
+ # ("SIZE" SP number64)
1568
+ # ; Extensions to the STATUS responses
1569
+ # ; should extend this production.
1570
+ # ; Extensions should use the generic
1571
+ # ; syntax defined by tagged-ext.
1572
+ # RFC7162:
1573
+ # status-att-val =/ "HIGHESTMODSEQ" SP mod-sequence-valzer
1574
+ # ;; Extends non-terminal defined in [RFC4466].
1575
+ # ;; Value 0 denotes that the mailbox doesn't
1576
+ # ;; support persistent mod-sequences
1577
+ # ;; as described in Section 3.1.2.2.
1578
+ # RFC7889:
1579
+ # status-att-val =/ "APPENDLIMIT" SP (number / nil)
1580
+ # ;; status-att-val is defined in RFC 4466
1581
+ # RFC8438:
1582
+ # status-att-val =/ "SIZE" SP number64
1583
+ # RFC8474:
1584
+ # status-att-val =/ "MAILBOXID" SP "(" objectid ")"
1585
+ # ; follows tagged-ext production from [RFC4466]
1586
+ def status_att_val
1587
+ key = tagged_ext_label
1588
+ SP!
1589
+ val =
1590
+ case key
1591
+ when "MESSAGES" then number # RFC3501, RFC9051
1592
+ when "UNSEEN" then number # RFC3501, RFC9051
1593
+ when "DELETED" then number # RFC3501, RFC9051
1594
+ when "UIDNEXT" then nz_number # RFC3501, RFC9051
1595
+ when "UIDVALIDITY" then nz_number # RFC3501, RFC9051
1596
+ when "RECENT" then number # RFC3501 (obsolete)
1597
+ when "SIZE" then number64 # RFC8483, RFC9051
1598
+ when "HIGHESTMODSEQ" then mod_sequence_valzer # RFC7162
1599
+ when "MAILBOXID" then parens__objectid # RFC8474
1600
+ else
1601
+ number? || ExtensionData.new(tagged_ext_val)
1366
1602
  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)
1603
+ [key, val]
1375
1604
  end
1376
1605
 
1377
1606
  # The presence of "IMAP4rev1" or "IMAP4rev2" is unenforced here.
@@ -1573,6 +1802,11 @@ module Net
1573
1802
  # resp-text-code =/ "HIGHESTMODSEQ" SP mod-sequence-value /
1574
1803
  # "NOMODSEQ" /
1575
1804
  # "MODIFIED" SP sequence-set
1805
+ # RFC7162 (QRESYNC):
1806
+ # resp-text-code =/ "CLOSED"
1807
+ #
1808
+ # RFC8474: OBJECTID
1809
+ # resp-text-code =/ "MAILBOXID" SP "(" objectid ")"
1576
1810
  def resp_text_code
1577
1811
  name = resp_text_code__name
1578
1812
  data =
@@ -1591,7 +1825,10 @@ module Net
1591
1825
  "EXPUNGEISSUED", "CORRUPTION", "SERVERBUG", "CLIENTBUG", "CANNOT",
1592
1826
  "LIMIT", "OVERQUOTA", "ALREADYEXISTS", "NONEXISTENT", "CLOSED",
1593
1827
  "NOTSAVED", "UIDNOTSTICKY", "UNKNOWN-CTE", "HASCHILDREN"
1594
- when "NOMODSEQ" # CONDSTORE
1828
+ when "NOMODSEQ" then nil # CONDSTORE
1829
+ when "HIGHESTMODSEQ" then SP!; mod_sequence_value # CONDSTORE
1830
+ when "MODIFIED" then SP!; sequence_set # CONDSTORE
1831
+ when "MAILBOXID" then SP!; parens__objectid # RFC8474: OBJECTID
1595
1832
  else
1596
1833
  SP? and text_chars_except_rbra
1597
1834
  end
@@ -1638,91 +1875,86 @@ module Net
1638
1875
  UIDPlusData.new(validity, src_uids, dst_uids)
1639
1876
  end
1640
1877
 
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
-
1878
+ ADDRESS_REGEXP = /\G
1879
+ \( (?: NIL | #{Patterns::QUOTED_rev2} ) # 1: NAME
1880
+ \s (?: NIL | #{Patterns::QUOTED_rev2} ) # 2: ROUTE
1881
+ \s (?: NIL | #{Patterns::QUOTED_rev2} ) # 3: MAILBOX
1882
+ \s (?: NIL | #{Patterns::QUOTED_rev2} ) # 4: HOST
1883
+ \)
1884
+ /nix
1885
+
1886
+ # address = "(" addr-name SP addr-adl SP addr-mailbox SP
1887
+ # addr-host ")"
1888
+ # addr-adl = nstring
1889
+ # addr-host = nstring
1890
+ # addr-mailbox = nstring
1891
+ # addr-name = nstring
1671
1892
  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)
1893
+ if (match = accept_re(ADDRESS_REGEXP))
1894
+ # note that "NIL" isn't captured by the regexp
1895
+ name, route, mailbox, host = match.captures
1896
+ .map { Patterns.unescape_quoted _1 }
1897
+ else # address may include literals
1898
+ lpar; name = addr_name
1899
+ SP!; route = addr_adl
1900
+ SP!; mailbox = addr_mailbox
1901
+ SP!; host = addr_host
1902
+ rpar
1692
1903
  end
1693
- return Address.new(name, route, mailbox, host)
1904
+ Address.new(name, route, mailbox, host)
1694
1905
  end
1695
1906
 
1907
+ alias addr_adl nstring
1908
+ alias addr_host nstring
1909
+ alias addr_mailbox nstring
1910
+ alias addr_name nstring
1911
+
1696
1912
  # flag-list = "(" [flag *(SP flag)] ")"
1697
1913
  def flag_list
1698
- match_re(Patterns::FLAG_LIST, "flag-list")[1]
1699
- .split(nil)
1700
- .map! { _1.start_with?("\\") ? _1[1..].capitalize.to_sym : _1 }
1914
+ if (match = accept_re(Patterns::FLAG_LIST))
1915
+ match[1].split(nil)
1916
+ .map! { _1.delete_prefix!("\\") ? _1.capitalize.to_sym : _1 }
1917
+ else
1918
+ quirky__flag_list "flags-list"
1919
+ end
1701
1920
  end
1702
1921
 
1703
1922
  # "(" [flag-perm *(SP flag-perm)] ")"
1704
1923
  def flag_perm__list
1705
- match_re(Patterns::FLAG_PERM_LIST, "PERMANENTFLAGS flag-perm list")[1]
1706
- .split(nil)
1707
- .map! { _1.start_with?("\\") ? _1[1..].capitalize.to_sym : _1 }
1708
- end
1709
-
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
1924
+ if (match = accept_re(Patterns::FLAG_PERM_LIST))
1925
+ match[1].split(nil)
1926
+ .map! { _1.delete_prefix!("\\") ? _1.capitalize.to_sym : _1 }
1927
+ else
1928
+ quirky__flag_list "PERMANENTFLAGS flag-perm list"
1929
+ end
1930
+ end
1931
+
1932
+ # This allows illegal "]" in flag names (Gmail),
1933
+ # or "\*" in a FLAGS response (greenmail).
1934
+ def quirky__flag_list(name)
1935
+ match_re(Patterns::QUIRKY_FLAGS_LIST, "quirks mode #{name}")[1]
1936
+ .scan(Patterns::QUIRKY_FLAG)
1937
+ .map! { _1.delete_prefix!("\\") ? _1.capitalize.to_sym : _1 }
1938
+ end
1939
+
1940
+ # See Patterns::MBX_LIST_FLAGS
1941
+ def mbx_list_flags
1724
1942
  match_re(Patterns::MBX_LIST_FLAGS, "mbx-list-flags")[1]
1725
- .split(nil).map! { _1.capitalize.to_sym }
1943
+ .split(nil)
1944
+ .map! { _1.delete_prefix!("\\"); _1.capitalize.to_sym }
1945
+ end
1946
+
1947
+ # See https://developers.google.com/gmail/imap/imap-extensions
1948
+ def x_gm_label; accept(T_BSLASH) ? atom.capitalize.to_sym : astring end
1949
+
1950
+ # See https://developers.google.com/gmail/imap/imap-extensions
1951
+ def x_gm_labels
1952
+ lpar; return [] if rpar?
1953
+ labels = []
1954
+ labels << x_gm_label
1955
+ labels << x_gm_label while SP?
1956
+ rpar
1957
+ labels
1726
1958
  end
1727
1959
 
1728
1960
  # See https://www.rfc-editor.org/errata/rfc3501
@@ -1742,8 +1974,21 @@ module Net
1742
1974
  # ;; Per-message mod-sequence.
1743
1975
  alias permsg_modsequence mod_sequence_value
1744
1976
 
1977
+ # RFC7162:
1978
+ # mod-sequence-valzer = "0" / mod-sequence-value
1979
+ alias mod_sequence_valzer number64
1980
+
1745
1981
  def parens__modseq; lpar; _ = permsg_modsequence; rpar; _ end
1746
1982
 
1983
+ # RFC8474:
1984
+ # objectid = 1*255(ALPHA / DIGIT / "_" / "-")
1985
+ # ; characters in object identifiers are case
1986
+ # ; significant
1987
+ alias objectid atom
1988
+
1989
+ def parens__objectid; lpar; _ = objectid; rpar; _ end
1990
+ def nparens__objectid; NIL? ? nil : parens__objectid end
1991
+
1747
1992
  # RFC-4315 (UIDPLUS) or RFC9051 (IMAP4rev2):
1748
1993
  # uid-set = (uniqueid / uid-range) *("," uid-set)
1749
1994
  # uid-range = (uniqueid ":" uniqueid)
@@ -1789,42 +2034,47 @@ module Net
1789
2034
  @pos = $~.end(0)
1790
2035
  if $1
1791
2036
  return Token.new(T_SPACE, $+)
1792
- elsif $2 && $6
2037
+ elsif $2
2038
+ len = $+.to_i
2039
+ val = @str[@pos, len]
2040
+ @pos += len
2041
+ return Token.new(T_LITERAL8, val)
2042
+ elsif $3 && $7
1793
2043
  # 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, $+)
2044
+ return Token.new(T_ATOM, $3)
1797
2045
  elsif $4
1798
- return Token.new(T_NUMBER, $+)
2046
+ return Token.new(T_NIL, $+)
1799
2047
  elsif $5
2048
+ return Token.new(T_NUMBER, $+)
2049
+ elsif $6
1800
2050
  return Token.new(T_PLUS, $+)
1801
- elsif $7
2051
+ elsif $8
1802
2052
  # match ATOM, without a NUMBER, NIL, or PLUS prefix
1803
2053
  return Token.new(T_ATOM, $+)
1804
- elsif $8
1805
- return Token.new(T_QUOTED, Patterns.unescape_quoted($+))
1806
2054
  elsif $9
1807
- return Token.new(T_LPAR, $+)
2055
+ return Token.new(T_QUOTED, Patterns.unescape_quoted($+))
1808
2056
  elsif $10
1809
- return Token.new(T_RPAR, $+)
2057
+ return Token.new(T_LPAR, $+)
1810
2058
  elsif $11
1811
- return Token.new(T_BSLASH, $+)
2059
+ return Token.new(T_RPAR, $+)
1812
2060
  elsif $12
1813
- return Token.new(T_STAR, $+)
2061
+ return Token.new(T_BSLASH, $+)
1814
2062
  elsif $13
1815
- return Token.new(T_LBRA, $+)
2063
+ return Token.new(T_STAR, $+)
1816
2064
  elsif $14
1817
- return Token.new(T_RBRA, $+)
2065
+ return Token.new(T_LBRA, $+)
1818
2066
  elsif $15
2067
+ return Token.new(T_RBRA, $+)
2068
+ elsif $16
1819
2069
  len = $+.to_i
1820
2070
  val = @str[@pos, len]
1821
2071
  @pos += len
1822
2072
  return Token.new(T_LITERAL, val)
1823
- elsif $16
1824
- return Token.new(T_PERCENT, $+)
1825
2073
  elsif $17
1826
- return Token.new(T_CRLF, $+)
2074
+ return Token.new(T_PERCENT, $+)
1827
2075
  elsif $18
2076
+ return Token.new(T_CRLF, $+)
2077
+ elsif $19
1828
2078
  return Token.new(T_EOF, $+)
1829
2079
  else
1830
2080
  parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid")