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` (changed from empty array).
  • Loading branch information
nevans committed Nov 11, 2024
1 parent 107d943 commit 0587646
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 4 deletions.
36 changes: 32 additions & 4 deletions lib/net/imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1930,7 +1930,7 @@ def uid_expunge(uid_set)
end

# :call-seq:
# search(criteria, charset = nil) -> result
# search(criteria, charset = nil, esearch: false) -> result
#
# 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+,
Expand Down Expand Up @@ -1973,6 +1973,11 @@ def uid_expunge(uid_set)
# Do not use the +charset+ argument when either return options or charset
# are embedded in +criteria+.
#
# +esearch+ controls the return type when the server does not return any
# search results. If +esearch+ is +true+ or +criteria+ begins with
# +RETURN+, an empty ESearchResult will be returned. When +esearch+ is
# +false+, an empty SearchResult will be returned.
#
# Related: #uid_search
#
# ===== For example:
Expand Down Expand Up @@ -3146,12 +3151,35 @@ def enforce_logindisabled?
end
end

def search_internal(cmd, keys, charset = nil)
HasSearchReturnOpts = ->keys {
keys in RawData[/\ARETURN /] | Array[/\ARETURN\z/i, *]
}
private_constant :HasSearchReturnOpts

def search_internal(cmd, keys, charset = nil, esearch: nil)
keys = normalize_searching_criteria(keys)
args = charset ? ["CHARSET", charset, *keys] : keys
# TODO: check if certain extensions are enabled
esearch = (keys in HasSearchReturnOpts) if esearch.nil?
synchronize do
send_command(cmd, *args)
clear_responses("SEARCH").last || []
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 esearch_result
esearch_result # silently ignore SEARCH results
elsif search_result
# TODO: warn if ESEARCH result was expected, i.e: buggy server?
# warn EXPECTED_ESEARCH_RESULT if esearch
search_result
elsif esearch
ESearchResult[tag:, uid: cmd == "UID SEARCH"]
else
SearchResult[]
end
end
end

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 @@ -1257,6 +1257,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 0587646

Please sign in to comment.