Skip to content

Commit b80fd4c

Browse files
authored
🔀 Merge pull request #291 from ruby/config-class
Add Config class for `debug`, `open_timeout`, and `idle_response_timeout`
2 parents 4e16814 + a972bf8 commit b80fd4c

13 files changed

+500
-27
lines changed

lib/net/imap.rb

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,8 @@ class IMAP < Protocol
725725
"UTF8=ONLY" => "UTF8=ACCEPT",
726726
}.freeze
727727

728+
autoload :Config, File.expand_path("imap/config", __dir__)
729+
728730
autoload :SASL, File.expand_path("imap/sasl", __dir__)
729731
autoload :SASLAdapter, File.expand_path("imap/sasl_adapter", __dir__)
730732
autoload :StringPrep, File.expand_path("imap/stringprep", __dir__)
@@ -735,14 +737,15 @@ class IMAP < Protocol
735737
include SSL
736738
end
737739

738-
# Returns the debug mode.
739-
def self.debug
740-
return @@debug
741-
end
740+
# Returns the global Config object
741+
def self.config; Config.global end
742742

743-
# Sets the debug mode.
743+
# Returns the global debug mode.
744+
def self.debug; config.debug end
745+
746+
# Sets the global debug mode.
744747
def self.debug=(val)
745-
return @@debug = val
748+
config.debug = val
746749
end
747750

748751
# The default port for IMAP connections, port 143
@@ -764,13 +767,18 @@ class << self
764767
# Returns the initial greeting the server, an UntaggedResponse.
765768
attr_reader :greeting
766769

770+
# The client configuration. See Net::IMAP::Config.
771+
#
772+
# By default, config inherits from the global Net::IMAP.config.
773+
attr_reader :config
774+
767775
# Seconds to wait until a connection is opened.
768776
# If the IMAP object cannot open a connection within this time,
769777
# it raises a Net::OpenTimeout exception. The default value is 30 seconds.
770-
attr_reader :open_timeout
778+
def open_timeout; config.open_timeout end
771779

772780
# Seconds to wait until an IDLE response is received.
773-
attr_reader :idle_response_timeout
781+
def idle_response_timeout; config.idle_response_timeout end
774782

775783
# The hostname this client connected to
776784
attr_reader :host
@@ -811,6 +819,15 @@ class << self
811819
# the keys are names of attribute assignment methods on
812820
# SSLContext[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html].
813821
#
822+
# [config]
823+
# A Net::IMAP::Config object to base the client #config on. By default
824+
# the global Net::IMAP.config is used. Note that this sets the _parent_
825+
# config object for inheritance. Every Net::IMAP client has its own
826+
# unique #config for overrides.
827+
#
828+
# Any other keyword arguments will be forwarded to Config.new, to create the
829+
# client's #config. For example:
830+
#
814831
# [open_timeout]
815832
# Seconds to wait until a connection is opened
816833
# [idle_response_timeout]
@@ -872,13 +889,12 @@ class << self
872889
# Connected to the host successfully, but it immediately said goodbye.
873890
#
874891
def initialize(host, port: nil, ssl: nil,
875-
open_timeout: 30, idle_response_timeout: 5)
892+
config: Config.global, **config_options)
876893
super()
877894
# Config options
878895
@host = host
896+
@config = Config.new(config, **config_options)
879897
@port = port || (ssl ? SSL_PORT : PORT)
880-
@open_timeout = Integer(open_timeout)
881-
@idle_response_timeout = Integer(idle_response_timeout)
882898
@ssl_ctx_params, @ssl_ctx = build_ssl_ctx(ssl)
883899

