Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions lib/net/imap/command_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,23 @@ def send_binary_literal(*, **) = send_literal(*, **, binary: true)

# `non_sync` is an optional tri-state flag:
# * `true` -> Force non-synchronizing `LITERAL+`/`LITERAL-` behavior.
# TODO: raise or warn when capabilities don't allow non_sync.
# NOTE: raises DataFormatError when server doesn't support
# non-synchronizing literal, or literal is too large for LITERAL-.
# * `false` -> Force normal synchronizing literal behavior.
# * `nil` -> (default) Currently behaves like `false` (will be dynamic).
def send_literal(str, tag = nil, binary: false, non_sync: nil)
bytesize = str.bytesize
synchronize do
non_sync = non_sync_literal?(str.bytesize) if non_sync.nil?
if non_sync && !non_sync_literal_allowed?(bytesize)
# TODO: check in Printer, so we don't need to close the connection.
@sock.close
raise DataFormatError, "Connection closed: " \
"Cannot send non-synchronizing literal without known server support"
end
non_sync = non_sync_literal?(bytesize) if non_sync.nil?
prefix = "~" if binary
plus = "+" if non_sync
put_string("#{prefix}{#{str.bytesize}#{plus}}\r\n")
put_string("#{prefix}{#{bytesize}#{plus}}\r\n")
if non_sync
put_string(str)
return
Expand All @@ -113,12 +121,19 @@ def send_literal(str, tag = nil, binary: false, non_sync: nil)
end

def non_sync_literal?(bytesize)
capabilities_cached? &&
bytesize <= config.max_non_synchronizing_literal &&
(capable?("LITERAL+") ||
bytesize <= 4096 && (capable?("IMAP4rev2") || capable?("LITERAL-")))
bytesize <= config.max_non_synchronizing_literal \
&& non_sync_literal_allowed?(bytesize)
end

def non_sync_literal_allowed?(bytesize)
return unless capabilities_cached?
return "+" if capable?("LITERAL+")
return "-" if capable_literal_minus? && bytesize <= 4096
false
end

def capable_literal_minus? = capable?("LITERAL-") || capable?("IMAP4rev2")

# NOTE: +num+ should already be an Integer
def send_number_data(num)
put_string(Integer(num).to_s)
Expand Down
49 changes: 49 additions & 0 deletions test/net/imap/test_imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,55 @@ def test_raw_data
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]
Expand Down
Loading