Skip to content

Commit d38357b

Browse files
committed
✨ Gather ESEARCH response to #search/#uid_search
If the server returns both `ESEARCH` and `SEARCH`, both are cleared from the responses hash, but only the `ESEARCH` is returned. When the server doesn't send any search responses: If return options are passed, return an empty ESearchResult. It will have the appropriate `tag` and `uid` values, but no `data`. Otherwise return an empty `SearchResult`.
1 parent 8c432dd commit d38357b

File tree

2 files changed

+89
-7
lines changed

2 files changed

+89
-7
lines changed

lib/net/imap.rb

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1934,9 +1934,11 @@ def uid_expunge(uid_set)
19341934
#
19351935
# Sends a {SEARCH command [IMAP4rev1 §6.4.4]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.4]
19361936
# to search the mailbox for messages that match the given search +criteria+,
1937-
# and returns a SearchResult. SearchResult inherits from Array (for
1938-
# backward compatibility) but adds SearchResult#modseq when the +CONDSTORE+
1939-
# capability has been enabled.
1937+
# and returns either a SearchResult or an ESearchResult. SearchResult
1938+
# inherits from Array (for backward compatibility) but adds
1939+
# SearchResult#modseq when the +CONDSTORE+ capability has been enabled.
1940+
# ESearchResult also implements to_a{rdoc-ref:ESearchResult#to_a}, for
1941+
# compatibility with SearchResult.
19401942
#
19411943
# +criteria+ is one or more search keys and their arguments, which may be
19421944
# provided as an array or a string.
@@ -1947,8 +1949,11 @@ def uid_expunge(uid_set)
19471949
# set}[https://www.iana.org/assignments/character-sets/character-sets.xhtml]
19481950
# used by strings in the search +criteria+. When +charset+ isn't specified,
19491951
# either <tt>"US-ASCII"</tt> or <tt>"UTF-8"</tt> is assumed, depending on
1950-
# the server's capabilities. +charset+ may be sent inside +criteria+
1951-
# instead of as a separate argument.
1952+
# the server's capabilities.
1953+
#
1954+
# _NOTE:_ Return options and +charset+ may be sent as part of +criteria+.
1955+
# Do not use the +charset+ argument when either return options or charset
1956+
# are embedded in +criteria+.
19521957
#
19531958
# Related: #uid_search
19541959
#
@@ -1968,6 +1973,12 @@ def uid_expunge(uid_set)
19681973
# # criteria string contains charset arg
19691974
# imap.search("CHARSET UTF-8 OR UNSEEN (FLAGGED SUBJECT foo)")
19701975
#
1976+
# Sending return options and charset embedded in the +criteria+ arg:
1977+
# imap.search("RETURN (MIN MAX) CHARSET UTF-8 (OR UNSEEN FLAGGED)")
1978+
# imap.search(["RETURN", %w(MIN MAX),
1979+
# "CHARSET", "UTF-8",
1980+
# %w(OR UNSEEN FLAGGED)])
1981+
#
19711982
# ==== Argument translation
19721983
#
19731984
# [When +criteria+ is an Array]
@@ -2197,6 +2208,12 @@ def uid_expunge(uid_set)
21972208
#
21982209
# ==== Capabilities
21992210
#
2211+
# Return options should only be specified when the server supports
2212+
# +IMAP4rev2+ or an extension that allows them, such as +ESEARCH+.
2213+
#
2214+
# When +IMAP4rev2+ is enabled, or when the server supports +IMAP4rev2+ but
2215+
# not +IMAP4rev1+, ESearchResult is always returned instead of SearchResult.
2216+
#
22002217
# If CONDSTORE[https://www.rfc-editor.org/rfc/rfc7162.html] is supported
22012218
# and enabled for the selected mailbox, a non-empty SearchResult will
22022219
# include a +MODSEQ+ value.
@@ -3150,14 +3167,31 @@ def enforce_logindisabled?
31503167
end
31513168
end
31523169

