diff --git a/lib/net/imap/command_data.rb b/lib/net/imap/command_data.rb index e46125b9..f7f22955 100644 --- a/lib/net/imap/command_data.rb +++ b/lib/net/imap/command_data.rb @@ -15,6 +15,9 @@ def validate_data(data) case data when nil when String + if data.include?("\0") + raise DataFormatError, "String argument contains NULL byte" + end when Integer # Covers modseq-valzer, which is the largest valid IMAP integer if data.negative? @@ -55,24 +58,32 @@ def send_data(data, tag = nil) end end + UNQUOTABLE_CHARS = /\0\r\n/n + ASTRING_SPECIALS = /[(){ \x00-\x1f\x7f%*"\\]/n + private_constant :UNQUOTABLE_CHARS, :ASTRING_SPECIALS + + # Sends generic strings formatted as +astring+: + # * as an atom (note: this is based on +ASTRING-CHAR+, not +ATOM-CHAR+) + # * as a quoted string + # * as a literal (may send non-synchronizing) + # + # NOTE: This does not validate that +str+ contains no +NULL+ bytes. def send_string_data(str, tag = nil) if str.empty? + # same as send_quoted_string(str), but incompatible encoding is allowed put_string('""') - elsif str.match?(/[\r\n]/n) - # literal, because multiline + elsif str.match?(UNQUOTABLE_CHARS) send_literal(str, tag) elsif !str.ascii_only? if @utf8_strings - # quoted string send_quoted_string(str) else - # literal, because of non-ASCII bytes send_literal(str, tag) end - elsif str.match?(/[(){ \x00-\x1f\x7f%*"\\]/n) - # quoted string + elsif str.match?(ASTRING_SPECIALS) send_quoted_string(str) else + # valid +astring+ atom: non-empty, ASCII only, no ASTRING_SPECIALS put_string(str) end end @@ -273,6 +284,17 @@ def self.extract_literal(data, binary:, bytesize:, non_sync:) private_class_method :extract_literal end + # Please note that +ATOM-CHAR+ and +atom+ are not the same as +ASTRING-char+ + # and the atom form of +astring+. +astring+ allows ], but +atom+ + # does not. + # + # Generic string arguments in Net::IMAP use +astring+, so they will send as + # atoms even if they contain ]. + # + # This class is used by: + # * to validate generic symbol arguments, as the superclass of Flag + # * to validate #enable +capabilities+ + # * to validate #store (and #uid_store) +attr+ class Atom < CommandData # :nodoc: def initialize(**) super diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb index 2140d5e1..6bd9aada 100644 --- a/test/net/imap/test_imap.rb +++ b/test/net/imap/test_imap.rb @@ -921,6 +921,94 @@ def test_raw_data 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) + + # 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) + + # After enabling UTF-8 strings + imap.enable(:utf8) + server.commands.pop.args => ["UTF8=ACCEPT"] + + imap.test_args "quoted-utf8", "αβγδε" + assert_equal 'quoted-utf8 "αβγδε"'.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