net-imap 0.4.4 → 0.4.9

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