884900
# Basic Client State
@@ -889,7 +905,7 @@ def initialize(host, port: nil, ssl: nil,
889905
@capabilities = nil
890906

891907
# Client Protocol Receiver
892-
@parser = ResponseParser.new
908+
@parser = ResponseParser.new(config: @config)
893909
@responses = Hash.new {|h, k| h[k] = [] }
894910
@response_handlers = []
895911
@receiver_thread = nil
@@ -2434,7 +2450,7 @@ def idle(timeout = nil, &response_handler)
24342450
unless @receiver_thread_terminating
24352451
remove_response_handler(response_handler)
24362452
put_string("DONE#{CRLF}")
2437-
response = get_tagged_response(tag, "IDLE", @idle_response_timeout)
2453+
response = get_tagged_response(tag, "IDLE", idle_response_timeout)
24382454
end
24392455
end
24402456
end
@@ -2590,8 +2606,6 @@ def remove_response_handler(handler)
25902606
PORT = 143 # :nodoc:
25912607
SSL_PORT = 993 # :nodoc:
25922608

2593-
@@debug = false
2594-
25952609
def start_imap_connection
25962610
@greeting = get_server_greeting
25972611
@capabilities = capabilities_from_resp_code @greeting
@@ -2619,12 +2633,12 @@ def start_receiver_thread
26192633
end
26202634

26212635
def tcp_socket(host, port)
2622-
s = Socket.tcp(host, port, :connect_timeout => @open_timeout)
2636+
s = Socket.tcp(host, port, :connect_timeout => open_timeout)
26232637
s.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, true)
26242638
s
26252639
rescue Errno::ETIMEDOUT
26262640
raise Net::OpenTimeout, "Timeout to open TCP connection to " +
2627-
"#{host}:#{port} (exceeds #{@open_timeout} seconds)"
2641+
"#{host}:#{port} (exceeds #{open_timeout} seconds)"
26282642
end
26292643

26302644
def receive_responses
@@ -2736,7 +2750,7 @@ def get_response
27362750
end
27372751
end
27382752
return nil if buff.length == 0
2739-
if @@debug
2753+
if config.debug?
27402754
$stderr.print(buff.gsub(/^/n, "S: "))
27412755
end
27422756
return @parser.parse(buff)
@@ -2815,7 +2829,7 @@ def generate_tag
28152829

28162830
def put_string(str)
28172831
@sock.print(str)
2818-
if @@debug
2832+
if config.debug?
28192833
if @debug_output_bol
28202834
$stderr.print("C: ")
28212835
end
@@ -2942,7 +2956,7 @@ def start_tls_session
29422956
@sock = SSLSocket.new(@sock, ssl_ctx)
29432957
@sock.sync_close = true
29442958
@sock.hostname = @host if @sock.respond_to? :hostname=
2945-
ssl_socket_connect(@sock, @open_timeout)
2959+
ssl_socket_connect(@sock, open_timeout)
29462960
if ssl_ctx.verify_mode != VERIFY_NONE
29472961
@sock.post_connection_check(@host)
29482962
@tls_verified = true

