Skip to content

Commit 5c8c0e9

Browse files
committed
✨ QRESYNC: Add vanished kwarg to #uid_fetch
The `vanished` kwarg to `#uid_fetch` 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: ```ruby imap.enable("QRESYNC") # must enable before selecting the mailbox imap.select("INBOX") # the first value in the returned array is a VanishedData object vanished, *fetched = imap.uid_fetch(301..500, %w[FLAGS], changedsince: 12345, vanished: true) ```
1 parent e230fe1 commit 5c8c0e9

File tree

2 files changed

+108
-17
lines changed

2 files changed

+108
-17
lines changed

lib/net/imap.rb

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2636,6 +2636,7 @@ def fetch(...)
26362636

26372637
# :call-seq:
26382638
# uid_fetch(set, attr, changedsince: nil, partial: nil) -> array of FetchData (or UIDFetchData)
2639+
# uid_fetch(set, attr, changedsince:, vanished: true, partial: nil) -> array of VanishedData and FetchData (or UIDFetchData)
26392640
#
26402641
# Sends a {UID FETCH command [IMAP4rev1 §6.4.8]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.8]
26412642
# to retrieve data associated with a message in the mailbox.
@@ -2652,6 +2653,23 @@ def fetch(...)
26522653
#
26532654
# +changedsince+ (optional) behaves the same as with #fetch.
26542655
#
2656+
# +vanished+ can be used to request a list all of the message UIDs in +set+
2657+
# that have been expunged since +changedsince+. Setting +vanished+ to true
2658+
# prepends a VanishedData object to the returned array. If the server does
2659+
# not return a +VANISHED+ response, an empty VanishedData object will still
2660+
# be added.
2661+
# <em>The +QRESYNC+ capabability must be enabled.</em>
2662+
# {[RFC7162]}[https://rfc-editor.org/rfc/rfc7162]
2663+
#
2664+
# For example:
2665+
#
2666+
# # must enable "QRESYNC" before selecting the mailbox
2667+
# imap.enable("QRESYNC")
2668+
# imap.select("INBOX")
2669+
# # first value in the array is VanishedData
2670+
# vanished, *fetched = imap.uid_fetch(301..500, %w[flags],
2671+
# changedsince: 12345, vanished: true)
2672+
#
26552673
# +partial+ is an optional range to limit the number of results returned.
26562674
# It's useful when +set+ contains an unknown number of messages.
26572675
# <tt>1..500</tt> returns the first 500 messages in +set+ (in mailbox
@@ -2683,6 +2701,9 @@ def fetch(...)
26832701
#
26842702
# ==== Capabilities
26852703
#
2704+
# QRESYNC[https://www.rfc-editor.org/rfc/rfc7162] must be enabled in order
2705+
# to use the +vanished+ fetch modifier.
2706+
#
26862707
# The server's capabilities must include +PARTIAL+
26872708
# {[RFC9394]}[https://rfc-editor.org/rfc/rfc9394] in order to use the
26882709
# +partial+ argument.
@@ -2962,9 +2983,8 @@ def uid_thread(algorithm, search_keys, charset)
29622983
# See {[RFC7162 §3.1]}[https://www.rfc-editor.org/rfc/rfc7162.html#section-3.1].
29632984
#
29642985
# [+QRESYNC+ {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html]]
2965-
# *NOTE:* Enabling QRESYNC will replace +EXPUNGE+ with +VANISHED+, but
2966-
# the extension arguments to #select, #examine, and #uid_fetch are not
2967-
# supported yet.
2986+
# *NOTE:* The +QRESYNC+ argument to #select and #examine is not supported
2987+
# yet.
29682988
#
29692989
# Adds quick resynchronization options to #select, #examine, and
29702990
# #uid_fetch. +QRESYNC+ _must_ be explicitly enabled before using any of
@@ -3683,19 +3703,23 @@ def search_internal(cmd, ...)
36833703
end
36843704
end
36853705

3686-
def fetch_internal(cmd, set, attr, mod = nil, partial: nil, changedsince: nil)
3687-
if partial && !cmd.start_with?("UID ")
3706+
def fetch_internal(cmd, set, attr, mod = nil,
3707+
partial: nil,
3708+
changedsince: nil,
3709+
vanished: false)
3710+
if cmd.start_with?("UID ")
3711+
if vanished && !changedsince
3712+
raise ArgumentError, "vanished must be used with changedsince"
3713+
end
3714+
elsif vanished
3715+
raise ArgumentError, "vanished can only be used with uid_fetch"
3716+
elsif partial
36883717
raise ArgumentError, "partial can only be used with uid_fetch"
36893718
end
36903719
set = SequenceSet[set]
3691-
if partial
3692-
mod ||= []
3693-
mod << "PARTIAL" << PartialRange[partial]
3694-
end
3695-
if changedsince
3696-
mod ||= []
3697-
mod << "CHANGEDSINCE" << Integer(changedsince)
3698-
end
3720+
(mod ||= []) << "PARTIAL" << PartialRange[partial] if partial
3721+
(mod ||= []) << "CHANGEDSINCE" << Integer(changedsince) if changedsince
3722+
(mod ||= []) << "VANISHED" if vanished
36993723
case attr
37003724
when String then
37013725
attr = RawData.new(attr)
@@ -3707,7 +3731,7 @@ def fetch_internal(cmd, set, attr, mod = nil, partial: nil, changedsince: nil)
37073731

