From 61ff47445a2541c3f736d4e74df3d2a608f83163 Mon Sep 17 00:00:00 2001 From: nick evans Date: Sat, 22 Mar 2025 15:33:02 -0400 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=85=20Add=20tests=20for=20#add=5Fresp?= =?UTF-8?q?onse=5Fhandler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There weren't any response_handler tests at all, prior to this! --- test/net/imap/test_imap_response_handlers.rb | 53 ++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 test/net/imap/test_imap_response_handlers.rb diff --git a/test/net/imap/test_imap_response_handlers.rb b/test/net/imap/test_imap_response_handlers.rb new file mode 100644 index 000000000..900c2ebca --- /dev/null +++ b/test/net/imap/test_imap_response_handlers.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require "net/imap" +require "test/unit" +require_relative "fake_server" + +class IMAPResponseHandlersTest < Test::Unit::TestCase + include Net::IMAP::FakeServer::TestHelper + + def setup + Net::IMAP.config.reset + @do_not_reverse_lookup = Socket.do_not_reverse_lookup + Socket.do_not_reverse_lookup = true + @threads = [] + end + + def teardown + if !@threads.empty? + assert_join_threads(@threads) + end + ensure + Socket.do_not_reverse_lookup = @do_not_reverse_lookup + end + + test "#add_response_handlers" do + responses = [] + with_fake_server do |server, imap| + server.on("NOOP") do |resp| + 3.times do resp.untagged("#{_1 + 1} EXPUNGE") end + resp.done_ok + end + + assert_equal 0, imap.response_handlers.length + imap.add_response_handler do responses << [:block, _1] end + assert_equal 1, imap.response_handlers.length + imap.add_response_handler(->{ responses << [:proc, _1] }) + assert_equal 2, imap.response_handlers.length + + imap.noop + assert_pattern do + responses => [ + [:block, Net::IMAP::UntaggedResponse[name: "EXPUNGE", data: 1]], + [:proc, Net::IMAP::UntaggedResponse[name: "EXPUNGE", data: 1]], + [:block, Net::IMAP::UntaggedResponse[name: "EXPUNGE", data: 2]], + [:proc, Net::IMAP::UntaggedResponse[name: "EXPUNGE", data: 2]], + [:block, Net::IMAP::UntaggedResponse[name: "EXPUNGE", data: 3]], + [:proc, Net::IMAP::UntaggedResponse[name: "EXPUNGE", data: 3]], + ] + end + end + end + +end From 624f6da3323f7316cc99a4e4c5f1a3a931546a40 Mon Sep 17 00:00:00 2001 From: nick evans Date: Sat, 22 Mar 2025 14:53:00 -0400 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9C=A8=20Add=20`response=5Fhandlers`=20k?= =?UTF-8?q?warg=20to=20Net::IMAP.new?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This ensures every server response is handled, including the greeting. --- lib/net/imap.rb | 15 ++++++++ test/net/imap/test_imap_response_handlers.rb | 37 ++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 0fa91c832..96eef1e2d 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -160,6 +160,8 @@ module Net # Use paginated or limited versions of commands whenever possible. # # Use #add_response_handler to handle responses after each one is received. + # Use the +response_handlers+ argument to ::new to assign response handlers + # before the receiver thread is started. # # == Errors # @@ -1994,6 +1996,11 @@ def idle_done # end # } # + # Response handlers can also be added when the client is created before the + # receiver thread is started, by the +response_handlers+ argument to ::new. + # This ensures every server response is handled, including the #greeting. + # + # Related: #remove_response_handler, #response_handlers def add_response_handler(handler = nil, &block) raise ArgumentError, "two Procs are passed" if handler && block @response_handlers.push(block || handler) @@ -2029,6 +2036,12 @@ def remove_response_handler(handler) # OpenSSL::SSL::SSLContext#set_params as parameters. # open_timeout:: Seconds to wait until a connection is opened # idle_response_timeout:: Seconds to wait until an IDLE response is received + # response_handlers:: A list of response handlers to be added before the + # receiver thread is started. This ensures every server + # response is handled, including the #greeting. Note + # that the greeting is handled in the current thread, + # but all other responses are handled in the receiver + # thread. # # The most common errors are: # @@ -2071,6 +2084,7 @@ def initialize(host, port_or_options = {}, @responses = Hash.new([].freeze) @tagged_responses = {} @response_handlers = [] + options[:response_handlers]&.each do |h| add_response_handler(h) end @tagged_response_arrival = new_cond @continued_command_tag = nil @continuation_request_arrival = new_cond @@ -2087,6 +2101,7 @@ def initialize(host, port_or_options = {}, if @greeting.name == "BYE" raise ByeResponseError, @greeting end + @response_handlers.each do |handler| handler.call(@greeting) end @client_thread = Thread.current @receiver_thread = Thread.start { diff --git a/test/net/imap/test_imap_response_handlers.rb b/test/net/imap/test_imap_response_handlers.rb index 900c2ebca..f513d867f 100644 --- a/test/net/imap/test_imap_response_handlers.rb +++ b/test/net/imap/test_imap_response_handlers.rb @@ -50,4 +50,41 @@ def teardown end end + test "::new with response_handlers kwarg" do + greeting = nil + expunges = [] + alerts = [] + untagged = 0 + handler0 = ->{ greeting ||= _1 } + handler1 = ->{ alerts << _1.data.text if _1 in {data: {code: {name: "ALERT"}}} } + handler2 = ->{ expunges << _1.data if _1 in {name: "EXPUNGE"} } + handler3 = ->{ untagged += 1 if _1.is_a?(Net::IMAP::UntaggedResponse) } + response_handlers = [handler0, handler1, handler2, handler3] + + run_fake_server_in_thread do |server| + port = server.port + imap = Net::IMAP.new("localhost", port:, response_handlers:) + assert_equal response_handlers, imap.response_handlers + refute_same response_handlers, imap.response_handlers + + # handler0 recieved the greeting and handler3 counted it + assert_equal imap.greeting, greeting + assert_equal 1, untagged + + server.on("NOOP") do |resp| + resp.untagged "1 EXPUNGE" + resp.untagged "1 EXPUNGE" + resp.untagged "OK [ALERT] The first alert." + resp.done_ok "[ALERT] Did you see the alert?" + end + + imap.noop + assert_equal 4, untagged + assert_equal [1, 1], expunges # from handler2 + assert_equal ["The first alert.", "Did you see the alert?"], alerts + ensure + imap&.logout! unless imap&.disconnected? + end + end + end From 471416104e37a05f3c2af813bcbef5b6d3168608 Mon Sep 17 00:00:00 2001 From: nick evans Date: Wed, 16 Apr 2025 23:11:24 -0400 Subject: [PATCH 3/4] =?UTF-8?q?=E2=9C=85=20Fix=20ruby=202.7=20compatabilit?= =?UTF-8?q?y=20in=20backported=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/net/imap/test_imap_response_handlers.rb | 30 +++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/test/net/imap/test_imap_response_handlers.rb b/test/net/imap/test_imap_response_handlers.rb index f513d867f..7e79cf6a6 100644 --- a/test/net/imap/test_imap_response_handlers.rb +++ b/test/net/imap/test_imap_response_handlers.rb @@ -37,16 +37,17 @@ def teardown assert_equal 2, imap.response_handlers.length imap.noop - assert_pattern do - responses => [ - [:block, Net::IMAP::UntaggedResponse[name: "EXPUNGE", data: 1]], - [:proc, Net::IMAP::UntaggedResponse[name: "EXPUNGE", data: 1]], - [:block, Net::IMAP::UntaggedResponse[name: "EXPUNGE", data: 2]], - [:proc, Net::IMAP::UntaggedResponse[name: "EXPUNGE", data: 2]], - [:block, Net::IMAP::UntaggedResponse[name: "EXPUNGE", data: 3]], - [:proc, Net::IMAP::UntaggedResponse[name: "EXPUNGE", data: 3]], - ] - end + responses = responses[0, 6].map {|which, resp| + [which, resp.class, resp.name, resp.data] + } + assert_equal [ + [:block, Net::IMAP::UntaggedResponse, "EXPUNGE", 1], + [:proc, Net::IMAP::UntaggedResponse, "EXPUNGE", 1], + [:block, Net::IMAP::UntaggedResponse, "EXPUNGE", 2], + [:proc, Net::IMAP::UntaggedResponse, "EXPUNGE", 2], + [:block, Net::IMAP::UntaggedResponse, "EXPUNGE", 3], + [:proc, Net::IMAP::UntaggedResponse, "EXPUNGE", 3], + ], responses end end @@ -56,14 +57,15 @@ def teardown alerts = [] untagged = 0 handler0 = ->{ greeting ||= _1 } - handler1 = ->{ alerts << _1.data.text if _1 in {data: {code: {name: "ALERT"}}} } - handler2 = ->{ expunges << _1.data if _1 in {name: "EXPUNGE"} } - handler3 = ->{ untagged += 1 if _1.is_a?(Net::IMAP::UntaggedResponse) } + handler1 = ->(r) { alerts << r.data.text if r.data.code.name == "ALERT" rescue nil } + handler2 = ->(r) { expunges << r.data if r.name == "EXPUNGE" } + handler3 = ->(r) { untagged += 1 if r.is_a?(Net::IMAP::UntaggedResponse) } response_handlers = [handler0, handler1, handler2, handler3] run_fake_server_in_thread do |server| port = server.port - imap = Net::IMAP.new("localhost", port:, response_handlers:) + imap = Net::IMAP.new("localhost", port: port, + response_handlers: response_handlers) assert_equal response_handlers, imap.response_handlers refute_same response_handlers, imap.response_handlers From 58c5ef2528f89c2e58b35d7d2f44d848c239497f Mon Sep 17 00:00:00 2001 From: nick evans Date: Sat, 22 Mar 2025 15:33:02 -0400 Subject: [PATCH 4/4] =?UTF-8?q?=E2=9C=85=20Fix=20backport=20to=20net-imap?= =?UTF-8?q?=200.3=20and=20ruby=202.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FakeServer was introduced by v0.4, so the tests needed to be rewritten without it. And ruby 2.6 doesn't support numbered params or "...". --- test/net/imap/test_imap_response_handlers.rb | 86 +++++++++++++++----- 1 file changed, 64 insertions(+), 22 deletions(-) diff --git a/test/net/imap/test_imap_response_handlers.rb b/test/net/imap/test_imap_response_handlers.rb index 7e79cf6a6..3786f2427 100644 --- a/test/net/imap/test_imap_response_handlers.rb +++ b/test/net/imap/test_imap_response_handlers.rb @@ -2,13 +2,10 @@ require "net/imap" require "test/unit" -require_relative "fake_server" class IMAPResponseHandlersTest < Test::Unit::TestCase - include Net::IMAP::FakeServer::TestHelper def setup - Net::IMAP.config.reset @do_not_reverse_lookup = Socket.do_not_reverse_lookup Socket.do_not_reverse_lookup = true @threads = [] @@ -23,17 +20,32 @@ def teardown end test "#add_response_handlers" do - responses = [] - with_fake_server do |server, imap| - server.on("NOOP") do |resp| - 3.times do resp.untagged("#{_1 + 1} EXPUNGE") end - resp.done_ok + server = create_tcp_server + port = server.addr[1] + start_server do + sock = server.accept + Timeout.timeout(5) do + sock.print("* OK connection established\r\n") + sock.gets # => NOOP + sock.print("* 1 EXPUNGE\r\n") + sock.print("* 2 EXPUNGE\r\n") + sock.print("* 3 EXPUNGE\r\n") + sock.print("RUBY0001 OK NOOP completed\r\n") + sock.gets # => LOGOUT + sock.print("* BYE terminating connection\r\n") + sock.print("RUBY0002 OK LOGOUT completed\r\n") + ensure + sock.close + server.close end - + end + begin + responses = [] + imap = Net::IMAP.new(server_addr, port: port) assert_equal 0, imap.response_handlers.length - imap.add_response_handler do responses << [:block, _1] end + imap.add_response_handler do |r| responses << [:block, r] end assert_equal 1, imap.response_handlers.length - imap.add_response_handler(->{ responses << [:proc, _1] }) + imap.add_response_handler(->(r) { responses << [:proc, r] }) assert_equal 2, imap.response_handlers.length imap.noop @@ -48,6 +60,9 @@ def teardown [:block, Net::IMAP::UntaggedResponse, "EXPUNGE", 3], [:proc, Net::IMAP::UntaggedResponse, "EXPUNGE", 3], ], responses + ensure + imap&.logout + imap&.disconnect end end @@ -56,14 +71,32 @@ def teardown expunges = [] alerts = [] untagged = 0 - handler0 = ->{ greeting ||= _1 } + handler0 = ->(r) { greeting ||= r } handler1 = ->(r) { alerts << r.data.text if r.data.code.name == "ALERT" rescue nil } handler2 = ->(r) { expunges << r.data if r.name == "EXPUNGE" } handler3 = ->(r) { untagged += 1 if r.is_a?(Net::IMAP::UntaggedResponse) } response_handlers = [handler0, handler1, handler2, handler3] - run_fake_server_in_thread do |server| - port = server.port + server = create_tcp_server + port = server.addr[1] + start_server do + sock = server.accept + Timeout.timeout(5) do + sock.print("* OK connection established\r\n") + sock.gets # => NOOP + sock.print("* 1 EXPUNGE\r\n") + sock.print("* 1 EXPUNGE\r\n") + sock.print("* OK [ALERT] The first alert.\r\n") + sock.print("RUBY0001 OK [ALERT] Did you see the alert?\r\n") + sock.gets # => LOGOUT + sock.print("* BYE terminating connection\r\n") + sock.print("RUBY0002 OK LOGOUT completed\r\n") + ensure + sock.close + server.close + end + end + begin imap = Net::IMAP.new("localhost", port: port, response_handlers: response_handlers) assert_equal response_handlers, imap.response_handlers @@ -73,20 +106,29 @@ def teardown assert_equal imap.greeting, greeting assert_equal 1, untagged - server.on("NOOP") do |resp| - resp.untagged "1 EXPUNGE" - resp.untagged "1 EXPUNGE" - resp.untagged "OK [ALERT] The first alert." - resp.done_ok "[ALERT] Did you see the alert?" - end - imap.noop assert_equal 4, untagged assert_equal [1, 1], expunges # from handler2 assert_equal ["The first alert.", "Did you see the alert?"], alerts ensure - imap&.logout! unless imap&.disconnected? + imap&.logout + imap&.disconnect end end + def start_server + th = Thread.new do + yield + end + @threads << th + sleep 0.1 until th.stop? + end + + def create_tcp_server + return TCPServer.new(server_addr, 0) + end + + def server_addr + Addrinfo.tcp("localhost", 0).ip_address + end end