Skip to content

♻️ Backport ResponseReaderto v0.3 #439

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Apr 21, 2025
Merged
25 changes: 9 additions & 16 deletions lib/net/imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,8 @@ module Net
class IMAP < Protocol
VERSION = "0.3.8"

autoload :ResponseReader, File.expand_path("imap/response_reader", __dir__)

include MonitorMixin
if defined?(OpenSSL::SSL)
include OpenSSL
Expand Down Expand Up @@ -2074,6 +2076,7 @@ def initialize(host, port_or_options = {},
@idle_response_timeout = options[:idle_response_timeout] || 5
@parser = ResponseParser.new
@sock = tcp_socket(@host, @port)
@reader = ResponseReader.new(self, @sock)
begin
if options[:ssl]
start_tls_session(options[:ssl])
Expand Down Expand Up @@ -2225,25 +2228,14 @@ def get_tagged_response(tag, cmd, timeout = nil)
end

def get_response
buff = String.new
while true
s = @sock.gets(CRLF)
break unless s
buff.concat(s)
if /\{(\d+)\}\r\n/n =~ s
s = @sock.read($1.to_i)
buff.concat(s)
else
break
end
end
buff = @reader.read_response_buffer
return nil if buff.length == 0
if @@debug
$stderr.print(buff.gsub(/^/n, "S: "))
end
return @parser.parse(buff)
$stderr.print(buff.gsub(/^/n, "S: ")) if @@debug
@parser.parse(buff)
end

#############################

def record_response(name, data)
unless @responses.has_key?(name)
@responses[name] = []
Expand Down Expand Up @@ -2421,6 +2413,7 @@ def start_tls_session(params = {})
context.verify_callback = VerifyCallbackProc
end
@sock = SSLSocket.new(@sock, context)
@reader = ResponseReader.new(self, @sock)
@sock.sync_close = true
@sock.hostname = @host if @sock.respond_to? :hostname=
ssl_socket_connect(@sock, @open_timeout)
Expand Down
46 changes: 46 additions & 0 deletions lib/net/imap/response_reader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# 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
break unless (@literal_size = get_literal_size)
read_literal
end
end
buff
ensure
@buff = nil
end

private

attr_reader :buff, :literal_size

def get_literal_size; /\{(\d+)\}\r\n\z/n =~ buff && $1.to_i end

def read_line
buff << (@sock.gets(CRLF) or throw :eof)
end

def read_literal
literal = String.new(capacity: literal_size)
buff << (@sock.read(literal_size, literal) or throw :eof)
ensure
@literal_size = nil
end

end
end
end
47 changes: 47 additions & 0 deletions test/net/imap/test_response_reader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

require "net/imap"
require "stringio"
require "test/unit"

class ResponseReaderTest < Test::Unit::TestCase
class FakeClient
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"
zero_literal = "tag ok #{literal ""} #{literal ""}\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,
zero_literal,
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 zero_literal, 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