Skip to content

Commit d8a2e6c

Browse files
committed
✨ Add keyword param for search return options
This also extracts the `return` kwarg out of the `criteria` array, so it can be processed differently.
1 parent 331db7f commit d8a2e6c

File tree

2 files changed

+117
-24
lines changed

2 files changed

+117
-24
lines changed

lib/net/imap.rb

Lines changed: 92 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1935,7 +1935,7 @@ def uid_expunge(uid_set)
19351935

19361936
# :call-seq:
19371937
# search(criteria, charset = nil) -> result
1938-
# search(criteria, charset: nil) -> result
1938+
# search(criteria, charset: nil, return: nil) -> result
19391939
#
19401940
# Sends a {SEARCH command [IMAP4rev1 §6.4.4]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.4]
19411941
# to search the mailbox for messages that match the given search +criteria+,
@@ -1950,44 +1950,86 @@ def uid_expunge(uid_set)
19501950
# See {"Argument translation"}[rdoc-ref:#search@Argument+translation]
19511951
# and {"Search criteria"}[rdoc-ref:#search@Search+criteria], below.
19521952
#
1953+
# +return+ options control what kind of information is returned about
1954+
# messages matching the search +criteria+. Specifying +return+ should force
1955+
# the server to return an ESearchResult instead of a SearchResult, but some
1956+
# servers disobey this requirement. <em>Requires an extended search
1957+
# capability, such as +ESEARCH+ or +IMAP4rev2+.</em>
1958+
# See {"Argument translation"}[rdoc-ref:#search@Argument+translation]
1959+
# and {"Return options"}[rdoc-ref:#search@Return+options], below.
1960+
#
19531961
# +charset+ is the name of the {registered character
19541962
# set}[https://www.iana.org/assignments/character-sets/character-sets.xhtml]
19551963
# used by strings in the search +criteria+. When +charset+ isn't specified,
19561964
# either <tt>"US-ASCII"</tt> or <tt>"UTF-8"</tt> is assumed, depending on
19571965
# the server's capabilities.
19581966
#
19591967
# _NOTE:_ Return options and charset may be sent as part of +criteria+. Do
1960-
# not use the +charset+ argument when either return options or charset are
1961-
# embedded in +criteria+.
1968+
# not use the +return+ or +charset+ arguments when either return options or
1969+
# charset are embedded in +criteria+.
19621970
#
19631971
# Related: #uid_search
19641972
#
19651973
# ==== For example:
19661974
#
1967-
# p imap.search(["SUBJECT", "hello", "NOT", "SEEN"])
1975+
# imap.search(["SUBJECT", "hello", "NOT", "SEEN"])
19681976
# #=> [1, 6, 7, 8]
19691977
#
1970-
# The following searches send the exact same command to the server:
1971-
#
1972-
# # criteria array, charset arg
1973-
# imap.search(["OR", "UNSEEN", %w(FLAGGED SUBJECT foo)], "UTF-8")
1974-
# # criteria string, charset arg
1975-
# imap.search("OR UNSEEN (FLAGGED SUBJECT foo)", "UTF-8")
1976-
# # criteria array contains charset arg
1977-
# imap.search([*%w[CHARSET UTF-8], "OR", "UNSEEN", %w(FLAGGED SUBJECT foo)])
1978-
# # criteria string contains charset arg
1979-
# imap.search("CHARSET UTF-8 OR UNSEEN (FLAGGED SUBJECT foo)")
1980-
#
1981-
# Sending return options and charset embedded in the +criteria+ arg:
1982-
# imap.search("RETURN (MIN MAX) CHARSET UTF-8 (OR UNSEEN FLAGGED)")
1983-
# imap.search(["RETURN", %w(MIN MAX),
1984-
# "CHARSET", "UTF-8",
1985-
# %w(OR UNSEEN FLAGGED)])
1978+
# The following assumes the server supports +ESEARCH+ and +CONDSTORE+:
1979+
#
1980+
# result = imap.uid_search(["UID", 12345.., "MODSEQ", 620_162_338],
1981+
# return: %w(all count min max))
1982+
# # => #<data Net::IMAP::ESearchResult tag="RUBY0123", uid=true,
1983+
# # data=[["ALL", Net::IMAP::SequenceSet["12346:12349,22222:22230"]],
1984+
# # ["COUNT", 13], ["MIN", 12346], ["MAX", 22230],
1985+
# # ["MODSEQ", 917162488]]>
1986+
# result.to_a # => [12346, 12347, 12348, 12349, 22222, 22223, 22224,
1987+
# # 22225, 22226, 22227, 22228, 22229, 22230]
1988+
# result.uid? # => true
1989+
# result.count # => 13
1990+
# result.min # => 12346
1991+
# result.max # => 22230
1992+
# result.modseq # => 917162488
1993+
#
1994+
# Using +return+ options to limit the result to only min, max, and count:
1995+
#
1996+
# result = imap.uid_search(["UID", 12345..,], return: %w(count min max))
1997+
# # => #<data Net::IMAP::ESearchResult tag="RUBY0124", uid=true,
1998+
# # data=[["COUNT", 13], ["MIN", 12346], ["MAX", 22230]]>
1999+
# result.to_a # => []
2000+
# result.count # => 13
2001+
# result.min # => 12346
2002+
# result.max # => 22230
2003+
#
2004+
# Return options and charset may be sent as keyword args or embedded in the
2005+
# +criteria+ arg, but they must be in the correct order: <tt>"RETURN (...)
2006+
# CHARSET ... criteria..."</tt>. The following searches
2007+
# send the exact same command to the server:
2008+
#
2009+
# # Return options and charset as keyword arguments (preferred)
2010+
# imap.search(%w(OR UNSEEN FLAGGED), return: %w(MIN MAX), charset: "UTF-8")
2011+
# # Embedding return and charset in the criteria array
2012+
# imap.search(["RETURN", %w(MIN MAX), "CHARSET", "UTF-8", *%w(OR UNSEEN FLAGGED)])
2013+
# # Embedding return and charset in the criteria string
2014+
# imap.search("RETURN (MIN MAX) CHARSET UTF-8 OR UNSEEN FLAGGED")
2015+
#
2016+
# Sending charset as the second positional argument is supported for
2017+
# backward compatibility. Future versions may print a deprecation warning:
2018+
# imap.search(%w(OR UNSEEN FLAGGED), "UTF-8", return: %w(MIN MAX))
19862019
#
19872020
# ==== Argument translation
19882021
#
2022+
# [+return+ options]
2023+
# Must be an Array. Return option names are strings.
2024+
# Unlike +criteria+, other return option arguments are not automatically
2025+
# converted to SequenceSet.
2026+
#
19892027
# [When +criteria+ is an Array]
1990-
# Each member is a +SEARCH+ command argument:
2028+
# When the array begins with <tt>"RETURN"</tt> (case insensitive), the
2029+
# second array element is translated like the +return+ parameter (as
2030+
# described above).
2031+
#
2032+
# Every other member is a +SEARCH+ command argument:
19912033
# [SequenceSet]
19922034
# Encoded as an \IMAP +sequence-set+ with SequenceSet#valid_string.
19932035
# [Set, Range, <tt>-1</tt>, +:*+, responds to +#to_sequence_set+]
@@ -2276,7 +2318,7 @@ def search(...)
22762318

