=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.

== Net::IMAP

Net::IMAP implements Internet Message Access Protocol (IMAP) clients.
(The protocol is described in ((<[IMAP]>)).)

Net::IMAP supports multiple commands. For example,

  imap = Net::IMAP.new("imap.foo.net", "imap2")
  imap.authenticate("cram-md5", "bar", "password")
  imap.select("inbox")
  fetch_thread = Thread.start { imap.fetch(1..-1, "UID") }
  search_result = imap.search(["BODY", "hello"])
  fetch_result = fetch_thread.value
  imap.disconnect

This script invokes the FETCH command and the SEARCH command concurrently.

=== Super Class

Object

=== Class Methods

: new(host, port = 143, usessl = false, certs = nil, verify = false)
      Creates a new Net::IMAP object and connects it to the specified
      port on the named host.  If usessl is true, then an attempt will
      be made to use SSL (now TLS) to connect to the server.  For this
      to work OpenSSL((<[OSSL]>)) and the Ruby OpenSSL((<[RSSL]>))
      extension need to be installed.  The certs parameter indicates
      the path or file containing the CA cert of the server, and the
      verify parameter is for the OpenSSL verification callback.

: debug
      Returns the debug mode.

: debug = val
      Sets the debug mode.

: add_authenticator(auth_type, authenticator)
      Adds an authenticator for Net::IMAP#authenticate.

=== 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.
      The return value is an array of ((<Net::IMAP::MailboxList>)).

      ex).
        imap.create("foo/bar")
        imap.create("foo/baz")
        p imap.list("", "foo/%")
        #=> [#<Net::IMAP::MailboxList attr=[:Noselect], delim="/", name="foo/">, #<Net::IMAP::MailboxList attr=[:Noinferiors, :Marked], delim="/", name="foo/bar">, #<Net::IMAP::MailboxList attr=[:Noinferiors], delim="/", name="foo/baz">]

: 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".
      The return value is an array of ((<Net::IMAP::MailboxList>)).

: status(mailbox, attr)
      Sends a STATUS command, and returns the status of the indicated
      mailbox.
      The return value is a hash of attributes.

      ex).
        p imap.status("inbox", ["MESSAGES", "RECENT"])
        #=> {"RECENT"=>0, "MESSAGES"=>44}

: 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]
        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).
      The return value is an array of ((<Net::IMAP::FetchData>)).

      ex).
        p imap.fetch(6..8, "UID")
        #=> [#<Net::IMAP::FetchData seqno=6, attr={"UID"=>98}>, #<Net::IMAP::FetchData seqno=7, attr={"UID"=>99}>, #<Net::IMAP::FetchData seqno=8, attr={"UID"=>100}>]
        p imap.fetch(6, "BODY[HEADER.FIELDS (SUBJECT)]")
        #=> [#<Net::IMAP::FetchData seqno=6, attr={"BODY[HEADER.FIELDS (SUBJECT)]"=>"Subject: test\r\n\r\n"}>]
        data = imap.uid_fetch(98, ["RFC822.SIZE", "INTERNALDATE"])[0]
        p data.seqno
        #=> 6
        p data.attr["RFC822.SIZE"]
        #=> 611
        p data.attr["INTERNALDATE"]
        #=> "12-Oct-2000 22:40:59 +0900"
        p data.attr["UID"]
        #=> 98

: 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).
      The return value is an array of ((<Net::IMAP::FetchData>)).

      ex).
        p imap.store(6..8, "+FLAGS", [:Deleted])
        #=> [#<Net::IMAP::FetchData seqno=6, attr={"FLAGS"=>[:Seen, :Deleted]}>, #<Net::IMAP::FetchData seqno=7, attr={"FLAGS"=>[:Seen, :Deleted]}>, #<Net::IMAP::FetchData seqno=8, attr={"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]

: setquota(mailbox, quota)
      Sends a SETQUOTA command along with the specified mailbox and
      quota.  If quota is nil, then quota will be unset for that
      mailbox.  Typically one needs to be logged in as server admin
      for this to work.  The IMAP quota commands are described in
      ((<[RFC-2087]>)).

: getquota(mailbox)
      Sends the GETQUOTA command along with specified mailbox.
      If this mailbox exists, then an array containing a
      ((<Net::IMAP::MailboxQuota>)) object is returned.  This
      command generally is only available to server admin.

: getquotaroot(mailbox)
      Sends the GETQUOTAROOT command along with specified mailbox.
      This command is generally available to both admin and user.
      If mailbox exists, returns an array containing objects of
      ((<Net::IMAP::MailboxQuotaRoot>)) and ((<Net::IMAP::MailboxQuota>)).

: setacl(mailbox, user, rights)
      Sends the SETACL command along with mailbox, user and the
      rights that user is to have on that mailbox.  If rights is nil,
      then that user will be stripped of any rights to that mailbox.
      The IMAP ACL commands are described in ((<[RFC-2086]>)).

: getacl(mailbox)
      Send the GETACL command along with specified mailbox.
      If this mailbox exists, an array containing objects of
      ((<Net::IMAP::MailboxACLItem>)) will be returned.

: add_response_handler(handler = Proc.new)
      Adds a response handler.

      ex).
        imap.add_response_handler do |resp|
          p resp
        end

: remove_response_handler(handler)
      Removes the response handler.

: response_handlers
      Returns all response handlers.

== Net::IMAP::ContinuationRequest

Net::IMAP::ContinuationRequest represents command continuation requests.

The command continuation request response is indicated by a "+" token
instead of a tag.  This form of response indicates that the server is
ready to accept the continuation of a command from the client.  The
remainder of this response is a line of text.

  continue_req    ::= "+" SPACE (resp_text / base64)

=== Super Class

Struct

=== Methods

: data
      Returns the data (Net::IMAP::ResponseText).

: raw_data
      Returns the raw data string.

== Net::IMAP::UntaggedResponse

Net::IMAP::UntaggedResponse represents untagged responses.

Data transmitted by the server to the client and status responses
that do not indicate command completion are prefixed with the token
"*", and are called untagged responses.

  response_data   ::= "*" SPACE (resp_cond_state / resp_cond_bye /
                      mailbox_data / message_data / capability_data)

=== Super Class

Struct

=== Methods

: name
      Returns the name such as "FLAGS", "LIST", "FETCH"....

: data
      Returns the data such as an array of flag symbols,
      a ((<Net::IMAP::MailboxList>)) object....

: raw_data
      Returns the raw data string.

== Net::IMAP::TaggedResponse

Net::IMAP::TaggedResponse represents tagged responses.

The server completion result response indicates the success or
failure of the operation.  It is tagged with the same tag as the
client command which began the operation.

  response_tagged ::= tag SPACE resp_cond_state CRLF
  
  tag             ::= 1*<any ATOM_CHAR except "+">
  
  resp_cond_state ::= ("OK" / "NO" / "BAD") SPACE resp_text

=== Super Class

Struct

=== Methods

: tag
      Returns the tag.

: name
      Returns the name. the name is one of "OK", "NO", "BAD".

: data
      Returns the data. See ((<Net::IMAP::ResponseText>)).

: raw_data
      Returns the raw data string.

== Net::IMAP::ResponseText

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 "="
  
=== Super Class

Struct

=== Methods

: code
      Returns the response code. See ((<Net::IMAP::ResponseCode>)).
      
: text
      Returns the text.

