Skip to content

Commit 6a520fb

Browse files
authored
🔀 Merge pull request #329 from nevans/vanished
✨ Add support for VANISHED responses
2 parents 849c360 + 55c1d24 commit 6a520fb

File tree

7 files changed

+318
-29
lines changed

7 files changed

+318
-29
lines changed

‎lib/net/imap.rb

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1889,48 +1889,64 @@ 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.
1899+
#
1900+
# Returns either an array of expunged message <em>sequence numbers</em> or
1901+
# (when the appropriate capability is enabled) VanishedData of expunged
1902+
# UIDs. Previously unhandled +EXPUNGE+ or +VANISHED+ responses are merged
1903+
# with the direct response to this command. <tt>VANISHED (EARLIER)</tt>
1904+
# responses will _not_ be merged.
1905+
#
1906+
# When no messages have been expunged, an empty array is returned,
1907+
# regardless of which extensions are enabled. In a future release, an empty
1908+
# VanishedData may be returned, based on the currently enabled extensions.
18951909
#
18961910
# Related: #uid_expunge
1911+
#
1912+
# ==== Capabilities
1913+
#
1914+
# When either QRESYNC[https://tools.ietf.org/html/rfc7162] or
1915+
# UIDONLY[https://tools.ietf.org/html/rfc9586] are enabled, #expunge
1916+
# returns VanishedData, which contains UIDs---<em>not message sequence
1917+
# numbers</em>.
18971918
def expunge
1898-
synchronize do
1899-
send_command("EXPUNGE")
1900-
clear_responses("EXPUNGE")
1901-
end
1919+
expunge_internal("EXPUNGE")
19021920
end
19031921

1922+
# call-seq:
1923+
# uid_expunge{uid_set) -> array of message sequence numbers
1924+
# uid_expunge{uid_set) -> VanishedData of UIDs
1925+
#
19041926
# Sends a {UID EXPUNGE command [RFC4315 §2.1]}[https://www.rfc-editor.org/rfc/rfc4315#section-2.1]
19051927
# {[IMAP4rev2 §6.4.9]}[https://www.rfc-editor.org/rfc/rfc9051#section-6.4.9]
19061928
# to permanently remove all messages that have both the <tt>\\Deleted</tt>
19071929
# flag set and a UID that is included in +uid_set+.
19081930
#
1931+
# Returns the same result type as #expunge.
1932+
#
19091933
# By using #uid_expunge instead of #expunge when resynchronizing with
19101934
# the server, the client can ensure that it does not inadvertantly
19111935
# remove any messages that have been marked as <tt>\\Deleted</tt> by other
19121936
# clients between the time that the client was last connected and
19131937
# the time the client resynchronizes.
19141938
#
1915-
# *Note:*
1916-
# >>>
1917-
# Although the command takes a set of UIDs for its argument, the
1918-
# server still returns regular EXPUNGE responses, which contain
1919-
# a <em>sequence number</em>. These will be deleted from
1920-
# #responses and this method returns them as an array of
1921-
# <em>sequence number</em> integers.
1922-
#
19231939
# Related: #expunge
19241940
#
19251941
# ==== Capabilities
19261942
#
1927-
# The server's capabilities must include +UIDPLUS+
1943+
# The server's capabilities must include either +IMAP4rev2+ or +UIDPLUS+
19281944
# [RFC4315[https://www.rfc-editor.org/rfc/rfc4315.html]].
1945+
#
1946+
# Otherwise, #uid_expunge is updated by extensions in the same way as
1947+
# #expunge.
19291948
def uid_expunge(uid_set)
1930-
synchronize do
1931-
send_command("UID EXPUNGE", SequenceSet.new(uid_set))
1932-
clear_responses("EXPUNGE")
1933-
end
1949+
expunge_internal("UID EXPUNGE", SequenceSet.new(uid_set))
19341950
end
19351951

19361952
# :call-seq:
@@ -3261,6 +3277,22 @@ def enforce_logindisabled?
32613277
end
32623278
end
32633279

3280+
def expunge_internal(...)
3281+
synchronize do
3282+
send_command(...)
3283+
expunged_array = clear_responses("EXPUNGE")
3284+
vanished_array = extract_responses("VANISHED") { !_1.earlier? }
3285+
if vanished_array.empty?
3286+
expunged_array
3287+
elsif vanished_array.length == 1
3288+
vanished_array.first
3289+
else
3290+
merged_uids = SequenceSet[*vanished_array.map(&:uids)]
3291+
VanishedData[uids: merged_uids, earlier: false]
3292+
end
3293+
end
3294+
end
3295+
32643296
RETURN_WHOLE = /\ARETURN\z/i
32653297
RETURN_START = /\ARETURN\b/i
32663298
private_constant :RETURN_WHOLE, :RETURN_START

‎lib/net/imap/response_data.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ class IMAP < Protocol
66
autoload :FetchData, "#{__dir__}/fetch_data"
77
autoload :SearchResult, "#{__dir__}/search_result"
88
autoload :SequenceSet, "#{__dir__}/sequence_set"
9+
autoload :VanishedData, "#{__dir__}/vanished_data"
910

1011
# Net::IMAP::ContinuationRequest represents command continuation requests.
1112
#

‎lib/net/imap/response_parser.rb

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,6 @@ def remaining_unparsed
769769
def response_data__ignored; response_data__unhandled(IgnoredResponse) end
770770
alias response_data__noop response_data__ignored
771771

