Skip to content

Commit e2383d0

Browse files
committed
🚧✨ Support VANISHED response to #expunge
Both the `QRESYNC` and `UIDONLY` extensions replace `EXPUNGE` responses with `VANISHED` responses. This updates the #expunge and #uid_expunge commands to return VanishedData, rather than a (misleading) empty array.
1 parent c106feb commit e2383d0

File tree

3 files changed

+126
-14
lines changed

3 files changed

+126
-14
lines changed

‎lib/net/imap.rb‎

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1903,18 +1903,39 @@ def unselect
19031903
send_command("UNSELECT")
19041904
end
19051905

1906+
# call-seq:
1907+
# expunge -> array of message sequence numbers
1908+
# expunge -> VanishedData of UIDs
1909+
#
19061910
# Sends an {EXPUNGE command [IMAP4rev1 §6.4.3]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.3]
1907-
# Sends a EXPUNGE command to permanently remove from the currently
1908-
# selected mailbox all messages that have the \Deleted flag set.
1911+
# to permanently remove all messages with the +\Deleted+ flag from the
1912+
# currently selected mailbox.
19091913
#
19101914
# Related: #uid_expunge
1915+
#
1916+
# ===== Capabilities
1917+
#
1918+
# When either QRESYNC[https://tools.ietf.org/html/rfc7162] or
1919+
# UIDONLY[https://tools.ietf.org/html/rfc9586] are enabled, #expunge
1920+
# returns VanishedData, which contains UIDs---<em>not message sequence
1921+
# numbers</em>.
1922+
#
1923+
# *NOTE:* Any unhandled +VANISHED+ #responses without the +EARLIER+ modifier
1924+
# will be merged into the VanishedData and deleted from #responses. This is
1925+
# consistent with how Net::IMAP handles +EXPUNGE+ responses. Unhandled
1926+
# <tt>VANISHED (EARLIER)</tt> responses will _not_ be merged or returned.
1927+
#
1928+
# *NOTE:* When no messages are expunged, Net::IMAP currently returns an
1929+
# empty array, regardless of which extensions have been enabled. In the
1930+
# future, an empty VanishedData will be returned instead.
19111931
def expunge
1912-
synchronize do
1913-
send_command("EXPUNGE")
1914-
clear_responses("EXPUNGE")
1915-
end
1932+
expunge_internal("EXPUNGE")
19161933
end
19171934

1935+
# call-seq:
1936+
# uid_expunge -> array of message sequence numbers
1937+
# uid_expunge -> VanishedData of UIDs
1938+
#
19181939
# Sends a {UID EXPUNGE command [RFC4315 §2.1]}[https://www.rfc-editor.org/rfc/rfc4315#section-2.1]
19191940
# {[IMAP4rev2 §6.4.9]}[https://www.rfc-editor.org/rfc/rfc9051#section-6.4.9]
19201941
# to permanently remove all messages that have both the <tt>\\Deleted</tt>
@@ -1938,13 +1959,13 @@ def expunge
19381959
#
19391960
# ===== Capabilities
19401961
#
1941-
# The server's capabilities must include +UIDPLUS+
1962+
# The server's capabilities must include either +IMAP4rev2+ or +UIDPLUS+
19421963
# [RFC4315[https://www.rfc-editor.org/rfc/rfc4315.html]].
1964+
#
1965+
# Otherwise, #uid_expunge is updated by extensions in the same way as
1966+
# #expunge.
19431967
def uid_expunge(uid_set)
1944-
synchronize do
1945-
send_command("UID EXPUNGE", SequenceSet.new(uid_set))
1946-
clear_responses("EXPUNGE")
1947-
end
1968+
expunge_internal("UID EXPUNGE", SequenceSet.new(uid_set))
19481969
end
19491970

19501971
# Sends a {SEARCH command [IMAP4rev1 §6.4.4]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.4]
@@ -2885,6 +2906,21 @@ def enforce_logindisabled?
28852906
end
28862907
end
28872908

2909+
def expunge_internal(...)
2910+
synchronize do
2911+
send_command(...)
2912+
vanished_array = extract_responses("VANISHED") { !_1.earlier? }
2913+
if vanished_array.empty?
2914+
clear_responses("EXPUNGE")
2915+
elsif vanished_array.length == 1
2916+
vanished_array.first
2917+
else
2918+
merged_uids = SequenceSet[*vanished_array.map(&:uids)]
2919+
VanishedData[uids: merged_uids, earlier: false]
2920+
end
2921+
end
2922+
end
2923+
28882924
def search_internal(cmd, keys, charset)
28892925
if keys.instance_of?(String)
28902926
keys = [RawData.new(keys)]

