Skip to content

Commit

Permalink
🔀 Merge pull request #291 from ruby/config-class
Browse files Browse the repository at this point in the history
Add Config class for `debug`, `open_timeout`, and `idle_response_timeout`
  • Loading branch information
nevans authored Jun 14, 2024
2 parents 4e16814 + a972bf8 commit b80fd4c
Show file tree
Hide file tree
Showing 13 changed files with 500 additions and 27 deletions.
54 changes: 34 additions & 20 deletions lib/net/imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand All @@ -735,14 +737,15 @@ class IMAP < Protocol
include SSL
end

# Returns the debug mode.
def self.debug
return @@debug
end
# Returns the global Config object
def self.config; Config.global end

# Sets the debug mode.
# Returns the global debug mode.
def self.debug; config.debug end

# Sets the global debug mode.
def self.debug=(val)
return @@debug = val
config.debug = val
end

# The default port for IMAP connections, port 143
Expand All @@ -764,13 +767,18 @@ 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.
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
def idle_response_timeout; config.idle_response_timeout end

# The hostname this client connected to
attr_reader :host
Expand Down Expand Up @@ -811,6 +819,15 @@ 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.
#
# 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]
Expand Down Expand Up @@ -872,13 +889,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)
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)

# Basic Client State
Expand All @@ -889,7 +905,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
Expand Down Expand Up @@ -2434,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
Expand Down Expand Up @@ -2590,8 +2606,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
Expand Down Expand Up @@ -2619,12 +2633,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
Expand Down Expand Up @@ -2736,7 +2750,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)
Expand Down Expand Up @@ -2815,7 +2829,7 @@ def generate_tag

def put_string(str)
@sock.print(str)
if @@debug
if config.debug?
if @debug_output_bol
$stderr.print("C: ")
end
Expand Down Expand Up @@ -2942,7 +2956,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
Expand Down
109 changes: 109 additions & 0 deletions lib/net/imap/config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# frozen_string_literal: true
# :markup: markdown

require_relative "config/attr_accessors"
require_relative "config/attr_inheritance"
require_relative "config/attr_type_coercion"

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. 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. 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
# 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.
#
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
include AttrTypeCoercion

# The debug mode (boolean)
#
# | Starting with version | The default value is |
# |-----------------------|----------------------|
# | _original_ | +false+ |
attr_accessor :debug, type: :boolean

# method: debug?
# :call-seq: debug? -> boolean
#
# Alias for #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.
#
# | Starting with version | The default value is |
# |-----------------------|----------------------|
# | _original_ | +30+ seconds |
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.
#
# | Starting with version | The default value is |
# |-----------------------|----------------------|
# | _original_ | +5+ seconds |
attr_accessor :idle_response_timeout, type: Integer

# 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(parent = Config.global, **attrs)
super(parent)
attrs.each do send(:"#{_1}=", _2) end
yield self if block_given?
end

@default = new(
debug: false,
open_timeout: 30,
idle_response_timeout: 5,
).freeze

@global = default.new

end
end
end
67 changes: 67 additions & 0 deletions lib/net/imap/config/attr_accessors.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit b80fd4c

Please sign in to comment.