diff --git a/lib/net/imap.rb b/lib/net/imap.rb index e1ce6300..06d7bf25 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -2490,41 +2490,98 @@ def idle_done end end + RESPONSES_DEPRECATION_MSG = + "Pass a type or block to #responses, " \ + "set config.responses_without_block to :frozen_dup " \ + "or :silence_deprecation_warning, " \ + "or use #extract_responses or #clear_responses." + private_constant :RESPONSES_DEPRECATION_MSG + # :call-seq: + # responses -> hash of {String => Array} (see config.responses_without_block) + # responses(type) -> frozen array # responses {|hash| ...} -> block result # responses(type) {|array| ...} -> block result # - # Yields unhandled responses and returns the result of the block. + # Yields or returns unhandled server responses. Unhandled responses are + # stored in a hash, with arrays of UntaggedResponse#data keyed by + # UntaggedResponse#name and non-+nil+ untagged ResponseCode#data + # keyed by ResponseCode#name. + # + # When a block is given, yields unhandled responses and returns the block's + # result. Without a block, returns the unhandled responses. + # + # [With +type+] + # Yield or return only the array of responses for that +type+. + # When no block is given, the returned array is a frozen copy. + # [Without +type+] + # Yield or return the entire responses hash. + # + # When no block is given, the behavior is determined by + # Config#responses_without_block: + # >>> + # [+:silence_deprecation_warning+ (original behavior)] + # Returns the mutable responses hash (without any warnings). + # This is not thread-safe. + # + # [+:warn+ (default since +v0.5+)] + # Prints a warning and returns the mutable responses hash. + # This is not thread-safe. + # + # [+:frozen_dup+ (planned default for +v0.6+)] + # Returns a frozen copy of the unhandled responses hash, with frozen + # array values. # - # Unhandled responses are stored in a hash, with arrays of - # non-+nil+ UntaggedResponse#data keyed by UntaggedResponse#name - # and ResponseCode#data keyed by ResponseCode#name. Call without +type+ to - # yield the entire responses hash. Call with +type+ to yield only the array - # of responses for that type. + # [+:raise+] + # Raise an +ArgumentError+ with the deprecation warning. # # For example: # # imap.select("inbox") - # p imap.responses("EXISTS", &:last) + # p imap.responses("EXISTS").last # #=> 2 + # p imap.responses("UIDNEXT", &:last) + # #=> 123456 # p imap.responses("UIDVALIDITY", &:last) # #=> 968263756 + # p imap.responses {|responses| + # { + # exists: responses.delete("EXISTS").last, + # uidnext: responses.delete("UIDNEXT").last, + # uidvalidity: responses.delete("UIDVALIDITY").last, + # } + # } + # #=> {:exists=>2, :uidnext=>123456, :uidvalidity=>968263756} + # # "EXISTS", "UIDNEXT", and "UIDVALIDITY" have been removed: + # p imap.responses(&:keys) + # #=> ["FLAGS", "OK", "PERMANENTFLAGS", "RECENT", "HIGHESTMODSEQ"] + # + # Related: #extract_responses, #clear_responses, #response_handlers, #greeting # + # ===== Thread safety # >>> # *Note:* Access to the responses hash is synchronized for thread-safety. # The receiver thread and response_handlers cannot process new responses # until the block completes. Accessing either the response hash or its - # response type arrays outside of the block is unsafe. + # response type arrays outside of the block is unsafe. They can be safely + # updated inside the block. Consider using #clear_responses or + # #extract_responses instead. + # + # Net::IMAP will add and remove responses from the responses hash and its + # array values, in the calling threads for commands and in the receiver + # thread, but will not modify any responses after adding them to the + # responses hash. # - # Calling without a block is unsafe and deprecated. Future releases will - # raise ArgumentError unless a block is given. - # See Config#responses_without_block. + # ===== Clearing responses # # Previously unhandled responses are automatically cleared before entering a # mailbox with #select or #examine. Long-lived connections can receive many # unhandled server responses, which must be pruned or they will continually # consume more memory. Update or clear the responses hash or arrays inside - # the block, or use #clear_responses. + # the block, or remove responses with #extract_responses, #clear_responses, + # or #add_response_handler. + # + # ===== Missing responses # # Only non-+nil+ data is stored. Many important response codes have no data # of their own, but are used as "tags" on the ResponseText object they are @@ -2535,20 +2592,24 @@ def idle_done # ResponseCode#data on tagged responses. Although some command methods do # return the TaggedResponse directly, #add_response_handler must be used to # handle all response codes. - # - # Related: #extract_responses, #clear_responses, #response_handlers, #greeting def responses(type = nil) if block_given? synchronize { yield(type ? @responses[type.to_s.upcase] : @responses) } elsif type - raise ArgumentError, "Pass a block or use #clear_responses" + synchronize { @responses[type.to_s.upcase].dup.freeze } else case config.responses_without_block when :raise - raise ArgumentError, "Pass a block or use #clear_responses" + raise ArgumentError, RESPONSES_DEPRECATION_MSG when :warn - warn("DEPRECATED: pass a block or use #clear_responses", - uplevel: 1, category: :deprecated) + warn(RESPONSES_DEPRECATION_MSG, uplevel: 1, category: :deprecated) + when :frozen_dup + synchronize { + responses = @responses.transform_values(&:freeze) + responses.default_proc = nil + responses.default = [].freeze + return responses.freeze + } end @responses end diff --git a/lib/net/imap/config.rb b/lib/net/imap/config.rb index bcf5b72d..7532737c 100644 --- a/lib/net/imap/config.rb +++ b/lib/net/imap/config.rb @@ -7,10 +7,10 @@ module Net class IMAP - # Net::IMAP::Config stores configuration options for Net::IMAP clients. - # The global configuration can be seen at either Net::IMAP.config or - # Net::IMAP::Config.global, and the client-specific configuration can be - # seen at Net::IMAP#config. + # Net::IMAP::Config (available since +v0.4.13+) stores + # configuration options for Net::IMAP clients. The global configuration can + # be seen at either Net::IMAP.config or Net::IMAP::Config.global, and the + # client-specific configuration can be seen at Net::IMAP#config. # # When creating a new client, all unhandled keyword arguments to # Net::IMAP.new are delegated to Config.new. Every client has its own @@ -128,7 +128,7 @@ def self.default; @default end # The global config object. Also available from Net::IMAP.config. def self.global; @global if defined?(@global) end - # A hash of hard-coded configurations, indexed by version number. + # A hash of hard-coded configurations, indexed by version number or name. def self.version_defaults; @version_defaults end @version_defaults = {} @@ -172,9 +172,16 @@ def self.[](config) include AttrInheritance include AttrTypeCoercion - # The debug mode (boolean) + # The debug mode (boolean). The default value is +false+. # - # The default value is +false+. + # When #debug is +true+: + # * Data sent to and received from the server will be logged. + # * ResponseParser will print warnings with extra detail for parse + # errors. _This may include recoverable errors._ + # * ResponseParser makes extra assertions. + # + # *NOTE:* Versioned default configs inherit #debug from Config.global, and + # #load_defaults will not override #debug. attr_accessor :debug, type: :boolean # method: debug? @@ -200,60 +207,84 @@ def self.[](config) # The default value is +5+ seconds. attr_accessor :idle_response_timeout, type: Integer - # :markup: markdown - # # Whether to use the +SASL-IR+ extension when the server and \SASL - # mechanism both support it. + # mechanism both support it. Can be overridden by the +sasl_ir+ keyword + # parameter to Net::IMAP#authenticate. + # + # (Support for +SASL-IR+ was added in +v0.4.0+.) # - # See Net::IMAP#authenticate. + # ==== Valid options # - # | Starting with version | The default value is | - # |-----------------------|------------------------------------------| - # | _original_ | +false+ (extension unsupported) | - # | v0.4 | +true+ (support added) | + # [+false+ (original behavior, before support was added)] + # Do not use +SASL-IR+, even when it is supported by the server and the + # mechanism. + # + # [+true+ (default since +v0.4+)] + # Use +SASL-IR+ when it is supported by the server and the mechanism. attr_accessor :sasl_ir, type: :boolean - # :markup: markdown - # - # Controls the behavior of Net::IMAP#login when the `LOGINDISABLED` + # Controls the behavior of Net::IMAP#login when the +LOGINDISABLED+ # capability is present. When enforced, Net::IMAP will raise a - # LoginDisabledError when that capability is present. Valid values are: + # LoginDisabledError when that capability is present. # - # [+false+] + # (Support for +LOGINDISABLED+ was added in +v0.5.0+.) + # + # ==== Valid options + # + # [+false+ (original behavior, before support was added)] # Send the +LOGIN+ command without checking for +LOGINDISABLED+. # # [+:when_capabilities_cached+] # Enforce the requirement when Net::IMAP#capabilities_cached? is true, # but do not send a +CAPABILITY+ command to discover the capabilities. # - # [+true+] + # [+true+ (default since +v0.5+)] # Only send the +LOGIN+ command if the +LOGINDISABLED+ capability is not # present. When capabilities are unknown, Net::IMAP will automatically # send a +CAPABILITY+ command first before sending +LOGIN+. # - # | Starting with version | The default value is | - # |-------------------------|--------------------------------| - # | _original_ | `false` | - # | v0.5 | `true` | attr_accessor :enforce_logindisabled, type: [ false, :when_capabilities_cached, true ] - # :markup: markdown + # Controls the behavior of Net::IMAP#responses when called without any + # arguments (+type+ or +block+). + # + # ==== Valid options + # + # [+:silence_deprecation_warning+ (original behavior)] + # Returns the mutable responses hash (without any warnings). + # This is not thread-safe. + # + # [+:warn+ (default since +v0.5+)] + # Prints a warning and returns the mutable responses hash. + # This is not thread-safe. # - # Controls the behavior of Net::IMAP#responses when called without a - # block. Valid options are `:warn`, `:raise`, or - # `:silence_deprecation_warning`. + # [+:frozen_dup+ (planned default for +v0.6+)] + # Returns a frozen copy of the unhandled responses hash, with frozen + # array values. # - # | Starting with version | The default value is | - # |-------------------------|--------------------------------| - # | v0.4.13 | +:silence_deprecation_warning+ | - # | v0.5 | +:warn+ | - # | _eventually_ | +:raise+ | + # Note that calling IMAP#responses with a +type+ and without a block is + # not configurable and always behaves like +:frozen_dup+. + # + # (+:frozen_dup+ config option was added in +v0.4.17+) + # + # [+:raise+] + # Raise an ArgumentError with the deprecation warning. + # + # Note: #responses_without_args is an alias for #responses_without_block. attr_accessor :responses_without_block, type: [ - :silence_deprecation_warning, :warn, :raise, + :silence_deprecation_warning, :warn, :frozen_dup, :raise, ] + alias responses_without_args responses_without_block # :nodoc: + alias responses_without_args= responses_without_block= # :nodoc: + + ## + # :attr_accessor: responses_without_args + # + # Alias for responses_without_block + # Creates a new config object and initialize its attribute with +attrs+. # # If +parent+ is not given, the global config is used by default. @@ -357,12 +388,11 @@ def defaults_hash version_defaults[0.5] = Config[:current] - version_defaults[0.6] = Config[0.5] - version_defaults[:next] = Config[0.6] - - version_defaults[:future] = Config[0.6].dup.update( - responses_without_block: :raise, + version_defaults[0.6] = Config[0.5].dup.update( + responses_without_block: :frozen_dup, ).freeze + version_defaults[:next] = Config[0.6] + version_defaults[:future] = Config[:next] version_defaults.freeze end diff --git a/test/lib/helper.rb b/test/lib/helper.rb index bef82346..e84854a5 100644 --- a/test/lib/helper.rb +++ b/test/lib/helper.rb @@ -2,3 +2,16 @@ require "core_assertions" Test::Unit::TestCase.include Test::Unit::CoreAssertions + +class Test::Unit::TestCase + 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 diff --git a/test/net/imap/test_config.rb b/test/net/imap/test_config.rb index 6c709735..fa11c74a 100644 --- a/test/net/imap/test_config.rb +++ b/test/net/imap/test_config.rb @@ -190,7 +190,7 @@ class ConfigTest < Test::Unit::TestCase assert_same Config.default, Config.new(Config.default).parent assert_same Config.global, Config.new(Config.global).parent assert_same Config[0.4], Config.new(0.4).parent - assert_same Config[0.5], Config.new(:next).parent + assert_same Config[0.6], Config.new(:next).parent assert_equal true, Config.new({debug: true}, debug: false).parent.debug? assert_equal true, Config.new({debug: true}, debug: false).parent.frozen? end diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb index 97e9a14d..6c30323b 100644 --- a/test/net/imap/test_imap.rb +++ b/test/net/imap/test_imap.rb @@ -1110,107 +1110,6 @@ def test_enable end end - test "#responses" do - with_fake_server do |server, imap| - # responses available before SELECT/EXAMINE - assert_equal(%w[IMAP4REV1 NAMESPACE MOVE IDLE UTF8=ACCEPT], - imap.responses("CAPABILITY", &:last)) - resp = imap.select "INBOX" - # responses are cleared after SELECT/EXAMINE - assert_equal(nil, imap.responses("CAPABILITY", &:last)) - assert_equal([Net::IMAP::TaggedResponse, "RUBY0001", "OK"], - [resp.class, resp.tag, resp.name]) - assert_equal([172], imap.responses { _1["EXISTS"] }) - assert_equal([3857529045], imap.responses("UIDVALIDITY") { _1 }) - assert_equal(1, imap.responses("RECENT", &:last)) - assert_raise(ArgumentError) do imap.responses("UIDNEXT") end - # Deprecated style, without a block: - imap.config.responses_without_block = :raise - assert_raise(ArgumentError) do imap.responses end - imap.config.responses_without_block = :warn - assert_raise(ArgumentError) do imap.responses("UIDNEXT") end - assert_warn(/Pass a block.*or.*clear_responses/i) do - assert_equal(%i[Answered Flagged Deleted Seen Draft], - imap.responses["FLAGS"]&.last) - end - # TODO: assert_no_warn? - imap.config.responses_without_block = :silence_deprecation_warning - assert_raise(ArgumentError) do imap.responses("UIDNEXT") end - stderr = EnvUtil.verbose_warning { - assert_equal(%i[Answered Flagged Deleted Seen Draft], - imap.responses["FLAGS"]&.last) - } - assert_empty stderr - end - end - - test "#clear_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]) - # called with "type", clears and returns only that type - assert_equal([172], imap.clear_responses("EXISTS")) - assert_equal([], imap.clear_responses("EXISTS")) - assert_equal([1], imap.clear_responses("RECENT")) - assert_equal([3857529045], imap.clear_responses("UIDVALIDITY")) - # called without "type", clears and returns all responses - responses = imap.clear_responses - assert_equal([], responses["EXISTS"]) - assert_equal([], responses["RECENT"]) - assert_equal([], responses["UIDVALIDITY"]) - assert_equal([12], responses["UNSEEN"]) - assert_equal([4392], responses["UIDNEXT"]) - assert_equal(5, responses["FLAGS"].last&.size) - assert_equal(3, responses["PERMANENTFLAGS"].last&.size) - assert_equal({}, imap.responses(&:itself)) - assert_equal({}, imap.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 @@ -1467,15 +1366,4 @@ 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 diff --git a/test/net/imap/test_imap_responses.rb b/test/net/imap/test_imap_responses.rb new file mode 100644 index 00000000..aaa61d43 --- /dev/null +++ b/test/net/imap/test_imap_responses.rb @@ -0,0 +1,248 @@ +# frozen_string_literal: true + +require "net/imap" +require "test/unit" +require_relative "fake_server" + +class IMAPResponsesTest < Test::Unit::TestCase + include Net::IMAP::FakeServer::TestHelper + + CONFIG_OPTIONS = %i[ + silence_deprecation_warning + warn + raise + ].freeze + + 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 + + def for_each_config_option(imap) + original = imap.config.responses_without_block + CONFIG_OPTIONS.each do |option| + imap.config.responses_without_block = option + yield option + end + ensure + imap.config.responses_without_block = original + end + + # with a block: returns the block result + test "#responses(&block)" do + with_fake_server do |server, imap| + stderr = EnvUtil.verbose_warning do + # Config options make no difference to responses(&block) + for_each_config_option(imap) do + # responses available before SELECT/EXAMINE + assert_equal(%w[IMAP4REV1 NAMESPACE MOVE IDLE UTF8=ACCEPT], + imap.responses { _1["CAPABILITY"].last }) + end + # responses are cleared after SELECT/EXAMINE + imap.select "INBOX" + for_each_config_option(imap) do + assert_equal nil, imap.responses { _1["CAPABILITY"].last } + assert_equal [172], imap.responses { _1["EXISTS"].dup } + assert_equal [3857529045], imap.responses { _1["UIDVALIDITY"].dup } + assert_equal 1, imap.responses { _1["RECENT"].last } + assert_equal(%i[Answered Flagged Deleted Seen Draft], + imap.responses { _1["FLAGS"].last }) + end + end + assert_empty stderr # never warn when a block is given + end + end + + # with a type and a block: returns the block result + test "#responses(type, &block)" do + with_fake_server do |server, imap| + stderr = EnvUtil.verbose_warning do + # Config options make no difference to responses(type, &block) + for_each_config_option(imap) do + # responses available before SELECT/EXAMINE + assert_equal(%w[IMAP4REV1 NAMESPACE MOVE IDLE UTF8=ACCEPT], + imap.responses("CAPABILITY", &:last)) + end + # responses are cleared after SELECT/EXAMINE + imap.select "INBOX" + for_each_config_option(imap) do + assert_equal nil, imap.responses("CAPABILITY", &:last) + assert_equal [172], imap.responses("EXISTS", &:dup) + assert_equal [3857529045], imap.responses("UIDVALIDITY", &:dup) + assert_equal 1, imap.responses("RECENT", &:last) + assert_equal [4392], imap.responses("UIDNEXT", &:dup) + assert_equal(%i[Answered Flagged Deleted Seen Draft], + imap.responses("FLAGS", &:last)) + end + end + assert_empty stderr # never warn when type or block are given + end + end + + # with with a type and no block: always returns a frozen duplicate + test "#responses(type, &nil)" do + with_fake_server do |server, imap| + stderr = EnvUtil.verbose_warning do + # Config options make no difference to responses(type) + for_each_config_option(imap) do + # responses available before SELECT/EXAMINE + assert imap.responses("CAPABILITY").frozen? + assert_equal(%w[IMAP4REV1 NAMESPACE MOVE IDLE UTF8=ACCEPT], + imap.responses("CAPABILITY").last) + end + # responses are cleared after SELECT/EXAMINE + imap.select "INBOX" + for_each_config_option(imap) do + assert imap.responses("CAPABILITY").frozen? + assert imap.responses("EXISTS").frozen? + assert imap.responses("UIDVALIDITIY").frozen? + assert_equal [], imap.responses("CAPABILITY") + assert_equal [172], imap.responses("EXISTS") + assert_equal [3857529045], imap.responses("UIDVALIDITY") + assert_equal 1, imap.responses("RECENT").last + assert imap.responses("UIDNEXT").frozen? + assert_equal [4392], imap.responses("UIDNEXT") + assert imap.responses("FLAGS").frozen? + assert_equal(%i[Answered Flagged Deleted Seen Draft], + imap.responses("FLAGS").last) + end + end + assert_empty stderr # never warn when type is given + end + end + + def assert_responses_warn + assert_warn( + / + (?=(?-x)Pass a type or block to #responses\b) + (?=.*config\.responses_without_block.*:silence_deprecation_warning\b) + (?=.*\#extract_responses\b) + .*\#clear_responses\b + /ix + ) do + yield + end + end + + # without type or block: relies on config.responses_without_block + test "#responses without type or block" do + with_fake_server do |server, imap| + # can be configured to raise + imap.config.responses_without_block = :raise + assert_raise(ArgumentError) do imap.responses end + # with warnings (default for v0.5) + imap.config.responses_without_block = :warn + assert_responses_warn do assert_kind_of Hash, imap.responses end + assert_responses_warn do refute imap.responses.frozen? end + assert_responses_warn do refute imap.responses["CAPABILITY"].frozen? end + assert_responses_warn do + assert_equal(%w[IMAP4REV1 NAMESPACE MOVE IDLE UTF8=ACCEPT], + imap.responses["CAPABILITY"].last) + end + assert_responses_warn do imap.responses["FAKE"] = :uh_oh! end + assert_responses_warn do assert_equal :uh_oh!, imap.responses["FAKE"] end + assert_responses_warn do imap.responses.delete("FAKE") end + assert_responses_warn do assert_equal [], imap.responses["FAKE"] end + # warnings can be silenced + imap.config.responses_without_block = :silence_deprecation_warning + stderr = EnvUtil.verbose_warning do + refute imap.responses.frozen? + refute imap.responses["CAPABILITY"].frozen? + assert_equal(%w[IMAP4REV1 NAMESPACE MOVE IDLE UTF8=ACCEPT], + imap.responses["CAPABILITY"].last) + imap.responses["FAKE"] = :uh_oh! + assert_equal :uh_oh!, imap.responses["FAKE"] + imap.responses.delete("FAKE") + assert_equal [], imap.responses["FAKE"] + end + assert_empty stderr + # opt-in to future behavior + imap.config.responses_without_block = :frozen_dup + stderr = EnvUtil.verbose_warning do + assert imap.responses.frozen? + assert imap.responses["CAPABILITY"].frozen? + assert_equal(%w[IMAP4REV1 NAMESPACE MOVE IDLE UTF8=ACCEPT], + imap.responses["CAPABILITY"].last) + end + assert_empty stderr + end + end + + test "#clear_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]) + # called with "type", clears and returns only that type + assert_equal([172], imap.clear_responses("EXISTS")) + assert_equal([], imap.clear_responses("EXISTS")) + assert_equal([1], imap.clear_responses("RECENT")) + assert_equal([3857529045], imap.clear_responses("UIDVALIDITY")) + # called without "type", clears and returns all responses + responses = imap.clear_responses + assert_equal([], responses["EXISTS"]) + assert_equal([], responses["RECENT"]) + assert_equal([], responses["UIDVALIDITY"]) + assert_equal([12], responses["UNSEEN"]) + assert_equal([4392], responses["UIDNEXT"]) + assert_equal(5, responses["FLAGS"].last&.size) + assert_equal(3, responses["PERMANENTFLAGS"].last&.size) + assert_equal({}, imap.responses(&:itself)) + assert_equal({}, imap.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 + +end