== Net::IMAP::ResponseCode

Net::IMAP::ResponseCode represents response codes.

  resp_text_code  ::= "ALERT" / "PARSE" /
                      "PERMANENTFLAGS" SPACE "(" #(flag / "\*") ")" /
                      "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
                      "UIDVALIDITY" SPACE nz_number /
                      "UNSEEN" SPACE nz_number /
                      atom [SPACE 1*<any TEXT_CHAR except "]">]

=== SuperClass

Struct

=== Methods

: name
      Returns the name such as "ALERT", "PERMANENTFLAGS", "UIDVALIDITY"....

: data
      Returns the data if it exists.

== Net::IMAP::MailboxList

Net::IMAP::MailboxList represents contents of the LIST response.

  mailbox_list    ::= "(" #("\Marked" / "\Noinferiors" /
                      "\Noselect" / "\Unmarked" / flag_extension) ")"
                      SPACE (<"> QUOTED_CHAR <"> / nil) SPACE mailbox

=== Super Class

Struct

=== Methods

: attr
      Returns the name attributes. Each name attribute is a symbol
      capitalized by String#capitalize, such as :Noselect (not :NoSelect).

: delim
      Returns the hierarchy delimiter

: name
      Returns the mailbox name.

== Net::IMAP::MailboxQuota

Net::IMAP::MailboxQuota represents contents of GETQUOTA response.
This object can also be a response to GETQUOTAROOT.  In the syntax
specification below, the delimiter used with the "#" construct is a
single space (SPACE).

   quota_list      ::= "(" #quota_resource ")"

   quota_resource  ::= atom SPACE number SPACE number

   quota_response  ::= "QUOTA" SPACE astring SPACE quota_list

=== Super Class

Struct

=== Methods

: mailbox
      The mailbox with the associated quota.

: usage
      Current storage usage of mailbox.

: quota
      Quota limit imposed on mailbox.

== Net::IMAP::MailboxQuotaRoot

Net::IMAP::MailboxQuotaRoot represents part of the GETQUOTAROOT
response. (GETQUOTAROOT can also return Net::IMAP::MailboxQuota.)

   quotaroot_response
                   ::= "QUOTAROOT" SPACE astring *(SPACE astring)

=== Super Class

Struct

=== Methods

: mailbox
      The mailbox with the associated quota.

: quotaroots
      Zero or more quotaroots that effect the quota on the
      specified mailbox.

== Net::IMAP::MailboxACLItem

Net::IMAP::MailboxACLItem represents response from GETACL.

   acl_data        ::= "ACL" SPACE mailbox *(SPACE identifier SPACE
                        rights)

   identifier      ::= astring

   rights          ::= astring

=== Super Class

Struct

=== Methods

: user
      Login name that has certain rights to the mailbox
      that was specified with the getacl command.

: rights
      The access rights the indicated user has to the
      mailbox.

== Net::IMAP::StatusData

Net::IMAP::StatusData represents contents of the STATUS response.

=== Super Class

Object

=== Methods

: mailbox
      Returns the mailbox name.

: attr
      Returns a hash. Each key is one of "MESSAGES", "RECENT", "UIDNEXT",
      "UIDVALIDITY", "UNSEEN". Each value is a number.

== Net::IMAP::FetchData

Net::IMAP::FetchData represents contents of the FETCH response.

=== Super Class

Object

=== Methods

: seqno
      Returns the message sequence number.
      (Note: not the unique identifier, even for the UID command response.)

: attr
      Returns a hash. Each key is a data item name, and each value is
      its value.

      The current data items are:

      : BODY
          A form of BODYSTRUCTURE without extension data.
      : BODY[<section>]<<origin_octet>>
          A string expressing the body contents of the specified section.
      : BODYSTRUCTURE
          An object that describes the ((<[MIME-IMB]>)) body structure of a message.
          See ((<Net::IMAP::BodyTypeBasic>)), ((<Net::IMAP::BodyTypeText>)),
          ((<Net::IMAP::BodyTypeMessage>)), ((<Net::IMAP::BodyTypeMultipart>)).
      : ENVELOPE
          A ((<Net::IMAP::Envelope>)) object that describes the envelope
          structure of a message.
      : FLAGS
          A array of flag symbols that are set for this message. flag symbols
          are capitalized by String#capitalize.
      : INTERNALDATE
          A string representing the internal date of the message.
      : RFC822
          Equivalent to BODY[].
      : RFC822.HEADER
          Equivalent to BODY.PEEK[HEADER].
      : RFC822.SIZE
          A number expressing the ((<[RFC-822]>)) size of the message.
      : RFC822.TEXT
          Equivalent to BODY[TEXT].
      : UID
          A number expressing the unique identifier of the message.

== Net::IMAP::Envelope

Net::IMAP::Envelope represents envelope structures of messages.

=== Super Class

Struct

=== Methods

: date
      Retunns a string that represents the date.

: subject
      Retunns a string that represents the subject.

: from
      Retunns an array of ((<Net::IMAP::Address>)) that represents the from.

: sender
      Retunns an array of ((<Net::IMAP::Address>)) that represents the sender.

: reply_to
      Retunns an array of ((<Net::IMAP::Address>)) that represents the reply-to.

: to
      Retunns an array of ((<Net::IMAP::Address>)) that represents the to.

: cc
      Retunns an array of ((<Net::IMAP::Address>)) that represents the cc.

: bcc
      Retunns an array of ((<Net::IMAP::Address>)) that represents the bcc.

: in_reply_to
      Retunns a string that represents the in-reply-to.

: message_id
      Retunns a string that represents the message-id.

== Net::IMAP::Address

((<Net::IMAP::Address>)) represents electronic mail addresses.

=== Super Class

Struct

=== Methods

: name
      Returns the phrase from ((<[RFC-822]>)) mailbox.

: route
      Returns the route from ((<[RFC-822]>)) route-addr.

: mailbox
      nil indicates end of ((<[RFC-822]>)) group.
      If non-nil and host is nil, returns ((<[RFC-822]>)) group name.
      Otherwise, returns ((<[RFC-822]>)) local-part

: host
      nil indicates ((<[RFC-822]>)) group syntax.
      Otherwise, returns ((<[RFC-822]>)) domain name.

== Net::IMAP::ContentDisposition

Net::IMAP::ContentDisposition represents Content-Disposition fields.

=== Super Class

Struct

=== Methods

: dsp_type
      Returns the disposition type.

: param
      Returns a hash that represents parameters of the Content-Disposition
      field.

== Net::IMAP::BodyTypeBasic

Net::IMAP::BodyTypeBasic represents basic body structures of messages.

=== Super Class

Struct

=== Methods

: media_type
      Returns the content media type name as defined in ((<[MIME-IMB]>)).

: subtype
      Returns the content subtype name as defined in ((<[MIME-IMB]>)).

: param
      Returns a hash that represents parameters as defined in
      ((<[MIME-IMB]>)).

: content_id
      Returns a string giving the content id as defined in ((<[MIME-IMB]>)).

: description
      Returns a string giving the content description as defined in
      ((<[MIME-IMB]>)).

: encoding
      Returns a string giving the content transfer encoding as defined in
      ((<[MIME-IMB]>)).

: size
      Returns a number giving the size of the body in octets.

: md5
      Returns a string giving the body MD5 value as defined in ((<[MD5]>)).

