Skip to content

Commit e479952

Browse files
committed
🗑️ Add deprecation warnings to .new and #starttls
Preparing for a (backwards-incompatible) secure-by-default configuration, Net::IMAP.default_ssl will be used when no explicit port or tls setting is provided. TODO: should truthy default_ssl be used to config params when port is 993 but ssl is implicit? Another var? Moved all deprecated option handling to DeprecatedClientOptions, which is prepended to Net::IMAP. Additionally, split `initialize` up into small helper methods making it easier to understand at a glance.
1 parent 16dafde commit e479952

File tree

2 files changed

+227
-113
lines changed

2 files changed

+227
-113
lines changed

lib/net/imap.rb

+144-113
Original file line numberDiff line numberDiff line change
@@ -716,11 +716,104 @@ class << self
716716
alias default_imap_port default_port
717717
alias default_imaps_port default_tls_port
718718
alias default_ssl_port default_tls_port
719+
720+
# The default value for the +ssl+ option of ::new, when +port+ is
721+
# unspecified or non-standard.
722+
#
723+
# Defaults to +nil+ for backward compatibility, which prints a warning and
724+
# does _not_ use TLS.
725+
#
726+
# >>>
727+
# *Note*: A future version of Net::IMAP will default to +true+, as per
728+
# RFC7525[https://tools.ietf.org/html/rfc7525],
729+
# RFC7817[https://tools.ietf.org/html/rfc7817],
730+
# and RFC8314[https://tools.ietf.org/html/rfc8314].
731+
#
732+
# Set to +false+ to *globally* use insecure defaults and silence warnings.
733+
# Send <tt>ssl: false</tt> to ::new to explicitly silence warnings for a
734+
# single connection.
735+
attr_accessor :default_ssl
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+
# [ssl]
746+
# When +true+, the connection will use TLS using the defaults chosen by
747+
# {OpenSSL::SSL::SSLContext#set_params}[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html#method-i-set_params].
748+
# Use a hash to override the defaults---the keys are assignment methods on
749+
# SSLContext[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html].
750+
# Defaults to +true+ or +false+ to match +port+, or to ::default_ssl when
751+
# +port+ is unspecified or non-standard.
752+
# [open_timeout]
753+
# Seconds to wait until a connection is opened
754+
# [idle_response_timeout]
755+
# Seconds to wait until an IDLE response is received
756+
#
757+
# The most common errors are:
758+
#
759+
# Errno::ECONNREFUSED:: Connection refused by +host+ or an intervening
760+
# firewall.
761+
# Errno::ETIMEDOUT:: Connection timed out (possibly due to packets
762+
# being dropped by an intervening firewall).
763+
# Errno::ENETUNREACH:: There is no route to that network.
764+
# SocketError:: Hostname not known or other socket error.
765+
# Net::IMAP::ByeResponseError:: Connected to the host successfully, but
766+
# it immediately said goodbye.
767+
def initialize(host,
768+
port: nil,
769+
ssl: nil,
770+
open_timeout: 30,
771+
idle_response_timeout: 5)
772+
# Basic configuration
773+
@host = host
774+
@ssl, @port = default_ssl_and_port(ssl, port)
775+
@open_timeout = Integer(open_timeout)
776+
@idle_response_timeout = Integer(idle_response_timeout)
777+
778+
# Basic Client state
779+
super() # Mutex and condition vars (MonitorMixin#initialize)
780+
@greeting = nil
781+
@capabilities = nil
782+
@utf8_strings = false # TODO: use @enabled instead
783+
@debug_output_bol = true
784+
785+
# Client Protocol Reciever
786+
@parser = ResponseParser.new
787+
@receiver_thread = nil
788+
@receiver_thread_terminating = false
789+
@exception = nil
790+
791+
# Client Protocol Sender
792+
@tag_prefix = "RUBY"
793+
@tagno = 0
794+
795+
# Response handlers
796+
@continuation_request_arrival = new_cond
797+
@continuation_request_exception = nil
798+
@tagged_response_arrival = new_cond
799+
@tagged_responses = {}
800+
@response_handlers = []
801+
@responses = Hash.new {|h, k| h[k] = [] }
802+
803+
# Command execution state
804+
@logout_command_tag = nil
805+
@continued_command_tag = nil
806+
@idle_done_cond = nil
807+
808+
# create the connection
809+
@sock = nil
810+
start_connection
719811
end
720812

