Skip to content

Commit a945c89

Browse files
authored
🔀 Merge pull request #442 from ruby/backport/v0.2-response_handlers
✨ Add `response_handlers` option to `new` (backport)
2 parents d50e9f3 + 15b6a65 commit a945c89

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
@@ -134,6 +134,8 @@ module Net
134134
# Use paginated or limited versions of commands whenever possible.
135135
#
136136
# Use #add_response_handler to handle responses after each one is received.
137+
# Use the +response_handlers+ argument to ::new to assign response handlers
138+
# before the receiver thread is started.
137139
#
138140
# == Errors
139141
#
@@ -984,6 +986,11 @@ def uid_sort(sort_keys, search_keys, charset)
984986
# end
985987
# }
986988
#
989+
# Response handlers can also be added when the client is created before the
990+
# receiver thread is started, by the +response_handlers+ argument to ::new.
991+
# This ensures every server response is handled, including the #greeting.
992+
#
993+
# Related: #remove_response_handler, #response_handlers
987994
def add_response_handler(handler = nil, &block)
988995
raise ArgumentError, "two Procs are passed" if handler && block
989996
@response_handlers.push(block || handler)
@@ -1099,6 +1106,12 @@ def idle_done
10991106
# OpenSSL::SSL::SSLContext#set_params as parameters.
11001107
# open_timeout:: Seconds to wait until a connection is opened
11011108
# idle_response_timeout:: Seconds to wait until an IDLE response is received
1109+
# response_handlers:: A list of response handlers to be added before the
1110+
# receiver thread is started. This ensures every server
1111+
# response is handled, including the #greeting. Note
1112+
# that the greeting is handled in the current thread,
1113+
# but all other responses are handled in the receiver
1114+
# thread.
11021115
#
11031116
# The most common errors are:
11041117
#
@@ -1141,6 +1154,7 @@ def initialize(host, port_or_options = {},
11411154
@responses = Hash.new([].freeze)
11421155
@tagged_responses = {}
11431156
@response_handlers = []
1157+
options[:response_handlers]&.each do |h| add_response_handler(h) end
11441158
@tagged_response_arrival = new_cond
11451159
@continued_command_tag = nil
11461160
@continuation_request_arrival = new_cond
@@ -1157,6 +1171,7 @@ def initialize(host, port_or_options = {},
11571171
if @greeting.name == "BYE"
11581172
raise ByeResponseError, @greeting
11591173
end
1174+
@response_handlers.each do |handler| handler.call(@greeting) end
11601175

11611176
@client_thread = Thread.current
11621177
@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)