diff --git a/lib/net/imap.rb b/lib/net/imap.rb
index da028106..b3330b9a 100644
--- a/lib/net/imap.rb
+++ b/lib/net/imap.rb
@@ -1934,9 +1934,9 @@ def uid_expunge(uid_set)
#
# Sends a {SEARCH command [IMAP4rev1 §6.4.4]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.4]
# to search the mailbox for messages that match the given search +criteria+,
- # and returns a SearchResult. SearchResult inherits from Array (for
- # backward compatibility) but adds SearchResult#modseq when the +CONDSTORE+
- # capability has been enabled.
+ # and returns either a SearchResult or an ESearchResult. SearchResult
+ # inherits from Array (for backward compatibility) but adds
+ # SearchResult#modseq when the +CONDSTORE+ capability has been enabled.
#
# +criteria+ is one or more search keys and their arguments, which may be
# provided as an array or a string.
@@ -1967,8 +1967,11 @@ def uid_expunge(uid_set)
# set}[https://www.iana.org/assignments/character-sets/character-sets.xhtml]
# used by strings in the search +criteria+. When +charset+ isn't specified,
# either "US-ASCII" or "UTF-8" is assumed, depending on
- # the server's capabilities. +charset+ may be sent inside +criteria+
- # instead of as a separate argument.
+ # the server's capabilities.
+ #
+ # _NOTE:_ Return options and +charset+ may be sent as part of +criteria+.
+ # Do not use the +charset+ argument when either return options or charset
+ # are embedded in +criteria+.
#
# Related: #uid_search
#
@@ -1988,6 +1991,12 @@ def uid_expunge(uid_set)
# # criteria string contains charset arg
# imap.search("CHARSET UTF-8 OR UNSEEN (FLAGGED SUBJECT foo)")
#
+ # Sending return optionsand charset embedded in the +crriteria+ arg:
+ # imap.search("RETURN (MIN MAX) CHARSET UTF-8 (OR UNSEEN FLAGGED)")
+ # imap.search(["RETURN", %w(MIN MAX),
+ # "CHARSET", "UTF-8",
+ # %w(OR UNSEEN FLAGGED)])
+ #
# ===== Search keys
#
# For full definitions of the standard search +criteria+,
@@ -2178,6 +2187,12 @@ def uid_expunge(uid_set)
#
# ===== Capabilities
#
+ # Return options should only be specified when the server supports
+ # +IMAP4rev2+ or an extension that allows them, such as +ESEARCH+.
+ #
+ # When +IMAP4rev2+ is enabled, or when the server supports +IMAP4rev2+ but
+ # not +IMAP4rev1+, ESearchResult is always returned instead of SearchResult.
+ #
# If CONDSTORE[https://www.rfc-editor.org/rfc/rfc7162.html] is supported
# and enabled for the selected mailbox, a non-empty SearchResult will
# include a +MODSEQ+ value.
diff --git a/lib/net/imap/esearch_result.rb b/lib/net/imap/esearch_result.rb
new file mode 100644
index 00000000..796b5e86
--- /dev/null
+++ b/lib/net/imap/esearch_result.rb
@@ -0,0 +1,225 @@
+# frozen_string_literal: true
+
+module Net
+ class IMAP
+ # An Extended search result which is returned by IMAP#search,
+ # IMAP#uid_search, IMAP#sort, and IMAP#uid_sort instead of SearchResult
+ # under the following conditions:
+ #
+ # * The server supports +ESEARCH+ and a +return+ option was specified.
+ # * The server supports +ESORT+ and a +return+ options was specified (for
+ # IMAP#sort and IMAP#uid_sort).
+ # * The server supports +IMAP4rev2+ but _not_ +IMAP4rev1+.
+ # * +IMAP4rev2+ has been enabled.
+ #
+ class ESearchResult < Data.define(:tag, :uid, :data)
+ def initialize(tag: nil, uid: nil, data: nil)
+ tag => String | nil; tag = -tag if tag
+ uid => true | false | nil; uid = !!uid
+ data => Array | nil; data ||= []; data.freeze
+ super
+ end
+
+ # :call-seq: to_a -> Array of integers
+ #
+ # When either #all or #partial contains a SequenceSet of message sequence
+ # numbers or UIDs, +to_a+ returns that set as an array of integers.
+ #
+ # When both #all and #partial are +nil+, either because the server
+ # returned no results or because +ALL+ and +PARTIAL+ were not included in
+ # the IMAP#search +RETURN+ options, #to_a returns an empty array.
+ #
+ # Note that +to_a+ is also a valid method on SearchResult, so it can be
+ # used without checking if the server returned +SEARCH+ or +ESEARCH+ data.
+ def to_a; all&.numbers || partial&.to_a || [] end
+
+ ##
+ # method: tag
+ # :call-seq: tag -> string or nil
+ #
+ # The tag of the command that caused the response to be returned.
+ #
+ # If it is missing, then the response was not caused by a particular IMAP
+ # command.
+
+ ##
+ # method: uid
+ # :call-seq: uid -> boolean
+ #
+ # When true, all #data in the +ESEARCH+ response refers to UIDs;
+ # otherwise, all returned #data refers to message sequence numbers.
+
+ alias uid? uid
+
+ ##
+ # method: data
+ # :call-seq: data -> array of [name, value] pairs
+ #
+ # Search return data, which can also be retrieved by #min, #max, #all,
+ # #count, #modseq, and other methods. Most names correspond to an
+ # IMAP#search +return+ option of the same name.
+ #
+ # Stored as an array of (name, value) pairs rather than as a hash, because
+ # extensions may allow the same name to be used more than once per result.
+
+ # :call-seq: min -> integer or nil
+ #
+ # The lowest message number/UID that satisfies the SEARCH criteria.
+ # Returns nil when the associated search command has no results, or when
+ # the +MIN+ return option wasn't specified.
+ #
+ # See +ESEARCH+ ({RFC4731
+ # §3.1}[https://www.rfc-editor.org/rfc/rfc4731.html#section-3.1]) or
+ # +IMAP4rev2+ ({RFC9051
+ # §7.3.4}[https://www.rfc-editor.org/rfc/rfc9051.html#section-7.3.4])
+ def min; data.assoc("MIN")&.last end
+
+ # :call-seq: max -> integer or nil
+ #
+ # The highest message number/UID that satisfies the SEARCH criteria.
+ # Returns nil when the associated search command has no results, or when
+ # the +MAX+ return option wasn't specified.
+ #
+ # See +ESEARCH+ ({RFC4731
+ # §3.1}[https://www.rfc-editor.org/rfc/rfc4731.html#section-3.1]) or
+ # +IMAP4rev2+ ({RFC9051
+ # §7.3.4}[https://www.rfc-editor.org/rfc/rfc9051.html#section-7.3.4])
+ def max; data.assoc("MAX")&.last end
+
+ # :call-seq: all -> sequence set or nil
+ #
+ # A SequenceSet containing all message numbers/UIDs that satisfy the
+ # SEARCH criteria. Returns +nil+ when the associated search command has
+ # no results, or when the +ALL+ return option wasn't specified.
+ #
+ # See +ESEARCH+ ({RFC4731
+ # §3.1}[https://www.rfc-editor.org/rfc/rfc4731.html#section-3.1]) or
+ # +IMAP4rev2+ ({RFC9051
+ # §7.3.4}[https://www.rfc-editor.org/rfc/rfc9051.html#section-7.3.4])
+ #
+ # See also: #to_a
+ def all; data.assoc("ALL")&.last end
+
+ # :call-seq: count -> integer or nil
+ #
+ # Returns the number of messages that satisfy the SEARCH criteria.
+ # Returns +nil+ when the associated search command has no results.
+ #
+ # See +ESEARCH+ ({RFC4731
+ # §3.1}[https://www.rfc-editor.org/rfc/rfc4731.html#section-3.1]) or
+ # +IMAP4rev2+ ({RFC9051
+ # §7.3.4}[https://www.rfc-editor.org/rfc/rfc9051.html#section-7.3.4])
+ def count; data.assoc("COUNT")&.last end
+
+ # :call-seq: modseq -> integer or nil
+ #
+ # The highest +mod-sequence+ of all messages in the set that satisfy the
+ # SEARCH criteria and result options. Returns +nil+ when the associated
+ # search command has no results.
+ #
+ # See +CONDSTORE+
+ # {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
+ def modseq; data.assoc("MODSEQ")&.last end
+
+ class ContextUpdate < Data.define(:position, :set)
+ def initialize(position:, set:)
+ position = NumValidator.ensure_number(position)
+ set => SequenceSet
+ super
+ end
+
+ ##
+ # method: position
+
+ ##
+ # method: set
+
+ end
+
+ class AddToContext < ContextUpdate
+ end
+
+ class RemoveFromContext < ContextUpdate
+ end
+
+ # :call-seq: addto -> array of insertion updates, or nil
+ #
+ # Notification of updates, inserting messages into the result list for the
+ # command issued with #tag.
+ #
+ # See CONTEXT=SEARCH/CONTEXT=SORT
+ # {[RFC5267]}[https://www.rfc-editor.org/rfc/rfc5267.html]
+ def addto
+ data.flat_map { _1 == "ADDTO" ? _2 : [] }
+ end
+
+ # :call-seq: removefrom -> array of removal updates, or nil
+ #
+ # Notification of updates, removing messages into the result list for the
+ # command issued with #tag.
+ #
+ # See CONTEXT=SEARCH/CONTEXT=SORT
+ # {[RFC5267]}[https://www.rfc-editor.org/rfc/rfc5267.html]
+ def removefrom
+ data.flat_map { _1 == "REMOVEFROM" ? _2 : [] }
+ end
+
+ # :call-seq: updates -> array of context updates, or nil
+ #
+ # Notification of updates, inserting or removing messages to or from the
+ # result list for the command issued with #tag.
+ #
+ # See CONTEXT=SEARCH/CONTEXT=SORT
+ # {[RFC5267]}[https://www.rfc-editor.org/rfc/rfc5267.html]
+ def updates
+ data.flat_map { %w[ADDTO REMOVEFROM].include?(_1) ? _2 : [] }
+ end
+
+ # See +PARTIAL+ {[RFC9394]}[https://www.rfc-editor.org/rfc/rfc9394.html]
+ # or CONTEXT=SEARCH/CONTEXT=SORT
+ # {[RFC5267]}[https://www.rfc-editor.org/rfc/rfc5267.html]
+ #
+ # See also: #to_a
+ class PartialResult < Data.define(:range, :results)
+ def initialize(range:, results:)
+ range => Range
+ results => SequenceSet | nil
+ super
+ end
+
+ ##
+ # method: range
+ # :call-seq: range -> range
+
+ ##
+ # method: results
+ # :call-seq: results -> sequence set or nil
+
+ # Converts #results to an array of integers.
+ #
+ # See ESearchResult#to_a.
+ def to_a; results&.numbers || [] end
+ end
+
+ # :call-seq: partial -> PartialResult or nil
+ #
+ # Return a PartialResult with a subset of the message numbers/UIDs that
+ # satisfy the SEARCH criteria.
+ #
+ # See +PARTIAL+ {[RFC9394]}[https://www.rfc-editor.org/rfc/rfc9394.html]
+ # or CONTEXT=SEARCH/CONTEXT=SORT
+ # {[RFC5267]}[https://www.rfc-editor.org/rfc/rfc5267.html]
+ def partial; data.assoc("PARTIAL")&.last end
+
+ # :call-seq: relevancy -> integer or nil
+ #
+ # Return a relevancy score for each message that satisfies the SEARCH
+ # criteria.
+ #
+ # See SEARCH=FUZZY
+ # {[RFC6203]}[https://www.rfc-editor.org/rfc/rfc6203.html]
+ def relevancy; data.assoc("RELEVANCY")&.last end
+
+ end
+ end
+end
diff --git a/lib/net/imap/response_data.rb b/lib/net/imap/response_data.rb
index 25c1d42c..6b50cf81 100644
--- a/lib/net/imap/response_data.rb
+++ b/lib/net/imap/response_data.rb
@@ -2,6 +2,7 @@
module Net
class IMAP < Protocol
+ autoload :ESearchResult, "#{__dir__}/esearch_result"
autoload :FetchData, "#{__dir__}/fetch_data"
autoload :SearchResult, "#{__dir__}/search_result"
autoload :SequenceSet, "#{__dir__}/sequence_set"
diff --git a/lib/net/imap/response_parser.rb b/lib/net/imap/response_parser.rb
index f120f9ba..42354332 100644
--- a/lib/net/imap/response_parser.rb
+++ b/lib/net/imap/response_parser.rb
@@ -321,6 +321,20 @@ module RFC3629
SEQUENCE_SET = /#{SEQUENCE_SET_ITEM}(?:,#{SEQUENCE_SET_ITEM})*/n
SEQUENCE_SET_STR = /\A#{SEQUENCE_SET}\z/n
+ # partial-range-first = nz-number ":" nz-number
+ # ;; Request to search from oldest (lowest UIDs) to
+ # ;; more recent messages.
+ # ;; A range 500:400 is the same as 400:500.
+ # ;; This is similar to from [RFC3501]
+ # ;; but cannot contain "*".
+ PARTIAL_RANGE_FIRST = /\A(#{NZ_NUMBER}):(#{NZ_NUMBER})\z/n
+
+ # partial-range-last = MINUS nz-number ":" MINUS nz-number
+ # ;; Request to search from newest (highest UIDs) to
+ # ;; oldest messages.
+ # ;; A range -500:-400 is the same as -400:-500.
+ PARTIAL_RANGE_LAST = /\A(-#{NZ_NUMBER}):(-#{NZ_NUMBER})\z/n
+
# RFC3501:
# literal = "{" number "}" CRLF *CHAR8
# ; Number represents the number of CHAR8s
@@ -769,7 +783,6 @@ def remaining_unparsed
def response_data__ignored; response_data__unhandled(IgnoredResponse) end
alias response_data__noop response_data__ignored
- alias esearch_response response_data__unhandled
alias expunged_resp response_data__unhandled
alias uidfetch_resp response_data__unhandled
alias listrights_data response_data__unhandled
@@ -1468,6 +1481,168 @@ def mailbox_data__search
end
alias sort_data mailbox_data__search
+ # esearch-response = "ESEARCH" [search-correlator] [SP "UID"]
+ # *(SP search-return-data)
+ # ;; Note that SEARCH and ESEARCH responses
+ # ;; SHOULD be mutually exclusive,
+ # ;; i.e., only one of the response types
+ # ;; should be
+ # ;; returned as a result of a command.
+ # esearch-response = "ESEARCH" [search-correlator] [SP "UID"]
+ # *(SP search-return-data)
+ # ; ESEARCH response replaces SEARCH response
+ # ; from IMAP4rev1.
+ # search-correlator = SP "(" "TAG" SP tag-string ")"
+ def esearch_response
+ name = label("ESEARCH")
+ tag = search_correlator if peek_str?(" (")
+ uid = peek_re?(/\G UID\b/i) && (SP!; label("UID"); true)
+ data = []
+ data << search_return_data while SP?
+ esearch = ESearchResult.new(tag, uid, data)
+ UntaggedResponse.new(name, esearch, @str)
+ end
+
+ # From RFC4731 (ESEARCH):
+ # search-return-data = "MIN" SP nz-number /
+ # "MAX" SP nz-number /
+ # "ALL" SP sequence-set /
+ # "COUNT" SP number /
+ # search-ret-data-ext
+ # ; All return data items conform to
+ # ; search-ret-data-ext syntax.
+ # search-ret-data-ext = search-modifier-name SP search-return-value
+ # search-modifier-name = tagged-ext-label
+ # search-return-value = tagged-ext-val
+ #
+ # From RFC4731 (ESEARCH):
+ # search-return-data =/ "MODSEQ" SP mod-sequence-value
+ #
+ # From RFC5267 (CONTEXT=SEARCH, CONTEXT=SORT):
+ # search-return-data =/ ret-data-partial / ret-data-addto /
+ # ret-data-removefrom
+ #
+ # From RFC6203 (SEARCH=FUZZY):
+ # search-return-data =/ "RELEVANCY" SP score-list
+ #
+ # From RFC9394 (PARTIAL):
+ # search-return-data =/ ret-data-partial
+ #
+ def search_return_data
+ label = search_modifier_name; SP!
+ value =
+ case label
+ when "MIN" then nz_number
+ when "MAX" then nz_number
+ when "ALL" then sequence_set
+ when "COUNT" then number
+ when "MODSEQ" then mod_sequence_value # RFC7162: CONDSTORE
+ when "RELEVANCY" then score_list # RFC6203: SEARCH=FUZZY
+ when "PARTIAL" then ret_data_partial__value # RFC9394: PARTIAL
+ when "ADDTO" then ret_data_addto__value # RFC5267: CONTEXT=*
+ when "REMOVEFROM" then ret_data_removefrom__value # RFC5267: CONTEXT=*
+ else search_return_value
+ end
+ [label, value]
+ end
+
+ # From RFC5267 (CONTEXT=SEARCH, CONTEXT=SORT) and RFC9394 (PARTIAL):
+ # ret-data-partial = "PARTIAL"
+ # SP "(" partial-range SP partial-results ")"
+ def ret_data_partial__value
+ lpar
+ range = partial_range; SP!
+ results = partial_results
+ rpar
+ ESearchResult::PartialResult.new(range, results)
+ end
+
+ # partial-range = partial-range-first / partial-range-last
+ # tagged-ext-simple =/ partial-range-last
+ def partial_range
+ case (str = atom)
+ when Patterns::PARTIAL_RANGE_FIRST, Patterns::PARTIAL_RANGE_LAST
+ min, max = [Integer($1), Integer($2)].minmax
+ min..max
+ else
+ parse_error("unexpected atom %p, expected partial-range", str)
+ end
+ end
+
+ # partial-results = sequence-set / "NIL"
+ # ;; from [RFC3501].
+ # ;; NIL indicates that no results correspond to
+ # ;; the requested range.
+ def partial_results; NIL? ? nil : sequence_set end
+
+ # ret-data-addto = "ADDTO"
+ # SP "(" context-position SP sequence-set
+ # *(SP context-position SP sequence-set)
+ # ")"
+ def ret_data_addto__value
+ lpar; list = [ret_data_addto__item]
+ (SP!; list << ret_data_addto__item) until rpar?
+ list
+ end
+
+ def ret_data_addto__item
+ ESearchResult::AddToContext.new(context_position, (SP!; sequence_set))
+ end
+
+ # ret-data-removefrom = "REMOVEFROM"
+ # SP "(" context-position SP sequence-set
+ # *(SP context-position SP sequence-set)
+ # ")"
+ def ret_data_removefrom__value
+ lpar; list = [ret_data_removefrom__item]
+ (SP!; list << ret_data_removefrom__item) until rpar?
+ list
+ end
+
+ def ret_data_removefrom__item
+ ESearchResult::RemoveFromContext.new(context_position,
+ (SP!; sequence_set))
+ end
+
+ # context-position = number
+ # ;; Context position may be 0 for SEARCH result additions.
+ # ;; from [IMAP]
+ alias context_position number
+
+ # search-modifier-name = tagged-ext-label
+ alias search_modifier_name tagged_ext_label
+
+ # search-return-value = tagged-ext-val
+ # ; Data for the returned search option.
+ # ; A single "nz-number"/"number"/"number64" value
+ # ; can be returned as an atom (i.e., without
+ # ; quoting). A sequence-set can be returned
+ # ; as an atom as well.
+ def search_return_value; ExtensionData.new(tagged_ext_val) end
+
+ # From RFC6203 (SEARCH=FUZZY):
+ # score = 1*3DIGIT
+ # ;; (1 <= n <= 100)
+ alias score nz_number
+
+ # From RFC6203 (SEARCH=FUZZY):
+ # score-list = "(" [score *(SP score)] ")"
+ def score_list
+ lpar; return [] if rpar?
+ list = [score]; (SP!; list << score) until rpar?
+ list
+ end
+
+ # search-correlator = SP "(" "TAG" SP tag-string ")"
+ def search_correlator
+ SP!; lpar; label("TAG"); SP!; tag = tag_string; rpar
+ tag
+ end
+
+ # tag-string = astring
+ # ; represented as
+ alias tag_string astring
+
# RFC5256: THREAD
# thread-data = "THREAD" [SP 1*thread-list]
def thread_data
diff --git a/lib/net/imap/response_parser/parser_utils.rb b/lib/net/imap/response_parser/parser_utils.rb
index bc84bed9..85fd2b8d 100644
--- a/lib/net/imap/response_parser/parser_utils.rb
+++ b/lib/net/imap/response_parser/parser_utils.rb
@@ -185,6 +185,11 @@ def peek_str?(str)
@str[@pos, str.length] == str
end
+ def peek_re?(re)
+ assert_no_lookahead if Net::IMAP.debug
+ re.match?(@str, @pos)
+ end
+
def peek_re(re)
assert_no_lookahead if config.debug?
re.match(@str, @pos)
diff --git a/rakelib/rfcs.rake b/rakelib/rfcs.rake
index d1b9bb7c..950c49cc 100644
--- a/rakelib/rfcs.rake
+++ b/rakelib/rfcs.rake
@@ -145,6 +145,7 @@ RFCS = {
8514 => "IMAP SAVEDATE",
8970 => "IMAP PREVIEW",
9208 => "IMAP QUOTA, QUOTA=, QUOTASET",
+ 9394 => "IMAP PARTIAL",
# etc...
3629 => "UTF8",
diff --git a/test/net/imap/fixtures/response_parser/esearch_responses.yml b/test/net/imap/fixtures/response_parser/esearch_responses.yml
new file mode 100644
index 00000000..d070d433
--- /dev/null
+++ b/test/net/imap/fixtures/response_parser/esearch_responses.yml
@@ -0,0 +1,331 @@
+---
+:tests:
+ rfc9051_6.4.4_ESEARCH_example_1:
+ :response: "* ESEARCH (TAG \"A282\") MIN 2 COUNT 3\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: A282
+ uid: false
+ data:
+ - - MIN
+ - 2
+ - - COUNT
+ - 3
+ raw_data: "* ESEARCH (TAG \"A282\") MIN 2 COUNT 3\r\n"
+
+ rfc9051_6.4.4_ESEARCH_example_2:
+ :response: "* ESEARCH (TAG \"A283\") ALL 2,10:11\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: A283
+ uid: false
+ data:
+ - - ALL
+ - !ruby/object:Net::IMAP::SequenceSet
+ string: 2,10:11
+ tuples:
+ - - 2
+ - 2
+ - - 10
+ - 11
+ raw_data: "* ESEARCH (TAG \"A283\") ALL 2,10:11\r\n"
+
+ rfc9051_6.4.4_ESEARCH_example_3:
+ :response: "* ESEARCH (TAG \"A284\")\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: A284
+ uid: false
+ data: []
+ raw_data: "* ESEARCH (TAG \"A284\")\r\n"
+
+ rfc9051_6.4.4_ESEARCH_example_4:
+ :response: "* ESEARCH (TAG \"A285\") ALL 43\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: A285
+ uid: false
+ data:
+ - - ALL
+ - !ruby/object:Net::IMAP::SequenceSet
+ string: '43'
+ tuples:
+ - - 43
+ - 43
+ raw_data: "* ESEARCH (TAG \"A285\") ALL 43\r\n"
+
+ rfc9051_6.4.4_ESEARCH_example_5:
+ :response: "* ESEARCH (TAG \"A284\") MIN 4\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: A284
+ uid: false
+ data:
+ - - MIN
+ - 4
+ raw_data: "* ESEARCH (TAG \"A284\") MIN 4\r\n"
+
+ rfc9051_6.4.4_ESEARCH_example_6:
+ :response: "* ESEARCH (TAG \"A285\") UID MIN 7 MAX 3800\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: A285
+ uid: true
+ data:
+ - - MIN
+ - 7
+ - - MAX
+ - 3800
+ raw_data: "* ESEARCH (TAG \"A285\") UID MIN 7 MAX 3800\r\n"
+
+ rfc9051_6.4.4_ESEARCH_example_7:
+ :response: "* ESEARCH (TAG \"A286\") COUNT 15\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: A286
+ uid: false
+ data:
+ - - COUNT
+ - 15
+ raw_data: "* ESEARCH (TAG \"A286\") COUNT 15\r\n"
+
+ rfc9051_6.4.4.4_ESEARCH_example_1:
+ :response: "* ESEARCH (TAG \"A301\") UID ALL 17,900,901\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: A301
+ uid: true
+ data:
+ - - ALL
+ - !ruby/object:Net::IMAP::SequenceSet
+ string: '17,900,901'
+ tuples:
+ - - 17
+ - 17
+ - - 900
+ - 901
+ raw_data: "* ESEARCH (TAG \"A301\") UID ALL 17,900,901\r\n"
+
+ rfc9051_6.4.4.4_ESEARCH_example_2:
+ :response: "* ESEARCH (TAG \"P283\") ALL 882,1102,3003,3005:3006\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: P283
+ uid: false
+ data:
+ - - ALL
+ - !ruby/object:Net::IMAP::SequenceSet
+ string: 882,1102,3003,3005:3006
+ tuples:
+ - - 882
+ - 882
+ - - 1102
+ - 1102
+ - - 3003
+ - 3003
+ - - 3005
+ - 3006
+ raw_data: "* ESEARCH (TAG \"P283\") ALL 882,1102,3003,3005:3006\r\n"
+
+ rfc9051_6.4.4.4_ESEARCH_example_3:
+ :response: "* ESEARCH (TAG \"G283\") ALL 3:15,27,29:103\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: G283
+ uid: false
+ data:
+ - - ALL
+ - !ruby/object:Net::IMAP::SequenceSet
+ string: 3:15,27,29:103
+ tuples:
+ - - 3
+ - 15
+ - - 27
+ - 27
+ - - 29
+ - 103
+ raw_data: "* ESEARCH (TAG \"G283\") ALL 3:15,27,29:103\r\n"
+
+ rfc9051_6.4.4.4_ESEARCH_example_4:
+ :response: "* ESEARCH (TAG \"C283\") ALL 2,10:15,21\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: C283
+ uid: false
+ data:
+ - - ALL
+ - !ruby/object:Net::IMAP::SequenceSet
+ string: 2,10:15,21
+ tuples:
+ - - 2
+ - 2
+ - - 10
+ - 15
+ - - 21
+ - 21
+ raw_data: "* ESEARCH (TAG \"C283\") ALL 2,10:15,21\r\n"
+
+ rfc9051_6.4.4.4_ESEARCH_example_5:
+ :response: "* ESEARCH (TAG \"C284\") MIN 2\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: C284
+ uid: false
+ data:
+ - - MIN
+ - 2
+ raw_data: "* ESEARCH (TAG \"C284\") MIN 2\r\n"
+
+ rfc9051_6.4.4.4_ESEARCH_example_6:
+ :response: "* ESEARCH (TAG \"C285\") MIN 2 MAX 21\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: C285
+ uid: false
+ data:
+ - - MIN
+ - 2
+ - - MAX
+ - 21
+ raw_data: "* ESEARCH (TAG \"C285\") MIN 2 MAX 21\r\n"
+
+ rfc9051_6.4.4.4_ESEARCH_example_7:
+ :response: "* ESEARCH (TAG \"C286\") MIN 2 MAX 21 COUNT 8\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: C286
+ uid: false
+ data:
+ - - MIN
+ - 2
+ - - MAX
+ - 21
+ - - COUNT
+ - 8
+ raw_data: "* ESEARCH (TAG \"C286\") MIN 2 MAX 21 COUNT 8\r\n"
+
+ rfc9051_6.4.4.4_ESEARCH_example_8:
+ :response: "* ESEARCH (TAG \"C286\") MIN 2 ALL 2,10:15,21\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: C286
+ uid: false
+ data:
+ - - MIN
+ - 2
+ - - ALL
+ - !ruby/object:Net::IMAP::SequenceSet
+ string: 2,10:15,21
+ tuples:
+ - - 2
+ - 2
+ - - 10
+ - 15
+ - - 21
+ - 21
+ raw_data: "* ESEARCH (TAG \"C286\") MIN 2 ALL 2,10:15,21\r\n"
+
+ rfc9051_7.1_ESEARCH_example_1:
+ :response: "* ESEARCH (TAG \"h\") ALL 1:3,5,8,13,21,42\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: h
+ uid: false
+ data:
+ - - ALL
+ - !ruby/object:Net::IMAP::SequenceSet
+ string: 1:3,5,8,13,21,42
+ tuples:
+ - - 1
+ - 3
+ - - 5
+ - 5
+ - - 8
+ - 8
+ - - 13
+ - 13
+ - - 21
+ - 21
+ - - 42
+ - 42
+ raw_data: "* ESEARCH (TAG \"h\") ALL 1:3,5,8,13,21,42\r\n"
+
+ rfc9051_7.3.4_ESEARCH_example_1:
+ :response: "* ESEARCH UID COUNT 17 ALL 4:18,21,28\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag:
+ uid: true
+ data:
+ - - COUNT
+ - 17
+ - - ALL
+ - !ruby/object:Net::IMAP::SequenceSet
+ string: 4:18,21,28
+ tuples:
+ - - 4
+ - 18
+ - - 21
+ - 21
+ - - 28
+ - 28
+ raw_data: "* ESEARCH UID COUNT 17 ALL 4:18,21,28\r\n"
+
+ rfc9051_7.3.4_ESEARCH_example_2:
+ :response: "* ESEARCH (TAG \"a567\") UID COUNT 17 ALL 4:18,21,28\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: a567
+ uid: true
+ data:
+ - - COUNT
+ - 17
+ - - ALL
+ - !ruby/object:Net::IMAP::SequenceSet
+ string: 4:18,21,28
+ tuples:
+ - - 4
+ - 18
+ - - 21
+ - 21
+ - - 28
+ - 28
+ raw_data: "* ESEARCH (TAG \"a567\") UID COUNT 17 ALL 4:18,21,28\r\n"
+
+ rfc9051_7.3.4_ESEARCH_example_3:
+ :response: "* ESEARCH COUNT 18 ALL 1:17,21\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag:
+ uid: false
+ data:
+ - - COUNT
+ - 18
+ - - ALL
+ - !ruby/object:Net::IMAP::SequenceSet
+ string: 1:17,21
+ tuples:
+ - - 1
+ - 17
+ - - 21
+ - 21
+ raw_data: "* ESEARCH COUNT 18 ALL 1:17,21\r\n"
diff --git a/test/net/imap/fixtures/response_parser/rfc5267_context_updates.yml b/test/net/imap/fixtures/response_parser/rfc5267_context_updates.yml
new file mode 100644
index 00000000..152fd754
--- /dev/null
+++ b/test/net/imap/fixtures/response_parser/rfc5267_context_updates.yml
@@ -0,0 +1,114 @@
+---
+:tests:
+
+ "RFC5267 4.3.3. ADDTO Return Data Item example 1":
+ :response: "* ESEARCH (TAG \"B01\") UID ADDTO (0 32768:32769)\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: B01
+ uid: true
+ data:
+ - - ADDTO
+ - - !ruby/object:Net::IMAP::ESearchResult::AddToContext
+ position: 0
+ set: !ruby/object:Net::IMAP::SequenceSet
+ string: 32768:32769
+ tuples:
+ - - 32768
+ - 32769
+ raw_data: "* ESEARCH (TAG \"B01\") UID ADDTO (0 32768:32769)\r\n"
+
+ "RFC5267 4.3.3. ADDTO Return Data Item example 2":
+ :response: "* ESEARCH (TAG \"C01\") UID ADDTO (1 2733 1 2732 1 2731)\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: C01
+ uid: true
+ data:
+ - - ADDTO
+ - - !ruby/object:Net::IMAP::ESearchResult::AddToContext
+ position: 1
+ set: !ruby/object:Net::IMAP::SequenceSet
+ string: '2733'
+ tuples:
+ - - 2733
+ - 2733
+ - !ruby/object:Net::IMAP::ESearchResult::AddToContext
+ position: 1
+ set: !ruby/object:Net::IMAP::SequenceSet
+ string: '2732'
+ tuples:
+ - - 2732
+ - 2732
+ - !ruby/object:Net::IMAP::ESearchResult::AddToContext
+ position: 1
+ set: !ruby/object:Net::IMAP::SequenceSet
+ string: '2731'
+ tuples:
+ - - 2731
+ - 2731
+ raw_data: "* ESEARCH (TAG \"C01\") UID ADDTO (1 2733 1 2732 1 2731)\r\n"
+
+ "RFC5267 4.3.3. ADDTO Return Data Item example 3":
+ :response: "* ESEARCH (TAG \"C01\") UID ADDTO (1 2733) ADDTO (1 2731:2732)\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: C01
+ uid: true
+ data:
+ - - ADDTO
+ - - !ruby/object:Net::IMAP::ESearchResult::AddToContext
+ position: 1
+ set: !ruby/object:Net::IMAP::SequenceSet
+ string: '2733'
+ tuples:
+ - - 2733
+ - 2733
+ - - ADDTO
+ - - !ruby/object:Net::IMAP::ESearchResult::AddToContext
+ position: 1
+ set: !ruby/object:Net::IMAP::SequenceSet
+ string: 2731:2732
+ tuples:
+ - - 2731
+ - 2732
+ raw_data: "* ESEARCH (TAG \"C01\") UID ADDTO (1 2733) ADDTO (1 2731:2732)\r\n"
+
+ "RFC5267 4.3.3. ADDTO Return Data Item example 4":
+ :response: "* ESEARCH (TAG \"C01\") UID ADDTO (1 2731:2733)\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: C01
+ uid: true
+ data:
+ - - ADDTO
+ - - !ruby/object:Net::IMAP::ESearchResult::AddToContext
+ position: 1
+ set: !ruby/object:Net::IMAP::SequenceSet
+ string: 2731:2733
+ tuples:
+ - - 2731
+ - 2733
+ raw_data: "* ESEARCH (TAG \"C01\") UID ADDTO (1 2731:2733)\r\n"
+
+ "RFC5267 4.3.4. REMOVEFROM Return Data Item":
+ :response: "* ESEARCH (TAG \"B01\") UID REMOVEFROM (0 32768)\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: B01
+ uid: true
+ data:
+ - - REMOVEFROM
+ - - !ruby/object:Net::IMAP::ESearchResult::RemoveFromContext
+ position: 0
+ set: !ruby/object:Net::IMAP::SequenceSet
+ string: '32768'
+ tuples:
+ - - 32768
+ - 32768
+ raw_data: "* ESEARCH (TAG \"B01\") UID REMOVEFROM (0 32768)\r\n"
diff --git a/test/net/imap/fixtures/response_parser/rfc6203_fuzzy_search.yml b/test/net/imap/fixtures/response_parser/rfc6203_fuzzy_search.yml
new file mode 100644
index 00000000..fe3960b7
--- /dev/null
+++ b/test/net/imap/fixtures/response_parser/rfc6203_fuzzy_search.yml
@@ -0,0 +1,54 @@
+---
+:tests:
+
+ "RFC6203 SEARCH=FUZZY 4. Relevancy scores for search results":
+ :response: "* ESEARCH (TAG \"B1\") ALL 1,5,10 RELEVANCY (4 99 42)\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: B1
+ uid: false
+ data:
+ - - ALL
+ - !ruby/object:Net::IMAP::SequenceSet
+ string: '1,5,10'
+ tuples:
+ - - 1
+ - 1
+ - - 5
+ - 5
+ - - 10
+ - 10
+ - - RELEVANCY
+ - - 4
+ - 99
+ - 42
+ raw_data: "* ESEARCH (TAG \"B1\") ALL 1,5,10 RELEVANCY (4 99 42)\r\n"
+
+ "RFC6203 SEARCH=FUZZY 6. Relevency score with ESORT results":
+ comment: |
+ This doesn't require anything new for the parser beyond the prior example.
+ But it important to note that the SequenceSet has *not* been normalized
+ (sorted and coalesced).
+ :response: "* ESEARCH (TAG \"C2\") ALL 5,10,1 RELEVANCY (99 42 4)\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: C2
+ uid: false
+ data:
+ - - ALL
+ - !ruby/object:Net::IMAP::SequenceSet
+ string: '5,10,1'
+ tuples:
+ - - 1
+ - 1
+ - - 5
+ - 5
+ - - 10
+ - 10
+ - - RELEVANCY
+ - - 99
+ - 42
+ - 4
+ raw_data: "* ESEARCH (TAG \"C2\") ALL 5,10,1 RELEVANCY (99 42 4)\r\n"
diff --git a/test/net/imap/fixtures/response_parser/rfc7162_condstore_qresync_responses.yml b/test/net/imap/fixtures/response_parser/rfc7162_condstore_qresync_responses.yml
index dc8fadcf..b571c990 100644
--- a/test/net/imap/fixtures/response_parser/rfc7162_condstore_qresync_responses.yml
+++ b/test/net/imap/fixtures/response_parser/rfc7162_condstore_qresync_responses.yml
@@ -98,6 +98,46 @@
raw_data: "* STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292 HIGHESTMODSEQ
7011231777)\r\n"
+ "RFC7162 CONDSTORE 3.1.10. Example 19 (Interaction with ESEARCH)":
+ :response: "* ESEARCH (TAG \"a\") ALL 1:3,5 MODSEQ 1236\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: a
+ uid: false
+ data:
+ - - ALL
+ - !ruby/object:Net::IMAP::SequenceSet
+ string: 1:3,5
+ tuples:
+ - - 1
+ - 3
+ - - 5
+ - 5
+ - - MODSEQ
+ - 1236
+ raw_data: "* ESEARCH (TAG \"a\") ALL 1:3,5 MODSEQ 1236\r\n"
+
+ "RFC7162 CONDSTORE 3.1.10. Example 20 (Interaction with ESEARCH)":
+ :response: "* ESEARCH (TAG \"a\") ALL 5,3,2,1 MODSEQ 1236\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: a
+ uid: false
+ data:
+ - - ALL
+ - !ruby/object:Net::IMAP::SequenceSet
+ string: '5,3,2,1'
+ tuples:
+ - - 1
+ - 3
+ - - 5
+ - 5
+ - - MODSEQ
+ - 1236
+ raw_data: "* ESEARCH (TAG \"a\") ALL 5,3,2,1 MODSEQ 1236\r\n"
+
"RFC7162 QRESYNC 3.2.5.1. Modification Sequence and UID Parameters":
:response: "* VANISHED (EARLIER) 41,43:116,118,120:211,214:540\r\n"
:expected: !ruby/struct:Net::IMAP::UntaggedResponse
diff --git a/test/net/imap/fixtures/response_parser/rfc9394_partial.yml b/test/net/imap/fixtures/response_parser/rfc9394_partial.yml
new file mode 100644
index 00000000..233a9096
--- /dev/null
+++ b/test/net/imap/fixtures/response_parser/rfc9394_partial.yml
@@ -0,0 +1,66 @@
+---
+:tests:
+
+ "RFC9394 PARTIAL 3.1. example 1":
+ comment: |
+ Neither RFC9394 nor RFC5267 contain any examples of a normal unelided
+ sequence-set result. I've edited it to include a sequence-set here.
+ :response: "* ESEARCH (TAG \"A01\") UID PARTIAL (-1:-100 200:250,252:300)\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: A01
+ uid: true
+ data:
+ - - PARTIAL
+ - !ruby/object:Net::IMAP::ESearchResult::PartialResult
+ range: !ruby/range
+ begin: -100
+ end: -1
+ excl: false
+ results: !ruby/object:Net::IMAP::SequenceSet
+ string: 200:250,252:300
+ tuples:
+ - - 200
+ - 250
+ - - 252
+ - 300
+ raw_data: "* ESEARCH (TAG \"A01\") UID PARTIAL (-1:-100 200:250,252:300)\r\n"
+
+ "RFC9394 PARTIAL 3.1. example 2":
+ :response: "* ESEARCH (TAG \"A02\") UID PARTIAL (23500:24000 55500:56000)\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: A02
+ uid: true
+ data:
+ - - PARTIAL
+ - !ruby/object:Net::IMAP::ESearchResult::PartialResult
+ range: !ruby/range
+ begin: 23500
+ end: 24000
+ excl: false
+ results: !ruby/object:Net::IMAP::SequenceSet
+ string: 55500:56000
+ tuples:
+ - - 55500
+ - 56000
+ raw_data: "* ESEARCH (TAG \"A02\") UID PARTIAL (23500:24000 55500:56000)\r\n"
+
+ "RFC9394 PARTIAL 3.1. example 3":
+ :response: "* ESEARCH (TAG \"A04\") UID PARTIAL (24000:24500 NIL)\r\n"
+ :expected: !ruby/struct:Net::IMAP::UntaggedResponse
+ name: ESEARCH
+ data: !ruby/object:Net::IMAP::ESearchResult
+ tag: A04
+ uid: true
+ data:
+ - - PARTIAL
+ - !ruby/object:Net::IMAP::ESearchResult::PartialResult
+ range: !ruby/range
+ begin: 24000
+ end: 24500
+ excl: false
+ results:
+ raw_data: "* ESEARCH (TAG \"A04\") UID PARTIAL (24000:24500 NIL)\r\n"
diff --git a/test/net/imap/test_esearch_result.rb b/test/net/imap/test_esearch_result.rb
new file mode 100644
index 00000000..c40d763d
--- /dev/null
+++ b/test/net/imap/test_esearch_result.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+require "net/imap"
+require "test/unit"
+
+class ESearchResultTest < Test::Unit::TestCase
+ ESearchResult = Net::IMAP::ESearchResult
+ SequenceSet = Net::IMAP::SequenceSet
+
+ test "#to_a" do
+ esearch = ESearchResult.new(nil, true, [])
+ assert_equal [], esearch.to_a
+ esearch = ESearchResult.new(nil, false, [])
+ assert_equal [], esearch.to_a
+ esearch = ESearchResult.new(nil, false, [["ALL", SequenceSet["1,5:8"]]])
+ assert_equal [1, 5, 6, 7, 8], esearch.to_a
+ end
+
+ test "#tag" do
+ esearch = ESearchResult.new("A0001", false, [["count", 0]])
+ assert_equal "A0001", esearch.tag
+ esearch = ESearchResult.new("A0002", false, [["count", 0]])
+ assert_equal "A0002", esearch.tag
+ end
+
+ test "#uid" do
+ esearch = ESearchResult.new("A0003", true, [["count", 0]])
+ assert_equal true, esearch.uid
+ assert_equal true, esearch.uid?
+ esearch = ESearchResult.new("A0004", false, [["count", 0]])
+ assert_equal false, esearch.uid
+ assert_equal false, esearch.uid?
+ end
+
+ # "simple" result da¨a return exactly what is in the data.assoc
+ test "simple RFC4731 and RFC9051 return data accessors" do
+ seqset = SequenceSet["5:9,101:105,151:152"]
+ esearch = ESearchResult.new(
+ "A0005",
+ true,
+ [
+ ["MIN", 5],
+ ["MAX", 152],
+ ["COUNT", 12],
+ ["ALL", seqset],
+ ["MODSEQ", 12345],
+ ]
+ )
+ assert_equal 5, esearch.min
+ assert_equal 152, esearch.max
+ assert_equal 12, esearch.count
+ assert_equal seqset, esearch.all
+ assert_equal 12345, esearch.modseq
+ end
+
+ test "#partial returns PARTIAL value (RFC9394: PARTIAL)" do
+ result = Net::IMAP::ResponseParser.new.parse(
+ "* ESEARCH (TAG \"A0006\") UID PARTIAL (-1:-100 200:250,252:300)\r\n"
+ ).data
+ assert_equal(ESearchResult, result.class)
+ assert_equal(
+ ESearchResult::PartialResult.new(
+ -100..-1, SequenceSet[200..250, 252..300]
+ ),
+ result.partial
+ )
+ end
+
+ test "#relevancy returns RELEVANCY value (RFC6203: SEARCH=FUZZY)" do
+ esearch = ESearchResult.new("A0007", true, [["RELEVANCY", 1]])
+ assert_equal 1, esearch.relevancy
+ esearch = ESearchResult.new("A0008", true, [["RELEVANCY", 99]])
+ assert_equal 99, esearch.relevancy
+ end
+
+ test "#addto returns ADDTO values (RFC5267: CONTEXT=SEARCH, CONTEXT=SORT)" do
+ parser = Net::IMAP::ResponseParser.new
+ expected = [
+ ESearchResult::AddToContext.new(1, SequenceSet[2733]),
+ ESearchResult::AddToContext.new(1, SequenceSet[2732]),
+ ESearchResult::AddToContext.new(1, SequenceSet[2731]),
+ ]
+ assert_equal expected, parser.parse(
+ "* ESEARCH (TAG \"C01\") UID ADDTO (1 2733 1 2732 1 2731)\r\n"
+ ).data.addto
+ assert_equal expected, parser.parse(
+ "* ESEARCH (TAG \"C01\") UID ADDTO (1 2733) ADDTO (1 2732) ADDTO (1 2731)\r\n"
+ ).data.addto
+ end
+
+ test "#removefrom returns REMOVEFROM values (RFC5267: CONTEXT=SEARCH...)" do
+ parser = Net::IMAP::ResponseParser.new
+ expected = [
+ ESearchResult::RemoveFromContext.new(1, SequenceSet[2733]),
+ ESearchResult::RemoveFromContext.new(1, SequenceSet[2732]),
+ ESearchResult::RemoveFromContext.new(1, SequenceSet[2731]),
+ ]
+ assert_equal expected, parser.parse(
+ "* ESEARCH (TAG \"C01\") UID REMOVEFROM (1 2733 1 2732 1 2731)\r\n"
+ ).data.removefrom
+ assert_equal expected, parser.parse(
+ "* ESEARCH (TAG \"C01\") UID REMOVEFROM (1 2733) REMOVEFROM (1 2732) REMOVEFROM (1 2731)\r\n"
+ ).data.removefrom
+ end
+
+ test "#updates returns both ADDTO and REMOVEFROM values (RFC5267: CONTEXT)" do
+ parser = Net::IMAP::ResponseParser.new
+ expected = [
+ ESearchResult::AddToContext.new(1, SequenceSet[2733]),
+ ESearchResult::RemoveFromContext.new(1, SequenceSet[2732]),
+ ESearchResult::AddToContext.new(1, SequenceSet[2731]),
+ ]
+ assert_equal expected, parser.parse(
+ "* ESEARCH (TAG \"C01\") UID ADDTO (1 2733) REMOVEFROM (1 2732) ADDTO (1 2731)\r\n"
+ ).data.updates
+ end
+
+end
diff --git a/test/net/imap/test_imap_response_parser.rb b/test/net/imap/test_imap_response_parser.rb
index a4d70f92..365ff7e3 100644
--- a/test/net/imap/test_imap_response_parser.rb
+++ b/test/net/imap/test_imap_response_parser.rb
@@ -55,6 +55,9 @@ def teardown
# §7.3.3: STATUS response
generate_tests_from fixture_file: "status_responses.yml"
+ # §7.3.4: ESEARCH response
+ generate_tests_from fixture_file: "esearch_responses.yml"
+
# RFC3501 §7.2.5: SEARCH response (obsolete in IMAP4rev2):
generate_tests_from fixture_file: "search_responses.yml"
@@ -91,7 +94,13 @@ def teardown
# RFC 5256: THREAD response
generate_tests_from fixture_file: "thread_responses.yml"
- # RFC 7164: CONDSTORE and QRESYNC responses
+ # RFC 5267: ADDTO and REMOVEFROM search return options
+ generate_tests_from fixture_file: "rfc5267_context_updates.yml"
+
+ # RFC6203: SEARCH=FUZZY extension (RELEVANCY search return option)
+ generate_tests_from fixture_file: "rfc6203_fuzzy_search.yml"
+
+ # RFC 7162: CONDSTORE and QRESYNC responses
generate_tests_from fixture_file: "rfc7162_condstore_qresync_responses.yml"
# RFC 8474: OBJECTID responses
@@ -100,6 +109,9 @@ def teardown
# RFC 9208: QUOTA extension
generate_tests_from fixture_file: "rfc9208_quota_responses.yml"
+ # RFC 9394: PARTIAL extension
+ generate_tests_from fixture_file: "rfc9394_partial.yml"
+
############################################################################
# Workarounds or unspecified extensions:
generate_tests_from fixture_file: "quirky_behaviors.yml"