diff --git a/lib/net/imap.rb b/lib/net/imap.rb
index b9e39762..505f5ef0 100644
--- a/lib/net/imap.rb
+++ b/lib/net/imap.rb
@@ -1944,13 +1944,19 @@ def uid_expunge(uid_set)
#
# * When +criteria+ is an array, each member is a +SEARCH+ command argument:
# * Any SequenceSet sends SequenceSet#valid_string.
- # +Range+, -1, and nested +Array+ elements are converted to
- # SequenceSet.
- # * Any +String+ is sent verbatim when it is a valid \IMAP atom,
+ # These types are converted to SequenceSet for validation and encoding:
+ # * +Set+
+ # * +Range+
+ # * -1 and +:*+ -- both translate to *
+ # * responds to +#to_sequence_set+
+ # * +String+, when formatted as a \IMAP sequence-set
+ # * deeply nested +Array+, when all members are one of these types.
+ # * Any other +String+ is sent verbatim when it is a valid \IMAP atom,
# and encoded as an \IMAP quoted or literal string otherwise.
+ # * Any other nested +Array+ is encoded as a parenthesized list, to group
+ # multiple search keys (e.g., for use with +OR+ and +NOT+).
# * Any other +Integer+ (besides -1) will be sent as +#to_s+.
# * +Date+ objects will be encoded as an \IMAP date (see ::encode_date).
- #
# * When +criteria+ is a string, it will be sent directly to the server
# without any validation or encoding. *WARNING:* This is
# vulnerable to injection attacks when external inputs are used.
@@ -1972,13 +1978,13 @@ def uid_expunge(uid_set)
# The following searches send the exact same command to the server:
#
# # criteria array, charset arg
- # imap.search(%w[OR UNSEEN FLAGGED SUBJECT foo], "UTF-8")
+ # imap.search(["OR", "UNSEEN", %w(FLAGGED SUBJECT foo)], "UTF-8")
# # criteria string, charset arg
- # imap.search("OR UNSEEN FLAGGED SUBJECT foo", "UTF-8")
+ # imap.search("OR UNSEEN (FLAGGED SUBJECT foo)", "UTF-8")
# # criteria array contains charset arg
- # imap.search(%w[CHARSET UTF-8 OR UNSEEN FLAGGED SUBJECT foo])
+ # imap.search([*%w[CHARSET UTF-8], "OR", "UNSEEN", %w(FLAGGED SUBJECT foo)])
# # criteria string contains charset arg
- # imap.search("CHARSET UTF-8 OR UNSEEN FLAGGED SUBJECT foo")
+ # imap.search("CHARSET UTF-8 OR UNSEEN (FLAGGED SUBJECT foo)")
#
# ===== Search keys
#
@@ -3191,14 +3197,7 @@ def thread_internal(cmd, algorithm, search_keys, charset)
def normalize_searching_criteria(criteria)
return RawData.new(criteria) if criteria.is_a?(String)
- criteria.map do |i|
- case i
- when -1, Range, Array
- SequenceSet.new(i)
- else
- i
- end
- end
+ criteria.map {|i| SequenceSet::Coercible[i] ? SequenceSet[i] : i }
end
def build_ssl_ctx(ssl)
diff --git a/lib/net/imap/sequence_set.rb b/lib/net/imap/sequence_set.rb
index f5da7b24..81fc59b4 100644
--- a/lib/net/imap/sequence_set.rb
+++ b/lib/net/imap/sequence_set.rb
@@ -276,16 +276,29 @@ class SequenceSet
# The largest possible non-zero unsigned 32-bit integer
UINT32_MAX = 2**32 - 1
+ REGEXP = ResponseParser::Patterns::SEQUENCE_SET_STR
+ private_constant :REGEXP
+
# represents "*" internally, to simplify sorting (etc)
STAR_INT = UINT32_MAX + 1
private_constant :STAR_INT
# valid inputs for "*"
STARS = [:*, ?*, -1].freeze
- private_constant :STAR_INT, :STARS
+ private_constant :STARS
- COERCIBLE = ->{ _1.respond_to? :to_sequence_set }
- private_constant :COERCIBLE
+ # Matches objects which should be implicitly converted into SequenceSet
+ # objects. Note that the inputs are not validated, and some valid inputs
+ # (Enumerable other than Array or Set) will be rejected.
+ Coercible = ->(obj) do
+ case obj
+ when SequenceSet then true
+ when Integer, Range, *STARS then true
+ when String then REGEXP.match?(obj.b)
+ when Array, Set then obj.all?(Coercible) && !obj.empty?
+ else obj.respond_to?(:to_sequence_set)
+ end
+ end
class << self
diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb
index 6c30323b..6670a5a5 100644
--- a/test/net/imap/test_imap.rb
+++ b/test/net/imap/test_imap.rb
@@ -1206,16 +1206,31 @@ def test_unselect
end
server.on "SEARCH", &search_resp
- assert_equal search_result, imap.search(["subject", "hello",
- [1..5, 8, 10..-1]])
+ server.on "UID SEARCH", &search_resp
+
+ assert_equal search_result, imap.search(
+ ["subject", "hello", [1..5, 8, 10..-1]]
+ )
cmd = server.commands.pop
assert_equal ["SEARCH", "subject hello 1:5,8,10:*"], [cmd.name, cmd.args]
- server.on "UID SEARCH", &search_resp
- assert_equal search_result, imap.uid_search(["subject", "hello",
- [1..22, 30..-1]])
+ assert_equal search_result, imap.uid_search(
+ ["subject", "hello", [1..22, 30..-1]]
+ )
cmd = server.commands.pop
assert_equal ["UID SEARCH", "subject hello 1:22,30:*"], [cmd.name, cmd.args]
+
+ assert_equal search_result, imap.search(
+ "RETURN (COUNT) NOT (FLAGGED (OR SEEN ANSWERED))"
+ )
+ cmd = server.commands.pop
+ assert_equal "RETURN (COUNT) NOT (FLAGGED (OR SEEN ANSWERED))", cmd.args
+
+ assert_equal search_result, imap.search([
+ "RETURN", %w(MIN MAX COUNT), "NOT", ["FLAGGED", %w(OR SEEN ANSWERED)]
+ ])
+ cmd = server.commands.pop
+ assert_equal "RETURN (MIN MAX COUNT) NOT (FLAGGED (OR SEEN ANSWERED))", cmd.args
end
end