Skip to content

Commit

Permalink
✨ Gather ESEARCH response to #search/#uid_search
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
nevans committed Dec 14, 2024
1 parent 8c432dd commit d38357b
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 7 deletions.
48 changes: 41 additions & 7 deletions lib/net/imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1934,9 +1934,11 @@ 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.
# ESearchResult also implements to_a{rdoc-ref:ESearchResult#to_a}, for
# compatibility with SearchResult.
#
# +criteria+ is one or more search keys and their arguments, which may be
# provided as an array or a string.
Expand All @@ -1947,8 +1949,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 <tt>"US-ASCII"</tt> or <tt>"UTF-8"</tt> 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
#
Expand All @@ -1968,6 +1973,12 @@ def uid_expunge(uid_set)
# # criteria string contains charset arg
# imap.search("CHARSET UTF-8 OR UNSEEN (FLAGGED SUBJECT foo)")
#
# Sending return options and charset embedded in the +criteria+ 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)])
#
# ==== Argument translation
#
# [When +criteria+ is an Array]
Expand Down Expand Up @@ -2197,6 +2208,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.
Expand Down Expand Up @@ -3150,14 +3167,31 @@ def enforce_logindisabled?
end
end

HasSearchReturnOpts = ->keys {
keys in RawData[/\ARETURN /] | Array[/\ARETURN\z/i, *]
}
private_constant :HasSearchReturnOpts

def search_internal(cmd, keys, charset = nil)
keys = normalize_searching_criteria(keys)
args = charset ? ["CHARSET", charset, *keys] : keys
synchronize do
send_command(cmd, *args)
tagged = send_command(cmd, *args)
tag = tagged.tag
# Only the last ESEARCH or SEARCH is used. Excess results are ignored.
esearch_result = extract_responses("ESEARCH") {|response|
response in ESearchResult(tag: ^tag)
}.last
search_result = clear_responses("SEARCH").last
if search_result
if esearch_result
# silently ignore SEARCH results, if any
esearch_result
elsif search_result
# warn EXPECTED_ESEARCH_RESULT if esearch
search_result
elsif keys in HasSearchReturnOpts # TODO: check if IMAP4rev2 enabled
# warn NO_SEARCH_RESPONSE
ESearchResult[tag:, uid: cmd.start_with?("UID ")]
else
# warn NO_SEARCH_RESPONSE
SearchResult[]
Expand Down
48 changes: 48 additions & 0 deletions test/net/imap/test_imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1259,6 +1259,54 @@ def seqset_coercible.to_sequence_set
end
end

test("#search/#uid_search with ESEARCH or IMAP4rev2") do
with_fake_server do |server, imap|
# Example from RFC9051, 6.4.4:
# C: A282 SEARCH RETURN (MIN COUNT) FLAGGED
# SINCE 1-Feb-1994 NOT FROM "Smith"
# S: * ESEARCH (TAG "A282") MIN 2 COUNT 3
# S: A282 OK SEARCH completed
server.on "SEARCH" do |cmd|
cmd.untagged "ESEARCH", "(TAG \"unrelated1\") MIN 1 COUNT 2"
cmd.untagged "ESEARCH", "(TAG %p) MIN 2 COUNT 3" % [cmd.tag]
cmd.untagged "ESEARCH", "(TAG \"unrelated2\") MIN 222 COUNT 333"
cmd.done_ok
end
result = imap.search(
'RETURN (MIN COUNT) FLAGGED SINCE 1-Feb-1994 NOT FROM "Smith"'
)
cmd = server.commands.pop
assert_equal Net::IMAP::ESearchResult.new(
cmd.tag, false, [["MIN", 2], ["COUNT", 3]]
), result
esearch_responses = imap.clear_responses("ESEARCH")
assert_equal 2, esearch_responses.count
refute esearch_responses.include?(result)
end
end

test("missing server ESEARCH response") do
with_fake_server do |server, imap|
# Example from RFC9051, 6.4.4:
# C: A282 SEARCH RETURN (SAVE) FLAGGED SINCE 1-Feb-1994 NOT FROM "Smith"
# S: A282 OK SEARCH completed, result saved
server.on "SEARCH" do |cmd| cmd.done_ok "result saved" end
server.on "UID SEARCH" do |cmd| cmd.done_ok "result saved" end
result = imap.search(
'RETURN (SAVE) FLAGGED SINCE 1-Feb-1994 NOT FROM "Smith"'
)
assert_pattern do
result => Net::IMAP::ESearchResult[uid: false, tag: /^RUBY\d+/, data: []]
end
result = imap.uid_search(
'RETURN (SAVE) FLAGGED SINCE 1-Feb-1994 NOT FROM "Smith"'
)
assert_pattern do
result => Net::IMAP::ESearchResult[uid: true, tag: /^RUBY\d+/, data: []]
end
end
end

test("missing server SEARCH response") do
with_fake_server do |server, imap|
server.on "SEARCH", &:done_ok
Expand Down

0 comments on commit d38357b

Please sign in to comment.