772-
alias expunged_resp response_data__unhandled
773772
alias uidfetch_resp response_data__unhandled
774773
alias listrights_data response_data__unhandled
775774
alias myrights_data response_data__unhandled
@@ -841,6 +840,20 @@ def response_data__simple_numeric
841840
alias mailbox_data__exists response_data__simple_numeric
842841
alias mailbox_data__recent response_data__simple_numeric
843842

843+
# The name for this is confusing, because it *replaces* EXPUNGE
844+
# >>>
845+
# expunged-resp = "VANISHED" [SP "(EARLIER)"] SP known-uids
846+
def expunged_resp
847+
name = label "VANISHED"; SP!
848+
earlier = if lpar? then label("EARLIER"); rpar; SP!; true else false end
849+
uids = known_uids
850+
data = VanishedData[uids, earlier]
851+
UntaggedResponse.new name, data, @str
852+
end
853+
854+
# TODO: replace with uid_set
855+
alias known_uids sequence_set
856+
844857
# RFC3501 & RFC9051:
845858
# msg-att = "(" (msg-att-dynamic / msg-att-static)
846859
# *(SP (msg-att-dynamic / msg-att-static)) ")"

‎lib/net/imap/vanished_data.rb

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# frozen_string_literal: true
2+
3+
module Net
4+
class IMAP < Protocol
5+
6+
# Net::IMAP::VanishedData represents the contents of a +VANISHED+ response,
7+
# which is described by the
8+
# {QRESYNC}[https://www.rfc-editor.org/rfc/rfc7162.html] extension.
9+
# [{RFC7162 §3.2.10}[https://www.rfc-editor.org/rfc/rfc7162.html#section-3.2.10]].
10+
#
11+
# +VANISHED+ responses replace +EXPUNGE+ responses when either the
12+
# {QRESYNC}[https://www.rfc-editor.org/rfc/rfc7162.html] or the
13+
# {UIDONLY}[https://www.rfc-editor.org/rfc/rfc9586.html] extension has been
14+
# enabled.
15+
class VanishedData < Data.define(:uids, :earlier)
16+
17+
# Returns a new VanishedData object.
18+
#
19+
# * +uids+ will be converted by SequenceSet.[].
20+
# * +earlier+ will be converted to +true+ or +false+
21+
def initialize(uids:, earlier:)
22+
uids = SequenceSet[uids]
23+
earlier = !!earlier
24+
super
25+
end
26+
27+
##
28+
# :attr_reader: uids
29+
#
30+
# SequenceSet of UIDs that have been permanently removed from the mailbox.
31+
32+
##
33+
# :attr_reader: earlier
34+
#
35+
# +true+ when the response was caused by Net::IMAP#uid_fetch with
36+
# <tt>vanished: true</tt> or Net::IMAP#select/Net::IMAP#examine with
37+
# <tt>qresync: true</tt>.
38+
#
39+
# +false+ when the response is used to announce message removals within an
40+
# already selected mailbox.
41+
42+
# rdoc doesn't handle attr aliases nicely. :(
43+
alias earlier? earlier # :nodoc:
44+
##
45+
# :attr_reader: earlier?
46+
#
47+
# Alias for #earlier.
48+
49+
# Returns an Array of all of the UIDs in #uids.
50+
#
51+
# See SequenceSet#numbers.
52+
def to_a; uids.numbers end
53+
54+
end
55+
end
56+
end

‎test/net/imap/fixtures/response_parser/rfc7162_condstore_qresync_responses.yml

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -142,18 +142,38 @@
142142
:response: "* VANISHED (EARLIER) 41,43:116,118,120:211,214:540\r\n"
143143
:expected: !ruby/struct:Net::IMAP::UntaggedResponse
144144
name: VANISHED
145-
data: !ruby/struct:Net::IMAP::UnparsedData
146-
unparsed_data: "(EARLIER) 41,43:116,118,120:211,214:540"
145+
data: !ruby/object:Net::IMAP::VanishedData
146+
uids: !ruby/object:Net::IMAP::SequenceSet
147+
string: 41,43:116,118,120:211,214:540
148+
tuples:
149+
- - 41
150+
- 41
151+
- - 43
152+
- 116
153+
- - 118
154+
- 118
155+
- - 120
156+
- 211
157+
- - 214
158+
- 540
159+
earlier: true
147160
raw_data: "* VANISHED (EARLIER) 41,43:116,118,120:211,214:540\r\n"
148-
comment: |
149-
Note that QRESYNC isn't supported yet, so the data is unparsed.
150161

151162
"RFC7162 QRESYNC 3.2.7. EXPUNGE Command":
152163
:response: "* VANISHED 405,407,410,425\r\n"
153164
:expected: !ruby/struct:Net::IMAP::UntaggedResponse
154165
name: VANISHED
155-
data: !ruby/struct:Net::IMAP::UnparsedData
156-
unparsed_data: '405,407,410,425'
166+
data: !ruby/object:Net::IMAP::VanishedData
167+
uids: !ruby/object:Net::IMAP::SequenceSet
168+
string: '405,407,410,425'
169+
tuples:
170+
- - 405
171+
- 405
172+
- - 407
173+
- 407
174+
- - 410
175+
- 410
176+
- - 425
177+
- 425
178+
earlier: false
157179
raw_data: "* VANISHED 405,407,410,425\r\n"
158-
comment: |
159-
Note that QRESYNC isn't supported yet, so the data is unparsed.

‎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)