Skip to content

Commit 18bc621

Browse files
committed
♻️ Extract ResponseReader from get_response
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.
1 parent 4cc1cb9 commit 18bc621

File tree

3 files changed

+91
-19
lines changed

3 files changed

+91
-19
lines changed

lib/net/imap.rb

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,7 @@ class IMAP < Protocol
794794

795795
dir = File.expand_path("imap", __dir__)
796796
autoload :ConnectionState, "#{dir}/connection_state"
797+
autoload :ResponseReader, "#{dir}/response_reader"
797798
autoload :SASL, "#{dir}/sasl"
798799
autoload :SASLAdapter, "#{dir}/sasl_adapter"
799800
autoload :StringPrep, "#{dir}/stringprep"
@@ -1089,6 +1090,7 @@ def initialize(host, port: nil, ssl: nil, response_handlers: nil,
10891090

10901091
# Connection
10911092
@sock = tcp_socket(@host, @port)
1093+
@reader = ResponseReader.new(self, @sock)
10921094
start_tls_session if ssl_ctx
10931095
start_imap_connection
10941096
end
@@ -3446,30 +3448,12 @@ def get_tagged_response(tag, cmd, timeout = nil)
34463448
end
34473449

34483450
def get_response
3449-
buff = String.new
3450-
catch :eof do
3451-
while true
3452-
get_response_line(buff)
3453-
break unless /\{(\d+)\}\r\n\z/n =~ buff
3454-
get_response_literal(buff, $1.to_i)
3455-
end
3456-
end
3451+
buff = @reader.read_response_buffer
34573452
return nil if buff.length == 0
34583453
$stderr.print(buff.gsub(/^/n, "S: ")) if config.debug?
34593454
@parser.parse(buff)
34603455
end
34613456

3462-
def get_response_line(buff)
3463-
line = @sock.gets(CRLF) or throw :eof
3464-
buff << line
3465-
end
3466-
3467-
def get_response_literal(buff, literal_size)
3468-
literal = String.new(capacity: literal_size)
3469-
@sock.read(literal_size, literal) or throw :eof
3470-
buff << literal
3471-
end
3472-
34733457
#############################
34743458
# built-in response handlers
34753459

@@ -3771,6 +3755,7 @@ def start_tls_session
37713755
raise "already using SSL" if @sock.kind_of?(OpenSSL::SSL::SSLSocket)
37723756
raise "cannot start TLS without SSLContext" unless ssl_ctx
37733757
@sock = SSLSocket.new(@sock, ssl_ctx)
3758+
@reader = ResponseReader.new(self, @sock)
37743759
@sock.sync_close = true
37753760
@sock.hostname = @host if @sock.respond_to? :hostname=
37763761
ssl_socket_connect(@sock, open_timeout)

lib/net/imap/response_reader.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# frozen_string_literal: true
2+
3+
module Net
4+
class IMAP
5+
# See https://www.rfc-editor.org/rfc/rfc9051#section-2.2.2
6+
class ResponseReader # :nodoc:
7+
attr_reader :client
8+
9+
def initialize(client, sock)
10+
@client, @sock = client, sock
11+
end
12+
13+
def read_response_buffer
14+
buff = String.new
15+
catch :eof do
16+
while true
17+
read_line(buff)
18+
break unless /\{(\d+)\}\r\n\z/n =~ buff
19+
read_literal(buff, $1.to_i)
20+
end
21+
end
22+
buff
23+
end
24+
25+
private
26+
27+
def read_line(buff)
28+
buff << (@sock.gets(CRLF) or throw :eof)
29+
end
30+
31+
def read_literal(buff, literal_size)
32+
literal = String.new(capacity: literal_size)
33+
buff << (@sock.read(literal_size, literal) or throw :eof)
34+
end
35+
36+
end
37+
end
38+
end

test/net/imap/test_response_reader.rb

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# frozen_string_literal: true
2+
3+
require "net/imap"
4+
require "stringio"
5+
require "test/unit"
6+
7+
class ResponseReaderTest < Test::Unit::TestCase
8+
def setup
9+
Net::IMAP.config.reset
10+
end
11+
12+
class FakeClient
13+
def config = @config ||= Net::IMAP.config.new
14+
end
15+
16+
def literal(str) = "{#{str.bytesize}}\r\n#{str}"
17+
18+
test "#read_response_buffer" do
19+
client = FakeClient.new
20+
aaaaaaaaa = "a" * (20 << 10)
21+
many_crs = "\r" * 1000
22+
many_crlfs = "\r\n" * 500
23+
simple = "* OK greeting\r\n"
24+
long_line = "tag ok #{aaaaaaaaa} #{aaaaaaaaa}\r\n"
25+
literal_aaaa = "* fake #{literal aaaaaaaaa}\r\n"
26+
literal_crlf = "tag ok #{literal many_crlfs} #{literal many_crlfs}\r\n"
27+
illegal_crs = "tag ok #{many_crs} #{many_crs}\r\n"
28+
illegal_lfs = "tag ok #{literal "\r"}\n#{literal "\r"}\n\r\n"
29+
io = StringIO.new([
30+
simple,
31+
long_line,
32+
literal_aaaa,
33+
literal_crlf,
34+
illegal_crs,
35+
illegal_lfs,
36+
simple,
37+
].join)
38+
rcvr = Net::IMAP::ResponseReader.new(client, io)
39+
assert_equal simple, rcvr.read_response_buffer.to_str
40+
assert_equal long_line, rcvr.read_response_buffer.to_str
41+
assert_equal literal_aaaa, rcvr.read_response_buffer.to_str
42+
assert_equal literal_crlf, rcvr.read_response_buffer.to_str
43+
assert_equal illegal_crs, rcvr.read_response_buffer.to_str
44+
assert_equal illegal_lfs, rcvr.read_response_buffer.to_str
45+
assert_equal simple, rcvr.read_response_buffer.to_str
46+
assert_equal "", rcvr.read_response_buffer.to_str
47+
end
48+
49+
end

0 commit comments

Comments
 (0)