Skip to content

Commit

Permalink
✨ Add support for VANISHED responses
Browse files Browse the repository at this point in the history
This updates the parser to handle the VANISHED response, and adds a new
response data class: VanishedData.

VanishedData was originally written as a plain class and then converted
to Data.  I'm keeping most of the tests that I wrote prior to converting
it to Data, but I probably wouldn't have written many of them if I had
used Data in the first place.
  • Loading branch information
nevans committed Dec 16, 2024
1 parent 849c360 commit a82c1d0
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 9 deletions.
1 change: 1 addition & 0 deletions lib/net/imap/response_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class IMAP < Protocol
autoload :FetchData, "#{__dir__}/fetch_data"
autoload :SearchResult, "#{__dir__}/search_result"
autoload :SequenceSet, "#{__dir__}/sequence_set"
autoload :VanishedData, "#{__dir__}/vanished_data"

# Net::IMAP::ContinuationRequest represents command continuation requests.
#
Expand Down
15 changes: 14 additions & 1 deletion lib/net/imap/response_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -769,7 +769,6 @@ def remaining_unparsed
def response_data__ignored; response_data__unhandled(IgnoredResponse) end
alias response_data__noop response_data__ignored

alias expunged_resp response_data__unhandled
alias uidfetch_resp response_data__unhandled
alias listrights_data response_data__unhandled
alias myrights_data response_data__unhandled
Expand Down Expand Up @@ -841,6 +840,20 @@ def response_data__simple_numeric
alias mailbox_data__exists response_data__simple_numeric
alias mailbox_data__recent response_data__simple_numeric

# The name for this is confusing, because it *replaces* EXPUNGE
# >>>
# expunged-resp = "VANISHED" [SP "(EARLIER)"] SP known-uids
def expunged_resp
name = label "VANISHED"; SP!
earlier = if lpar? then label("EARLIER"); rpar; SP!; true else false end
uids = known_uids
data = VanishedData[uids, earlier]
UntaggedResponse.new name, data, @str
end

# TODO: replace with uid_set
alias known_uids sequence_set

# RFC3501 & RFC9051:
# msg-att = "(" (msg-att-dynamic / msg-att-static)
# *(SP (msg-att-dynamic / msg-att-static)) ")"
Expand Down
56 changes: 56 additions & 0 deletions lib/net/imap/vanished_data.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

module Net
class IMAP < Protocol

# Net::IMAP::VanishedData represents the contents of a +VANISHED+ response,
# which is described by the
# {QRESYNC}[https://www.rfc-editor.org/rfc/rfc7162.html] extension.
# [{RFC7162 §3.2.10}[https://www.rfc-editor.org/rfc/rfc7162.html#section-3.2.10]].
#
# +VANISHED+ responses replace +EXPUNGE+ responses when either the
# {QRESYNC}[https://www.rfc-editor.org/rfc/rfc7162.html] or the
# {UIDONLY}[https://www.rfc-editor.org/rfc/rfc9586.html] extension has been
# enabled.
class VanishedData < Data.define(:uids, :earlier)

# Returns a new VanishedData object.
#
# * +uids+ will be converted by SequenceSet.[].
# * +earlier+ will be converted to +true+ or +false+
def initialize(uids:, earlier:)
uids = SequenceSet[uids]
earlier = !!earlier
super
end

##
# :attr_reader: uids
#
# SequenceSet of UIDs that have been permanently removed from the mailbox.

##
# :attr_reader: earlier
#
# +true+ when the response was caused by Net::IMAP#uid_fetch with
# <tt>vanished: true</tt> or Net::IMAP#select/Net::IMAP#examine with
# <tt>qresync: true</tt>.
#
# +false+ when the response is used to announce message removals within an
# already selected mailbox.

# rdoc doesn't handle attr aliases nicely. :(
alias earlier? earlier # :nodoc:
##
# :attr_reader: earlier?
#
# Alias for #earlier.

# Returns an Array of all of the UIDs in #uids.
#
# See SequenceSet#numbers.
def to_a; uids.numbers end

end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -142,18 +142,38 @@
:response: "* VANISHED (EARLIER) 41,43:116,118,120:211,214:540\r\n"
:expected: !ruby/struct:Net::IMAP::UntaggedResponse
name: VANISHED
data: !ruby/struct:Net::IMAP::UnparsedData
unparsed_data: "(EARLIER) 41,43:116,118,120:211,214:540"
data: !ruby/object:Net::IMAP::VanishedData
uids: !ruby/object:Net::IMAP::SequenceSet
string: 41,43:116,118,120:211,214:540
tuples:
- - 41
- 41
- - 43
- 116
- - 118
- 118
- - 120
- 211
- - 214
- 540
earlier: true
raw_data: "* VANISHED (EARLIER) 41,43:116,118,120:211,214:540\r\n"
comment: |
Note that QRESYNC isn't supported yet, so the data is unparsed.

