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

✨ New config option to return frozen dup from #responses #334

Merged
merged 8 commits into from
Oct 13, 2024
97 changes: 79 additions & 18 deletions lib/net/imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2490,41 +2490,98 @@ def idle_done
end
end

RESPONSES_DEPRECATION_MSG =
"Pass a type or block to #responses, " \
"set config.responses_without_block to :frozen_dup " \
"or :silence_deprecation_warning, " \
"or use #extract_responses or #clear_responses."
private_constant :RESPONSES_DEPRECATION_MSG

# :call-seq:
# responses -> hash of {String => Array} (see config.responses_without_block)
# responses(type) -> frozen array
# responses {|hash| ...} -> block result
# responses(type) {|array| ...} -> block result
#
# Yields unhandled responses and returns the result of the block.
# Yields or returns unhandled server responses. Unhandled responses are
# stored in a hash, with arrays of UntaggedResponse#data keyed by
# UntaggedResponse#name and <em>non-+nil+</em> untagged ResponseCode#data
# keyed by ResponseCode#name.
#
# When a block is given, yields unhandled responses and returns the block's
# result. Without a block, returns the unhandled responses.
#
# [With +type+]
# Yield or return only the array of responses for that +type+.
# When no block is given, the returned array is a frozen copy.
# [Without +type+]
# Yield or return the entire responses hash.
#
# When no block is given, the behavior is determined by
# Config#responses_without_block:
# >>>
# [+:silence_deprecation_warning+ <em>(original behavior)</em>]
# Returns the mutable responses hash (without any warnings).
# <em>This is not thread-safe.</em>
#
# [+:warn+ <em>(default since +v0.5+)</em>]
# Prints a warning and returns the mutable responses hash.
# <em>This is not thread-safe.</em>
#
# [+:frozen_dup+ <em>(planned default for +v0.6+)</em>]
# Returns a frozen copy of the unhandled responses hash, with frozen
# array values.
#
# Unhandled responses are stored in a hash, with arrays of
# <em>non-+nil+</em> UntaggedResponse#data keyed by UntaggedResponse#name
# and ResponseCode#data keyed by ResponseCode#name. Call without +type+ to
# yield the entire responses hash. Call with +type+ to yield only the array
# of responses for that type.
# [+:raise+]
# Raise an +ArgumentError+ with the deprecation warning.
#
# For example:
#
# imap.select("inbox")
# p imap.responses("EXISTS", &:last)
# p imap.responses("EXISTS").last
# #=> 2
# p imap.responses("UIDNEXT", &:last)
# #=> 123456
# p imap.responses("UIDVALIDITY", &:last)
# #=> 968263756
# p imap.responses {|responses|
# {
# exists: responses.delete("EXISTS").last,
# uidnext: responses.delete("UIDNEXT").last,
# uidvalidity: responses.delete("UIDVALIDITY").last,
# }
# }
# #=> {:exists=>2, :uidnext=>123456, :uidvalidity=>968263756}
# # "EXISTS", "UIDNEXT", and "UIDVALIDITY" have been removed:
# p imap.responses(&:keys)
# #=> ["FLAGS", "OK", "PERMANENTFLAGS", "RECENT", "HIGHESTMODSEQ"]
#
# Related: #extract_responses, #clear_responses, #response_handlers, #greeting
#
# ===== Thread safety
# >>>
# *Note:* Access to the responses hash is synchronized for thread-safety.
# The receiver thread and response_handlers cannot process new responses
# until the block completes. Accessing either the response hash or its
# response type arrays outside of the block is unsafe.
# response type arrays outside of the block is unsafe. They can be safely
# updated inside the block. Consider using #clear_responses or
# #extract_responses instead.
#
# Net::IMAP will add and remove responses from the responses hash and its
# array values, in the calling threads for commands and in the receiver
# thread, but will not modify any responses after adding them to the
# responses hash.
#
# Calling without a block is unsafe and deprecated. Future releases will
# raise ArgumentError unless a block is given.
# See Config#responses_without_block.
# ===== Clearing responses
#
# Previously unhandled responses are automatically cleared before entering a
# mailbox with #select or #examine. Long-lived connections can receive many
# unhandled server responses, which must be pruned or they will continually
# consume more memory. Update or clear the responses hash or arrays inside
# the block, or use #clear_responses.
# the block, or remove responses with #extract_responses, #clear_responses,
# or #add_response_handler.
#
# ===== Missing responses
#
# Only non-+nil+ data is stored. Many important response codes have no data
# of their own, but are used as "tags" on the ResponseText object they are
Expand All @@ -2535,20 +2592,24 @@ def idle_done
# ResponseCode#data on tagged responses. Although some command methods do
# return the TaggedResponse directly, #add_response_handler must be used to
# handle all response codes.
#
# Related: #extract_responses, #clear_responses, #response_handlers, #greeting
def responses(type = nil)
if block_given?
synchronize { yield(type ? @responses[type.to_s.upcase] : @responses) }
elsif type
raise ArgumentError, "Pass a block or use #clear_responses"
synchronize { @responses[type.to_s.upcase].dup.freeze }
else
case config.responses_without_block
when :raise
raise ArgumentError, "Pass a block or use #clear_responses"
raise ArgumentError, RESPONSES_DEPRECATION_MSG
when :warn
warn("DEPRECATED: pass a block or use #clear_responses",
uplevel: 1, category: :deprecated)
warn(RESPONSES_DEPRECATION_MSG, uplevel: 1, category: :deprecated)
when :frozen_dup
synchronize {
responses = @responses.transform_values(&:freeze)
responses.default_proc = nil
responses.default = [].freeze
return responses.freeze
}
end
@responses
end
Expand Down
110 changes: 70 additions & 40 deletions lib/net/imap/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
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.
# Net::IMAP::Config <em>(available since +v0.4.13+)</em> 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
Expand Down Expand Up @@ -128,7 +128,7 @@ 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.
# A hash of hard-coded configurations, indexed by version number or name.
def self.version_defaults; @version_defaults end
@version_defaults = {}

