From 18fe6dd0e58033ed2283372263e15dfda82af626 Mon Sep 17 00:00:00 2001 From: nick evans Date: Sat, 8 Jun 2024 21:47:59 -0400 Subject: [PATCH 01/11] =?UTF-8?q?=F0=9F=94=A7=20Add=20very=20basic=20Confi?= =?UTF-8?q?g=20class=20(currently=20unused)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nothing uses this new Config class yet. Currently, it holds only three config options: `debug`, `open_timeout`, and `idle_response_timeout`, but more will be added soon. This version does not have any support for inheritance. That will be added in one of the next commits. --- lib/net/imap.rb | 2 ++ lib/net/imap/config.rb | 40 ++++++++++++++++++++++++++++++++++++ test/net/imap/test_config.rb | 27 ++++++++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 lib/net/imap/config.rb create mode 100644 test/net/imap/test_config.rb diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 96ec3a50..af55d699 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -725,6 +725,8 @@ class IMAP < Protocol "UTF8=ONLY" => "UTF8=ACCEPT", }.freeze + autoload :Config, File.expand_path("imap/config", __dir__) + autoload :SASL, File.expand_path("imap/sasl", __dir__) autoload :SASLAdapter, File.expand_path("imap/sasl_adapter", __dir__) autoload :StringPrep, File.expand_path("imap/stringprep", __dir__) diff --git a/lib/net/imap/config.rb b/lib/net/imap/config.rb new file mode 100644 index 00000000..e411389c --- /dev/null +++ b/lib/net/imap/config.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true +# :markup: markdown + +module Net + class IMAP + + # Net::IMAP::Config stores configuration options for Net::IMAP clients. + # + # ## Thread Safety + # + # *NOTE:* Updates to config objects are not synchronized for thread-safety. + # + class Config + + # The debug mode (boolean) + attr_accessor :debug + alias debug? debug + + # Seconds to wait until a connection is opened. + # + # If the IMAP object cannot open a connection within this time, + # it raises a Net::OpenTimeout exception. See Net::IMAP.new. + attr_accessor :open_timeout + + # Seconds to wait until an IDLE response is received, after + # the client asks to leave the IDLE state. See Net::IMAP#idle_done. + attr_accessor :idle_response_timeout + + # Creates a new config object and initialize its attribute with +attrs+. + # + # If a block is given, the new config object is yielded to it. + def initialize(**attrs) + super() + attrs.each do send(:"#{_1}=", _2) end + yield self if block_given? + end + + end + end +end diff --git a/test/net/imap/test_config.rb b/test/net/imap/test_config.rb new file mode 100644 index 00000000..6b81729f --- /dev/null +++ b/test/net/imap/test_config.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "net/imap" +require "test/unit" + +class ConfigTest < Test::Unit::TestCase + Config = Net::IMAP::Config + + test "#debug" do + assert Config.new(debug: true).debug + refute Config.new(debug: false).debug + assert Config.new(debug: true).debug? + refute Config.new(debug: false).debug? + config = Config.new do |c| + c.debug = true + end + assert config.debug + config = Config.new + config.debug = true + assert config.debug + assert config.debug? + config.debug = false + refute config.debug + refute config.debug? + end + +end From d63022f4d4c36262e2d9f32c9bbc74952f2137b8 Mon Sep 17 00:00:00 2001 From: nick evans Date: Sat, 8 Jun 2024 22:10:59 -0400 Subject: [PATCH 02/11] =?UTF-8?q?=F0=9F=94=A7=E2=9A=A1=EF=B8=8F=20Store=20?= =?UTF-8?q?Config=20attr=5Faccessors=20in=20a=20Struct?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Config values are stored in a struct rather than ivars to simplify: * ensuring that all config objects share a single object shape * querying only locally configured values, e.g for inspection. --- lib/net/imap/config.rb | 3 ++ lib/net/imap/config/attr_accessors.rb | 67 +++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 lib/net/imap/config/attr_accessors.rb diff --git a/lib/net/imap/config.rb b/lib/net/imap/config.rb index e411389c..cec0a8bb 100644 --- a/lib/net/imap/config.rb +++ b/lib/net/imap/config.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true # :markup: markdown +require_relative "config/attr_accessors" + module Net class IMAP @@ -11,6 +13,7 @@ class IMAP # *NOTE:* Updates to config objects are not synchronized for thread-safety. # class Config + include AttrAccessors # The debug mode (boolean) attr_accessor :debug diff --git a/lib/net/imap/config/attr_accessors.rb b/lib/net/imap/config/attr_accessors.rb new file mode 100644 index 00000000..2cb2272c --- /dev/null +++ b/lib/net/imap/config/attr_accessors.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require "forwardable" + +module Net + class IMAP + class Config + + # Config values are stored in a struct rather than ivars to simplify: + # * ensuring that all config objects share a single object shape + # * querying only locally configured values, e.g for inspection. + module AttrAccessors + module Macros # :nodoc: internal API + def attr_accessor(name) AttrAccessors.attr_accessor(name) end + end + private_constant :Macros + + def self.included(mod) + mod.extend Macros + end + private_class_method :included + + extend Forwardable + + def self.attr_accessor(name) # :nodoc: internal API + name = name.to_sym + def_delegators :data, name, :"#{name}=" + end + + def self.attributes + instance_methods.grep(/=\z/).map { _1.to_s.delete_suffix("=").to_sym } + end + private_class_method :attributes + + def self.struct # :nodoc: internal API + unless defined?(self::Struct) + const_set :Struct, Struct.new(*attributes) + end + self::Struct + end + + def initialize # :notnew: + super() + @data = AttrAccessors.struct.new + end + + # Freezes the internal attributes struct, in addition to +self+. + def freeze + data.freeze + super + end + + protected + + attr_reader :data # :nodoc: internal API + + private + + def initialize_dup(other) + super + @data = other.data.dup + end + + end + end + end +end From 198c3e6f55b64bce7b8593211d8cd352bd3a309c Mon Sep 17 00:00:00 2001 From: nick evans Date: Sat, 8 Jun 2024 22:26:22 -0400 Subject: [PATCH 03/11] =?UTF-8?q?=F0=9F=94=A7=20Add=20a=20default=20config?= =?UTF-8?q?=20object?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/net/imap/config.rb | 21 +++++++++++++++++++++ test/net/imap/test_config.rb | 8 ++++++++ 2 files changed, 29 insertions(+) diff --git a/lib/net/imap/config.rb b/lib/net/imap/config.rb index cec0a8bb..76b8c68f 100644 --- a/lib/net/imap/config.rb +++ b/lib/net/imap/config.rb @@ -13,9 +13,16 @@ class IMAP # *NOTE:* Updates to config objects are not synchronized for thread-safety. # class Config + # The default config, which is hardcoded and frozen. + def self.default; @default end + include AttrAccessors # The debug mode (boolean) + # + # | Starting with version | The default value is | + # |-----------------------|----------------------| + # | _original_ | +false+ | attr_accessor :debug alias debug? debug @@ -23,10 +30,18 @@ class Config # # If the IMAP object cannot open a connection within this time, # it raises a Net::OpenTimeout exception. See Net::IMAP.new. + # + # | Starting with version | The default value is | + # |-----------------------|----------------------| + # | _original_ | +30+ seconds | attr_accessor :open_timeout # Seconds to wait until an IDLE response is received, after # the client asks to leave the IDLE state. See Net::IMAP#idle_done. + # + # | Starting with version | The default value is | + # |-----------------------|----------------------| + # | _original_ | +5+ seconds | attr_accessor :idle_response_timeout # Creates a new config object and initialize its attribute with +attrs+. @@ -38,6 +53,12 @@ def initialize(**attrs) yield self if block_given? end + @default = new( + debug: false, + open_timeout: 30, + idle_response_timeout: 5, + ).freeze + end end end diff --git a/test/net/imap/test_config.rb b/test/net/imap/test_config.rb index 6b81729f..28dd44da 100644 --- a/test/net/imap/test_config.rb +++ b/test/net/imap/test_config.rb @@ -24,4 +24,12 @@ class ConfigTest < Test::Unit::TestCase refute config.debug? end + test ".default" do + default = Config.default + assert default.equal?(Config.default) + assert default.is_a?(Config) + assert default.frozen? + refute default.debug? + end + end From 306384721579474c6e6a0eaa2395516122810bf8 Mon Sep 17 00:00:00 2001 From: nick evans Date: Sat, 8 Jun 2024 23:00:30 -0400 Subject: [PATCH 04/11] =?UTF-8?q?=F0=9F=94=A7=20Add=20config=20inheritance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inheritance forms a singly-linked-list, so lookup will be O(n) on the number of ancestors. So arbitrarily deep ancestor lists may have poor performance, but normal cases should be fine. Without customization, ancestor trees will be three or four deep: client -> [versioned ->] global -> default --- lib/net/imap/config.rb | 22 +++++++- lib/net/imap/config/attr_inheritance.rb | 75 +++++++++++++++++++++++++ test/net/imap/test_config.rb | 71 +++++++++++++++++++++++ 3 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 lib/net/imap/config/attr_inheritance.rb diff --git a/lib/net/imap/config.rb b/lib/net/imap/config.rb index 76b8c68f..df31b18d 100644 --- a/lib/net/imap/config.rb +++ b/lib/net/imap/config.rb @@ -2,12 +2,27 @@ # :markup: markdown require_relative "config/attr_accessors" +require_relative "config/attr_inheritance" module Net class IMAP # Net::IMAP::Config stores configuration options for Net::IMAP clients. # + # ## Inheritance + # + # Configs have a parent[rdoc-ref:Config::AttrInheritance#parent] config, and + # any attributes which have not been set locally will inherit the parent's + # value. + # + # See the following methods, defined by Config::AttrInheritance: + # - {#new}[rdoc-ref:Config::AttrInheritance#reset] -- create a new config + # which inherits from the receiver. + # - {#inherited?}[rdoc-ref:Config::AttrInheritance#inherited?] -- return + # whether a particular attribute is inherited. + # - {#reset}[rdoc-ref:Config::AttrInheritance#reset] -- reset attributes to + # be inherited. + # # ## Thread Safety # # *NOTE:* Updates to config objects are not synchronized for thread-safety. @@ -17,6 +32,7 @@ class Config def self.default; @default end include AttrAccessors + include AttrInheritance # The debug mode (boolean) # @@ -46,9 +62,11 @@ def self.default; @default end # Creates a new config object and initialize its attribute with +attrs+. # + # If +parent+ is not given, the global config is used by default. + # # If a block is given, the new config object is yielded to it. - def initialize(**attrs) - super() + def initialize(parent = nil, **attrs) + super(parent) attrs.each do send(:"#{_1}=", _2) end yield self if block_given? end diff --git a/lib/net/imap/config/attr_inheritance.rb b/lib/net/imap/config/attr_inheritance.rb new file mode 100644 index 00000000..1057c511 --- /dev/null +++ b/lib/net/imap/config/attr_inheritance.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module Net + class IMAP + class Config + # Inheritance forms a singly-linked-list, so lookup will be O(n) on the + # number of ancestors. Without customization, ancestor trees will only be + # three or four deep: + # client -> [versioned ->] global -> default + module AttrInheritance + INHERITED = Module.new.freeze + private_constant :INHERITED + + module Macros # :nodoc: internal API + def attr_accessor(name) super; AttrInheritance.attr_accessor(name) end + end + private_constant :Macros + + def self.included(mod) + mod.extend Macros + end + private_class_method :included + + def self.attr_accessor(name) # :nodoc: internal API + module_eval <<~RUBY, __FILE__, __LINE__ + 1 + def #{name}; (val = super) == INHERITED ? parent&.#{name} : val end + RUBY + end + + # The parent Config object + attr_reader :parent + + def initialize(parent = nil) # :notnew: + super() + @parent = parent + reset + end + + # Creates a new config, which inherits from +self+. + def new(**attrs) self.class.new(self, **attrs) end + + # Returns +true+ if +attr+ is inherited from #parent and not overridden + # by this config. + def inherited?(attr) data[attr] == INHERITED end + + # :call-seq: + # reset -> self + # reset(attr) -> attribute value + # + # Resets an +attr+ to inherit from the #parent config. + # + # When +attr+ is nil or not given, all attributes are reset. + def reset(attr = nil) + if attr.nil? + data.members.each do |attr| data[attr] = INHERITED end + self + elsif inherited?(attr) + nil + else + old, data[attr] = data[attr], INHERITED + old + end + end + + private + + def initialize_copy(other) + super + @parent ||= other # only default has nil parent + end + + end + end + end +end diff --git a/test/net/imap/test_config.rb b/test/net/imap/test_config.rb index 28dd44da..f20c7433 100644 --- a/test/net/imap/test_config.rb +++ b/test/net/imap/test_config.rb @@ -32,4 +32,75 @@ class ConfigTest < Test::Unit::TestCase refute default.debug? end + test ".new(parent, ...) and inheritance" do + base = Config.new debug: false + child = Config.new(base) + assert_equal base, child.parent + assert_equal false, child.debug + assert_equal false, child.debug? + base.debug = true + assert_equal true, child.debug? + child.debug = false + assert_equal false, child.debug? + child.reset(:debug) + assert_equal true, child.debug? + base.debug = false + child.debug = true + assert_equal true, child.debug? + child = Config.new(base, debug: true) + assert_equal true, child.debug? + base.debug = true + child = Config.new(base, debug: false) + assert_equal false, child.debug? + end + + test "#new and inheritance" do + base = Config.new debug: false + child = base.new + assert_equal base, child.parent + assert_equal false, child.debug + assert_equal false, child.debug? + base.debug = true + assert_equal true, child.debug? + child.debug = false + assert_equal false, child.debug? + child.reset(:debug) + assert_equal true, child.debug? + base.debug = false + child.debug = true + assert_equal true, child.debug? + child = base.new(debug: true) + assert_equal true, child.debug? + base.debug = true + child = base.new(debug: false) + assert_equal false, child.debug? + end + + test "#inherited? and #reset(attr)" do + base = Config.new debug: false, open_timeout: 99, idle_response_timeout: 15 + child = base.new debug: true, open_timeout: 15, idle_response_timeout: 10 + refute child.inherited?(:idle_response_timeout) + assert_equal 10, child.reset(:idle_response_timeout) + assert child.inherited?(:idle_response_timeout) + assert_equal 15, child.idle_response_timeout + refute child.inherited?(:open_timeout) + refute child.inherited?(:debug) + child.debug = false + refute child.inherited?(:debug) + assert_equal false, child.reset(:debug) + assert child.inherited?(:debug) + assert_equal false, child.debug + assert_equal nil, child.reset(:debug) + end + + test "#reset all attributes" do + base = Config.new debug: false, open_timeout: 99, idle_response_timeout: 15 + child = base.new debug: true, open_timeout: 15, idle_response_timeout: 10 + result = child.reset + assert_same child, result + assert child.inherited?(:debug) + assert child.inherited?(:open_timeout) + assert child.inherited?(:idle_response_timeout) + end + end From fd518a5b0e8d20a8a3d6dfde76c080a13b57eb70 Mon Sep 17 00:00:00 2001 From: nick evans Date: Sat, 8 Jun 2024 23:18:53 -0400 Subject: [PATCH 05/11] =?UTF-8?q?=F0=9F=94=A7=20Add=20a=20global=20config?= =?UTF-8?q?=20object?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Config.global inherits from Config.default --- lib/net/imap.rb | 3 +++ lib/net/imap/config.rb | 21 +++++++++++++-- lib/net/imap/config/attr_inheritance.rb | 2 +- test/net/imap/test_config.rb | 27 +++++++++++++++++++ .../imap/test_deprecated_client_options.rb | 1 + test/net/imap/test_imap.rb | 1 + test/net/imap/test_imap_capabilities.rb | 1 + test/net/imap/test_imap_response_data.rb | 1 + test/net/imap/test_imap_response_parser.rb | 1 + 9 files changed, 55 insertions(+), 3 deletions(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index af55d699..62fb5333 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -737,6 +737,9 @@ class IMAP < Protocol include SSL end + # Returns the global Config object + def self.config; Config.global end + # Returns the debug mode. def self.debug return @@debug diff --git a/lib/net/imap/config.rb b/lib/net/imap/config.rb index df31b18d..853e23c4 100644 --- a/lib/net/imap/config.rb +++ b/lib/net/imap/config.rb @@ -8,12 +8,14 @@ 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. # # ## Inheritance # # Configs have a parent[rdoc-ref:Config::AttrInheritance#parent] config, and # any attributes which have not been set locally will inherit the parent's - # value. + # value. Config.global inherits from Config.default. # # See the following methods, defined by Config::AttrInheritance: # - {#new}[rdoc-ref:Config::AttrInheritance#reset] -- create a new config @@ -31,6 +33,19 @@ class Config # The default config, which is hardcoded and frozen. def self.default; @default end + # The global config object. + def self.global; @global end + + def self.[](config) # :nodoc: unfinished API + if config.is_a?(Config) || config.nil? && global.nil? + config + else + raise TypeError, "no implicit conversion of %s to %s" % [ + config.class, Config + ] + end + end + include AttrAccessors include AttrInheritance @@ -65,7 +80,7 @@ def self.default; @default end # If +parent+ is not given, the global config is used by default. # # If a block is given, the new config object is yielded to it. - def initialize(parent = nil, **attrs) + def initialize(parent = Config.global, **attrs) super(parent) attrs.each do send(:"#{_1}=", _2) end yield self if block_given? @@ -77,6 +92,8 @@ def initialize(parent = nil, **attrs) idle_response_timeout: 5, ).freeze + @global = default.new + end end end diff --git a/lib/net/imap/config/attr_inheritance.rb b/lib/net/imap/config/attr_inheritance.rb index 1057c511..be68a8cb 100644 --- a/lib/net/imap/config/attr_inheritance.rb +++ b/lib/net/imap/config/attr_inheritance.rb @@ -32,7 +32,7 @@ def #{name}; (val = super) == INHERITED ? parent&.#{name} : val end def initialize(parent = nil) # :notnew: super() - @parent = parent + @parent = Config[parent] reset end diff --git a/test/net/imap/test_config.rb b/test/net/imap/test_config.rb index f20c7433..208c09df 100644 --- a/test/net/imap/test_config.rb +++ b/test/net/imap/test_config.rb @@ -6,6 +6,10 @@ class ConfigTest < Test::Unit::TestCase Config = Net::IMAP::Config + setup do + Config.global.reset + end + test "#debug" do assert Config.new(debug: true).debug refute Config.new(debug: false).debug @@ -32,6 +36,23 @@ class ConfigTest < Test::Unit::TestCase refute default.debug? end + test ".global" do + global = Config.global + assert global.equal?(Config.global) + assert global.is_a?(Config) + assert_same Config.default, global.parent + assert_equal false, global.debug? + global.debug = true + assert_equal true, global.debug? + global.reset(:debug) + assert_equal false, global.debug? + refute global.frozen? + end + + test "Net::IMAP.config" do + assert Net::IMAP.config.equal?(Config.global) + end + test ".new(parent, ...) and inheritance" do base = Config.new debug: false child = Config.new(base) @@ -76,6 +97,12 @@ class ConfigTest < Test::Unit::TestCase assert_equal false, child.debug? end + test ".new always sets a parent" do + assert_same Config.global, Config.new.parent + assert_same Config.default, Config.new(Config.default).parent + assert_same Config.global, Config.new(Config.global).parent + end + test "#inherited? and #reset(attr)" do base = Config.new debug: false, open_timeout: 99, idle_response_timeout: 15 child = base.new debug: true, open_timeout: 15, idle_response_timeout: 10 diff --git a/test/net/imap/test_deprecated_client_options.rb b/test/net/imap/test_deprecated_client_options.rb index 1dcdc339..042bf90f 100644 --- a/test/net/imap/test_deprecated_client_options.rb +++ b/test/net/imap/test_deprecated_client_options.rb @@ -8,6 +8,7 @@ class DeprecatedClientOptionsTest < 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 = [] diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb index 0078956d..30012666 100644 --- a/test/net/imap/test_imap.rb +++ b/test/net/imap/test_imap.rb @@ -12,6 +12,7 @@ class IMAPTest < 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 = [] diff --git a/test/net/imap/test_imap_capabilities.rb b/test/net/imap/test_imap_capabilities.rb index 14e1f79b..252a7d9b 100644 --- a/test/net/imap/test_imap_capabilities.rb +++ b/test/net/imap/test_imap_capabilities.rb @@ -9,6 +9,7 @@ class IMAPCapabilitiesTest < 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 = [] diff --git a/test/net/imap/test_imap_response_data.rb b/test/net/imap/test_imap_response_data.rb index dc546432..0bee8b9a 100644 --- a/test/net/imap/test_imap_response_data.rb +++ b/test/net/imap/test_imap_response_data.rb @@ -6,6 +6,7 @@ class IMAPResponseDataTest < Test::Unit::TestCase def setup + Net::IMAP.config.reset @do_not_reverse_lookup = Socket.do_not_reverse_lookup Socket.do_not_reverse_lookup = true end diff --git a/test/net/imap/test_imap_response_parser.rb b/test/net/imap/test_imap_response_parser.rb index 0d4682cd..a4d70f92 100644 --- a/test/net/imap/test_imap_response_parser.rb +++ b/test/net/imap/test_imap_response_parser.rb @@ -11,6 +11,7 @@ class IMAPResponseParserTest < Test::Unit::TestCase extend NetIMAPTestHelpers::TestFixtureGenerators def setup + Net::IMAP.config.reset @do_not_reverse_lookup = Socket.do_not_reverse_lookup Socket.do_not_reverse_lookup = true end From 716804f47c932fbf54ef89df8067120b5c233b5e Mon Sep 17 00:00:00 2001 From: nick evans Date: Thu, 30 May 2024 09:55:11 -0400 Subject: [PATCH 06/11] =?UTF-8?q?=F0=9F=94=A7=F0=9F=8F=B7=20Add=20type=20c?= =?UTF-8?q?oercion=20for=20config=20attributes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is implemented as another module included under the other two, still based on overriding `attr_accessor`. Types are only enforced by the attr_writer methods. Fortunately, rdoc isn't confused by keyword arguments to `attr_accessor`, so the type can be added to that. Currently, only `:boolean` and `Integer` are supported, but it should be easy to add more. --- lib/net/imap/config.rb | 14 +++++-- lib/net/imap/config/attr_type_coercion.rb | 45 +++++++++++++++++++++++ test/net/imap/test_config.rb | 22 +++++++++++ 3 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 lib/net/imap/config/attr_type_coercion.rb diff --git a/lib/net/imap/config.rb b/lib/net/imap/config.rb index 853e23c4..a58a4ca8 100644 --- a/lib/net/imap/config.rb +++ b/lib/net/imap/config.rb @@ -3,6 +3,7 @@ require_relative "config/attr_accessors" require_relative "config/attr_inheritance" +require_relative "config/attr_type_coercion" module Net class IMAP @@ -48,14 +49,19 @@ def self.[](config) # :nodoc: unfinished API include AttrAccessors include AttrInheritance + include AttrTypeCoercion # The debug mode (boolean) # # | Starting with version | The default value is | # |-----------------------|----------------------| # | _original_ | +false+ | - attr_accessor :debug - alias debug? debug + attr_accessor :debug, type: :boolean + + # method: debug? + # :call-seq: debug? -> boolean + # + # Alias for #debug # Seconds to wait until a connection is opened. # @@ -65,7 +71,7 @@ def self.[](config) # :nodoc: unfinished API # | Starting with version | The default value is | # |-----------------------|----------------------| # | _original_ | +30+ seconds | - attr_accessor :open_timeout + attr_accessor :open_timeout, type: Integer # Seconds to wait until an IDLE response is received, after # the client asks to leave the IDLE state. See Net::IMAP#idle_done. @@ -73,7 +79,7 @@ def self.[](config) # :nodoc: unfinished API # | Starting with version | The default value is | # |-----------------------|----------------------| # | _original_ | +5+ seconds | - attr_accessor :idle_response_timeout + attr_accessor :idle_response_timeout, type: Integer # Creates a new config object and initialize its attribute with +attrs+. # diff --git a/lib/net/imap/config/attr_type_coercion.rb b/lib/net/imap/config/attr_type_coercion.rb new file mode 100644 index 00000000..7511193d --- /dev/null +++ b/lib/net/imap/config/attr_type_coercion.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Net + class IMAP + class Config + # Adds a +type+ keyword parameter to +attr_accessor+, which enforces + # config attributes have valid types, for example: boolean, numeric, + # enumeration, non-nullable, etc. + module AttrTypeCoercion + # :stopdoc: internal APIs only + + module Macros # :nodoc: internal API + def attr_accessor(attr, type: nil) + super(attr) + AttrTypeCoercion.attr_accessor(attr, type: type) + end + end + private_constant :Macros + + def self.included(mod) + mod.extend Macros + end + private_class_method :included + + def self.attr_accessor(attr, type: nil) + return unless type + if :boolean == type then boolean attr + elsif Integer == type then integer attr + else raise ArgumentError, "unknown type coercion %p" % [type] + end + end + + def self.boolean(attr) + define_method :"#{attr}=" do |val| super !!val end + define_method :"#{attr}?" do send attr end + end + + def self.integer(attr) + define_method :"#{attr}=" do |val| super Integer val end + end + + end + end + end +end diff --git a/test/net/imap/test_config.rb b/test/net/imap/test_config.rb index 208c09df..e14d7b74 100644 --- a/test/net/imap/test_config.rb +++ b/test/net/imap/test_config.rb @@ -28,6 +28,28 @@ class ConfigTest < Test::Unit::TestCase refute config.debug? end + test "boolean type constraints and conversion" do + config = Config.new + config.debug = 111 + assert_equal true, config.debug + config.debug = nil + assert_equal false, config.debug + end + + test "integer type constraints and conversion" do + config = Config.new + config.open_timeout = "111" + assert_equal 111, config.open_timeout + config.open_timeout = 222.0 + assert_equal 222, config.open_timeout + config.open_timeout = 333.3 + assert_equal 333, config.open_timeout + assert_raise(ArgumentError) do + config.open_timeout = "444 NaN" + end + assert_equal 333, config.open_timeout + end + test ".default" do default = Config.default assert default.equal?(Config.default) From 06677aa3bc3375cad991a7e45cf6c788567b839e Mon Sep 17 00:00:00 2001 From: nick evans Date: Sat, 25 May 2024 10:03:09 -0400 Subject: [PATCH 07/11] =?UTF-8?q?=F0=9F=94=A7=20Add=20client=20config=20(c?= =?UTF-8?q?urrently=20unused)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An (unused) client config object has been added to both Net::IMAP and to ResponseParser. Every client creates its own config object. By default, the client config inherits from the global config. Unlike the client, which always creates a new Config object, the parser simply shares the client's config. --- lib/net/imap.rb | 20 ++++++++++++++++++-- lib/net/imap/config.rb | 8 ++++++-- lib/net/imap/response_parser.rb | 5 ++++- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 62fb5333..7eaee02b 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -769,6 +769,11 @@ class << self # Returns the initial greeting the server, an UntaggedResponse. attr_reader :greeting + # The client configuration. See Net::IMAP::Config. + # + # By default, config inherits from the global Net::IMAP.config. + attr_reader :config + # Seconds to wait until a connection is opened. # If the IMAP object cannot open a connection within this time, # it raises a Net::OpenTimeout exception. The default value is 30 seconds. @@ -816,11 +821,20 @@ class << self # the keys are names of attribute assignment methods on # SSLContext[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html]. # + # [config] + # A Net::IMAP::Config object to base the client #config on. By default + # the global Net::IMAP.config is used. Note that this sets the _parent_ + # config object for inheritance. Every Net::IMAP client has its own + # unique #config for overrides. + # # [open_timeout] # Seconds to wait until a connection is opened # [idle_response_timeout] # Seconds to wait until an IDLE response is received # + # Any other keyword arguments will be forwarded to Config.new, to create the + # client's #config. + # # See DeprecatedClientOptions.new for deprecated arguments. # # ==== Examples @@ -877,10 +891,12 @@ class << self # Connected to the host successfully, but it immediately said goodbye. # def initialize(host, port: nil, ssl: nil, - open_timeout: 30, idle_response_timeout: 5) + open_timeout: 30, idle_response_timeout: 5, + config: Config.global, **config_options) super() # Config options @host = host + @config = Config.new(config, **config_options) @port = port || (ssl ? SSL_PORT : PORT) @open_timeout = Integer(open_timeout) @idle_response_timeout = Integer(idle_response_timeout) @@ -894,7 +910,7 @@ def initialize(host, port: nil, ssl: nil, @capabilities = nil # Client Protocol Receiver - @parser = ResponseParser.new + @parser = ResponseParser.new(config: @config) @responses = Hash.new {|h, k| h[k] = [] } @response_handlers = [] @receiver_thread = nil diff --git a/lib/net/imap/config.rb b/lib/net/imap/config.rb index a58a4ca8..f8201c5c 100644 --- a/lib/net/imap/config.rb +++ b/lib/net/imap/config.rb @@ -10,13 +10,17 @@ 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. + # 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 config. # # ## Inheritance # # Configs have a parent[rdoc-ref:Config::AttrInheritance#parent] config, and # any attributes which have not been set locally will inherit the parent's - # value. Config.global inherits from Config.default. + # value. Every client creates its own specific config. By default, client + # configs inherit from Config.global which inherits from Config.default. # # See the following methods, defined by Config::AttrInheritance: # - {#new}[rdoc-ref:Config::AttrInheritance#reset] -- create a new config diff --git a/lib/net/imap/response_parser.rb b/lib/net/imap/response_parser.rb index 84f6cf79..fe6badc8 100644 --- a/lib/net/imap/response_parser.rb +++ b/lib/net/imap/response_parser.rb @@ -11,12 +11,15 @@ class ResponseParser include ParserUtils extend ParserUtils::Generator + attr_reader :config + # :call-seq: Net::IMAP::ResponseParser.new -> Net::IMAP::ResponseParser - def initialize + def initialize(config: Config.global) @str = nil @pos = nil @lex_state = nil @token = nil + @config = Config[config] end # :call-seq: From f49c3cfbf1db0cd3178164e1e434b8df0cd31392 Mon Sep 17 00:00:00 2001 From: nick evans Date: Sat, 25 May 2024 10:21:06 -0400 Subject: [PATCH 08/11] =?UTF-8?q?=F0=9F=94=A7=20Use=20global=20config=20fo?= =?UTF-8?q?r=20Net::IMAP.debug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/net/imap.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 7eaee02b..2940fff2 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -740,14 +740,12 @@ class IMAP < Protocol # Returns the global Config object def self.config; Config.global end - # Returns the debug mode. - def self.debug - return @@debug - end + # Returns the global debug mode. + def self.debug; config.debug end - # Sets the debug mode. + # Sets the global debug mode. def self.debug=(val) - return @@debug = val + config.debug = val end # The default port for IMAP connections, port 143 From f971c6c7095732933d1bcc5e3145ffecb605538d Mon Sep 17 00:00:00 2001 From: nick evans Date: Sat, 25 May 2024 08:57:11 -0400 Subject: [PATCH 09/11] =?UTF-8?q?=F0=9F=94=A7=20Use=20client=20config=20fo?= =?UTF-8?q?r=20debug=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In Net::IMAP, the `@@debug` cvar is replaced by `config.debug?`. And in ResponseParser, references to the global `Net::IMAP.debug` is replaced by `config.debug?`. By default, the client's own config won't have a value set for `debug`, so it will still inherit from the global `Net::IMAP.config.debug?`. Since the parser "belongs" to a client, it uses the client's configuration rather than simply following the global debug mode. --- lib/net/imap.rb | 6 ++---- lib/net/imap/response_parser/parser_utils.rb | 12 ++++++------ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 2940fff2..d57c2e65 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -2609,8 +2609,6 @@ def remove_response_handler(handler) PORT = 143 # :nodoc: SSL_PORT = 993 # :nodoc: - @@debug = false - def start_imap_connection @greeting = get_server_greeting @capabilities = capabilities_from_resp_code @greeting @@ -2755,7 +2753,7 @@ def get_response end end return nil if buff.length == 0 - if @@debug + if config.debug? $stderr.print(buff.gsub(/^/n, "S: ")) end return @parser.parse(buff) @@ -2834,7 +2832,7 @@ def generate_tag def put_string(str) @sock.print(str) - if @@debug + if config.debug? if @debug_output_bol $stderr.print("C: ") end diff --git a/lib/net/imap/response_parser/parser_utils.rb b/lib/net/imap/response_parser/parser_utils.rb index d458ede7..bc84bed9 100644 --- a/lib/net/imap/response_parser/parser_utils.rb +++ b/lib/net/imap/response_parser/parser_utils.rb @@ -154,7 +154,7 @@ def accept(*args) end # To be used conditionally: - # assert_no_lookahead if Net::IMAP.debug + # assert_no_lookahead if config.debug? def assert_no_lookahead @token.nil? or parse_error("assertion failed: expected @token.nil?, actual %s: %p", @@ -181,23 +181,23 @@ def lookahead!(*args) end def peek_str?(str) - assert_no_lookahead if Net::IMAP.debug + assert_no_lookahead if config.debug? @str[@pos, str.length] == str end def peek_re(re) - assert_no_lookahead if Net::IMAP.debug + assert_no_lookahead if config.debug? re.match(@str, @pos) end def accept_re(re) - assert_no_lookahead if Net::IMAP.debug + assert_no_lookahead if config.debug? re.match(@str, @pos) and @pos = $~.end(0) $~ end def match_re(re, name) - assert_no_lookahead if Net::IMAP.debug + assert_no_lookahead if config.debug? if re.match(@str, @pos) @pos = $~.end(0) $~ @@ -212,7 +212,7 @@ def shift_token def parse_error(fmt, *args) msg = format(fmt, *args) - if IMAP.debug + if config.debug? local_path = File.dirname(__dir__) tok = @token ? "%s: %p" % [@token.symbol, @token.value] : "nil" warn "%s %s: %s" % [self.class, __method__, msg] From d9fdfbe1a24e8e62697a13f122669d43f35474ca Mon Sep 17 00:00:00 2001 From: nick evans Date: Sat, 25 May 2024 12:42:27 -0400 Subject: [PATCH 10/11] =?UTF-8?q?=F0=9F=94=A7=20Use=20client=20config=20fo?= =?UTF-8?q?r=20open=5Ftimeout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/net/imap.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index d57c2e65..ae1cd4a0 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -775,7 +775,7 @@ class << self # Seconds to wait until a connection is opened. # If the IMAP object cannot open a connection within this time, # it raises a Net::OpenTimeout exception. The default value is 30 seconds. - attr_reader :open_timeout + def open_timeout; config.open_timeout end # Seconds to wait until an IDLE response is received. attr_reader :idle_response_timeout @@ -825,13 +825,14 @@ class << self # config object for inheritance. Every Net::IMAP client has its own # unique #config for overrides. # - # [open_timeout] - # Seconds to wait until a connection is opened # [idle_response_timeout] # Seconds to wait until an IDLE response is received # # Any other keyword arguments will be forwarded to Config.new, to create the - # client's #config. + # client's #config. For example: + # + # [open_timeout] + # Seconds to wait until a connection is opened # # See DeprecatedClientOptions.new for deprecated arguments. # @@ -889,14 +890,13 @@ class << self # Connected to the host successfully, but it immediately said goodbye. # def initialize(host, port: nil, ssl: nil, - open_timeout: 30, idle_response_timeout: 5, + idle_response_timeout: 5, config: Config.global, **config_options) super() # Config options @host = host @config = Config.new(config, **config_options) @port = port || (ssl ? SSL_PORT : PORT) - @open_timeout = Integer(open_timeout) @idle_response_timeout = Integer(idle_response_timeout) @ssl_ctx_params, @ssl_ctx = build_ssl_ctx(ssl) @@ -2636,12 +2636,12 @@ def start_receiver_thread end def tcp_socket(host, port) - s = Socket.tcp(host, port, :connect_timeout => @open_timeout) + s = Socket.tcp(host, port, :connect_timeout => open_timeout) s.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, true) s rescue Errno::ETIMEDOUT raise Net::OpenTimeout, "Timeout to open TCP connection to " + - "#{host}:#{port} (exceeds #{@open_timeout} seconds)" + "#{host}:#{port} (exceeds #{open_timeout} seconds)" end def receive_responses @@ -2959,7 +2959,7 @@ def start_tls_session @sock = SSLSocket.new(@sock, ssl_ctx) @sock.sync_close = true @sock.hostname = @host if @sock.respond_to? :hostname= - ssl_socket_connect(@sock, @open_timeout) + ssl_socket_connect(@sock, open_timeout) if ssl_ctx.verify_mode != VERIFY_NONE @sock.post_connection_check(@host) @tls_verified = true From a972bf8e19f80d3afd0f10e83ec7172c4ebbd985 Mon Sep 17 00:00:00 2001 From: nick evans Date: Sun, 19 May 2024 21:25:48 -0400 Subject: [PATCH 11/11] =?UTF-8?q?=F0=9F=94=A7=20Use=20client=20config=20fo?= =?UTF-8?q?r=20idle=5Fresponse=5Ftimeout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/net/imap.rb | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index ae1cd4a0..232b3728 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -778,7 +778,7 @@ class << self def open_timeout; config.open_timeout end # Seconds to wait until an IDLE response is received. - attr_reader :idle_response_timeout + def idle_response_timeout; config.idle_response_timeout end # The hostname this client connected to attr_reader :host @@ -825,14 +825,13 @@ def open_timeout; config.open_timeout end # config object for inheritance. Every Net::IMAP client has its own # unique #config for overrides. # - # [idle_response_timeout] - # Seconds to wait until an IDLE response is received - # # Any other keyword arguments will be forwarded to Config.new, to create the # client's #config. For example: # # [open_timeout] # Seconds to wait until a connection is opened + # [idle_response_timeout] + # Seconds to wait until an IDLE response is received # # See DeprecatedClientOptions.new for deprecated arguments. # @@ -890,14 +889,12 @@ def open_timeout; config.open_timeout end # Connected to the host successfully, but it immediately said goodbye. # def initialize(host, port: nil, ssl: nil, - idle_response_timeout: 5, config: Config.global, **config_options) super() # Config options @host = host @config = Config.new(config, **config_options) @port = port || (ssl ? SSL_PORT : PORT) - @idle_response_timeout = Integer(idle_response_timeout) @ssl_ctx_params, @ssl_ctx = build_ssl_ctx(ssl) # Basic Client State @@ -2453,7 +2450,7 @@ def idle(timeout = nil, &response_handler) unless @receiver_thread_terminating remove_response_handler(response_handler) put_string("DONE#{CRLF}") - response = get_tagged_response(tag, "IDLE", @idle_response_timeout) + response = get_tagged_response(tag, "IDLE", idle_response_timeout) end end end