From a8369da546b3d8edaaae9df42cd5736c46004e94 Mon Sep 17 00:00:00 2001 From: nick evans Date: Tue, 5 Sep 2023 09:07:58 -0400 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=9A=9A=20Reorder=20method=20definitio?= =?UTF-8?q?ns=20in=20imap.rb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To match the most common order in modern ruby projects: * Moved `attr_reader`s down below class methods. * Moved `#initialize` up above all public instance method `def`s. * Moved an method down with other public instance method `def`s. This commit should contain no other code changes. --- lib/net/imap.rb | 232 ++++++++++++++++++++++++------------------------ 1 file changed, 116 insertions(+), 116 deletions(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index a07d5858..a67f2d7a 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -679,28 +679,6 @@ class IMAP < Protocol include SSL end - # Returns the initial greeting the server, an UntaggedResponse. - attr_reader :greeting - - # 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. The default value is 30 seconds. - attr_reader :open_timeout - - # Seconds to wait until an IDLE response is received. - attr_reader :idle_response_timeout - - # The hostname this client connected to - attr_reader :host - - # The port this client connected to - attr_reader :port - - # Returns true after the TLS negotiation has completed and the remote - # hostname has been verified. Returns false when TLS has been established - # but peer verification was disabled. - def tls_verified?; @tls_verified end - # Returns the debug mode. def self.debug return @@debug @@ -727,6 +705,122 @@ class << self alias default_ssl_port default_tls_port end + # Returns the initial greeting the server, an UntaggedResponse. + attr_reader :greeting + + # 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. The default value is 30 seconds. + attr_reader :open_timeout + + # Seconds to wait until an IDLE response is received. + attr_reader :idle_response_timeout + + # The hostname this client connected to + attr_reader :host + + # The port this client connected to + attr_reader :port + + # :call-seq: + # Net::IMAP.new(host, options = {}) + # + # Creates a new Net::IMAP object and connects it to the specified + # +host+. + # + # +options+ is an option hash, each key of which is a symbol. + # + # The available options are: + # + # port:: Port number (default value is 143 for imap, or 993 for imaps) + # ssl:: If +options[:ssl]+ is true, then an attempt will be made + # to use SSL (now TLS) to connect to the server. + # If +options[:ssl]+ is a hash, it's passed to + # OpenSSL::SSL::SSLContext#set_params as parameters. + # open_timeout:: Seconds to wait until a connection is opened + # idle_response_timeout:: Seconds to wait until an IDLE response is received + # + # The most common errors are: + # + # Errno::ECONNREFUSED:: Connection refused by +host+ or an intervening + # firewall. + # Errno::ETIMEDOUT:: Connection timed out (possibly due to packets + # being dropped by an intervening firewall). + # Errno::ENETUNREACH:: There is no route to that network. + # SocketError:: Hostname not known or other socket error. + # Net::IMAP::ByeResponseError:: The connected to the host was successful, but + # it immediately said goodbye. + def initialize(host, port_or_options = {}, + usessl = false, certs = nil, verify = true) + super() + @host = host + begin + options = port_or_options.to_hash + rescue NoMethodError + # for backward compatibility + options = {} + options[:port] = port_or_options + if usessl + options[:ssl] = create_ssl_params(certs, verify) + end + end + @port = options[:port] || (options[:ssl] ? SSL_PORT : PORT) + @tag_prefix = "RUBY" + @tagno = 0 + @utf8_strings = false + @open_timeout = options[:open_timeout] || 30 + @idle_response_timeout = options[:idle_response_timeout] || 5 + @tls_verified = false + @parser = ResponseParser.new + @sock = tcp_socket(@host, @port) + begin + if options[:ssl] + start_tls_session(options[:ssl]) + @usessl = true + else + @usessl = false + end + @responses = Hash.new {|h, k| h[k] = [] } + @tagged_responses = {} + @response_handlers = [] + @tagged_response_arrival = new_cond + @continued_command_tag = nil + @continuation_request_arrival = new_cond + @continuation_request_exception = nil + @idle_done_cond = nil + @logout_command_tag = nil + @debug_output_bol = true + @exception = nil + + @greeting = get_response + if @greeting.nil? + raise Error, "connection closed" + end + record_untagged_response_code @greeting + @capabilities = capabilities_from_resp_code @greeting + if @greeting.name == "BYE" + raise ByeResponseError, @greeting + end + + @client_thread = Thread.current + @receiver_thread = Thread.start { + begin + receive_responses + rescue Exception + end + } + @receiver_thread_terminating = false + rescue Exception + @sock.close + raise + end + end + + # Returns true after the TLS negotiation has completed and the remote + # hostname has been verified. Returns false when TLS has been established + # but peer verification was disabled. + def tls_verified?; @tls_verified end + def client_thread # :nodoc: warn "Net::IMAP#client_thread is deprecated and will be removed soon." @client_thread @@ -2218,100 +2312,6 @@ def remove_response_handler(handler) @@debug = false - # :call-seq: - # Net::IMAP.new(host, options = {}) - # - # Creates a new Net::IMAP object and connects it to the specified - # +host+. - # - # +options+ is an option hash, each key of which is a symbol. - # - # The available options are: - # - # port:: Port number (default value is 143 for imap, or 993 for imaps) - # ssl:: If +options[:ssl]+ is true, then an attempt will be made - # to use SSL (now TLS) to connect to the server. - # If +options[:ssl]+ is a hash, it's passed to - # OpenSSL::SSL::SSLContext#set_params as parameters. - # open_timeout:: Seconds to wait until a connection is opened - # idle_response_timeout:: Seconds to wait until an IDLE response is received - # - # The most common errors are: - # - # Errno::ECONNREFUSED:: Connection refused by +host+ or an intervening - # firewall. - # Errno::ETIMEDOUT:: Connection timed out (possibly due to packets - # being dropped by an intervening firewall). - # Errno::ENETUNREACH:: There is no route to that network. - # SocketError:: Hostname not known or other socket error. - # Net::IMAP::ByeResponseError:: The connected to the host was successful, but - # it immediately said goodbye. - def initialize(host, port_or_options = {}, - usessl = false, certs = nil, verify = true) - super() - @host = host - begin - options = port_or_options.to_hash - rescue NoMethodError - # for backward compatibility - options = {} - options[:port] = port_or_options - if usessl - options[:ssl] = create_ssl_params(certs, verify) - end - end - @port = options[:port] || (options[:ssl] ? SSL_PORT : PORT) - @tag_prefix = "RUBY" - @tagno = 0 - @utf8_strings = false - @open_timeout = options[:open_timeout] || 30 - @idle_response_timeout = options[:idle_response_timeout] || 5 - @tls_verified = false - @parser = ResponseParser.new - @sock = tcp_socket(@host, @port) - begin - if options[:ssl] - start_tls_session(options[:ssl]) - @usessl = true - else - @usessl = false - end - @responses = Hash.new {|h, k| h[k] = [] } - @tagged_responses = {} - @response_handlers = [] - @tagged_response_arrival = new_cond - @continued_command_tag = nil - @continuation_request_arrival = new_cond - @continuation_request_exception = nil - @idle_done_cond = nil - @logout_command_tag = nil - @debug_output_bol = true - @exception = nil - - @greeting = get_response - if @greeting.nil? - raise Error, "connection closed" - end - record_untagged_response_code @greeting - @capabilities = capabilities_from_resp_code @greeting - if @greeting.name == "BYE" - raise ByeResponseError, @greeting - end - - @client_thread = Thread.current - @receiver_thread = Thread.start { - begin - receive_responses - rescue Exception - end - } - @receiver_thread_terminating = false - rescue Exception - @sock.close - raise - end - end - def tcp_socket(host, port) s = Socket.tcp(host, port, :connect_timeout => @open_timeout) s.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, true) From 77273838db55c2b4c7783db87f6ad9d15b60383a Mon Sep 17 00:00:00 2001 From: nick evans Date: Tue, 5 Sep 2023 09:05:10 -0400 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=93=9A=20Update=20Net::IMAP.new=20doc?= =?UTF-8?q?umentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated rdoc for #initialize: * Various stylistic changes, mostly for options and exceptions. * Added examples to demonstrate connecting with clear-text, TLS, and checking the greeting for "OK" or "PREAUTH". * Added links to OpenSSL::SSL::SSLContext rdoc. --- lib/net/imap.rb | 87 ++++++++++++++++++------ test/net/imap/fake_server/test_helper.rb | 28 +++++--- 2 files changed, 86 insertions(+), 29 deletions(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index a67f2d7a..90aeda13 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -722,34 +722,79 @@ class << self # The port this client connected to attr_reader :port - # :call-seq: - # Net::IMAP.new(host, options = {}) - # # Creates a new Net::IMAP object and connects it to the specified # +host+. # - # +options+ is an option hash, each key of which is a symbol. - # - # The available options are: + # ==== Options + # + # Accepts the following options: + # + # [port] + # Port number. Defaults to 993 when +ssl+ is truthy, and 143 otherwise. + # + # [ssl] + # If +true+, the connection will use TLS with the default params set by + # {OpenSSL::SSL::SSLContext#set_params}[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html#method-i-set_params]. + # If +ssl+ is a hash, it's passed to + # {OpenSSL::SSL::SSLContext#set_params}[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html#method-i-set_params]; + # the keys are names of attribute assignment methods on + # SSLContext[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html]. + # + # [open_timeout] + # Seconds to wait until a connection is opened + # [idle_response_timeout] + # Seconds to wait until an IDLE response is received + # + # See DeprecatedClientOptions for obsolete backwards compatible arguments. + # + # ==== Examples + # + # Connect to cleartext port 143 at mail.example.com and recieve the server greeting: + # imap = Net::IMAP.new('mail.example.com', ssl: false) # => # + # imap.port => 143 + # imap.tls_verified? => false + # imap.greeting => name: ("OK" | "PREAUTH") => status + # status # => "OK" + # # The client is connected in the "Not Authenticated" state. + # + # Connect with TLS to port 993 at mail.example.com: + # imap = Net::IMAP.new('mail.example.com', ssl: true) # => # + # imap.port => 993 + # imap.tls_verified? => true + # imap.greeting => name: ("OK" | "PREAUTH") => status + # status # => "OK" + # # The client is connected in the "Not Authenticated" state. + # + # Connect with prior authentication, for example using an SSL certificate: + # ssl_ctx_params = { + # cert: OpenSSL::X509::Certificate.new(File.read("client.crt")), + # key: OpenSSL::PKey::EC.new(File.read('client.key')), + # extra_chain_cert: [ + # OpenSSL::X509::Certificate.new(File.read("intermediate.crt")), + # ], + # } + # imap = Net::IMAP.new('mail.example.com', ssl: ssl_ctx_params) + # imap.port => 993 + # imap.tls_verified? => true + # imap.greeting => name: "PREAUTH" + # # The client is connected in the "Authenticated" state. # - # port:: Port number (default value is 143 for imap, or 993 for imaps) - # ssl:: If +options[:ssl]+ is true, then an attempt will be made - # to use SSL (now TLS) to connect to the server. - # If +options[:ssl]+ is a hash, it's passed to - # OpenSSL::SSL::SSLContext#set_params as parameters. - # open_timeout:: Seconds to wait until a connection is opened - # idle_response_timeout:: Seconds to wait until an IDLE response is received + # ==== Exceptions # # The most common errors are: # - # Errno::ECONNREFUSED:: Connection refused by +host+ or an intervening - # firewall. - # Errno::ETIMEDOUT:: Connection timed out (possibly due to packets - # being dropped by an intervening firewall). - # Errno::ENETUNREACH:: There is no route to that network. - # SocketError:: Hostname not known or other socket error. - # Net::IMAP::ByeResponseError:: The connected to the host was successful, but - # it immediately said goodbye. + # [Errno::ECONNREFUSED] + # Connection refused by +host+ or an intervening firewall. + # [Errno::ETIMEDOUT] + # Connection timed out (possibly due to packets being dropped by an + # intervening firewall). + # [Errno::ENETUNREACH] + # There is no route to that network. + # [SocketError] + # Hostname not known or other socket error. + # [Net::IMAP::ByeResponseError] + # Connected to the host successfully, but it immediately said goodbye. + # def initialize(host, port_or_options = {}, usessl = false, certs = nil, verify = true) super() diff --git a/test/net/imap/fake_server/test_helper.rb b/test/net/imap/fake_server/test_helper.rb index 1291a3a7..cf68dd9f 100644 --- a/test/net/imap/fake_server/test_helper.rb +++ b/test/net/imap/fake_server/test_helper.rb @@ -4,26 +4,38 @@ module Net::IMAP::FakeServer::TestHelper - def with_fake_server(select: nil, timeout: 5, **opts) + def run_fake_server_in_thread(timeout: 5, **opts) Timeout.timeout(timeout) do server = Net::IMAP::FakeServer.new(timeout: timeout, **opts) @threads << Thread.new do server.run end if @threads + yield server + ensure + server&.shutdown + end + end + + def with_client(*args, **kwargs) + client = Net::IMAP.new(*args, **kwargs) + yield client + ensure + if client && !client.disconnected? + client.logout rescue pp $! + client.disconnect unless client.disconnected? + end + end + + def with_fake_server(select: nil, **opts) + run_fake_server_in_thread(**opts) do |server| tls = opts[:implicit_tls] tls = {ca_file: server.config.tls[:ca_file]} if tls == true - client = Net::IMAP.new("localhost", port: server.port, ssl: tls) - begin + with_client("localhost", port: server.port, ssl: tls) do |client| if select client.select(select) server.commands.pop assert server.state.selected? end yield server, client - ensure - client.logout rescue pp $! - client.disconnect if !client.disconnected? end - ensure - server&.shutdown end end From 805111e815102a48617649d39878e3e83c17b426 Mon Sep 17 00:00:00 2001 From: nick evans Date: Wed, 6 Sep 2023 09:17:36 -0400 Subject: [PATCH 3/4] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20and=20organ?= =?UTF-8?q?ize=20Net::IMAP#initialize?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Extract four methods: * `convert_deprecated_options` * `start_imap_connection`: begins IMAP protocol on connected socket * `get_server_greeting`: wraps `get_response` with greeting specifics * `start_receiver_thread` * Various other small stylistic tweaks, e.g: grouped similar ivars. This _should_ be the result of a series of "pure" safe refactorings. All existing behavior should remain unchanged. --- lib/net/imap.rb | 133 ++++++++++++++++++++++++++++-------------------- 1 file changed, 77 insertions(+), 56 deletions(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 90aeda13..87ec7ca2 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -795,70 +795,50 @@ class << self # [Net::IMAP::ByeResponseError] # Connected to the host successfully, but it immediately said goodbye. # - def initialize(host, port_or_options = {}, - usessl = false, certs = nil, verify = true) + def initialize(host, options = {}, *deprecated) super() + options = convert_deprecated_options(options, *deprecated) + + # Config options @host = host - begin - options = port_or_options.to_hash - rescue NoMethodError - # for backward compatibility - options = {} - options[:port] = port_or_options - if usessl - options[:ssl] = create_ssl_params(certs, verify) - end - end @port = options[:port] || (options[:ssl] ? SSL_PORT : PORT) - @tag_prefix = "RUBY" - @tagno = 0 - @utf8_strings = false @open_timeout = options[:open_timeout] || 30 @idle_response_timeout = options[:idle_response_timeout] || 5 - @tls_verified = false + ssl_ctx_params = options[:ssl] + + # Basic Client State + @utf8_strings = false + @debug_output_bol = true + @exception = nil + @greeting = nil + @capabilities = nil + + # Client Protocol Reciever @parser = ResponseParser.new + @responses = Hash.new {|h, k| h[k] = [] } + @response_handlers = [] + @receiver_thread = nil + @receiver_thread_exception = nil + @receiver_thread_terminating = false + + # Client Protocol Sender (including state for currently running commands) + @tag_prefix = "RUBY" + @tagno = 0 + @tagged_responses = {} + @tagged_response_arrival = new_cond + @continued_command_tag = nil + @continuation_request_arrival = new_cond + @continuation_request_exception = nil + @idle_done_cond = nil + @logout_command_tag = nil + + # Connection + @tls_verified = false @sock = tcp_socket(@host, @port) - begin - if options[:ssl] - start_tls_session(options[:ssl]) - @usessl = true - else - @usessl = false - end - @responses = Hash.new {|h, k| h[k] = [] } - @tagged_responses = {} - @response_handlers = [] - @tagged_response_arrival = new_cond - @continued_command_tag = nil - @continuation_request_arrival = new_cond - @continuation_request_exception = nil - @idle_done_cond = nil - @logout_command_tag = nil - @debug_output_bol = true - @exception = nil - - @greeting = get_response - if @greeting.nil? - raise Error, "connection closed" - end - record_untagged_response_code @greeting - @capabilities = capabilities_from_resp_code @greeting - if @greeting.name == "BYE" - raise ByeResponseError, @greeting - end + start_imap_connection(ssl_ctx_params) - @client_thread = Thread.current - @receiver_thread = Thread.start { - begin - receive_responses - rescue Exception - end - } - @receiver_thread_terminating = false - rescue Exception - @sock.close - raise - end + # DEPRECATED: to remove in next version + @client_thread = Thread.current end # Returns true after the TLS negotiation has completed and the remote @@ -2357,6 +2337,47 @@ def remove_response_handler(handler) @@debug = false + def convert_deprecated_options( + port_or_options = {}, usessl = false, certs = nil, verify = true + ) + port_or_options.to_hash + rescue NoMethodError + # for backward compatibility + options = {} + options[:port] = port_or_options + if usessl + options[:ssl] = create_ssl_params(certs, verify) + end + options + end + + def start_imap_connection(ssl_ctx_params) + start_tls_session(ssl_ctx_params) if ssl_ctx_params + @greeting = get_server_greeting + @capabilities = capabilities_from_resp_code @greeting + @receiver_thread = start_receiver_thread + rescue Exception + @sock.close + raise + end + + def get_server_greeting + greeting = get_response + raise Error, "No server greeting - connection closed" unless greeting + record_untagged_response_code greeting + raise ByeResponseError, greeting if greeting.name == "BYE" + greeting + end + + def start_receiver_thread + Thread.start do + receive_responses + rescue Exception => ex + @receiver_thread_exception = ex + # don't exit the thread with an exception + end + end + def tcp_socket(host, port) s = Socket.tcp(host, port, :connect_timeout => @open_timeout) s.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, true) From 68999a919cad29f7b0c78f589eefbf8ff72c952a Mon Sep 17 00:00:00 2001 From: nick evans Date: Thu, 14 Sep 2023 09:11:17 -0400 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=94=92=20Add=20ssl=5Fctx=20and=20ssl?= =?UTF-8?q?=5Fctx=5Fparams=20attr=20readers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Net::IMAP#ssl_ctx` will return the `OpenSSL::SSL::SSLContext` used by the `OpenSSL::SSL::SSLSocket` when TLS is attempted, even when the TLS handshake is unsuccessful. The context object will be frozen. It will return `nil` for a plaintext connection. `Net::IMAP#ssl_ctx_params` will return the parameters that were sent to `OpenSSL::SSL::SSLContext#set_params` when the connection tries to use TLS (even when unsuccessful). It will return `false` for a plaintext connection. Additionally, error handling for the arguments to `#starttls` was moved *before* sending the command to the server. This way a simple argument error will still raise an exception, but won't result in dropping the connection. Because `ssl_ctx` is also created ahead of time, this will also catch invalid SSLContext params. --- lib/net/imap.rb | 78 +++++++++++++++++++++++--------------- test/net/imap/test_imap.rb | 47 ++++++++++++++++++++--- 2 files changed, 89 insertions(+), 36 deletions(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 87ec7ca2..b8cb6fd9 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -722,6 +722,21 @@ class << self # The port this client connected to attr_reader :port + # Returns the + # {SSLContext}[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html] + # used by the SSLSocket when TLS is attempted, even when the TLS handshake + # is unsuccessful. The context object will be frozen. + # + # Returns +nil+ for a plaintext connection. + attr_reader :ssl_ctx + + # Returns the parameters that were sent to #ssl_ctx + # {set_params}[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html#method-i-set_params] + # when the connection tries to use TLS (even when unsuccessful). + # + # Returns +false+ for a plaintext connection. + attr_reader :ssl_ctx_params + # Creates a new Net::IMAP object and connects it to the specified # +host+. # @@ -804,7 +819,7 @@ def initialize(host, options = {}, *deprecated) @port = options[:port] || (options[:ssl] ? SSL_PORT : PORT) @open_timeout = options[:open_timeout] || 30 @idle_response_timeout = options[:idle_response_timeout] || 5 - ssl_ctx_params = options[:ssl] + @ssl_ctx_params, @ssl_ctx = build_ssl_ctx(options[:ssl]) # Basic Client State @utf8_strings = false @@ -835,7 +850,8 @@ def initialize(host, options = {}, *deprecated) # Connection @tls_verified = false @sock = tcp_socket(@host, @port) - start_imap_connection(ssl_ctx_params) + start_tls_session if ssl_ctx + start_imap_connection # DEPRECATED: to remove in next version @client_thread = Thread.current @@ -1083,17 +1099,18 @@ def logout # Cached #capabilities will be cleared when this method completes. # def starttls(options = {}, verify = true) + begin + # for backward compatibility + certs = options.to_str + options = create_ssl_params(certs, verify) + rescue NoMethodError + end + @ssl_ctx_params, @ssl_ctx = build_ssl_ctx(options || {}) send_command("STARTTLS") do |resp| if resp.kind_of?(TaggedResponse) && resp.name == "OK" - begin - # for backward compatibility - certs = options.to_str - options = create_ssl_params(certs, verify) - rescue NoMethodError - end clear_cached_capabilities clear_responses - start_tls_session(options) + start_tls_session end end end @@ -2351,8 +2368,7 @@ def convert_deprecated_options( options end - def start_imap_connection(ssl_ctx_params) - start_tls_session(ssl_ctx_params) if ssl_ctx_params + def start_imap_connection @greeting = get_server_greeting @capabilities = capabilities_from_resp_code @greeting @receiver_thread = start_receiver_thread @@ -2664,6 +2680,21 @@ def normalize_searching_criteria(keys) end end + def build_ssl_ctx(ssl) + if ssl + params = (Hash.try_convert(ssl) || {}).freeze + context = SSLContext.new + context.set_params(params) + if defined?(VerifyCallbackProc) + context.verify_callback = VerifyCallbackProc + end + context.freeze + [params, context] + else + false + end + end + def create_ssl_params(certs = nil, verify = true) params = {} if certs @@ -2681,28 +2712,15 @@ def create_ssl_params(certs = nil, verify = true) return params end - def start_tls_session(params = {}) - unless defined?(OpenSSL::SSL) - raise "SSL extension not installed" - end - if @sock.kind_of?(OpenSSL::SSL::SSLSocket) - raise RuntimeError, "already using SSL" - end - begin - params = params.to_hash - rescue NoMethodError - params = {} - end - context = SSLContext.new - context.set_params(params) - if defined?(VerifyCallbackProc) - context.verify_callback = VerifyCallbackProc - end - @sock = SSLSocket.new(@sock, context) + def start_tls_session + raise "SSL extension not installed" unless defined?(OpenSSL::SSL) + raise "already using SSL" if @sock.kind_of?(OpenSSL::SSL::SSLSocket) + raise "cannot start TLS without SSLContext" unless ssl_ctx + @sock = SSLSocket.new(@sock, ssl_ctx) @sock.sync_close = true @sock.hostname = @host if @sock.respond_to? :hostname= ssl_socket_connect(@sock, @open_timeout) - if context.verify_mode != VERIFY_NONE + if ssl_ctx.verify_mode != VERIFY_NONE @sock.post_connection_check(@host) @tls_verified = true end diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb index 0f89691e..8baf67b4 100644 --- a/test/net/imap/test_imap.rb +++ b/test/net/imap/test_imap.rb @@ -57,6 +57,10 @@ def test_imaps_with_ca_file end assert_equal true, verified assert_equal true, imap.tls_verified? + assert_equal({ca_file: CA_FILE}, imap.ssl_ctx_params) + assert_equal(CA_FILE, imap.ssl_ctx.ca_file) + assert_equal(OpenSSL::SSL::VERIFY_PEER, imap.ssl_ctx.verify_mode) + assert imap.ssl_ctx.verify_hostname end def test_imaps_verify_none @@ -76,6 +80,10 @@ def test_imaps_verify_none end assert_equal false, verified assert_equal false, imap.tls_verified? + assert_equal({verify_mode: OpenSSL::SSL::VERIFY_NONE}, + imap.ssl_ctx_params) + assert_equal(nil, imap.ssl_ctx.ca_file) + assert_equal(OpenSSL::SSL::VERIFY_NONE, imap.ssl_ctx.verify_mode) end def test_imaps_post_connection_check @@ -92,16 +100,41 @@ def test_imaps_post_connection_check end if defined?(OpenSSL::SSL) + def test_starttls_unknown_ca + imap = nil + assert_raise(OpenSSL::SSL::SSLError) do + ex = nil + starttls_test do |port| + imap = Net::IMAP.new("localhost", port: port) + imap.starttls + imap + rescue => ex + imap + end + raise ex if ex + end + assert_equal false, imap.tls_verified? + assert_equal({}, imap.ssl_ctx_params) + assert_equal(nil, imap.ssl_ctx.ca_file) + assert_equal(OpenSSL::SSL::VERIFY_PEER, imap.ssl_ctx.verify_mode) + end + def test_starttls - verified, imap = :unknown, nil + initial_verified, initial_ctx, initial_params = :unknown, :unknown, :unknown + imap = nil starttls_test do |port| imap = Net::IMAP.new("localhost", :port => port) + initial_verified = imap.tls_verified? + initial_params = imap.ssl_ctx_params + initial_ctx = imap.ssl_ctx imap.starttls(:ca_file => CA_FILE) - verified = imap.tls_verified? imap end - assert_equal true, verified + assert_equal false, initial_verified + assert_equal false, initial_params + assert_equal nil, initial_ctx assert_equal true, imap.tls_verified? + assert_equal({ca_file: CA_FILE}, imap.ssl_ctx_params) rescue SystemCallError skip $! ensure @@ -111,17 +144,18 @@ def test_starttls end def test_starttls_stripping - verified, imap = :unknown, nil + imap = nil starttls_stripping_test do |port| imap = Net::IMAP.new("localhost", :port => port) assert_raise(Net::IMAP::UnknownResponseError) do imap.starttls(:ca_file => CA_FILE) end - verified = imap.tls_verified? imap end - assert_equal false, verified assert_equal false, imap.tls_verified? + assert_equal({ca_file: CA_FILE}, imap.ssl_ctx_params) + assert_equal(CA_FILE, imap.ssl_ctx.ca_file) + assert_equal(OpenSSL::SSL::VERIFY_PEER, imap.ssl_ctx.verify_mode) end end @@ -1123,6 +1157,7 @@ def starttls_test sock.gets sock.print("* BYE terminating connection\r\n") sock.print("RUBY0002 OK LOGOUT completed\r\n") + rescue OpenSSL::SSL::SSLError ensure sock.close server.close