Skip to content

Commit 331db7f

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 5cbf05f commit 331db7f

File tree

2 files changed

+136
-10
lines changed

2 files changed

+136
-10
lines changed

lib/net/imap.rb

Lines changed: 88 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ module Net
414414
# >>>
415415
# <em>The following are folded into +IMAP4rev2+ but are currently
416416
# unsupported or incompletely supported by</em> Net::IMAP<em>: RFC4466
417-
# extensions, +ESEARCH+, +SEARCHRES+, +LIST-EXTENDED+, +LIST-STATUS+,
417+
# extensions, +SEARCHRES+, +LIST-EXTENDED+, +LIST-STATUS+,
418418
# +LITERAL-+, and +SPECIAL-USE+.</em>
419419
#
420420
# ==== RFC2087: +QUOTA+
@@ -466,6 +466,10 @@ module Net
466466
# - Updates #append with the +APPENDUID+ ResponseCode
467467
# - Updates #copy, #move with the +COPYUID+ ResponseCode
468468
#
469+
# ==== RFC4731: +ESEARCH+
470+
# Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051].
471+
# - Updates #search, #uid_search with +return+ options and ESearchResult.
472+
#
469473
# ==== RFC4959: +SASL-IR+
470474
# Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051].
471475
# - Updates #authenticate with the option to send an initial response.
@@ -1935,9 +1939,11 @@ def uid_expunge(uid_set)
19351939
#
19361940
# Sends a {SEARCH command [IMAP4rev1 §6.4.4]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.4]
19371941
# to search the mailbox for messages that match the given search +criteria+,
1938-
# and returns a SearchResult. SearchResult inherits from Array (for
1939-
# backward compatibility) but adds SearchResult#modseq when the +CONDSTORE+
1940-
# capability has been enabled.
1942+
# and returns either a SearchResult or an ESearchResult. SearchResult
1943+
# inherits from Array (for backward compatibility) but adds
1944+
# SearchResult#modseq when the +CONDSTORE+ capability has been enabled.
1945+
# ESearchResult also implements to_a{rdoc-ref:ESearchResult#to_a}, for
1946+
# compatibility with SearchResult.
19411947
#
19421948
# +criteria+ is one or more search keys and their arguments, which may be
19431949
# provided as an array or a string.
@@ -1948,8 +1954,11 @@ def uid_expunge(uid_set)
19481954
# set}[https://www.iana.org/assignments/character-sets/character-sets.xhtml]
19491955
# used by strings in the search +criteria+. When +charset+ isn't specified,
19501956
# either <tt>"US-ASCII"</tt> or <tt>"UTF-8"</tt> is assumed, depending on
1951-
# the server's capabilities. +charset+ may be sent inside +criteria+
1952-
# instead of as a separate argument.
1957+
# the server's capabilities.
1958+
#
1959+
# _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+.
19531962
#
19541963
# Related: #uid_search
19551964
#
@@ -1969,6 +1978,12 @@ def uid_expunge(uid_set)
19691978
# # criteria string contains charset arg
19701979
# imap.search("CHARSET UTF-8 OR UNSEEN (FLAGGED SUBJECT foo)")
19711980
#
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)])
1986+
#
19721987
# ==== Argument translation
19731988
#
19741989
# [When +criteria+ is an Array]
@@ -2007,6 +2022,49 @@ def uid_expunge(uid_set)
20072022
# <em>*WARNING:* This is vulnerable to injection attacks when external
20082023
# inputs are used.</em>
20092024
#
2025+
# ==== Return options
2026+
#
2027+
# For full definitions of the standard return options and return data, see
2028+
# the relevant RFCs.
2029+
#
2030+
# ===== +ESEARCH+ or +IMAP4rev2+
2031+
#
2032+
# The following return options require either +ESEARCH+ or +IMAP4rev2+.
2033+
# See [{RFC4731 §3.1}[https://rfc-editor.org/rfc/rfc4731#section-3.1]] or
2034+
# [{IMAP4rev2 §6.4.4}[https://www.rfc-editor.org/rfc/rfc9051.html#section-6.4.4]].
2035+
#
2036+
# [+ALL+]
2037+
# Returns ESearchResult#all with a SequenceSet of all matching sequence
2038+
# numbers or UIDs. This is the default, when return options are empty.
2039+
#
2040+
# For compatibility with SearchResult, ESearchResult#to_a returns an
2041+
# Array of message sequence numbers or UIDs.
2042+
# [+COUNT+]
2043+
# Returns ESearchResult#count with the number of matching messages.
2044+
# [+MAX+]
2045+
# Returns ESearchResult#max with the highest matching sequence number or
2046+
# UID.
2047+
# [+MIN+]
2048+
# Returns ESearchResult#min with the lowest matching sequence number or
2049+
# UID.
2050+
#
2051+
# ===== +CONDSTORE+
2052+
#
2053+
# ESearchResult#modseq return data does not have a corresponding return
2054+
# option. Instead, it is returned if the +MODSEQ+ search key is used or
2055+
# when the +CONDSTORE+ extension is enabled for the selected mailbox.
2056+
# See [{RFC4731 §3.2}[https://www.rfc-editor.org/rfc/rfc4731#section-3.2]]
2057+
# or [{RFC7162 §2.1.5}[https://www.rfc-editor.org/rfc/rfc7162#section-3.1.5]].
2058+
#
2059+
# ===== +RFC4466+ compatible extensions
2060+
#
2061+
# {RFC4466 §2.6}[https://www.rfc-editor.org/rfc/rfc4466.html#section-2.6]
2062+
# defines standard syntax for search extensions. Net::IMAP allows sending
2063+
# unknown search return options and will parse unknown search extensions'
2064+
# return values into ExtensionData. Please note that this is an
2065+
# intentionally _unstable_ API. Future releases may return different
2066+
# (incompatible) objects, <em>without deprecation or warning</em>.
2067+
#
20102068
# ==== Search keys
20112069
#
20122070
# For full definitions of the standard search +criteria+,
@@ -2198,6 +2256,13 @@ def uid_expunge(uid_set)
21982256
#
21992257
# ==== Capabilities
22002258
#
2259+
# Return options should only be specified when the server supports
2260+
# +IMAP4rev2+ or an extension that allows them, such as +ESEARCH+
2261+
# [RFC4731[https://rfc-editor.org/rfc/rfc4731#section-3.1]].
2262+
#
2263+
# When +IMAP4rev2+ is enabled, or when the server supports +IMAP4rev2+ but
2264+
# not +IMAP4rev1+, ESearchResult is always returned instead of SearchResult.
2265+
#
22012266
# If CONDSTORE[https://www.rfc-editor.org/rfc/rfc7162.html] is supported
22022267
# and enabled for the selected mailbox, a non-empty SearchResult will
22032268
# include a +MODSEQ+ value.
@@ -3153,6 +3218,7 @@ def enforce_logindisabled?
31533218
end
31543219