37083732
args = [cmd, set, attr]
37093733
args << mod if mod
3710-
send_command_returning_fetch_results(*args)
3734+
send_command_returning_fetch_results(*args, vanished:)
37113735
end
37123736

37133737
def store_internal(cmd, set, attr, flags, unchangedsince: nil)
@@ -3718,14 +3742,20 @@ def store_internal(cmd, set, attr, flags, unchangedsince: nil)
37183742
send_command_returning_fetch_results(cmd, *args)
37193743
end
37203744

3721-
def send_command_returning_fetch_results(...)
3745+
def send_command_returning_fetch_results(*args, vanished: false)
37223746
synchronize do
37233747
clear_responses("FETCH")
37243748
clear_responses("UIDFETCH")
3725-
send_command(...)
3749+
send_command(*args)
37263750
fetches = clear_responses("FETCH")
37273751
uidfetches = clear_responses("UIDFETCH")
3728-
uidfetches.any? ? uidfetches : fetches
3752+
fetches = uidfetches if uidfetches.any?
3753+
if vanished
3754+
vanished = extract_responses("VANISHED", &:earlier?).last ||
3755+
VanishedData[uids: SequenceSet.empty, earlier: true]
3756+
fetches = [vanished, *fetches].freeze
3757+
end
3758+
fetches
37293759
end
37303760
end
37313761

test/net/imap/test_imap_fetch.rb

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ class IMAPFetchTest < Net::IMAP::TestCase
1212
assert_raise_with_message(ArgumentError, /\Apartial.*uid_fetch/) do
1313
imap.fetch(1, "FAST", partial: 1..10)
1414
end
15+
assert_raise_with_message(ArgumentError, /\Avanished.*uid_fetch/) do
16+
imap.fetch(1, "FAST", changedsince: 1234, vanished: true)
17+
end
18+
assert_raise_with_message(ArgumentError, /\Avanished.*changedsince/) do
19+
imap.uid_fetch(1, "FAST", vanished: true)
20+
end
1521
end
1622
end
1723

@@ -107,4 +113,59 @@ class IMAPFetchTest < Net::IMAP::TestCase
107113
end
108114
end
109115

116+
test "#uid_fetch with changedsince and vanished" do
117+
with_fake_server select: "inbox" do |server, imap|
118+
server.on("UID FETCH") do |resp|
119+
resp.untagged "VANISHED (EARLIER) 300:310,405,411"
120+
resp.untagged "1 FETCH (UID 404 MODSEQ (65402) FLAGS (\\Seen))"
121+
resp.untagged "2 FETCH (UID 406 MODSEQ (75403) FLAGS (\\Deleted))"
122+
resp.untagged "4 FETCH (UID 408 MODSEQ (29738) " \
123+
"FLAGS ($NoJunk $AutoJunk $MDNSent))"
124+
resp.done_ok
125+
end
126+
# vanished: true changes the output to begin with VanishedData
127+
vanished, *fetched = imap.uid_fetch(300..500, %w[FLAGS],
128+
changedsince: 12345, vanished: true)
129+
assert_equal(
130+
"RUBY0002 UID FETCH 300:500 (FLAGS) (CHANGEDSINCE 12345 VANISHED)",
131+
server.commands.pop.raw.strip
132+
)
133+
assert_equal Net::IMAP::VanishedData["300:310,405,411", true], vanished
134+
expected = [
135+
[1, 404, 65402, %i[Seen]],
136+
[2, 406, 75403, %i[Deleted]],
137+
[4, 408, 29738, %w[$NoJunk $AutoJunk $MDNSent]],
138+
]
139+
assert_equal expected.size, fetched.size
140+
fetched.zip(expected).each do |fetch, (seqno, uid, modseq, flags)|
141+
assert_instance_of Net::IMAP::FetchData, fetch
142+
assert_equal seqno, fetch.seqno
143+
assert_equal uid, fetch.uid
144+
assert_equal modseq, fetch.modseq
145+
assert_equal flags, fetch.flags
146+
end
147+
148+
# without VANISHED
149+
server.on("UID FETCH") do |resp|
150+
resp.untagged "1 FETCH (UID 404 MODSEQ (65402) FLAGS (\\Seen))"
151+
resp.untagged "2 FETCH (UID 406 MODSEQ (75403) FLAGS (\\Deleted))"
152+
resp.untagged "4 FETCH (UID 408 MODSEQ (29738) " \
153+
"FLAGS ($NoJunk $AutoJunk $MDNSent))"
154+
resp.done_ok
155+
end
156+
vanished, *fetched = imap.uid_fetch(300..500, %w[FLAGS],
157+
changedsince: 12345, vanished: true)
158+
assert_equal(Net::IMAP::VanishedData[Net::IMAP::SequenceSet.empty, true],
159+
vanished)
160+
assert_equal expected.size, fetched.size
161+
fetched.zip(expected).each do |fetch, (seqno, uid, modseq, flags)|
162+
assert_instance_of Net::IMAP::FetchData, fetch
163+
assert_equal seqno, fetch.seqno
164+
assert_equal uid, fetch.uid
165+
assert_equal modseq, fetch.modseq
166+
assert_equal flags, fetch.flags
167+
end
168+
end
169+
end
170+
110171
end

0 commit comments

Comments
 (0)