From c779866b9bb36af4037192474cbc798b52479c8f Mon Sep 17 00:00:00 2001 From: nick evans Date: Tue, 21 May 2024 14:40:18 -0400 Subject: [PATCH] Fix IMAP search issues The code had several issues with existing IMAP UID search: * net-imap <= v0.3 returns `nil` when the server returns nothing. (>= v0.4 returns an empty array when the server returns nothing.) * net-imap v0.4.8 and v0.4.9 return a _frozen_ `SearchResult`. `SearchResult` inherits from `Array`, but because it's frozen, mutating it with `reverse!` results in an exception. See https://github.com/ruby/net-imap/issues/262. (v0.4.10 and above doesn't freeze SearchResult.) * newer net-imap (>= 0.5) will support servers that send `ESEARCH` results, but those will be returned as an `ESearchResult` struct (probably frozen). `IMAP4rev1` servers shouldn't return `ESEARCH` unless the client has activated that extension, but `IMAP4rev2` servers will _always_ return `ESEARCH`. * RFC3501 says nothing about sorting the UIDs that come back from `UID SEARCH`. Almost all servers do return sorted UIDs, most of the time. But it isn't 100% reliable. The fix is simple: * use `#to_a` to convert both `nil` and `ESearchResult` into an array. * use `#sort` to ensure the UIDs are sorted. This also returns a new array, without mutating the original (which may be frozen). --- lib/mail/network/retriever_methods/imap.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/mail/network/retriever_methods/imap.rb b/lib/mail/network/retriever_methods/imap.rb index 76c900152..0b3e30127 100644 --- a/lib/mail/network/retriever_methods/imap.rb +++ b/lib/mail/network/retriever_methods/imap.rb @@ -75,7 +75,10 @@ def find(options=nil, &block) start do |imap| options[:read_only] ? imap.examine(options[:mailbox]) : imap.select(options[:mailbox]) + uids = imap.uid_search(options[:keys], options[:search_charset]) + .to_a # older net-imap may return nil, newer may return ESearchResult struct + .sort # RFC3501 does _not_ require UIDs to be returned in order uids.reverse! if options[:what].to_sym == :last uids = uids.first(options[:count]) if options[:count].is_a?(Integer) uids.reverse! if (options[:what].to_sym == :last && options[:order].to_sym == :asc) ||