31553220
def search_args(keys, charset_arg = nil, charset: nil)
3221+
esearch = (keys in /\ARETURN\b/i | Array[/\ARETURN\z/i, *])
31563222
if charset && charset_arg
31573223
raise ArgumentError, "multiple charset arguments"
31583224
end
@@ -3163,16 +3229,28 @@ def search_args(keys, charset_arg = nil, charset: nil)
31633229
end
31643230
args = normalize_searching_criteria(keys)
31653231
args.prepend("CHARSET", charset) if charset
3166-
args
3232+
return args, esearch
31673233
end
31683234

31693235
def search_internal(cmd, ...)
3170-
args = search_args(...)
3236+
args, esearch = search_args(...)
31713237
synchronize do
3172-
send_command(cmd, *args)
3238+
tagged = send_command(cmd, *args)
3239+
tag = tagged.tag
3240+
# Only the last ESEARCH or SEARCH is used. Excess results are ignored.
3241+
esearch_result = extract_responses("ESEARCH") {|response|
3242+
response in ESearchResult(tag: ^tag)
3243+
}.last
31733244
search_result = clear_responses("SEARCH").last
3174-
if search_result
3245+
if esearch_result
3246+
# silently ignore SEARCH results, if any
3247+
esearch_result
3248+
elsif search_result
3249+
# warn EXPECTED_ESEARCH_RESULT if esearch
31753250
search_result
3251+
elsif esearch
3252+
# warn NO_SEARCH_RESPONSE
3253+
ESearchResult[tag:, uid: cmd.start_with?("UID ")]
31763254
else
31773255
# warn NO_SEARCH_RESPONSE
31783256
SearchResult[]

