Skip to content

Commit 42a247d

Browse files
committed
🗑️ Add deprecation warnings to .new and #starttls [🚧 WIP]
* Moved all deprecated option handling to DeprecatedClientOptions, which is prepended to Net::IMAP. This allows `Net::IMAP` to cleanly implement and document the simplified keyword args API, while still providing a place to document the old APIs. Some weird and potentially confusing edge-cases were converted to `ArgumentError`. The very old forms are documented and will warn loudly. Some cases are obsolete but don't print any warnings... yet. * `ssl` was renamed to `tls` in most places, with backwards compatible aliases. Using `ssl` does not print any deprecation warnings. Using both `tls` and `ssl` keywords raises an ArgumentError. * ♻️ Additionally, split `initialize` up into small helper methods making it easier to understand at a glance. * Preparing for a (backwards-incompatible) secure-by-default configuration, `Net::IMAP.default_tls` will determine the value for `tls` when no explicit port or tls setting is provided. Using port 143 will be insecure by default. Using port 993 will be secure by default. Providing no explicit port will use `Net::IMAP.default_tls` with the appropriate port. And providing any other unknown port will use `default_tls` with a warning. 🚧 TODO: should we use a different config var for default tls params when port is 993 and `tls` is unspecified? 🚧 TODO: should we use a different config var for choosing `tls` when `port` is non-standard vs choosing `port` and `tls` when neither are specified? 🚧 TODO: should we use a different var for `default_tls` be used to config params when port is 993 but tls is implicit? Another var?
1 parent fb9d9ee commit 42a247d

File tree

2 files changed

+319
-116
lines changed

2 files changed

+319
-116
lines changed

lib/net/imap.rb

+153-116
Original file line numberDiff line numberDiff line change
@@ -720,11 +720,110 @@ class << self
720720
alias default_imap_port default_port
721721
alias default_imaps_port default_tls_port
722722
alias default_ssl_port default_tls_port
723+
724+
# The default value for the +tls+ option of ::new, when +port+ is
725+
# unspecified or non-standard.
726+
#
727+
# *Note*: A future release of Net::IMAP will set the default to +true+, as
728+
# per RFC7525[https://tools.ietf.org/html/rfc7525],
729+
# RFC7817[https://tools.ietf.org/html/rfc7817], and
730+
# RFC8314[https://tools.ietf.org/html/rfc8314].
731+
#
732+
# Set to +true+ for the secure default without warnings. Set to
733+
# +false+ to globally silence warnings and use insecure defaults.
734+
attr_accessor :default_tls
735+
alias default_ssl default_tls
736+
end
737+
738+
# Creates a new Net::IMAP object and connects it to the specified
739+
# +host+.
740+
#
741+
# Accepts the following options:
742+
#
743+
# [port]
744+
# Port number (default value is 143 for imap, or 993 for imaps)
745+
# [tls]
746+
# When +true+, the connection will use TLS with the default params set by
747+
# {OpenSSL::SSL::SSLContext#set_params}[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html#method-i-set_params].
748+
# Assign a hash to override TLS params—the keys are assignment methods on
749+
# SSLContext[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html].
750+
#
751+
# When <tt>port: 993</tt>, +tls+ defaults to +true+.
752+
# When <tt>port: 143</tt>, +tls+ defaults to +false+.
753+
# When port is unspecified or non-standard, +tls+ defaults to
754+
# ::default_tls. When ::default_tls is also +nil+, a warning is printed
755+
# and the connection does _not_ use TLS.
756+
#
757+
# When +nil+ or unassigned a default value is assigned: the default is
758+
# +true+ if <tt>port: 993</tt>, +false+ if <tt>port: 143</tt>, and
759+
# ::default_tls when +port+ is unspecified or non-standard. When
760+
# ::default_tls is +nil+, a back
761+
#
762+
# [open_timeout]
763+
# Seconds to wait until a connection is opened
764+
# [idle_response_timeout]
765+
# Seconds to wait until an IDLE response is received
766+
#
767+
# The most common errors are:
768+
#
769+
# Errno::ECONNREFUSED:: Connection refused by +host+ or an intervening
770+
# firewall.
771+
# Errno::ETIMEDOUT:: Connection timed out (possibly due to packets
772+
# being dropped by an intervening firewall).
773+
# Errno::ENETUNREACH:: There is no route to that network.
774+
# SocketError:: Hostname not known or other socket error.
775+
# Net::IMAP::ByeResponseError:: Connected to the host successfully, but
776+
# it immediately said goodbye.
777+
def initialize(host,
778+
port: nil,
779+
tls: nil,
780+
open_timeout: 30,
781+
idle_response_timeout: 5)
782+
# Basic configuration
783+
@host = host
784+
@tls, @port = default_tls_and_port(tls, port)
785+
@open_timeout = Integer(open_timeout)
786+
@idle_response_timeout = Integer(idle_response_timeout)
787+
788+
# Basic Client state
789+
super() # Mutex and condition vars (MonitorMixin#initialize)
790+
@greeting = nil
791+
@capabilities = nil
792+
@utf8_strings = false # TODO: use @enabled instead
793+
@debug_output_bol = true
794+
795+
# Client Protocol Reciever
796+
@parser = ResponseParser.new
797+
@receiver_thread = nil
798+
@receiver_thread_terminating = false
799+
@exception = nil
800+
801+
# Client Protocol Sender
802+
@tag_prefix = "RUBY"
803+
@tagno = 0
804+
805+
# Response handlers
806+
@continuation_request_arrival = new_cond
807+
@continuation_request_exception = nil
808+
@tagged_response_arrival = new_cond
809+
@tagged_responses = {}
810+
@response_handlers = []
811+
@responses = Hash.new {|h, k| h[k] = [] }
812+
813+
# Command execution state
814+
@logout_command_tag = nil
815+
@continued_command_tag = nil
816+
@idle_done_cond = nil
817+
818+
# create the connection
819+
@sock = nil
820+
start_connection
723821
end
724822

