Skip to content

Commit 673cab8

Browse files
committed
✅ Fix backport to not-imap 0.3 and ruby 2.6
For the net-imap v0.3 backport, two major changes were needed: * the tests needed to be almost completely rewritten because FakeServer was added for v0.4. * `max_response_size` needed to be on Net::IMAP directly, because Config was added for v0.4.
1 parent 450bb4d commit 673cab8

File tree

4 files changed

+118
-45
lines changed

4 files changed

+118
-45
lines changed

lib/net/imap.rb

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ module Net
133133
#
134134
# Use paginated or limited versions of commands whenever possible.
135135
#
136-
# Use Config#max_response_size to impose a limit on incoming server responses
136+
# Use #max_response_size to impose a limit on incoming server responses
137137
# as they are being read. <em>This is especially important for untrusted
138138
# servers.</em>
139139
#
@@ -288,6 +288,40 @@ class IMAP < Protocol
288288
# Seconds to wait until an IDLE response is received.
289289
attr_reader :idle_response_timeout
290290

291+
# The maximum allowed server response size. When +nil+, there is no limit
292+
# on response size.
293+
#
294+
# The default value is _unlimited_ (after +v0.5.8+, the default is 512 MiB).
295+
# A _much_ lower value should be used with untrusted servers (for example,
296+
# when connecting to a user-provided hostname). When using a lower limit,
297+
# message bodies should be fetched in chunks rather than all at once.
298+
#
299+
# <em>Please Note:</em> this only limits the size per response. It does
300+
# not prevent a flood of individual responses and it does not limit how
301+
# many unhandled responses may be stored on the responses hash. See
302+
# Net::IMAP@Unbounded+memory+use.
303+
#
304+
# Socket reads are limited to the maximum remaining bytes for the current
305+
# response: max_response_size minus the bytes that have already been read.
306+
# When the limit is reached, or reading a +literal+ _would_ go over the
307+
# limit, ResponseTooLargeError is raised and the connection is closed.
308+
# See also #socket_read_limit.
309+
#
310+
# Note that changes will not take effect immediately, because the receiver
311+
# thread may already be waiting for the next response using the previous
312+
# value. Net::IMAP#noop can force a response and enforce the new setting
313+
# immediately.
314+
#
315+
# ==== Versioned Defaults
316+
#
317+
# Net::IMAP#max_response_size <em>was added in +v0.2.5+ and +v0.3.9+ as an
318+
# attr_accessor, and in +v0.4.20+ and +v0.5.7+ as a delegator to a config
319+
# attribute.</em>
320+
#
321+
# * original: +nil+ <em>(no limit)</em>
322+
# * +0.5+: 512 MiB
323+
attr_accessor :max_response_size
324+
291325
# The thread to receive exceptions.
292326
attr_accessor :client_thread
293327

@@ -317,17 +351,6 @@ class << self
317351
alias default_ssl_port default_tls_port
318352
end
319353

