diff options
author | Hiroshi SHIBATA <hsbt@ruby-lang.org> | 2021-04-22 14:35:52 +0900 |
---|---|---|
committer | Hiroshi SHIBATA <hsbt@ruby-lang.org> | 2021-04-22 14:37:45 +0900 |
commit | 674760316ce5b68aa182c1b3b25665de250341b3 (patch) | |
tree | 3d0ee5c236607d926e5b169d1d291a620ea9b16d | |
parent | 01f131457ffac39f018b342fbd5f7598171d10fa (diff) | |
download | ruby-674760316ce5b68aa182c1b3b25665de250341b3.tar.gz |
Merge net-imap-0.2.0
-rw-r--r-- | lib/net/imap.rb | 524 | ||||
-rw-r--r-- | test/net/imap/test_imap.rb | 69 | ||||
-rw-r--r-- | test/net/imap/test_imap_response_parser.rb | 78 |
3 files changed, 593 insertions, 78 deletions
diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 36920c4a91..ff9dff41ac 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -201,7 +201,7 @@ module Net # Unicode", RFC 2152, May 1997. # class IMAP < Protocol - VERSION = "0.1.1" + VERSION = "0.2.0" include MonitorMixin if defined?(OpenSSL::SSL) @@ -304,6 +304,16 @@ module Net @@authenticators[auth_type] = authenticator end + # Builds an authenticator for Net::IMAP#authenticate. + def self.authenticator(auth_type, *args) + auth_type = auth_type.upcase + unless @@authenticators.has_key?(auth_type) + raise ArgumentError, + format('unknown auth type - "%s"', auth_type) + end + @@authenticators[auth_type].new(*args) + end + # The default port for IMAP connections, port 143 def self.default_port return PORT @@ -365,6 +375,30 @@ module Net end end + # Sends an ID command, and returns a hash of the server's + # response, or nil if the server does not identify itself. + # + # Note that the user should first check if the server supports the ID + # capability. For example: + # + # capabilities = imap.capability + # if capabilities.include?("ID") + # id = imap.id( + # name: "my IMAP client (ruby)", + # version: MyIMAP::VERSION, + # "support-url": "mailto:bugs@example.com", + # os: RbConfig::CONFIG["host_os"], + # ) + # end + # + # See RFC 2971, Section 3.3, for defined fields. + def id(client_id=nil) + synchronize do + send_command("ID", ClientID.new(client_id)) + @responses.delete("ID")&.last + end + end + # Sends a NOOP command to the server. It does nothing. def noop send_command("NOOP") @@ -408,7 +442,7 @@ module Net # the form "AUTH=LOGIN" or "AUTH=CRAM-MD5". # # Authentication is done using the appropriate authenticator object: - # see @@authenticators for more information on plugging in your own + # see +add_authenticator+ for more information on plugging in your own # authenticator. # # For example: @@ -417,12 +451,7 @@ module Net # # A Net::IMAP::NoResponseError is raised if authentication fails. def authenticate(auth_type, *args) - auth_type = auth_type.upcase - unless @@authenticators.has_key?(auth_type) - raise ArgumentError, - format('unknown auth type - "%s"', auth_type) - end - authenticator = @@authenticators[auth_type].new(*args) + authenticator = self.class.authenticator(auth_type, *args) send_command("AUTHENTICATE", auth_type) do |resp| if resp.instance_of?(ContinuationRequest) data = authenticator.process(resp.data.text.unpack("m")[0]) @@ -552,6 +581,60 @@ module Net end end + # Sends a NAMESPACE command [RFC2342] and returns the namespaces that are + # available. The NAMESPACE command allows a client to discover the prefixes + # of namespaces used by a server for personal mailboxes, other users' + # mailboxes, and shared mailboxes. + # + # This extension predates IMAP4rev1 (RFC3501), so most IMAP servers support + # it. Many popular IMAP servers are configured with the default personal + # namespaces as `("" "/")`: no prefix and "/" hierarchy delimiter. In that + # common case, the naive client may not have any trouble naming mailboxes. + # + # But many servers are configured with the default personal namespace as + # e.g. `("INBOX." ".")`, placing all personal folders under INBOX, with "." + # as the hierarchy delimiter. If the client does not check for this, but + # naively assumes it can use the same folder names for all servers, then + # folder creation (and listing, moving, etc) can lead to errors. + # + # From RFC2342: + # + # Although typically a server will support only a single Personal + # Namespace, and a single Other User's Namespace, circumstances exist + # where there MAY be multiples of these, and a client MUST be prepared + # for them. If a client is configured such that it is required to create + # a certain mailbox, there can be circumstances where it is unclear which + # Personal Namespaces it should create the mailbox in. In these + # situations a client SHOULD let the user select which namespaces to + # create the mailbox in. + # + # The user of this method should first check if the server supports the + # NAMESPACE capability. The return value is a +Net::IMAP::Namespaces+ + # object which has +personal+, +other+, and +shared+ fields, each an array + # of +Net::IMAP::Namespace+ objects. These arrays will be empty when the + # server responds with nil. + # + # For example: + # + # capabilities = imap.capability + # if capabilities.include?("NAMESPACE") + # namespaces = imap.namespace + # if namespace = namespaces.personal.first + # prefix = namespace.prefix # e.g. "" or "INBOX." + # delim = namespace.delim # e.g. "/" or "." + # # personal folders should use the prefix and delimiter + # imap.create(prefix + "foo") + # imap.create(prefix + "bar") + # imap.create(prefix + %w[path to my folder].join(delim)) + # end + # end + def namespace + synchronize do + send_command("NAMESPACE") + return @responses.delete("NAMESPACE")[-1] + end + end + # Sends a XLIST command, and returns a subset of names from # the complete set of all names available to the client. # +refname+ provides a context (for instance, a base directory @@ -1656,6 +1739,74 @@ module Net end end + class ClientID # :nodoc: + + def send_data(imap, tag) + imap.__send__(:send_data, format_internal(@data), tag) + end + + def validate + validate_internal(@data) + end + + private + + def initialize(data) + @data = data + end + + def validate_internal(client_id) + client_id.to_h.each do |k,v| + unless StringFormatter.valid_string?(k) + raise DataFormatError, client_id.inspect + end + end + rescue NoMethodError, TypeError # to_h failed + raise DataFormatError, client_id.inspect + end + + def format_internal(client_id) + return nil if client_id.nil? + client_id.to_h.flat_map {|k,v| + [StringFormatter.string(k), StringFormatter.nstring(v)] + } + end + + end + + module StringFormatter + + LITERAL_REGEX = /[\x80-\xff\r\n]/n + + module_function + + # Allows symbols in addition to strings + def valid_string?(str) + str.is_a?(Symbol) || str.respond_to?(:to_str) + end + + # Allows nil, symbols, and strings + def valid_nstring?(str) + str.nil? || valid_string?(str) + end + + # coerces using +to_s+ + def string(str) + str = str.to_s + if str =~ LITERAL_REGEX + Literal.new(str) + else + QuotedString.new(str) + end + end + + # coerces non-nil using +to_s+ + def nstring(str) + str.nil? ? nil : string(str) + end + + end + # Common validators of number and nz_number types module NumValidator # :nodoc class << self @@ -1747,6 +1898,18 @@ module Net # raw_data:: Returns the raw data string. UntaggedResponse = Struct.new(:name, :data, :raw_data) + # Net::IMAP::IgnoredResponse represents intentionaly ignored responses. + # + # This includes untagged response "NOOP" sent by eg. Zimbra to avoid some + # clients to close the connection. + # + # It matches no IMAP standard. + # + # ==== Fields: + # + # raw_data:: Returns the raw data string. + IgnoredResponse = Struct.new(:raw_data) + # Net::IMAP::TaggedResponse represents tagged responses. # # The server completion result response indicates the success or @@ -1774,8 +1937,7 @@ module Net # Net::IMAP::ResponseText represents texts of responses. # The text may be prefixed by the response code. # - # resp_text ::= ["[" resp_text_code "]" SPACE] (text_mime2 / text) - # ;; text SHOULD NOT begin with "[" or "=" + # resp_text ::= ["[" resp-text-code "]" SP] text # # ==== Fields: # @@ -1787,12 +1949,15 @@ module Net # Net::IMAP::ResponseCode represents response codes. # - # resp_text_code ::= "ALERT" / "PARSE" / - # "PERMANENTFLAGS" SPACE "(" #(flag / "\*") ")" / + # resp_text_code ::= "ALERT" / + # "BADCHARSET" [SP "(" astring *(SP astring) ")" ] / + # capability_data / "PARSE" / + # "PERMANENTFLAGS" SP "(" + # [flag_perm *(SP flag_perm)] ")" / # "READ-ONLY" / "READ-WRITE" / "TRYCREATE" / - # "UIDVALIDITY" SPACE nz_number / - # "UNSEEN" SPACE nz_number / - # atom [SPACE 1*<any TEXT_CHAR except "]">] + # "UIDNEXT" SP nz_number / "UIDVALIDITY" SP nz_number / + # "UNSEEN" SP nz_number / + # atom [SP 1*<any TEXT-CHAR except "]">] # # ==== Fields: # @@ -1872,6 +2037,39 @@ module Net # MailboxACLItem = Struct.new(:user, :rights, :mailbox) + # Net::IMAP::Namespace represents a single [RFC-2342] namespace. + # + # Namespace = nil / "(" 1*( "(" string SP (<"> QUOTED_CHAR <"> / + # nil) *(Namespace_Response_Extension) ")" ) ")" + # + # Namespace_Response_Extension = SP string SP "(" string *(SP string) + # ")" + # + # ==== Fields: + # + # prefix:: Returns the namespace prefix string. + # delim:: Returns nil or the hierarchy delimiter character. + # extensions:: Returns a hash of extension names to extension flag arrays. + # + Namespace = Struct.new(:prefix, :delim, :extensions) + + # Net::IMAP::Namespaces represents the response from [RFC-2342] NAMESPACE. + # + # Namespace_Response = "*" SP "NAMESPACE" SP Namespace SP Namespace SP + # Namespace + # + # ; The first Namespace is the Personal Namespace(s) + # ; The second Namespace is the Other Users' Namespace(s) + # ; The third Namespace is the Shared Namespace(s) + # + # ==== Fields: + # + # personal:: Returns an array of Personal Net::IMAP::Namespace objects. + # other:: Returns an array of Other Users' Net::IMAP::Namespace objects. + # shared:: Returns an array of Shared Net::IMAP::Namespace objects. + # + Namespaces = Struct.new(:personal, :other, :shared) + # Net::IMAP::StatusData represents the contents of the STATUS response. # # ==== Fields: @@ -2291,8 +2489,12 @@ module Net return response_cond when /\A(?:FLAGS)\z/ni return flags_response + when /\A(?:ID)\z/ni + return id_response when /\A(?:LIST|LSUB|XLIST)\z/ni return list_response + when /\A(?:NAMESPACE)\z/ni + return namespace_response when /\A(?:QUOTA)\z/ni return getquota_response when /\A(?:QUOTAROOT)\z/ni @@ -2307,6 +2509,8 @@ module Net return status_response when /\A(?:CAPABILITY)\z/ni return capability_response + when /\A(?:NOOP)\z/ni + return ignored_response else return text_response end @@ -2316,7 +2520,7 @@ module Net end def response_tagged - tag = atom + tag = astring_chars match(T_SPACE) token = match(T_ATOM) name = token.value.upcase @@ -2876,14 +3080,18 @@ module Net return name, modseq end + def ignored_response + while lookahead.symbol != T_CRLF + shift_token + end + return IgnoredResponse.new(@str) + end + def text_response token = match(T_ATOM) name = token.value.upcase match(T_SPACE) - @lex_state = EXPR_TEXT - token = match(T_TEXT) - @lex_state = EXPR_BEG - return UntaggedResponse.new(name, token.value) + return UntaggedResponse.new(name, text) end def flags_response @@ -3114,11 +3322,15 @@ module Net token = match(T_ATOM) name = token.value.upcase match(T_SPACE) + UntaggedResponse.new(name, capability_data, @str) + end + + def capability_data data = [] while true token = lookahead case token.symbol - when T_CRLF + when T_CRLF, T_RBRA break when T_SPACE shift_token @@ -3126,30 +3338,142 @@ module Net end data.push(atom.upcase) end + data + end + + def id_response + token = match(T_ATOM) + name = token.value.upcase + match(T_SPACE) + token = match(T_LPAR, T_NIL) + if token.symbol == T_NIL + return UntaggedResponse.new(name, nil, @str) + else + data = {} + while true + token = lookahead + case token.symbol + when T_RPAR + shift_token + break + when T_SPACE + shift_token + next + else + key = string + match(T_SPACE) + val = nstring + data[key] = val + end + end + return UntaggedResponse.new(name, data, @str) + end + end + + def namespace_response + @lex_state = EXPR_DATA + token = lookahead + token = match(T_ATOM) + name = token.value.upcase + match(T_SPACE) + personal = namespaces + match(T_SPACE) + other = namespaces + match(T_SPACE) + shared = namespaces + @lex_state = EXPR_BEG + data = Namespaces.new(personal, other, shared) return UntaggedResponse.new(name, data, @str) end - def resp_text - @lex_state = EXPR_RTEXT + def namespaces token = lookahead - if token.symbol == T_LBRA - code = resp_text_code + # empty () is not allowed, so nil is functionally identical to empty. + data = [] + if token.symbol == T_NIL + shift_token else - code = nil + match(T_LPAR) + loop do + data << namespace + break unless lookahead.symbol == T_SPACE + shift_token + end + match(T_RPAR) + end + data + end + + def namespace + match(T_LPAR) + prefix = match(T_QUOTED, T_LITERAL).value + match(T_SPACE) + delimiter = string + extensions = namespace_response_extensions + match(T_RPAR) + Namespace.new(prefix, delimiter, extensions) + end + + def namespace_response_extensions + data = {} + token = lookahead + if token.symbol == T_SPACE + shift_token + name = match(T_QUOTED, T_LITERAL).value + data[name] ||= [] + match(T_SPACE) + match(T_LPAR) + loop do + data[name].push match(T_QUOTED, T_LITERAL).value + break unless lookahead.symbol == T_SPACE + shift_token + end + match(T_RPAR) + end + data + end + + # text = 1*TEXT-CHAR + # TEXT-CHAR = <any CHAR except CR and LF> + def text + match(T_TEXT, lex_state: EXPR_TEXT).value + end + + # resp-text = ["[" resp-text-code "]" SP] text + def resp_text + token = match(T_LBRA, T_TEXT, lex_state: EXPR_RTEXT) + case token.symbol + when T_LBRA + code = resp_text_code + match(T_RBRA) + accept_space # violating RFC + ResponseText.new(code, text) + when T_TEXT + ResponseText.new(nil, token.value) end - token = match(T_TEXT) - @lex_state = EXPR_BEG - return ResponseText.new(code, token.value) end + # See https://www.rfc-editor.org/errata/rfc3501 + # + # resp-text-code = "ALERT" / + # "BADCHARSET" [SP "(" charset *(SP charset) ")" ] / + # capability-data / "PARSE" / + # "PERMANENTFLAGS" SP "(" + # [flag-perm *(SP flag-perm)] ")" / + # "READ-ONLY" / "READ-WRITE" / "TRYCREATE" / + # "UIDNEXT" SP nz-number / "UIDVALIDITY" SP nz-number / + # "UNSEEN" SP nz-number / + # atom [SP 1*<any TEXT-CHAR except "]">] def resp_text_code - @lex_state = EXPR_BEG - match(T_LBRA) token = match(T_ATOM) name = token.value.upcase case name when /\A(?:ALERT|PARSE|READ-ONLY|READ-WRITE|TRYCREATE|NOMODSEQ)\z/n result = ResponseCode.new(name, nil) + when /\A(?:BADCHARSET)\z/n + result = ResponseCode.new(name, charset_list) + when /\A(?:CAPABILITY)\z/ni + result = ResponseCode.new(name, capability_data) when /\A(?:PERMANENTFLAGS)\z/n match(T_SPACE) result = ResponseCode.new(name, flag_list) @@ -3160,19 +3484,28 @@ module Net token = lookahead if token.symbol == T_SPACE shift_token - @lex_state = EXPR_CTEXT - token = match(T_TEXT) - @lex_state = EXPR_BEG + token = match(T_TEXT, lex_state: EXPR_CTEXT) result = ResponseCode.new(name, token.value) else result = ResponseCode.new(name, nil) end end - match(T_RBRA) - @lex_state = EXPR_RTEXT return result end + def charset_list + result = [] + if accept(T_SPACE) + match(T_LPAR) + result << charset + while accept(T_SPACE) + result << charset + end + match(T_RPAR) + end + result + end + def address_list token = lookahead if token.symbol == T_NIL @@ -3269,7 +3602,7 @@ module Net if string_token?(token) return string else - return atom + return astring_chars end end @@ -3299,34 +3632,49 @@ module Net return token.value.upcase end - def atom - result = String.new - while true - token = lookahead - if atom_token?(token) - result.concat(token.value) - shift_token - else - if result.empty? - parse_error("unexpected token %s", token.symbol) - else - return result - end - end - end - end - + # atom = 1*ATOM-CHAR + # ATOM-CHAR = <any CHAR except atom-specials> ATOM_TOKENS = [ T_ATOM, T_NUMBER, T_NIL, T_LBRA, - T_RBRA, T_PLUS ] - def atom_token?(token) - return ATOM_TOKENS.include?(token.symbol) + def atom + -combine_adjacent(*ATOM_TOKENS) + end + + # ASTRING-CHAR = ATOM-CHAR / resp-specials + # resp-specials = "]" + ASTRING_CHARS_TOKENS = [*ATOM_TOKENS, T_RBRA] + + def astring_chars + combine_adjacent(*ASTRING_CHARS_TOKENS) + end + + def combine_adjacent(*tokens) + result = "".b + while token = accept(*tokens) + result << token.value + end + if result.empty? + parse_error('unexpected token %s (expected %s)', + lookahead.symbol, args.join(" or ")) + end + result + end + + # See https://www.rfc-editor.org/errata/rfc3501 + # + # charset = atom / quoted + def charset + if token = accept(T_QUOTED) + token.value + else + atom + end end def number @@ -3344,22 +3692,62 @@ module Net return nil end - def match(*args) + SPACES_REGEXP = /\G */n + + # This advances @pos directly so it's safe before changing @lex_state. + def accept_space + if @token + shift_token if @token.symbol == T_SPACE + elsif @str[@pos] == " " + @pos += 1 + end + end + + # The RFC is very strict about this and usually we should be too. + # But skipping spaces is usually a safe workaround for buggy servers. + # + # This advances @pos directly so it's safe before changing @lex_state. + def accept_spaces + shift_token if @token&.symbol == T_SPACE + if @str.index(SPACES_REGEXP, @pos) + @pos = $~.end(0) + end + end + + def match(*args, lex_state: @lex_state) + if @token && lex_state != @lex_state + parse_error("invalid lex_state change to %s with unconsumed token", + lex_state) + end + begin + @lex_state, original_lex_state = lex_state, @lex_state + token = lookahead + unless args.include?(token.symbol) + parse_error('unexpected token %s (expected %s)', + token.symbol.id2name, + args.collect {|i| i.id2name}.join(" or ")) + end + shift_token + return token + ensure + @lex_state = original_lex_state + end + end + + # like match, but does not raise error on failure. + # + # returns and shifts token on successful match + # returns nil and leaves @token unshifted on no match + def accept(*args) token = lookahead - unless args.include?(token.symbol) - parse_error('unexpected token %s (expected %s)', - token.symbol.id2name, - args.collect {|i| i.id2name}.join(" or ")) + if args.include?(token.symbol) + shift_token + token end - shift_token - return token end def lookahead - unless @token - @token = next_token - end - return @token + @token ||= next_token end def shift_token diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb index 8b924b524e..4fb9f744fc 100644 --- a/test/net/imap/test_imap.rb +++ b/test/net/imap/test_imap.rb @@ -578,23 +578,23 @@ class IMAPTest < Test::Unit::TestCase begin imap = Net::IMAP.new(server_addr, :port => port) assert_raise(Net::IMAP::DataFormatError) do - imap.send(:send_command, "TEST", -1) + imap.__send__(:send_command, "TEST", -1) end - imap.send(:send_command, "TEST", 0) - imap.send(:send_command, "TEST", 4294967295) + imap.__send__(:send_command, "TEST", 0) + imap.__send__(:send_command, "TEST", 4294967295) assert_raise(Net::IMAP::DataFormatError) do - imap.send(:send_command, "TEST", 4294967296) + imap.__send__(:send_command, "TEST", 4294967296) end assert_raise(Net::IMAP::DataFormatError) do - imap.send(:send_command, "TEST", Net::IMAP::MessageSet.new(-1)) + imap.__send__(:send_command, "TEST", Net::IMAP::MessageSet.new(-1)) end assert_raise(Net::IMAP::DataFormatError) do - imap.send(:send_command, "TEST", Net::IMAP::MessageSet.new(0)) + imap.__send__(:send_command, "TEST", Net::IMAP::MessageSet.new(0)) end - imap.send(:send_command, "TEST", Net::IMAP::MessageSet.new(1)) - imap.send(:send_command, "TEST", Net::IMAP::MessageSet.new(4294967295)) + imap.__send__(:send_command, "TEST", Net::IMAP::MessageSet.new(1)) + imap.__send__(:send_command, "TEST", Net::IMAP::MessageSet.new(4294967295)) assert_raise(Net::IMAP::DataFormatError) do - imap.send(:send_command, "TEST", Net::IMAP::MessageSet.new(4294967296)) + imap.__send__(:send_command, "TEST", Net::IMAP::MessageSet.new(4294967296)) end imap.logout ensure @@ -628,7 +628,7 @@ class IMAPTest < Test::Unit::TestCase end begin imap = Net::IMAP.new(server_addr, :port => port) - imap.send(:send_command, "TEST", ["\xDE\xAD\xBE\xEF".b]) + imap.__send__(:send_command, "TEST", ["\xDE\xAD\xBE\xEF".b]) assert_equal(2, requests.length) assert_equal("RUBY0001 TEST ({4}\r\n", requests[0]) assert_equal("\xDE\xAD\xBE\xEF".b, literal) @@ -753,6 +753,55 @@ EOF 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 diff --git a/test/net/imap/test_imap_response_parser.rb b/test/net/imap/test_imap_response_parser.rb index 4e470459c9..5b519edeff 100644 --- a/test/net/imap/test_imap_response_parser.rb +++ b/test/net/imap/test_imap_response_parser.rb @@ -234,6 +234,27 @@ EOF response = parser.parse("* CAPABILITY st11p00mm-iscream009 1Q49 XAPPLEPUSHSERVICE IMAP4 IMAP4rev1 SASL-IR AUTH=ATOKEN AUTH=PLAIN \r\n") assert_equal("CAPABILITY", response.name) assert_equal("AUTH=PLAIN", response.data.last) + response = parser.parse("* OK [CAPABILITY IMAP4rev1 SASL-IR 1234 NIL THIS+THAT + AUTH=PLAIN ID] IMAP4rev1 Hello\r\n") + assert_equal("OK", response.name) + assert_equal("IMAP4rev1 Hello", response.data.text) + code = response.data.code + assert_equal("CAPABILITY", code.name) + assert_equal( + ["IMAP4REV1", "SASL-IR", "1234", "NIL", "THIS+THAT", "+", "AUTH=PLAIN", "ID"], + code.data + ) + end + + def test_id + parser = Net::IMAP::ResponseParser.new + response = parser.parse("* ID NIL\r\n") + assert_equal("ID", response.name) + assert_equal(nil, response.data) + response = parser.parse("* ID (\"name\" \"GImap\" \"vendor\" \"Google, Inc.\" \"support-url\" NIL)\r\n") + assert_equal("ID", response.name) + assert_equal("GImap", response.data["name"]) + assert_equal("Google, Inc.", response.data["vendor"]) + assert_equal(nil, response.data.fetch("support-url")) end def test_mixed_boundary @@ -301,6 +322,22 @@ EOF assert_equal(12345, response.data.attr["MODSEQ"]) end + def test_msg_rfc3501_response_text_with_T_LBRA + parser = Net::IMAP::ResponseParser.new + response = parser.parse("RUBY0004 OK [READ-WRITE] [Gmail]/Sent Mail selected. (Success)\r\n") + assert_equal("RUBY0004", response.tag) + assert_equal("READ-WRITE", response.data.code.name) + assert_equal("[Gmail]/Sent Mail selected. (Success)", response.data.text) + end + + def test_msg_rfc3501_response_text_with_BADCHARSET_astrings + parser = Net::IMAP::ResponseParser.new + response = parser.parse("t BAD [BADCHARSET (US-ASCII \"[astring with brackets]\")] unsupported charset foo.\r\n") + assert_equal("t", response.tag) + assert_equal("unsupported charset foo.", response.data.text) + assert_equal("BADCHARSET", response.data.code.name) + end + def test_continuation_request_without_response_text parser = Net::IMAP::ResponseParser.new response = parser.parse("+\r\n") @@ -308,4 +345,45 @@ EOF assert_equal(nil, response.data.code) assert_equal("", response.data.text) end + + def test_ignored_response + parser = Net::IMAP::ResponseParser.new + response = nil + assert_nothing_raised do + response = parser.parse("* NOOP\r\n") + end + assert_instance_of(Net::IMAP::IgnoredResponse, response) + end + + def test_namespace + parser = Net::IMAP::ResponseParser.new + # RFC2342 Example 5.1 + response = parser.parse(%Q{* NAMESPACE (("" "/")) NIL NIL\r\n}) + assert_equal("NAMESPACE", response.name) + assert_equal([Net::IMAP::Namespace.new("", "/", {})], response.data.personal) + assert_equal([], response.data.other) + assert_equal([], response.data.shared) + # RFC2342 Example 5.4 + response = parser.parse(%Q{* NAMESPACE (("" "/")) (("~" "/")) (("#shared/" "/")} + + %Q{ ("#public/" "/") ("#ftp/" "/") ("#news." "."))\r\n}) + assert_equal("NAMESPACE", response.name) + assert_equal([Net::IMAP::Namespace.new("", "/", {})], response.data.personal) + assert_equal([Net::IMAP::Namespace.new("~", "/", {})], response.data.other) + assert_equal( + [ + Net::IMAP::Namespace.new("#shared/", "/", {}), + Net::IMAP::Namespace.new("#public/", "/", {}), + Net::IMAP::Namespace.new("#ftp/", "/", {}), + Net::IMAP::Namespace.new("#news.", ".", {}), + ], + response.data.shared + ) + # RFC2342 Example 5.6 + response = parser.parse(%Q{* NAMESPACE (("" "/") ("#mh/" "/" "X-PARAM" ("FLAG1" "FLAG2"))) NIL NIL\r\n}) + assert_equal("NAMESPACE", response.name) + namespace = response.data.personal.last + assert_equal("#mh/", namespace.prefix) + assert_equal("/", namespace.delim) + assert_equal({"X-PARAM" => ["FLAG1", "FLAG2"]}, namespace.extensions) + end end |