‎lib/net/imap/vanished_data.rb‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,8 @@ def self.[](uids_arg = nil, earlier_arg = nil, uids: nil, earlier: nil)
5252
def initialize(uids:, earlier:)
5353
raise ArgumentError, "uids must not be nil" if uids.nil?
5454
raise ArgumentError, "earlier must be true or false" if earlier.nil?
55-
@uids = SequenceSet.new(uids).freeze
55+
@uids = SequenceSet.new(uids)
5656
@earlier = !!earlier
57-
freeze
5857
end
5958

6059
# Delegates to #uids.

‎test/net/imap/test_imap.rb‎

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1014,7 +1014,7 @@ def test_id
10141014
end
10151015
end
10161016

1017-
def test_uidplus_uid_expunge
1017+
test "#uid_expunge with EXPUNGE responses" do
10181018
with_fake_server(select: "INBOX",
10191019
extensions: %i[UIDPLUS]) do |server, imap|
10201020
server.on "UID EXPUNGE" do |resp|
@@ -1030,6 +1030,24 @@ def test_uidplus_uid_expunge
10301030
end
10311031
end
10321032

1033+
test "#uid_expunge with VANISHED response" do
1034+
with_fake_server(select: "INBOX",
1035+
extensions: %i[UIDPLUS]) do |server, imap|
1036+
server.on "UID EXPUNGE" do |resp|
1037+
resp.untagged("VANISHED 1001,1003")
1038+
resp.done_ok
1039+
end
1040+
response = imap.uid_expunge(1000..1003)
1041+
cmd = server.commands.pop
1042+
assert_equal ["UID EXPUNGE", "1000:1003"], [cmd.name, cmd.args]
1043+
assert_equal(
1044+
Net::IMAP::VanishedData[uids: [1001, 1003], earlier: false],
1045+
response
1046+
)
1047+
assert_equal([], imap.clear_responses("VANISHED"))
1048+
end
1049+
end
1050+
10331051
def test_uidplus_appenduid
10341052
with_fake_server(select: "INBOX",
10351053
extensions: %i[UIDPLUS]) do |server, imap|
@@ -1224,6 +1242,65 @@ def test_clear_responses
12241242
end
12251243
end
12261244

1245+
test "#expunge with EXPUNGE responses" do
1246+
with_fake_server(select: "INBOX") do |server, imap|
1247+
server.on "EXPUNGE" do |resp|
1248+
resp.untagged("1 EXPUNGE")
1249+
resp.untagged("1 EXPUNGE")
1250+
resp.untagged("99 EXPUNGE")
1251+
resp.done_ok
1252+
end
1253+
response = imap.expunge
1254+
cmd = server.commands.pop
1255+
assert_equal ["EXPUNGE", nil], [cmd.name, cmd.args]
1256+
assert_equal [1, 1, 99], response
1257+
assert_equal [], imap.clear_responses("EXPUNGED")
1258+
end
1259+
end
1260+
1261+
test "#expunge with a VANISHED response" do
1262+
with_fake_server(select: "INBOX") do |server, imap|
1263+
server.on "EXPUNGE" do |resp|
1264+
resp.untagged("VANISHED 15:456")
1265+
resp.done_ok
1266+
end
1267+
response = imap.expunge
1268+
cmd = server.commands.pop
1269+
assert_equal ["EXPUNGE", nil], [cmd.name, cmd.args]
1270+
assert_equal(
1271+
Net::IMAP::VanishedData[uids: [15..456], earlier: false],
1272+
response
1273+
)
1274+
assert_equal([], imap.clear_responses("VANISHED"))
1275+
end
1276+
end
1277+
1278+
test "#expunge with multiple VANISHED responses" do
1279+
with_fake_server(select: "INBOX") do |server, imap|
1280+
server.unsolicited("VANISHED 86")
1281+
server.on "EXPUNGE" do |resp|
1282+
resp.untagged("VANISHED (EARLIER) 1:5,99,123")
1283+
resp.untagged("VANISHED 15,456")
1284+
resp.untagged("VANISHED (EARLIER) 987,1001")
1285+
resp.done_ok
1286+
end
1287+
response = imap.expunge
1288+
cmd = server.commands.pop
1289+
assert_equal ["EXPUNGE", nil], [cmd.name, cmd.args]
1290+
assert_equal(
1291+
Net::IMAP::VanishedData[uids: [15, 86, 456], earlier: false],
1292+
response
1293+
)
1294+
assert_equal(
1295+
[
1296+
Net::IMAP::VanishedData[uids: [1..5, 99, 123], earlier: true],
1297+
Net::IMAP::VanishedData[uids: [987, 1001], earlier: true],
1298+
],
1299+
imap.clear_responses("VANISHED")
1300+
)
1301+
end
1302+
end
1303+
12271304
def test_close
12281305
with_fake_server(select: "inbox") do |server, imap|
12291306
resp = imap.close

0 commit comments

Comments
 (0)