Skip to content

Commit b6553f2

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 a82c1d0 commit b6553f2

File tree

2 files changed

+125
-12
lines changed

2 files changed

+125
-12
lines changed

‎lib/net/imap.rb‎

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1889,18 +1889,39 @@ def unselect
18891889
send_command("UNSELECT")
18901890
end
18911891

1892+
# call-seq:
1893+
# expunge -> array of message sequence numbers
1894+
# expunge -> VanishedData of UIDs
1895+
#
18921896
# Sends an {EXPUNGE command [IMAP4rev1 §6.4.3]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.3]
1893-
# Sends a EXPUNGE command to permanently remove from the currently
1894-
# selected mailbox all messages that have the \Deleted flag set.
1897+
# to permanently remove all messages with the +\Deleted+ flag from the
1898+
# currently selected mailbox.
18951899
#
18961900
# Related: #uid_expunge
1901+
#
1902+
# ==== Capabilities
1903+
#
1904+
# When either QRESYNC[https://tools.ietf.org/html/rfc7162] or
1905+
# UIDONLY[https://tools.ietf.org/html/rfc9586] are enabled, #expunge
1906+
# returns VanishedData, which contains UIDs---<em>not message sequence
1907+
# numbers</em>.
1908+
#
1909+
# *NOTE:* Any unhandled +VANISHED+ #responses without the +EARLIER+ modifier
1910+
# will be merged into the VanishedData and deleted from #responses. This is
1911+
# consistent with how Net::IMAP handles +EXPUNGE+ responses. Unhandled
1912+
# <tt>VANISHED (EARLIER)</tt> responses will _not_ be merged or returned.
1913+
#
1914+
# *NOTE:* When no messages are expunged, Net::IMAP currently returns an
1915+
# empty array, regardless of which extensions have been enabled. In the
1916+
# future, an empty VanishedData will be returned instead.
18971917
def expunge
1898-
synchronize do
1899-
send_command("EXPUNGE")
1900-
clear_responses("EXPUNGE")
1901-
end
1918+
expunge_internal("EXPUNGE")
19021919
end
19031920

1921+
# call-seq:
1922+
# uid_expunge -> array of message sequence numbers
1923+
# uid_expunge -> VanishedData of UIDs
1924+
#
19041925
# Sends a {UID EXPUNGE command [RFC4315 §2.1]}[https://www.rfc-editor.org/rfc/rfc4315#section-2.1]
19051926
# {[IMAP4rev2 §6.4.9]}[https://www.rfc-editor.org/rfc/rfc9051#section-6.4.9]
19061927
# to permanently remove all messages that have both the <tt>\\Deleted</tt>
@@ -1924,13 +1945,13 @@ def expunge
19241945
#
19251946
# ==== Capabilities
19261947
#
1927-
# The server's capabilities must include +UIDPLUS+
1948+
# The server's capabilities must include either +IMAP4rev2+ or +UIDPLUS+
19281949
# [RFC4315[https://www.rfc-editor.org/rfc/rfc4315.html]].
1950+
#
1951+
# Otherwise, #uid_expunge is updated by extensions in the same way as
1952+
# #expunge.
19291953
def uid_expunge(uid_set)
1930-
synchronize do
1931-
send_command("UID EXPUNGE", SequenceSet.new(uid_set))
1932-
clear_responses("EXPUNGE")
1933-
end
1954+
expunge_internal("UID EXPUNGE", SequenceSet.new(uid_set))
19341955
end
19351956

19361957
# :call-seq:
@@ -3261,6 +3282,21 @@ def enforce_logindisabled?
32613282
end
32623283
end
32633284

3285+
def expunge_internal(...)
3286+
synchronize do
3287+
send_command(...)
3288+
vanished_array = extract_responses("VANISHED") { !_1.earlier? }
3289+
if vanished_array.empty?
3290+
clear_responses("EXPUNGE")
3291+
elsif vanished_array.length == 1
3292+
vanished_array.first
3293+
else
3294+
merged_uids = SequenceSet[*vanished_array.map(&:uids)]
3295+
VanishedData[uids: merged_uids, earlier: false]
3296+
end
3297+
end
3298+
end
3299+
32643300
RETURN_WHOLE = /\ARETURN\z/i
32653301
RETURN_START = /\ARETURN\b/i
32663302
private_constant :RETURN_WHOLE, :RETURN_START

