diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 181fd4a0..d33831d2 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -235,8 +235,9 @@ module Net # Use paginated or limited versions of commands whenever possible. # # Use #add_response_handler to handle responses after each one is received. - # Use #extract_responses, #clear_responses, or #responses (with a block) to - # prune responses. + # Use the +response_handlers+ argument to ::new to assign response handlers + # before the receiver thread is started. Use #extract_responses, + # #clear_responses, or #responses (with a block) to prune responses. # # == Errors # @@ -961,6 +962,12 @@ def idle_response_timeout; config.idle_response_timeout end # # See DeprecatedClientOptions.new for deprecated SSL arguments. # + # [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. + # # [config] # A Net::IMAP::Config object to use as the basis for #config. By default, # the global Net::IMAP.config is used. @@ -1032,7 +1039,7 @@ def idle_response_timeout; config.idle_response_timeout end # [Net::IMAP::ByeResponseError] # Connected to the host successfully, but it immediately said goodbye. # - def initialize(host, port: nil, ssl: nil, + def initialize(host, port: nil, ssl: nil, response_handlers: nil, config: Config.global, **config_options) super() # Config options @@ -1057,6 +1064,7 @@ def initialize(host, port: nil, ssl: nil, @receiver_thread = nil @receiver_thread_exception = nil @receiver_thread_terminating = false + response_handlers&.each do add_response_handler(_1) end # Client Protocol Sender (including state for currently running commands) @tag_prefix = "RUBY" @@ -3255,6 +3263,10 @@ def response_handlers # 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 @@ -3281,6 +3293,7 @@ def remove_response_handler(handler) def start_imap_connection @greeting = get_server_greeting @capabilities = capabilities_from_resp_code @greeting + @response_handlers.each do |handler| handler.call(@greeting) end @receiver_thread = start_receiver_thread rescue Exception state_logout! 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 00000000..f513d867 --- /dev/null +++ b/test/net/imap/test_imap_response_handlers.rb @@ -0,0 +1,90 @@ +# 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 + + 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