Skip to content

Commit c106feb

Browse files
committed
✨ Add support for VANISHED responses
This updates the parser to handle the VANISHED response, and adds a new response data class: VanishedData.
1 parent cfaeb7e commit c106feb

File tree

5 files changed

+227
-9
lines changed

5 files changed

+227
-9
lines changed

lib/net/imap/response_data.rb

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

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

lib/net/imap/response_parser.rb

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

772772
alias esearch_response response_data__unhandled
773-
alias expunged_resp response_data__unhandled
774773
alias uidfetch_resp response_data__unhandled
775774
alias listrights_data response_data__unhandled
776775
alias myrights_data response_data__unhandled
@@ -842,6 +841,20 @@ def response_data__simple_numeric
842841
alias mailbox_data__exists response_data__simple_numeric
843842
alias mailbox_data__recent response_data__simple_numeric
844843

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

lib/net/imap/vanished_data.rb

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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
16+
17+
# call-seq:
18+
# VanishedData[uids, earlier] -> VanishedData
19+
# VanishedData[uids:, earlier:] -> VanishedData
20+
#
21+
# Delegates to ::new. Unlike ::new, ::[] can be given positional args.
22+
def self.[](uids_arg = nil, earlier_arg = nil, uids: nil, earlier: nil)
23+
if !(uids_arg.nil? && earlier_arg.nil?)
24+
if !(uids.nil? && earlier.nil?)
25+
raise ArgumentError, "do not combine positional and keyword args"
26+
else
27+
new(uids: uids_arg, earlier: earlier_arg)
28+
end
29+
else
30+
new(uids: uids, earlier: earlier)
31+
end
32+
end
33+
34+
# SequenceSet of UIDs that have been permanently removed from the mailbox.
35+
attr_reader :uids
36+
37+
# +true+ when the response was caused by Net::IMAP#uid_fetch with
38+
# <tt>vanished: true</tt> or Net::IMAP#select/Net::IMAP#examine with
39+
# <tt>qresync: true</tt>.
40+
#
41+
# +false+ when the response is used to announce message removals within an
42+
# already selected mailbox.
43+
attr_reader :earlier
44+
alias earlier? earlier
45+
46+
# Returns a new VanishedData object.
47+
#
48+
# * +uids+ will be coerced by SequenceSet.new.
49+
# * +earlier+ will be converted to +true+ or +false+
50+
#
51+
# Arguments must not be +nil+.
52+
def initialize(uids:, earlier:)
53+
raise ArgumentError, "uids must not be nil" if uids.nil?
54+
raise ArgumentError, "earlier must be true or false" if earlier.nil?
55+
@uids = SequenceSet.new(uids).freeze
56+
@earlier = !!earlier
57+
freeze
58+
end
59+
60+
# Delegates to #uids.
61+
#
62+
# See SequenceSet#numbers.
63+
def to_a; uids.numbers end
64+
65+
# Returns a hash with +:uids+ and +:earlier+ keys and the corresponding
66+
# #uid and #earlier values.
67+
def deconstruct_keys(keys) {uids: uids, earlier: earlier} end
68+
69+
# :call-seq: self == other -> true or false
70+
#
71+
# Returns +true+ when the other VanishedData represents the same #uids and
72+
# has the same value for #earlier?.
73+
def ==(other)
74+
self.class == other.class &&
75+
uids == other.uids &&
76+
earlier == other.earlier
77+
end
78+
79+
# :call-seq: eql?(other) -> true or false
80+
#
81+
# Hash equality requires the same encoded string representation for #uids.
82+
def eql?(other)
83+
self.class == other.class &&
84+
uids.eql?(other.uids) &&
85+
earlier.eql?(other.earlier)
86+
end
87+
88+
def hash # :nodoc:
89+
[self.class, uids, earlier].hash
90+
end
91+
92+
end
93+
end
94+
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
@@ -102,18 +102,38 @@
102102
:response: "* VANISHED (EARLIER) 41,43:116,118,120:211,214:540\r\n"
103103
:expected: !ruby/struct:Net::IMAP::UntaggedResponse
104104
name: VANISHED
105-
data: !ruby/struct:Net::IMAP::UnparsedData
106-
unparsed_data: "(EARLIER) 41,43:116,118,120:211,214:540"
105+
data: !ruby/object:Net::IMAP::VanishedData
106+
uids: !ruby/object:Net::IMAP::SequenceSet
107+
string: 41,43:116,118,120:211,214:540
108+
tuples:
109+
- - 41
110+
- 41
111+
- - 43
112+
- 116
113+
- - 118
114+
- 118
115+
- - 120
116+
- 211
117+
- - 214
118+
- 540
119+
earlier: true
107120
raw_data: "* VANISHED (EARLIER) 41,43:116,118,120:211,214:540\r\n"
108-
comment: |
109-
Note that QRESYNC isn't supported yet, so the data is unparsed.
110121

