diff --git a/lib/net/imap.rb b/lib/net/imap.rb index d076fc96..bacd46f6 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -3338,24 +3338,14 @@ def convert_return_opts(unconverted) ] return_opts.map {|opt| case opt - when Symbol then opt.to_s - when Range then partial_range_last_or_seqset(opt) - else opt + when Symbol then opt.to_s + when PartialRange::Negative then PartialRange[opt] + when Range then SequenceSet[opt] + else opt end } end - def partial_range_last_or_seqset(range) - case [range.begin, range.end] - in [Integer => first, Integer => last] if first.negative? && last.negative? - # partial-range-last [RFC9394] - first <= last or raise DataFormatError, "empty range: %p" % [range] - "#{first}:#{last}" - else - SequenceSet[range] - end - end - def search_internal(cmd, ...) args, esearch = search_args(...) synchronize do diff --git a/lib/net/imap/command_data.rb b/lib/net/imap/command_data.rb index 2b3a05b4..3516b444 100644 --- a/lib/net/imap/command_data.rb +++ b/lib/net/imap/command_data.rb @@ -153,6 +153,38 @@ def send_data(imap, tag) end end + class PartialRange < CommandData # :nodoc: + uint32_max = 2**32 - 1 + POS_RANGE = 1..uint32_max + NEG_RANGE = -uint32_max..-1 + Positive = ->{ (_1 in Range) and POS_RANGE.cover?(_1) } + Negative = ->{ (_1 in Range) and NEG_RANGE.cover?(_1) } + + def initialize(data:) + min, max = case data + in Range + data.minmax.map { Integer _1 } + in ResponseParser::Patterns::PARTIAL_RANGE + data.split(":").map { Integer _1 }.minmax + else + raise ArgumentError, "invalid partial range input: %p" % [data] + end + data = min..max + unless data in Positive | Negative + raise ArgumentError, "invalid partial-range: %p" % [data] + end + super + rescue TypeError, RangeError + raise ArgumentError, "expected range min/max to be Integers" + end + + def formatted = "%d:%d" % data.minmax + + def send_data(imap, tag) + imap.__send__(:put_string, formatted) + end + end + # *DEPRECATED*. Replaced by SequenceSet. class MessageSet < CommandData # :nodoc: def send_data(imap, tag) diff --git a/lib/net/imap/response_parser.rb b/lib/net/imap/response_parser.rb index 04c084a5..8d87ec1b 100644 --- a/lib/net/imap/response_parser.rb +++ b/lib/net/imap/response_parser.rb @@ -321,6 +321,24 @@ module RFC3629 SEQUENCE_SET = /#{SEQUENCE_SET_ITEM}(?:,#{SEQUENCE_SET_ITEM})*/n SEQUENCE_SET_STR = /\A#{SEQUENCE_SET}\z/n + # partial-range-first = nz-number ":" nz-number + # ;; Request to search from oldest (lowest UIDs) to + # ;; more recent messages. + # ;; A range 500:400 is the same as 400:500. + # ;; This is similar to from [RFC3501] + # ;; but cannot contain "*". + PARTIAL_RANGE_FIRST = /\A(#{NZ_NUMBER}):(#{NZ_NUMBER})\z/n + + # partial-range-last = MINUS nz-number ":" MINUS nz-number + # ;; Request to search from newest (highest UIDs) to + # ;; oldest messages. + # ;; A range -500:-400 is the same as -400:-500. + PARTIAL_RANGE_LAST = /\A(-#{NZ_NUMBER}):(-#{NZ_NUMBER})\z/n + + # partial-range = partial-range-first / partial-range-last + PARTIAL_RANGE = Regexp.union(PARTIAL_RANGE_FIRST, + PARTIAL_RANGE_LAST) + # RFC3501: # literal = "{" number "}" CRLF *CHAR8 # ; Number represents the number of CHAR8s diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb index 25dfc013..1f20c773 100644 --- a/test/net/imap/test_imap.rb +++ b/test/net/imap/test_imap.rb @@ -658,6 +658,56 @@ def test_send_invalid_number end end + test("send PartialRange args") do + with_fake_server do |server, imap| + server.on "TEST", &:done_ok + send_partial_ranges = ->(*args) do + args.map! { Net::IMAP::PartialRange[_1] } + imap.__send__(:send_command, "TEST", *args) + end + # simple strings + send_partial_ranges.call "1:5", "-5:-1" + assert_equal "1:5 -5:-1", server.commands.pop.args + # backwards strings are reversed + send_partial_ranges.call "5:1", "-1:-5" + assert_equal "1:5 -5:-1", server.commands.pop.args + # simple ranges + send_partial_ranges.call 1..5, -5..-1 + assert_equal "1:5 -5:-1", server.commands.pop.args + # exclusive ranges drop end + send_partial_ranges.call 1...5, -5...-1 + assert_equal "1:4 -5:-2", server.commands.pop.args + + # backwards ranges are invalid + assert_raise(ArgumentError) do send_partial_ranges.call( 5.. 1) end + assert_raise(ArgumentError) do send_partial_ranges.call(-1..-5) end + + # bounds checks + uint32_max = 2**32 - 1 + not_uint32 = 2**32 + send_partial_ranges.call 500..uint32_max + assert_equal "500:#{uint32_max}", server.commands.pop.args + send_partial_ranges.call 500...not_uint32 + assert_equal "500:#{uint32_max}", server.commands.pop.args + send_partial_ranges.call "#{uint32_max}:500" + assert_equal "500:#{uint32_max}", server.commands.pop.args + + send_partial_ranges.call(-uint32_max..-500) + assert_equal "-#{uint32_max}:-500", server.commands.pop.args + send_partial_ranges.call "-500:-#{uint32_max}" + assert_equal "-#{uint32_max}:-500", server.commands.pop.args + + assert_raise(ArgumentError) do send_partial_ranges.call("foo") end + assert_raise(ArgumentError) do send_partial_ranges.call("foo:bar") end + assert_raise(ArgumentError) do send_partial_ranges.call("1.2:3.5") end + assert_raise(ArgumentError) do send_partial_ranges.call("1:*") end + assert_raise(ArgumentError) do send_partial_ranges.call("1:#{not_uint32}") end + assert_raise(ArgumentError) do send_partial_ranges.call(1..) end + assert_raise(ArgumentError) do send_partial_ranges.call(1..not_uint32) end + assert_raise(ArgumentError) do send_partial_ranges.call(..1) end + end + end + def test_send_literal server = create_tcp_server port = server.addr[1]