diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 340339a8..e148b10b 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 # @@ -2439,7 +2439,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. @@ -2456,11 +2456,42 @@ def fetch(...) # # +changedsince+ (optional) behaves the same as with #fetch. # + # +partial+ is an optional range to limit the number of results returned. + # It's useful when +set+ contains an unknown number of messages. + # 1..500 returns the first 500 messages in +set+ (in mailbox + # order), 501..1000 the second 500, and so on. +partial+ may also + # be negative: -500..-1 selects the last 500 messages in +set+. + # 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 + # # Related: #fetch, FetchData # # ==== Capabilities # - # Same as #fetch. + # The server's capabilities must include +PARTIAL+ + # {[RFC9394]}[https://rfc-editor.org/rfc/rfc9394] in order to use the + # +partial+ argument. + # + # Otherwise, the same as #fetch. def uid_fetch(...) fetch_internal("UID FETCH", ...) end @@ -3420,8 +3451,26 @@ def search_internal(cmd, ...) end end - def fetch_internal(cmd, set, attr, mod = nil, changedsince: nil) + def partial_range(range) + case range + in ResponseParser::Patterns::PARTIAL_RANGE + 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 25dfc013..b475deec 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)