111122
"RFC7162 QRESYNC 3.2.7. EXPUNGE Command":
112123
:response: "* VANISHED 405,407,410,425\r\n"
113124
:expected: !ruby/struct:Net::IMAP::UntaggedResponse
114125
name: VANISHED
115-
data: !ruby/struct:Net::IMAP::UnparsedData
116-
unparsed_data: '405,407,410,425'
126+
data: !ruby/object:Net::IMAP::VanishedData
127+
uids: !ruby/object:Net::IMAP::SequenceSet
128+
string: '405,407,410,425'
129+
tuples:
130+
- - 405
131+
- 405
132+
- - 407
133+
- 407
134+
- - 410
135+
- 410
136+
- - 425
137+
- 425
138+
earlier: false
117139
raw_data: "* VANISHED 405,407,410,425\r\n"
118-
comment: |
119-
Note that QRESYNC isn't supported yet, so the data is unparsed.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# frozen_string_literal: true
2+
3+
require "net/imap"
4+
require "test/unit"
5+
6+
class VanishedDataTest < Test::Unit::TestCase
7+
VanishedData = Net::IMAP::VanishedData
8+
SequenceSet = Net::IMAP::SequenceSet
9+
10+
test ".new(uids: string, earlier: bool)" do
11+
vanished = VanishedData.new(uids: "1,3:5,7", earlier: true)
12+
assert_equal SequenceSet["1,3:5,7"], vanished.uids
13+
assert vanished.earlier?
14+
vanished = VanishedData.new(uids: "99,111", earlier: false)
15+
assert_equal SequenceSet["99,111"], vanished.uids
16+
refute vanished.earlier?
17+
end
18+
19+
test ".new(uids, earlier) raises ArgumentError" do
20+
assert_raise ArgumentError do VanishedData.new "22222", false end
21+
end
22+
23+
test ".new missing args raises ArgumentError" do
24+
assert_raise ArgumentError do VanishedData.new end
25+
assert_raise ArgumentError do VanishedData.new uids: "1234" end
26+
assert_raise ArgumentError do VanishedData.new earlier: true end
27+
end
28+
29+
test ".new nil args raises ArgumentError" do
30+
assert_raise ArgumentError do VanishedData.new uids: "14", earlier: nil end
31+
assert_raise ArgumentError do VanishedData.new uids: nil, earlier: true end
32+
end
33+
34+
test ".[uids: string, earlier: bool]" do
35+
vanished = VanishedData[uids: "1,3:5,7", earlier: true]
36+
assert_equal SequenceSet["1,3:5,7"], vanished.uids
37+
assert vanished.earlier?
38+
vanished = VanishedData[uids: "99,111", earlier: false]
39+
assert_equal SequenceSet["99,111"], vanished.uids
40+
refute vanished.earlier?
41+
end
42+
43+
test ".[uids, earlier]" do
44+
vanished = VanishedData["1,3:5,7", true]
45+
assert_equal SequenceSet["1,3:5,7"], vanished.uids
46+
assert vanished.earlier?
47+
vanished = VanishedData["99,111", false]
48+
assert_equal SequenceSet["99,111"], vanished.uids
49+
refute vanished.earlier?
50+
end
51+
52+
test ".[] mixing args raises ArgumentError" do
53+
assert_raise ArgumentError do
54+
VanishedData[1, true, uids: "1", earlier: true]
55+
end
56+
assert_raise ArgumentError do VanishedData["1234", earlier: true] end
57+
assert_raise ArgumentError do VanishedData[nil, true, uids: "1"] end
58+
end
59+
60+
test ".[] missing args raises ArgumentError" do
61+
assert_raise ArgumentError do VanishedData[] end
62+
assert_raise ArgumentError do VanishedData["1234"] end
63+
assert_raise ArgumentError do VanishedData["1234", nil] end
64+
assert_raise ArgumentError do VanishedData[nil, true] end
65+
assert_raise ArgumentError do VanishedData[nil, nil] end
66+
end
67+
68+
test "#to_a delegates to uids (SequenceSet#to_a)" do
69+
assert_equal [1, 2, 3, 4], VanishedData["1:4", true].to_a
70+
end
71+
72+
test "#deconstruct_keys returns uids and earlier" do
73+
assert_equal({uids: SequenceSet[1,9], earlier: true},
74+
VanishedData["1,9", true].deconstruct_keys([:uids, :earlier]))
75+
VanishedData["1:5", false] => VanishedData[uids: SequenceSet, earlier: false]
76+
end
77+
78+
test "#==" do
79+
assert_equal VanishedData[123, false], VanishedData["123", false]
80+
assert_equal VanishedData["3:1", false], VanishedData["1:3", false]
81+
end
82+
83+
test "#eql?" do
84+
assert VanishedData["1:3", false].eql?(VanishedData[1..3, false])
85+
refute VanishedData["3:1", false].eql?(VanishedData["1:3", false])
86+
refute VanishedData["1:5", false].eql?(VanishedData["1:3", false])
87+
refute VanishedData["1:3", true].eql?(VanishedData["1:3", false])
88+
end
89+
90+
end

0 commit comments

Comments
 (0)