Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Config class for debug, open_timeout, and idle_response_timeout #291

Merged
merged 11 commits into from
Jun 14, 2024
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