diff --git a/lib/net/imap.rb b/lib/net/imap.rb index b9f718ebd..03f01f21c 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -765,6 +765,7 @@ class IMAP < Protocol "UTF8=ONLY" => "UTF8=ACCEPT", }.freeze + autoload :ResponseReader, File.expand_path("imap/response_reader", __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__) @@ -998,6 +999,7 @@ def initialize(host, port: nil, ssl: nil, response_handlers: nil, # Connection @tls_verified = false @sock = tcp_socket(@host, @port) + @reader = ResponseReader.new(self, @sock) start_tls_session if ssl_ctx start_imap_connection @@ -2923,30 +2925,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 @@ -3145,6 +3129,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 000000000..57770e3b8 --- /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 000000000..319dece22 --- /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 + end + + def literal(str) "{#{str.bytesize}}\r\n#{str}" end + + 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