Skip to content

Commit 19495b8

Browse files
authored
🔀 Merge pull request #438 from ruby/backport/v0.3-response_handlers
✨ Backport `response_handlers`option to `new`
2 parents 945df80 + 58c5ef2 commit 19495b8

File tree

2 files changed

+149
-0
lines changed

2 files changed

+149
-0
lines changed

‎lib/net/imap.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ module Net
160160
# Use paginated or limited versions of commands whenever possible.
161161
#
162162
# Use #add_response_handler to handle responses after each one is received.
163+
# Use the +response_handlers+ argument to ::new to assign response handlers
164+
# before the receiver thread is started.
163165
#
164166
# == Errors
165167
#
@@ -1994,6 +1996,11 @@ def idle_done
19941996
# end
19951997
# }
19961998
#
1999+
# Response handlers can also be added when the client is created before the
2000+
# receiver thread is started, by the +response_handlers+ argument to ::new.
2001+
# This ensures every server response is handled, including the #greeting.
2002+
#
2003+
# Related: #remove_response_handler, #response_handlers
19972004
def add_response_handler(handler = nil, &block)
19982005
raise ArgumentError, "two Procs are passed" if handler && block
19992006
@response_handlers.push(block || handler)
@@ -2029,6 +2036,12 @@ def remove_response_handler(handler)
20292036
# OpenSSL::SSL::SSLContext#set_params as parameters.
20302037
# open_timeout:: Seconds to wait until a connection is opened
20312038
# idle_response_timeout:: Seconds to wait until an IDLE response is received
2039+
# response_handlers:: A list of response handlers to be added before the
2040+
# receiver thread is started. This ensures every server
2041+
# response is handled, including the #greeting. Note
2042+
# that the greeting is handled in the current thread,
2043+
# but all other responses are handled in the receiver
2044+
# thread.
20322045
#
20332046
# The most common errors are:
20342047
#
@@ -2071,6 +2084,7 @@ def initialize(host, port_or_options = {},
20712084
@responses = Hash.new([].freeze)
20722085
@tagged_responses = {}
20732086
@response_handlers = []
2087+
options[:response_handlers]&.each do |h| add_response_handler(h) end
20742088
@tagged_response_arrival = new_cond
20752089
@continued_command_tag = nil
20762090
@continuation_request_arrival = new_cond
@@ -2087,6 +2101,7 @@ def initialize(host, port_or_options = {},
20872101
if @greeting.name == "BYE"
20882102
raise ByeResponseError, @greeting
20892103
end
2104+
@response_handlers.each do |handler| handler.call(@greeting) end
20902105

20912106
@client_thread = Thread.current
20922107
@receiver_thread = Thread.start {
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# frozen_string_literal: true
2+
3+
require "net/imap"
4+
require "test/unit"
5+
6+
class IMAPResponseHandlersTest < Test::Unit::TestCase
7+
8+
def setup
9+
@do_not_reverse_lookup = Socket.do_not_reverse_lookup
10+
Socket.do_not_reverse_lookup = true
11+
@threads = []
12+
end
13+
14+
def teardown
15+
if !@threads.empty?
16+
assert_join_threads(@threads)
17+
end
18+
ensure
19+
Socket.do_not_reverse_lookup = @do_not_reverse_lookup
20+
end
21+
22+
test "#add_response_handlers" do
23+
server = create_tcp_server
24+
port = server.addr[1]
25+
start_server do
26+
sock = server.accept
27+
Timeout.timeout(5) do
28+
sock.print("* OK connection established\r\n")
29+
sock.gets # => NOOP
30+
sock.print("* 1 EXPUNGE\r\n")
31+
sock.print("* 2 EXPUNGE\r\n")
32+
sock.print("* 3 EXPUNGE\r\n")
33+
sock.print("RUBY0001 OK NOOP completed\r\n")
34+
sock.gets # => LOGOUT
35+
sock.print("* BYE terminating connection\r\n")
36+
sock.print("RUBY0002 OK LOGOUT completed\r\n")
37+
ensure
38+
sock.close
39+
server.close
40+
end
41+
end
42+
begin
43+
responses = []
44+
imap = Net::IMAP.new(server_addr, port: port)
45+
assert_equal 0, imap.response_handlers.length
46+
imap.add_response_handler do |r| responses << [:block, r] end
47+
assert_equal 1, imap.response_handlers.length
48+
imap.add_response_handler(->(r) { responses << [:proc, r] })
49+
assert_equal 2, imap.response_handlers.length
50+
51+
imap.noop
52+
responses = responses[0, 6].map {|which, resp|
53+
[which, resp.class, resp.name, resp.data]
54+
}
55+
assert_equal [
56+
[:block, Net::IMAP::UntaggedResponse, "EXPUNGE", 1],
57+
[:proc, Net::IMAP::UntaggedResponse, "EXPUNGE", 1],
58+
[:block, Net::IMAP::UntaggedResponse, "EXPUNGE", 2],
59+
[:proc, Net::IMAP::UntaggedResponse, "EXPUNGE", 2],
60+
[:block, Net::IMAP::UntaggedResponse, "EXPUNGE", 3],
61+
[:proc, Net::IMAP::UntaggedResponse, "EXPUNGE", 3],
62+
], responses
63+
ensure
64+
imap&.logout
65+
imap&.disconnect
66+
end
67+
end
68+
69+
test "::new with response_handlers kwarg" do
70+
greeting = nil
71+
expunges = []
72+
alerts = []
73+
untagged = 0
74+
handler0 = ->(r) { greeting ||= r }
75+
handler1 = ->(r) { alerts << r.data.text if r.data.code.name == "ALERT" rescue nil }
76+
handler2 = ->(r) { expunges << r.data if r.name == "EXPUNGE" }
77+
handler3 = ->(r) { untagged += 1 if r.is_a?(Net::IMAP::UntaggedResponse) }
78+
response_handlers = [handler0, handler1, handler2, handler3]
79+
80+
server = create_tcp_server
81+
port = server.addr[1]
82+
start_server do
83+
sock = server.accept
84+
Timeout.timeout(5) do
85+
sock.print("* OK connection established\r\n")
86+
sock.gets # => NOOP
87+
sock.print("* 1 EXPUNGE\r\n")
88+
sock.print("* 1 EXPUNGE\r\n")
89+
sock.print("* OK [ALERT] The first alert.\r\n")
90+
sock.print("RUBY0001 OK [ALERT] Did you see the alert?\r\n")
91+
sock.gets # => LOGOUT
92+
sock.print("* BYE terminating connection\r\n")
93+
sock.print("RUBY0002 OK LOGOUT completed\r\n")
94+
ensure
95+
sock.close
96+
server.close
97+
end
98+
end
99+
begin
100+
imap = Net::IMAP.new("localhost", port: port,
101+
response_handlers: response_handlers)
102+
assert_equal response_handlers, imap.response_handlers
103+
refute_same response_handlers, imap.response_handlers
104+
105+
# handler0 recieved the greeting and handler3 counted it
106+
assert_equal imap.greeting, greeting
107+
assert_equal 1, untagged
108+
109+
imap.noop
110+
assert_equal 4, untagged
111+
assert_equal [1, 1], expunges # from handler2
112+
assert_equal ["The first alert.", "Did you see the alert?"], alerts
113+
ensure
114+
imap&.logout
115+
imap&.disconnect
116+
end
117+
end
118+
119+
def start_server
120+
th = Thread.new do
121+
yield
122+
end
123+
@threads << th
124+
sleep 0.1 until th.stop?
125+
end
126+
127+
def create_tcp_server
128+
return TCPServer.new(server_addr, 0)
129+
end
130+
131+
def server_addr
132+
Addrinfo.tcp("localhost", 0).ip_address
133+
end
134+
end

0 commit comments

Comments
 (0)