3170+
HasSearchReturnOpts = ->keys {
3171+
keys in RawData[/\ARETURN /] | Array[/\ARETURN\z/i, *]
3172+
}
3173+
private_constant :HasSearchReturnOpts
3174+
31533175
def search_internal(cmd, keys, charset = nil)
31543176
keys = normalize_searching_criteria(keys)
31553177
args = charset ? ["CHARSET", charset, *keys] : keys
31563178
synchronize do
3157-
send_command(cmd, *args)
3179+
tagged = send_command(cmd, *args)
3180+
tag = tagged.tag
3181+
# Only the last ESEARCH or SEARCH is used. Excess results are ignored.
3182+
esearch_result = extract_responses("ESEARCH") {|response|
3183+
response in ESearchResult(tag: ^tag)
3184+
}.last
31583185
search_result = clear_responses("SEARCH").last
3159-
if search_result
3186+
if esearch_result
3187+
# silently ignore SEARCH results, if any
3188+
esearch_result
3189+
elsif search_result
3190+
# warn EXPECTED_ESEARCH_RESULT if esearch
31603191
search_result
3192+
elsif keys in HasSearchReturnOpts # TODO: check if IMAP4rev2 enabled
3193+
# warn NO_SEARCH_RESPONSE
3194+
ESearchResult[tag:, uid: cmd.start_with?("UID ")]
31613195
else
31623196
# warn NO_SEARCH_RESPONSE
31633197
SearchResult[]

test/net/imap/test_imap.rb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1259,6 +1259,54 @@ def seqset_coercible.to_sequence_set
12591259
end
12601260
end
12611261

1262+
test("#search/#uid_search with ESEARCH or IMAP4rev2") do
1263+
with_fake_server do |server, imap|
1264+
# Example from RFC9051, 6.4.4:
1265+
# C: A282 SEARCH RETURN (MIN COUNT) FLAGGED
1266+
# SINCE 1-Feb-1994 NOT FROM "Smith"
1267+
# S: * ESEARCH (TAG "A282") MIN 2 COUNT 3
1268+
# S: A282 OK SEARCH completed
1269+
server.on "SEARCH" do |cmd|
1270+
cmd.untagged "ESEARCH", "(TAG \"unrelated1\") MIN 1 COUNT 2"
1271+
cmd.untagged "ESEARCH", "(TAG %p) MIN 2 COUNT 3" % [cmd.tag]
1272+
cmd.untagged "ESEARCH", "(TAG \"unrelated2\") MIN 222 COUNT 333"
1273+
cmd.done_ok
1274+
end
1275+
result = imap.search(
1276+
'RETURN (MIN COUNT) FLAGGED SINCE 1-Feb-1994 NOT FROM "Smith"'
1277+
)
1278+
cmd = server.commands.pop
1279+
assert_equal Net::IMAP::ESearchResult.new(
1280+
cmd.tag, false, [["MIN", 2], ["COUNT", 3]]
1281+
), result
1282+
esearch_responses = imap.clear_responses("ESEARCH")
1283+
assert_equal 2, esearch_responses.count
1284+
refute esearch_responses.include?(result)
1285+
end
1286+
end
1287+
1288+
test("missing server ESEARCH response") do
1289+
with_fake_server do |server, imap|
1290+
# Example from RFC9051, 6.4.4:
1291+
# C: A282 SEARCH RETURN (SAVE) FLAGGED SINCE 1-Feb-1994 NOT FROM "Smith"
1292+
# S: A282 OK SEARCH completed, result saved
1293+
server.on "SEARCH" do |cmd| cmd.done_ok "result saved" end
1294+
server.on "UID SEARCH" do |cmd| cmd.done_ok "result saved" end
1295+
result = imap.search(
1296+
'RETURN (SAVE) FLAGGED SINCE 1-Feb-1994 NOT FROM "Smith"'
1297+
)
1298+
assert_pattern do
1299+
result => Net::IMAP::ESearchResult[uid: false, tag: /^RUBY\d+/, data: []]
1300+
end
1301+
result = imap.uid_search(
1302+
'RETURN (SAVE) FLAGGED SINCE 1-Feb-1994 NOT FROM "Smith"'
1303+
)
1304+
assert_pattern do
1305+
result => Net::IMAP::ESearchResult[uid: true, tag: /^RUBY\d+/, data: []]
1306+
end
1307+
end
1308+
end
1309+
12621310
test("missing server SEARCH response") do
12631311
with_fake_server do |server, imap|
12641312
server.on "SEARCH", &:done_ok

0 commit comments

Comments
 (0)