725823
def client_thread # :nodoc:
726-
warn "Net::IMAP#client_thread is deprecated and will be removed soon."
727-
@client_thread
824+
warn "Net::IMAP#client_thread is deprecated and always returns the " \
825+
"caller's current thread."
826+
Thread.current
728827
end
729828

730829
# Disconnects from the server.
@@ -795,7 +894,7 @@ def capabilities
795894
# servers will drop all <tt>AUTH=</tt> mechanisms from #capabilities after
796895
# the connection has authenticated.
797896
#
798-
# imap = Net::IMAP.new(hostname, ssl: false)
897+
# imap = Net::IMAP.new(hostname, tls: false)
799898
# imap.capabilities # => ["IMAP4REV1", "LOGINDISABLED"]
800899
# imap.auth_mechanisms # => []
801900
#
@@ -970,15 +1069,9 @@ def logout
9701069
# Server capabilities may change after #starttls, #login, and #authenticate.
9711070
# Cached #capabilities will be cleared when this method completes.
9721071
#
973-
def starttls(options = {}, verify = true)
1072+
def starttls(**options)
9741073
send_command("STARTTLS") do |resp|
9751074
if resp.kind_of?(TaggedResponse) && resp.name == "OK"
976-
begin
977-
# for backward compatibility
978-
certs = options.to_str
979-
options = create_ssl_params(certs, verify)
980-
rescue NoMethodError
981-
end
9821075
clear_cached_capabilities
9831076
clear_responses
9841077
start_tls_session(options)
@@ -2213,99 +2306,62 @@ def remove_response_handler(handler)
22132306

22142307
@@debug = false
22152308

