From 40bff51aac21c985d75edac03ec24cbf98b8ab49 Mon Sep 17 00:00:00 2001 From: nick evans Date: Wed, 9 Sep 2026 12:25:38 -0400 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=93=9A=20Link=20to=20SSLSocket=20in?= =?UTF-8?q?=20ssl=5Fctx=20rdoc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/net/imap.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 54753b5a..d831e16f 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -1035,8 +1035,10 @@ def max_response_size=(val) config.max_response_size = val end # 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. + # used by the + # {OpenSSL::SSL::SSLSocket}[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLSocket.html]. + # when TLS is attempted, even when the TLS handshake is unsuccessful. The + # context object will be frozen. # # Returns +nil+ for a plaintext connection. # From 9dc5d1dd4547b6f71dbcae2f02370f1e1b4e758f Mon Sep 17 00:00:00 2001 From: nick evans Date: Wed, 9 Sep 2026 12:38:19 -0400 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=94=93=EF=B8=8F=20Add=20`tls=5Fsocket?= =?UTF-8?q?=3F`=20and=20`tls=5Fconnected=3F`=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Please note that these are for connection inspection and debug only, since they do _not_ indicate a secure TLS session has been established. --- lib/net/imap.rb | 41 +++++++++++++++++++++++++++++----- test/net/imap/test_imap_tls.rb | 8 ++++++- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index d831e16f..3ee77208 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -404,6 +404,12 @@ module Net # - #connection_state: Returns the connection state. # - #disconnected?: True if the connection has been closed. # - #tls_verified?: Returns whether TLS is used and #host has been verified. + # - #tls_connected?: Returns +true+ after TLS negotiation has completed. + # + # *NOTE:* This does _NOT_ indicate a secure TLS connection. + # - #tls_socket?: Returns +true+ after TLS negotiation has started. + # + # *NOTE:* This does _NOT_ indicate a secure TLS connection. # - #ssl_ctx: Returns the {SSLContext}[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html] # after attempting to start TLS. # @@ -1301,12 +1307,11 @@ def inspect end private def inspect_tls_state - if tls_verified? - "TLS" - elsif ssl_ctx && @sock.kind_of?(OpenSSL::SSL::SSLSocket) - "TLS (#{@tls_connected ? "NOT VERIFIED" : "NOT ESTABLISHED"})" - else - "PLAINTEXT#{" (TLS NOT STARTED)" if ssl_ctx}" + if tls_verified? then "TLS" + elsif tls_connected? then "TLS (NOT VERIFIED)" + elsif tls_socket? then "TLS (NOT ESTABLISHED)" + elsif ssl_ctx then "PLAINTEXT (TLS NOT STARTED)" + else "PLAINTEXT" end end @@ -1315,6 +1320,30 @@ def inspect # but peer verification was disabled. def tls_verified?; @tls_verified end + # Returns +true+ after + # {OpenSSL::SSL::SSLSocket#connect}[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLSocket.html#method-i-connect] + # completes successfully. + # + # *NOTE:* This does _NOT_ indicate that the remote hostname has been + # verified. + # + # This does _not_ indicate current connection state. It will continue to + # return +true+ even after a successful connection has disconnected. + # + # See #tls_verified? + def tls_connected?; @tls_connected end + + # Returns +true+ when the connection is a + # {OpenSSL::SSL::SSLSocket}[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLSocket.html] + # + # *NOTE:* This does _NOT_ indicate that a TLS session has been + # established or that remote hostname has been verified. + # + # This only indicates that TLS negotiation has started. + # + # See #tls_verified? + def tls_socket?; @sock.kind_of?(OpenSSL::SSL::SSLSocket) end + # Disconnects from the server. # # Waits for receiver thread to close before returning, except when called diff --git a/test/net/imap/test_imap_tls.rb b/test/net/imap/test_imap_tls.rb index b5873299..12ad1476 100644 --- a/test/net/imap/test_imap_tls.rb +++ b/test/net/imap/test_imap_tls.rb @@ -35,6 +35,8 @@ def assert_tls_incomplete(imap) = assert_tls_stage imap, :incomplete def assert_tls_unverified(imap) = assert_tls_stage imap, :unverified def assert_tls_verified(imap) = assert_tls_stage imap, :verified + STAGES = %i[unstarted incomplete unverified verified].freeze + INSPECT_INCLUDES = { unstarted: " PLAINTEXT (TLS NOT STARTED) disconnected", incomplete: " TLS (NOT ESTABLISHED) disconnected", @@ -43,12 +45,16 @@ def assert_tls_verified(imap) = assert_tls_stage imap, :verified } def assert_tls_stage(imap, stage) - verified = stage == :verified + socket, connected, verified = Array.new(3) { _1 < STAGES.index(stage) } assert imap.ssl_ctx_params.frozen?, "#ssl_ctx_params should be frozen" assert_kind_of Hash, imap.ssl_ctx_params, "#ssl_ctx_params should be a Hash" assert_kind_of OpenSSL::SSL::SSLContext, imap.ssl_ctx, "#ssl_ctx should be an OpenSSL::SSL::SSLContext" + assert_equal socket, imap.tls_socket?, + "#tls_socket? should be #{socket}" + assert_equal connected, imap.tls_connected?, + "#tls_connected? should be #{connected}" assert_equal verified, imap.tls_verified?, "#tls_verified? should be #{verified}" assert_include imap.inspect, INSPECT_INCLUDES.fetch(stage)