Skip to content

Commit 688fae2

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 688fae2

File tree

2 files changed

+319
-114
lines changed

2 files changed

+319
-114
lines changed

lib/net/imap.rb

+153-114
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,107 @@ 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+
# DEPRECATED
819+
@client_thread = Thread.current
820+
821+
# create the connection
822+
@sock = nil
823+
start_connection
723824
end
724825

725826
def client_thread # :nodoc:
@@ -795,7 +896,7 @@ def capabilities
795896
# servers will drop all <tt>AUTH=</tt> mechanisms from #capabilities after
796897
# the connection has authenticated.
797898
#
798-
# imap = Net::IMAP.new(hostname, ssl: false)
899+
# imap = Net::IMAP.new(hostname, tls: false)
799900
# imap.capabilities # => ["IMAP4REV1", "LOGINDISABLED"]
800901
# imap.auth_mechanisms # => []
801902
#
@@ -970,15 +1071,9 @@ def logout
9701071
# Server capabilities may change after #starttls, #login, and #authenticate.
9711072
# Cached #capabilities will be cleared when this method completes.
9721073
#
973-
def starttls(options = {}, verify = true)
1074+
def starttls(**options)
9741075
send_command("STARTTLS") do |resp|
9751076
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
9821077
clear_cached_capabilities
9831078
clear_responses
9841079
start_tls_session(options)
@@ -2213,99 +2308,62 @@ def remove_response_handler(handler)
22132308

22142309
@@debug = false
22152310

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)
2311+
def default_tls_and_port(tls, port)
2312+
if tls.nil? && port
2313+
tls = true if port == SSL_PORT || /\Aimaps\z/i === port
2314+
tls = false if port == PORT
2315+
elsif port.nil? && !tls.nil?
2316+
port = tls ? SSL_PORT : PORT
2317+
end
2318+
if tls.nil? && port.nil?
2319+
tls = self.class.default_tls.dup.freeze
2320+
port = tls ? SSL_PORT : PORT
2321+
if tls.nil?
2322+
warn "A future version of Net::IMAP.default_tls " \
2323+
"will default to 'true', for secure connections by default. " \
2324+
"Use 'Net::IMAP.new(host, tls: false)' or set " \
2325+
"Net::IMAP.default_tls = false' to silence this warning."
22562326
end
22572327
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
2328+
tls &&= tls.respond_to?(:to_hash) ? tls.to_hash : {}
2329+
[tls, port]
2330+
end
2331+
2332+
def start_connection
22652333
@sock = tcp_socket(@host, @port)
22662334
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-
2335+
start_tls_session(@tls) if @tls
22852336
@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
2337+
handle_server_greeting
2338+
@receiver_thread = start_receiver_thread
23032339
rescue Exception
23042340
@sock.close
23052341
raise
23062342
end
23072343
end
23082344

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

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-
26122653
def start_tls_session(params = {})
26132654
unless defined?(OpenSSL::SSL)
2614-
raise "SSL extension not installed"
2655+
raise "OpenSSL extension not installed"
26152656
end
26162657
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 = {}
2658+
raise RuntimeError, "already using TLS"
26232659
end
26242660
context = SSLContext.new
26252661
context.set_params(params)
@@ -2655,3 +2691,6 @@ def self.saslprep(string, **opts)
26552691
require_relative "imap/response_data"
26562692
require_relative "imap/response_parser"
26572693
require_relative "imap/authenticators"
2694+
2695+
require_relative "imap/deprecated_client_options"
2696+
Net::IMAP.prepend Net::IMAP::DeprecatedClientOptions

0 commit comments

Comments
 (0)