22772319
# :call-seq:
22782320
# uid_search(criteria, charset = nil) -> result
2279-
# uid_search(criteria, charset: nil) -> result
2321+
# uid_search(criteria, charset: nil, return: nil) -> result
22802322
#
22812323
# Sends a {UID SEARCH command [IMAP4rev1 §6.4.8]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.8]
22822324
# to search the mailbox for messages that match the given searching
@@ -3217,8 +3259,26 @@ def enforce_logindisabled?
32173259
end
32183260
end
32193261

3220-
def search_args(keys, charset_arg = nil, charset: nil)
3221-
esearch = (keys in /\ARETURN\b/i | Array[/\ARETURN\z/i, *])
3262+
RETURN_WHOLE = /\ARETURN\z/i
3263+
RETURN_START = /\ARETURN\b/i
3264+
private_constant :RETURN_WHOLE, :RETURN_START
3265+
3266+
def search_args(keys, charset_arg = nil, return: nil, charset: nil)
3267+
{return:} => {return: return_kw}
3268+
case [return_kw, keys]
3269+
in [nil, Array[RETURN_WHOLE, return_opts, *keys]]
3270+
return_opts = convert_return_opts(return_opts)
3271+
esearch = true
3272+
in [nil => return_opts, RETURN_START]
3273+
esearch = true
3274+
in [nil => return_opts, keys]
3275+
esearch = false
3276+
in [_, Array[RETURN_WHOLE, _, *] | RETURN_START]
3277+
raise ArgumentError, "conflicting return options"
3278+
in [return_opts, keys]
3279+
return_opts = convert_return_opts(return_opts)
3280+
esearch = true
3281+
end
32223282
if charset && charset_arg
32233283
raise ArgumentError, "multiple charset arguments"
32243284
end
@@ -3229,9 +3289,17 @@ def search_args(keys, charset_arg = nil, charset: nil)
32293289
end
32303290
args = normalize_searching_criteria(keys)
32313291
args.prepend("CHARSET", charset) if charset
3292+
args.prepend("RETURN", return_opts) if return_opts
32323293
return args, esearch
32333294
end
32343295

