summaryrefslogtreecommitdiff
path: root/lib/net/imap.rb
diff options
context:
space:
mode:
authorshugo <shugo@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>2000-10-13 08:06:16 +0000
committershugo <shugo@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>2000-10-13 08:06:16 +0000
commit0f568bcf355ae7ffa7cfc95dd33b6fa424dd738b (patch)
tree7a57dfc28a6b46191066d4802434ead001371305 /lib/net/imap.rb
parent76819ef99ab0986f9d5b174b68fed919e4cd9585 (diff)
lib/net/imap.rb
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@1000 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Diffstat (limited to 'lib/net/imap.rb')
-rw-r--r--lib/net/imap.rb1087
1 files changed, 1087 insertions, 0 deletions
diff --git a/lib/net/imap.rb b/lib/net/imap.rb
new file mode 100644
index 0000000..3bc3750
--- /dev/null
+++ b/lib/net/imap.rb
@@ -0,0 +1,1087 @@
+=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