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)