3296+
def convert_return_opts(unconverted)
3297+
Array.try_convert(unconverted) or
3298+
raise TypeError, "expected return options to be Array, got %s" % [
3299+
unconverted.class
3300+
]
3301+
end
3302+
32353303
def search_internal(cmd, ...)
32363304
args, esearch = search_args(...)
32373305
synchronize do

test/net/imap/test_imap.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,6 +1265,18 @@ def seqset_coercible.to_sequence_set
12651265
])
12661266
cmd = server.commands.pop
12671267
assert_equal "RETURN (MIN MAX COUNT) NOT (FLAGGED (OR SEEN ANSWERED))", cmd.args
1268+
1269+
assert_equal search_result, imap.search(
1270+
["NOT", ["FLAGGED", %w(OR SEEN ANSWERED)]], return: %w(MIN MAX COUNT)
1271+
)
1272+
cmd = server.commands.pop
1273+
assert_equal "RETURN (MIN MAX COUNT) NOT (FLAGGED (OR SEEN ANSWERED))", cmd.args
1274+
1275+
assert_equal search_result, imap.search(
1276+
["UID", 1234..], return: %w(PARTIAL -500:-1)
1277+
)
1278+
cmd = server.commands.pop
1279+
assert_equal "RETURN (PARTIAL -500:-1) UID 1234:*", cmd.args
12681280
end
12691281
end
12701282

@@ -1292,6 +1304,19 @@ def seqset_coercible.to_sequence_set
12921304
# assert_raise(ArgumentError) do
12931305
# imap.search("return () charset foo ALL", "bar")
12941306
# end
1307+
1308+
assert_raise(ArgumentError) do
1309+
imap.search(["retURN", %w(foo bar), "ALL"], return: %w[foo bar])
1310+
end
1311+
assert_raise(ArgumentError) do
1312+
imap.search("RETURN (foo bar) ALL", return: %w[foo bar])
1313+
end
1314+
assert_raise(TypeError) do
1315+
imap.search("ALL", return: "foo bar")
1316+
end
1317+
assert_raise(TypeError) do
1318+
imap.search(["retURN", "foo bar", "ALL"])
1319+
end
12951320
end
12961321
end
12971322

0 commit comments

Comments
 (0)