# frozen_string_literal: true
require_relative "config/attr_accessors"
require_relative "config/attr_inheritance"
require_relative "config/attr_type_coercion"
module Net
class IMAP
# 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
# config.
#
# debug_client = Net::IMAP.new(hostname, debug: true)
# quiet_client = Net::IMAP.new(hostname, debug: false)
# debug_client.config.debug? # => true
# quiet_client.config.debug? # => false
#
# == 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.
#
# plain_client = Net::IMAP.new(hostname)
# debug_client = Net::IMAP.new(hostname, debug: true)
# quiet_client = Net::IMAP.new(hostname, debug: false)
#
# plain_client.config.inherited?(:debug) # => true
# debug_client.config.inherited?(:debug) # => false
# quiet_client.config.inherited?(:debug) # => false
#
# plain_client.config.debug? # => false
# debug_client.config.debug? # => true
# quiet_client.config.debug? # => false
#
# # Net::IMAP.debug is delegated to Net::IMAP::Config.global.debug
# Net::IMAP.debug = true
# plain_client.config.debug? # => true
# debug_client.config.debug? # => true
# quiet_client.config.debug? # => false
#
# Net::IMAP.debug = false
# plain_client.config.debug = true
# plain_client.config.inherited?(:debug) # => false
# plain_client.config.debug? # => true
# plain_client.config.reset(:debug)
# plain_client.config.inherited?(:debug) # => true
# plain_client.config.debug? # => false
#
# == Versioned defaults
#
# The effective default configuration for a specific +x.y+ version of
# +net-imap+ can be loaded with the +config+ keyword argument to
# Net::IMAP.new. Requesting default configurations for previous versions
# enables extra backward compatibility with those versions:
#
# client = Net::IMAP.new(hostname, config: 0.3)
# client.config.sasl_ir # => false
# client.config.responses_without_block # => :silence_deprecation_warning
#
# client = Net::IMAP.new(hostname, config: 0.4)
# client.config.sasl_ir # => true
# client.config.responses_without_block # => :silence_deprecation_warning
#
# client = Net::IMAP.new(hostname, config: 0.5)
# client.config.sasl_ir # => true
# client.config.responses_without_block # => :warn
#
# client = Net::IMAP.new(hostname, config: :future)
# client.config.sasl_ir # => true
# client.config.responses_without_block # => :raise
#
# The versioned default configs inherit certain specific config options from
# Config.global, for example #debug:
#
# client = Net::IMAP.new(hostname, config: 0.4)
# Net::IMAP.debug = false
# client.config.debug? # => false
#
# Net::IMAP.debug = true
# client.config.debug? # => true
#
# Use #load_defaults to globally behave like a specific version:
# client = Net::IMAP.new(hostname)
# client.config.sasl_ir # => true
# Net::IMAP.config.load_defaults 0.3
# client.config.sasl_ir # => false
#
# === Named defaults
# In addition to +x.y+ version numbers, the following aliases are supported:
#
# [+:default+]
# An alias for +:current+.
#
# >>>
# *NOTE*: This is _not_ the same as Config.default. It inherits some
# attributes from Config.global, for example: #debug.
# [+:current+]
# An alias for the current +x.y+ version's defaults.
# [+:next+]
# The _planned_ config for the next +x.y+ version.
# [+:future+]
# The _planned_ eventual config for some future +x.y+ version.
#
# For example, to raise exceptions for all current deprecations:
# client = Net::IMAP.new(hostname, config: :future)
# client.responses # raises an ArgumentError
#
# == Thread Safety
#
# *NOTE:* Updates to config objects are not synchronized for thread-safety.
#
class Config
# Array of attribute names that are _not_ loaded by #load_defaults.
DEFAULT_TO_INHERIT = %i[debug].freeze
private_constant :DEFAULT_TO_INHERIT
# The default config, which is hardcoded and frozen.
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 or name.
def self.version_defaults; @version_defaults end
@version_defaults = {}
# :call-seq:
# Net::IMAP::Config[number] -> versioned config
# Net::IMAP::Config[symbol] -> named config
# Net::IMAP::Config[hash] -> new frozen config
# Net::IMAP::Config[config] -> same config
#
# Given a version number, returns the default configuration for the target
# version. See Config@Versioned+defaults.
#
# Given a version name, returns the default configuration for the target
# version. See Config@Named+defaults.
#
# Given a Hash, creates a new _frozen_ config which inherits from
# Config.global. Use Config.new for an unfrozen config.
#
# Given a config, returns that same config.
def self.[](config)
if config.is_a?(Config) then config
elsif config.nil? && global.nil? then nil
elsif config.respond_to?(:to_hash) then new(global, **config).freeze
else
version_defaults.fetch(config) do
case config
when Numeric
raise RangeError, "unknown config version: %p" % [config]
when Symbol
raise KeyError, "unknown config name: %p" % [config]
else
raise TypeError, "no implicit conversion of %s to %s" % [
config.class, Config
]
end
end
end
end
include AttrAccessors
include AttrInheritance
include AttrTypeCoercion
# The debug mode (boolean). 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?
# :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.
#
# The default value is +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 and Net::IMAP#idle_done.
#
# The default value is +5+ seconds.
attr_accessor :idle_response_timeout, type: Integer
# Whether to use the +SASL-IR+ extension when the server and \SASL
# 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+.)
#
# ==== Valid options
#
# [+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
# 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.
#
# (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+ (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+.
#
attr_accessor :enforce_logindisabled, type: [
false, :when_capabilities_cached, true
]
# 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.
#
# [+:frozen_dup+ (planned default for +v0.6+)]
# Returns a frozen copy of the unhandled responses hash, with frozen
# array values.
#
# 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, :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.
#
# If a block is given, the new config object is yielded to it.
def initialize(parent = Config.global, **attrs)
super(parent)
update(**attrs)
yield self if block_given?
end
# :call-seq: update(**attrs) -> self
#
# Assigns all of the provided +attrs+ to this config, and returns +self+.
#
# An ArgumentError is raised unless every key in +attrs+ matches an
# assignment method on Config.
#
# >>>
# *NOTE:* #update is not atomic. If an exception is raised due to an
# invalid attribute value, +attrs+ may be partially applied.
def update(**attrs)
unless (bad = attrs.keys.reject { respond_to?(:"#{_1}=") }).empty?
raise ArgumentError, "invalid config options: #{bad.join(", ")}"
end
attrs.each do send(:"#{_1}=", _2) end
self
end
# :call-seq:
# with(**attrs) -> config
# with(**attrs) {|config| } -> result
#
# Without a block, returns a new config which inherits from self. With a
# block, yields the new config and returns the block's result.
#
# If no keyword arguments are given, an ArgumentError will be raised.
#
# If +self+ is frozen, the copy will also be frozen.
def with(**attrs)
attrs.empty? and
raise ArgumentError, "expected keyword arguments, none given"
copy = new(**attrs)
copy.freeze if frozen?
block_given? ? yield(copy) : copy
end
# :call-seq: load_defaults(version) -> self
#
# Resets the current config to behave like the versioned default
# configuration for +version+. #parent will not be changed.
#
# Some config attributes default to inheriting from their #parent (which
# is usually Config.global) and are left unchanged, for example: #debug.
#
# See Config@Versioned+defaults and Config@Named+defaults.
def load_defaults(version)
[Numeric, Symbol, String].any? { _1 === version } or
raise ArgumentError, "expected number or symbol, got %p" % [version]
update(**Config[version].defaults_hash)
end
# :call-seq: to_h -> hash
#
# Returns all config attributes in a hash.
def to_h; data.members.to_h { [_1, send(_1)] } end
protected
def defaults_hash
to_h.reject {|k,v| DEFAULT_TO_INHERIT.include?(k) }
end
@default = new(
debug: false,
open_timeout: 30,
idle_response_timeout: 5,
sasl_ir: true,
enforce_logindisabled: true,
responses_without_block: :warn,
).freeze
@global = default.new
version_defaults[:default] = Config[default.send(:defaults_hash)]
version_defaults[:current] = Config[:default]
version_defaults[0] = Config[:current].dup.update(
sasl_ir: false,
responses_without_block: :silence_deprecation_warning,
enforce_logindisabled: false,
).freeze
version_defaults[0.0] = Config[0]
version_defaults[0.1] = Config[0]
version_defaults[0.2] = Config[0]
version_defaults[0.3] = Config[0]
version_defaults[0.4] = Config[0.3].dup.update(
sasl_ir: true,
).freeze
version_defaults[0.5] = Config[:current]
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
end
end