: disposition
      Returns a ((<Net::IMAP::ContentDisposition>)) object giving
      the content disposition.

: language
      Returns a string or an array of strings giving the body
      language value as defined in [LANGUAGE-TAGS].

: extension
      Returns extension data.

: multipart?
      Returns false.

== Net::IMAP::BodyTypeText

Net::IMAP::BodyTypeText represents TEXT body structures of messages.

=== Super Class

Struct

=== Methods

: lines
      Returns the size of the body in text lines.

And Net::IMAP::BodyTypeText has all methods of ((<Net::IMAP::BodyTypeBasic>)).

== Net::IMAP::BodyTypeMessage

Net::IMAP::BodyTypeMessage represents MESSAGE/RFC822 body structures of messages.

=== Super Class

Struct

=== Methods

: envelope
      Returns a ((<Net::IMAP::Envelope>)) giving the envelope structure.

: body
      Returns an object giving the body structure.

And Net::IMAP::BodyTypeMessage has all methods of ((<Net::IMAP::BodyTypeText>)).

== Net::IMAP::BodyTypeText

=== Super Class

Struct

=== Methods

: media_type
      Returns the content media type name as defined in ((<[MIME-IMB]>)).

: subtype
      Returns the content subtype name as defined in ((<[MIME-IMB]>)).

: parts
      Returns multiple parts.

: param
      Returns a hash that represents parameters as defined in
      ((<[MIME-IMB]>)).

: disposition
      Returns a ((<Net::IMAP::ContentDisposition>)) object giving
      the content disposition.

: language
      Returns a string or an array of strings giving the body
      language value as defined in [LANGUAGE-TAGS].

: extension
      Returns extension data.

: multipart?
      Returns true.

== References

: [IMAP]
    M. Crispin, "INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1",
    RFC 2060, December 1996.

: [LANGUAGE-TAGS]
    Alvestrand, H., "Tags for the Identification of
    Languages", RFC 1766, March 1995.

: [MD5]
    Myers, J., and M. Rose, "The Content-MD5 Header Field", RFC
    1864, October 1995.

: [MIME-IMB]
    Freed, N., and N. Borenstein, "MIME (Multipurpose Internet
    Mail Extensions) Part One: Format of Internet Message Bodies", RFC
    2045, November 1996.

: [RFC-822]
    Crocker, D., "Standard for the Format of ARPA Internet Text
    Messages", STD 11, RFC 822, University of Delaware, August 1982.

: [RFC-2087]
    Myers, J., "IMAP4 QUOTA extension", RFC 2087, January 1997.

: [RFC-2086]
    Myers, J., "IMAP4 ACL extension", RFC 2086, January 1997.

: [OSSL]
    http://www.openssl.org

: [RSSL]
    http://savannah.gnu.org/projects/rubypki

=end

require "socket"
require "monitor"
require "digest/md5"
begin
  require "openssl"
rescue LoadError
end

