net-imap 0.4.4 → 0.4.5

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
@@ -266,6 +267,56 @@ module Net
266
267
  # ; Is a valid RFC 3501 "atom".
267
268
  TAGGED_EXT_LABEL = /#{TAGGED_LABEL_FCHAR}#{TAGGED_LABEL_CHAR}*/n
268
269
 
270
+ # nz-number = digit-nz *DIGIT
271
+ # ; Non-zero unsigned 32-bit integer
272
+ # ; (0 < n < 4,294,967,296)
273
+ NZ_NUMBER = /[1-9]\d*/n
274
+
275
+ # seq-number = nz-number / "*"
276
+ # ; message sequence number (COPY, FETCH, STORE
277
+ # ; commands) or unique identifier (UID COPY,
278
+ # ; UID FETCH, UID STORE commands).
279
+ # ; * represents the largest number in use. In
280
+ # ; the case of message sequence numbers, it is
281
+ # ; the number of messages in a non-empty mailbox.
282
+ # ; In the case of unique identifiers, it is the
283
+ # ; unique identifier of the last message in the
284
+ # ; mailbox or, if the mailbox is empty, the
285
+ # ; mailbox's current UIDNEXT value.
286
+ # ; The server should respond with a tagged BAD
287
+ # ; response to a command that uses a message
288
+ # ; sequence number greater than the number of
289
+ # ; messages in the selected mailbox. This
290
+ # ; includes "*" if the selected mailbox is empty.
291
+ SEQ_NUMBER = /#{NZ_NUMBER}|\*/n
292
+
293
+ # seq-range = seq-number ":" seq-number
294
+ # ; two seq-number values and all values between
295
+ # ; these two regardless of order.
296
+ # ; Example: 2:4 and 4:2 are equivalent and
297
+ # ; indicate values 2, 3, and 4.
298
+ # ; Example: a unique identifier sequence range of
299
+ # ; 3291:* includes the UID of the last message in
300
+ # ; the mailbox, even if that value is less than
301
+ # ; 3291.
302
+ SEQ_RANGE = /#{SEQ_NUMBER}:#{SEQ_NUMBER}/n
303
+
304
+ # sequence-set = (seq-number / seq-range) ["," sequence-set]
305
+ # ; set of seq-number values, regardless of order.
306
+ # ; Servers MAY coalesce overlaps and/or execute
307
+ # ; the sequence in any order.
308
+ # ; Example: a message sequence number set of
309
+ # ; 2,4:7,9,12:* for a mailbox with 15 messages is
310
+ # ; equivalent to 2,4,5,6,7,9,12,13,14,15
311
+ # ; Example: a message sequence number set of
312
+ # ; *:4,5:7 for a mailbox with 10 messages is
313
+ # ; equivalent to 10,9,8,7,6,5,4,5,6,7 and MAY
314
+ # ; be reordered and overlap coalesced to be
315
+ # ; 4,5,6,7,8,9,10.
316
+ SEQUENCE_SET_ITEM = /#{SEQ_NUMBER}|#{SEQ_RANGE}/n
317
+ SEQUENCE_SET = /#{SEQUENCE_SET_ITEM}(?:,#{SEQUENCE_SET_ITEM})*/n
318
+ SEQUENCE_SET_STR = /\A#{SEQUENCE_SET}\z/n
319
+
269
320
  # RFC3501:
270
321
  # literal = "{" number "}" CRLF *CHAR8
271
322
  # ; Number represents the number of CHAR8s
@@ -279,6 +330,16 @@ module Net
279
330
  # ; sent from server to the client.
280
331
  LITERAL = /\{(\d+)\}\r\n/n
281
332
 