2216-
# :call-seq:
2217-
# Net::IMAP.new(host, options = {})
2218-
#
2219-
# Creates a new Net::IMAP object and connects it to the specified
2220-
# +host+.
2221-
#
2222-
# +options+ is an option hash, each key of which is a symbol.
2223-
#
2224-
# The available options are:
2225-
#
2226-
# port:: Port number (default value is 143 for imap, or 993 for imaps)
2227-
# ssl:: If +options[:ssl]+ is true, then an attempt will be made
2228-
# to use SSL (now TLS) to connect to the server.
2229-
# If +options[:ssl]+ is a hash, it's passed to
2230-
# OpenSSL::SSL::SSLContext#set_params as parameters.
2231-
# open_timeout:: Seconds to wait until a connection is opened
2232-
# idle_response_timeout:: Seconds to wait until an IDLE response is received
2233-
#
2234-
# The most common errors are:
2235-
#
2236-
# Errno::ECONNREFUSED:: Connection refused by +host+ or an intervening
2237-
# firewall.
2238-
# Errno::ETIMEDOUT:: Connection timed out (possibly due to packets
2239-
# being dropped by an intervening firewall).
2240-
# Errno::ENETUNREACH:: There is no route to that network.
2241-
# SocketError:: Hostname not known or other socket error.
2242-
# Net::IMAP::ByeResponseError:: The connected to the host was successful, but
2243-
# it immediately said goodbye.
2244-
def initialize(host, port_or_options = {},
2245-
usessl = false, certs = nil, verify = true)
2246-
super()
2247-
@host = host
2248-
begin
2249-
options = port_or_options.to_hash
2250-
rescue NoMethodError
2251-
# for backward compatibility
2252-
options = {}
2253-
options[:port] = port_or_options
2254-
if usessl
2255-
options[:ssl] = create_ssl_params(certs, verify)
2309+
def default_tls_and_port(tls, port)
2310+
if tls.nil? && port
2311+
tls = true if port == SSL_PORT || /\Aimaps\z/i === port
2312+
tls = false if port == PORT
2313+
elsif port.nil? && !tls.nil?
2314+
port = tls ? SSL_PORT : PORT
2315+
end
2316+
if tls.nil? && port.nil?
2317+
tls = self.class.default_tls.dup.freeze
2318+
port = tls ? SSL_PORT : PORT
2319+
if tls.nil?
2320+
warn "A future version of Net::IMAP.default_tls " \
2321+
"will default to 'true', for secure connections by default. " \
2322+
"Use 'Net::IMAP.new(host, tls: false)' or set " \
2323+
"Net::IMAP.default_tls = false' to silence this warning."
22562324
end
22572325
end
2258-
@port = options[:port] || (options[:ssl] ? SSL_PORT : PORT)
2259-
@tag_prefix = "RUBY"
2260-
@tagno = 0
2261-
@utf8_strings = false
2262-
@open_timeout = options[:open_timeout] || 30
2263-
@idle_response_timeout = options[:idle_response_timeout] || 5
2264-
@parser = ResponseParser.new
2326+
tls &&= tls.respond_to?(:to_hash) ? tls.to_hash : {}
2327+
[tls, port]
2328+
end
2329+
2330+
def start_connection
22652331
@sock = tcp_socket(@host, @port)
22662332
begin
2267-
if options[:ssl]
2268-
start_tls_session(options[:ssl])
2269-
@usessl = true
2270-
else
2271-
@usessl = false
2272-
end
2273-
@responses = Hash.new {|h, k| h[k] = [] }
2274-
@tagged_responses = {}
2275-
@response_handlers = []
2276-
@tagged_response_arrival = new_cond
2277-
@continued_command_tag = nil
2278-
@continuation_request_arrival = new_cond
2279-
@continuation_request_exception = nil
2280-
@idle_done_cond = nil
2281-
@logout_command_tag = nil
2282-
@debug_output_bol = true
2283-
@exception = nil
2284-
2333+
start_tls_session(@tls) if @tls
22852334
@greeting = get_response
2286-
if @greeting.nil?
2287-
raise Error, "connection closed"
2288-
end
2289-
record_untagged_response_code @greeting
2290-
@capabilities = capabilities_from_resp_code @greeting
2291-
if @greeting.name == "BYE"
2292-
raise ByeResponseError, @greeting
2293-
end
2294-
2295-
@client_thread = Thread.current
2296-
@receiver_thread = Thread.start {
2297-
begin
2298-
receive_responses
2299-
rescue Exception
2300-
end
2301-
}
2302-
@receiver_thread_terminating = false
2335+
handle_server_greeting
2336+
@receiver_thread = start_receiver_thread
23032337
rescue Exception
23042338
@sock.close
23052339
raise
23062340
end
23072341
end
23082342

2343+
def handle_server_greeting
2344+
if @greeting.nil?
2345+
raise Error, "connection closed"
2346+
end
2347+
record_untagged_response_code(@greeting)
2348+
@capabilities = capabilities_from_resp_code @greeting
2349+
if @greeting.name == "BYE"
2350+
raise ByeResponseError, @greeting
2351+
end
2352+
end
2353+
2354+
def start_receiver_thread
2355+
Thread.start do
2356+
receive_responses
2357+
rescue Exception
2358+
# don't exit the thread with an exception
2359+
end
2360+
rescue Exception
2361+
@sock.close
2362+
raise
2363+
end
2364+
23092365
def tcp_socket(host, port)
23102366
s = Socket.tcp(host, port, :connect_timeout => @open_timeout)
23112367
s.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, true)
@@ -2592,34 +2648,12 @@ def normalize_searching_criteria(keys)
25922648
end
25932649
end
25942650

2595-
def create_ssl_params(certs = nil, verify = true)
2596-
params = {}
2597-
if certs
2598-
if File.file?(certs)
2599-
params[:ca_file] = certs
2600-
elsif File.directory?(certs)
2601-
params[:ca_path] = certs
2602-
end
2603-
end
2604-
if verify
2605-
params[:verify_mode] = VERIFY_PEER
2606-
else
2607-
params[:verify_mode] = VERIFY_NONE
2608-
end
2609-
return params
2610-
end
2611-
26122651
def start_tls_session(params = {})
26132652
unless defined?(OpenSSL::SSL)
2614-
raise "SSL extension not installed"
2653+
raise "OpenSSL extension not installed"
26152654
end
26162655
if @sock.kind_of?(OpenSSL::SSL::SSLSocket)
2617-
raise RuntimeError, "already using SSL"
2618-
end
2619-
begin
2620-
params = params.to_hash
2621-
rescue NoMethodError
2622-
params = {}
2656+
raise RuntimeError, "already using TLS"
26232657
end
26242658
context = SSLContext.new
26252659
context.set_params(params)
@@ -2655,3 +2689,6 @@ def self.saslprep(string, **opts)
26552689
require_relative "imap/response_data"
26562690
require_relative "imap/response_parser"
26572691
require_relative "imap/authenticators"
2692+
2693+
require_relative "imap/deprecated_client_options"
2694+
Net::IMAP.prepend Net::IMAP::DeprecatedClientOptions

0 commit comments

Comments
 (0)