module Net
  class IMAP
    include MonitorMixin
    if defined?(OpenSSL)
      include OpenSSL
      include SSL
    end

    attr_reader :greeting, :responses, :response_handlers

    SEEN = :Seen
    ANSWERED = :Answered
    FLAGGED = :Flagged
    DELETED = :Deleted
    DRAFT = :Draft
    RECENT = :Recent

    NOINFERIORS = :Noinferiors
    NOSELECT = :Noselect
    MARKED = :Marked
    UNMARKED = :Unmarked

    def self.debug
      return @@debug
    end

    def self.debug=(val)
      return @@debug = val
    end

    def self.add_authenticator(auth_type, authenticator)
      @@authenticators[auth_type] = authenticator
    end

    def disconnect
      @sock.shutdown unless @usessl
      @receiver_thread.join
      @sock.close
    end

    def capability
      synchronize do
	send_command("CAPABILITY")
	return @responses.delete("CAPABILITY")[-1]
      end
    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.instance_of?(ContinuationRequest)
	  data = authenticator.process(resp.data.text.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)
      synchronize do
	@responses.clear
	send_command("SELECT", mailbox)
      end
    end

    def examine(mailbox)
      synchronize do
	@responses.clear
	send_command("EXAMINE", mailbox)
      end
    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)
      synchronize do
	send_command("LIST", refname, mailbox)
	return @responses.delete("LIST")
      end
    end

    def getquotaroot(mailbox)
      synchronize do
        send_command("GETQUOTAROOT", mailbox)
        result = []
        result.concat(@responses.delete("QUOTAROOT"))
        result.concat(@responses.delete("QUOTA"))
        return result
      end
    end

    def getquota(mailbox)
      synchronize do
	send_command("GETQUOTA", mailbox)
	return @responses.delete("QUOTA")
      end
    end

    # setquota(mailbox, nil) will unset quota.
    def setquota(mailbox, quota)
      if quota.nil?
        data = '()'
      else
        data = '(STORAGE ' + quota.to_s + ')'
      end
      send_command("SETQUOTA", mailbox, RawData.new(data))
    end

    # setacl(mailbox, user, nil) will remove rights.
    def setacl(mailbox, user, rights)
      if rights.nil? 
        send_command("SETACL", mailbox, user, "")
      else
        send_command("SETACL", mailbox, user, rights)
      end
    end

    def getacl(mailbox)
      synchronize do
	send_command("GETACL", mailbox)
	return @responses.delete("ACL")[-1]
      end
    end

    def lsub(refname, mailbox)
      synchronize do
	send_command("LSUB", refname, mailbox)
	return @responses.delete("LSUB")
      end
    end

    def status(mailbox, attr)
      synchronize do
	send_command("STATUS", mailbox, attr)
	return @responses.delete("STATUS")[-1].attr
      end
    end

    def append(mailbox, message, flags = nil, date_time = nil)
      args = []
      if flags
	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
      synchronize do
	send_command("EXPUNGE")
	return @responses.delete("EXPUNGE")
      end
    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

    def add_response_handler(handler = Proc.new)
      @response_handlers.push(handler)
    end

    def remove_response_handler(handler)
      @response_handlers.delete(handler)
    end

    private

    CRLF = "\r\n"
    PORT = 143

    @@debug = false
    @@authenticators = {}

    def initialize(host, port = PORT, usessl = false, certs = nil, verify = false)
      super()
      @host = host
      @port = port
      @tag_prefix = "RUBY"
      @tagno = 0
      @parser = ResponseParser.new
      @sock = TCPSocket.open(host, port)
      if usessl
        unless defined?(OpenSSL)
          raise "SSL extension not installed"
        end
        @usessl = true
        @sock = SSLSocket.new(@sock)

        # verify the server.
        @sock.ca_file = certs if certs && FileTest::file?(certs)
        @sock.ca_path = certs if certs && FileTest::directory?(certs)
        @sock.verify_mode = VERIFY_PEER if verify
        @sock.verify_callback = VerifyCallbackProc if defined?(VerifyCallbackProc)

        @sock.connect   # start ssl session.
      else
        @usessl = false
      end
      @responses = Hash.new([].freeze)
      @tagged_responses = {}
      @response_handlers = []
      @tag_arrival = new_cond

      @greeting = get_response
      if /\ABYE\z/ni =~ @greeting.name
	@sock.close
	raise ByeResponseError, resp[0]
      end

      @receiver_thread = Thread.start {
	receive_responses
      }
    end

    def receive_responses
      while resp = get_response
	synchronize do
	  case resp
	  when TaggedResponse
	    @tagged_responses[resp.tag] = resp
	    @tag_arrival.broadcast
	  when UntaggedResponse
	    record_response(resp.name, resp.data)
	    if resp.data.instance_of?(ResponseText) &&
		(code = resp.data.code)
	      record_response(code.name, code.data)
	    end
	  end
	  @response_handlers.each do |handler|
	    handler.call(resp)
	  end
	end
      end
    end

    def get_tagged_response(tag, cmd)
      until @tagged_responses.key?(tag)
	@tag_arrival.wait
      end
      resp = @tagged_responses.delete(tag)
      case resp.name
      when /\A(?:NO)\z/ni
	raise NoResponseError, resp.data.text
      when /\A(?:BAD)\z/ni
	raise BadResponseError, resp.data.text
      else
	return resp
      end
    end

    def get_response
      buff = ""
      while true
	s = @sock.gets(CRLF)
	break unless s
	buff.concat(s)
	if /\{(\d+)\}\r\n/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(/^/n, "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 send_command(cmd, *args, &block)
      synchronize do
	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
	if block
	  add_response_handler(block)
	end
	begin
	  return get_tagged_response(tag, cmd)
	ensure
	  if block
	    remove_response_handler(block)
	  end
	end
      end
    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(/^/n, "C: "))
      end
    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 /[\x80-\xff\r\n]/n
	# literal
	return "{" + str.length.to_s + "}" + CRLF + str
      when /[(){ \x00-\x1f\x7f%*"\\]/n
	# quoted string
	return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"'
      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)
      if keys.instance_of?(String)
	keys = [RawData.new(keys)]
      else
	normalize_searching_criteria(keys)
      end
      synchronize do
	if charset
	  send_command(cmd, "CHARSET", charset, *keys)
	else
	  send_command(cmd, *keys)
	end
	return @responses.delete("SEARCH")[-1]
      end
    end

    def fetch_internal(cmd, set, attr)
      if attr.instance_of?(String)
	attr = RawData.new(attr)
      end
      synchronize do
	@responses.delete("FETCH")
	send_command(cmd, MessageSet.new(set), attr)
	return @responses.delete("FETCH")
      end
    end

    def store_internal(cmd, set, attr, flags)
      if attr.instance_of?(String)
	attr = RawData.new(attr)
      end
      synchronize do
	@responses.delete("FETCH")
	send_command(cmd, MessageSet.new(set), attr, flags)
	return @responses.delete("FETCH")
      end
    end

    def copy_internal(cmd, set, mailbox)
      send_command(cmd, MessageSet.new(set), mailbox)
    end

    def sort_internal(cmd, sort_keys, search_keys, charset)
      if search_keys.instance_of?(String)
	search_keys = [RawData.new(search_keys)]
      else
	normalize_searching_criteria(search_keys)
      end
      normalize_searching_criteria(search_keys)
      synchronize do
	send_command(cmd, sort_keys, charset, *search_keys)
	return @responses.delete("SORT")[-1]
      end
    end

    def normalize_searching_criteria(keys)
      keys.collect! do |i|
	case i
	when -1, Range, Array
	  MessageSet.new(i)
	else
	  i
	end
      end
    end

    class RawData
      def format_data
	return @data
      end

      private

      def initialize(data)
	@data = data
      end
    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(/["\\]/n, "\\\\\\&") + '"'
      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

    ContinuationRequest = Struct.new(:data, :raw_data)
    UntaggedResponse = Struct.new(:name, :data, :raw_data)
    TaggedResponse = Struct.new(:tag, :name, :data, :raw_data)
    ResponseText = Struct.new(:code, :text)
    ResponseCode = Struct.new(:name, :data)
    MailboxList = Struct.new(:attr, :delim, :name)
    MailboxQuota = Struct.new(:mailbox, :usage, :quota)
    MailboxQuotaRoot = Struct.new(:mailbox, :quotaroots)
    MailboxACLItem = Struct.new(:user, :rights)
    StatusData = Struct.new(:mailbox, :attr)
    FetchData = Struct.new(:seqno, :attr)
    Envelope = Struct.new(:date, :subject, :from, :sender, :reply_to,
			  :to, :cc, :bcc, :in_reply_to, :message_id)
    Address = Struct.new(:name, :route, :mailbox, :host)
    ContentDisposition = Struct.new(:dsp_type, :param)

    class BodyTypeBasic < Struct.new(:media_type, :subtype,
				     :param, :content_id,
				     :description, :encoding, :size,
				     :md5, :disposition, :language,
				     :extension)
      def multipart?
	return false
      end

      def media_subtype
	$stderr.printf("warning: media_subtype is obsolete.\n")
	$stderr.printf("         use subtype instead.\n")
	return subtype
      end
    end

    class BodyTypeText < Struct.new(:media_type, :subtype,
				    :param, :content_id,
				    :description, :encoding, :size,
				    :lines,
				    :md5, :disposition, :language,
				    :extension)
      def multipart?
	return false
      end

      def media_subtype
	$stderr.printf("warning: media_subtype is obsolete.\n")
	$stderr.printf("         use subtype instead.\n")
	return subtype
      end
    end

    class BodyTypeMessage < Struct.new(:media_type, :subtype,
				       :param, :content_id,
				       :description, :encoding, :size,
				       :envelope, :body, :lines,
				       :md5, :disposition, :language,
				       :extension)
      def multipart?
	return false
      end

      def media_subtype
	$stderr.printf("warning: media_subtype is obsolete.\n")
	$stderr.printf("         use subtype instead.\n")
	return subtype
      end
    end

    class BodyTypeMultipart < Struct.new(:media_type, :subtype,
					 :parts,
					 :param, :disposition, :language,
					 :extension)
      def multipart?
	return true
      end

      def media_subtype
	$stderr.printf("warning: media_subtype is obsolete.\n")
	$stderr.printf("         use subtype instead.\n")
	return subtype
      end
    end

    class ResponseParser
      def parse(str)
	@str = str
	@pos = 0
	@lex_state = EXPR_BEG
	@token = nil
	return response
      end

      private

      EXPR_BEG		= :EXPR_BEG
      EXPR_DATA		= :EXPR_DATA
      EXPR_TEXT		= :EXPR_TEXT
      EXPR_RTEXT	= :EXPR_RTEXT
      EXPR_CTEXT	= :EXPR_CTEXT

      T_SPACE	= :SPACE
      T_NIL	= :NIL
      T_NUMBER	= :NUMBER
      T_ATOM	= :ATOM
      T_QUOTED	= :QUOTED
      T_LPAR	= :LPAR
      T_RPAR	= :RPAR
      T_BSLASH	= :BSLASH
      T_STAR	= :STAR
      T_LBRA	= :LBRA
      T_RBRA	= :RBRA
      T_LITERAL	= :LITERAL
      T_PLUS	= :PLUS
      T_PERCENT	= :PERCENT
      T_CRLF	= :CRLF
      T_EOF	= :EOF
      T_TEXT	= :TEXT

      BEG_REGEXP = /\G(?:\
(?# 1:	SPACE	)( )|\
(?# 2:	NIL	)(NIL)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
(?# 3:	NUMBER	)(\d+)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
(?# 4:	ATOM	)([^\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+]+)|\
(?# 5:	QUOTED	)"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)"|\
(?# 6:	LPAR	)(\()|\
(?# 7:	RPAR	)(\))|\
(?# 8:	BSLASH	)(\\)|\
(?# 9:	STAR	)(\*)|\
(?# 10:	LBRA	)(\[)|\
(?# 11:	RBRA	)(\])|\
(?# 12:	LITERAL	)\{(\d+)\}\r\n|\
(?# 13:	PLUS	)(\+)|\
(?# 14:	PERCENT	)(%)|\
(?# 15:	CRLF	)(\r\n)|\
(?# 16:	EOF	)(\z))/ni

      DATA_REGEXP = /\G(?:\
(?# 1:	SPACE	)( )|\
(?# 2:	NIL	)(NIL)|\
(?# 3:	NUMBER	)(\d+)|\
(?# 4:	QUOTED	)"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)"|\
(?# 5:	LITERAL	)\{(\d+)\}\r\n|\
(?# 6:	LPAR	)(\()|\
(?# 7:	RPAR	)(\)))/ni

      TEXT_REGEXP = /\G(?:\
(?# 1:	TEXT	)([^\x00\x80-\xff\r\n]*))/ni

      RTEXT_REGEXP = /\G(?:\
(?# 1:	LBRA	)(\[)|\
(?# 2:	TEXT	)([^\x00\x80-\xff\r\n]*))/ni

      CTEXT_REGEXP = /\G(?:\
(?# 1:	TEXT	)([^\x00\x80-\xff\r\n\]]*))/ni

      Token = Struct.new(:symbol, :value)

      def response
	token = lookahead
	case token.symbol
	when T_PLUS
	  result = continue_req
	when T_STAR
	  result = response_untagged
	else
	  result = response_tagged
	end
	match(T_CRLF)
	match(T_EOF)
	return result
      end

      def continue_req
	match(T_PLUS)
	match(T_SPACE)
	return ContinuationRequest.new(resp_text, @str)
      end

      def response_untagged
	match(T_STAR)
	match(T_SPACE)
	token = lookahead
	if token.symbol == T_NUMBER
	  return numeric_response
	elsif token.symbol == T_ATOM
	  case token.value
	  when /\A(?:OK|NO|BAD|BYE|PREAUTH)\z/ni
	    return response_cond
	  when /\A(?:FLAGS)\z/ni
	    return flags_response
	  when /\A(?:LIST|LSUB)\z/ni
	    return list_response
	  when /\A(?:QUOTA)\z/ni
	    return getquota_response
	  when /\A(?:QUOTAROOT)\z/ni
	    return getquotaroot_response
	  when /\A(?:ACL)\z/ni
	    return getacl_response
	  when /\A(?:SEARCH|SORT)\z/ni
	    return search_response
	  when /\A(?:STATUS)\z/ni
	    return status_response
	  when /\A(?:CAPABILITY)\z/ni
	    return capability_response
	  else
	    return text_response
	  end
	else
	  parse_error("unexpected token %s", token.symbol)
	end
      end

      def response_tagged
	tag = atom
	match(T_SPACE)
	token = match(T_ATOM)
	name = token.value.upcase
	match(T_SPACE)
	return TaggedResponse.new(tag, name, resp_text, @str)
      end

      def response_cond
	token = match(T_ATOM)
	name = token.value.upcase
	match(T_SPACE)
	return UntaggedResponse.new(name, resp_text, @str)
      end

      def numeric_response
	n = number
	match(T_SPACE)
	token = match(T_ATOM)
	name = token.value.upcase
	case name
	when "EXISTS", "RECENT", "EXPUNGE"
	  return UntaggedResponse.new(name, n, @str)
	when "FETCH"
	  shift_token
	  match(T_SPACE)
	  data = FetchData.new(n, msg_att)
	  return UntaggedResponse.new(name, data, @str)
	end
      end

      def msg_att
	match(T_LPAR)
	attr = {}
	while true
	  token = lookahead
	  case token.symbol
	  when T_RPAR
	    shift_token
	    break
	  when T_SPACE
	    shift_token
	    token = lookahead
	  end
	  case token.value
	  when /\A(?:ENVELOPE)\z/ni
	    name, val = envelope_data
	  when /\A(?:FLAGS)\z/ni
	    name, val = flags_data
	  when /\A(?:INTERNALDATE)\z/ni
	    name, val = internaldate_data
	  when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni
	    name, val = rfc822_text
	  when /\A(?:RFC822\.SIZE)\z/ni
	    name, val = rfc822_size
	  when /\A(?:BODY(?:STRUCTURE)?)\z/ni
	    name, val = body_data
	  when /\A(?:UID)\z/ni
	    name, val = uid_data
	  else
	    parse_error("unknown attribute `%s'", token.value)
	  end
	  attr[name] = val
	end
	return attr
      end

      def envelope_data
	token = match(T_ATOM)
	name = token.value.upcase
	match(T_SPACE)
	return name, envelope
      end

      def envelope
	@lex_state = EXPR_DATA
	match(T_LPAR)
	date = nstring
	match(T_SPACE)
	subject = nstring
	match(T_SPACE)
	from = address_list
	match(T_SPACE)
	sender = address_list
	match(T_SPACE)
	reply_to = address_list
	match(T_SPACE)
	to = address_list
	match(T_SPACE)
	cc = address_list
	match(T_SPACE)
	bcc = address_list
	match(T_SPACE)
	in_reply_to = nstring
	match(T_SPACE)
	message_id = nstring
	match(T_RPAR)
	@lex_state = EXPR_BEG
	return Envelope.new(date, subject, from, sender, reply_to,
			    to, cc, bcc, in_reply_to, message_id)
      end

      def flags_data
	token = match(T_ATOM)
	name = token.value.upcase
	match(T_SPACE)
	return name, flag_list
      end

      def internaldate_data
	token = match(T_ATOM)
	name = token.value.upcase
	match(T_SPACE)
	token = match(T_QUOTED)
	return name, token.value
      end

      def rfc822_text
	token = match(T_ATOM)
	name = token.value.upcase
	match(T_SPACE)
	return name, nstring
      end

      def rfc822_size
	token = match(T_ATOM)
	name = token.value.upcase
	match(T_SPACE)
	return name, number
      end

      def body_data
	token = match(T_ATOM)
	name = token.value.upcase
	token = lookahead
	if token.symbol == T_SPACE
	  shift_token
	  return name, body
	end
	name.concat(section)
	token = lookahead
	if token.symbol == T_ATOM
	  name.concat(token.value)
	  shift_token
	end
	match(T_SPACE)
	data = nstring
	return name, data
      end

      def body
	@lex_state = EXPR_DATA
	match(T_LPAR)
	token = lookahead
	if token.symbol == T_LPAR
	  result = body_type_mpart
	else
	  result = body_type_1part
	end
	match(T_RPAR)
	@lex_state = EXPR_BEG
	return result
      end

      def body_type_1part
	token = lookahead
	case token.value
	when /\A(?:TEXT)\z/ni
	  return body_type_text
	when /\A(?:MESSAGE)\z/ni
	  return body_type_msg
	else
	  return body_type_basic
	end
      end

      def body_type_basic
	mtype, msubtype = media_type
	match(T_SPACE)
	param, content_id, desc, enc, size = body_fields
	md5, disposition, language, extension = body_ext_1part
	return BodyTypeBasic.new(mtype, msubtype,
				 param, content_id,
				 desc, enc, size,
				 md5, disposition, language, extension)
      end

      def body_type_text
	mtype, msubtype = media_type
	match(T_SPACE)
	param, content_id, desc, enc, size = body_fields
	match(T_SPACE)
	lines = number
	md5, disposition, language, extension = body_ext_1part
	return BodyTypeText.new(mtype, msubtype,
				param, content_id,
				desc, enc, size,
				lines,
				md5, disposition, language, extension)
      end

      def body_type_msg
	mtype, msubtype = media_type
	match(T_SPACE)
	param, content_id, desc, enc, size = body_fields
	match(T_SPACE)
	env = envelope
	match(T_SPACE)
	b = body
	match(T_SPACE)
	lines = number
	md5, disposition, language, extension = body_ext_1part
	return BodyTypeMessage.new(mtype, msubtype,
				   param, content_id,
				   desc, enc, size,
				   env, b, lines,
				   md5, disposition, language, extension)
      end

      def body_type_mpart
	parts = []
	while true
	  token = lookahead
	  if token.symbol == T_SPACE
	    shift_token
	    break
	  end
	  parts.push(body)
	end
	mtype = "MULTIPART"
	msubtype = string.upcase
	param, disposition, language, extension = body_ext_mpart
	return BodyTypeMultipart.new(mtype, msubtype, parts,
				     param, disposition, language,
				     extension)
      end

      def media_type
	mtype = string.upcase
	match(T_SPACE)
	msubtype = string.upcase
	return mtype, msubtype
      end

      def body_fields
	param = body_fld_param
	match(T_SPACE)
	content_id = nstring
	match(T_SPACE)
	desc = nstring
	match(T_SPACE)
	enc = string.upcase
	match(T_SPACE)
	size = number
	return param, content_id, desc, enc, size
      end

      def body_fld_param
	token = lookahead
	if token.symbol == T_NIL
	  shift_token
	  return nil
	end
	match(T_LPAR)
	param = {}
	while true
	  token = lookahead
	  case token.symbol
	  when T_RPAR
	    shift_token
	    break
	  when T_SPACE
	    shift_token
	  end
	  name = string.upcase
	  match(T_SPACE)
	  val = string
	  param[name] = val
	end
	return param
      end

      def body_ext_1part
	token = lookahead
	if token.symbol == T_SPACE
	  shift_token
	else
	  return nil
	end
	md5 = nstring

	token = lookahead
	if token.symbol == T_SPACE
	  shift_token
	else
	  return md5
	end
	disposition = body_fld_dsp

	token = lookahead
	if token.symbol == T_SPACE
	  shift_token
	else
	  return md5, disposition
	end
	language = body_fld_lang

	token = lookahead
	if token.symbol == T_SPACE
	  shift_token
	else
	  return md5, disposition, language
	end

	extension = body_extensions
	return md5, disposition, language, extension
      end

      def body_ext_mpart
	token = lookahead
	if token.symbol == T_SPACE
	  shift_token
	else
	  return nil
	end
	param = body_fld_param

	token = lookahead
	if token.symbol == T_SPACE
	  shift_token
	else
	  return param
	end
	disposition = body_fld_dsp
	match(T_SPACE)
	language = body_fld_lang

	token = lookahead
	if token.symbol == T_SPACE
	  shift_token
	else
	  return param, disposition, language
	end

	extension = body_extensions
	return param, disposition, language, extension
      end

      def body_fld_dsp
	token = lookahead
	if token.symbol == T_NIL
	  shift_token
	  return nil
	end
	match(T_LPAR)
	dsp_type = string.upcase
	match(T_SPACE)
	param = body_fld_param
	match(T_RPAR)
	return ContentDisposition.new(dsp_type, param)
      end

      def body_fld_lang
	token = lookahead
	if token.symbol == T_LPAR
	  shift_token
	  result = []
	  while true
	    token = lookahead
	    case token.symbol
	    when T_RPAR
	      shift_token
	      return result
	    when T_SPACE
	      shift_token
	    end
	    result.push(string.upcase)
	  end
	else
	  lang = nstring
	  if lang
	    return lang.upcase
	  else
	    return lang
	  end
	end
      end

      def body_extensions
	result = []
	while true
	  token = lookahead
	  case token.symbol
	  when T_RPAR
	    return result
	  when T_SPACE
	    shift_token
	  end
	  result.push(body_extension)
	end
      end

      def body_extension
	token = lookahead
	case token.symbol
	when T_LPAR
	  shift_token
	  result = body_extensions
	  match(T_RPAR)
	  return result
	when T_NUMBER
	  return number
	else
	  return nstring
	end
      end

      def section
	str = ""
	token = match(T_LBRA)
	str.concat(token.value)
	token = match(T_ATOM, T_NUMBER, T_RBRA)
	if token.symbol == T_RBRA
	  str.concat(token.value)
	  return str
	end
	str.concat(token.value)
	token = lookahead
	if token.symbol == T_SPACE
	  shift_token
	  str.concat(token.value)
	  token = match(T_LPAR)
	  str.concat(token.value)
	  while true
	    token = lookahead
	    case token.symbol
	    when T_RPAR
	      str.concat(token.value)
	      shift_token
	      break
	    when T_SPACE
	      shift_token
	      str.concat(token.value)
	    end
	    str.concat(format_string(astring))
	  end
	end
	token = match(T_RBRA)
	str.concat(token.value)
	return str
      end

      def format_string(str)
	case str
	when ""
	  return '""'
	when /[\x80-\xff\r\n]/n
	  # literal
	  return "{" + str.length.to_s + "}" + CRLF + str
	when /[(){ \x00-\x1f\x7f%*"\\]/n
	  # quoted string
	  return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"'
	else
	  # atom
	  return str
	end
      end

      def uid_data
	token = match(T_ATOM)
	name = token.value.upcase
	match(T_SPACE)
	return name, number
      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)
      end

      def flags_response
	token = match(T_ATOM)
	name = token.value.upcase
	match(T_SPACE)
	return UntaggedResponse.new(name, flag_list, @str)
      end

      def list_response
	token = match(T_ATOM)
	name = token.value.upcase
	match(T_SPACE)
	return UntaggedResponse.new(name, mailbox_list, @str)
      end

      def mailbox_list
	attr = flag_list
	match(T_SPACE)
	token = match(T_QUOTED, T_NIL)
	if token.symbol == T_NIL
	  delim = nil
	else
	  delim = token.value
	end
	match(T_SPACE)
	name = astring
	return MailboxList.new(attr, delim, name)
      end

      def getquota_response
        # If quota never established, get back
        # `NO Quota root does not exist'.
        # If quota removed, get `()' after the
        # folder spec with no mention of `STORAGE'.
        token = match(T_ATOM)
        name = token.value.upcase
        match(T_SPACE)
        mailbox = astring
        match(T_SPACE)
        match(T_LPAR)
        token = lookahead
        case token.symbol
        when T_RPAR
          shift_token
          data = MailboxQuota.new(mailbox, nil, nil)
          return UntaggedResponse.new(name, data, @str)
        when T_ATOM
          shift_token
          match(T_SPACE)
          token = match(T_NUMBER)
          usage = token.value
          match(T_SPACE)
          token = match(T_NUMBER)
          quota = token.value
          match(T_RPAR)
          data = MailboxQuota.new(mailbox, usage, quota)
          return UntaggedResponse.new(name, data, @str)
        else
          parse_error("unexpected token %s", token.symbol)
        end
      end

      def getquotaroot_response
        # Similar to getquota, but only admin can use getquota.
        token = match(T_ATOM)
        name = token.value.upcase
        match(T_SPACE)
        mailbox = astring
        quotaroots = []
        while true
          token = lookahead
          break unless token.symbol == T_SPACE
          shift_token
          quotaroots.push(astring)
        end
        data = MailboxQuotaRoot.new(mailbox, quotaroots)
        return UntaggedResponse.new(name, data, @str)
      end

      def getacl_response
        token = match(T_ATOM)
        name = token.value.upcase
        match(T_SPACE)
        mailbox = astring
        data = []
        token = lookahead
        if token.symbol == T_SPACE
          shift_token
          while true
            token = lookahead
            case token.symbol
            when T_CRLF
              break
            when T_SPACE
              shift_token
            end
            user = astring
            match(T_SPACE)
            rights = astring
            ##XXX data.push([user, rights])
            data.push(MailboxACLItem.new(user, rights))
          end
        end
	return UntaggedResponse.new(name, data, @str)
      end

      def search_response
	token = match(T_ATOM)
	name = token.value.upcase
	token = lookahead
	if token.symbol == T_SPACE
	  shift_token
	  data = []
	  while true
	    token = lookahead
	    case token.symbol
	    when T_CRLF
	      break
	    when T_SPACE
	      shift_token
	    end
	    data.push(number)
	  end
	else
	  data = []
	end
	return UntaggedResponse.new(name, data, @str)
      end

      def status_response
	token = match(T_ATOM)
	name = token.value.upcase
	match(T_SPACE)
	mailbox = astring
	match(T_SPACE)
	match(T_LPAR)
	attr = {}
	while true
	  token = lookahead
	  case token.symbol
	  when T_RPAR
	    shift_token
	    break
	  when T_SPACE
	    shift_token
	  end
	  token = match(T_ATOM)
	  key = token.value.upcase
	  match(T_SPACE)
	  val = number
	  attr[key] = val
	end
	data = StatusData.new(mailbox, attr)
	return UntaggedResponse.new(name, data, @str)
      end

      def capability_response
	token = match(T_ATOM)
	name = token.value.upcase
	match(T_SPACE)
	data = []
	while true
	  token = lookahead
	  case token.symbol
	  when T_CRLF
	    break
	  when T_SPACE
	    shift_token
	  end
	  data.push(atom.upcase)
	end
	return UntaggedResponse.new(name, data, @str)
      end

      def resp_text
	@lex_state = EXPR_RTEXT
	token = lookahead
	if token.symbol == T_LBRA
	  code = resp_text_code
	else
	  code = nil
	end
	token = match(T_TEXT)
	@lex_state = EXPR_BEG
	return ResponseText.new(code, token.value)
      end

      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)\z/n
	  result = ResponseCode.new(name, nil)
	when /\A(?:PERMANENTFLAGS)\z/n
	  match(T_SPACE)
	  result = ResponseCode.new(name, flag_list)
	when /\A(?:UIDVALIDITY|UIDNEXT|UNSEEN)\z/n
	  match(T_SPACE)
	  result = ResponseCode.new(name, number)
	else
	  match(T_SPACE)
	  @lex_state = EXPR_CTEXT
	  token = match(T_TEXT)
	  @lex_state = EXPR_BEG
	  result = ResponseCode.new(name, token.value)
	end
	match(T_RBRA)
	@lex_state = EXPR_RTEXT
	return result
      end

      def address_list
	token = lookahead
	if token.symbol == T_NIL
	  shift_token
	  return nil
	else
	  result = []
	  match(T_LPAR)
	  while true
	    token = lookahead
	    case token.symbol
	    when T_RPAR
	      shift_token
	      break
	    when T_SPACE
	      shift_token
	    end
	    result.push(address)
	  end
	  return result
	end
      end

      ADDRESS_REGEXP = /\G\
(?# 1: NAME	)(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
(?# 2: ROUTE	)(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
(?# 3: MAILBOX	)(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
(?# 4: HOST	)(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)")\
\)/ni

      def address
	match(T_LPAR)
	if @str.index(ADDRESS_REGEXP, @pos)
	  # address does not include literal.
	  @pos = $~.end(0)
	  name = $1
	  route = $2
	  mailbox = $3
	  host = $4
	  for s in [name, route, mailbox, host]
	    if s
	      s.gsub!(/\\(["\\])/n, "\\1")
	    end
	  end
	else
	  name = nstring
	  match(T_SPACE)
	  route = nstring
	  match(T_SPACE)
	  mailbox = nstring
	  match(T_SPACE)
	  host = nstring
	  match(T_RPAR)
	end
	return Address.new(name, route, mailbox, host)
      end

#        def flag_list
#  	result = []
#  	match(T_LPAR)
#  	while true
#  	  token = lookahead
#  	  case token.symbol
#  	  when T_RPAR
#  	    shift_token
#  	    break
#  	  when T_SPACE
#  	    shift_token
#  	  end
#  	  result.push(flag)
#  	end
#  	return result
#        end

#        def flag
#  	token = lookahead
#  	if token.symbol == T_BSLASH
#  	  shift_token
#  	  token = lookahead
#  	  if token.symbol == T_STAR
#  	    shift_token
#  	    return token.value.intern
#  	  else
#  	    return atom.intern
#  	  end
#  	else
#  	  return atom
#  	end
#        end

      FLAG_REGEXP = /\
(?# FLAG	)\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)|\
(?# ATOM	)([^\x80-\xff(){ \x00-\x1f\x7f%*"\\]+)/n

      def flag_list
	if @str.index(/\(([^)]*)\)/ni, @pos)
	  @pos = $~.end(0)
	  return $1.scan(FLAG_REGEXP).collect { |flag, atom|
	    atom || flag.capitalize.intern
	  }
	else
	  parse_error("invalid flag list")
	end
      end

      def nstring
	token = lookahead
	if token.symbol == T_NIL
	  shift_token
	  return nil
	else
	  return string
	end
      end

      def astring
	token = lookahead
	if string_token?(token)
	  return string
	else
	  return atom
	end
      end

      def string
	token = match(T_QUOTED, T_LITERAL)
	return token.value
      end

      STRING_TOKENS = [T_QUOTED, T_LITERAL]

      def string_token?(token)
	return STRING_TOKENS.include?(token.symbol)
      end

      def atom
  	result = ""
  	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 = [
	T_ATOM,
	T_NUMBER,
	T_NIL,
	T_LBRA,
	T_RBRA,
	T_PLUS
      ]

      def atom_token?(token)
	return ATOM_TOKENS.include?(token.symbol)
      end

      def number
	token = match(T_NUMBER)
	return token.value.to_i
      end

      def nil_atom
	match(T_NIL)
	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 "))
	end
	shift_token
	return token
      end

      def lookahead
	unless @token
	  @token = next_token
	end
	return @token
      end

      def shift_token
	@token = nil
      end

      def next_token
	case @lex_state
	when EXPR_BEG
	  if @str.index(BEG_REGEXP, @pos)
	    @pos = $~.end(0)
	    if $1
	      return Token.new(T_SPACE, $+)
	    elsif $2
	      return Token.new(T_NIL, $+)
	    elsif $3
	      return Token.new(T_NUMBER, $+)
	    elsif $4
	      return Token.new(T_ATOM, $+)
	    elsif $5
	      return Token.new(T_QUOTED,
			       $+.gsub(/\\(["\\])/n, "\\1"))
	    elsif $6
	      return Token.new(T_LPAR, $+)
	    elsif $7
	      return Token.new(T_RPAR, $+)
	    elsif $8
	      return Token.new(T_BSLASH, $+)
	    elsif $9
	      return Token.new(T_STAR, $+)
	    elsif $10
	      return Token.new(T_LBRA, $+)
	    elsif $11
	      return Token.new(T_RBRA, $+)
	    elsif $12
	      len = $+.to_i
	      val = @str[@pos, len]
	      @pos += len
	      return Token.new(T_LITERAL, val)
	    elsif $13
	      return Token.new(T_PLUS, $+)
	    elsif $14
	      return Token.new(T_PERCENT, $+)
	    elsif $15
	      return Token.new(T_CRLF, $+)
	    elsif $16
	      return Token.new(T_EOF, $+)
	    else
	      parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid")
	    end
	  else
	    @str.index(/\S*/n, @pos)
	    parse_error("unknown token - %s", $&.dump)
	  end
	when EXPR_DATA
	  if @str.index(DATA_REGEXP, @pos)
	    @pos = $~.end(0)
	    if $1
	      return Token.new(T_SPACE, $+)
	    elsif $2
	      return Token.new(T_NIL, $+)
	    elsif $3
	      return Token.new(T_NUMBER, $+)
	    elsif $4
	      return Token.new(T_QUOTED,
			       $+.gsub(/\\(["\\])/n, "\\1"))
	    elsif $5
	      len = $+.to_i
	      val = @str[@pos, len]
	      @pos += len
	      return Token.new(T_LITERAL, val)
	    elsif $6
	      return Token.new(T_LPAR, $+)
	    elsif $7
	      return Token.new(T_RPAR, $+)
	    else
	      parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid")
	    end
	  else
	    @str.index(/\S*/n, @pos)
	    parse_error("unknown token - %s", $&.dump)
	  end
	when EXPR_TEXT
	  if @str.index(TEXT_REGEXP, @pos)
	    @pos = $~.end(0)
	    if $1
	      return Token.new(T_TEXT, $+)
	    else
	      parse_error("[Net::IMAP BUG] TEXT_REGEXP is invalid")
	    end
	  else
	    @str.index(/\S*/n, @pos)
	    parse_error("unknown token - %s", $&.dump)
	  end
	when EXPR_RTEXT
	  if @str.index(RTEXT_REGEXP, @pos)
	    @pos = $~.end(0)
	    if $1
	      return Token.new(T_LBRA, $+)
	    elsif $2
	      return Token.new(T_TEXT, $+)
	    else
	      parse_error("[Net::IMAP BUG] RTEXT_REGEXP is invalid")
	    end
	  else
	    @str.index(/\S*/n, @pos)
	    parse_error("unknown token - %s", $&.dump)
	  end
	when EXPR_CTEXT
	  if @str.index(CTEXT_REGEXP, @pos)
	    @pos = $~.end(0)
	    if $1
	      return Token.new(T_TEXT, $+)
	    else
	      parse_error("[Net::IMAP BUG] CTEXT_REGEXP is invalid")
	    end
	  else
	    @str.index(/\S*/n, @pos) #/
	    parse_error("unknown token - %s", $&.dump)
	  end
	else
	  parse_error("illegal @lex_state - %s", @lex_state.inspect)
	end
      end

      def parse_error(fmt, *args)
	if IMAP.debug
	  $stderr.printf("@str: %s\n", @str.dump)
	  $stderr.printf("@pos: %d\n", @pos)
	  $stderr.printf("@lex_state: %s\n", @lex_state)
	  if @token.symbol
	    $stderr.printf("@token.symbol: %s\n", @token.symbol)
	    $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
    add_authenticator "LOGIN", LoginAuthenticator

    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
	  key = Digest::MD5.digest(key)
	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

	digest = Digest::MD5.digest(k_ipad + text)

	return Digest::MD5.hexdigest(k_opad + digest)
      end
    end
    add_authenticator "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

if __FILE__ == $0
  require "getoptlong"

  $stdout.sync = true
  $port = "imap2"
  $user = ENV["USER"] || ENV["LOGNAME"]
  $auth = "cram-md5"

  def usage
    $stderr.print <<EOF
usage: #{$0} [options] <host>

  --help                        print this message
  --port=PORT                   specifies port
  --user=USER                   specifies user
  --auth=AUTH                   specifies auth type
EOF
  end

  def get_password
    print "password: "
    system("stty", "-echo")
    begin
      return gets.chop
    ensure
      system("stty", "echo")
      print "\n"
    end
  end

  def get_command
    printf("%s@%s> ", $user, $host)
    if line = gets
      return line.strip.split(/\s+/)
    else
      return nil
    end
  end

  parser = GetoptLong.new
  parser.set_options(['--help', GetoptLong::NO_ARGUMENT],
		     ['--port', GetoptLong::REQUIRED_ARGUMENT],
		     ['--user', GetoptLong::REQUIRED_ARGUMENT],
		     ['--auth', GetoptLong::REQUIRED_ARGUMENT])
  begin
    parser.each_option do |name, arg|
      case name
      when "--port"
	$port = arg
      when "--user"
	$user = arg
      when "--auth"
	$auth = arg
      when "--help"
	usage
	exit(1)
      end
    end
  rescue
    usage
    exit(1)
  end

  $host = ARGV.shift
  unless $host
    usage
    exit(1)
  end
    
  imap = Net::IMAP.new($host, $port)
  begin
    password = get_password
    imap.authenticate($auth, $user, password)
    while true
      cmd, *args = get_command
      break unless cmd
      begin
	case cmd
	when "list"
	  for mbox in imap.list("", args[0] || "*")
	    if mbox.attr.include?(Net::IMAP::NOSELECT)
	      prefix = "!"
	    elsif mbox.attr.include?(Net::IMAP::MARKED)
	      prefix = "*"
	    else
	      prefix = " "
	    end
	    print prefix, mbox.name, "\n"
	  end
	when "select"
	  imap.select(args[0] || "inbox")
	  print "ok\n"
	when "close"
	  imap.close
	  print "ok\n"
	when "summary"
	  unless messages = imap.responses["EXISTS"][-1]
	    puts "not selected"
	    next
	  end
	  if messages > 0
	    for data in imap.fetch(1..-1, ["ENVELOPE"])
	      print data.seqno, ": ", data.attr["ENVELOPE"].subject, "\n"
	    end
	  else
	    puts "no message"
	  end
	when "fetch"
	  if args[0]
	    data = imap.fetch(args[0].to_i, ["RFC822.HEADER", "RFC822.TEXT"])[0]
	    puts data.attr["RFC822.HEADER"]
	    puts data.attr["RFC822.TEXT"]
	  else
	    puts "missing argument"
	  end
	when "logout", "exit", "quit"
	  break
	when "help", "?"
	  print <<EOF
list [pattern]			list mailboxes
select [mailbox]		select mailbox
close				close mailbox
summary				display summary
fetch [msgno]			display message
logout				logout
help, ?				display help message
EOF
	else
	  print "unknown command: ", cmd, "\n"
	end
      rescue Net::IMAP::Error
	puts $!
      end
    end
  ensure
    imap.logout
    imap.disconnect
  end
end