320-
##
321-
# :attr_accessor: max_response_size
322-
#
323-
# The maximum allowed server response size, in bytes.
324-
# Delegates to {config.max_response_size}[rdoc-ref:Config#max_response_size].
325-
326-
# :stopdoc:
327-
def max_response_size; config.max_response_size end
328-
def max_response_size=(val) config.max_response_size = val end
329-
# :startdoc:
330-
331354
# Disconnects from the server.
332355
def disconnect
333356
return if disconnected?
@@ -1129,6 +1152,7 @@ def idle_done
11291152
# that the greeting is handled in the current thread,
11301153
# but all other responses are handled in the receiver
11311154
# thread.
1155+
# max_response_size:: See #max_response_size.
11321156
#
11331157
# The most common errors are:
11341158
#
@@ -1159,6 +1183,7 @@ def initialize(host, port_or_options = {},
11591183
@tagno = 0
11601184
@open_timeout = options[:open_timeout] || 30
11611185
@idle_response_timeout = options[:idle_response_timeout] || 5
1186+
@max_response_size = options[:max_response_size]
11621187
@parser = ResponseParser.new
11631188
@sock = tcp_socket(@host, @port)
11641189
@reader = ResponseReader.new(self, @sock)

lib/net/imap/errors.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def initialize(msg = nil, *args,
3232
"Response size", response_size_msg, "exceeds max_response_size",
3333
max_response_size && "(#{max_response_size}B)",
3434
].compact.join(" ")
35+
return super(msg, *args) if kwargs.empty? # ruby 2.6 compatibility
3536
super(msg, *args, **kwargs)
3637
end
3738

test/net/imap/test_imap_max_response_size.rb

Lines changed: 78 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,10 @@
22

33
require "net/imap"
44
require "test/unit"
5-
require_relative "fake_server"
65

76
class IMAPMaxResponseSizeTest < Test::Unit::TestCase
8-
include Net::IMAP::FakeServer::TestHelper
97

108
def setup
11-
Net::IMAP.config.reset
129
@do_not_reverse_lookup = Socket.do_not_reverse_lookup
1310
Socket.do_not_reverse_lookup = true
1411
@threads = []
@@ -23,45 +20,95 @@ def teardown
2320
end
2421

2522
test "#max_response_size reading literals" do
26-
with_fake_server(preauth: true) do |server, imap|
23+
_, port = with_server_socket do |sock|
24+
sock.gets # => NOOP
25+
sock.print("RUBY0001 OK done\r\n")
26+
sock.gets # => NOOP
27+
sock.print("* 1 FETCH (BODY[] {12345}\r\n" + "a" * 12_345 + ")\r\n")
28+
sock.print("RUBY0002 OK done\r\n")
29+
"RUBY0003"
30+
end
31+
Timeout.timeout(5) do
32+
imap = Net::IMAP.new("localhost", port: port, max_response_size: 640 << 20)
33+
assert_equal 640 << 20, imap.max_response_size
2734
imap.max_response_size = 12_345 + 30
28-
server.on("NOOP") do |resp|
29-
resp.untagged("1 FETCH (BODY[] {12345}\r\n" + "a" * 12_345 + ")")
30-
resp.done_ok
31-
end
32-
imap.noop
33-
assert_equal "a" * 12_345, imap.responses("FETCH").first.message
35+
assert_equal 12_345 + 30, imap.max_response_size
36+
imap.noop # to reset the get_response limit
37+
imap.noop # to send the FETCH
38+
assert_equal "a" * 12_345, imap.responses["FETCH"].first.attr["BODY[]"]
39+
ensure
40+
imap.logout rescue nil
41+
imap.disconnect rescue nil
3442
end
3543
end
3644

3745
test "#max_response_size closes connection for too long line" do
38-
Net::IMAP.config.max_response_size = 10
39-
run_fake_server_in_thread(preauth: false, ignore_io_error: true) do |server|
40-
assert_raise_with_message(
41-
Net::IMAP::ResponseTooLargeError, /exceeds max_response_size .*\b10B\b/
42-
) do
43-
with_client("localhost", port: server.port) do
44-
fail "should not get here (greeting longer than max_response_size)"
45-
end
46-
end
46+
_, port = with_server_socket do |sock|
47+
sock.gets or next # => never called
48+
fail "client disconnects first"
49+
end
50+
assert_raise_with_message(
51+
Net::IMAP::ResponseTooLargeError, /exceeds max_response_size .*\b10B\b/
52+
) do
53+
Net::IMAP.new("localhost", port: port, max_response_size: 10)
54+
fail "should not get here (greeting longer than max_response_size)"
4755
end
4856
end
4957

5058
test "#max_response_size closes connection for too long literal" do
51-
Net::IMAP.config.max_response_size = 1<<20
52-
with_fake_server(preauth: false, ignore_io_error: true) do |server, client|
53-
client.max_response_size = 50
54-
server.on("NOOP") do |resp|
55-
resp.untagged("1 FETCH (BODY[] {1000}\r\n" + "a" * 1000 + ")")
56-
end
57-
assert_raise_with_message(
58-
Net::IMAP::ResponseTooLargeError,
59-
/\d+B read \+ 1000B literal.* exceeds max_response_size .*\b50B\b/
60-
) do
61-
client.noop
62-
fail "should not get here (FETCH literal longer than max_response_size)"
59+
_, port = with_server_socket(ignore_io_error: true) do |sock|
60+
sock.gets # => NOOP
61+
sock.print "* 1 FETCH (BODY[] {1000}\r\n" + "a" * 1000 + ")\r\n"
62+
sock.print("RUBY0001 OK done\r\n")
63+
end
64+
client = Net::IMAP.new("localhost", port: port, max_response_size: 1000)
65+
assert_equal 1000, client.max_response_size
66+
client.max_response_size = 50
67+
assert_equal 50, client.max_response_size
68+
assert_raise_with_message(
69+
Net::IMAP::ResponseTooLargeError,
70+
/\d+B read \+ 1000B literal.* exceeds max_response_size .*\b50B\b/
71+
) do
72+
client.noop
73+
fail "should not get here (FETCH literal longer than max_response_size)"
74+
end
75+
end
76+
77+
def with_server_socket(ignore_io_error: false)
78+
server = create_tcp_server
79+
port = server.addr[1]
80+
start_server do
81+
Timeout.timeout(5) do
82+
sock = server.accept
83+
sock.print("* OK connection established\r\n")
84+
logout_tag = yield sock if block_given?
85+
sock.gets # => LOGOUT
86+
sock.print("* BYE terminating connection\r\n")
87+
sock.print("#{logout_tag} OK LOGOUT completed\r\n") if logout_tag
88+
rescue IOError, EOFError, Errno::ECONNABORTED, Errno::ECONNRESET,
89+
Errno::EPIPE, Errno::ETIMEDOUT
90+
ignore_io_error or raise
91+
ensure
92+
sock.close rescue nil
93+
server.close rescue nil
6394
end
6495
end
96+
return server, port
97+
end
98+
99+
def start_server
100+
th = Thread.new do
101+
yield
102+
end
103+
@threads << th
104+
sleep 0.1 until th.stop?
65105
end
66106

107+
def create_tcp_server
108+
return TCPServer.new(server_addr, 0)
109+
end
110+
111+
def server_addr
112+
Addrinfo.tcp("localhost", 0).ip_address
113+
end
67114
end

test/net/imap/test_response_reader.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
class ResponseReaderTest < Test::Unit::TestCase
88
class FakeClient
9-
def max_response_size; config.max_response_size end
9+
attr_accessor :max_response_size
1010
end
1111

1212
def literal(str) "{#{str.bytesize}}\r\n#{str}" end
@@ -47,7 +47,7 @@ def literal(str) "{#{str.bytesize}}\r\n#{str}" end
4747

4848
test "#read_response_buffer with max_response_size" do
4949
client = FakeClient.new
50-
client.config.max_response_size = 10
50+
client.max_response_size = 10
5151
under = "+ 3456\r\n"
5252
exact = "+ 345678\r\n"
5353
over = "+ 3456789\r\n"

0 commit comments

Comments
 (0)