Skip to content

Commit 0b6f2ad

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 77680a7 commit 0b6f2ad

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
@@ -1905,18 +1905,39 @@ def unselect
19051905
send_command("UNSELECT")
19061906
end
19071907

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

1937+
# call-seq:
1938+
# uid_expunge -> array of message sequence numbers
1939+
# uid_expunge -> VanishedData of UIDs
1940+
#
19201941
# Sends a {UID EXPUNGE command [RFC4315 §2.1]}[https://www.rfc-editor.org/rfc/rfc4315#section-2.1]
19211942
# {[IMAP4rev2 §6.4.9]}[https://www.rfc-editor.org/rfc/rfc9051#section-6.4.9]
19221943
# to permanently remove all messages that have both the <tt>\\Deleted</tt>
@@ -1940,13 +1961,13 @@ def expunge
19401961
#
19411962
# ===== Capabilities
19421963
#
1943-
# The server's capabilities must include +UIDPLUS+
1964+
# The server's capabilities must include either +IMAP4rev2+ or +UIDPLUS+
19441965
# [RFC4315[https://www.rfc-editor.org/rfc/rfc4315.html]].
1966+
#
1967+
# Otherwise, #uid_expunge is updated by extensions in the same way as
1968+
# #expunge.
19451969
def uid_expunge(uid_set)
1946-
synchronize do
1947-
send_command("UID EXPUNGE", SequenceSet.new(uid_set))
1948-
clear_responses("EXPUNGE")
1949-
end
1970+
expunge_internal("UID EXPUNGE", SequenceSet.new(uid_set))
19501971
end
19511972

19521973
# Sends a {SEARCH command [IMAP4rev1 §6.4.4]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.4]
@@ -2898,6 +2919,21 @@ def enforce_logindisabled?
28982919
end
28992920
end
29002921

2922+
def expunge_internal(...)
2923+
synchronize do
2924+
send_command(...)
2925+
vanished_array = extract_responses("VANISHED") { !_1.earlier? }
2926+
if vanished_array.empty?
2927+
clear_responses("EXPUNGE")
2928+
elsif vanished_array.length == 1
2929+
vanished_array.first
2930+
else
2931+
merged_uids = SequenceSet[*vanished_array.map(&:uids)]
2932+
VanishedData[uids: merged_uids, earlier: false]
2933+
end
2934+
end
2935+
end
2936+
29012937
def search_internal(cmd, keys, charset)
29022938
if keys.instance_of?(String)
29032939
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|
@@ -1267,6 +1285,65 @@ def test_enable
12671285
end
12681286
end
12691287

1288+
test "#expunge with EXPUNGE responses" do
1289+
with_fake_server(select: "INBOX") do |server, imap|
1290+
server.on "EXPUNGE" do |resp|
1291+
resp.untagged("1 EXPUNGE")
1292+
resp.untagged("1 EXPUNGE")
1293+
resp.untagged("99 EXPUNGE")
1294+
resp.done_ok
1295+
end
1296+
response = imap.expunge
1297+
cmd = server.commands.pop
1298+
assert_equal ["EXPUNGE", nil], [cmd.name, cmd.args]
1299+
assert_equal [1, 1, 99], response
1300+
assert_equal [], imap.clear_responses("EXPUNGED")
1301+
end
1302+
end
1303+
1304+
test "#expunge with a VANISHED response" do
1305+
with_fake_server(select: "INBOX") do |server, imap|
1306+
server.on "EXPUNGE" do |resp|
1307+
resp.untagged("VANISHED 15:456")
1308+
resp.done_ok
1309+
end
1310+
response = imap.expunge
1311+
cmd = server.commands.pop
1312+
assert_equal ["EXPUNGE", nil], [cmd.name, cmd.args]
1313+
assert_equal(
1314+
Net::IMAP::VanishedData[uids: [15..456], earlier: false],
1315+
response
1316+
)
1317+
assert_equal([], imap.clear_responses("VANISHED"))
1318+
end
1319+
end
1320+
1321+
test "#expunge with multiple VANISHED responses" do
1322+
with_fake_server(select: "INBOX") do |server, imap|
1323+
server.unsolicited("VANISHED 86")
1324+
server.on "EXPUNGE" do |resp|
1325+
resp.untagged("VANISHED (EARLIER) 1:5,99,123")
1326+
resp.untagged("VANISHED 15,456")
1327+
resp.untagged("VANISHED (EARLIER) 987,1001")
1328+
resp.done_ok
1329+
end
1330+
response = imap.expunge
1331+
cmd = server.commands.pop
1332+
assert_equal ["EXPUNGE", nil], [cmd.name, cmd.args]
1333+
assert_equal(
1334+
Net::IMAP::VanishedData[uids: [15, 86, 456], earlier: false],
1335+
response
1336+
)
1337+
assert_equal(
1338+
[
1339+
Net::IMAP::VanishedData[uids: [1..5, 99, 123], earlier: true],
1340+
Net::IMAP::VanishedData[uids: [987, 1001], earlier: true],
1341+
],
1342+
imap.clear_responses("VANISHED")
1343+
)
1344+
end
1345+
end
1346+
12701347
def test_close
12711348
with_fake_server(select: "inbox") do |server, imap|
12721349
resp = imap.close

0 commit comments

Comments
 (0)