‎test/net/imap/test_imap.rb‎

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1016,7 +1016,7 @@ def test_id
10161016
end
10171017
end
10181018

1019-
def test_uidplus_uid_expunge
1019+
test "#uid_expunge with EXPUNGE responses" do
10201020
with_fake_server(select: "INBOX",
10211021
extensions: %i[UIDPLUS]) do |server, imap|
10221022
server.on "UID EXPUNGE" do |resp|
@@ -1032,6 +1032,24 @@ def test_uidplus_uid_expunge
10321032
end
10331033
end
10341034

1035+
test "#uid_expunge with VANISHED response" do
1036+
with_fake_server(select: "INBOX",
1037+
extensions: %i[UIDPLUS]) do |server, imap|
1038+
server.on "UID EXPUNGE" do |resp|
1039+
resp.untagged("VANISHED 1001,1003")
1040+
resp.done_ok
1041+
end
1042+
response = imap.uid_expunge(1000..1003)
1043+
cmd = server.commands.pop
1044+
assert_equal ["UID EXPUNGE", "1000:1003"], [cmd.name, cmd.args]
1045+
assert_equal(
1046+
Net::IMAP::VanishedData[uids: [1001, 1003], earlier: false],
1047+
response
1048+
)
1049+
assert_equal([], imap.clear_responses("VANISHED"))
1050+
end
1051+
end
1052+
10351053
def test_uidplus_appenduid
10361054
with_fake_server(select: "INBOX",
10371055
extensions: %i[UIDPLUS]) do |server, imap|
@@ -1168,6 +1186,65 @@ def test_enable
11681186
end
11691187
end
11701188

1189+
test "#expunge with EXPUNGE responses" do
1190+
with_fake_server(select: "INBOX") do |server, imap|
1191+
server.on "EXPUNGE" do |resp|
1192+
resp.untagged("1 EXPUNGE")
1193+
resp.untagged("1 EXPUNGE")
1194+
resp.untagged("99 EXPUNGE")
1195+
resp.done_ok
1196+
end
1197+
response = imap.expunge
1198+
cmd = server.commands.pop
1199+
assert_equal ["EXPUNGE", nil], [cmd.name, cmd.args]
1200+
assert_equal [1, 1, 99], response
1201+
assert_equal [], imap.clear_responses("EXPUNGED")
1202+
end
1203+
end
1204+
1205+
test "#expunge with a VANISHED response" do
1206+
with_fake_server(select: "INBOX") do |server, imap|
1207+
server.on "EXPUNGE" do |resp|
1208+
resp.untagged("VANISHED 15:456")
1209+
resp.done_ok
1210+
end
1211+
response = imap.expunge
1212+
cmd = server.commands.pop
1213+
assert_equal ["EXPUNGE", nil], [cmd.name, cmd.args]
1214+
assert_equal(
1215+
Net::IMAP::VanishedData[uids: [15..456], earlier: false],
1216+
response
1217+
)
1218+
assert_equal([], imap.clear_responses("VANISHED"))
1219+
end
1220+
end
1221+
1222+
test "#expunge with multiple VANISHED responses" do
1223+
with_fake_server(select: "INBOX") do |server, imap|
1224+
server.unsolicited("VANISHED 86")
1225+
server.on "EXPUNGE" do |resp|
1226+
resp.untagged("VANISHED (EARLIER) 1:5,99,123")
1227+
resp.untagged("VANISHED 15,456")
1228+
resp.untagged("VANISHED (EARLIER) 987,1001")
1229+
resp.done_ok
1230+
end
1231+
response = imap.expunge
1232+
cmd = server.commands.pop
1233+
assert_equal ["EXPUNGE", nil], [cmd.name, cmd.args]
1234+
assert_equal(
1235+
Net::IMAP::VanishedData[uids: [15, 86, 456], earlier: false],
1236+
response
1237+
)
1238+
assert_equal(
1239+
[
1240+
Net::IMAP::VanishedData[uids: [1..5, 99, 123], earlier: true],
1241+
Net::IMAP::VanishedData[uids: [987, 1001], earlier: true],
1242+
],
1243+
imap.clear_responses("VANISHED")
1244+
)
1245+
end
1246+
end
1247+
11711248
def test_close
11721249
with_fake_server(select: "inbox") do |server, imap|
11731250
resp = imap.close

0 commit comments

Comments
 (0)