From 47715f93a52ef0f9dedd69aa1574fd5ca4dcf253 Mon Sep 17 00:00:00 2001 From: nick evans Date: Fri, 31 Jul 2026 16:46:26 -0400 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9C=85=20Extract=20shared=20SimpleTCPSer?= =?UTF-8?q?verHelper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is partially deprecated. Most tests should use `FakeServer` instead. But it may continue to be used for tests that intentionally violate normal IMAP protocol behavior, for example when testing SSL error conditions, abrupt network disconnects, etc, where it's simpler to just use a straight TCP server socket. --- test/lib/helper.rb | 15 +++++++++++++++ test/net/imap/test_imap.rb | 18 ++---------------- test/net/imap/test_imap_append.rb | 20 ++------------------ 3 files changed, 19 insertions(+), 34 deletions(-) diff --git a/test/lib/helper.rb b/test/lib/helper.rb index 95865637..fe11727d 100644 --- a/test/lib/helper.rb +++ b/test/lib/helper.rb @@ -45,6 +45,21 @@ def teardown end end + module SimpleTCPServerHelper + private + + def start_server + th = Thread.new do + yield + end + @threads << th + sleep 0.001 until th.stop? + end + + def create_tcp_server = TCPServer.new(server_addr, 0) + def server_addr = Addrinfo.tcp("localhost", 0).ip_address + end + def wait_for_response_count(imap, type:, count:, timeout: 0.5, interval: 0.001) deadline = Time.now + timeout diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb index b178681c..47003e35 100644 --- a/test/net/imap/test_imap.rb +++ b/test/net/imap/test_imap.rb @@ -9,6 +9,8 @@ class IMAPTest < Net::IMAP::TestCase SERVER_KEY = File.expand_path("../fixtures/server.key", __dir__) SERVER_CERT = File.expand_path("../fixtures/server.crt", __dir__) + # TODO: convert to use Net::IMAP::FakeServer::TestHelper + include Net::IMAP::TestCase::SimpleTCPServerHelper include Net::IMAP::FakeServer::TestHelper if defined?(OpenSSL::SSL::SSLError) @@ -273,14 +275,6 @@ def test_starttls_stripping_ok_sent_before_response end end - def start_server - th = Thread.new do - yield - end - @threads << th - sleep 0.001 until th.stop? - end - def test_unexpected_eof server = create_tcp_server port = server.addr[1] @@ -1322,12 +1316,4 @@ def starttls_test end end - def create_tcp_server - return TCPServer.new(server_addr, 0) - end - - def server_addr - Addrinfo.tcp("localhost", 0).ip_address - end - end diff --git a/test/net/imap/test_imap_append.rb b/test/net/imap/test_imap_append.rb index ee139233..f4b9d282 100644 --- a/test/net/imap/test_imap_append.rb +++ b/test/net/imap/test_imap_append.rb @@ -7,6 +7,8 @@ class IMAPAppendTest < Net::IMAP::TestCase TEST_FIXTURE_PATH = File.join(__dir__, "fixtures/response_parser") + # TODO: convert all tests to use Net::IMAP::FakeServer::TestHelper + include Net::IMAP::TestCase::SimpleTCPServerHelper include Net::IMAP::FakeServer::TestHelper test "#append" do @@ -120,22 +122,4 @@ class IMAPAppendTest < Net::IMAP::TestCase end end - private - - def start_server - th = Thread.new do - yield - end - @threads << th - sleep 0.1 until th.stop? - end - - def create_tcp_server - return TCPServer.new(server_addr, 0) - end - - def server_addr - Addrinfo.tcp("localhost", 0).ip_address - end - end From 753414b457367eb680a2436b626271cef2eedb78 Mon Sep 17 00:00:00 2001 From: nick evans Date: Fri, 31 Jul 2026 16:54:14 -0400 Subject: [PATCH 2/6] =?UTF-8?q?=E2=9C=85=F0=9F=9A=9A=20Move=20IDLE=20tests?= =?UTF-8?q?=20into=20their=20own=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This matches the pattern used by most other current "integration tests". --- test/net/imap/test_imap.rb | 254 ------------------------------ test/net/imap/test_imap_idle.rb | 263 ++++++++++++++++++++++++++++++++ 2 files changed, 263 insertions(+), 254 deletions(-) create mode 100644 test/net/imap/test_imap_idle.rb diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb index 47003e35..7e4f8b58 100644 --- a/test/net/imap/test_imap.rb +++ b/test/net/imap/test_imap.rb @@ -362,197 +362,6 @@ def @reader.read_response_buffer end end - def test_idle - server = create_tcp_server - port = server.addr[1] - requests = [] - start_server do - sock = server.accept - begin - sock.print("* OK test server\r\n") - requests.push(sock.gets) - sock.print("+ idling\r\n") - sock.print("* 3 EXISTS\r\n") - sock.print("* 2 EXPUNGE\r\n") - requests.push(sock.gets) - sock.print("RUBY0001 OK IDLE terminated\r\n") - sock.gets - sock.print("* BYE terminating connection\r\n") - sock.print("RUBY0002 OK LOGOUT completed\r\n") - ensure - sock.close - server.close - end - end - - begin - imap = Net::IMAP.new(server_addr, :port => port) - responses = [] - imap.idle do |res| - responses.push(res) - if res.name == "EXPUNGE" - imap.idle_done - end - end - assert_equal(3, responses.length) - assert_instance_of(Net::IMAP::ContinuationRequest, responses[0]) - assert_equal("EXISTS", responses[1].name) - assert_equal(3, responses[1].data) - assert_equal("EXPUNGE", responses[2].name) - assert_equal(2, responses[2].data) - assert_equal(2, requests.length) - assert_equal("RUBY0001 IDLE\r\n", requests[0]) - assert_equal("DONE\r\n", requests[1]) - imap.logout - ensure - imap.disconnect if imap - end - end - - def test_exception_during_idle - server = create_tcp_server - port = server.addr[1] - requests = [] - start_server do - sock = server.accept - begin - sock.print("* OK test server\r\n") - requests.push(sock.gets) - sock.print("+ idling\r\n") - sock.print("* 3 EXISTS\r\n") - sock.print("* 2 EXPUNGE\r\n") - requests.push(sock.gets) - sock.print("RUBY0001 OK IDLE terminated\r\n") - sock.gets - sock.print("* BYE terminating connection\r\n") - sock.print("RUBY0002 OK LOGOUT completed\r\n") - ensure - sock.close - server.close - end - end - begin - imap = Net::IMAP.new(server_addr, :port => port) - begin - th = Thread.current - m = Monitor.new - in_idle = false - exception_raised = false - c = m.new_cond - raiser = Thread.start do - m.synchronize do - until in_idle - c.wait(0.1) - end - end - th.raise(Interrupt) - m.synchronize do - exception_raised = true - c.signal - end - end - @threads << raiser - imap.idle do |res| - m.synchronize do - in_idle = true - c.signal - until exception_raised - c.wait(0.1) - end - end - end - rescue Interrupt - end - assert_equal(2, requests.length) - assert_equal("RUBY0001 IDLE\r\n", requests[0]) - assert_equal("DONE\r\n", requests[1]) - imap.logout - ensure - imap.disconnect if imap - raiser.kill unless in_idle - end - end - - def test_idle_done_not_during_idle - server = create_tcp_server - port = server.addr[1] - start_server do - sock = server.accept - begin - sock.print("* OK test server\r\n") - sleep 0.1 - ensure - sock.close - server.close - end - end - begin - imap = Net::IMAP.new(server_addr, :port => port) - assert_local_raise(Net::IMAP::Error) do - imap.idle_done - end - ensure - imap.disconnect if imap - end - end - - def test_idle_timeout - server = create_tcp_server - port = server.addr[1] - requests = [] - start_server do - sock = server.accept - begin - sock.print("* OK test server\r\n") - requests.push(sock.gets) - sock.print("+ idling\r\n") - sock.print("* 3 EXISTS\r\n") - sock.print("* 2 EXPUNGE\r\n") - requests.push(sock.gets) - sock.print("RUBY0001 OK IDLE terminated\r\n") - sock.gets - sock.print("* BYE terminating connection\r\n") - sock.print("RUBY0002 OK LOGOUT completed\r\n") - ensure - sock.close - server.close - end - end - - begin - imap = Net::IMAP.new(server_addr, :port => port) - responses = [] - Thread.pass - imap.idle(0.2) do |res| - responses.push(res) - end - # There is no guarantee that this thread has received all the responses, - # so check the response length. - if responses.length > 0 - assert_instance_of(Net::IMAP::ContinuationRequest, responses[0]) - if responses.length > 1 - assert_equal("EXISTS", responses[1].name) - assert_equal(3, responses[1].data) - if responses.length > 2 - assert_equal("EXPUNGE", responses[2].name) - assert_equal(2, responses[2].data) - end - end - end - # Also, there is no guarantee that the server thread has stored - # all the requests into the array, so check the length. - if requests.length > 0 - assert_equal("RUBY0001 IDLE\r\n", requests[0]) - if requests.length > 1 - assert_equal("DONE\r\n", requests[1]) - end - end - imap.logout - ensure - imap.disconnect if imap - end - end - def test_unexpected_bye server = create_tcp_server port = server.addr[1] @@ -607,69 +416,6 @@ def @sock.shutdown(*args) end end - def test_connection_closed_during_idle - server = create_tcp_server - port = server.addr[1] - requests = [] - sock = nil - threads = [] - started = false - threads << Thread.start do - started = true - begin - sock = server.accept - sock.print("* OK test server\r\n") - requests.push(sock.gets) - sock.print("+ idling\r\n") - rescue IOError # sock is closed by another thread - ensure - server.close - end - end - sleep 0.001 until started - threads << Thread.start do - imap = Net::IMAP.new(server_addr, :port => port) - begin - m = Monitor.new - in_idle = false - closed = false - c = m.new_cond - threads << Thread.start do - m.synchronize do - until in_idle - c.wait(0.1) - end - end - sock.close - m.synchronize do - closed = true - c.signal - end - end - assert_local_raise(EOFError) do - imap.idle do |res| - m.synchronize do - in_idle = true - c.signal - until closed - c.wait(0.1) - end - end - end - end - assert_equal(1, requests.length) - assert_equal("RUBY0001 IDLE\r\n", requests[0]) - ensure - imap.disconnect if imap - end - end - assert_join_threads(threads) - ensure - if sock && !sock.closed? - sock.close - end - end - def test_connection_closed_without_greeting server = create_tcp_server port = server.addr[1] diff --git a/test/net/imap/test_imap_idle.rb b/test/net/imap/test_imap_idle.rb new file mode 100644 index 00000000..796104c2 --- /dev/null +++ b/test/net/imap/test_imap_idle.rb @@ -0,0 +1,263 @@ +# frozen_string_literal: true + +require_relative "../../lib/helper" + +class IMAPIdleTest < Net::IMAP::TestCase + # TODO: convert to use Net::IMAP::FakeServer::TestHelper + include Net::IMAP::TestCase::SimpleTCPServerHelper + + def test_idle + server = create_tcp_server + port = server.addr[1] + requests = [] + start_server do + sock = server.accept + begin + sock.print("* OK test server\r\n") + requests.push(sock.gets) + sock.print("+ idling\r\n") + sock.print("* 3 EXISTS\r\n") + sock.print("* 2 EXPUNGE\r\n") + requests.push(sock.gets) + sock.print("RUBY0001 OK IDLE terminated\r\n") + sock.gets + sock.print("* BYE terminating connection\r\n") + sock.print("RUBY0002 OK LOGOUT completed\r\n") + ensure + sock.close + server.close + end + end + + begin + imap = Net::IMAP.new(server_addr, :port => port) + responses = [] + imap.idle do |res| + responses.push(res) + if res.name == "EXPUNGE" + imap.idle_done + end + end + assert_equal(3, responses.length) + assert_instance_of(Net::IMAP::ContinuationRequest, responses[0]) + assert_equal("EXISTS", responses[1].name) + assert_equal(3, responses[1].data) + assert_equal("EXPUNGE", responses[2].name) + assert_equal(2, responses[2].data) + assert_equal(2, requests.length) + assert_equal("RUBY0001 IDLE\r\n", requests[0]) + assert_equal("DONE\r\n", requests[1]) + imap.logout + ensure + imap.disconnect if imap + end + end + + def test_exception_during_idle + server = create_tcp_server + port = server.addr[1] + requests = [] + start_server do + sock = server.accept + begin + sock.print("* OK test server\r\n") + requests.push(sock.gets) + sock.print("+ idling\r\n") + sock.print("* 3 EXISTS\r\n") + sock.print("* 2 EXPUNGE\r\n") + requests.push(sock.gets) + sock.print("RUBY0001 OK IDLE terminated\r\n") + sock.gets + sock.print("* BYE terminating connection\r\n") + sock.print("RUBY0002 OK LOGOUT completed\r\n") + ensure + sock.close + server.close + end + end + begin + imap = Net::IMAP.new(server_addr, :port => port) + begin + th = Thread.current + m = Monitor.new + in_idle = false + exception_raised = false + c = m.new_cond + raiser = Thread.start do + m.synchronize do + until in_idle + c.wait(0.1) + end + end + th.raise(Interrupt) + m.synchronize do + exception_raised = true + c.signal + end + end + @threads << raiser + imap.idle do |res| + m.synchronize do + in_idle = true + c.signal + until exception_raised + c.wait(0.1) + end + end + end + rescue Interrupt + end + assert_equal(2, requests.length) + assert_equal("RUBY0001 IDLE\r\n", requests[0]) + assert_equal("DONE\r\n", requests[1]) + imap.logout + ensure + imap.disconnect if imap + raiser.kill unless in_idle + end + end + + def test_idle_done_not_during_idle + server = create_tcp_server + port = server.addr[1] + start_server do + sock = server.accept + begin + sock.print("* OK test server\r\n") + sleep 0.1 + ensure + sock.close + server.close + end + end + begin + imap = Net::IMAP.new(server_addr, :port => port) + assert_local_raise(Net::IMAP::Error) do + imap.idle_done + end + ensure + imap.disconnect if imap + end + end + + def test_idle_timeout + server = create_tcp_server + port = server.addr[1] + requests = [] + start_server do + sock = server.accept + begin + sock.print("* OK test server\r\n") + requests.push(sock.gets) + sock.print("+ idling\r\n") + sock.print("* 3 EXISTS\r\n") + sock.print("* 2 EXPUNGE\r\n") + requests.push(sock.gets) + sock.print("RUBY0001 OK IDLE terminated\r\n") + sock.gets + sock.print("* BYE terminating connection\r\n") + sock.print("RUBY0002 OK LOGOUT completed\r\n") + ensure + sock.close + server.close + end + end + + begin + imap = Net::IMAP.new(server_addr, :port => port) + responses = [] + Thread.pass + imap.idle(0.2) do |res| + responses.push(res) + end + # There is no guarantee that this thread has received all the responses, + # so check the response length. + if responses.length > 0 + assert_instance_of(Net::IMAP::ContinuationRequest, responses[0]) + if responses.length > 1 + assert_equal("EXISTS", responses[1].name) + assert_equal(3, responses[1].data) + if responses.length > 2 + assert_equal("EXPUNGE", responses[2].name) + assert_equal(2, responses[2].data) + end + end + end + # Also, there is no guarantee that the server thread has stored + # all the requests into the array, so check the length. + if requests.length > 0 + assert_equal("RUBY0001 IDLE\r\n", requests[0]) + if requests.length > 1 + assert_equal("DONE\r\n", requests[1]) + end + end + imap.logout + ensure + imap.disconnect if imap + end + end + + def test_connection_closed_during_idle + server = create_tcp_server + port = server.addr[1] + requests = [] + sock = nil + threads = [] + started = false + threads << Thread.start do + started = true + begin + sock = server.accept + sock.print("* OK test server\r\n") + requests.push(sock.gets) + sock.print("+ idling\r\n") + rescue IOError # sock is closed by another thread + ensure + server.close + end + end + sleep 0.001 until started + threads << Thread.start do + imap = Net::IMAP.new(server_addr, :port => port) + begin + m = Monitor.new + in_idle = false + closed = false + c = m.new_cond + threads << Thread.start do + m.synchronize do + until in_idle + c.wait(0.1) + end + end + sock.close + m.synchronize do + closed = true + c.signal + end + end + assert_local_raise(EOFError) do + imap.idle do |res| + m.synchronize do + in_idle = true + c.signal + until closed + c.wait(0.1) + end + end + end + end + assert_equal(1, requests.length) + assert_equal("RUBY0001 IDLE\r\n", requests[0]) + ensure + imap.disconnect if imap + end + end + assert_join_threads(threads) + ensure + if sock && !sock.closed? + sock.close + end + end + +end From 8c07e50c12dcedc08060fca7dad5d7c1448ada80 Mon Sep 17 00:00:00 2001 From: nick evans Date: Fri, 31 Jul 2026 16:56:42 -0400 Subject: [PATCH 3/6] =?UTF-8?q?=E2=9C=85=F0=9F=9A=9A=20Move=20ID=20test=20?= =?UTF-8?q?into=20its=20own=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This matches the pattern used by most other current "integration tests". --- test/net/imap/test_imap.rb | 49 ----------------------------- test/net/imap/test_imap_id.rb | 58 +++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 49 deletions(-) create mode 100644 test/net/imap/test_imap_id.rb diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb index 7e4f8b58..b6561e3e 100644 --- a/test/net/imap/test_imap.rb +++ b/test/net/imap/test_imap.rb @@ -927,55 +927,6 @@ def test_disconnect end end - def test_id - server = create_tcp_server - port = server.addr[1] - requests = Queue.new - server_id = {"name" => "test server", "version" => "v0.1.0"} - server_id_str = '("name" "test server" "version" "v0.1.0")' - @threads << Thread.start do - sock = server.accept - begin - sock.print("* OK test server\r\n") - requests.push(sock.gets) - # RFC 2971 very clearly states (in section 3.2): - # "a server MUST send a tagged ID response to an ID command." - # And yet... some servers report ID capability but won't the response. - sock.print("RUBY0001 OK ID completed\r\n") - requests.push(sock.gets) - sock.print("* ID #{server_id_str}\r\n") - sock.print("RUBY0002 OK ID completed\r\n") - requests.push(sock.gets) - sock.print("* ID #{server_id_str}\r\n") - sock.print("RUBY0003 OK ID completed\r\n") - requests.push(sock.gets) - sock.print("* BYE terminating connection\r\n") - sock.print("RUBY0004 OK LOGOUT completed\r\n") - ensure - sock.close - server.close - end - end - - begin - imap = Net::IMAP.new(server_addr, :port => port) - resp = imap.id - assert_equal(nil, resp) - assert_equal("RUBY0001 ID NIL\r\n", requests.pop) - resp = imap.id({}) - assert_equal(server_id, resp) - assert_equal("RUBY0002 ID ()\r\n", requests.pop) - resp = imap.id("name" => "test client", "version" => "latest") - assert_equal(server_id, resp) - assert_equal("RUBY0003 ID (\"name\" \"test client\" \"version\" \"latest\")\r\n", - requests.pop) - imap.logout - assert_equal("RUBY0004 LOGOUT\r\n", requests.pop) - ensure - imap.disconnect if imap - end - end - private def imaps_test(timeout: 10) diff --git a/test/net/imap/test_imap_id.rb b/test/net/imap/test_imap_id.rb new file mode 100644 index 00000000..2e5452fb --- /dev/null +++ b/test/net/imap/test_imap_id.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require_relative "../../lib/helper" + +class IMAPIdTest < Net::IMAP::TestCase + # TODO: convert to use Net::IMAP::FakeServer::TestHelper + include Net::IMAP::TestCase::SimpleTCPServerHelper + + def test_id + server = create_tcp_server + port = server.addr[1] + requests = Queue.new + server_id = {"name" => "test server", "version" => "v0.1.0"} + server_id_str = '("name" "test server" "version" "v0.1.0")' + @threads << Thread.start do + sock = server.accept + begin + sock.print("* OK test server\r\n") + requests.push(sock.gets) + # RFC 2971 very clearly states (in section 3.2): + # "a server MUST send a tagged ID response to an ID command." + # And yet... some servers report ID capability but won't the response. + sock.print("RUBY0001 OK ID completed\r\n") + requests.push(sock.gets) + sock.print("* ID #{server_id_str}\r\n") + sock.print("RUBY0002 OK ID completed\r\n") + requests.push(sock.gets) + sock.print("* ID #{server_id_str}\r\n") + sock.print("RUBY0003 OK ID completed\r\n") + requests.push(sock.gets) + sock.print("* BYE terminating connection\r\n") + sock.print("RUBY0004 OK LOGOUT completed\r\n") + ensure + sock.close + server.close + end + end + + begin + imap = Net::IMAP.new(server_addr, :port => port) + resp = imap.id + assert_equal(nil, resp) + assert_equal("RUBY0001 ID NIL\r\n", requests.pop) + resp = imap.id({}) + assert_equal(server_id, resp) + assert_equal("RUBY0002 ID ()\r\n", requests.pop) + resp = imap.id("name" => "test client", "version" => "latest") + assert_equal(server_id, resp) + assert_equal("RUBY0003 ID (\"name\" \"test client\" \"version\" \"latest\")\r\n", + requests.pop) + imap.logout + assert_equal("RUBY0004 LOGOUT\r\n", requests.pop) + ensure + imap.disconnect if imap + end + end + +end From c650f3d9919e4803b89abe899337429d58d05f19 Mon Sep 17 00:00:00 2001 From: nick evans Date: Fri, 31 Jul 2026 17:10:06 -0400 Subject: [PATCH 4/6] =?UTF-8?q?=E2=9C=85=F0=9F=9A=9A=20Move=20TLS=20and=20?= =?UTF-8?q?STARTTLS=20tests=20to=20their=20own=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This matches the pattern used by most other current "integration tests". --- test/net/imap/test_imap.rb | 299 ------------------------------- test/net/imap/test_imap_tls.rb | 312 +++++++++++++++++++++++++++++++++ 2 files changed, 312 insertions(+), 299 deletions(-) create mode 100644 test/net/imap/test_imap_tls.rb diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb index b6561e3e..61f1e0ee 100644 --- a/test/net/imap/test_imap.rb +++ b/test/net/imap/test_imap.rb @@ -5,223 +5,10 @@ require_relative "fake_server" class IMAPTest < Net::IMAP::TestCase - CA_FILE = File.expand_path("../fixtures/cacert.pem", __dir__) - SERVER_KEY = File.expand_path("../fixtures/server.key", __dir__) - SERVER_CERT = File.expand_path("../fixtures/server.crt", __dir__) - # TODO: convert to use Net::IMAP::FakeServer::TestHelper include Net::IMAP::TestCase::SimpleTCPServerHelper include Net::IMAP::FakeServer::TestHelper - if defined?(OpenSSL::SSL::SSLError) - def test_imaps_unknown_ca - assert_local_raise(OpenSSL::SSL::SSLError) do - imaps_test do |port| - begin - Net::IMAP.new("localhost", - :port => port, - :ssl => true) - rescue SystemCallError - skip $! - end - end - end - end - - def test_imaps_with_ca_file - # Assert verified *after* the imaps_test and assert_nothing_raised blocks. - # Otherwise, failures can't logout and need to wait for the timeout. - verified, imap = :unknown, nil - assert_nothing_raised do - begin - imaps_test do |port| - imap = Net::IMAP.new("localhost", - port: port, - ssl: { :ca_file => CA_FILE }) - verified = imap.tls_verified? - imap - rescue SystemCallError - skip $! - end - rescue OpenSSL::SSL::SSLError => e - raise e unless /darwin/ =~ RUBY_PLATFORM - end - end - assert_equal true, verified - assert_equal true, imap.tls_verified? - assert_equal({ca_file: CA_FILE}, imap.ssl_ctx_params) - assert_equal(CA_FILE, imap.ssl_ctx.ca_file) - assert_equal(OpenSSL::SSL::VERIFY_PEER, imap.ssl_ctx.verify_mode) - assert imap.ssl_ctx.verify_hostname - end - - def test_imaps_verify_none - # Assert verified *after* the imaps_test and assert_nothing_raised blocks. - # Otherwise, failures can't logout and need to wait for the timeout. - verified, imap = :unknown, nil - assert_nothing_raised do - begin - imaps_test do |port| - imap = Net::IMAP.new( - server_addr, - port: port, - ssl: { :verify_mode => OpenSSL::SSL::VERIFY_NONE } - ) - verified = imap.tls_verified? - imap - end - rescue OpenSSL::SSL::SSLError => e - raise e unless /darwin/ =~ RUBY_PLATFORM - end - end - assert_equal false, verified - assert_equal false, imap.tls_verified? - assert_equal({verify_mode: OpenSSL::SSL::VERIFY_NONE}, - imap.ssl_ctx_params) - assert_equal(nil, imap.ssl_ctx.ca_file) - assert_equal(OpenSSL::SSL::VERIFY_NONE, imap.ssl_ctx.verify_mode) - end - - def test_imaps_post_connection_check - assert_local_raise(OpenSSL::SSL::SSLError) do - imaps_test do |port| - # server_addr is different from the hostname in the certificate, - # so the following code should raise a SSLError. - Net::IMAP.new(server_addr, - :port => port, - :ssl => { :ca_file => CA_FILE }) - end - end - end - end - - if defined?(OpenSSL::SSL) - def test_starttls_unknown_ca - omit "This test is not working with Windows" if RUBY_PLATFORM =~ /mswin|mingw/ - - imap = nil - ex = nil - starttls_test do |port| - imap = Net::IMAP.new("localhost", port: port) - begin - imap.starttls - rescue => ex - end - imap - end - assert_kind_of(OpenSSL::SSL::SSLError, ex) - assert_equal (stack = caller), ex.backtrace&.last(stack.size) - assert_equal false, imap.tls_verified? - assert_equal({}, imap.ssl_ctx_params) - assert_equal(nil, imap.ssl_ctx.ca_file) - assert_equal(OpenSSL::SSL::VERIFY_PEER, imap.ssl_ctx.verify_mode) - end - - def test_starttls - initial_verified, initial_ctx, initial_params = :unknown, :unknown, :unknown - imap = nil - starttls_test do |port| - imap = Net::IMAP.new("localhost", :port => port) - initial_verified = imap.tls_verified? - initial_params = imap.ssl_ctx_params - initial_ctx = imap.ssl_ctx - imap.starttls(:ca_file => CA_FILE) - imap - end - assert_equal false, initial_verified - assert_equal false, initial_params - assert_equal nil, initial_ctx - assert_equal true, imap.tls_verified? - assert_include imap.inspect, " TLS disconnected" - assert_equal({ca_file: CA_FILE}, imap.ssl_ctx_params) - rescue SystemCallError - skip $! - ensure - if imap && !imap.disconnected? - imap.disconnect - end - end - - def test_starttls_stripping_not_ok - imap = nil - server = create_tcp_server - port = server.addr[1] - start_server do - sock = server.accept - begin - sock.print("* OK test server\r\n") - sock.gets - sock.print("RUBY0001 BUG unhandled command\r\n") - ensure - sock.close - server.close - end - end - begin - imap = Net::IMAP.new("localhost", :port => port) - assert_reraised(Net::IMAP::InvalidResponseError, imap:) do - imap.starttls(:ca_file => CA_FILE) - end - assert imap.disconnected? - ensure - imap.disconnect if imap && !imap.disconnected? - end - - assert_equal false, imap.tls_verified? - assert_include imap.inspect, " PLAINTEXT (TLS NOT STARTED) " - assert_equal({ca_file: CA_FILE}, imap.ssl_ctx_params) - assert_equal(CA_FILE, imap.ssl_ctx.ca_file) - assert_equal(OpenSSL::SSL::VERIFY_PEER, imap.ssl_ctx.verify_mode) - end - - def test_starttls_stripping_ok_sent_before_response - # to coordinate between threads (better than sleep) - server_to_client, client_to_server = Queue.new, Queue.new - rcvr_to_client = Queue.new - imap = nil - server = create_tcp_server - port = server.addr[1] - start_server do - sock = server.accept - begin - sock.print("* OK test server\r\n") - assert_equal :send_malicious_response, client_to_server.pop - sock.print("RUBY0001 OK hahaha, fooled you!\r\n") - server_to_client << :malicious_response_sent - sock.gets - ensure - sock.close - server.close - end - end - timeout = 5 - timeout *= EnvUtil.timeout_scale || 1 if defined?(EnvUtil.timeout_scale) - begin - Timeout.timeout(timeout) do - imap = Net::IMAP.new("localhost", :port => port) - imap.add_response_handler do |resp| rcvr_to_client << resp end - client_to_server << :send_malicious_response - assert_equal :malicious_response_sent, server_to_client.pop - # Wait until the receive thread has parsed the injected response and - # stored it in @tagged_responses, so finish_sending_command can see it. - # (handle_response stores the tagged response before calling handlers.) - rcvr_to_client.pop - assert_local_raise(Net::IMAP::InvalidTaggedResponseError) do - imap.starttls(:ca_file => CA_FILE) - end - assert imap.disconnected? - end - ensure - imap.disconnect if imap && !imap.disconnected? - end - assert_equal false, imap.tls_verified? - assert_include imap.inspect, " PLAINTEXT (TLS NOT STARTED) " - assert_equal({ca_file: CA_FILE}, imap.ssl_ctx_params) - assert_equal(CA_FILE, imap.ssl_ctx.ca_file) - assert_equal(OpenSSL::SSL::VERIFY_PEER, imap.ssl_ctx.verify_mode) - end - end - # Similar to STARTTLS stripping test, but checks other commands too data( "IDLE" => ->imap do imap.idle(1) do end end, @@ -927,90 +714,4 @@ def test_disconnect end end - private - - def imaps_test(timeout: 10) - Timeout.timeout(timeout) do - server = create_tcp_server - port = server.addr[1] - ctx = OpenSSL::SSL::SSLContext.new - ctx.ca_file = CA_FILE - ctx.key = File.open(SERVER_KEY) { |f| - OpenSSL::PKey::RSA.new(f) - } - ctx.cert = File.open(SERVER_CERT) { |f| - OpenSSL::X509::Certificate.new(f) - } - ssl_server = OpenSSL::SSL::SSLServer.new(server, ctx) - started = false - ths = Thread.start do - Thread.current.report_on_exception = false # always join-ed - begin - started = true - sock = ssl_server.accept - begin - sock.print("* OK test server\r\n") - sock.gets - sock.print("* BYE terminating connection\r\n") - sock.print("RUBY0001 OK LOGOUT completed\r\n") - ensure - sock.close - end - rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED - rescue OpenSSL::SSL::SSLError - end - end - sleep 0.001 until started - begin - begin - imap = yield(port) - imap.logout - imap - ensure - imap.disconnect if imap - end - ensure - ssl_server.close - ths.join - end - end - end - - def starttls_test - server = create_tcp_server - port = server.addr[1] - start_server do - sock = server.accept - begin - sock.print("* OK test server\r\n") - sock.gets - sock.print("RUBY0001 OK completed\r\n") - ctx = OpenSSL::SSL::SSLContext.new - ctx.ca_file = CA_FILE - ctx.key = File.open(SERVER_KEY) { |f| - OpenSSL::PKey::RSA.new(f) - } - ctx.cert = File.open(SERVER_CERT) { |f| - OpenSSL::X509::Certificate.new(f) - } - sock = OpenSSL::SSL::SSLSocket.new(sock, ctx) - sock.sync_close = true - sock.accept - sock.gets - sock.print("* BYE terminating connection\r\n") - sock.print("RUBY0002 OK LOGOUT completed\r\n") - rescue OpenSSL::SSL::SSLError - ensure - sock.close - server.close - end - end - begin - imap = yield(port) - imap.logout if !imap.disconnected? - ensure - imap.disconnect if imap && !imap.disconnected? - end - end - end diff --git a/test/net/imap/test_imap_tls.rb b/test/net/imap/test_imap_tls.rb new file mode 100644 index 00000000..052c114e --- /dev/null +++ b/test/net/imap/test_imap_tls.rb @@ -0,0 +1,312 @@ +# frozen_string_literal: true + +require_relative "../../lib/helper" + +# This file is for directly testing both implicit TLS connection and the +# STARTTLS command. +# +# Note that, although the default for test connections is plain-text, some other +# test files also use TLS connections when the TLS state affects the behavior +# of the methods being tested. +class IMAP_TLS_Test < Net::IMAP::TestCase + CA_FILE = File.expand_path("../fixtures/cacert.pem", __dir__) + SERVER_KEY = File.expand_path("../fixtures/server.key", __dir__) + SERVER_CERT = File.expand_path("../fixtures/server.crt", __dir__) + + include Net::IMAP::TestCase::SimpleTCPServerHelper + + if defined?(OpenSSL) + def test_imaps_unknown_ca + assert_local_raise(OpenSSL::SSL::SSLError) do + imaps_test do |port| + begin + Net::IMAP.new("localhost", + :port => port, + :ssl => true) + rescue SystemCallError + skip $! + end + end + end + end + + def test_imaps_with_ca_file + # Assert verified *after* the imaps_test and assert_nothing_raised blocks. + # Otherwise, failures can't logout and need to wait for the timeout. + verified, imap = :unknown, nil + assert_nothing_raised do + begin + imaps_test do |port| + imap = Net::IMAP.new("localhost", + port: port, + ssl: { :ca_file => CA_FILE }) + verified = imap.tls_verified? + imap + rescue SystemCallError + skip $! + end + rescue OpenSSL::SSL::SSLError => e + raise e unless /darwin/ =~ RUBY_PLATFORM + end + end + assert_equal true, verified + assert_equal true, imap.tls_verified? + assert_equal({ca_file: CA_FILE}, imap.ssl_ctx_params) + assert_equal(CA_FILE, imap.ssl_ctx.ca_file) + assert_equal(OpenSSL::SSL::VERIFY_PEER, imap.ssl_ctx.verify_mode) + assert imap.ssl_ctx.verify_hostname + end + + def test_imaps_verify_none + # Assert verified *after* the imaps_test and assert_nothing_raised blocks. + # Otherwise, failures can't logout and need to wait for the timeout. + verified, imap = :unknown, nil + assert_nothing_raised do + begin + imaps_test do |port| + imap = Net::IMAP.new( + server_addr, + port: port, + ssl: { :verify_mode => OpenSSL::SSL::VERIFY_NONE } + ) + verified = imap.tls_verified? + imap + end + rescue OpenSSL::SSL::SSLError => e + raise e unless /darwin/ =~ RUBY_PLATFORM + end + end + assert_equal false, verified + assert_equal false, imap.tls_verified? + assert_equal({verify_mode: OpenSSL::SSL::VERIFY_NONE}, + imap.ssl_ctx_params) + assert_equal(nil, imap.ssl_ctx.ca_file) + assert_equal(OpenSSL::SSL::VERIFY_NONE, imap.ssl_ctx.verify_mode) + end + + def test_imaps_post_connection_check + assert_local_raise(OpenSSL::SSL::SSLError) do + imaps_test do |port| + # server_addr is different from the hostname in the certificate, + # so the following code should raise a SSLError. + Net::IMAP.new(server_addr, + :port => port, + :ssl => { :ca_file => CA_FILE }) + end + end + end + + def test_starttls_unknown_ca + omit "This test is not working with Windows" if RUBY_PLATFORM =~ /mswin|mingw/ + + imap = nil + ex = nil + starttls_test do |port| + imap = Net::IMAP.new("localhost", port: port) + begin + imap.starttls + rescue => ex + end + imap + end + assert_kind_of(OpenSSL::SSL::SSLError, ex) + assert_equal (stack = caller), ex.backtrace&.last(stack.size) + assert_equal false, imap.tls_verified? + assert_equal({}, imap.ssl_ctx_params) + assert_equal(nil, imap.ssl_ctx.ca_file) + assert_equal(OpenSSL::SSL::VERIFY_PEER, imap.ssl_ctx.verify_mode) + end + + def test_starttls + initial_verified, initial_ctx, initial_params = :unknown, :unknown, :unknown + imap = nil + starttls_test do |port| + imap = Net::IMAP.new("localhost", :port => port) + initial_verified = imap.tls_verified? + initial_params = imap.ssl_ctx_params + initial_ctx = imap.ssl_ctx + imap.starttls(:ca_file => CA_FILE) + imap + end + assert_equal false, initial_verified + assert_equal false, initial_params + assert_equal nil, initial_ctx + assert_equal true, imap.tls_verified? + assert_include imap.inspect, " TLS disconnected" + assert_equal({ca_file: CA_FILE}, imap.ssl_ctx_params) + rescue SystemCallError + skip $! + ensure + if imap && !imap.disconnected? + imap.disconnect + end + end + + def test_starttls_stripping_not_ok + imap = nil + server = create_tcp_server + port = server.addr[1] + start_server do + sock = server.accept + begin + sock.print("* OK test server\r\n") + sock.gets + sock.print("RUBY0001 BUG unhandled command\r\n") + ensure + sock.close + server.close + end + end + begin + imap = Net::IMAP.new("localhost", :port => port) + assert_reraised(Net::IMAP::InvalidResponseError, imap:) do + imap.starttls(:ca_file => CA_FILE) + end + assert imap.disconnected? + ensure + imap.disconnect if imap && !imap.disconnected? + end + + assert_equal false, imap.tls_verified? + assert_include imap.inspect, " PLAINTEXT (TLS NOT STARTED) " + assert_equal({ca_file: CA_FILE}, imap.ssl_ctx_params) + assert_equal(CA_FILE, imap.ssl_ctx.ca_file) + assert_equal(OpenSSL::SSL::VERIFY_PEER, imap.ssl_ctx.verify_mode) + end + + def test_starttls_stripping_ok_sent_before_response + # to coordinate between threads (better than sleep) + server_to_client, client_to_server = Queue.new, Queue.new + rcvr_to_client = Queue.new + imap = nil + server = create_tcp_server + port = server.addr[1] + start_server do + sock = server.accept + begin + sock.print("* OK test server\r\n") + assert_equal :send_malicious_response, client_to_server.pop + sock.print("RUBY0001 OK hahaha, fooled you!\r\n") + server_to_client << :malicious_response_sent + sock.gets + ensure + sock.close + server.close + end + end + timeout = 5 + timeout *= EnvUtil.timeout_scale || 1 if defined?(EnvUtil.timeout_scale) + begin + Timeout.timeout(timeout) do + imap = Net::IMAP.new("localhost", :port => port) + imap.add_response_handler do |resp| rcvr_to_client << resp end + client_to_server << :send_malicious_response + assert_equal :malicious_response_sent, server_to_client.pop + # Wait until the receive thread has parsed the injected response and + # stored it in @tagged_responses, so finish_sending_command can see it. + # (handle_response stores the tagged response before calling handlers.) + rcvr_to_client.pop + assert_local_raise(Net::IMAP::InvalidTaggedResponseError) do + imap.starttls(:ca_file => CA_FILE) + end + assert imap.disconnected? + end + ensure + imap.disconnect if imap && !imap.disconnected? + end + assert_equal false, imap.tls_verified? + assert_include imap.inspect, " PLAINTEXT (TLS NOT STARTED) " + assert_equal({ca_file: CA_FILE}, imap.ssl_ctx_params) + assert_equal(CA_FILE, imap.ssl_ctx.ca_file) + assert_equal(OpenSSL::SSL::VERIFY_PEER, imap.ssl_ctx.verify_mode) + end + + end + + private + + def imaps_test(timeout: 10) + Timeout.timeout(timeout) do + server = create_tcp_server + port = server.addr[1] + ctx = OpenSSL::SSL::SSLContext.new + ctx.ca_file = CA_FILE + ctx.key = File.open(SERVER_KEY) { |f| + OpenSSL::PKey::RSA.new(f) + } + ctx.cert = File.open(SERVER_CERT) { |f| + OpenSSL::X509::Certificate.new(f) + } + ssl_server = OpenSSL::SSL::SSLServer.new(server, ctx) + started = false + ths = Thread.start do + Thread.current.report_on_exception = false # always join-ed + begin + started = true + sock = ssl_server.accept + begin + sock.print("* OK test server\r\n") + sock.gets + sock.print("* BYE terminating connection\r\n") + sock.print("RUBY0001 OK LOGOUT completed\r\n") + ensure + sock.close + end + rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED + rescue OpenSSL::SSL::SSLError + end + end + sleep 0.001 until started + begin + begin + imap = yield(port) + imap.logout + imap + ensure + imap.disconnect if imap + end + ensure + ssl_server.close + ths.join + end + end + end + + def starttls_test + server = create_tcp_server + port = server.addr[1] + start_server do + sock = server.accept + begin + sock.print("* OK test server\r\n") + sock.gets + sock.print("RUBY0001 OK completed\r\n") + ctx = OpenSSL::SSL::SSLContext.new + ctx.ca_file = CA_FILE + ctx.key = File.open(SERVER_KEY) { |f| + OpenSSL::PKey::RSA.new(f) + } + ctx.cert = File.open(SERVER_CERT) { |f| + OpenSSL::X509::Certificate.new(f) + } + sock = OpenSSL::SSL::SSLSocket.new(sock, ctx) + sock.sync_close = true + sock.accept + sock.gets + sock.print("* BYE terminating connection\r\n") + sock.print("RUBY0002 OK LOGOUT completed\r\n") + rescue OpenSSL::SSL::SSLError + ensure + sock.close + server.close + end + end + begin + imap = yield(port) + imap.logout if !imap.disconnected? + ensure + imap.disconnect if imap && !imap.disconnected? + end + end + +end From 2b414443ee41c4e5c4ba9f9734ee3c670bfc68c1 Mon Sep 17 00:00:00 2001 From: nick evans Date: Mon, 3 Aug 2026 13:18:16 -0400 Subject: [PATCH 5/6] =?UTF-8?q?=E2=9C=85=F0=9F=9A=9A=20Move=20test=5Fdata?= =?UTF-8?q?=5Fencoding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In keeping with the naming convention that `test_imap*.rb` files are for client integration tests, this test file must be renamed because it does not create a client connection. It only tests class methods. --- .../imap/{test_imap_data_encoding.rb => test_data_encoding.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/net/imap/{test_imap_data_encoding.rb => test_data_encoding.rb} (100%) diff --git a/test/net/imap/test_imap_data_encoding.rb b/test/net/imap/test_data_encoding.rb similarity index 100% rename from test/net/imap/test_imap_data_encoding.rb rename to test/net/imap/test_data_encoding.rb From 877c739a204f7eea4b957d3e8390e5af7b28e607 Mon Sep 17 00:00:00 2001 From: nick evans Date: Mon, 3 Aug 2026 13:39:31 -0400 Subject: [PATCH 6/6] =?UTF-8?q?=E2=9C=85=F0=9F=9A=9A=20Move=20argument=20s?= =?UTF-8?q?ending=20tests=20to=20their=20own=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/net/imap/test_command_data.rb | 2 + test/net/imap/test_imap.rb | 401 ----------------- .../imap/test_imap_sending_command_data.rb | 415 ++++++++++++++++++ 3 files changed, 417 insertions(+), 401 deletions(-) create mode 100644 test/net/imap/test_imap_sending_command_data.rb diff --git a/test/net/imap/test_command_data.rb b/test/net/imap/test_command_data.rb index dafa6a08..d6928acf 100644 --- a/test/net/imap/test_command_data.rb +++ b/test/net/imap/test_command_data.rb @@ -3,6 +3,8 @@ require "net/imap" require "test/unit" +# For integration testing of sending of command arguments (over a real socket), +# see test_imap_sending_command_data.rb. class CommandDataTest < Net::IMAP::TestCase DataFormatError = Net::IMAP::DataFormatError diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb index 61f1e0ee..95d61c11 100644 --- a/test/net/imap/test_imap.rb +++ b/test/net/imap/test_imap.rb @@ -286,407 +286,6 @@ def test_default_port assert_equal(993, Net::IMAP.default_imaps_port) end - def test_send_integer - with_fake_server do |server, imap| - server.on "TEST", &:done_ok - - # regular numbers may be any uint32 - assert_raise(Net::IMAP::DataFormatError) do - imap.__send__(:send_command, "TEST", -1) - end - assert_empty server.commands - - imap.__send__(:send_command, "TEST", 0) - assert_equal "0", server.commands.pop.args - - imap.__send__(:send_command, "TEST", 2**32 - 1) - assert_equal (2**32 - 1).to_s, server.commands.pop.args - - imap.__send__(:send_command, "TEST", 2**32) - assert_equal (2**32).to_s, server.commands.pop.args - - imap.__send__(:send_command, "TEST", 2**64 - 1) - assert_equal (2**64 - 1).to_s, server.commands.pop.args - - assert_raise(Net::IMAP::DataFormatError) do - imap.__send__(:send_command, "TEST", 2**64) - end - assert_empty server.commands - end - end - - def test_send_sequence_set - with_fake_server do |server, imap| - server.on "TEST", &:done_ok - - # SequenceSet numbers may be non-zero uint3, and -1 is translated to * - imap.__send__(:send_command, "TEST", Net::IMAP::SequenceSet.new(-1)) - assert_equal "*", server.commands.pop.args - - assert_raise(Net::IMAP::DataFormatError) do - imap.__send__(:send_command, "TEST", Net::IMAP::SequenceSet.new(0)) - end - assert_empty server.commands - - imap.__send__(:send_command, "TEST", Net::IMAP::SequenceSet.new(1)) - assert_equal "1", server.commands.pop.args - - imap.__send__(:send_command, "TEST", Net::IMAP::SequenceSet.new(2**32-1)) - assert_equal (2**32 - 1).to_s, server.commands.pop.args - - assert_raise(Net::IMAP::DataFormatError) do - imap.__send__(:send_command, "TEST", Net::IMAP::SequenceSet.new(2**32)) - end - assert_empty server.commands - end - end - - def test_send_symbol_as_flag - with_fake_server do |server, imap| - server.on "TEST", &:done_ok - - imap.__send__(:send_command, "TEST", :Seen, :Flagged) - assert_equal "\\Seen \\Flagged", server.commands.pop.args - - # symbol may not contain atom-specials - [ - :"with_parens()", - :"with_list_wildcards*", - :"with_list_wildcards%", - :"with_resp_special]", - :"with\0null", - :"with\x7fcontrol_char", - :'"with_quoted_specials"', - :"with_quoted_specials\\", - :"with\rCR", - :"with\nLF", - ].each do |symbol| - assert_raise_with_message(Net::IMAP::DataFormatError, /\bflag\b/i) do - imap.__send__(:send_command, "TEST", symbol) - end - assert_empty server.commands - end - end - end - - def test_raw_data - with_fake_server do |server, imap| - server.on "TEST", &:done_ok - - imap.__send__(:send_command, "TEST", Net::IMAP::RawData.new("foo bar")) - assert_equal "foo bar", server.commands.pop.args - - imap.__send__(:send_command, "TEST", - Net::IMAP::RawData.new("{3}\r\nfoo"), - Net::IMAP::RawData.new("~{4}\r\n\0bar")) - assert_equal "{3}\r\nfoo ~{4}\r\n\0bar", server.commands.pop.args - - # RawData must pass basic validation before sending command - [ - "with \0 NULL", - "with \r CR", - "with \n LF", - "with \r\n CRLF", - "{1234}\r\nliteral is too small", - "{1}\r\n\0 literal contains NULL", - ].each do |data| - assert_raise(Net::IMAP::DataFormatError) do - imap.__send__(:send_command, "TEST", Net::IMAP::RawData[data:]) - end - assert_empty server.commands - end - end - end - - test("send PartialRange args") do - with_fake_server do |server, imap| - server.on "TEST", &:done_ok - send_partial_ranges = ->(*args) do - args.map! { Net::IMAP::PartialRange[_1] } - imap.__send__(:send_command, "TEST", *args) - end - # simple strings - send_partial_ranges.call "1:5", "-5:-1" - assert_equal "1:5 -5:-1", server.commands.pop.args - # backwards strings are reversed - send_partial_ranges.call "5:1", "-1:-5" - assert_equal "1:5 -5:-1", server.commands.pop.args - # simple ranges - send_partial_ranges.call 1..5, -5..-1 - assert_equal "1:5 -5:-1", server.commands.pop.args - # exclusive ranges drop end - send_partial_ranges.call 1...5, -5...-1 - assert_equal "1:4 -5:-2", server.commands.pop.args - - # backwards ranges are invalid - assert_raise(ArgumentError) do send_partial_ranges.call( 5.. 1) end - assert_raise(ArgumentError) do send_partial_ranges.call(-1..-5) end - - # bounds checks - uint32_max = 2**32 - 1 - not_uint32 = 2**32 - send_partial_ranges.call 500..uint32_max - assert_equal "500:#{uint32_max}", server.commands.pop.args - send_partial_ranges.call 500...not_uint32 - assert_equal "500:#{uint32_max}", server.commands.pop.args - send_partial_ranges.call "#{uint32_max}:500" - assert_equal "500:#{uint32_max}", server.commands.pop.args - - send_partial_ranges.call(-uint32_max..-500) - assert_equal "-#{uint32_max}:-500", server.commands.pop.args - send_partial_ranges.call "-500:-#{uint32_max}" - assert_equal "-#{uint32_max}:-500", server.commands.pop.args - - assert_raise(ArgumentError) do send_partial_ranges.call("foo") end - assert_raise(ArgumentError) do send_partial_ranges.call("foo:bar") end - assert_raise(ArgumentError) do send_partial_ranges.call("1.2:3.5") end - assert_raise(ArgumentError) do send_partial_ranges.call("1:*") end - assert_raise(ArgumentError) do send_partial_ranges.call("1:#{not_uint32}") end - assert_raise(ArgumentError) do send_partial_ranges.call(1..) end - assert_raise(ArgumentError) do send_partial_ranges.call(1..not_uint32) end - assert_raise(ArgumentError) do send_partial_ranges.call(..1) end - end - end - - test "sending nil args" do - with_fake_server do |server, imap| - server.on "TEST", &:done_ok - def imap.test_args(*args) = send_command("TEST", *args) - - imap.test_args nil, [nil] - assert_equal "NIL (NIL)", server.commands.pop.args - end - end - - test "sending atom string args (astring-chars)" do - with_fake_server do |server, imap| - server.on "TEST", &:done_ok - def imap.test_args(*args) = send_command("TEST", *args) - - imap.test_args "valid-atoms", %w[foo=bar $baz] - assert_equal "valid-atoms (foo=bar $baz)", server.commands.pop.args - - imap.test_args "unquoted-astring", "[resp-specials]" - assert_equal "unquoted-astring [resp-specials]", server.commands.pop.args - end - end - - test "string args don't allow NULL bytes" do - with_fake_server do |server, imap| - server.on "TEST", &:done_ok - def imap.test_args(*args) = send_command("TEST", *args) - - assert_raise_with_message(Net::IMAP::DataFormatError, /NULL byte/) do - imap.test_args "NULL=\0" - end - - assert_raise_with_message(Net::IMAP::DataFormatError, /NULL byte/) do - imap.test_args ["ok", "also ok", "not ok: \0"] - end - end - end - - test "sending quoted string args" do - with_fake_server do |server, imap| - server.on "TEST", &:done_ok - def imap.test_args(*args) = send_command("TEST", *args) - - imap.test_args "empty", "", [""] - assert_equal 'empty "" ("")', server.commands.pop.args - - imap.test_args "simple-quotable-specials", "() {} %*" - assert_equal('simple-quotable-specials "() {} %*"'.b, - server.commands.pop.args) - - imap.test_args "ascii-ctrl-chars", "\b\x7f" - assert_equal("ascii-ctrl-chars \"\b\x7f\"".b, server.commands.pop.args) - - imap.test_args "quoted-specials", ["backslash=\\", 'dquotes=""'] - assert_equal('quoted-specials ("backslash=\\\\" "dquotes=\\"\\"")'.b, - server.commands.pop.args) - end - end - - test "sending UTF-8 string args" do - with_fake_server( - with_extensions: %w[UTF8=ACCEPT LITERAL-], - greeting_capabilities: true, - capabilities_enablable: %w[UTF8=ACCEPT], - ) do |server, imap| - server.on "TEST", &:done_ok - def imap.test_args(*args) = send_command("TEST", *args) - - # Before enabling UTF-8 strings, with non-synchronizing literals - imap.test_args "sync-literal-utf8", ["αβγδε"] - assert_equal("sync-literal-utf8 ({10+}\r\nαβγδε)".b, - server.commands.pop.args) - - imap.test_args "utf8-with-wrong-encoding", "αβγδε".b - assert_equal("utf8-with-wrong-encoding {10+}\r\nαβγδε".b, - server.commands.pop.args) - - imap.test_args "invalid-utf8", "\x80".b.force_encoding("UTF-8") - assert_equal("invalid-utf8 {1+}\r\n\x80".b, - server.commands.pop.args) - - # Before enabling UTF-8 strings, without non-synchronizing literals - imap.config.max_non_synchronizing_literal = -1 - imap.test_args "sync-literal-utf8", ["αβγδε"] - assert_equal("sync-literal-utf8 ({10}\r\nαβγδε)".b, - server.commands.pop.args) - - imap.test_args "utf8-with-wrong-encoding", "αβγδε".b - assert_equal("utf8-with-wrong-encoding {10}\r\nαβγδε".b, - server.commands.pop.args) - - imap.test_args "invalid-utf8", "\x80".b.force_encoding("UTF-8") - assert_equal("invalid-utf8 {1}\r\n\x80".b, - server.commands.pop.args) - - # After enabling UTF-8 strings - imap.enable(:utf8) - assert imap.utf8_enabled? - server.commands.pop.args => ["UTF8=ACCEPT"] - - imap.test_args "quoted-utf8", "αβγδε" - assert_equal 'quoted-utf8 "αβγδε"'.b, server.commands.pop.args - - imap.test_args "utf8-with-wrong-encoding", "αβγδε".b - assert_equal("utf8-with-wrong-encoding {10}\r\nαβγδε".b, - server.commands.pop.args) - - imap.test_args "invalid-utf8", "\x80".b.force_encoding("UTF-8") - assert_equal("invalid-utf8 {1}\r\n\x80".b, - server.commands.pop.args) - end - end - - test("send literal args") do - with_fake_server(with_extensions: %w[LITERAL-]) do |server, imap| - # disable automatic non-synchronizing literals - imap.config.max_non_synchronizing_literal = -1 - server.on "TEST", &:done_ok - send_args = ->(*args) do - imap.__send__(:send_command, "TEST", *args) - end - send_args.call ["\xDE\xAD\xBE\xEF".b] - assert_equal "({4}\r\n\xDE\xAD\xBE\xEF)".b, server.commands.pop.args - - send_args.call ["hi\rthere\n", "huh?\r\nfake out"] - assert_equal "({9}\r\nhi\rthere\n {14}\r\nhuh?\r\nfake out)".b, - server.commands.pop.args - - # enable automatic non-synchronizing literals - imap.config.max_non_synchronizing_literal = 1024 - buff = bytes = nil - server.literal_acceptor = proc { buff, bytes = _1, _2; false } - server.on "TEST", &:done_ok - send_args = ->(*args) do - imap.__send__(:send_command, "TEST", *args) - end - send_args.call ["\xDE\xAD\xBE\xEF".b] - assert_equal "({4+}\r\n\xDE\xAD\xBE\xEF)".b, server.commands.pop.args - assert_nil buff - assert_nil bytes - - # limited automatic non-synchronizing literals - imap.config.max_non_synchronizing_literal = 5 - assert_local_raise(Net::IMAP::NoResponseError) do - send_args.call [ - Net::IMAP::Literal["\rhi\r"], - Net::IMAP::Literal["\x01" * 10], - ] - end - assert_match(/TEST \(\{4\+\}\r\n\rhi\r \{10\}\r\n\z/, buff) - assert_equal 10, bytes - assert_empty server.commands - - server.literal_acceptor = proc { true } - send_args.call Net::IMAP::Literal["\x01" * 10] - assert_equal "{10}\r\n\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01", - server.commands.pop.args - - buff = bytes = nil - server.literal_acceptor = proc { buff, bytes = _1, _2; false } - send_args.call("nonsync", - Net::IMAP::Literal[data: "\x01\x02\x03", non_sync: true]) - assert_equal "nonsync {3+}\r\n\x01\x02\03".b, server.commands.pop.args - assert_nil buff - assert_nil bytes - - imap.config.max_non_synchronizing_literal = 5 - server.literal_acceptor = proc { true } - send_args.call("literal", Net::IMAP::Literal["\r", false], - "literal", Net::IMAP::Literal["αβ", nil], - "literal", Net::IMAP::Literal["αβγδε", nil], - "literal+", Net::IMAP::Literal["αβγδε", true], - "literal8", Net::IMAP::Literal8["\0", false], - "literal8+", Net::IMAP::Literal8["\0" * 2, nil], - "literal8", Net::IMAP::Literal8["\0" * 6, nil], - "literal8+", Net::IMAP::Literal8["\0" * 8, true], - "done") - assert_equal("literal" " {1}\r\n\r " \ - "literal" " {4+}\r\nαβ " \ - "literal" " {10}\r\nαβγδε " \ - "literal+" " {10+}\r\nαβγδε " \ - "literal8" " ~{1}\r\n\0 " \ - "literal8+" " ~{2+}\r\n\0\0 " \ - "literal8" " ~{6}\r\n\0\0\0\0\0\0 " \ - "literal8+" " ~{8+}\r\n\0\0\0\0\0\0\0\0 " \ - "done".b, - server.commands.pop.args) - end - end - - test("send non-synchronizing literals with LITERAL+") do - with_fake_server( - with_extensions: %w[LITERAL+], greeting_capabilities: true, - ) do |server, imap| - def imap.send_test_args(*args) = send_command("TEST", *args) - server.on "TEST", &:done_ok - - imap.config.max_non_synchronizing_literal = 5_000 - large = "\xff".b * 5_000 - imap.send_test_args Net::IMAP::Literal[large, nil] - assert_equal("{5000+}\r\n#{large}".b, server.commands.pop.args) - - large = "\xff".b * 10_000 - imap.send_test_args Net::IMAP::Literal[large, nil] - assert_equal("{10000}\r\n#{large}".b, server.commands.pop.args) - - imap.send_test_args Net::IMAP::Literal[large, true] - assert_equal("{10000+}\r\n#{large}".b, server.commands.pop.args) - end - end - - test("send non-synchronizing literal that's too large for LITERAL-") do - with_fake_server( - with_extensions: %w[LITERAL-], greeting_capabilities: true, - ignore_abrupt_eof: true, ignore_io_error: true - ) do |server, imap| - def imap.send_test_args(*args) = send_command("TEST", *args) - server.on "TEST", &:done_ok - assert_raise(Net::IMAP::DataFormatError) do - imap.send_test_args Net::IMAP::Literal["\xff".b * 5000, true] - end - assert imap.disconnected? - end - end - - test("send non-synchronizing literal without known server support") do - with_fake_server( - with_extensions: %w[LITERAL+], greeting_capabilities: false, - ignore_abrupt_eof: true, ignore_io_error: true - ) do |server, imap| - def imap.send_test_args(*args) = send_command("TEST", *args) - server.on "TEST", &:done_ok - assert_raise(Net::IMAP::DataFormatError) do - imap.send_test_args Net::IMAP::Literal["\xff".b * 100, true] - end - assert imap.disconnected? - end - end - def test_disconnect server = create_tcp_server port = server.addr[1] diff --git a/test/net/imap/test_imap_sending_command_data.rb b/test/net/imap/test_imap_sending_command_data.rb new file mode 100644 index 00000000..0044d245 --- /dev/null +++ b/test/net/imap/test_imap_sending_command_data.rb @@ -0,0 +1,415 @@ +# frozen_string_literal: true + +require_relative "../../lib/helper" +require_relative "fake_server" + +# This file is for integration testing of command data sending, without testing +# the specifics of any particular command. +# +# For isolated unit tests of different types of command arguments, see +# test_command_data.rb. +class IMAPSendingCommandDataTest < Net::IMAP::TestCase + include Net::IMAP::FakeServer::TestHelper + + def test_send_integer + with_fake_server do |server, imap| + server.on "TEST", &:done_ok + + # regular numbers may be any uint32 + assert_raise(Net::IMAP::DataFormatError) do + imap.__send__(:send_command, "TEST", -1) + end + assert_empty server.commands + + imap.__send__(:send_command, "TEST", 0) + assert_equal "0", server.commands.pop.args + + imap.__send__(:send_command, "TEST", 2**32 - 1) + assert_equal (2**32 - 1).to_s, server.commands.pop.args + + imap.__send__(:send_command, "TEST", 2**32) + assert_equal (2**32).to_s, server.commands.pop.args + + imap.__send__(:send_command, "TEST", 2**64 - 1) + assert_equal (2**64 - 1).to_s, server.commands.pop.args + + assert_raise(Net::IMAP::DataFormatError) do + imap.__send__(:send_command, "TEST", 2**64) + end + assert_empty server.commands + end + end + + def test_send_sequence_set + with_fake_server do |server, imap| + server.on "TEST", &:done_ok + + # SequenceSet numbers may be non-zero uint3, and -1 is translated to * + imap.__send__(:send_command, "TEST", Net::IMAP::SequenceSet.new(-1)) + assert_equal "*", server.commands.pop.args + + assert_raise(Net::IMAP::DataFormatError) do + imap.__send__(:send_command, "TEST", Net::IMAP::SequenceSet.new(0)) + end + assert_empty server.commands + + imap.__send__(:send_command, "TEST", Net::IMAP::SequenceSet.new(1)) + assert_equal "1", server.commands.pop.args + + imap.__send__(:send_command, "TEST", Net::IMAP::SequenceSet.new(2**32-1)) + assert_equal (2**32 - 1).to_s, server.commands.pop.args + + assert_raise(Net::IMAP::DataFormatError) do + imap.__send__(:send_command, "TEST", Net::IMAP::SequenceSet.new(2**32)) + end + assert_empty server.commands + end + end + + def test_send_symbol_as_flag + with_fake_server do |server, imap| + server.on "TEST", &:done_ok + + imap.__send__(:send_command, "TEST", :Seen, :Flagged) + assert_equal "\\Seen \\Flagged", server.commands.pop.args + + # symbol may not contain atom-specials + [ + :"with_parens()", + :"with_list_wildcards*", + :"with_list_wildcards%", + :"with_resp_special]", + :"with\0null", + :"with\x7fcontrol_char", + :'"with_quoted_specials"', + :"with_quoted_specials\\", + :"with\rCR", + :"with\nLF", + ].each do |symbol| + assert_raise_with_message(Net::IMAP::DataFormatError, /\bflag\b/i) do + imap.__send__(:send_command, "TEST", symbol) + end + assert_empty server.commands + end + end + end + + def test_raw_data + with_fake_server do |server, imap| + server.on "TEST", &:done_ok + + imap.__send__(:send_command, "TEST", Net::IMAP::RawData.new("foo bar")) + assert_equal "foo bar", server.commands.pop.args + + imap.__send__(:send_command, "TEST", + Net::IMAP::RawData.new("{3}\r\nfoo"), + Net::IMAP::RawData.new("~{4}\r\n\0bar")) + assert_equal "{3}\r\nfoo ~{4}\r\n\0bar", server.commands.pop.args + + # RawData must pass basic validation before sending command + [ + "with \0 NULL", + "with \r CR", + "with \n LF", + "with \r\n CRLF", + "{1234}\r\nliteral is too small", + "{1}\r\n\0 literal contains NULL", + ].each do |data| + assert_raise(Net::IMAP::DataFormatError) do + imap.__send__(:send_command, "TEST", Net::IMAP::RawData[data:]) + end + assert_empty server.commands + end + end + end + + test("send PartialRange args") do + with_fake_server do |server, imap| + server.on "TEST", &:done_ok + send_partial_ranges = ->(*args) do + args.map! { Net::IMAP::PartialRange[_1] } + imap.__send__(:send_command, "TEST", *args) + end + # simple strings + send_partial_ranges.call "1:5", "-5:-1" + assert_equal "1:5 -5:-1", server.commands.pop.args + # backwards strings are reversed + send_partial_ranges.call "5:1", "-1:-5" + assert_equal "1:5 -5:-1", server.commands.pop.args + # simple ranges + send_partial_ranges.call 1..5, -5..-1 + assert_equal "1:5 -5:-1", server.commands.pop.args + # exclusive ranges drop end + send_partial_ranges.call 1...5, -5...-1 + assert_equal "1:4 -5:-2", server.commands.pop.args + + # backwards ranges are invalid + assert_raise(ArgumentError) do send_partial_ranges.call( 5.. 1) end + assert_raise(ArgumentError) do send_partial_ranges.call(-1..-5) end + + # bounds checks + uint32_max = 2**32 - 1 + not_uint32 = 2**32 + send_partial_ranges.call 500..uint32_max + assert_equal "500:#{uint32_max}", server.commands.pop.args + send_partial_ranges.call 500...not_uint32 + assert_equal "500:#{uint32_max}", server.commands.pop.args + send_partial_ranges.call "#{uint32_max}:500" + assert_equal "500:#{uint32_max}", server.commands.pop.args + + send_partial_ranges.call(-uint32_max..-500) + assert_equal "-#{uint32_max}:-500", server.commands.pop.args + send_partial_ranges.call "-500:-#{uint32_max}" + assert_equal "-#{uint32_max}:-500", server.commands.pop.args + + assert_raise(ArgumentError) do send_partial_ranges.call("foo") end + assert_raise(ArgumentError) do send_partial_ranges.call("foo:bar") end + assert_raise(ArgumentError) do send_partial_ranges.call("1.2:3.5") end + assert_raise(ArgumentError) do send_partial_ranges.call("1:*") end + assert_raise(ArgumentError) do send_partial_ranges.call("1:#{not_uint32}") end + assert_raise(ArgumentError) do send_partial_ranges.call(1..) end + assert_raise(ArgumentError) do send_partial_ranges.call(1..not_uint32) end + assert_raise(ArgumentError) do send_partial_ranges.call(..1) end + end + end + + test "sending nil args" do + with_fake_server do |server, imap| + server.on "TEST", &:done_ok + def imap.test_args(*args) = send_command("TEST", *args) + + imap.test_args nil, [nil] + assert_equal "NIL (NIL)", server.commands.pop.args + end + end + + test "sending atom string args (astring-chars)" do + with_fake_server do |server, imap| + server.on "TEST", &:done_ok + def imap.test_args(*args) = send_command("TEST", *args) + + imap.test_args "valid-atoms", %w[foo=bar $baz] + assert_equal "valid-atoms (foo=bar $baz)", server.commands.pop.args + + imap.test_args "unquoted-astring", "[resp-specials]" + assert_equal "unquoted-astring [resp-specials]", server.commands.pop.args + end + end + + test "string args don't allow NULL bytes" do + with_fake_server do |server, imap| + server.on "TEST", &:done_ok + def imap.test_args(*args) = send_command("TEST", *args) + + assert_raise_with_message(Net::IMAP::DataFormatError, /NULL byte/) do + imap.test_args "NULL=\0" + end + + assert_raise_with_message(Net::IMAP::DataFormatError, /NULL byte/) do + imap.test_args ["ok", "also ok", "not ok: \0"] + end + end + end + + test "sending quoted string args" do + with_fake_server do |server, imap| + server.on "TEST", &:done_ok + def imap.test_args(*args) = send_command("TEST", *args) + + imap.test_args "empty", "", [""] + assert_equal 'empty "" ("")', server.commands.pop.args + + imap.test_args "simple-quotable-specials", "() {} %*" + assert_equal('simple-quotable-specials "() {} %*"'.b, + server.commands.pop.args) + + imap.test_args "ascii-ctrl-chars", "\b\x7f" + assert_equal("ascii-ctrl-chars \"\b\x7f\"".b, server.commands.pop.args) + + imap.test_args "quoted-specials", ["backslash=\\", 'dquotes=""'] + assert_equal('quoted-specials ("backslash=\\\\" "dquotes=\\"\\"")'.b, + server.commands.pop.args) + end + end + + test "sending UTF-8 string args" do + with_fake_server( + with_extensions: %w[UTF8=ACCEPT LITERAL-], + greeting_capabilities: true, + capabilities_enablable: %w[UTF8=ACCEPT], + ) do |server, imap| + server.on "TEST", &:done_ok + def imap.test_args(*args) = send_command("TEST", *args) + + # Before enabling UTF-8 strings, with non-synchronizing literals + imap.test_args "sync-literal-utf8", ["αβγδε"] + assert_equal("sync-literal-utf8 ({10+}\r\nαβγδε)".b, + server.commands.pop.args) + + imap.test_args "utf8-with-wrong-encoding", "αβγδε".b + assert_equal("utf8-with-wrong-encoding {10+}\r\nαβγδε".b, + server.commands.pop.args) + + imap.test_args "invalid-utf8", "\x80".b.force_encoding("UTF-8") + assert_equal("invalid-utf8 {1+}\r\n\x80".b, + server.commands.pop.args) + + # Before enabling UTF-8 strings, without non-synchronizing literals + imap.config.max_non_synchronizing_literal = -1 + imap.test_args "sync-literal-utf8", ["αβγδε"] + assert_equal("sync-literal-utf8 ({10}\r\nαβγδε)".b, + server.commands.pop.args) + + imap.test_args "utf8-with-wrong-encoding", "αβγδε".b + assert_equal("utf8-with-wrong-encoding {10}\r\nαβγδε".b, + server.commands.pop.args) + + imap.test_args "invalid-utf8", "\x80".b.force_encoding("UTF-8") + assert_equal("invalid-utf8 {1}\r\n\x80".b, + server.commands.pop.args) + + # After enabling UTF-8 strings + imap.enable(:utf8) + assert imap.utf8_enabled? + server.commands.pop.args => ["UTF8=ACCEPT"] + + imap.test_args "quoted-utf8", "αβγδε" + assert_equal 'quoted-utf8 "αβγδε"'.b, server.commands.pop.args + + imap.test_args "utf8-with-wrong-encoding", "αβγδε".b + assert_equal("utf8-with-wrong-encoding {10}\r\nαβγδε".b, + server.commands.pop.args) + + imap.test_args "invalid-utf8", "\x80".b.force_encoding("UTF-8") + assert_equal("invalid-utf8 {1}\r\n\x80".b, + server.commands.pop.args) + end + end + + test("send literal args") do + with_fake_server(with_extensions: %w[LITERAL-]) do |server, imap| + # disable automatic non-synchronizing literals + imap.config.max_non_synchronizing_literal = -1 + server.on "TEST", &:done_ok + send_args = ->(*args) do + imap.__send__(:send_command, "TEST", *args) + end + send_args.call ["\xDE\xAD\xBE\xEF".b] + assert_equal "({4}\r\n\xDE\xAD\xBE\xEF)".b, server.commands.pop.args + + send_args.call ["hi\rthere\n", "huh?\r\nfake out"] + assert_equal "({9}\r\nhi\rthere\n {14}\r\nhuh?\r\nfake out)".b, + server.commands.pop.args + + # enable automatic non-synchronizing literals + imap.config.max_non_synchronizing_literal = 1024 + buff = bytes = nil + server.literal_acceptor = proc { buff, bytes = _1, _2; false } + server.on "TEST", &:done_ok + send_args = ->(*args) do + imap.__send__(:send_command, "TEST", *args) + end + send_args.call ["\xDE\xAD\xBE\xEF".b] + assert_equal "({4+}\r\n\xDE\xAD\xBE\xEF)".b, server.commands.pop.args + assert_nil buff + assert_nil bytes + + # limited automatic non-synchronizing literals + imap.config.max_non_synchronizing_literal = 5 + assert_local_raise(Net::IMAP::NoResponseError) do + send_args.call [ + Net::IMAP::Literal["\rhi\r"], + Net::IMAP::Literal["\x01" * 10], + ] + end + assert_match(/TEST \(\{4\+\}\r\n\rhi\r \{10\}\r\n\z/, buff) + assert_equal 10, bytes + assert_empty server.commands + + server.literal_acceptor = proc { true } + send_args.call Net::IMAP::Literal["\x01" * 10] + assert_equal "{10}\r\n\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01", + server.commands.pop.args + + buff = bytes = nil + server.literal_acceptor = proc { buff, bytes = _1, _2; false } + send_args.call("nonsync", + Net::IMAP::Literal[data: "\x01\x02\x03", non_sync: true]) + assert_equal "nonsync {3+}\r\n\x01\x02\03".b, server.commands.pop.args + assert_nil buff + assert_nil bytes + + imap.config.max_non_synchronizing_literal = 5 + server.literal_acceptor = proc { true } + send_args.call("literal", Net::IMAP::Literal["\r", false], + "literal", Net::IMAP::Literal["αβ", nil], + "literal", Net::IMAP::Literal["αβγδε", nil], + "literal+", Net::IMAP::Literal["αβγδε", true], + "literal8", Net::IMAP::Literal8["\0", false], + "literal8+", Net::IMAP::Literal8["\0" * 2, nil], + "literal8", Net::IMAP::Literal8["\0" * 6, nil], + "literal8+", Net::IMAP::Literal8["\0" * 8, true], + "done") + assert_equal("literal" " {1}\r\n\r " \ + "literal" " {4+}\r\nαβ " \ + "literal" " {10}\r\nαβγδε " \ + "literal+" " {10+}\r\nαβγδε " \ + "literal8" " ~{1}\r\n\0 " \ + "literal8+" " ~{2+}\r\n\0\0 " \ + "literal8" " ~{6}\r\n\0\0\0\0\0\0 " \ + "literal8+" " ~{8+}\r\n\0\0\0\0\0\0\0\0 " \ + "done".b, + server.commands.pop.args) + end + end + + test("send non-synchronizing literals with LITERAL+") do + with_fake_server( + with_extensions: %w[LITERAL+], greeting_capabilities: true, + ) do |server, imap| + def imap.send_test_args(*args) = send_command("TEST", *args) + server.on "TEST", &:done_ok + + imap.config.max_non_synchronizing_literal = 5_000 + large = "\xff".b * 5_000 + imap.send_test_args Net::IMAP::Literal[large, nil] + assert_equal("{5000+}\r\n#{large}".b, server.commands.pop.args) + + large = "\xff".b * 10_000 + imap.send_test_args Net::IMAP::Literal[large, nil] + assert_equal("{10000}\r\n#{large}".b, server.commands.pop.args) + + imap.send_test_args Net::IMAP::Literal[large, true] + assert_equal("{10000+}\r\n#{large}".b, server.commands.pop.args) + end + end + + test("send non-synchronizing literal that's too large for LITERAL-") do + with_fake_server( + with_extensions: %w[LITERAL-], greeting_capabilities: true, + ignore_abrupt_eof: true, ignore_io_error: true + ) do |server, imap| + def imap.send_test_args(*args) = send_command("TEST", *args) + server.on "TEST", &:done_ok + assert_raise(Net::IMAP::DataFormatError) do + imap.send_test_args Net::IMAP::Literal["\xff".b * 5000, true] + end + assert imap.disconnected? + end + end + + test("send non-synchronizing literal without known server support") do + with_fake_server( + with_extensions: %w[LITERAL+], greeting_capabilities: false, + ignore_abrupt_eof: true, ignore_io_error: true + ) do |server, imap| + def imap.send_test_args(*args) = send_command("TEST", *args) + server.on "TEST", &:done_ok + assert_raise(Net::IMAP::DataFormatError) do + imap.send_test_args Net::IMAP::Literal["\xff".b * 100, true] + end + assert imap.disconnected? + end + end + +end