From 4cc1cb9d2fdde6b9ab0374bfe45a11ac73aea89a Mon Sep 17 00:00:00 2001 From: nick evans Date: Tue, 15 Apr 2025 06:42:58 -0400 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=8E=A8=20Reformat=20autoloads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/net/imap.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index f636fe1c..5286653a 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -792,10 +792,11 @@ class IMAP < Protocol "UTF8=ONLY" => "UTF8=ACCEPT", }.freeze - autoload :ConnectionState, File.expand_path("imap/connection_state", __dir__) - autoload :SASL, File.expand_path("imap/sasl", __dir__) - autoload :SASLAdapter, File.expand_path("imap/sasl_adapter", __dir__) - autoload :StringPrep, File.expand_path("imap/stringprep", __dir__) + dir = File.expand_path("imap", __dir__) + autoload :ConnectionState, "#{dir}/connection_state" + autoload :SASL, "#{dir}/sasl" + autoload :SASLAdapter, "#{dir}/sasl_adapter" + autoload :StringPrep, "#{dir}/stringprep" include MonitorMixin if defined?(OpenSSL::SSL) From 18bc62150df697596510c6f47a765155e4d6c0f9 Mon Sep 17 00:00:00 2001 From: nick evans Date: Wed, 2 Apr 2025 20:50:15 -0400 Subject: [PATCH 2/2] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extract=20ResponseRead?= =?UTF-8?q?er=20from=20get=5Fresponse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It's nice to extract a little bit of the complexity from the core `Net::IMAP` class. But my primary motivation was so that I could directly test this code quickly and in isolation from needing to simulate a full IMAP connection. --- lib/net/imap.rb | 23 +++---------- lib/net/imap/response_reader.rb | 38 +++++++++++++++++++++ test/net/imap/test_response_reader.rb | 49 +++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 19 deletions(-) create mode 100644 lib/net/imap/response_reader.rb create mode 100644 test/net/imap/test_response_reader.rb diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 5286653a..3dd82ef2 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -794,6 +794,7 @@ class IMAP < Protocol dir = File.expand_path("imap", __dir__) autoload :ConnectionState, "#{dir}/connection_state" + autoload :ResponseReader, "#{dir}/response_reader" autoload :SASL, "#{dir}/sasl" autoload :SASLAdapter, "#{dir}/sasl_adapter" autoload :StringPrep, "#{dir}/stringprep" @@ -1089,6 +1090,7 @@ def initialize(host, port: nil, ssl: nil, response_handlers: nil, # Connection @sock = tcp_socket(@host, @port) + @reader = ResponseReader.new(self, @sock) start_tls_session if ssl_ctx start_imap_connection end @@ -3446,30 +3448,12 @@ def get_tagged_response(tag, cmd, timeout = nil) end def get_response - buff = String.new - catch :eof do - while true - get_response_line(buff) - break unless /\{(\d+)\}\r\n\z/n =~ buff - get_response_literal(buff, $1.to_i) - end - end + buff = @reader.read_response_buffer return nil if buff.length == 0 $stderr.print(buff.gsub(/^/n, "S: ")) if config.debug? @parser.parse(buff) end - def get_response_line(buff) - line = @sock.gets(CRLF) or throw :eof - buff << line - end - - def get_response_literal(buff, literal_size) - literal = String.new(capacity: literal_size) - @sock.read(literal_size, literal) or throw :eof - buff << literal - end - ############################# # built-in response handlers @@ -3771,6 +3755,7 @@ def start_tls_session 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) + @reader = ResponseReader.new(self, @sock) @sock.sync_close = true @sock.hostname = @host if @sock.respond_to? :hostname= ssl_socket_connect(@sock, open_timeout) diff --git a/lib/net/imap/response_reader.rb b/lib/net/imap/response_reader.rb new file mode 100644 index 00000000..57770e3b --- /dev/null +++ b/lib/net/imap/response_reader.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Net + class IMAP + # See https://www.rfc-editor.org/rfc/rfc9051#section-2.2.2 + class ResponseReader # :nodoc: + attr_reader :client + + def initialize(client, sock) + @client, @sock = client, sock + end + + def read_response_buffer + buff = String.new + catch :eof do + while true + read_line(buff) + break unless /\{(\d+)\}\r\n\z/n =~ buff + read_literal(buff, $1.to_i) + end + end + buff + end + + private + + def read_line(buff) + buff << (@sock.gets(CRLF) or throw :eof) + end + + def read_literal(buff, literal_size) + literal = String.new(capacity: literal_size) + buff << (@sock.read(literal_size, literal) or throw :eof) + end + + end + end +end diff --git a/test/net/imap/test_response_reader.rb b/test/net/imap/test_response_reader.rb new file mode 100644 index 00000000..c24f1269 --- /dev/null +++ b/test/net/imap/test_response_reader.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require "net/imap" +require "stringio" +require "test/unit" + +class ResponseReaderTest < Test::Unit::TestCase + def setup + Net::IMAP.config.reset + end + + class FakeClient + def config = @config ||= Net::IMAP.config.new + end + + def literal(str) = "{#{str.bytesize}}\r\n#{str}" + + test "#read_response_buffer" do + client = FakeClient.new + aaaaaaaaa = "a" * (20 << 10) + many_crs = "\r" * 1000 + many_crlfs = "\r\n" * 500 + simple = "* OK greeting\r\n" + long_line = "tag ok #{aaaaaaaaa} #{aaaaaaaaa}\r\n" + literal_aaaa = "* fake #{literal aaaaaaaaa}\r\n" + literal_crlf = "tag ok #{literal many_crlfs} #{literal many_crlfs}\r\n" + illegal_crs = "tag ok #{many_crs} #{many_crs}\r\n" + illegal_lfs = "tag ok #{literal "\r"}\n#{literal "\r"}\n\r\n" + io = StringIO.new([ + simple, + long_line, + literal_aaaa, + literal_crlf, + illegal_crs, + illegal_lfs, + simple, + ].join) + rcvr = Net::IMAP::ResponseReader.new(client, io) + assert_equal simple, rcvr.read_response_buffer.to_str + assert_equal long_line, rcvr.read_response_buffer.to_str + assert_equal literal_aaaa, rcvr.read_response_buffer.to_str + assert_equal literal_crlf, rcvr.read_response_buffer.to_str + assert_equal illegal_crs, rcvr.read_response_buffer.to_str + assert_equal illegal_lfs, rcvr.read_response_buffer.to_str + assert_equal simple, rcvr.read_response_buffer.to_str + assert_equal "", rcvr.read_response_buffer.to_str + end + +end