Skip to content

Commit

Permalink
🔀 Backport #317 and #330 into v0.4-stable
Browse files Browse the repository at this point in the history
  • Loading branch information
nevans committed Oct 13, 2024
2 parents b1f8d33 + 39b7568 commit e60cde6
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 7 deletions.
30 changes: 28 additions & 2 deletions lib/net/imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ module Net
# pre-authenticated connection.
# - #responses: Yields unhandled UntaggedResponse#data and <em>non-+nil+</em>
# ResponseCode#data.
# - #extract_responses: Removes and returns the responses for which the block
# returns a true value.
# - #clear_responses: Deletes unhandled data from #responses and returns it.
# - #add_response_handler: Add a block to be called inside the receiver thread
# with every server response.
Expand Down Expand Up @@ -2540,7 +2542,7 @@ def idle_done
# return the TaggedResponse directly, #add_response_handler must be used to
# handle all response codes.
#
# Related: #clear_responses, #response_handlers, #greeting
# Related: #extract_responses, #clear_responses, #response_handlers, #greeting
def responses(type = nil)
if block_given?
synchronize { yield(type ? @responses[type.to_s.upcase] : @responses) }
Expand All @@ -2567,7 +2569,7 @@ def responses(type = nil)
# Clearing responses is synchronized with other threads. The lock is
# released before returning.
#
# Related: #responses, #response_handlers
# Related: #extract_responses, #responses, #response_handlers
def clear_responses(type = nil)
synchronize {
if type
Expand All @@ -2581,6 +2583,30 @@ def clear_responses(type = nil)
.freeze
end

# :call-seq:
# extract_responses(type) {|response| ... } -> array
#
# Yields all of the unhandled #responses for a single response +type+.
# Removes and returns the responses for which the block returns a true
# value.
#
# Extracting responses is synchronized with other threads. The lock is
# released before returning.
#
# Related: #responses, #clear_responses
def extract_responses(type)
type = String.try_convert(type) or
raise ArgumentError, "type must be a string"
raise ArgumentError, "must provide a block" unless block_given?
extracted = []
responses(type) do |all|
all.reject! do |response|
extracted << response if yield response
end
end
extracted
end

# Returns all response handlers, including those that are added internally
# by commands. Each response handler will be called with every new
# UntaggedResponse, TaggedResponse, and ContinuationRequest.
Expand Down
12 changes: 9 additions & 3 deletions test/net/imap/fake_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def initialize(...)
@config = Configuration.new(...)
@tcp_server = TCPServer.new(config.hostname, config.port)
@connection = nil
@mutex = Thread::Mutex.new
end

def host; tcp_server.addr[2] end
Expand All @@ -84,9 +85,11 @@ def run
# accepted and closed. This may change in the future. Call #shutdown
# explicitly to ensure the server socket is unbound.
def shutdown
connection&.close
commands&.close if connection&.commands&.closed?&.!
tcp_server.close
@mutex.synchronize do
connection&.close
commands&.close if connection&.commands&.closed?&.!
tcp_server.close
end
end

# A Queue that contains every command the server has received.
Expand All @@ -100,6 +103,9 @@ def state; connection.state end
# See CommandRouter#on
def on(...) connection&.on(...) end

# See Connection#unsolicited
def unsolicited(...) @mutex.synchronize { connection&.unsolicited(...) } end

private

attr_reader :tcp_server, :connection
Expand Down
1 change: 1 addition & 0 deletions test/net/imap/fake_server/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def initialize(server, tcp_socket:)

def commands; state.commands end
def on(...) router.on(...) end
def unsolicited(...) writer.untagged(...) end

def run
writer.greeting
Expand Down
59 changes: 57 additions & 2 deletions test/net/imap/test_imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1107,7 +1107,7 @@ def test_enable
end
end

def test_responses
test "#responses" do
with_fake_server do |server, imap|
# responses available before SELECT/EXAMINE
assert_equal(%w[IMAP4REV1 NAMESPACE MOVE IDLE UTF8=ACCEPT],
Expand Down Expand Up @@ -1141,7 +1141,7 @@ def test_responses
end
end

def test_clear_responses
test "#clear_responses" do
with_fake_server do |server, imap|
resp = imap.select "INBOX"
assert_equal([Net::IMAP::TaggedResponse, "RUBY0001", "OK"],
Expand All @@ -1165,6 +1165,49 @@ def test_clear_responses
end
end

test "#extract_responses" do
with_fake_server do |server, imap|
resp = imap.select "INBOX"
assert_equal([Net::IMAP::TaggedResponse, "RUBY0001", "OK"],
[resp.class, resp.tag, resp.name])
# Need to send a string type and a block
assert_raise(ArgumentError) do imap.extract_responses { true } end
assert_raise(ArgumentError) do imap.extract_responses(nil) { true } end
assert_raise(ArgumentError) do imap.extract_responses("OK") end
# matching nothing
assert_equal([172], imap.responses("EXISTS", &:dup))
assert_equal([], imap.extract_responses("EXISTS") { String === _1 })
assert_equal([172], imap.responses("EXISTS", &:dup))
# matching everything
assert_equal([172], imap.responses("EXISTS", &:dup))
assert_equal([172], imap.extract_responses("EXISTS", &:even?))
assert_equal([], imap.responses("EXISTS", &:dup))
# matching some
server.unsolicited("101 FETCH (UID 1111 FLAGS (\\Seen))")
server.unsolicited("102 FETCH (UID 2222 FLAGS (\\Seen \\Flagged))")
server.unsolicited("103 FETCH (UID 3333 FLAGS (\\Deleted))")
wait_for_response_count(imap, type: "FETCH", count: 3)

result = imap.extract_responses("FETCH") { _1.flags.include?(:Flagged) }
assert_equal(
[
Net::IMAP::FetchData.new(
102, {"UID" => 2222, "FLAGS" => [:Seen, :Flagged]}
),
],
result,
)
assert_equal 2, imap.responses("FETCH", &:count)

result = imap.extract_responses("FETCH") { _1.flags.include?(:Deleted) }
assert_equal(
[Net::IMAP::FetchData.new(103, {"UID" => 3333, "FLAGS" => [:Deleted]})],
result
)
assert_equal 1, imap.responses("FETCH", &:count)
end
end

test "#select with condstore" do
with_fake_server do |server, imap|
imap.select "inbox", condstore: true
Expand Down Expand Up @@ -1420,4 +1463,16 @@ def create_tcp_server
def server_addr
Addrinfo.tcp("localhost", 0).ip_address
end

def wait_for_response_count(imap, type:, count:,
timeout: 0.5, interval: 0.001)
deadline = Time.now + timeout
loop do
current_count = imap.responses(type, &:size)
break :count if count <= current_count
break :deadline if deadline < Time.now
sleep interval
end
end

end

0 comments on commit e60cde6

Please sign in to comment.