diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 4ac9d7a8..874b3a04 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -2632,6 +2632,7 @@ def fetch(...) # :call-seq: # uid_fetch(set, attr, changedsince: nil, partial: nil) -> array of FetchData (or UIDFetchData) + # uid_fetch(set, attr, changedsince:, vanished: true, partial: nil) -> array of VanishedData and FetchData (or UIDFetchData) # # 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. @@ -2648,6 +2649,22 @@ def fetch(...) # # +changedsince+ (optional) behaves the same as with #fetch. # + # +vanished+ can be used to request a list all of the message UIDs in +set+ + # that have been expunged since +changedsince+. Setting +vanished+ to true + # prepends a VanishedData object to the returned array. If the server does + # not return a +VANISHED+ response, an empty VanishedData object will still + # be added. + # The +QRESYNC+ capabability must be enabled. + # {[RFC7162]}[https://rfc-editor.org/rfc/rfc7162] + # + # For example: + # + # imap.enable("QRESYNC") # must enable before selecting the mailbox + # imap.select("INBOX") + # # first value in the array is VanishedData + # vanished, *fetched = imap.uid_fetch(301..500, %w[flags], + # changedsince: 12345, vanished: true) + # # +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 @@ -2680,6 +2697,9 @@ def fetch(...) # # ==== Capabilities # + # QRESYNC[https://www.rfc-editor.org/rfc/rfc7162] must be enabled in order + # to use the +vanished+ fetch modifier. + # # The server's capabilities must include +PARTIAL+ # {[RFC9394]}[https://rfc-editor.org/rfc/rfc9394] in order to use the # +partial+ argument. @@ -2959,9 +2979,8 @@ def uid_thread(algorithm, search_keys, charset) # See {[RFC7162 §3.1]}[https://www.rfc-editor.org/rfc/rfc7162.html#section-3.1]. # # [+QRESYNC+ {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html]] - # *NOTE:* Enabling QRESYNC will replace +EXPUNGE+ with +VANISHED+, but - # the extension arguments to #select, #examine, and #uid_fetch are not - # supported yet. + # *NOTE:* The +QRESYNC+ argument to #select and #examine is not supported + # yet. # # Adds quick resynchronization options to #select, #examine, and # #uid_fetch. +QRESYNC+ _must_ be explicitly enabled before using any of @@ -3680,19 +3699,23 @@ def search_internal(cmd, ...) end end - def fetch_internal(cmd, set, attr, mod = nil, partial: nil, changedsince: nil) - if partial && !cmd.start_with?("UID ") + def fetch_internal(cmd, set, attr, mod = nil, + partial: nil, + changedsince: nil, + vanished: false) + if cmd.start_with?("UID ") + if vanished && !changedsince + raise ArgumentError, "vanished must be used with changedsince" + end + elsif vanished + raise ArgumentError, "vanished can only be used with uid_fetch" + elsif partial raise ArgumentError, "partial can only be used with uid_fetch" end set = SequenceSet[set] - if partial - mod ||= [] - mod << "PARTIAL" << PartialRange[partial] - end - if changedsince - mod ||= [] - mod << "CHANGEDSINCE" << Integer(changedsince) - end + (mod ||= []) << "PARTIAL" << PartialRange[partial] if partial + (mod ||= []) << "CHANGEDSINCE" << Integer(changedsince) if changedsince + (mod ||= []) << "VANISHED" if vanished case attr when String then attr = RawData.new(attr) @@ -3704,7 +3727,7 @@ def fetch_internal(cmd, set, attr, mod = nil, partial: nil, changedsince: nil) args = [cmd, set, attr] args << mod if mod - send_command_returning_fetch_results(*args) + send_command_returning_fetch_results(*args, vanished:) end def store_internal(cmd, set, attr, flags, unchangedsince: nil) @@ -3715,14 +3738,20 @@ def store_internal(cmd, set, attr, flags, unchangedsince: nil) send_command_returning_fetch_results(cmd, *args) end - def send_command_returning_fetch_results(...) + def send_command_returning_fetch_results(*args, vanished: false) synchronize do clear_responses("FETCH") clear_responses("UIDFETCH") - send_command(...) + send_command(*args) fetches = clear_responses("FETCH") uidfetches = clear_responses("UIDFETCH") - uidfetches.any? ? uidfetches : fetches + fetches = uidfetches if uidfetches.any? + if vanished + vanished = extract_responses("VANISHED", &:earlier?).last || + VanishedData[uids: SequenceSet.empty, earlier: true] + fetches = [vanished, *fetches].freeze + end + fetches end end diff --git a/test/net/imap/test_imap_fetch.rb b/test/net/imap/test_imap_fetch.rb index 32e2a639..79a397c1 100644 --- a/test/net/imap/test_imap_fetch.rb +++ b/test/net/imap/test_imap_fetch.rb @@ -12,6 +12,12 @@ class IMAPFetchTest < Net::IMAP::TestCase assert_raise_with_message(ArgumentError, /\Apartial.*uid_fetch/) do imap.fetch(1, "FAST", partial: 1..10) end + assert_raise_with_message(ArgumentError, /\Avanished.*uid_fetch/) do + imap.fetch(1, "FAST", changedsince: 1234, vanished: true) + end + assert_raise_with_message(ArgumentError, /\Avanished.*changedsince/) do + imap.uid_fetch(1, "FAST", vanished: true) + end end end @@ -107,4 +113,59 @@ class IMAPFetchTest < Net::IMAP::TestCase end end + test "#uid_fetch with changedsince and vanished" do + with_fake_server select: "inbox" do |server, imap| + server.on("UID FETCH") do |resp| + resp.untagged "VANISHED (EARLIER) 300:310,405,411" + resp.untagged "1 FETCH (UID 404 MODSEQ (65402) FLAGS (\\Seen))" + resp.untagged "2 FETCH (UID 406 MODSEQ (75403) FLAGS (\\Deleted))" + resp.untagged "4 FETCH (UID 408 MODSEQ (29738) " \ + "FLAGS ($NoJunk $AutoJunk $MDNSent))" + resp.done_ok + end + # vanished: true changes the output to begin with VanishedData + vanished, *fetched = imap.uid_fetch(300..500, %w[FLAGS], + changedsince: 12345, vanished: true) + assert_equal( + "RUBY0002 UID FETCH 300:500 (FLAGS) (CHANGEDSINCE 12345 VANISHED)", + server.commands.pop.raw.strip + ) + assert_equal Net::IMAP::VanishedData["300:310,405,411", true], vanished + expected = [ + [1, 404, 65402, %i[Seen]], + [2, 406, 75403, %i[Deleted]], + [4, 408, 29738, %w[$NoJunk $AutoJunk $MDNSent]], + ] + assert_equal expected.size, fetched.size + fetched.zip(expected).each do |fetch, (seqno, uid, modseq, flags)| + assert_instance_of Net::IMAP::FetchData, fetch + assert_equal seqno, fetch.seqno + assert_equal uid, fetch.uid + assert_equal modseq, fetch.modseq + assert_equal flags, fetch.flags + end + + # without VANISHED + server.on("UID FETCH") do |resp| + resp.untagged "1 FETCH (UID 404 MODSEQ (65402) FLAGS (\\Seen))" + resp.untagged "2 FETCH (UID 406 MODSEQ (75403) FLAGS (\\Deleted))" + resp.untagged "4 FETCH (UID 408 MODSEQ (29738) " \ + "FLAGS ($NoJunk $AutoJunk $MDNSent))" + resp.done_ok + end + vanished, *fetched = imap.uid_fetch(300..500, %w[FLAGS], + changedsince: 12345, vanished: true) + assert_equal(Net::IMAP::VanishedData[Net::IMAP::SequenceSet.empty, true], + vanished) + assert_equal expected.size, fetched.size + fetched.zip(expected).each do |fetch, (seqno, uid, modseq, flags)| + assert_instance_of Net::IMAP::FetchData, fetch + assert_equal seqno, fetch.seqno + assert_equal uid, fetch.uid + assert_equal modseq, fetch.modseq + assert_equal flags, fetch.flags + end + end + end + end