Merge net-imap-0.2.0
This commit is contained in:
parent
01f131457f
commit
674760316c
512
lib/net/imap.rb
512
lib/net/imap.rb
@ -201,7 +201,7 @@ module Net
|
|||||||
# Unicode", RFC 2152, May 1997.
|
# Unicode", RFC 2152, May 1997.
|
||||||
#
|
#
|
||||||
class IMAP < Protocol
|
class IMAP < Protocol
|
||||||
VERSION = "0.1.1"
|
VERSION = "0.2.0"
|
||||||
|
|
||||||
include MonitorMixin
|
include MonitorMixin
|
||||||
if defined?(OpenSSL::SSL)
|
if defined?(OpenSSL::SSL)
|
||||||
@ -304,6 +304,16 @@ module Net
|
|||||||
@@authenticators[auth_type] = authenticator
|
@@authenticators[auth_type] = authenticator
|
||||||
end
|
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
|
# The default port for IMAP connections, port 143
|
||||||
def self.default_port
|
def self.default_port
|
||||||
return PORT
|
return PORT
|
||||||
@ -365,6 +375,30 @@ module Net
|
|||||||
end
|
end
|
||||||
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.
|
# Sends a NOOP command to the server. It does nothing.
|
||||||
def noop
|
def noop
|
||||||
send_command("NOOP")
|
send_command("NOOP")
|
||||||
@ -408,7 +442,7 @@ module Net
|
|||||||
# the form "AUTH=LOGIN" or "AUTH=CRAM-MD5".
|
# the form "AUTH=LOGIN" or "AUTH=CRAM-MD5".
|
||||||
#
|
#
|
||||||
# Authentication is done using the appropriate authenticator object:
|
# 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.
|
# authenticator.
|
||||||
#
|
#
|
||||||
# For example:
|
# For example:
|
||||||
@ -417,12 +451,7 @@ module Net
|
|||||||
#
|
#
|
||||||
# A Net::IMAP::NoResponseError is raised if authentication fails.
|
# A Net::IMAP::NoResponseError is raised if authentication fails.
|
||||||
def authenticate(auth_type, *args)
|
def authenticate(auth_type, *args)
|
||||||
auth_type = auth_type.upcase
|
authenticator = self.class.authenticator(auth_type, *args)
|
||||||
unless @@authenticators.has_key?(auth_type)
|
|
||||||
raise ArgumentError,
|
|
||||||
format('unknown auth type - "%s"', auth_type)
|
|
||||||
end
|
|
||||||
authenticator = @@authenticators[auth_type].new(*args)
|
|
||||||
send_command("AUTHENTICATE", auth_type) do |resp|
|
send_command("AUTHENTICATE", auth_type) do |resp|
|
||||||
if resp.instance_of?(ContinuationRequest)
|
if resp.instance_of?(ContinuationRequest)
|
||||||
data = authenticator.process(resp.data.text.unpack("m")[0])
|
data = authenticator.process(resp.data.text.unpack("m")[0])
|
||||||
@ -552,6 +581,60 @@ module Net
|
|||||||
end
|
end
|
||||||
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
|
# Sends a XLIST command, and returns a subset of names from
|
||||||
# the complete set of all names available to the client.
|
# the complete set of all names available to the client.
|
||||||
# +refname+ provides a context (for instance, a base directory
|
# +refname+ provides a context (for instance, a base directory
|
||||||
@ -1656,6 +1739,74 @@ module Net
|
|||||||
end
|
end
|
||||||
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
|
# Common validators of number and nz_number types
|
||||||
module NumValidator # :nodoc
|
module NumValidator # :nodoc
|
||||||
class << self
|
class << self
|
||||||
@ -1747,6 +1898,18 @@ module Net
|
|||||||
# raw_data:: Returns the raw data string.
|
# raw_data:: Returns the raw data string.
|
||||||
UntaggedResponse = Struct.new(:name, :data, :raw_data)
|
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.
|
# Net::IMAP::TaggedResponse represents tagged responses.
|
||||||
#
|
#
|
||||||
# The server completion result response indicates the success or
|
# The server completion result response indicates the success or
|
||||||
@ -1774,8 +1937,7 @@ module Net
|
|||||||
# Net::IMAP::ResponseText represents texts of responses.
|
# Net::IMAP::ResponseText represents texts of responses.
|
||||||
# The text may be prefixed by the response code.
|
# The text may be prefixed by the response code.
|
||||||
#
|
#
|
||||||
# resp_text ::= ["[" resp_text_code "]" SPACE] (text_mime2 / text)
|
# resp_text ::= ["[" resp-text-code "]" SP] text
|
||||||
# ;; text SHOULD NOT begin with "[" or "="
|
|
||||||
#
|
#
|
||||||
# ==== Fields:
|
# ==== Fields:
|
||||||
#
|
#
|
||||||
@ -1787,12 +1949,15 @@ module Net
|
|||||||
|
|
||||||
# Net::IMAP::ResponseCode represents response codes.
|
# Net::IMAP::ResponseCode represents response codes.
|
||||||
#
|
#
|
||||||
# resp_text_code ::= "ALERT" / "PARSE" /
|
# resp_text_code ::= "ALERT" /
|
||||||
# "PERMANENTFLAGS" SPACE "(" #(flag / "\*") ")" /
|
# "BADCHARSET" [SP "(" astring *(SP astring) ")" ] /
|
||||||
|
# capability_data / "PARSE" /
|
||||||
|
# "PERMANENTFLAGS" SP "("
|
||||||
|
# [flag_perm *(SP flag_perm)] ")" /
|
||||||
# "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
|
# "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
|
||||||
# "UIDVALIDITY" SPACE nz_number /
|
# "UIDNEXT" SP nz_number / "UIDVALIDITY" SP nz_number /
|
||||||
# "UNSEEN" SPACE nz_number /
|
# "UNSEEN" SP nz_number /
|
||||||
# atom [SPACE 1*<any TEXT_CHAR except "]">]
|
# atom [SP 1*<any TEXT-CHAR except "]">]
|
||||||
#
|
#
|
||||||
# ==== Fields:
|
# ==== Fields:
|
||||||
#
|
#
|
||||||
@ -1872,6 +2037,39 @@ module Net
|
|||||||
#
|
#
|
||||||
MailboxACLItem = Struct.new(:user, :rights, :mailbox)
|
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.
|
# Net::IMAP::StatusData represents the contents of the STATUS response.
|
||||||
#
|
#
|
||||||
# ==== Fields:
|
# ==== Fields:
|
||||||
@ -2291,8 +2489,12 @@ module Net
|
|||||||
return response_cond
|
return response_cond
|
||||||
when /\A(?:FLAGS)\z/ni
|
when /\A(?:FLAGS)\z/ni
|
||||||
return flags_response
|
return flags_response
|
||||||
|
when /\A(?:ID)\z/ni
|
||||||
|
return id_response
|
||||||
when /\A(?:LIST|LSUB|XLIST)\z/ni
|
when /\A(?:LIST|LSUB|XLIST)\z/ni
|
||||||
return list_response
|
return list_response
|
||||||
|
when /\A(?:NAMESPACE)\z/ni
|
||||||
|
return namespace_response
|
||||||
when /\A(?:QUOTA)\z/ni
|
when /\A(?:QUOTA)\z/ni
|
||||||
return getquota_response
|
return getquota_response
|
||||||
when /\A(?:QUOTAROOT)\z/ni
|
when /\A(?:QUOTAROOT)\z/ni
|
||||||
@ -2307,6 +2509,8 @@ module Net
|
|||||||
return status_response
|
return status_response
|
||||||
when /\A(?:CAPABILITY)\z/ni
|
when /\A(?:CAPABILITY)\z/ni
|
||||||
return capability_response
|
return capability_response
|
||||||
|
when /\A(?:NOOP)\z/ni
|
||||||
|
return ignored_response
|
||||||
else
|
else
|
||||||
return text_response
|
return text_response
|
||||||
end
|
end
|
||||||
@ -2316,7 +2520,7 @@ module Net
|
|||||||
end
|
end
|
||||||
|
|
||||||
def response_tagged
|
def response_tagged
|
||||||
tag = atom
|
tag = astring_chars
|
||||||
match(T_SPACE)
|
match(T_SPACE)
|
||||||
token = match(T_ATOM)
|
token = match(T_ATOM)
|
||||||
name = token.value.upcase
|
name = token.value.upcase
|
||||||
@ -2876,14 +3080,18 @@ module Net
|
|||||||
return name, modseq
|
return name, modseq
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ignored_response
|
||||||
|
while lookahead.symbol != T_CRLF
|
||||||
|
shift_token
|
||||||
|
end
|
||||||
|
return IgnoredResponse.new(@str)
|
||||||
|
end
|
||||||
|
|
||||||
def text_response
|
def text_response
|
||||||
token = match(T_ATOM)
|
token = match(T_ATOM)
|
||||||
name = token.value.upcase
|
name = token.value.upcase
|
||||||
match(T_SPACE)
|
match(T_SPACE)
|
||||||
@lex_state = EXPR_TEXT
|
return UntaggedResponse.new(name, text)
|
||||||
token = match(T_TEXT)
|
|
||||||
@lex_state = EXPR_BEG
|
|
||||||
return UntaggedResponse.new(name, token.value)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def flags_response
|
def flags_response
|
||||||
@ -3114,11 +3322,15 @@ module Net
|
|||||||
token = match(T_ATOM)
|
token = match(T_ATOM)
|
||||||
name = token.value.upcase
|
name = token.value.upcase
|
||||||
match(T_SPACE)
|
match(T_SPACE)
|
||||||
|
UntaggedResponse.new(name, capability_data, @str)
|
||||||
|
end
|
||||||
|
|
||||||
|
def capability_data
|
||||||
data = []
|
data = []
|
||||||
while true
|
while true
|
||||||
token = lookahead
|
token = lookahead
|
||||||
case token.symbol
|
case token.symbol
|
||||||
when T_CRLF
|
when T_CRLF, T_RBRA
|
||||||
break
|
break
|
||||||
when T_SPACE
|
when T_SPACE
|
||||||
shift_token
|
shift_token
|
||||||
@ -3126,30 +3338,142 @@ module Net
|
|||||||
end
|
end
|
||||||
data.push(atom.upcase)
|
data.push(atom.upcase)
|
||||||
end
|
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)
|
return UntaggedResponse.new(name, data, @str)
|
||||||
end
|
end
|
||||||
|
|
||||||
def resp_text
|
def namespaces
|
||||||
@lex_state = EXPR_RTEXT
|
|
||||||
token = lookahead
|
token = lookahead
|
||||||
if token.symbol == T_LBRA
|
# empty () is not allowed, so nil is functionally identical to empty.
|
||||||
code = resp_text_code
|
data = []
|
||||||
|
if token.symbol == T_NIL
|
||||||
|
shift_token
|
||||||
else
|
else
|
||||||
code = nil
|
match(T_LPAR)
|
||||||
|
loop do
|
||||||
|
data << namespace
|
||||||
|
break unless lookahead.symbol == T_SPACE
|
||||||
|
shift_token
|
||||||
end
|
end
|
||||||
token = match(T_TEXT)
|
match(T_RPAR)
|
||||||
@lex_state = EXPR_BEG
|
end
|
||||||
return ResponseText.new(code, token.value)
|
data
|
||||||
end
|
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
|
def resp_text_code
|
||||||
@lex_state = EXPR_BEG
|
|
||||||
match(T_LBRA)
|
|
||||||
token = match(T_ATOM)
|
token = match(T_ATOM)
|
||||||
name = token.value.upcase
|
name = token.value.upcase
|
||||||
case name
|
case name
|
||||||
when /\A(?:ALERT|PARSE|READ-ONLY|READ-WRITE|TRYCREATE|NOMODSEQ)\z/n
|
when /\A(?:ALERT|PARSE|READ-ONLY|READ-WRITE|TRYCREATE|NOMODSEQ)\z/n
|
||||||
result = ResponseCode.new(name, nil)
|
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
|
when /\A(?:PERMANENTFLAGS)\z/n
|
||||||
match(T_SPACE)
|
match(T_SPACE)
|
||||||
result = ResponseCode.new(name, flag_list)
|
result = ResponseCode.new(name, flag_list)
|
||||||
@ -3160,19 +3484,28 @@ module Net
|
|||||||
token = lookahead
|
token = lookahead
|
||||||
if token.symbol == T_SPACE
|
if token.symbol == T_SPACE
|
||||||
shift_token
|
shift_token
|
||||||
@lex_state = EXPR_CTEXT
|
token = match(T_TEXT, lex_state: EXPR_CTEXT)
|
||||||
token = match(T_TEXT)
|
|
||||||
@lex_state = EXPR_BEG
|
|
||||||
result = ResponseCode.new(name, token.value)
|
result = ResponseCode.new(name, token.value)
|
||||||
else
|
else
|
||||||
result = ResponseCode.new(name, nil)
|
result = ResponseCode.new(name, nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
match(T_RBRA)
|
|
||||||
@lex_state = EXPR_RTEXT
|
|
||||||
return result
|
return result
|
||||||
end
|
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
|
def address_list
|
||||||
token = lookahead
|
token = lookahead
|
||||||
if token.symbol == T_NIL
|
if token.symbol == T_NIL
|
||||||
@ -3269,7 +3602,7 @@ module Net
|
|||||||
if string_token?(token)
|
if string_token?(token)
|
||||||
return string
|
return string
|
||||||
else
|
else
|
||||||
return atom
|
return astring_chars
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -3299,34 +3632,49 @@ module Net
|
|||||||
return token.value.upcase
|
return token.value.upcase
|
||||||
end
|
end
|
||||||
|
|
||||||
def atom
|
# atom = 1*ATOM-CHAR
|
||||||
result = String.new
|
# ATOM-CHAR = <any CHAR except atom-specials>
|
||||||
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_TOKENS = [
|
ATOM_TOKENS = [
|
||||||
T_ATOM,
|
T_ATOM,
|
||||||
T_NUMBER,
|
T_NUMBER,
|
||||||
T_NIL,
|
T_NIL,
|
||||||
T_LBRA,
|
T_LBRA,
|
||||||
T_RBRA,
|
|
||||||
T_PLUS
|
T_PLUS
|
||||||
]
|
]
|
||||||
|
|
||||||
def atom_token?(token)
|
def atom
|
||||||
return ATOM_TOKENS.include?(token.symbol)
|
-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
|
end
|
||||||
|
|
||||||
def number
|
def number
|
||||||
@ -3344,7 +3692,35 @@ module Net
|
|||||||
return nil
|
return nil
|
||||||
end
|
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
|
token = lookahead
|
||||||
unless args.include?(token.symbol)
|
unless args.include?(token.symbol)
|
||||||
parse_error('unexpected token %s (expected %s)',
|
parse_error('unexpected token %s (expected %s)',
|
||||||
@ -3353,13 +3729,25 @@ module Net
|
|||||||
end
|
end
|
||||||
shift_token
|
shift_token
|
||||||
return 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
|
||||||
end
|
end
|
||||||
|
|
||||||
def lookahead
|
def lookahead
|
||||||
unless @token
|
@token ||= next_token
|
||||||
@token = next_token
|
|
||||||
end
|
|
||||||
return @token
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def shift_token
|
def shift_token
|
||||||
|
@ -578,23 +578,23 @@ class IMAPTest < Test::Unit::TestCase
|
|||||||
begin
|
begin
|
||||||
imap = Net::IMAP.new(server_addr, :port => port)
|
imap = Net::IMAP.new(server_addr, :port => port)
|
||||||
assert_raise(Net::IMAP::DataFormatError) do
|
assert_raise(Net::IMAP::DataFormatError) do
|
||||||
imap.send(:send_command, "TEST", -1)
|
imap.__send__(:send_command, "TEST", -1)
|
||||||
end
|
end
|
||||||
imap.send(:send_command, "TEST", 0)
|
imap.__send__(:send_command, "TEST", 0)
|
||||||
imap.send(:send_command, "TEST", 4294967295)
|
imap.__send__(:send_command, "TEST", 4294967295)
|
||||||
assert_raise(Net::IMAP::DataFormatError) do
|
assert_raise(Net::IMAP::DataFormatError) do
|
||||||
imap.send(:send_command, "TEST", 4294967296)
|
imap.__send__(:send_command, "TEST", 4294967296)
|
||||||
end
|
end
|
||||||
assert_raise(Net::IMAP::DataFormatError) do
|
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
|
end
|
||||||
assert_raise(Net::IMAP::DataFormatError) do
|
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
|
end
|
||||||
imap.send(:send_command, "TEST", Net::IMAP::MessageSet.new(1))
|
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(4294967295))
|
||||||
assert_raise(Net::IMAP::DataFormatError) do
|
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
|
end
|
||||||
imap.logout
|
imap.logout
|
||||||
ensure
|
ensure
|
||||||
@ -628,7 +628,7 @@ class IMAPTest < Test::Unit::TestCase
|
|||||||
end
|
end
|
||||||
begin
|
begin
|
||||||
imap = Net::IMAP.new(server_addr, :port => port)
|
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(2, requests.length)
|
||||||
assert_equal("RUBY0001 TEST ({4}\r\n", requests[0])
|
assert_equal("RUBY0001 TEST ({4}\r\n", requests[0])
|
||||||
assert_equal("\xDE\xAD\xBE\xEF".b, literal)
|
assert_equal("\xDE\xAD\xBE\xEF".b, literal)
|
||||||
@ -753,6 +753,55 @@ EOF
|
|||||||
end
|
end
|
||||||
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
|
private
|
||||||
|
|
||||||
def imaps_test
|
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")
|
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("CAPABILITY", response.name)
|
||||||
assert_equal("AUTH=PLAIN", response.data.last)
|
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
|
end
|
||||||
|
|
||||||
def test_mixed_boundary
|
def test_mixed_boundary
|
||||||
@ -301,6 +322,22 @@ EOF
|
|||||||
assert_equal(12345, response.data.attr["MODSEQ"])
|
assert_equal(12345, response.data.attr["MODSEQ"])
|
||||||
end
|
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
|
def test_continuation_request_without_response_text
|
||||||
parser = Net::IMAP::ResponseParser.new
|
parser = Net::IMAP::ResponseParser.new
|
||||||
response = parser.parse("+\r\n")
|
response = parser.parse("+\r\n")
|
||||||
@ -308,4 +345,45 @@ EOF
|
|||||||
assert_equal(nil, response.data.code)
|
assert_equal(nil, response.data.code)
|
||||||
assert_equal("", response.data.text)
|
assert_equal("", response.data.text)
|
||||||
end
|
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
|
end
|
||||||
|
Loading…
x
Reference in New Issue
Block a user