mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
1088 lines
24 KiB
Ruby
1088 lines
24 KiB
Ruby
|
=begin
|
||
|
|
||
|
= net/imap.rb
|
||
|
|
||
|
Copyright (C) 2000 Shugo Maeda <shugo@ruby-lang.org>
|
||
|
|
||
|
This library is distributed under the terms of the Ruby license.
|
||
|
You can freely distribute/modify this library.
|
||
|
|
||
|
== class Net::IMAP
|
||
|
|
||
|
Net::IMAP implements Internet Message Access Protocol (IMAP) clients.
|
||
|
|
||
|
=== Super Class
|
||
|
|
||
|
Object
|
||
|
|
||
|
=== Class Methods
|
||
|
|
||
|
: new(host, port = 143)
|
||
|
Creates a new Net::IMAP object and connects it to the specified
|
||
|
port on the named host.
|
||
|
|
||
|
: debug
|
||
|
Returns the debug mode
|
||
|
|
||
|
: debug = val
|
||
|
Sets the debug mode
|
||
|
|
||
|
=== Methods
|
||
|
|
||
|
: greeting
|
||
|
Returns an initial greeting response from the server.
|
||
|
|
||
|
: responses
|
||
|
Returns recorded untagged responses.
|
||
|
|
||
|
ex).
|
||
|
imap.select("inbox")
|
||
|
p imap.responses["EXISTS"][-1]
|
||
|
#=> [2]
|
||
|
p imap.responses["UIDVALIDITY"][-1]
|
||
|
#=> [968263756]
|
||
|
|
||
|
: disconnect
|
||
|
Disconnects from the server.
|
||
|
|
||
|
: capability
|
||
|
Sends a CAPABILITY command, and returns a listing of
|
||
|
capabilities that the server supports.
|
||
|
|
||
|
: noop
|
||
|
Sends a NOOP command to the server. It does nothing.
|
||
|
|
||
|
: logout
|
||
|
Sends a LOGOUT command to inform the server that the client is
|
||
|
done with the connection.
|
||
|
|
||
|
: authenticate(auth_type, arg...)
|
||
|
Sends an AUTEHNTICATE command to authenticate the client.
|
||
|
The auth_type parameter is a string that represents
|
||
|
the authentication mechanism to be used. Currently Net::IMAP
|
||
|
supports "LOGIN" and "CRAM-MD5" for the auth_type.
|
||
|
|
||
|
ex).
|
||
|
imap.authenticate('LOGIN', user, password)
|
||
|
|
||
|
: login(user, password)
|
||
|
Sends a LOGIN command to identify the client and carries
|
||
|
the plaintext password authenticating this user.
|
||
|
|
||
|
: select(mailbox)
|
||
|
Sends a SELECT command to select a mailbox so that messages
|
||
|
in the mailbox can be accessed.
|
||
|
|
||
|
: examine(mailbox)
|
||
|
Sends a EXAMINE command to select a mailbox so that messages
|
||
|
in the mailbox can be accessed. However, the selected mailbox
|
||
|
is identified as read-only.
|
||
|
|
||
|
: create(mailbox)
|
||
|
Sends a CREATE command to create a new mailbox.
|
||
|
|
||
|
: delete(mailbox)
|
||
|
Sends a DELETE command to remove the mailbox.
|
||
|
|
||
|
: rename(mailbox, newname)
|
||
|
Sends a RENAME command to change the name of the mailbox to
|
||
|
the newname.
|
||
|
|
||
|
: subscribe(mailbox)
|
||
|
Sends a SUBSCRIBE command to add the specified mailbox name to
|
||
|
the server's set of "active" or "subscribed" mailboxes.
|
||
|
|
||
|
: unsubscribe(mailbox)
|
||
|
Sends a UNSUBSCRIBE command to remove the specified mailbox name
|
||
|
from the server's set of "active" or "subscribed" mailboxes.
|
||
|
|
||
|
: list(refname, mailbox)
|
||
|
Sends a LIST command, and returns a subset of names from
|
||
|
the complete set of all names available to the client.
|
||
|
|
||
|
ex).
|
||
|
imap.create("foo/bar")
|
||
|
imap.create("foo/baz")
|
||
|
p imap.list("", "foo/%")
|
||
|
#=> [[[:NoSelect], "/", "foo/"], [[:NoInferiors], "/", "foo/baz"], [[:NoInferiors], "/", "foo/bar"]]
|
||
|
|
||
|
: lsub(refname, mailbox)
|
||
|
Sends a LSUB command, and returns a subset of names from the set
|
||
|
of names that the user has declared as being "active" or
|
||
|
"subscribed".
|
||
|
|
||
|
: status(mailbox, attr)
|
||
|
Sends a STATUS command, and returns the status of the indicated
|
||
|
mailbox.
|
||
|
|
||
|
ex).
|
||
|
p imap.status("inbox", ["MESSAGES", "RECENT"])
|
||
|
#=> {"RECENT"=>0, "MESSAGES"=>5}
|
||
|
|
||
|
: append(mailbox, message, flags = nil, date_time = nil)
|
||
|
Sends a APPEND command to append the message to the end of
|
||
|
the mailbox.
|
||
|
|
||
|
ex).
|
||
|
imap.append("inbox", <<EOF.gsub(/\n/, "\r\n"), [:Seen], Time.now)
|
||
|
Subject: hello
|
||
|
From: shugo@ruby-lang.org
|
||
|
To: shugo@ruby-lang.org
|
||
|
|
||
|
hello world
|
||
|
EOF
|
||
|
|
||
|
: check
|
||
|
Sends a CHECK command to request a checkpoint of the currently
|
||
|
selected mailbox.
|
||
|
|
||
|
: close
|
||
|
Sends a CLOSE command to close the currently selected mailbox.
|
||
|
The CLOSE command permanently removes from the mailbox all
|
||
|
messages that have the \Deleted flag set.
|
||
|
|
||
|
: expunge
|
||
|
Sends a EXPUNGE command to permanently remove from the currently
|
||
|
selected mailbox all messages that have the \Deleted flag set.
|
||
|
|
||
|
: search(keys, charset = nil)
|
||
|
: uid_search(keys, charset = nil)
|
||
|
Sends a SEARCH command to search the mailbox for messages that
|
||
|
match the given searching criteria, and returns message sequence
|
||
|
numbers (search) or unique identifiers (uid_search).
|
||
|
|
||
|
ex).
|
||
|
p imap.search(["SUBJECT", "hello"])
|
||
|
#=> [1, 6, 7, 8]
|
||
|
|
||
|
: fetch(set, attr)
|
||
|
: uid_fetch(set, attr)
|
||
|
Sends a FETCH command to retrieve data associated with a message
|
||
|
in the mailbox. the set parameter is a number or an array of
|
||
|
numbers or a Range object. the number is a message sequence
|
||
|
number (fetch) or a unique identifier (uid_fetch).
|
||
|
|
||
|
ex).
|
||
|
p imap.fetch(6..-1, "UID")
|
||
|
#=> [[6, {"UID"=>28}], [7, {"UID"=>29}], [8, {"UID"=>30}]]
|
||
|
|
||
|
: store(set, attr, flags)
|
||
|
: uid_store(set, attr, flags)
|
||
|
Sends a STORE command to alter data associated with a message
|
||
|
in the mailbox. the set parameter is a number or an array of
|
||
|
numbers or a Range object. the number is a message sequence
|
||
|
number (store) or a unique identifier (uid_store).
|
||
|
|
||
|
ex).
|
||
|
p imap.store(6..-1, "+FLAGS", [:Deleted])
|
||
|
#=> [[6, {"FLAGS"=>[:Deleted]}], [7, {"FLAGS"=>[:Seen, :Deleted]}], [8, {"FLAGS"=>[:Seen, :Deleted]}]]
|
||
|
|
||
|
: copy(set, mailbox)
|
||
|
: uid_copy(set, mailbox)
|
||
|
Sends a COPY command to copy the specified message(s) to the end
|
||
|
of the specified destination mailbox. the set parameter is
|
||
|
a number or an array of numbers or a Range object. the number is
|
||
|
a message sequence number (copy) or a unique identifier (uid_copy).
|
||
|
|
||
|
: sort(sort_keys, search_keys, charset)
|
||
|
: uid_sort(sort_keys, search_keys, charset)
|
||
|
Sends a SORT command to sort messages in the mailbox.
|
||
|
|
||
|
ex).
|
||
|
p imap.sort(["FROM"], ["ALL"], "US-ASCII")
|
||
|
#=> [1, 2, 3, 5, 6, 7, 8, 4, 9]
|
||
|
p imap.sort(["DATE"], ["SUBJECT", "hello"], "US-ASCII")
|
||
|
#=> [6, 7, 8, 1]
|
||
|
|
||
|
=end
|
||
|
|
||
|
require "socket"
|
||
|
require "md5"
|
||
|
|
||
|
module Net
|
||
|
class IMAP
|
||
|
attr_reader :greeting, :responses
|
||
|
|
||
|
def self.debug
|
||
|
return @@debug
|
||
|
end
|
||
|
|
||
|
def self.debug=(val)
|
||
|
return @@debug = val
|
||
|
end
|
||
|
|
||
|
def disconnect
|
||
|
@sock.close
|
||
|
end
|
||
|
|
||
|
def capability
|
||
|
send_command("CAPABILITY")
|
||
|
return @responses.delete("CAPABILITY")[-1]
|
||
|
end
|
||
|
|
||
|
def noop
|
||
|
send_command("NOOP")
|
||
|
end
|
||
|
|
||
|
def logout
|
||
|
send_command("LOGOUT")
|
||
|
end
|
||
|
|
||
|
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)
|
||
|
send_command("AUTHENTICATE", auth_type) do |resp|
|
||
|
if resp.prefix == "+"
|
||
|
data = authenticator.process(resp[0].unpack("m")[0])
|
||
|
send_data([data].pack("m").chomp)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def login(user, password)
|
||
|
send_command("LOGIN", user, password)
|
||
|
end
|
||
|
|
||
|
def select(mailbox)
|
||
|
@responses.clear
|
||
|
send_command("SELECT", mailbox)
|
||
|
end
|
||
|
|
||
|
def examine(mailbox)
|
||
|
@responses.clear
|
||
|
send_command("EXAMINE", mailbox)
|
||
|
end
|
||
|
|
||
|
def create(mailbox)
|
||
|
send_command("CREATE", mailbox)
|
||
|
end
|
||
|
|
||
|
def delete(mailbox)
|
||
|
send_command("DELETE", mailbox)
|
||
|
end
|
||
|
|
||
|
def rename(mailbox, newname)
|
||
|
send_command("RENAME", mailbox, newname)
|
||
|
end
|
||
|
|
||
|
def subscribe(mailbox)
|
||
|
send_command("SUBSCRIBE", mailbox)
|
||
|
end
|
||
|
|
||
|
def unsubscribe(mailbox)
|
||
|
send_command("UNSUBSCRIBE", mailbox)
|
||
|
end
|
||
|
|
||
|
def list(refname, mailbox)
|
||
|
send_command("LIST", refname, mailbox)
|
||
|
return @responses.delete("LIST")
|
||
|
end
|
||
|
|
||
|
def lsub(refname, mailbox)
|
||
|
send_command("LSUB", refname, mailbox)
|
||
|
return @responses.delete("LSUB")
|
||
|
end
|
||
|
|
||
|
def status(mailbox, attr)
|
||
|
send_command("STATUS", mailbox, attr)
|
||
|
status_list = @responses.delete("STATUS")[-1][1]
|
||
|
return Hash[*status_list]
|
||
|
end
|
||
|
|
||
|
def append(mailbox, message, flags = nil, date_time = nil)
|
||
|
args = []
|
||
|
if flags
|
||
|
flags.collect! {|i| Flag.new(i)}
|
||
|
args.push(flags)
|
||
|
end
|
||
|
args.push(date_time) if date_time
|
||
|
args.push(Literal.new(message))
|
||
|
send_command("APPEND", mailbox, *args)
|
||
|
end
|
||
|
|
||
|
def check
|
||
|
send_command("CHECK")
|
||
|
end
|
||
|
|
||
|
def close
|
||
|
send_command("CLOSE")
|
||
|
end
|
||
|
|
||
|
def expunge
|
||
|
send_command("EXPUNGE")
|
||
|
return @responses.delete("EXPUNGE").collect {|i| i[0]}
|
||
|
end
|
||
|
|
||
|
def search(keys, charset = nil)
|
||
|
return search_internal("SEARCH", keys, charset)
|
||
|
end
|
||
|
|
||
|
def uid_search(keys, charset = nil)
|
||
|
return search_internal("UID SEARCH", keys, charset)
|
||
|
end
|
||
|
|
||
|
def fetch(set, attr)
|
||
|
return fetch_internal("FETCH", set, attr)
|
||
|
end
|
||
|
|
||
|
def uid_fetch(set, attr)
|
||
|
return fetch_internal("UID FETCH", set, attr)
|
||
|
end
|
||
|
|
||
|
def store(set, attr, flags)
|
||
|
return store_internal("STORE", set, attr, flags)
|
||
|
end
|
||
|
|
||
|
def uid_store(set, attr, flags)
|
||
|
return store_internal("UID STORE", set, attr, flags)
|
||
|
end
|
||
|
|
||
|
def copy(set, mailbox)
|
||
|
copy_internal("COPY", set, mailbox)
|
||
|
end
|
||
|
|
||
|
def uid_copy(set, mailbox)
|
||
|
copy_internal("UID COPY", set, mailbox)
|
||
|
end
|
||
|
|
||
|
def sort(sort_keys, search_keys, charset)
|
||
|
return sort_internal("SORT", sort_keys, search_keys, charset)
|
||
|
end
|
||
|
|
||
|
def uid_sort(sort_keys, search_keys, charset)
|
||
|
return sort_internal("UID SORT", sort_keys, search_keys, charset)
|
||
|
end
|
||
|
|
||
|
private
|
||
|
|
||
|
CRLF = "\r\n"
|
||
|
PORT = 143
|
||
|
|
||
|
@@debug = false
|
||
|
|
||
|
def initialize(host, port = PORT)
|
||
|
@host = host
|
||
|
@port = port
|
||
|
@tag_prefix = "RUBY"
|
||
|
@tagno = 0
|
||
|
@parser = ResponseParser.new
|
||
|
@sock = TCPSocket.open(host, port)
|
||
|
@responses = Hash.new([].freeze)
|
||
|
@greeting = get_response
|
||
|
if @greeting.name == "BYE"
|
||
|
@sock.close
|
||
|
raise ByeResponseError, resp[0]
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def send_command(cmd, *args, &block)
|
||
|
tag = generate_tag
|
||
|
data = args.collect {|i| format_data(i)}.join(" ")
|
||
|
if data.length > 0
|
||
|
put_line(tag + " " + cmd + " " + data)
|
||
|
else
|
||
|
put_line(tag + " " + cmd)
|
||
|
end
|
||
|
return get_all_responses(tag, cmd, &block)
|
||
|
end
|
||
|
|
||
|
def generate_tag
|
||
|
@tagno += 1
|
||
|
return format("%s%04d", @tag_prefix, @tagno)
|
||
|
end
|
||
|
|
||
|
def send_data(*args)
|
||
|
data = args.collect {|i| format_data(i)}.join(" ")
|
||
|
put_line(data)
|
||
|
end
|
||
|
|
||
|
def put_line(line)
|
||
|
line = line + CRLF
|
||
|
@sock.print(line)
|
||
|
if @@debug
|
||
|
$stderr.print(line.gsub(/^/, "C: "))
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def get_all_responses(tag, cmd, &block)
|
||
|
while resp = get_response
|
||
|
if @@debug
|
||
|
$stderr.puts(resp.inspect)
|
||
|
end
|
||
|
if resp.prefix == tag
|
||
|
case resp.name
|
||
|
when "NO"
|
||
|
raise NoResponseError, resp[0]
|
||
|
when "BAD"
|
||
|
raise BadResponseError, resp[0]
|
||
|
else
|
||
|
return resp
|
||
|
end
|
||
|
else
|
||
|
if resp.prefix == "*"
|
||
|
if resp.name == "BYE" &&
|
||
|
cmd != "LOGOUT"
|
||
|
raise ByeResponseError, resp[0]
|
||
|
end
|
||
|
record_response(resp.name, resp.data)
|
||
|
if /\A(OK|NO|BAD)\z/ =~ resp.name &&
|
||
|
resp[0].instance_of?(Array)
|
||
|
record_response(resp[0][0], resp[0][1..-1])
|
||
|
end
|
||
|
end
|
||
|
block.call(resp) if block
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def get_response
|
||
|
buff = ""
|
||
|
while true
|
||
|
s = @sock.gets(CRLF)
|
||
|
break unless s
|
||
|
buff.concat(s)
|
||
|
if /\{(\d+)\}\r\n/ =~ s
|
||
|
s = @sock.read($1.to_i)
|
||
|
buff.concat(s)
|
||
|
else
|
||
|
break
|
||
|
end
|
||
|
end
|
||
|
return nil if buff.length == 0
|
||
|
if @@debug
|
||
|
$stderr.print(buff.gsub(/^/, "S: "))
|
||
|
end
|
||
|
return @parser.parse(buff)
|
||
|
end
|
||
|
|
||
|
def record_response(name, data)
|
||
|
unless @responses.has_key?(name)
|
||
|
@responses[name] = []
|
||
|
end
|
||
|
@responses[name].push(data)
|
||
|
end
|
||
|
|
||
|
def format_data(data)
|
||
|
case data
|
||
|
when nil
|
||
|
return "NIL"
|
||
|
when String
|
||
|
return format_string(data)
|
||
|
when Integer
|
||
|
return format_number(data)
|
||
|
when Array
|
||
|
return format_list(data)
|
||
|
when Time
|
||
|
return format_time(data)
|
||
|
when Symbol
|
||
|
return format_symbol(data)
|
||
|
else
|
||
|
return data.format_data
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def format_string(str)
|
||
|
case str
|
||
|
when ""
|
||
|
return '""'
|
||
|
when /[\r\n]/
|
||
|
# literal
|
||
|
return "{" + str.length.to_s + "}" + CRLF + str
|
||
|
when /[(){ \x00-\x1f\x7f%*"\\]/
|
||
|
# quoted string
|
||
|
return '"' + str.gsub(/["\\]/, "\\\\\\&") + '"'
|
||
|
else
|
||
|
# atom
|
||
|
return str
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def format_number(num)
|
||
|
if num < 0 || num >= 4294967296
|
||
|
raise DataFormatError, num.to_s
|
||
|
end
|
||
|
return num.to_s
|
||
|
end
|
||
|
|
||
|
def format_list(list)
|
||
|
contents = list.collect {|i| format_data(i)}.join(" ")
|
||
|
return "(" + contents + ")"
|
||
|
end
|
||
|
|
||
|
DATE_MONTH = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)
|
||
|
|
||
|
def format_time(time)
|
||
|
t = time.dup.gmtime
|
||
|
return format('"%2d-%3s-%4d %02d:%02d:%02d +0000"',
|
||
|
t.day, DATE_MONTH[t.month - 1], t.year,
|
||
|
t.hour, t.min, t.sec)
|
||
|
end
|
||
|
|
||
|
def format_symbol(symbol)
|
||
|
return "\\" + symbol.to_s
|
||
|
end
|
||
|
|
||
|
def search_internal(cmd, keys, charset)
|
||
|
normalize_searching_criteria(keys)
|
||
|
if charset
|
||
|
send_command(cmd, "CHARSET", charset, *keys)
|
||
|
else
|
||
|
send_command(cmd, *keys)
|
||
|
end
|
||
|
return @responses.delete("SEARCH")[-1]
|
||
|
end
|
||
|
|
||
|
def fetch_internal(cmd, set, attr)
|
||
|
send_command(cmd, MessageSet.new(set), attr)
|
||
|
return get_fetch_response
|
||
|
end
|
||
|
|
||
|
def store_internal(cmd, set, attr, flags)
|
||
|
send_command(cmd, MessageSet.new(set), attr, flags)
|
||
|
return get_fetch_response
|
||
|
end
|
||
|
|
||
|
def copy_internal(cmd, set, mailbox)
|
||
|
send_command(cmd, MessageSet.new(set), mailbox)
|
||
|
end
|
||
|
|
||
|
def sort_internal(cmd, sort_keys, search_keys, charset)
|
||
|
normalize_searching_criteria(search_keys)
|
||
|
send_command(cmd, sort_keys, charset, *search_keys)
|
||
|
return @responses.delete("SORT")[-1]
|
||
|
end
|
||
|
|
||
|
def normalize_searching_criteria(keys)
|
||
|
keys.collect! do |i|
|
||
|
case i
|
||
|
when -1, Range, Array
|
||
|
MessageSet.new(i)
|
||
|
else
|
||
|
i
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def get_fetch_response
|
||
|
return @responses.delete("FETCH").collect { |i|
|
||
|
i[1] = Hash[*i[1]]
|
||
|
i
|
||
|
}
|
||
|
end
|
||
|
|
||
|
class Atom
|
||
|
def format_data
|
||
|
return @data
|
||
|
end
|
||
|
|
||
|
private
|
||
|
|
||
|
def initialize(data)
|
||
|
@data = data
|
||
|
end
|
||
|
end
|
||
|
|
||
|
class QuotedString
|
||
|
def format_data
|
||
|
return '"' + @data.gsub(/["\\]/, "\\\\\\&") + '"'
|
||
|
end
|
||
|
|
||
|
private
|
||
|
|
||
|
def initialize(data)
|
||
|
@data = data
|
||
|
end
|
||
|
end
|
||
|
|
||
|
class Literal
|
||
|
def format_data
|
||
|
return "{" + @data.length.to_s + "}" + CRLF + @data
|
||
|
end
|
||
|
|
||
|
private
|
||
|
|
||
|
def initialize(data)
|
||
|
@data = data
|
||
|
end
|
||
|
end
|
||
|
|
||
|
class MessageSet
|
||
|
def format_data
|
||
|
return format_internal(@data)
|
||
|
end
|
||
|
|
||
|
private
|
||
|
|
||
|
def initialize(data)
|
||
|
@data = data
|
||
|
end
|
||
|
|
||
|
def format_internal(data)
|
||
|
case data
|
||
|
when "*"
|
||
|
return data
|
||
|
when Integer
|
||
|
ensure_nz_number(data)
|
||
|
if data == -1
|
||
|
return "*"
|
||
|
else
|
||
|
return data.to_s
|
||
|
end
|
||
|
when Range
|
||
|
return format_internal(data.first) +
|
||
|
":" + format_internal(data.last)
|
||
|
when Array
|
||
|
return data.collect {|i| format_internal(i)}.join(",")
|
||
|
else
|
||
|
raise DataFormatError, data.inspect
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def ensure_nz_number(num)
|
||
|
if num < -1 || num == 0 || num >= 4294967296
|
||
|
raise DataFormatError, num.inspect
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
class Response
|
||
|
attr_reader :prefix, :name, :data, :raw_data
|
||
|
|
||
|
def inspect
|
||
|
s = @data.collect{|i| i.inspect}.join(" ")
|
||
|
if @name
|
||
|
return "#<Response: " + @prefix + " " + @name + " " + s + ">"
|
||
|
else
|
||
|
return "#<Response: " + @prefix + " " + s + ">"
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def method_missing(mid, *args)
|
||
|
return @data.send(mid, *args)
|
||
|
end
|
||
|
|
||
|
private
|
||
|
|
||
|
def initialize(prefix, data, raw_data)
|
||
|
@prefix = prefix
|
||
|
if prefix == "+"
|
||
|
@name = nil
|
||
|
else
|
||
|
data.each_with_index do |item, i|
|
||
|
if item.instance_of?(String)
|
||
|
@name = item
|
||
|
data.delete_at(i)
|
||
|
break
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
@data = data
|
||
|
@raw_data = raw_data
|
||
|
end
|
||
|
end
|
||
|
|
||
|
class ResponseParser
|
||
|
def parse(str)
|
||
|
@str = str
|
||
|
@pos = 0
|
||
|
@lex_state = EXPR_DATA
|
||
|
@token.symbol = nil
|
||
|
return parse_response
|
||
|
end
|
||
|
|
||
|
private
|
||
|
|
||
|
EXPR_DATA = :DATA
|
||
|
EXPR_TEXT = :TEXT
|
||
|
EXPR_CODE = :CODE
|
||
|
EXPR_CODE_TEXT = :CODE_TEXT
|
||
|
|
||
|
T_NIL = :NIL
|
||
|
T_NUMBER = :NUMBER
|
||
|
T_ATOM = :ATOM
|
||
|
T_QUOTED = :QUOTED
|
||
|
T_LITERAL = :LITERAL
|
||
|
T_FLAG = :FLAG
|
||
|
T_LPAREN = :LPAREN
|
||
|
T_RPAREN = :RPAREN
|
||
|
T_STAR = :STAR
|
||
|
T_CRLF = :CRLF
|
||
|
T_EOF = :EOF
|
||
|
T_LBRA = :LBRA
|
||
|
T_RBRA = :RBRA
|
||
|
T_TEXT = :TEXT
|
||
|
|
||
|
DATA_REGEXP = /\G *(?:\
|
||
|
(?# 1: NIL )(NIL)|\
|
||
|
(?# 2: NUMBER )(\d+)|\
|
||
|
(?# 3: ATOM )([^(){ \x00-\x1f\x7f%*"\\]+)|\
|
||
|
(?# 4: QUOTED )"((?:[^"\\]|\\["\\])*)"|\
|
||
|
(?# 5: LITERAL )\{(\d+)\}\r\n|\
|
||
|
(?# 6: FLAG )(\\(?:[^(){ \x00-\x1f\x7f%*"\\]+|\*))|\
|
||
|
(?# 7: LPAREN )(\()|\
|
||
|
(?# 8: RPAREN )(\))|\
|
||
|
(?# 9: STAR )(\*)|\
|
||
|
(?# 10: CRLF )(\r\n)|\
|
||
|
(?# 11: EOF )(\z))/i
|
||
|
|
||
|
CODE_REGEXP = /\G *(?:\
|
||
|
(?# 1: NUMBER )(\d+)|\
|
||
|
(?# 2: ATOM )([^(){ \x00-\x1f\x7f%*"\\\[\]]+)|\
|
||
|
(?# 3: FLAG )(\\(?:[^(){ \x00-\x1f\x7f%*"\\]+|\*))|\
|
||
|
(?# 4: LPAREN )(\()|\
|
||
|
(?# 5: RPAREN )(\))|\
|
||
|
(?# 6: LBRA )(\[)|\
|
||
|
(?# 7: RBRA )(\]))/i
|
||
|
|
||
|
CODE_TEXT_REGEXP = /\G *(?:\
|
||
|
(?# 1: TEXT )([^\r\n\]]*))/i
|
||
|
|
||
|
TEXT_REGEXP = /\G *(?:\
|
||
|
(?# 1: LBRA )(\[)|\
|
||
|
(?# 2: TEXT )([^\r\n]*))/i
|
||
|
|
||
|
Token = Struct.new("Token", :symbol, :value)
|
||
|
|
||
|
def initialize
|
||
|
@token = Token.new(nil, nil)
|
||
|
end
|
||
|
|
||
|
def parse_response
|
||
|
prefix = parse_prefix
|
||
|
case prefix
|
||
|
when "+"
|
||
|
data = parse_resp_text
|
||
|
when "*"
|
||
|
data = parse_response_data
|
||
|
else
|
||
|
data = parse_response_cond
|
||
|
end
|
||
|
match_token(T_CRLF)
|
||
|
match_token(T_EOF)
|
||
|
return Response.new(prefix, data, @str)
|
||
|
end
|
||
|
|
||
|
def parse_prefix
|
||
|
token = match_token(T_STAR, T_ATOM)
|
||
|
return token.value
|
||
|
end
|
||
|
|
||
|
def parse_resp_text
|
||
|
val = []
|
||
|
@lex_state = EXPR_TEXT
|
||
|
token = get_token
|
||
|
if token.symbol == T_LBRA
|
||
|
val.push(parse_resp_text_code)
|
||
|
end
|
||
|
val.push(parse_text)
|
||
|
@lex_state = EXPR_DATA
|
||
|
return val
|
||
|
end
|
||
|
|
||
|
def parse_resp_text_code
|
||
|
val = []
|
||
|
@lex_state = EXPR_CODE
|
||
|
match_token(T_LBRA)
|
||
|
token = match_token(T_ATOM)
|
||
|
val.push(token.value)
|
||
|
case token.value
|
||
|
when /\A(ALERT|PARSE|READ-ONLY|READ-WRITE|TRYCREATE)\z/
|
||
|
# do nothing
|
||
|
when /\A(PERMANENTFLAGS)\z/
|
||
|
token = get_token
|
||
|
if token.symbol != T_LPAREN
|
||
|
parse_error('unexpected token %s (expected "(")',
|
||
|
token.symbol.id2name)
|
||
|
end
|
||
|
val.push(parse_parenthesized_list)
|
||
|
when /\A(UIDVALIDITY|UIDNEXT|UNSEEN)\z/
|
||
|
token = match_token(T_NUMBER)
|
||
|
val.push(token.value)
|
||
|
else
|
||
|
@lex_state = EXPR_CODE_TEXT
|
||
|
val.push(parse_text)
|
||
|
@lex_state = EXPR_CODE
|
||
|
end
|
||
|
match_token(T_RBRA)
|
||
|
@lex_state = EXPR_TEXT
|
||
|
return val
|
||
|
end
|
||
|
|
||
|
def parse_text
|
||
|
token = match_token(T_TEXT)
|
||
|
return token.value
|
||
|
end
|
||
|
|
||
|
def parse_response_data
|
||
|
token = get_token
|
||
|
if token.symbol == T_ATOM &&
|
||
|
/\A(OK|NO|BAD|PREAUTH|BYE)\z/ =~ token.value
|
||
|
return parse_response_cond
|
||
|
else
|
||
|
return parse_data_list
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def parse_response_cond
|
||
|
val = []
|
||
|
token = match_token(T_ATOM)
|
||
|
val.push(token.value)
|
||
|
val += parse_resp_text
|
||
|
return val
|
||
|
end
|
||
|
|
||
|
def parse_data_list
|
||
|
val = []
|
||
|
while true
|
||
|
token = get_token
|
||
|
case token.symbol
|
||
|
when T_EOF
|
||
|
parse_error('unexpected token %s', token.symbol.id2name)
|
||
|
when T_CRLF, T_RPAREN
|
||
|
return val
|
||
|
when T_LPAREN
|
||
|
val.push(parse_parenthesized_list)
|
||
|
else
|
||
|
val.push(token.value)
|
||
|
@token.symbol = nil
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def parse_parenthesized_list
|
||
|
match_token(T_LPAREN)
|
||
|
val = parse_data_list
|
||
|
match_token(T_RPAREN)
|
||
|
return val
|
||
|
end
|
||
|
|
||
|
def match_token(*args)
|
||
|
token = get_token
|
||
|
unless args.include?(token.symbol)
|
||
|
parse_error('unexpected token %s (expected %s)',
|
||
|
token.symbol.id2name,
|
||
|
args.collect {|i| i.id2name}.join(" or "))
|
||
|
end
|
||
|
@token.symbol = nil
|
||
|
return token
|
||
|
end
|
||
|
|
||
|
def get_token
|
||
|
unless @token.symbol
|
||
|
next_token
|
||
|
end
|
||
|
return @token
|
||
|
end
|
||
|
|
||
|
def next_token
|
||
|
case @lex_state
|
||
|
when EXPR_DATA
|
||
|
if @str.index(DATA_REGEXP, @pos)
|
||
|
@pos = $~.end(0)
|
||
|
if $1
|
||
|
@token.value = nil
|
||
|
@token.symbol = T_NIL
|
||
|
elsif $2
|
||
|
@token.value = $+.to_i
|
||
|
@token.symbol = T_NUMBER
|
||
|
elsif $3
|
||
|
@token.value = $+.upcase
|
||
|
@token.symbol = T_ATOM
|
||
|
elsif $4
|
||
|
@token.value = $+.gsub(/\\(["\\])/, "\\1")
|
||
|
@token.symbol = T_QUOTED
|
||
|
elsif $5
|
||
|
len = $+.to_i
|
||
|
@token.value = @str[@pos, len]
|
||
|
@pos += len
|
||
|
@token.symbol = T_LITERAL
|
||
|
elsif $6
|
||
|
@token.value = $+[1..-1].capitalize.intern
|
||
|
@token.symbol = T_FLAG
|
||
|
elsif $7
|
||
|
@token.value = nil
|
||
|
@token.symbol = T_LPAREN
|
||
|
elsif $8
|
||
|
@token.value = nil
|
||
|
@token.symbol = T_RPAREN
|
||
|
elsif $9
|
||
|
@token.value = $+
|
||
|
@token.symbol = T_STAR
|
||
|
elsif $10
|
||
|
@token.value = nil
|
||
|
@token.symbol = T_CRLF
|
||
|
elsif $11
|
||
|
@token.value = nil
|
||
|
@token.symbol = T_EOF
|
||
|
else
|
||
|
parse_error("[BUG] DATA_REGEXP is invalid")
|
||
|
end
|
||
|
return
|
||
|
end
|
||
|
when EXPR_TEXT
|
||
|
if @str.index(TEXT_REGEXP, @pos)
|
||
|
@pos = $~.end(0)
|
||
|
if $1
|
||
|
@token.value = nil
|
||
|
@token.symbol = T_LBRA
|
||
|
elsif $2
|
||
|
@token.value = $+
|
||
|
@token.symbol = T_TEXT
|
||
|
else
|
||
|
parse_error("[BUG] TEXT_REGEXP is invalid")
|
||
|
end
|
||
|
return
|
||
|
end
|
||
|
when EXPR_CODE
|
||
|
if @str.index(CODE_REGEXP, @pos)
|
||
|
@pos = $~.end(0)
|
||
|
if $1
|
||
|
@token.value = $+.to_i
|
||
|
@token.symbol = T_NUMBER
|
||
|
elsif $2
|
||
|
@token.value = $+.upcase
|
||
|
@token.symbol = T_ATOM
|
||
|
elsif $3
|
||
|
@token.value = $+[1..-1].capitalize.intern
|
||
|
@token.symbol = T_FLAG
|
||
|
elsif $4
|
||
|
@token.value = nil
|
||
|
@token.symbol = T_LPAREN
|
||
|
elsif $5
|
||
|
@token.value = nil
|
||
|
@token.symbol = T_RPAREN
|
||
|
elsif $6
|
||
|
@token.value = nil
|
||
|
@token.symbol = T_LBRA
|
||
|
elsif $7
|
||
|
@token.value = nil
|
||
|
@token.symbol = T_RBRA
|
||
|
else
|
||
|
parse_error("[BUG] CODE_REGEXP is invalid")
|
||
|
end
|
||
|
return
|
||
|
end
|
||
|
when EXPR_CODE_TEXT
|
||
|
if @str.index(CODE_TEXT_REGEXP, @pos)
|
||
|
@pos = $~.end(0)
|
||
|
if $1
|
||
|
@token.value = $+
|
||
|
@token.symbol = T_TEXT
|
||
|
else
|
||
|
parse_error("[BUG] CODE_TEXT_REGEXP is invalid")
|
||
|
end
|
||
|
return
|
||
|
end
|
||
|
else
|
||
|
parse_error("illegal @lex_state - %s", @lex_state.inspect)
|
||
|
end
|
||
|
@str.index(/\S*/, @pos)
|
||
|
parse_error("unknown token - %s", $&.dump)
|
||
|
end
|
||
|
|
||
|
def parse_error(fmt, *args)
|
||
|
if @@debug
|
||
|
$stderr.printf("@str: %s\n", @str.dump)
|
||
|
$stderr.printf("@pos: %d\n", @pos)
|
||
|
if @token.symbol
|
||
|
$stderr.printf("@token.symbol: %s\n", @token.symbol.id2name)
|
||
|
$stderr.printf("@token.value: %s\n", @token.value.inspect)
|
||
|
end
|
||
|
end
|
||
|
raise ResponseParseError, format(fmt, *args)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
class LoginAuthenticator
|
||
|
def process(data)
|
||
|
case @state
|
||
|
when STATE_USER
|
||
|
@state = STATE_PASSWORD
|
||
|
return @user
|
||
|
when STATE_PASSWORD
|
||
|
return @password
|
||
|
end
|
||
|
end
|
||
|
|
||
|
private
|
||
|
|
||
|
STATE_USER = :USER
|
||
|
STATE_PASSWORD = :PASSWORD
|
||
|
|
||
|
def initialize(user, password)
|
||
|
@user = user
|
||
|
@password = password
|
||
|
@state = STATE_USER
|
||
|
end
|
||
|
end
|
||
|
|
||
|
class CramMD5Authenticator
|
||
|
def process(challenge)
|
||
|
digest = hmac_md5(challenge, @password)
|
||
|
return @user + " " + digest
|
||
|
end
|
||
|
|
||
|
private
|
||
|
|
||
|
def initialize(user, password)
|
||
|
@user = user
|
||
|
@password = password
|
||
|
end
|
||
|
|
||
|
def hmac_md5(text, key)
|
||
|
if key.length > 64
|
||
|
md5 = MD5.new(key)
|
||
|
key = md5.digest
|
||
|
end
|
||
|
|
||
|
k_ipad = key + "\0" * (64 - key.length)
|
||
|
k_opad = key + "\0" * (64 - key.length)
|
||
|
for i in 0..63
|
||
|
k_ipad[i] ^= 0x36
|
||
|
k_opad[i] ^= 0x5c
|
||
|
end
|
||
|
|
||
|
md5 = MD5.new
|
||
|
md5.update(k_ipad)
|
||
|
md5.update(text)
|
||
|
digest = md5.digest
|
||
|
|
||
|
md5 = MD5.new
|
||
|
md5.update(k_opad)
|
||
|
md5.update(digest)
|
||
|
return md5.hexdigest
|
||
|
end
|
||
|
end
|
||
|
|
||
|
AUTHENTICATORS = {
|
||
|
"LOGIN" => LoginAuthenticator,
|
||
|
"CRAM-MD5" => CramMD5Authenticator
|
||
|
}
|
||
|
|
||
|
class Error < StandardError
|
||
|
end
|
||
|
|
||
|
class DataFormatError < Error
|
||
|
end
|
||
|
|
||
|
class ResponseParseError < Error
|
||
|
end
|
||
|
|
||
|
class ResponseError < Error
|
||
|
end
|
||
|
|
||
|
class NoResponseError < ResponseError
|
||
|
end
|
||
|
|
||
|
class BadResponseError < ResponseError
|
||
|
end
|
||
|
|
||
|
class ByeResponseError < ResponseError
|
||
|
end
|
||
|
end
|
||
|
end
|