721813
def client_thread # :nodoc:
722-
warn "Net::IMAP#client_thread is deprecated and will be removed soon."
723-
@client_thread
814+
warn "Net::IMAP#client_thread is deprecated and always returns the " \
815+
"caller's current thread."
816+
Thread.current
724817
end
725818

726819
# Disconnects from the server.
@@ -966,15 +1059,9 @@ def logout
9661059
# Server capabilities may change after #starttls, #login, and #authenticate.
9671060
# Cached #capabilities will be cleared when this method completes.
9681061
#
969-
def starttls(options = {}, verify = true)
1062+
def starttls(options = {})
9701063
send_command("STARTTLS") do |resp|
9711064
if resp.kind_of?(TaggedResponse) && resp.name == "OK"
972-
begin
973-
# for backward compatibility
974-
certs = options.to_str
975-
options = create_ssl_params(certs, verify)
976-
rescue NoMethodError
977-
end
9781065
clear_cached_capabilities
9791066
clear_responses
9801067
start_tls_session(options)
@@ -2190,99 +2277,62 @@ def remove_response_handler(handler)
21902277

21912278
@@debug = false
21922279

2193-
# :call-seq:
2194-
# Net::IMAP.new(host, options = {})
2195-
#
2196-
# Creates a new Net::IMAP object and connects it to the specified
2197-
# +host+.
2198-
#
2199-
# +options+ is an option hash, each key of which is a symbol.
2200-
#
2201-
# The available options are:
2202-
#
2203-
# port:: Port number (default value is 143 for imap, or 993 for imaps)
2204-
# ssl:: If +options[:ssl]+ is true, then an attempt will be made
2205-
# to use SSL (now TLS) to connect to the server.
2206-
# If +options[:ssl]+ is a hash, it's passed to
2207-
# OpenSSL::SSL::SSLContext#set_params as parameters.
2208-
# open_timeout:: Seconds to wait until a connection is opened
2209-
# idle_response_timeout:: Seconds to wait until an IDLE response is received
2210-
#
2211-
# The most common errors are:
2212-
#
2213-
# Errno::ECONNREFUSED:: Connection refused by +host+ or an intervening
2214-
# firewall.
2215-
# Errno::ETIMEDOUT:: Connection timed out (possibly due to packets
2216-
# being dropped by an intervening firewall).
2217-
# Errno::ENETUNREACH:: There is no route to that network.
2218-
# SocketError:: Hostname not known or other socket error.
2219-
# Net::IMAP::ByeResponseError:: The connected to the host was successful, but
2220-
# it immediately said goodbye.
2221-
def initialize(host, port_or_options = {},
2222-
usessl = false, certs = nil, verify = true)
2223-
super()
2224-
@host = host
2225-
begin
2226-
options = port_or_options.to_hash
2227-
rescue NoMethodError
2228-
# for backward compatibility
2229-
options = {}
2230-
options[:port] = port_or_options
2231-
if usessl
2232-
options[:ssl] = create_ssl_params(certs, verify)
2280+
def default_ssl_and_port(ssl, port)
2281+
if ssl.nil? && port
2282+
ssl = true if port == SSL_PORT || /\Aimaps\z/i === port
2283+
ssl = false if port == PORT
2284+
elsif port.nil? && !ssl.nil?
2285+
port = ssl ? SSL_PORT : PORT
2286+
end
2287+
if ssl.nil? && port.nil?
2288+
ssl = self.class.default_ssl.dup.freeze
2289+
port = ssl ? SSL_PORT : PORT
2290+
if ssl.nil?
2291+
warn "A future version of Net::IMAP.default_ssl " \
2292+
"will default to 'true', for secure connections by default. " \
2293+
"Use 'Net::IMAP.new(host, ssl: false)' or set " \
2294+
"Net::IMAP.default_ssl = false' to silence this warning."
22332295
end
22342296
end
2235-
@port = options[:port] || (options[:ssl] ? SSL_PORT : PORT)
2236-
@tag_prefix = "RUBY"
2237-
@tagno = 0
2238-
@utf8_strings = false
2239-
@open_timeout = options[:open_timeout] || 30
2240-
@idle_response_timeout = options[:idle_response_timeout] || 5
2241-
@parser = ResponseParser.new
2297+
ssl &&= ssl.respond_to?(:to_hash) ? ssl.to_hash : {}
2298+
[ssl, port]
2299+
end
2300+
2301+
def start_connection
22422302
@sock = tcp_socket(@host, @port)
22432303
begin
2244-
if options[:ssl]
2245-
start_tls_session(options[:ssl])
2246-
@usessl = true
2247-
else
2248-
@usessl = false
2249-
end
2250-
@responses = Hash.new {|h, k| h[k] = [] }
2251-
@tagged_responses = {}
2252-
@response_handlers = []
2253-
@tagged_response_arrival = new_cond
2254-
@continued_command_tag = nil
2255-
@continuation_request_arrival = new_cond
2256-
@continuation_request_exception = nil
2257-
@idle_done_cond = nil
2258-
@logout_command_tag = nil
2259-
@debug_output_bol = true
2260-
@exception = nil
2261-
2304+
start_tls_session(@ssl) if @ssl
22622305
@greeting = get_response
2263-
if @greeting.nil?
2264-
raise Error, "connection closed"
2265-
end
2266-
record_untagged_response_code @greeting
2267-
@capabilities = capabilities_from_resp_code @greeting
2268-
if @greeting.name == "BYE"
2269-
raise ByeResponseError, @greeting
2270-
end
2271-
2272-
@client_thread = Thread.current
2273-
@receiver_thread = Thread.start {
2274-
begin
2275-
receive_responses
2276-
rescue Exception
2277-
end
2278-
}
2279-
@receiver_thread_terminating = false
2306+
handle_server_greeting
2307+
@receiver_thread = start_receiver_thread
22802308
rescue Exception
22812309
@sock.close
22822310
raise
22832311
end
22842312
end
22852313