333
+ # RFC3516 (BINARY):
334
+ # literal8 = "~{" number "}" CRLF *OCTET
335
+ # ; <number> represents the number of OCTETs
336
+ # ; in the response string.
337
+ # RFC9051:
338
+ # literal8 = "~{" number64 "}" CRLF *OCTET
339
+ # ; <number64> represents the number of OCTETs
340
+ # ; in the response string.
341
+ LITERAL8 = /~\{(\d+)\}\r\n/n
342
+
282
343
  module_function
283
344
 
284
345
  def unescape_quoted!(quoted)
@@ -298,27 +359,28 @@ module Net
298
359
  # the default, used in most places
299
360
  BEG_REGEXP = /\G(?:\
300
361
  (?# 1: SPACE )( )|\
301
- (?# 2: ATOM prefixed with a compatible subtype)\
362
+ (?# 2: LITERAL8)#{Patterns::LITERAL8}|\
363
+ (?# 3: ATOM prefixed with a compatible subtype)\
302
364
  ((?:\
303
- (?# 3: NIL )(NIL)|\
304
- (?# 4: NUMBER )(\d+)|\
305
- (?# 5: PLUS )(\+))\
306
- (?# 6: ATOM remaining after prefix )(#{Patterns::ATOMISH})?\
365
+ (?# 4: NIL )(NIL)|\
366
+ (?# 5: NUMBER )(\d+)|\
367
+ (?# 6: PLUS )(\+))\
368
+ (?# 7: ATOM remaining after prefix )(#{Patterns::ATOMISH})?\
307
369
  (?# This enables greedy alternation without lookahead, in linear time.)\
308
370
  )|\
309
371
  (?# 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
372
+ (?# 8: ATOM )(#{Patterns::ATOMISH})|\
373
+ (?# 9: QUOTED )#{Patterns::QUOTED_rev2}|\
374
+ (?# 10: LPAR )(\()|\
375
+ (?# 11: RPAR )(\))|\
376
+ (?# 12: BSLASH )(\\)|\
377
+ (?# 13: STAR )(\*)|\
378
+ (?# 14: LBRA )(\[)|\
379
+ (?# 15: RBRA )(\])|\
380
+ (?# 16: LITERAL )#{Patterns::LITERAL}|\
381
+ (?# 17: PERCENT )(%)|\
382
+ (?# 18: CRLF )(\r\n)|\
383
+ (?# 19: EOF )(\z))/ni
322
384
 
323
385
  # envelope, body(structure), namespaces
324
386
  DATA_REGEXP = /\G(?:\
@@ -359,6 +421,9 @@ module Net
359
421
  # string = quoted / literal
360
422
  def_token_matchers :string, T_QUOTED, T_LITERAL
361
423
 
424
+ # used by nstring8 = nstring / literal8
425
+ def_token_matchers :string8, T_QUOTED, T_LITERAL, T_LITERAL8
426
+
362
427
  # use where string represents "LABEL" values
363
428
  def_token_matchers :case_insensitive__string,
364
429
  T_QUOTED, T_LITERAL,
@@ -390,6 +455,24 @@ module Net
390
455
  # ATOM-CHAR = <any CHAR except atom-specials>
391
456
  ATOM_TOKENS = [T_ATOM, T_NUMBER, T_NIL, T_LBRA, T_PLUS]
392
457
 
458
+ SEQUENCE_SET_TOKENS = [T_ATOM, T_NUMBER, T_STAR]
459
+
460
+ # sequence-set = (seq-number / seq-range) ["," sequence-set]
461
+ # sequence-set =/ seq-last-command
462
+ # ; Allow for "result of the last command"
463
+ # ; indicator.
464
+ # seq-last-command = "$"
465
+ #
466
+ # *note*: doesn't match seq-last-command
467
+ def sequence_set
468
+ str = combine_adjacent(*SEQUENCE_SET_TOKENS)
469
+ if Patterns::SEQUENCE_SET_STR.match?(str)
470
+ SequenceSet.new(str)
471
+ else
472
+ parse_error("unexpected atom %p, expected sequence-set", str)
473
+ end
474
+ end
475
+
393
476
  # ASTRING-CHAR = ATOM-CHAR / resp-specials
394
477
  # resp-specials = "]"
395
478
  ASTRING_CHARS_TOKENS = [*ATOM_TOKENS, T_RBRA].freeze
@@ -460,6 +543,10 @@ module Net
460
543
  NIL? ? nil : string
461
544
  end
462
545
 
546
+ def nstring8
547
+ NIL? ? nil : string8
548
+ end
549
+
463
550
  def nquoted
464
551
  NIL? ? nil : quoted
465
552
  end
@@ -469,6 +556,60 @@ module Net
469
556
  NIL? ? nil : case_insensitive__string
470
557
  end
471
558
 
559
+ # tagged-ext-comp = astring /
560
+ # tagged-ext-comp *(SP tagged-ext-comp) /
561
+ # "(" tagged-ext-comp ")"
562
+ # ; Extensions that follow this general
563
+ # ; syntax should use nstring instead of
564
+ # ; astring when appropriate in the context
565
+ # ; of the extension.
566
+ # ; Note that a message set or a "number"
567
+ # ; can always be represented as an "atom".
568
+ # ; A URL should be represented as
569
+ # ; a "quoted" string.
570
+ def tagged_ext_comp
571
+ vals = []
572
+ while true
573
+ vals << case lookahead!(*ASTRING_TOKENS, T_LPAR).symbol
574
+ when T_LPAR then lpar; ary = tagged_ext_comp; rpar; ary
575
+ when T_NUMBER then number
576
+ else astring
577
+ end
578
+ SP? or break
579
+ end
580
+ vals
581
+ end
582
+
583
+ # tagged-ext-simple is a subset of atom
584
+ # TODO: recognize sequence-set in the lexer
585
+ #
586
+ # tagged-ext-simple = sequence-set / number / number64
587
+ def tagged_ext_simple
588
+ number? || sequence_set
589
+ end
590
+
591
+ # tagged-ext-val = tagged-ext-simple /
592
+ # "(" [tagged-ext-comp] ")"
593
+ def tagged_ext_val
594
+ if lpar?
595
+ _ = peek_rpar? ? [] : tagged_ext_comp
596
+ rpar
597
+ _
598
+ else
599
+ tagged_ext_simple
600
+ end
601
+ end
602
+
603
+ # mailbox = "INBOX" / astring
604
+ # ; INBOX is case-insensitive. All case variants of
605
+ # ; INBOX (e.g., "iNbOx") MUST be interpreted as INBOX
606
+ # ; not as an astring. An astring which consists of
607
+ # ; the case-insensitive sequence "I" "N" "B" "O" "X"
608
+ # ; is considered to be INBOX and not an astring.
609
+ # ; Refer to section 5.1 for further
610
+ # ; semantic details of mailbox names.
611
+ alias mailbox astring
612
+
472
613
  # valid number ranges are not enforced by parser
473
614
  # number64 = 1*DIGIT
474
615
  # ; Unsigned 63-bit integer
@@ -494,6 +635,12 @@ module Net
494
635
  # ; Strictly ascending
495
636
  alias uniqueid nz_number
496
637
 
638
+ # valid number ranges are not enforced by parser
639
+ #
640
+ # a 64-bit unsigned integer and is the decimal equivalent for the ID hex
641
+ # string used in the web interface and the Gmail API.
642
+ alias x_gm_id number
643
+
497
644
  # [RFC3501 & RFC9051:]
498
645
  # response = *(continue-req / response-data) response-done
499
646
  #
@@ -740,10 +887,17 @@ module Net
740
887
  when "ENVELOPE" then envelope
741
888
  when "INTERNALDATE" then date_time
742
889
  when "RFC822.SIZE" then number64
890
+ when /\ABINARY\[/ni then nstring8 # BINARY, IMAP4rev2
891
+ when /\ABINARY\.SIZE\[/ni then number # BINARY, IMAP4rev2
743
892
  when "RFC822" then nstring # not in rev2
744
893
  when "RFC822.HEADER" then nstring # not in rev2
745
894
  when "RFC822.TEXT" then nstring # not in rev2
746
895
  when "MODSEQ" then parens__modseq # CONDSTORE
896
+ when "EMAILID" then parens__objectid # OBJECTID
897
+ when "THREADID" then nparens__objectid # OBJECTID
898
+ when "X-GM-MSGID" then x_gm_id # GMail
899
+ when "X-GM-THRID" then x_gm_id # GMail
900
+ when "X-GM-LABELS" then x_gm_labels # GMail
747
901
  else parse_error("unknown attribute `%s' for {%d}", name, n)
748
902
  end
749
903
  attr[name] = val
@@ -762,11 +916,18 @@ module Net
762
916
  lbra? and rbra
763
917
  when "BODY"
764
918
  peek_lbra? and name << section and
765
- peek_str?("<") and name << atom # partial
919
+ peek_str?("<") and name << gt__number__lt # partial
920
+ when "BINARY", "BINARY.SIZE"
921
+ name << section_binary
922
+ # see https://www.rfc-editor.org/errata/eid7246 and the note above
923
+ peek_str?("<") and name << gt__number__lt # partial
766
924
  end
767
925
  name
768
926
  end
769
927
 
928
+ # this represents the partial size for BODY or BINARY
929
+ alias gt__number__lt atom
930
+
770
931
  def envelope
771
932
  @lex_state = EXPR_DATA
772
933
  token = lookahead
@@ -1070,6 +1231,13 @@ module Net
1070
1231
  str << rbra
1071
1232
  end
1072
1233
 
1234
+ # section-binary = "[" [section-part] "]"
1235
+ def section_binary
1236
+ str = +lbra
1237
+ str << section_part unless peek_rbra?
1238
+ str << rbra
1239
+ end
1240
+
1073
1241
  # section-spec = section-msgtext / (section-part ["." section-text])
1074
1242
  # section-msgtext = "HEADER" /
1075
1243
  # "HEADER.FIELDS" [".NOT"] SP header-list /
@@ -1100,6 +1268,11 @@ module Net
1100
1268
  str << rpar
1101
1269
  end
1102
1270
 
1271
+ # section-part = nz-number *("." nz-number)
1272
+ # ; body part reference.
1273
+ # ; Allows for accessing nested body parts.
1274
+ alias section_part atom
1275
+
1103
1276
  # RFC3501 & RFC9051:
1104
1277
  # header-fld-name = astring
1105
1278
  #
@@ -1347,31 +1520,80 @@ module Net
1347
1520
  return rootmember
1348
1521
  end
1349
1522
 
1523
+ # mailbox-data =/ "STATUS" SP mailbox SP "(" [status-att-list] ")"
1350
1524
  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
1525
+ resp_name = label("STATUS"); SP!
1526
+ mbox_name = mailbox; SP!
1527
+ lpar; attr = status_att_list; rpar
1528
+ UntaggedResponse.new(resp_name, StatusData.new(mbox_name, attr), @str)
1529
+ end
1530
+
1531
+ # RFC3501
1532
+ # status-att-list = status-att SP number *(SP status-att SP number)
1533
+ # RFC4466, RFC9051, and RFC3501 Errata
1534
+ # status-att-list = status-att-val *(SP status-att-val)
1535
+ def status_att_list
1536
+ attrs = [status_att_val]
1537
+ while SP? do attrs << status_att_val end
1538
+ attrs.to_h
1539
+ end
1540
+
1541
+ # RFC3501 Errata:
1542
+ # status-att-val = ("MESSAGES" SP number) / ("RECENT" SP number) /
1543
+ # ("UIDNEXT" SP nz-number) / ("UIDVALIDITY" SP nz-number) /
1544
+ # ("UNSEEN" SP number)
1545
+ # RFC4466:
1546
+ # status-att-val = ("MESSAGES" SP number) /
1547
+ # ("RECENT" SP number) /
1548
+ # ("UIDNEXT" SP nz-number) /
1549
+ # ("UIDVALIDITY" SP nz-number) /
1550
+ # ("UNSEEN" SP number)
1551
+ # ;; Extensions to the STATUS responses
1552
+ # ;; should extend this production.
1553
+ # ;; Extensions should use the generic
1554
+ # ;; syntax defined by tagged-ext.
1555
+ # RFC9051:
1556
+ # status-att-val = ("MESSAGES" SP number) /
1557
+ # ("UIDNEXT" SP nz-number) /
1558
+ # ("UIDVALIDITY" SP nz-number) /
1559
+ # ("UNSEEN" SP number) /
1560
+ # ("DELETED" SP number) /
1561
+ # ("SIZE" SP number64)
1562
+ # ; Extensions to the STATUS responses
1563
+ # ; should extend this production.
1564
+ # ; Extensions should use the generic
1565
+ # ; syntax defined by tagged-ext.
1566
+ # RFC7162:
1567
+ # status-att-val =/ "HIGHESTMODSEQ" SP mod-sequence-valzer
1568
+ # ;; Extends non-terminal defined in [RFC4466].
1569
+ # ;; Value 0 denotes that the mailbox doesn't
1570
+ # ;; support persistent mod-sequences
1571
+ # ;; as described in Section 3.1.2.2.
1572
+ # RFC7889:
1573
+ # status-att-val =/ "APPENDLIMIT" SP (number / nil)
1574
+ # ;; status-att-val is defined in RFC 4466
1575
+ # RFC8438:
1576
+ # status-att-val =/ "SIZE" SP number64
1577
+ # RFC8474:
1578
+ # status-att-val =/ "MAILBOXID" SP "(" objectid ")"
1579
+ # ; follows tagged-ext production from [RFC4466]
1580
+ def status_att_val
1581
+ key = tagged_ext_label
1582
+ SP!
1583
+ val =
1584
+ case key
1585
+ when "MESSAGES" then number # RFC3501, RFC9051
1586
+ when "UNSEEN" then number # RFC3501, RFC9051
1587
+ when "DELETED" then number # RFC3501, RFC9051
1588
+ when "UIDNEXT" then nz_number # RFC3501, RFC9051
1589
+ when "UIDVALIDITY" then nz_number # RFC3501, RFC9051
1590
+ when "RECENT" then number # RFC3501 (obsolete)
1591
+ when "SIZE" then number64 # RFC8483, RFC9051
1592
+ when "MAILBOXID" then parens__objectid # RFC8474
1593
+ else
1594
+ number? || ExtensionData.new(tagged_ext_val)
1366
1595
  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)
1596
+ [key, val]
1375
1597
  end
1376
1598
 
1377
1599
  # The presence of "IMAP4rev1" or "IMAP4rev2" is unenforced here.
@@ -1573,6 +1795,9 @@ module Net
1573
1795
  # resp-text-code =/ "HIGHESTMODSEQ" SP mod-sequence-value /
1574
1796
  # "NOMODSEQ" /
1575
1797
  # "MODIFIED" SP sequence-set
1798
+ #
1799
+ # RFC8474: OBJECTID
1800
+ # resp-text-code =/ "MAILBOXID" SP "(" objectid ")"
1576
1801
  def resp_text_code
1577
1802
  name = resp_text_code__name
1578
1803
  data =
@@ -1592,6 +1817,7 @@ module Net
1592
1817
  "LIMIT", "OVERQUOTA", "ALREADYEXISTS", "NONEXISTENT", "CLOSED",
1593
1818
  "NOTSAVED", "UIDNOTSTICKY", "UNKNOWN-CTE", "HASCHILDREN"
1594
1819
  when "NOMODSEQ" # CONDSTORE
1820
+ when "MAILBOXID" then SP!; parens__objectid # RFC8474: OBJECTID
1595
1821
  else
1596
1822
  SP? and text_chars_except_rbra
1597
1823
  end
@@ -1725,6 +1951,19 @@ module Net
1725
1951
  .split(nil).map! { _1.capitalize.to_sym }
1726
1952
  end
1727
1953
 
1954
+ # See https://developers.google.com/gmail/imap/imap-extensions
1955
+ def x_gm_label; accept(T_BSLASH) ? atom.capitalize.to_sym : astring end
1956
+
1957
+ # See https://developers.google.com/gmail/imap/imap-extensions
1958
+ def x_gm_labels
1959
+ lpar; return [] if rpar?
1960
+ labels = []
1961
+ labels << x_gm_label
1962
+ labels << x_gm_label while SP?
1963
+ rpar
1964
+ labels
1965
+ end
1966
+
1728
1967
  # See https://www.rfc-editor.org/errata/rfc3501
1729
1968
  #
1730
1969
  # charset = atom / quoted
@@ -1744,6 +1983,15 @@ module Net
1744
1983
 
1745
1984
  def parens__modseq; lpar; _ = permsg_modsequence; rpar; _ end
1746
1985
 
1986
+ # RFC8474:
1987
+ # objectid = 1*255(ALPHA / DIGIT / "_" / "-")
1988
+ # ; characters in object identifiers are case
1989
+ # ; significant
1990
+ alias objectid atom
1991
+
1992
+ def parens__objectid; lpar; _ = objectid; rpar; _ end
1993
+ def nparens__objectid; NIL? ? nil : parens__objectid end
1994
+
1747
1995
  # RFC-4315 (UIDPLUS) or RFC9051 (IMAP4rev2):
1748
1996
  # uid-set = (uniqueid / uid-range) *("," uid-set)
1749
1997
  # uid-range = (uniqueid ":" uniqueid)
@@ -1789,42 +2037,47 @@ module Net
1789
2037
  @pos = $~.end(0)
1790
2038
  if $1
1791
2039
  return Token.new(T_SPACE, $+)
1792
- elsif $2 && $6
2040
+ elsif $2
2041
+ len = $+.to_i
2042
+ val = @str[@pos, len]
2043
+ @pos += len
2044
+ return Token.new(T_LITERAL8, val)
2045
+ elsif $3 && $7
1793
2046
  # 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, $+)
2047
+ return Token.new(T_ATOM, $3)
1797
2048
  elsif $4
1798
- return Token.new(T_NUMBER, $+)
2049
+ return Token.new(T_NIL, $+)
1799
2050
  elsif $5
2051
+ return Token.new(T_NUMBER, $+)
2052
+ elsif $6
1800
2053
  return Token.new(T_PLUS, $+)
1801
- elsif $7
2054
+ elsif $8
1802
2055
  # match ATOM, without a NUMBER, NIL, or PLUS prefix
1803
2056
  return Token.new(T_ATOM, $+)
1804
- elsif $8
1805
- return Token.new(T_QUOTED, Patterns.unescape_quoted($+))
1806
2057
  elsif $9
1807
- return Token.new(T_LPAR, $+)
2058
+ return Token.new(T_QUOTED, Patterns.unescape_quoted($+))
1808
2059
  elsif $10
1809
- return Token.new(T_RPAR, $+)
2060
+ return Token.new(T_LPAR, $+)
1810
2061
  elsif $11
1811
- return Token.new(T_BSLASH, $+)
2062
+ return Token.new(T_RPAR, $+)
1812
2063
  elsif $12
1813
- return Token.new(T_STAR, $+)
2064
+ return Token.new(T_BSLASH, $+)
1814
2065
  elsif $13
1815
- return Token.new(T_LBRA, $+)
2066
+ return Token.new(T_STAR, $+)
1816
2067
  elsif $14
1817
- return Token.new(T_RBRA, $+)
2068
+ return Token.new(T_LBRA, $+)
1818
2069
  elsif $15
2070
+ return Token.new(T_RBRA, $+)
2071
+ elsif $16
1819
2072
  len = $+.to_i
1820
2073
  val = @str[@pos, len]
1821
2074
  @pos += len
1822
2075
  return Token.new(T_LITERAL, val)
1823
- elsif $16
1824
- return Token.new(T_PERCENT, $+)
1825
2076
  elsif $17
1826
- return Token.new(T_CRLF, $+)
2077
+ return Token.new(T_PERCENT, $+)
1827
2078
  elsif $18
2079
+ return Token.new(T_CRLF, $+)
2080
+ elsif $19
1828
2081
  return Token.new(T_EOF, $+)
1829
2082
  else
1830
2083
  parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid")
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP
5
+
6
+ ##
7
+ # An IMAP {sequence
8
+ # set}[https://www.rfc-editor.org/rfc/rfc9051.html#section-4.1.1],
9
+ # is a set of message sequence numbers or unique identifier numbers
10
+ # ("UIDs"). It contains numbers and ranges of numbers. The numbers are all
11
+ # non-zero unsigned 32-bit integers and one special value, <tt>*</tt>, that
12
+ # represents the largest value in the mailbox.
13
+ #
14
+ # *NOTE:* This SequenceSet class is currently a placeholder for unhandled
15
+ # extension data. All it does now is validate. It will be expanded to a
16
+ # full API in a future release.
17
+ class SequenceSet
18
+
19
+ def self.[](str) new(str).freeze end
20
+
21
+ def initialize(input)
22
+ @atom = -String.try_convert(input)
23
+ validate
24
+ end
25
+
26
+ # Returns the IMAP string representation. In the IMAP grammar,
27
+ # +sequence-set+ is a subset of +atom+ which is a subset of +astring+.
28
+ attr_accessor :atom
29
+
30
+ # Returns #atom. In the IMAP grammar, +atom+ is a subset of +astring+.
31
+ alias astring atom
32
+
33
+ # Returns the value of #atom
34
+ alias to_s atom
35
+
36
+ # Hash equality requires the same encoded #atom representation.
37
+ #
38
+ # Net::IMAP::SequenceSet["1:3"] .eql? Net::IMAP::SequenceSet["1:3"] # => true
39
+ # Net::IMAP::SequenceSet["1,2,3"].eql? Net::IMAP::SequenceSet["1:3"] # => false
40
+ # Net::IMAP::SequenceSet["1,3"] .eql? Net::IMAP::SequenceSet["3,1"] # => false
41
+ # Net::IMAP::SequenceSet["9,1:*"].eql? Net::IMAP::SequenceSet["1:*"] # => false
42
+ #
43
+ def eql?(other) self.class == other.class && atom == other.atom end
44
+ alias == eql?
45
+
46
+ # See #eql?
47
+ def hash; [self.class. atom].hash end
48
+
49
+ def inspect
50
+ (frozen? ? "%s[%p]" : "#<%s %p>") % [self.class, to_s]
51
+ end
52
+
53
+ # Unstable API, for internal use only (Net::IMAP#validate_data)
54
+ def validate # :nodoc:
55
+ ResponseParser::Patterns::SEQUENCE_SET_STR.match?(@atom) or
56
+ raise ArgumentError, "invalid sequence-set: %p" % [input]
57
+ true
58
+ end
59
+
60
+ # Unstable API, for internal use only (Net::IMAP#send_data)
61
+ def send_data(imap, tag) # :nodoc:
62
+ imap.__send__(:put_string, atom)
63
+ end
64
+
65
+ end
66
+ end
67
+ end