From 10e19bec6b5aa6184dc63cb9e46c26022e8b95c9 Mon Sep 17 00:00:00 2001 From: nick evans Date: Fri, 20 Dec 2024 11:31:41 -0500 Subject: [PATCH 1/3] =?UTF-8?q?=E2=99=BB=EF=B8=8F=F0=9F=93=9A=20Minor=20`#?= =?UTF-8?q?fetch`=20refactor=20and=20doc=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/net/imap.rb | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 37fe6c69d..bc99929dd 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -2357,14 +2357,7 @@ def uid_search(...) # Sends a {FETCH command [IMAP4rev1 §6.4.5]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.5] # to retrieve data associated with a message in the mailbox. # - # The +set+ parameter is a number or a range between two numbers, - # or an array of those. The number is a message sequence number, - # where -1 represents a '*' for use in range notation like 100..-1 - # being interpreted as '100:*'. Beware that the +exclude_end?+ - # property of a Range object is ignored, and the contents of a - # range are independent of the order of the range endpoints as per - # the protocol specification, so 1...5, 5..1 and 5...1 are all - # equivalent to 1..5. + # The +set+ parameter must be a valid input to SequenceSet::[]. # # +attr+ is a list of attributes to fetch; see the documentation # for FetchData for a list of valid attributes. @@ -2424,6 +2417,7 @@ def fetch(set, attr, mod = nil, changedsince: nil) # Related: #fetch, FetchData # # ==== Capabilities + # # Same as #fetch. def uid_fetch(set, attr, mod = nil, changedsince: nil) fetch_internal("UID FETCH", set, attr, mod, changedsince: changedsince) @@ -3383,6 +3377,7 @@ def search_internal(cmd, ...) end def fetch_internal(cmd, set, attr, mod = nil, changedsince: nil) + set = SequenceSet[set] if changedsince mod ||= [] mod << "CHANGEDSINCE" << Integer(changedsince) @@ -3399,9 +3394,9 @@ def fetch_internal(cmd, set, attr, mod = nil, changedsince: nil) synchronize do clear_responses("FETCH") if mod - send_command(cmd, SequenceSet.new(set), attr, mod) + send_command(cmd, set, attr, mod) else - send_command(cmd, SequenceSet.new(set), attr) + send_command(cmd, set, attr) end clear_responses("FETCH") end From 48062ce5c0a45f96f9f809c61e2c54bdcba677b4 Mon Sep 17 00:00:00 2001 From: nick evans Date: Tue, 17 Dec 2024 21:17:16 -0500 Subject: [PATCH 2/3] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20`PARTIAL`?= =?UTF-8?q?=20extension=20(RFC9394)=20[=F0=9F=9A=A7TODO:=20test=20#to=5Fa]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For compatibility, `ESearchResult#to_a` returns an array of integers (sequence numbers or UIDs) whenever either `ALL` or `PARTIAL` return data is available. --- lib/net/imap.rb | 74 ++++++++++++++----- lib/net/imap/esearch_result.rb | 48 +++++++++++- lib/net/imap/response_parser.rb | 47 ++++++++++++ rakelib/rfcs.rake | 1 + .../response_parser/rfc9394_partial.yml | 66 +++++++++++++++++ test/net/imap/test_esearch_result.rb | 13 ++++ test/net/imap/test_imap_response_parser.rb | 3 + 7 files changed, 229 insertions(+), 23 deletions(-) create mode 100644 test/net/imap/fixtures/response_parser/rfc9394_partial.yml diff --git a/lib/net/imap.rb b/lib/net/imap.rb index bc99929dd..63e0fc87a 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -534,6 +534,11 @@ module Net # See FetchData#emailid and FetchData#emailid. # - Updates #status with support for the +MAILBOXID+ status attribute. # + # ==== RFC9394: +PARTIAL+ + # - Updates #search, #uid_search with the +PARTIAL+ return option which adds + # ESearchResult#partial return data. + # - TODO: Updates #uid_fetch with the +partial+ modifier. + # # == References # # [{IMAP4rev1}[https://www.rfc-editor.org/rfc/rfc3501.html]]:: @@ -701,6 +706,11 @@ module Net # Gondwana, B., Ed., "IMAP Extension for Object Identifiers", # RFC 8474, DOI 10.17487/RFC8474, September 2018, # . + # [PARTIAL[https://www.rfc-editor.org/info/rfc9394]]: + # Melnikov, A., Achuthan, A., Nagulakonda, V., and L. Alves, + # "IMAP PARTIAL Extension for Paged SEARCH and FETCH", RFC 9394, + # DOI 10.17487/RFC9394, June 2023, + # . # # === IANA registries # * {IMAP Capabilities}[http://www.iana.org/assignments/imap4-capabilities] @@ -1971,8 +1981,9 @@ def uid_expunge(uid_set) # the server to return an ESearchResult instead of a SearchResult, but some # servers disobey this requirement. Requires an extended search # capability, such as +ESEARCH+ or +IMAP4rev2+. - # See {"Argument translation"}[rdoc-ref:#search@Argument+translation] - # and {"Return options"}[rdoc-ref:#search@Return+options], below. + # See {"Argument translation"}[rdoc-ref:#search@Argument+translation] and + # {"Supported return options"}[rdoc-ref:#search@Supported+return+options], + # below. # # +charset+ is the name of the {registered character # set}[https://www.iana.org/assignments/character-sets/character-sets.xhtml] @@ -2082,39 +2093,56 @@ def uid_expunge(uid_set) # *WARNING:* This is vulnerable to injection attacks when external # inputs are used. # - # ==== Return options + # ==== Supported return options # # For full definitions of the standard return options and return data, see # the relevant RFCs. # - # ===== +ESEARCH+ or +IMAP4rev2+ - # - # The following return options require either +ESEARCH+ or +IMAP4rev2+. - # See [{RFC4731 §3.1}[https://rfc-editor.org/rfc/rfc4731#section-3.1]] or - # [{IMAP4rev2 §6.4.4}[https://www.rfc-editor.org/rfc/rfc9051.html#section-6.4.4]]. - # # [+ALL+] # Returns ESearchResult#all with a SequenceSet of all matching sequence # numbers or UIDs. This is the default, when return options are empty. # # For compatibility with SearchResult, ESearchResult#to_a returns an # Array of message sequence numbers or UIDs. + # + # Requires either the +ESEARCH+ or +IMAP4rev2+ capabability. + # {[RFC4731]}[https://rfc-editor.org/rfc/rfc4731] + # {[RFC9051]}[https://rfc-editor.org/rfc/rfc9051] + # # [+COUNT+] # Returns ESearchResult#count with the number of matching messages. + # + # Requires either the +ESEARCH+ or +IMAP4rev2+ capabability. + # {[RFC4731]}[https://rfc-editor.org/rfc/rfc4731] + # {[RFC9051]}[https://rfc-editor.org/rfc/rfc9051] + # # [+MAX+] # Returns ESearchResult#max with the highest matching sequence number or # UID. + # + # Requires either the +ESEARCH+ or +IMAP4rev2+ capabability. + # {[RFC4731]}[https://rfc-editor.org/rfc/rfc4731] + # {[RFC9051]}[https://rfc-editor.org/rfc/rfc9051] + # # [+MIN+] # Returns ESearchResult#min with the lowest matching sequence number or # UID. # - # ===== +CONDSTORE+ + # Requires either the +ESEARCH+ or +IMAP4rev2+ capabability. + # {[RFC4731]}[https://rfc-editor.org/rfc/rfc4731] + # {[RFC9051]}[https://rfc-editor.org/rfc/rfc9051] # - # ESearchResult#modseq return data does not have a corresponding return - # option. Instead, it is returned if the +MODSEQ+ search key is used or - # when the +CONDSTORE+ extension is enabled for the selected mailbox. - # See [{RFC4731 §3.2}[https://www.rfc-editor.org/rfc/rfc4731#section-3.2]] - # or [{RFC7162 §2.1.5}[https://www.rfc-editor.org/rfc/rfc7162#section-3.1.5]]. + # [+PARTIAL+ _range_] + # Returns ESearchResult#partial with a SequenceSet of a subset of + # matching sequence numbers or UIDs, as selected by _range_. As with + # sequence numbers, the first result is +1+: 1..500 selects the + # first 500 search results (in mailbox order), 501..1000 the + # second 500, and so on. _range_ may also be negative: -500..-1 + # selects the last 500 search results. + # + # Requires either the CONTEXT=SEARCH or +PARTIAL+ capabability. + # {[RFC5267]}[https://rfc-editor.org/rfc/rfc5267] + # {[RFC9394]}[https://rfc-editor.org/rfc/rfc9394] # # ===== +RFC4466+ compatible extensions # @@ -2125,6 +2153,14 @@ def uid_expunge(uid_set) # intentionally _unstable_ API. Future releases may return different # (incompatible) objects, without deprecation or warning. # + # ===== +MODSEQ+ return data + # + # ESearchResult#modseq return data does not have a corresponding return + # option. Instead, it is returned if the +MODSEQ+ search key is used or + # when the +CONDSTORE+ extension is enabled for the selected mailbox. + # See [{RFC4731 §3.2}[https://www.rfc-editor.org/rfc/rfc4731#section-3.2]] + # or [{RFC7162 §2.1.5}[https://www.rfc-editor.org/rfc/rfc7162#section-3.1.5]]. + # # ==== Search keys # # For full definitions of the standard search +criteria+, @@ -2396,8 +2432,8 @@ def uid_search(...) # {[RFC7162]}[https://tools.ietf.org/html/rfc7162] in order to use the # +changedsince+ argument. Using +changedsince+ implicitly enables the # +CONDSTORE+ extension. - def fetch(set, attr, mod = nil, changedsince: nil) - fetch_internal("FETCH", set, attr, mod, changedsince: changedsince) + def fetch(...) + fetch_internal("FETCH", ...) end # :call-seq: @@ -2419,8 +2455,8 @@ def fetch(set, attr, mod = nil, changedsince: nil) # ==== Capabilities # # Same as #fetch. - def uid_fetch(set, attr, mod = nil, changedsince: nil) - fetch_internal("UID FETCH", set, attr, mod, changedsince: changedsince) + def uid_fetch(...) + fetch_internal("UID FETCH", ...) end # :call-seq: diff --git a/lib/net/imap/esearch_result.rb b/lib/net/imap/esearch_result.rb index c15c68aec..51c77359d 100644 --- a/lib/net/imap/esearch_result.rb +++ b/lib/net/imap/esearch_result.rb @@ -35,16 +35,16 @@ def initialize(tag: nil, uid: nil, data: nil) # :call-seq: to_a -> Array of integers # - # When #all contains a SequenceSet of message sequence + # When either #all or #partial contains a SequenceSet of message sequence # numbers or UIDs, +to_a+ returns that set as an array of integers. # - # When #all is +nil+, either because the server - # returned no results or because +ALL+ was not included in + # When both #all and #partial are +nil+, either because the server + # returned no results or because +ALL+ and +PARTIAL+ were not included in # the IMAP#search +RETURN+ options, #to_a returns an empty array. # # Note that SearchResult also implements +to_a+, so it can be used without # checking if the server returned +SEARCH+ or +ESEARCH+ data. - def to_a; all&.numbers || [] end + def to_a; all&.numbers || partial&.to_a || [] end ## # attr_reader: tag @@ -135,6 +135,46 @@ def count; data.assoc("COUNT")&.last end # and +ESEARCH+ {[RFC4731]}[https://www.rfc-editor.org/rfc/rfc4731.html#section-3.2]. def modseq; data.assoc("MODSEQ")&.last end + # Returned by ESearchResult#partial. + # + # Requires +PARTIAL+ {[RFC9394]}[https://www.rfc-editor.org/rfc/rfc9394.html] + # or CONTEXT=SEARCH/CONTEXT=SORT + # {[RFC5267]}[https://www.rfc-editor.org/rfc/rfc5267.html] + # + # See also: #to_a + class PartialResult < Data.define(:range, :results) + def initialize(range:, results:) + range => Range + results => SequenceSet | nil + super + end + + ## + # method: range + # :call-seq: range -> range + + ## + # method: results + # :call-seq: results -> sequence set or nil + + # Converts #results to an array of integers. + # + # See also: ESearchResult#to_a. + def to_a; results&.numbers || [] end + end + + # :call-seq: partial -> PartialResult or nil + # + # A PartialResult containing a subset of the message sequence numbers or + # UIDs that satisfy the SEARCH criteria. + # + # Requires +PARTIAL+ {[RFC9394]}[https://www.rfc-editor.org/rfc/rfc9394.html] + # or CONTEXT=SEARCH/CONTEXT=SORT + # {[RFC5267]}[https://www.rfc-editor.org/rfc/rfc5267.html] + # + # See also: #to_a + def partial; data.assoc("PARTIAL")&.last end + end end end diff --git a/lib/net/imap/response_parser.rb b/lib/net/imap/response_parser.rb index 04c084a5d..90d3c5fc1 100644 --- a/lib/net/imap/response_parser.rb +++ b/lib/net/imap/response_parser.rb @@ -321,6 +321,20 @@ 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 + # RFC3501: # literal = "{" number "}" CRLF *CHAR8 # ; Number represents the number of CHAR8s @@ -1517,6 +1531,9 @@ def esearch_response # From RFC4731 (ESEARCH): # search-return-data =/ "MODSEQ" SP mod-sequence-value # + # From RFC9394 (PARTIAL): + # search-return-data =/ ret-data-partial + # def search_return_data label = search_modifier_name; SP! value = @@ -1526,11 +1543,41 @@ def search_return_data when "ALL" then sequence_set when "COUNT" then number when "MODSEQ" then mod_sequence_value # RFC7162: CONDSTORE + when "PARTIAL" then ret_data_partial__value # RFC9394: PARTIAL else search_return_value end [label, value] end + # From RFC5267 (CONTEXT=SEARCH, CONTEXT=SORT) and RFC9394 (PARTIAL): + # ret-data-partial = "PARTIAL" + # SP "(" partial-range SP partial-results ")" + def ret_data_partial__value + lpar + range = partial_range; SP! + results = partial_results + rpar + ESearchResult::PartialResult.new(range, results) + end + + # partial-range = partial-range-first / partial-range-last + # tagged-ext-simple =/ partial-range-last + def partial_range + case (str = atom) + when Patterns::PARTIAL_RANGE_FIRST, Patterns::PARTIAL_RANGE_LAST + min, max = [Integer($1), Integer($2)].minmax + min..max + else + parse_error("unexpected atom %p, expected partial-range", str) + end + end + + # partial-results = sequence-set / "NIL" + # ;; from [RFC3501]. + # ;; NIL indicates that no results correspond to + # ;; the requested range. + def partial_results; NIL? ? nil : sequence_set end + # search-modifier-name = tagged-ext-label alias search_modifier_name tagged_ext_label diff --git a/rakelib/rfcs.rake b/rakelib/rfcs.rake index d1b9bb7c1..950c49ccd 100644 --- a/rakelib/rfcs.rake +++ b/rakelib/rfcs.rake @@ -145,6 +145,7 @@ RFCS = { 8514 => "IMAP SAVEDATE", 8970 => "IMAP PREVIEW", 9208 => "IMAP QUOTA, QUOTA=, QUOTASET", + 9394 => "IMAP PARTIAL", # etc... 3629 => "UTF8", diff --git a/test/net/imap/fixtures/response_parser/rfc9394_partial.yml b/test/net/imap/fixtures/response_parser/rfc9394_partial.yml new file mode 100644 index 000000000..233a9096e --- /dev/null +++ b/test/net/imap/fixtures/response_parser/rfc9394_partial.yml @@ -0,0 +1,66 @@ +--- +:tests: + + "RFC9394 PARTIAL 3.1. example 1": + comment: | + Neither RFC9394 nor RFC5267 contain any examples of a normal unelided + sequence-set result. I've edited it to include a sequence-set here. + :response: "* ESEARCH (TAG \"A01\") UID PARTIAL (-1:-100 200:250,252:300)\r\n" + :expected: !ruby/struct:Net::IMAP::UntaggedResponse + name: ESEARCH + data: !ruby/object:Net::IMAP::ESearchResult + tag: A01 + uid: true + data: + - - PARTIAL + - !ruby/object:Net::IMAP::ESearchResult::PartialResult + range: !ruby/range + begin: -100 + end: -1 + excl: false + results: !ruby/object:Net::IMAP::SequenceSet + string: 200:250,252:300 + tuples: + - - 200 + - 250 + - - 252 + - 300 + raw_data: "* ESEARCH (TAG \"A01\") UID PARTIAL (-1:-100 200:250,252:300)\r\n" + + "RFC9394 PARTIAL 3.1. example 2": + :response: "* ESEARCH (TAG \"A02\") UID PARTIAL (23500:24000 55500:56000)\r\n" + :expected: !ruby/struct:Net::IMAP::UntaggedResponse + name: ESEARCH + data: !ruby/object:Net::IMAP::ESearchResult + tag: A02 + uid: true + data: + - - PARTIAL + - !ruby/object:Net::IMAP::ESearchResult::PartialResult + range: !ruby/range + begin: 23500 + end: 24000 + excl: false + results: !ruby/object:Net::IMAP::SequenceSet + string: 55500:56000 + tuples: + - - 55500 + - 56000 + raw_data: "* ESEARCH (TAG \"A02\") UID PARTIAL (23500:24000 55500:56000)\r\n" + + "RFC9394 PARTIAL 3.1. example 3": + :response: "* ESEARCH (TAG \"A04\") UID PARTIAL (24000:24500 NIL)\r\n" + :expected: !ruby/struct:Net::IMAP::UntaggedResponse + name: ESEARCH + data: !ruby/object:Net::IMAP::ESearchResult + tag: A04 + uid: true + data: + - - PARTIAL + - !ruby/object:Net::IMAP::ESearchResult::PartialResult + range: !ruby/range + begin: 24000 + end: 24500 + excl: false + results: + raw_data: "* ESEARCH (TAG \"A04\") UID PARTIAL (24000:24500 NIL)\r\n" diff --git a/test/net/imap/test_esearch_result.rb b/test/net/imap/test_esearch_result.rb index ca214e515..b48e75867 100644 --- a/test/net/imap/test_esearch_result.rb +++ b/test/net/imap/test_esearch_result.rb @@ -80,4 +80,17 @@ class ESearchResultTest < Test::Unit::TestCase assert_equal 12345, esearch.modseq end + test "#partial returns PARTIAL value (RFC9394: PARTIAL)" do + result = Net::IMAP::ResponseParser.new.parse( + "* ESEARCH (TAG \"A0006\") UID PARTIAL (-1:-100 200:250,252:300)\r\n" + ).data + assert_equal(ESearchResult, result.class) + assert_equal( + ESearchResult::PartialResult.new( + -100..-1, SequenceSet[200..250, 252..300] + ), + result.partial + ) + end + end diff --git a/test/net/imap/test_imap_response_parser.rb b/test/net/imap/test_imap_response_parser.rb index 49bc14b6b..ac8f2f04a 100644 --- a/test/net/imap/test_imap_response_parser.rb +++ b/test/net/imap/test_imap_response_parser.rb @@ -103,6 +103,9 @@ def teardown # RFC 9208: QUOTA extension generate_tests_from fixture_file: "rfc9208_quota_responses.yml" + # RFC 9394: PARTIAL extension + generate_tests_from fixture_file: "rfc9394_partial.yml" + ############################################################################ # Workarounds or unspecified extensions: generate_tests_from fixture_file: "quirky_behaviors.yml" From 4de8e6bdfa637c56150effdd44e87e294fcf21fe Mon Sep 17 00:00:00 2001 From: nick evans Date: Fri, 20 Dec 2024 12:01:36 -0500 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9C=A8=20Add=20`partial`=20fetch=20modif?= =?UTF-8?q?ier=20to=20uid=5Ffetch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/net/imap.rb | 47 +++++++++++++++++++++++++++++++++++--- test/net/imap/test_imap.rb | 21 +++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 63e0fc87a..e42445ece 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -537,7 +537,7 @@ module Net # ==== RFC9394: +PARTIAL+ # - Updates #search, #uid_search with the +PARTIAL+ return option which adds # ESearchResult#partial return data. - # - TODO: Updates #uid_fetch with the +partial+ modifier. + # - Updates #uid_fetch with the +partial+ modifier. # # == References # @@ -2437,7 +2437,7 @@ def fetch(...) end # :call-seq: - # uid_fetch(set, attr, changedsince: nil) -> array of FetchData + # uid_fetch(set, attr, changedsince: nil, partial: nil) -> array of FetchData # # Sends a {UID FETCH command [IMAP4rev1 §6.4.8]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.8] # to retrieve data associated with a message in the mailbox. @@ -2445,6 +2445,29 @@ def fetch(...) # Similar to #fetch, but the +set+ parameter contains unique identifiers # instead of message sequence numbers. # + # When #uid_fetch may also be given a +partial+ range, which can be used to + # limit the number of results. Requires the +PARTIAL+ + # capabability. {[RFC9394]}[https://rfc-editor.org/rfc/rfc9394] + # + # For example: + # + # # Without partial, the size of the results may be unknown beforehand: + # results = imap.uid_fetch(next_uid_to_fetch.., %w(UID FLAGS)) + # # ... maybe wait for a long time ... and allocate a lot of memory ... + # results.size # => 0..2**32-1 + # process results # may also take a long time and use a lot of memory... + # + # # Using partial, the results may be paginated: + # loop do + # results = imap.uid_fetch(next_uid_to_fetch.., %w(UID FLAGS), + # partial: 1..500) + # # fetch should return quickly and allocate little memory + # results.size # => 0..500 + # break if results.empty? + # next_uid_to_fetch = results.last.uid + 1 + # process results + # end + # # >>> # *Note:* Servers _MUST_ implicitly include the +UID+ message data item as # part of any +FETCH+ response caused by a +UID+ command, regardless of @@ -3412,8 +3435,26 @@ def search_internal(cmd, ...) end end - def fetch_internal(cmd, set, attr, mod = nil, changedsince: nil) + def partial_range(range) + case range + in /\a(?:\d+:\d+|-\d+:-\d+)\z/ + range + in Range + minmax = range.minmax.map { Integer _1 } + if minmax.all?(1..2**32-1) || minmax.all?(-2**32..-1) + minmax.join(":") + else + raise ArgumentError, "invalid partial-range" + end + end + end + + def fetch_internal(cmd, set, attr, mod = nil, partial: nil, changedsince: nil) set = SequenceSet[set] + if partial + mod ||= [] + mod << "PARTIAL" << partial_range(partial) + end if changedsince mod ||= [] mod << "CHANGEDSINCE" << Integer(changedsince) diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb index 25dfc0139..b475deecf 100644 --- a/test/net/imap/test_imap.rb +++ b/test/net/imap/test_imap.rb @@ -1164,6 +1164,27 @@ def test_enable end end + test "#uid_fetch with partial" do + with_fake_server select: "inbox" do |server, imap| + server.on("UID FETCH", &:done_ok) + imap.uid_fetch 1.., "FAST", partial: 1..500 + assert_equal("RUBY0002 UID FETCH 1:* FAST (PARTIAL 1:500)", + server.commands.pop.raw.strip) + imap.uid_fetch 1.., "FAST", partial: 1...501 + assert_equal("RUBY0003 UID FETCH 1:* FAST (PARTIAL 1:500)", + server.commands.pop.raw.strip) + imap.uid_fetch 1.., "FAST", partial: -500..-1 + assert_equal("RUBY0004 UID FETCH 1:* FAST (PARTIAL -500:-1)", + server.commands.pop.raw.strip) + imap.uid_fetch 1.., "FAST", partial: -500...-1 + assert_equal("RUBY0005 UID FETCH 1:* FAST (PARTIAL -500:-2)", + server.commands.pop.raw.strip) + imap.uid_fetch 1.., "FAST", partial: 1..20, changedsince: 1234 + assert_equal("RUBY0006 UID FETCH 1:* FAST (PARTIAL 1:20 CHANGEDSINCE 1234)", + server.commands.pop.raw.strip) + end + end + test "#store with unchangedsince" do with_fake_server select: "inbox" do |server, imap| server.on("STORE", &:done_ok)