Merge net-imap-0.2.0
This commit is contained in:
parent
01f131457f
commit
674760316c
526
lib/net/imap.rb
526
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
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user