Expand Down Expand Up @@ -172,9 +172,16 @@ def self.[](config)
include AttrInheritance
include AttrTypeCoercion

# The debug mode (boolean)
# The debug mode (boolean). The default value is +false+.
#
# 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?
Expand All @@ -200,60 +207,84 @@ def self.[](config)
# The default value is +5+ seconds.
attr_accessor :idle_response_timeout, type: Integer

# :markup: markdown
#
# Whether to use the +SASL-IR+ extension when the server and \SASL
# mechanism both support it.
# mechanism both support it. Can be overridden by the +sasl_ir+ keyword
# parameter to Net::IMAP#authenticate.
#
# <em>(Support for +SASL-IR+ was added in +v0.4.0+.)</em>
#
# See Net::IMAP#authenticate.
# ==== Valid options
#
# | Starting with version | The default value is |
# |-----------------------|------------------------------------------|
# | _original_ | +false+ <em>(extension unsupported)</em> |
# | v0.4 | +true+ <em>(support added)</em> |
# [+false+ <em>(original behavior, before support was added)</em>]
# Do not use +SASL-IR+, even when it is supported by the server and the
# mechanism.
#
# [+true+ <em>(default since +v0.4+)</em>]
# Use +SASL-IR+ when it is supported by the server and the mechanism.
attr_accessor :sasl_ir, type: :boolean

# :markup: markdown
#
# Controls the behavior of Net::IMAP#login when the `LOGINDISABLED`
# 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. Valid values are:
# LoginDisabledError when that capability is present.
#
# [+false+]
# <em>(Support for +LOGINDISABLED+ was added in +v0.5.0+.)</em>
#
# ==== Valid options
#
# [+false+ <em>(original behavior, before support was added)</em>]
# 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+]
# [+true+ <em>(default since +v0.5+)</em>]
# 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+.
#
# | Starting with version | The default value is |
# |-------------------------|--------------------------------|
# | _original_ | `false` |
# | v0.5 | `true` |
attr_accessor :enforce_logindisabled, type: [
false, :when_capabilities_cached, true
]

# :markup: markdown
# Controls the behavior of Net::IMAP#responses when called without any
# arguments (+type+ or +block+).
#
# ==== Valid options
#
# [+:silence_deprecation_warning+ <em>(original behavior)</em>]
# Returns the mutable responses hash (without any warnings).
# <em>This is not thread-safe.</em>
#
# [+:warn+ <em>(default since +v0.5+)</em>]
# Prints a warning and returns the mutable responses hash.
# <em>This is not thread-safe.</em>
#
# Controls the behavior of Net::IMAP#responses when called without a
# block. Valid options are `:warn`, `:raise`, or
# `:silence_deprecation_warning`.
# [+:frozen_dup+ <em>(planned default for +v0.6+)</em>]
# Returns a frozen copy of the unhandled responses hash, with frozen
# array values.
#
# | Starting with version | The default value is |
# |-------------------------|--------------------------------|
# | v0.4.13 | +:silence_deprecation_warning+ |
# | v0.5 | +:warn+ |
# | _eventually_ | +:raise+ |
# Note that calling IMAP#responses with a +type+ and without a block is
# not configurable and always behaves like +:frozen_dup+.
#
# <em>(+:frozen_dup+ config option was added in +v0.4.17+)</em>
#
# [+: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, :raise,
: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.
Expand Down Expand Up @@ -357,12 +388,11 @@ def defaults_hash

version_defaults[0.5] = Config[:current]

version_defaults[0.6] = Config[0.5]
version_defaults[:next] = Config[0.6]

version_defaults[:future] = Config[0.6].dup.update(
responses_without_block: :raise,
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
Expand Down
13 changes: 13 additions & 0 deletions test/lib/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,16 @@
require "core_assertions"

Test::Unit::TestCase.include Test::Unit::CoreAssertions

class Test::Unit::TestCase
def wait_for_response_count(imap, type:, count:,
timeout: 0.5, interval: 0.001)
deadline = Time.now + timeout
loop do
current_count = imap.responses(type, &:size)
break :count if count <= current_count
break :deadline if deadline < Time.now
sleep interval
end
end
end
2 changes: 1 addition & 1 deletion test/net/imap/test_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ class ConfigTest < Test::Unit::TestCase
assert_same Config.default, Config.new(Config.default).parent
assert_same Config.global, Config.new(Config.global).parent
assert_same Config[0.4], Config.new(0.4).parent
assert_same Config[0.5], Config.new(:next).parent
assert_same Config[0.6], Config.new(:next).parent
assert_equal true, Config.new({debug: true}, debug: false).parent.debug?
assert_equal true, Config.new({debug: true}, debug: false).parent.frozen?
end
Expand Down
Loading