test/net/imap/test_imap.rb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1295,6 +1295,54 @@ def seqset_coercible.to_sequence_set
12951295
end
12961296
end
12971297

1298+
test("#search/#uid_search with ESEARCH or IMAP4rev2") do
1299+
with_fake_server do |server, imap|
1300+
# Example from RFC9051, 6.4.4:
1301+
# C: A282 SEARCH RETURN (MIN COUNT) FLAGGED
1302+
# SINCE 1-Feb-1994 NOT FROM "Smith"
1303+
# S: * ESEARCH (TAG "A282") MIN 2 COUNT 3
1304+
# S: A282 OK SEARCH completed
1305+
server.on "SEARCH" do |cmd|
1306+
cmd.untagged "ESEARCH", "(TAG \"unrelated1\") MIN 1 COUNT 2"
1307+
cmd.untagged "ESEARCH", "(TAG %p) MIN 2 COUNT 3" % [cmd.tag]
1308+
cmd.untagged "ESEARCH", "(TAG \"unrelated2\") MIN 222 COUNT 333"
1309+
cmd.done_ok
1310+
end
1311+
result = imap.search(
1312+
'RETURN (MIN COUNT) FLAGGED SINCE 1-Feb-1994 NOT FROM "Smith"'
1313+
)
1314+
cmd = server.commands.pop
1315+
assert_equal Net::IMAP::ESearchResult.new(
1316+
cmd.tag, false, [["MIN", 2], ["COUNT", 3]]
1317+
), result
1318+
esearch_responses = imap.clear_responses("ESEARCH")
1319+
assert_equal 2, esearch_responses.count
1320+
refute esearch_responses.include?(result)
1321+
end
1322+
end
1323+
1324+
test("missing server ESEARCH response") do
1325+
with_fake_server do |server, imap|
1326+
# Example from RFC9051, 6.4.4:
1327+
# C: A282 SEARCH RETURN (SAVE) FLAGGED SINCE 1-Feb-1994 NOT FROM "Smith"
1328+
# S: A282 OK SEARCH completed, result saved
1329+
server.on "SEARCH" do |cmd| cmd.done_ok "result saved" end
1330+
server.on "UID SEARCH" do |cmd| cmd.done_ok "result saved" end
1331+
result = imap.search(
1332+
'RETURN (SAVE) FLAGGED SINCE 1-Feb-1994 NOT FROM "Smith"'
1333+
)
1334+
assert_pattern do
1335+
result => Net::IMAP::ESearchResult[uid: false, tag: /^RUBY\d+/, data: []]
1336+
end
1337+
result = imap.uid_search(
1338+
'RETURN (SAVE) FLAGGED SINCE 1-Feb-1994 NOT FROM "Smith"'
1339+
)
1340+
assert_pattern do
1341+
result => Net::IMAP::ESearchResult[uid: true, tag: /^RUBY\d+/, data: []]
1342+
end
1343+
end
1344+
end
1345+
12981346
test("missing server SEARCH response") do
12991347
with_fake_server do |server, imap|
13001348
server.on "SEARCH", &:done_ok

0 commit comments

Comments
 (0)