diff --git a/lib/net/imap.rb b/lib/net/imap.rb index b3330b9a..0c37be05 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -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+, @@ -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: @@ -3145,12 +3150,24 @@ def enforce_logindisabled? end end - def search_internal(cmd, keys, charset = nil) + def search_internal(cmd, keys, charset = nil, esearch: false) keys = normalize_searching_criteria(keys) args = charset ? ["CHARSET", charset, *keys] : keys synchronize do - send_command(cmd, *args) - clear_responses("SEARCH").last || [] + clear_responses("SEARCH") + result = nil + send_command(cmd, *args) do |response, tag| + if response in data: ESearchResult(tag: ^tag) => result + responses("ESEARCH") { _1.delete(result) } + end + end + if result + result + elsif esearch || keys in RawData[/\ARETURN /] | Array[/\ARETURN\z/i, *] + ESearchResult.new + else + clear_responses("SEARCH").last || [] + end end end diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb index c9fe5ae1..bd64d7e5 100644 --- a/test/net/imap/test_imap.rb +++ b/test/net/imap/test_imap.rb @@ -1257,6 +1257,45 @@ 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 + result = imap.search( + 'RETURN (SAVE) FLAGGED SINCE 1-Feb-1994 NOT FROM "Smith"' + ) + assert_equal Net::IMAP::ESearchResult.new, result + end + end + test("missing server SEARCH response") do with_fake_server do |server, imap| server.on "SEARCH", &:done_ok