lib/net/imap/config.rb

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# frozen_string_literal: true
2+
# :markup: markdown
3+
4+
require_relative "config/attr_accessors"
5+
require_relative "config/attr_inheritance"
6+
require_relative "config/attr_type_coercion"
7+
8+
module Net
9+
class IMAP
10+
11+
# Net::IMAP::Config stores configuration options for Net::IMAP clients.
12+
# The global configuration can be seen at either Net::IMAP.config or
13+
# Net::IMAP::Config.global, and the client-specific configuration can be
14+
# seen at Net::IMAP#config. When creating a new client, all unhandled
15+
# keyword arguments to Net::IMAP.new are delegated to Config.new. Every
16+
# client has its own config.
17+
#
18+
# ## Inheritance
19+
#
20+
# Configs have a parent[rdoc-ref:Config::AttrInheritance#parent] config, and
21+
# any attributes which have not been set locally will inherit the parent's
22+
# value. Every client creates its own specific config. By default, client
23+
# configs inherit from Config.global which inherits from Config.default.
24+
#
25+
# See the following methods, defined by Config::AttrInheritance:
26+
# - {#new}[rdoc-ref:Config::AttrInheritance#reset] -- create a new config
27+
# which inherits from the receiver.
28+
# - {#inherited?}[rdoc-ref:Config::AttrInheritance#inherited?] -- return
29+
# whether a particular attribute is inherited.
30+
# - {#reset}[rdoc-ref:Config::AttrInheritance#reset] -- reset attributes to
31+
# be inherited.
32+
#
33+
# ## Thread Safety
34+
#
35+
# *NOTE:* Updates to config objects are not synchronized for thread-safety.
36+
#
37+
class Config
38+
# The default config, which is hardcoded and frozen.
39+
def self.default; @default end
40+
41+
# The global config object.
42+
def self.global; @global end
43+
44+
def self.[](config) # :nodoc: unfinished API
45+
if config.is_a?(Config) || config.nil? && global.nil?
46+
config
47+
else
48+
raise TypeError, "no implicit conversion of %s to %s" % [
49+
config.class, Config
50+
]
51+
end
52+
end
53+
54+
include AttrAccessors
55+
include AttrInheritance
56+
include AttrTypeCoercion
57+
58+
# The debug mode (boolean)
59+
#
60+
# | Starting with version | The default value is |
61+
# |-----------------------|----------------------|
62+
# | _original_ | +false+ |
63+
attr_accessor :debug, type: :boolean
64+
65+
# method: debug?
66+
# :call-seq: debug? -> boolean
67+
#
68+
# Alias for #debug
69+
70+
# Seconds to wait until a connection is opened.
71+
#
72+
# If the IMAP object cannot open a connection within this time,
73+
# it raises a Net::OpenTimeout exception. See Net::IMAP.new.
74+
#
75+
# | Starting with version | The default value is |
76+
# |-----------------------|----------------------|
77+
# | _original_ | +30+ seconds |
78+
attr_accessor :open_timeout, type: Integer
79+
80+
# Seconds to wait until an IDLE response is received, after
81+
# the client asks to leave the IDLE state. See Net::IMAP#idle_done.
82+
#
83+
# | Starting with version | The default value is |
84+
# |-----------------------|----------------------|
85+
# | _original_ | +5+ seconds |
86+
attr_accessor :idle_response_timeout, type: Integer
87+
88+
# Creates a new config object and initialize its attribute with +attrs+.
89+
#
90+
# If +parent+ is not given, the global config is used by default.
91+
#
92+
# If a block is given, the new config object is yielded to it.
93+
def initialize(parent = Config.global, **attrs)
94+
super(parent)
95+
attrs.each do send(:"#{_1}=", _2) end
96+
yield self if block_given?
97+
end
98+
99+
@default = new(
100+
debug: false,
101+
open_timeout: 30,
102+
idle_response_timeout: 5,
103+
).freeze
104+
105+
@global = default.new
106+
107+
end
108+
end
109+
end

lib/net/imap/config/attr_accessors.rb

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# frozen_string_literal: true
2+
3+
require "forwardable"
4+
5+
module Net
6+
class IMAP
7+
class Config
8+
9+
# Config values are stored in a struct rather than ivars to simplify:
10+
# * ensuring that all config objects share a single object shape
11+
# * querying only locally configured values, e.g for inspection.
12+
module AttrAccessors
13+
module Macros # :nodoc: internal API
14+
def attr_accessor(name) AttrAccessors.attr_accessor(name) end
15+
end
16+
private_constant :Macros
17+
18+
def self.included(mod)
19+
mod.extend Macros
20+
end
21+
private_class_method :included
22+
23+
extend Forwardable
24+
25+
def self.attr_accessor(name) # :nodoc: internal API
26+
name = name.to_sym
27+
def_delegators :data, name, :"#{name}="
28+
end
29+
30+
def self.attributes
31+
instance_methods.grep(/=\z/).map { _1.to_s.delete_suffix("=").to_sym }
32+
end
33+
private_class_method :attributes
34+
35+
def self.struct # :nodoc: internal API
36+
unless defined?(self::Struct)
37+
const_set :Struct, Struct.new(*attributes)
38+
end
39+
self::Struct
40+
end
41+
42+
def initialize # :notnew:
43+
super()
44+
@data = AttrAccessors.struct.new
45+
end
46+
47+
# Freezes the internal attributes struct, in addition to +self+.
48+
def freeze
49+
data.freeze
50+
super
51+
end
52+
53+
protected
54+
55+
attr_reader :data # :nodoc: internal API
56+
57+
private
58+
59+
def initialize_dup(other)
60+
super
61+
@data = other.data.dup
62+
end
63+
64+
end
65+
end
66+
end
67+
end

0 commit comments

Comments
 (0)