2314+
def handle_server_greeting
2315+
if @greeting.nil?
2316+
raise Error, "connection closed"
2317+
end
2318+
record_untagged_response_code(@greeting)
2319+
@capabilities = capabilities_from_resp_code @greeting
2320+
if @greeting.name == "BYE"
2321+
raise ByeResponseError, @greeting
2322+
end
2323+
end
2324+
2325+
def start_receiver_thread
2326+
Thread.start do
2327+
receive_responses
2328+
rescue Exception
2329+
# don't exit the thread with an exception
2330+
end
2331+
rescue Exception
2332+
@sock.close
2333+
raise
2334+
end
2335+
22862336
def tcp_socket(host, port)
22872337
s = Socket.tcp(host, port, :connect_timeout => @open_timeout)
22882338
s.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, true)
@@ -2569,35 +2619,13 @@ def normalize_searching_criteria(keys)
25692619
end
25702620
end
25712621

2572-
def create_ssl_params(certs = nil, verify = true)
2573-
params = {}
2574-
if certs
2575-
if File.file?(certs)
2576-
params[:ca_file] = certs
2577-
elsif File.directory?(certs)
2578-
params[:ca_path] = certs
2579-
end
2580-
end
2581-
if verify
2582-
params[:verify_mode] = VERIFY_PEER
2583-
else
2584-
params[:verify_mode] = VERIFY_NONE
2585-
end
2586-
return params
2587-
end
2588-
25892622
def start_tls_session(params = {})
25902623
unless defined?(OpenSSL::SSL)
25912624
raise "SSL extension not installed"
25922625
end
25932626
if @sock.kind_of?(OpenSSL::SSL::SSLSocket)
25942627
raise RuntimeError, "already using SSL"
25952628
end
2596-
begin
2597-
params = params.to_hash
2598-
rescue NoMethodError
2599-
params = {}
2600-
end
26012629
context = SSLContext.new
26022630
context.set_params(params)
26032631
if defined?(VerifyCallbackProc)
@@ -2632,3 +2660,6 @@ def self.saslprep(string, **opts)
26322660
require_relative "imap/response_data"
26332661
require_relative "imap/response_parser"
26342662
require_relative "imap/authenticators"
2663+
2664+
require_relative "imap/deprecated_client_options"
2665+
Net::IMAP.prepend Net::IMAP::DeprecatedClientOptions
+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# frozen_string_literal: true
2+
3+
module Net
4+
class IMAP < Protocol
5+
6+
# This module handles deprecated arguments to various methods. It will be
7+
# deleted in a future release.
8+
module DeprecatedClientOptions
9+
UNDEFINED = Module.new.freeze
10+
private_constant :UNDEFINED
11+
12+
# Passing any arguments *both* positionally and as a keyword will raise an
13+
# ArgumentError.
14+
#
15+
# Allows port to be sent as an integer or string without warning or error.
16+
#
17+
# SSL options sent positionally will print a deprecation warning (and,
18+
# eventually, raise an argument error).
19+
def initialize(host, *deprecated, **options)
20+
unless deprecated.empty?
21+
port_or_options, sslopts = deprecated
22+
if !port_or_options.nil? &&
23+
(port_or_options.respond_to?(:to_hash) ?
24+
options.empty? : options.key?(:port)) ||
25+
!sslopts.nil? && options.key?(:ssl)
26+
raise ArgumentError, "Don't use both positional and keyword options"
27+
end
28+
# handle port_or_options => options
29+
if port_or_options.respond_to?(:to_hash)
30+
warn "DEPRECATED: options should be set by keyword arguments"
31+
options = port_or_options.to_hash
32+
elsif !port_or_options.nil?
33+
warn "DEPRECATED: port should be set by keyword argument"
34+
options[:port] = port_or_options
35+
end
36+
# handle ssl options
37+
unless sslopts.nil?
38+
warn "DEPRECATED: SSL options should be set by keyword argument"
39+
usessl, certs, verify = sslopts
40+
if usessl
41+
options[:ssl] = create_ssl_params(certs, verify)
42+
end
43+
end
44+
end
45+
super(host, **options)
46+
end
47+
48+
# +call-seq:
49+
# starttls(options = {})
50+
# starttls(certs, verify = true)
51+
#
52+
# For backward compatibility. A future release will only accept
53+
# OpenSSL::SSL::SSLContext.set_params options.
54+
def starttls(options = {}, verify = UNDEFINED)
55+
if options.respond_to?(:to_str)
56+
warn "DEPRECATED: starttls(certs, verify). Use starttls(ssl_params)"
57+
certs = options.to_str
58+
verify = true if verify == UNDEFINED
59+
options = create_ssl_params(certs, verify)
60+
end
61+
super(options)
62+
end
63+
64+
private
65+
66+
def create_ssl_params(certs, verify)
67+
params = {}
68+
if certs
69+
if File.file?(certs)
70+
params[:ca_file] = certs
71+
elsif File.directory?(certs)
72+
params[:ca_path] = certs
73+
end
74+
end
75+
params[:verify_mode] = verify ? VERIFY_PEER : VERIFY_NONE
76+
params
77+
end
78+
79+
end
80+
81+
prepend DeprecatedClientOptions
82+
end
83+
end

0 commit comments

Comments
 (0)