"RFC7162 QRESYNC 3.2.7. EXPUNGE Command":
:response: "* VANISHED 405,407,410,425\r\n"
:expected: !ruby/struct:Net::IMAP::UntaggedResponse
name: VANISHED
data: !ruby/struct:Net::IMAP::UnparsedData
unparsed_data: '405,407,410,425'
data: !ruby/object:Net::IMAP::VanishedData
uids: !ruby/object:Net::IMAP::SequenceSet
string: '405,407,410,425'
tuples:
- - 405
- 405
- - 407
- 407
- - 410
- 410
- - 425
- 425
earlier: false
raw_data: "* VANISHED 405,407,410,425\r\n"
comment: |
Note that QRESYNC isn't supported yet, so the data is unparsed.
90 changes: 90 additions & 0 deletions test/net/imap/test_vanished_data.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# frozen_string_literal: true

require "net/imap"
require "test/unit"

class VanishedDataTest < Test::Unit::TestCase
VanishedData = Net::IMAP::VanishedData
SequenceSet = Net::IMAP::SequenceSet
DataFormatError = Net::IMAP::DataFormatError

test ".new(uids: string, earlier: bool)" do
vanished = VanishedData.new(uids: "1,3:5,7", earlier: true)
assert_equal SequenceSet["1,3:5,7"], vanished.uids
assert vanished.earlier?
vanished = VanishedData.new(uids: "99,111", earlier: false)
assert_equal SequenceSet["99,111"], vanished.uids
refute vanished.earlier?
end

test ".new, missing args raises ArgumentError" do
assert_raise ArgumentError do VanishedData.new end
assert_raise ArgumentError do VanishedData.new "1234" end
assert_raise ArgumentError do VanishedData.new uids: "1234" end
assert_raise ArgumentError do VanishedData.new earlier: true end
end

test ".new, nil uids raises DataFormatError" do
assert_raise DataFormatError do VanishedData.new uids: nil, earlier: true end
assert_raise DataFormatError do VanishedData.new nil, true end
end

test ".[uids: string, earlier: bool]" do
vanished = VanishedData[uids: "1,3:5,7", earlier: true]
assert_equal SequenceSet["1,3:5,7"], vanished.uids
assert vanished.earlier?
vanished = VanishedData[uids: "99,111", earlier: false]
assert_equal SequenceSet["99,111"], vanished.uids
refute vanished.earlier?
end

test ".[uids, earlier]" do
vanished = VanishedData["1,3:5,7", true]
assert_equal SequenceSet["1,3:5,7"], vanished.uids
assert vanished.earlier?
vanished = VanishedData["99,111", false]
assert_equal SequenceSet["99,111"], vanished.uids
refute vanished.earlier?
end

test ".[], mixing args raises ArgumentError" do
assert_raise ArgumentError do
VanishedData[1, true, uids: "1", earlier: true]
end
assert_raise ArgumentError do VanishedData["1234", earlier: true] end
assert_raise ArgumentError do VanishedData[nil, true, uids: "1"] end
end

test ".[], missing args raises ArgumentError" do
assert_raise ArgumentError do VanishedData[] end
assert_raise ArgumentError do VanishedData["1234"] end
end

test ".[], nil uids raises DataFormatError" do
assert_raise DataFormatError do VanishedData[nil, true] end
assert_raise DataFormatError do VanishedData[nil, nil] end
end

test "#to_a delegates to uids (SequenceSet#to_a)" do
assert_equal [1, 2, 3, 4], VanishedData["1:4", true].to_a
end

test "#deconstruct_keys returns uids and earlier" do
assert_equal({uids: SequenceSet[1,9], earlier: true},
VanishedData["1,9", true].deconstruct_keys([:uids, :earlier]))
VanishedData["1:5", false] => VanishedData[uids: SequenceSet, earlier: false]
end

test "#==" do
assert_equal VanishedData[123, false], VanishedData["123", false]
assert_equal VanishedData["3:1", false], VanishedData["1:3", false]
end

test "#eql?" do
assert VanishedData["1:3", false].eql?(VanishedData[1..3, false])
refute VanishedData["3:1", false].eql?(VanishedData["1:3", false])
refute VanishedData["1:5", false].eql?(VanishedData["1:3", false])
refute VanishedData["1:3", true].eql?(VanishedData["1:3", false])
end

end

0 comments on commit a82c1d0

Please sign in to comment.