Merge net-imap-0.2.0

This commit is contained in:
Hiroshi SHIBATA 2021-04-22 14:35:52 +09:00
parent 01f131457f
commit 674760316c
No known key found for this signature in database
GPG Key ID: F9CF13417264FAC2
3 changed files with 594 additions and 79 deletions

View File

@ -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
token = match(T_TEXT)
@lex_state = EXPR_BEG
return ResponseText.new(code, token.value)
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
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)
token = lookahead
unless args.include?(token.symbol)
parse_error('unexpected token %s (expected %s)',
token.symbol.id2name,
args.collect {|i| i.id2name}.join(" or "))
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
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

View File

@ -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

View File

@ -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