File: net/imap.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  module: Net#24
has properties
method: usage #3267
method: get_password #3279
method: get_command #3290
  class: IMAP#200
includes
  SSL ( Unknown-Module )
  MonitorMixin   
  OpenSSL   
inherits from
  Object ( Builtin-Module )
has properties
attribute: greeting [R] #208
attribute: responses [R] #217
attribute: response_handlers [R] #220
attribute: client_thread [RW] #223
constant: SEEN #226
constant: ANSWERED #229
constant: FLAGGED #233
constant: DELETED #237
constant: DRAFT #240
constant: RECENT #245
constant: NOINFERIORS #249
constant: NOSELECT #252
constant: MARKED #257
constant: UNMARKED #260
class method: debug #263
class method: debug= / 1 #268
class method: add_authenticator / 2 #281
method: disconnect #286
method: disconnected? #299
method: capability #313
method: noop #321
method: logout #327
method: authenticate / 2 #356
method: login / 2 #379
method: select / 1 #395
method: examine / 1 #408
method: create / 1 #419
method: delete / 1 #428
method: rename / 2 #439
method: subscribe / 1 #449
method: unsubscribe / 1 #459
method: list / 2 #485
method: getquotaroot / 1 #496
method: getquota / 1 #510
method: setquota / 2 #522
method: setacl / 3 #535
method: getacl / 1 #546
method: lsub / 2 #558
method: status / 2 #581
method: append / 4 #606
method: check #620
method: close #627
method: expunge #633
method: search / 2 #680
method: uid_search / 2 #685
method: fetch / 2 #712
method: uid_fetch / 2 #717
method: store / 3 #735
method: uid_store / 3 #740
method: copy / 2 #748
method: uid_copy / 2 #753
method: sort / 3 #766
method: uid_sort / 3 #771
method: add_response_handler / 1 #787
method: remove_response_handler / 1 #792
method: thread / 3 #809
method: uid_thread / 3 #815
class method: decode_utf7 / 1 #827
class method: encode_utf7 / 1 #843
constant: CRLF #856
constant: PORT #857
method: initialize / 5 #881
method: receive_responses #931
method: get_tagged_response / 1 #992
method: pick_up_tagged_response / 1 #1000
method: get_response #1012
method: record_response / 2 #1032
method: send_command / 3 #1039
method: generate_tag #1064
method: put_string / 1 #1069
method: send_data / 1 #1084
method: send_string_data / 1 #1103
method: send_quoted_string / 1 #1118
method: send_literal / 1 #1122
method: send_number_data / 1 #1137
method: send_list_data / 1 #1144
constant: DATE_MONTH #1158
method: send_time_data / 1 #1160
method: send_symbol_data / 1 #1168
method: search_internal / 3 #1172
method: fetch_internal / 3 #1188
method: store_internal / 4 #1205
method: copy_internal / 3 #1216
method: sort_internal / 4 #1220
method: thread_internal / 4 #1233
method: normalize_searching_criteria / 1 #1244
class method: u16tou8 / 1 #1255
class method: u8tou16 / 1 #1307
constant: ContinuationRequest #1480
constant: UntaggedResponse #1499
constant: TaggedResponse #1523
constant: ResponseText #1537
constant: ResponseCode #1555
constant: MailboxList #1572
constant: MailboxQuota #1593
constant: MailboxQuotaRoot #1607
constant: MailboxACLItem #1625
constant: StatusData #1636
constant: FetchData #1677
constant: Envelope #1703
constant: Address #1722
constant: ContentDisposition #1734
constant: ThreadMember #1746
  class: RawData#1374
inherits from
  Object ( Builtin-Module )
has properties
method: send_data / 1 #1375
method: initialize / 1 #1381
  class: Atom#1386
inherits from
  Object ( Builtin-Module )
has properties
method: send_data / 1 #1387
method: initialize / 1 #1393
  class: QuotedString#1398
inherits from
  Object ( Builtin-Module )
has properties
method: send_data / 1 #1399
method: initialize / 1 #1405
  class: Literal#1410
inherits from
  Object ( Builtin-Module )
has properties
method: send_data / 1 #1411
method: initialize / 1 #1417
  class: MessageSet#1422
inherits from
  Object ( Builtin-Module )
has properties
method: send_data / 1 #1423
method: initialize / 1 #1429
method: format_internal / 1 #1433
method: ensure_nz_number / 1 #1457
  class: BodyTypeBasic#1780
inherits from
  Struct ( Builtin-Module )
has properties
method: multipart? #1785
method: media_subtype #1792
  class: BodyTypeText#1807
inherits from
  Struct ( Builtin-Module )
has properties
method: multipart? #1813
method: media_subtype #1820
  class: BodyTypeMessage#1837
inherits from
  Struct ( Builtin-Module )
has properties
method: multipart? #1843
method: media_subtype #1850
  class: BodyTypeMultipart#1880
inherits from
  Struct ( Builtin-Module )
has properties
method: multipart? #1884
method: media_subtype #1891
  class: ResponseParser#1898
inherits from
  Object ( Builtin-Module )
has properties
method: parse / 1 #1899
constant: EXPR_BEG #1909
constant: EXPR_DATA #1910
constant: EXPR_TEXT #1911
constant: EXPR_RTEXT #1912
constant: EXPR_CTEXT #1913
constant: T_SPACE #1915
constant: T_NIL #1916
constant: T_NUMBER #1917
constant: T_ATOM #1918
constant: T_QUOTED #1919
constant: T_LPAR #1920
constant: T_RPAR #1921
constant: T_BSLASH #1922
constant: T_STAR #1923
constant: T_LBRA #1924
constant: T_RBRA #1925
constant: T_LITERAL #1926
constant: T_PLUS #1927
constant: T_PERCENT #1928
constant: T_CRLF #1929
constant: T_EOF #1930
constant: T_TEXT #1931
constant: BEG_REGEXP #1933
constant: DATA_REGEXP #1951
constant: TEXT_REGEXP #1960
constant: RTEXT_REGEXP #1963
constant: CTEXT_REGEXP #1967
constant: Token #1970
method: response #1972
method: continue_req #1987
method: response_untagged #1993
method: response_tagged #2029
method: response_cond #2038
method: numeric_response #2045
method: msg_att #2061
method: envelope_data #2097
method: envelope #2104
method: flags_data #2139
method: internaldate_data #2146
method: rfc822_text #2154
method: rfc822_size #2161
method: body_data #2168
method: body #2187
method: body_type_1part #2207
method: body_type_basic #2219
method: body_type_text #2234
method: body_type_msg #2248
method: body_type_mpart #2266
method: media_type #2284
method: body_fields #2291
method: body_fld_param #2304
method: body_ext_1part #2329
method: body_ext_mpart #2365
method: body_fld_dsp #2395
method: body_fld_lang #2409
method: body_extensions #2435
method: body_extension #2449
method: section #2464
method: format_string / 1 #2499
method: uid_data #2515
method: text_response #2522
method: flags_response #2532
method: list_response #2539
method: mailbox_list #2546
method: getquota_response #2560
method: getquotaroot_response #2593
method: getacl_response #2610
method: search_response #2637
method: thread_response #2660
method: thread_branch / 1 #2687
method: status_response #2722
method: capability_response #2749
method: resp_text #2767
method: resp_text_code #2780
method: address_list #2811
constant: ADDRESS_REGEXP #2834
method: address #2841
constant: FLAG_REGEXP #2901
method: flag_list #2905
method: nstring #2916
method: astring #2926
method: string #2935
constant: STRING_TOKENS #2945
method: string_token? / 1 #2947
method: case_insensitive_string #2951
method: atom #2961
constant: ATOM_TOKENS #2978
method: atom_token? / 1 #2987
method: number #2991
method: nil_atom #3001
method: match / 1 #3006
method: lookahead #3017
method: shift_token #3024
method: next_token #3028
method: parse_error / 2 #3147
  class: LoginAuthenticator#3163
inherits from
  Object ( Builtin-Module )
has properties
method: process / 1 #3164
constant: STATE_USER #3176
constant: STATE_PASSWORD #3177
method: initialize / 2 #3179
  class: CramMD5Authenticator#3189
inherits from
  Object ( Builtin-Module )
has properties
method: process / 1 #3190
method: initialize / 2 #3197
method: hmac_md5 / 2 #3202
  class: Error#3222
inherits from
  StandardError ( Builtin-Module )
  class: DataFormatError#3226
inherits from
  Error ( Net::IMAP::ResponseParser )
  class: ResponseParseError#3230
inherits from
  Error ( Net::IMAP::ResponseParser )
  class: ResponseError#3235
inherits from
  Error ( Net::IMAP::ResponseParser )
  class: NoResponseError#3240
  class: BadResponseError#3246
  class: ByeResponseError#3252

Code

   1  #
   2  # = net/imap.rb
   3  #
   4  # Copyright (C) 2000  Shugo Maeda <shugo@ruby-lang.org>
   5  #
   6  # This library is distributed under the terms of the Ruby license.
   7  # You can freely distribute/modify this library.
   8  #
   9  # Documentation: Shugo Maeda, with RDoc conversion and overview by William
  10  # Webber.
  11  #
  12  # See Net::IMAP for documentation. 
  13  #
  14 
  15 
  16  require "socket"
  17  require "monitor"
  18  require "digest/md5"
  19  begin
  20    require "openssl"
  21  rescue LoadError
  22  end
  23 
  24  module Net
  25 
  26    #
  27    # Net::IMAP implements Internet Message Access Protocol (IMAP) client
  28    # functionality.  The protocol is described in [IMAP].
  29    #
  30    # == IMAP Overview
  31    #
  32    # An IMAP client connects to a server, and then authenticates
  33    # itself using either #authenticate() or #login().  Having
  34    # authenticated itself, there is a range of commands
  35    # available to it.  Most work with mailboxes, which may be
  36    # arranged in an hierarchical namespace, and each of which
  37    # contains zero or more messages.  How this is implemented on
  38    # the server is implementation-dependent; on a UNIX server, it
  39    # will frequently be implemented as a files in mailbox format
  40    # within a hierarchy of directories.
  41    #
  42    # To work on the messages within a mailbox, the client must
  43    # first select that mailbox, using either #select() or (for
  44    # read-only access) #examine().  Once the client has successfully
  45    # selected a mailbox, they enter _selected_ state, and that
  46    # mailbox becomes the _current_ mailbox, on which mail-item
  47    # related commands implicitly operate.  
  48    #
  49    # Messages have two sorts of identifiers: message sequence
  50    # numbers, and UIDs.  
  51    #
  52    # Message sequence numbers number messages within a mail box 
  53    # from 1 up to the number of items in the mail box.  If new
  54    # message arrives during a session, it receives a sequence
  55    # number equal to the new size of the mail box.  If messages
  56    # are expunged from the mailbox, remaining messages have their
  57    # sequence numbers "shuffled down" to fill the gaps.
  58    #
  59    # UIDs, on the other hand, are permanently guaranteed not to
  60    # identify another message within the same mailbox, even if 
  61    # the existing message is deleted.  UIDs are required to
  62    # be assigned in ascending (but not necessarily sequential)
  63    # order within a mailbox; this means that if a non-IMAP client
  64    # rearranges the order of mailitems within a mailbox, the
  65    # UIDs have to be reassigned.  An IMAP client cannot thus
  66    # rearrange message orders.
  67    #
  68    # == Examples of Usage
  69    #
  70    # === List sender and subject of all recent messages in the default mailbox
  71    #
  72    #   imap = Net::IMAP.new('mail.example.com')
  73    #   imap.authenticate('LOGIN', 'joe_user', 'joes_password')
  74    #   imap.examine('INBOX')
  75    #   imap.search(["RECENT"]).each do |message_id|
  76    #     envelope = imap.fetch(message_id, "ENVELOPE")[0].attr["ENVELOPE"]
  77    #     puts "#{envelope.from[0].name}: \t#{envelope.subject}"
  78    #   end
  79    #
  80    # === Move all messages from April 2003 from "Mail/sent-mail" to "Mail/sent-apr03"
  81    #
  82    #   imap = Net::IMAP.new('mail.example.com')
  83    #   imap.authenticate('LOGIN', 'joe_user', 'joes_password')
  84    #   imap.select('Mail/sent-mail')
  85    #   if not imap.list('Mail/', 'sent-apr03')
  86    #     imap.create('Mail/sent-apr03')
  87    #   end
  88    #   imap.search(["BEFORE", "30-Apr-2003", "SINCE", "1-Apr-2003"]).each do |message_id|
  89    #     imap.copy(message_id, "Mail/sent-apr03")
  90    #     imap.store(message_id, "+FLAGS", [:Deleted])
  91    #   end
  92    #   imap.expunge
  93    # 
  94    # == Thread Safety
  95    #
  96    # Net::IMAP supports concurrent threads. For example,
  97    # 
  98    #   imap = Net::IMAP.new("imap.foo.net", "imap2")
  99    #   imap.authenticate("cram-md5", "bar", "password")
 100    #   imap.select("inbox")
 101    #   fetch_thread = Thread.start { imap.fetch(1..-1, "UID") }
 102    #   search_result = imap.search(["BODY", "hello"])
 103    #   fetch_result = fetch_thread.value
 104    #   imap.disconnect
 105    # 
 106    # This script invokes the FETCH command and the SEARCH command concurrently.
 107    #
 108    # == Errors
 109    #
 110    # An IMAP server can send three different types of responses to indicate
 111    # failure:
 112    #
 113    # NO:: the attempted command could not be successfully completed.  For
 114    #      instance, the username/password used for logging in are incorrect;
 115    #      the selected mailbox does not exists; etc.  
 116    #
 117    # BAD:: the request from the client does not follow the server's 
 118    #       understanding of the IMAP protocol.  This includes attempting
 119    #       commands from the wrong client state; for instance, attempting
 120    #       to perform a SEARCH command without having SELECTed a current
 121    #       mailbox.  It can also signal an internal server
 122    #       failure (such as a disk crash) has occurred.
 123    #
 124    # BYE:: the server is saying goodbye.  This can be part of a normal
 125    #       logout sequence, and can be used as part of a login sequence
 126    #       to indicate that the server is (for some reason) unwilling
 127    #       to accept our connection.  As a response to any other command,
 128    #       it indicates either that the server is shutting down, or that
 129    #       the server is timing out the client connection due to inactivity.
 130    #
 131    # These three error response are represented by the errors
 132    # Net::IMAP::NoResponseError, Net::IMAP::BadResponseError, and
 133    # Net::IMAP::ByeResponseError, all of which are subclasses of
 134    # Net::IMAP::ResponseError.  Essentially, all methods that involve
 135    # sending a request to the server can generate one of these errors.
 136    # Only the most pertinent instances have been documented below.
 137    #
 138    # Because the IMAP class uses Sockets for communication, its methods
 139    # are also susceptible to the various errors that can occur when
 140    # working with sockets.  These are generally represented as
 141    # Errno errors.  For instance, any method that involves sending a
 142    # request to the server and/or receiving a response from it could
 143    # raise an Errno::EPIPE error if the network connection unexpectedly
 144    # goes down.  See the socket(7), ip(7), tcp(7), socket(2), connect(2),
 145    # and associated man pages.
 146    #
 147    # Finally, a Net::IMAP::DataFormatError is thrown if low-level data
 148    # is found to be in an incorrect format (for instance, when converting
 149    # between UTF-8 and UTF-16), and Net::IMAP::ResponseParseError is 
 150    # thrown if a server response is non-parseable. 
 151    #
 152    #
 153    # == References
 154    #
 155    # [[IMAP]]
 156    #    M. Crispin, "INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1",
 157    #    RFC 2060, December 1996.  (Note: since obsoleted by RFC 3501)
 158    #
 159    # [[LANGUAGE-TAGS]]
 160    #    Alvestrand, H., "Tags for the Identification of
 161    #    Languages", RFC 1766, March 1995.
 162    #
 163    # [[MD5]]
 164    #    Myers, J., and M. Rose, "The Content-MD5 Header Field", RFC
 165    #    1864, October 1995.
 166    #
 167    # [[MIME-IMB]]
 168    #    Freed, N., and N. Borenstein, "MIME (Multipurpose Internet
 169    #    Mail Extensions) Part One: Format of Internet Message Bodies", RFC
 170    #    2045, November 1996.
 171    #
 172    # [[RFC-822]]
 173    #    Crocker, D., "Standard for the Format of ARPA Internet Text
 174    #    Messages", STD 11, RFC 822, University of Delaware, August 1982.
 175    #
 176    # [[RFC-2087]]
 177    #    Myers, J., "IMAP4 QUOTA extension", RFC 2087, January 1997.
 178    #
 179    # [[RFC-2086]]
 180    #    Myers, J., "IMAP4 ACL extension", RFC 2086, January 1997.
 181    #
 182    # [[RFC-2195]]
 183    #    Klensin, J., Catoe, R., and Krumviede, P., "IMAP/POP AUTHorize Extension
 184    #    for Simple Challenge/Response", RFC 2195, September 1997.
 185    #
 186    # [[SORT-THREAD-EXT]]
 187    #    Crispin, M., "INTERNET MESSAGE ACCESS PROTOCOL - SORT and THREAD
 188    #    Extensions", draft-ietf-imapext-sort, May 2003.
 189    #
 190    # [[OSSL]]
 191    #    http://www.openssl.org
 192    #
 193    # [[RSSL]]
 194    #    http://savannah.gnu.org/projects/rubypki
 195    #
 196    # [[UTF7]]
 197    #    Goldsmith, D. and Davis, M., "UTF-7: A Mail-Safe Transformation Format of
 198    #    Unicode", RFC 2152, May 1997.
 199    #
 200    class IMAP
 201      include MonitorMixin
 202      if defined?(OpenSSL)
 203        include OpenSSL
 204        include SSL
 205      end
 206 
 207      #  Returns an initial greeting response from the server.
 208      attr_reader :greeting
 209 
 210      # Returns recorded untagged responses.  For example:
 211      #
 212      #   imap.select("inbox")
 213      #   p imap.responses["EXISTS"][-1]
 214      #   #=> 2
 215      #   p imap.responses["UIDVALIDITY"][-1]
 216      #   #=> 968263756
 217      attr_reader :responses
 218 
 219      # Returns all response handlers.
 220      attr_reader :response_handlers
 221 
 222      # The thread to receive exceptions.
 223      attr_accessor :client_thread
 224 
 225      # Flag indicating a message has been seen
 226      SEEN = :Seen
 227 
 228      # Flag indicating a message has been answered
 229      ANSWERED = :Answered
 230 
 231      # Flag indicating a message has been flagged for special or urgent
 232      # attention
 233      FLAGGED = :Flagged
 234 
 235      # Flag indicating a message has been marked for deletion.  This
 236      # will occur when the mailbox is closed or expunged.
 237      DELETED = :Deleted
 238 
 239      # Flag indicating a message is only a draft or work-in-progress version.
 240      DRAFT = :Draft
 241 
 242      # Flag indicating that the message is "recent", meaning that this
 243      # session is the first session in which the client has been notified
 244      # of this message.
 245      RECENT = :Recent
 246 
 247      # Flag indicating that a mailbox context name cannot contain
 248      # children.
 249      NOINFERIORS = :Noinferiors
 250 
 251      # Flag indicating that a mailbox is not selected.
 252      NOSELECT = :Noselect
 253 
 254      # Flag indicating that a mailbox has been marked "interesting" by
 255      # the server; this commonly indicates that the mailbox contains
 256      # new messages.
 257      MARKED = :Marked
 258 
 259      # Flag indicating that the mailbox does not contains new messages.
 260      UNMARKED = :Unmarked
 261 
 262      # Returns the debug mode.
 263      def self.debug
 264        return @@debug
 265      end
 266 
 267      # Sets the debug mode.
 268      def self.debug=(val)
 269        return @@debug = val
 270      end
 271 
 272      # Adds an authenticator for Net::IMAP#authenticate.  +auth_type+
 273      # is the type of authentication this authenticator supports
 274      # (for instance, "LOGIN").  The +authenticator+ is an object
 275      # which defines a process() method to handle authentication with
 276      # the server.  See Net::IMAP::LoginAuthenticator and 
 277      # Net::IMAP::CramMD5Authenticator for examples.
 278      #
 279      # If +auth_type+ refers to an existing authenticator, it will be
 280      # replaced by the new one.
 281      def self.add_authenticator(auth_type, authenticator)
 282        @@authenticators[auth_type] = authenticator
 283      end
 284 
 285      # Disconnects from the server.
 286      def disconnect
 287        begin
 288          # try to call SSL::SSLSocket#io.
 289          @sock.io.shutdown
 290        rescue NoMethodError
 291          # @sock is not an SSL::SSLSocket.
 292          @sock.shutdown
 293        end
 294        @receiver_thread.join
 295        @sock.close
 296      end
 297 
 298      # Returns true if disconnected from the server.
 299      def disconnected?
 300        return @sock.closed?
 301      end
 302 
 303      # Sends a CAPABILITY command, and returns an array of
 304      # capabilities that the server supports.  Each capability
 305      # is a string.  See [IMAP] for a list of possible
 306      # capabilities.
 307      #
 308      # Note that the Net::IMAP class does not modify its
 309      # behaviour according to the capabilities of the server;
 310      # it is up to the user of the class to ensure that 
 311      # a certain capability is supported by a server before
 312      # using it.
 313      def capability
 314        synchronize do
 315          send_command("CAPABILITY")
 316          return @responses.delete("CAPABILITY")[-1]
 317        end
 318      end
 319 
 320      # Sends a NOOP command to the server. It does nothing.
 321      def noop
 322        send_command("NOOP")
 323      end
 324 
 325      # Sends a LOGOUT command to inform the server that the client is
 326      # done with the connection.
 327      def logout
 328        send_command("LOGOUT")
 329      end
 330 
 331      # Sends an AUTHENTICATE command to authenticate the client.
 332      # The +auth_type+ parameter is a string that represents
 333      # the authentication mechanism to be used. Currently Net::IMAP
 334      # supports authentication mechanisms:
 335      #
 336      #   LOGIN:: login using cleartext user and password. 
 337      #   CRAM-MD5:: login with cleartext user and encrypted password
 338      #              (see [RFC-2195] for a full description).  This
 339      #              mechanism requires that the server have the user's
 340      #              password stored in clear-text password.
 341      #
 342      # For both these mechanisms, there should be two +args+: username
 343      # and (cleartext) password.  A server may not support one or other
 344      # of these mechanisms; check #capability() for a capability of
 345      # the form "AUTH=LOGIN" or "AUTH=CRAM-MD5".
 346      #
 347      # Authentication is done using the appropriate authenticator object:
 348      # see @@authenticators for more information on plugging in your own
 349      # authenticator.
 350      #
 351      # For example:
 352      #
 353      #    imap.authenticate('LOGIN', user, password)
 354      #
 355      # A Net::IMAP::NoResponseError is raised if authentication fails.
 356      def authenticate(auth_type, *args)
 357        auth_type = auth_type.upcase
 358        unless @@authenticators.has_key?(auth_type)
 359          raise ArgumentError,
 360            format('unknown auth type - "%s"', auth_type)
 361        end
 362        authenticator = @@authenticators[auth_type].new(*args)
 363        send_command("AUTHENTICATE", auth_type) do |resp|
 364          if resp.instance_of?(ContinuationRequest)
 365            data = authenticator.process(resp.data.text.unpack("m")[0])
 366            s = [data].pack("m").gsub(/\n/, "")
 367            send_string_data(s)
 368            put_string(CRLF)
 369          end
 370        end
 371      end
 372 
 373      # Sends a LOGIN command to identify the client and carries
 374      # the plaintext +password+ authenticating this +user+.  Note
 375      # that, unlike calling #authenticate() with an +auth_type+
 376      # of "LOGIN", #login() does *not* use the login authenticator.
 377      #
 378      # A Net::IMAP::NoResponseError is raised if authentication fails.
 379      def login(user, password)
 380        send_command("LOGIN", user, password)
 381      end
 382 
 383      # Sends a SELECT command to select a +mailbox+ so that messages
 384      # in the +mailbox+ can be accessed. 
 385      #
 386      # After you have selected a mailbox, you may retrieve the
 387      # number of items in that mailbox from @responses["EXISTS"][-1],
 388      # and the number of recent messages from @responses["RECENT"][-1].
 389      # Note that these values can change if new messages arrive
 390      # during a session; see #add_response_handler() for a way of
 391      # detecting this event.
 392      #
 393      # A Net::IMAP::NoResponseError is raised if the mailbox does not
 394      # exist or is for some reason non-selectable.
 395      def select(mailbox)
 396        synchronize do
 397          @responses.clear
 398          send_command("SELECT", mailbox)
 399        end
 400      end
 401 
 402      # Sends a EXAMINE command to select a +mailbox+ so that messages
 403      # in the +mailbox+ can be accessed.  Behaves the same as #select(),
 404      # except that the selected +mailbox+ is identified as read-only.
 405      #
 406      # A Net::IMAP::NoResponseError is raised if the mailbox does not
 407      # exist or is for some reason non-examinable.
 408      def examine(mailbox)
 409        synchronize do
 410          @responses.clear
 411          send_command("EXAMINE", mailbox)
 412        end
 413      end
 414 
 415      # Sends a CREATE command to create a new +mailbox+.
 416      #
 417      # A Net::IMAP::NoResponseError is raised if a mailbox with that name
 418      # cannot be created.
 419      def create(mailbox)
 420        send_command("CREATE", mailbox)
 421      end
 422 
 423      # Sends a DELETE command to remove the +mailbox+.
 424      #
 425      # A Net::IMAP::NoResponseError is raised if a mailbox with that name
 426      # cannot be deleted, either because it does not exist or because the
 427      # client does not have permission to delete it.
 428      def delete(mailbox)
 429        send_command("DELETE", mailbox)
 430      end
 431 
 432      # Sends a RENAME command to change the name of the +mailbox+ to
 433      # +newname+.
 434      #
 435      # A Net::IMAP::NoResponseError is raised if a mailbox with the 
 436      # name +mailbox+ cannot be renamed to +newname+ for whatever
 437      # reason; for instance, because +mailbox+ does not exist, or
 438      # because there is already a mailbox with the name +newname+.
 439      def rename(mailbox, newname)
 440        send_command("RENAME", mailbox, newname)
 441      end
 442 
 443      # Sends a SUBSCRIBE command to add the specified +mailbox+ name to
 444      # the server's set of "active" or "subscribed" mailboxes as returned
 445      # by #lsub().
 446      #
 447      # A Net::IMAP::NoResponseError is raised if +mailbox+ cannot be
 448      # subscribed to, for instance because it does not exist.
 449      def subscribe(mailbox)
 450        send_command("SUBSCRIBE", mailbox)
 451      end
 452 
 453      # Sends a UNSUBSCRIBE command to remove the specified +mailbox+ name
 454      # from the server's set of "active" or "subscribed" mailboxes.
 455      #
 456      # A Net::IMAP::NoResponseError is raised if +mailbox+ cannot be
 457      # unsubscribed from, for instance because the client is not currently
 458      # subscribed to it.
 459      def unsubscribe(mailbox)
 460        send_command("UNSUBSCRIBE", mailbox)
 461      end
 462 
 463      # Sends a LIST command, and returns a subset of names from
 464      # the complete set of all names available to the client.
 465      # +refname+ provides a context (for instance, a base directory
 466      # in a directory-based mailbox hierarchy).  +mailbox+ specifies
 467      # a mailbox or (via wildcards) mailboxes under that context.
 468      # Two wildcards may be used in +mailbox+: '*', which matches
 469      # all characters *including* the hierarchy delimiter (for instance,
 470      # '/' on a UNIX-hosted directory-based mailbox hierarchy); and '%',
 471      # which matches all characters *except* the hierarchy delimiter.
 472      #
 473      # If +refname+ is empty, +mailbox+ is used directly to determine
 474      # which mailboxes to match.  If +mailbox+ is empty, the root
 475      # name of +refname+ and the hierarchy delimiter are returned.
 476      #
 477      # The return value is an array of +Net::IMAP::MailboxList+. For example:
 478      #
 479      #   imap.create("foo/bar")
 480      #   imap.create("foo/baz")
 481      #   p imap.list("", "foo/%")
 482      #   #=> [#<Net::IMAP::MailboxList attr=[:Noselect], delim="/", name="foo/">, \\ 
 483      #        #<Net::IMAP::MailboxList attr=[:Noinferiors, :Marked], delim="/", name="foo/bar">, \\ 
 484      #        #<Net::IMAP::MailboxList attr=[:Noinferiors], delim="/", name="foo/baz">]
 485      def list(refname, mailbox)
 486        synchronize do
 487          send_command("LIST", refname, mailbox)
 488          return @responses.delete("LIST")
 489        end
 490      end
 491 
 492      # Sends the GETQUOTAROOT command along with specified +mailbox+.
 493      # This command is generally available to both admin and user.
 494      # If mailbox exists, returns an array containing objects of
 495      # Net::IMAP::MailboxQuotaRoot and Net::IMAP::MailboxQuota.
 496      def getquotaroot(mailbox)
 497        synchronize do
 498          send_command("GETQUOTAROOT", mailbox)
 499          result = []
 500          result.concat(@responses.delete("QUOTAROOT"))
 501          result.concat(@responses.delete("QUOTA"))
 502          return result
 503        end
 504      end
 505 
 506      # Sends the GETQUOTA command along with specified +mailbox+.
 507      # If this mailbox exists, then an array containing a
 508      # Net::IMAP::MailboxQuota object is returned.  This
 509      # command generally is only available to server admin.
 510      def getquota(mailbox)
 511        synchronize do
 512          send_command("GETQUOTA", mailbox)
 513          return @responses.delete("QUOTA")
 514        end
 515      end
 516 
 517      # Sends a SETQUOTA command along with the specified +mailbox+ and
 518      # +quota+.  If +quota+ is nil, then quota will be unset for that
 519      # mailbox.  Typically one needs to be logged in as server admin
 520      # for this to work.  The IMAP quota commands are described in
 521      # [RFC-2087].
 522      def setquota(mailbox, quota)
 523        if quota.nil?
 524          data = '()'
 525        else
 526          data = '(STORAGE ' + quota.to_s + ')'
 527        end
 528        send_command("SETQUOTA", mailbox, RawData.new(data))
 529      end
 530 
 531      # Sends the SETACL command along with +mailbox+, +user+ and the
 532      # +rights+ that user is to have on that mailbox.  If +rights+ is nil,
 533      # then that user will be stripped of any rights to that mailbox.
 534      # The IMAP ACL commands are described in [RFC-2086].
 535      def setacl(mailbox, user, rights)
 536        if rights.nil? 
 537          send_command("SETACL", mailbox, user, "")
 538        else
 539          send_command("SETACL", mailbox, user, rights)
 540        end
 541      end
 542 
 543      # Send the GETACL command along with specified +mailbox+.
 544      # If this mailbox exists, an array containing objects of
 545      # Net::IMAP::MailboxACLItem will be returned.
 546      def getacl(mailbox)
 547        synchronize do
 548          send_command("GETACL", mailbox)
 549          return @responses.delete("ACL")[-1]
 550        end
 551      end
 552 
 553      # Sends a LSUB command, and returns a subset of names from the set
 554      # of names that the user has declared as being "active" or
 555      # "subscribed".  +refname+ and +mailbox+ are interpreted as 
 556      # for #list().
 557      # The return value is an array of +Net::IMAP::MailboxList+.
 558      def lsub(refname, mailbox)
 559        synchronize do
 560          send_command("LSUB", refname, mailbox)
 561          return @responses.delete("LSUB")
 562        end
 563      end
 564 
 565      # Sends a STATUS command, and returns the status of the indicated
 566      # +mailbox+. +attr+ is a list of one or more attributes that
 567      # we are request the status of.  Supported attributes include:
 568      #
 569      #   MESSAGES:: the number of messages in the mailbox.
 570      #   RECENT:: the number of recent messages in the mailbox.
 571      #   UNSEEN:: the number of unseen messages in the mailbox.
 572      #
 573      # The return value is a hash of attributes. For example:
 574      #
 575      #   p imap.status("inbox", ["MESSAGES", "RECENT"])
 576      #   #=> {"RECENT"=>0, "MESSAGES"=>44}
 577      #
 578      # A Net::IMAP::NoResponseError is raised if status values 
 579      # for +mailbox+ cannot be returned, for instance because it
 580      # does not exist.
 581      def status(mailbox, attr)
 582        synchronize do
 583          send_command("STATUS", mailbox, attr)
 584          return @responses.delete("STATUS")[-1].attr
 585        end
 586      end
 587 
 588      # Sends a APPEND command to append the +message+ to the end of
 589      # the +mailbox+. The optional +flags+ argument is an array of 
 590      # flags to initially passing to the new message.  The optional
 591      # +date_time+ argument specifies the creation time to assign to the 
 592      # new message; it defaults to the current time.
 593      # For example:
 594      #
 595      #   imap.append("inbox", <<EOF.gsub(/\n/, "\r\n"), [:Seen], Time.now)
 596      #   Subject: hello
 597      #   From: shugo@ruby-lang.org
 598      #   To: shugo@ruby-lang.org
 599      #   
 600      #   hello world
 601      #   EOF
 602      #
 603      # A Net::IMAP::NoResponseError is raised if the mailbox does
 604      # not exist (it is not created automatically), or if the flags,
 605      # date_time, or message arguments contain errors.
 606      def append(mailbox, message, flags = nil, date_time = nil)
 607        args = []
 608        if flags
 609          args.push(flags)
 610        end
 611        args.push(date_time) if date_time
 612        args.push(Literal.new(message))
 613        send_command("APPEND", mailbox, *args)
 614      end
 615 
 616      # Sends a CHECK command to request a checkpoint of the currently
 617      # selected mailbox.  This performs implementation-specific
 618      # housekeeping, for instance, reconciling the mailbox's 
 619      # in-memory and on-disk state.
 620      def check
 621        send_command("CHECK")
 622      end
 623 
 624      # Sends a CLOSE command to close the currently selected mailbox.
 625      # The CLOSE command permanently removes from the mailbox all
 626      # messages that have the \Deleted flag set.
 627      def close
 628        send_command("CLOSE")
 629      end
 630 
 631      # Sends a EXPUNGE command to permanently remove from the currently
 632      # selected mailbox all messages that have the \Deleted flag set.
 633      def expunge
 634        synchronize do
 635          send_command("EXPUNGE")
 636          return @responses.delete("EXPUNGE")
 637        end
 638      end
 639 
 640      # Sends a SEARCH command to search the mailbox for messages that
 641      # match the given searching criteria, and returns message sequence
 642      # numbers.  +keys+ can either be a string holding the entire 
 643      # search string, or a single-dimension array of search keywords and 
 644      # arguments.  The following are some common search criteria;
 645      # see [IMAP] section 6.4.4 for a full list.
 646      #
 647      # <message set>:: a set of message sequence numbers.  ',' indicates
 648      #                 an interval, ':' indicates a range.  For instance,
 649      #                 '2,10:12,15' means "2,10,11,12,15".
 650      #
 651      # BEFORE <date>:: messages with an internal date strictly before
 652      #                 <date>.  The date argument has a format similar
 653      #                 to 8-Aug-2002.
 654      #
 655      # BODY <string>:: messages that contain <string> within their body.
 656      #
 657      # CC <string>:: messages containing <string> in their CC field.
 658      #
 659      # FROM <string>:: messages that contain <string> in their FROM field.
 660      #
 661      # NEW:: messages with the \Recent, but not the \Seen, flag set.
 662      #
 663      # NOT <search-key>:: negate the following search key.
 664      #
 665      # OR <search-key> <search-key>:: "or" two search keys together.
 666      #
 667      # ON <date>:: messages with an internal date exactly equal to <date>, 
 668      #             which has a format similar to 8-Aug-2002.
 669      #
 670      # SINCE <date>:: messages with an internal date on or after <date>.
 671      #
 672      # SUBJECT <string>:: messages with <string> in their subject.
 673      #
 674      # TO <string>:: messages with <string> in their TO field.
 675      # 
 676      # For example:
 677      #
 678      #   p imap.search(["SUBJECT", "hello", "NOT", "NEW"])
 679      #   #=> [1, 6, 7, 8]
 680      def search(keys, charset = nil)
 681        return search_internal("SEARCH", keys, charset)
 682      end
 683 
 684      # As for #search(), but returns unique identifiers.
 685      def uid_search(keys, charset = nil)
 686        return search_internal("UID SEARCH", keys, charset)
 687      end
 688 
 689      # Sends a FETCH command to retrieve data associated with a message
 690      # in the mailbox. The +set+ parameter is a number or an array of
 691      # numbers or a Range object. The number is a message sequence
 692      # number.  +attr+ is a list of attributes to fetch; see the
 693      # documentation for Net::IMAP::FetchData for a list of valid
 694      # attributes.
 695      # The return value is an array of Net::IMAP::FetchData. For example:
 696      #
 697      #   p imap.fetch(6..8, "UID")
 698      #   #=> [#<Net::IMAP::FetchData seqno=6, attr={"UID"=>98}>, \\ 
 699      #        #<Net::IMAP::FetchData seqno=7, attr={"UID"=>99}>, \\ 
 700      #        #<Net::IMAP::FetchData seqno=8, attr={"UID"=>100}>]
 701      #   p imap.fetch(6, "BODY[HEADER.FIELDS (SUBJECT)]")
 702      #   #=> [#<Net::IMAP::FetchData seqno=6, attr={"BODY[HEADER.FIELDS (SUBJECT)]"=>"Subject: test\r\n\r\n"}>]
 703      #   data = imap.uid_fetch(98, ["RFC822.SIZE", "INTERNALDATE"])[0]
 704      #   p data.seqno
 705      #   #=> 6
 706      #   p data.attr["RFC822.SIZE"]
 707      #   #=> 611
 708      #   p data.attr["INTERNALDATE"]
 709      #   #=> "12-Oct-2000 22:40:59 +0900"
 710      #   p data.attr["UID"]
 711      #   #=> 98
 712      def fetch(set, attr)
 713        return fetch_internal("FETCH", set, attr)
 714      end
 715 
 716      # As for #fetch(), but +set+ contains unique identifiers.
 717      def uid_fetch(set, attr)
 718        return fetch_internal("UID FETCH", set, attr)
 719      end
 720 
 721      # Sends a STORE command to alter data associated with messages
 722      # in the mailbox, in particular their flags. The +set+ parameter 
 723      # is a number or an array of numbers or a Range object. Each number 
 724      # is a message sequence number.  +attr+ is the name of a data item 
 725      # to store: 'FLAGS' means to replace the message's flag list
 726      # with the provided one; '+FLAGS' means to add the provided flags;
 727      # and '-FLAGS' means to remove them.  +flags+ is a list of flags.
 728      #
 729      # The return value is an array of Net::IMAP::FetchData. For example:
 730      #
 731      #   p imap.store(6..8, "+FLAGS", [:Deleted])
 732      #   #=> [#<Net::IMAP::FetchData seqno=6, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\ 
 733      #        #<Net::IMAP::FetchData seqno=7, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\  
 734      #        #<Net::IMAP::FetchData seqno=8, attr={"FLAGS"=>[:Seen, :Deleted]}>]
 735      def store(set, attr, flags)
 736        return store_internal("STORE", set, attr, flags)
 737      end
 738 
 739      # As for #store(), but +set+ contains unique identifiers.
 740      def uid_store(set, attr, flags)
 741        return store_internal("UID STORE", set, attr, flags)
 742      end
 743 
 744      # Sends a COPY command to copy the specified message(s) to the end
 745      # of the specified destination +mailbox+. The +set+ parameter is
 746      # a number or an array of numbers or a Range object. The number is
 747      # a message sequence number.
 748      def copy(set, mailbox)
 749        copy_internal("COPY", set, mailbox)
 750      end
 751 
 752      # As for #copy(), but +set+ contains unique identifiers.
 753      def uid_copy(set, mailbox)
 754        copy_internal("UID COPY", set, mailbox)
 755      end
 756 
 757      # Sends a SORT command to sort messages in the mailbox.
 758      # Returns an array of message sequence numbers. For example:
 759      #
 760      #   p imap.sort(["FROM"], ["ALL"], "US-ASCII")
 761      #   #=> [1, 2, 3, 5, 6, 7, 8, 4, 9]
 762      #   p imap.sort(["DATE"], ["SUBJECT", "hello"], "US-ASCII")
 763      #   #=> [6, 7, 8, 1]
 764      #
 765      # See [SORT-THREAD-EXT] for more details.
 766      def sort(sort_keys, search_keys, charset)
 767        return sort_internal("SORT", sort_keys, search_keys, charset)
 768      end
 769 
 770      # As for #sort(), but returns an array of unique identifiers.
 771      def uid_sort(sort_keys, search_keys, charset)
 772        return sort_internal("UID SORT", sort_keys, search_keys, charset)
 773      end
 774 
 775      # Adds a response handler. For example, to detect when 
 776      # the server sends us a new EXISTS response (which normally
 777      # indicates new messages being added to the mail box), 
 778      # you could add the following handler after selecting the
 779      # mailbox.
 780      #
 781      #   imap.add_response_handler { |resp|
 782      #     if resp.kind_of?(Net::IMAP::UntaggedResponse) and resp.name == "EXISTS"
 783      #       puts "Mailbox now has #{resp.data} messages"
 784      #     end
 785      #   }
 786      #
 787      def add_response_handler(handler = Proc.new)
 788        @response_handlers.push(handler)
 789      end
 790 
 791      # Removes the response handler.
 792      def remove_response_handler(handler)
 793        @response_handlers.delete(handler)
 794      end
 795 
 796      # As for #search(), but returns message sequence numbers in threaded
 797      # format, as a Net::IMAP::ThreadMember tree.  The supported algorithms
 798      # are:
 799      #
 800      # ORDEREDSUBJECT:: split into single-level threads according to subject,
 801      #                  ordered by date.
 802      # REFERENCES:: split into threads by parent/child relationships determined
 803      #              by which message is a reply to which.
 804      #
 805      # Unlike #search(), +charset+ is a required argument.  US-ASCII
 806      # and UTF-8 are sample values.
 807      #
 808      # See [SORT-THREAD-EXT] for more details.
 809      def thread(algorithm, search_keys, charset)
 810        return thread_internal("THREAD", algorithm, search_keys, charset)
 811      end
 812 
 813      # As for #thread(), but returns unique identifiers instead of 
 814      # message sequence numbers.
 815      def uid_thread(algorithm, search_keys, charset)
 816        return thread_internal("UID THREAD", algorithm, search_keys, charset)
 817      end
 818 
 819      # Decode a string from modified UTF-7 format to UTF-8.
 820      #
 821      # UTF-7 is a 7-bit encoding of Unicode [UTF7].  IMAP uses a
 822      # slightly modified version of this to encode mailbox names
 823      # containing non-ASCII characters; see [IMAP] section 5.1.3.
 824      #
 825      # Net::IMAP does _not_ automatically encode and decode
 826      # mailbox names to and from utf7.
 827      def self.decode_utf7(s)
 828        return s.gsub(/&(.*?)-/n) {
 829          if $1.empty?
 830            "&"
 831          else
 832            base64 = $1.tr(",", "/")
 833            x = base64.length % 4
 834            if x > 0
 835              base64.concat("=" * (4 - x))
 836            end
 837            u16tou8(base64.unpack("m")[0])
 838          end
 839        }
 840      end
 841 
 842      # Encode a string from UTF-8 format to modified UTF-7.
 843      def self.encode_utf7(s)
 844        return s.gsub(/(&)|([^\x20-\x7e]+)/u) { |x|
 845          if $1
 846            "&-"
 847          else
 848            base64 = [u8tou16(x)].pack("m")
 849            "&" + base64.delete("=\n").tr("/", ",") + "-"
 850          end
 851        }
 852      end
 853 
 854      private
 855 
 856      CRLF = "\r\n"      # :nodoc:
 857      PORT = 143         # :nodoc:
 858 
 859      @@debug = false
 860      @@authenticators = {}
 861 
 862      # Creates a new Net::IMAP object and connects it to the specified
 863      # +port+ (143 by default) on the named +host+.  If +usessl+ is true, 
 864      # then an attempt will
 865      # be made to use SSL (now TLS) to connect to the server.  For this
 866      # to work OpenSSL [OSSL] and the Ruby OpenSSL [RSSL]
 867      # extensions need to be installed.  The +certs+ parameter indicates
 868      # the path or file containing the CA cert of the server, and the
 869      # +verify+ parameter is for the OpenSSL verification callback.
 870      #
 871      # The most common errors are:
 872      #
 873      # Errno::ECONNREFUSED:: connection refused by +host+ or an intervening
 874      #                       firewall.
 875      # Errno::ETIMEDOUT:: connection timed out (possibly due to packets
 876      #                    being dropped by an intervening firewall).
 877      # Errno::ENETUNREACH:: there is no route to that network.
 878      # SocketError:: hostname not known or other socket error.
 879      # Net::IMAP::ByeResponseError:: we connected to the host, but they 
 880      #                               immediately said goodbye to us.
 881      def initialize(host, port = PORT, usessl = false, certs = nil, verify = false)
 882        super()
 883        @host = host
 884        @port = port
 885        @tag_prefix = "RUBY"
 886        @tagno = 0
 887        @parser = ResponseParser.new
 888        @sock = TCPSocket.open(host, port)
 889        if usessl
 890          unless defined?(OpenSSL)
 891            raise "SSL extension not installed"
 892          end
 893          @usessl = true
 894 
 895          # verify the server.
 896          context = SSLContext::new()
 897          context.ca_file = certs if certs && FileTest::file?(certs)
 898          context.ca_path = certs if certs && FileTest::directory?(certs)
 899          context.verify_mode = VERIFY_PEER if verify
 900          if defined?(VerifyCallbackProc)
 901            context.verify_callback = VerifyCallbackProc 
 902          end
 903          @sock = SSLSocket.new(@sock, context)
 904          @sock.sync_close = true
 905          @sock.connect   # start ssl session.
 906          @sock.post_connection_check(@host) if verify
 907        else
 908          @usessl = false
 909        end
 910        @responses = Hash.new([].freeze)
 911        @tagged_responses = {}
 912        @response_handlers = []
 913        @response_arrival = new_cond
 914        @continuation_request = nil
 915        @logout_command_tag = nil
 916        @debug_output_bol = true
 917        @exception = nil
 918 
 919        @greeting = get_response
 920        if @greeting.name == "BYE"
 921          @sock.close
 922          raise ByeResponseError, @greeting.raw_data
 923        end
 924 
 925        @client_thread = Thread.current
 926        @receiver_thread = Thread.start {
 927          receive_responses
 928        }
 929      end
 930 
 931      def receive_responses
 932        while true
 933          synchronize do
 934            @exception = nil
 935          end
 936          begin
 937            resp = get_response
 938          rescue Exception => e
 939            synchronize do
 940              @sock.close unless @sock.closed?
 941              @exception = e
 942            end
 943            break
 944          end
 945          unless resp
 946            synchronize do
 947              @exception = EOFError.new("end of file reached")
 948            end
 949            break
 950          end
 951          begin
 952            synchronize do
 953              case resp
 954              when TaggedResponse
 955                @tagged_responses[resp.tag] = resp
 956                @response_arrival.broadcast
 957                if resp.tag == @logout_command_tag
 958                  return
 959                end
 960              when UntaggedResponse
 961                record_response(resp.name, resp.data)
 962                if resp.data.instance_of?(ResponseText) &&
 963                    (code = resp.data.code)
 964                  record_response(code.name, code.data)
 965                end
 966                if resp.name == "BYE" && @logout_command_tag.nil?
 967                  @sock.close
 968                  @exception = ByeResponseError.new(resp.raw_data)
 969                  @response_arrival.broadcast
 970                  return
 971                end
 972              when ContinuationRequest
 973                @continuation_request = resp
 974                @response_arrival.broadcast
 975              end
 976              @response_handlers.each do |handler|
 977                handler.call(resp)
 978              end
 979            end
 980          rescue Exception => e
 981            @exception = e
 982            synchronize do
 983              @response_arrival.broadcast
 984            end
 985          end
 986        end
 987        synchronize do
 988          @response_arrival.broadcast
 989        end
 990      end
 991 
 992      def get_tagged_response(tag)
 993        until @tagged_responses.key?(tag)
 994          raise @exception if @exception
 995          @response_arrival.wait
 996        end
 997        return pick_up_tagged_response(tag)
 998      end
 999 
1000      def pick_up_tagged_response(tag)
1001        resp = @tagged_responses.delete(tag)
1002        case resp.name
1003        when /\A(?:NO)\z/ni
1004          raise NoResponseError, resp.data.text
1005        when /\A(?:BAD)\z/ni
1006          raise BadResponseError, resp.data.text
1007        else
1008          return resp
1009        end
1010      end
1011 
1012      def get_response
1013        buff = ""
1014        while true
1015          s = @sock.gets(CRLF)
1016          break unless s
1017          buff.concat(s)
1018          if /\{(\d+)\}\r\n/n =~ s
1019            s = @sock.read($1.to_i)
1020            buff.concat(s)
1021          else
1022            break
1023          end
1024        end
1025        return nil if buff.length == 0
1026        if @@debug
1027          $stderr.print(buff.gsub(/^/n, "S: "))
1028        end
1029        return @parser.parse(buff)
1030      end
1031 
1032      def record_response(name, data)
1033        unless @responses.has_key?(name)
1034          @responses[name] = []
1035        end
1036        @responses[name].push(data)
1037      end
1038 
1039      def send_command(cmd, *args, &block)
1040        synchronize do
1041          tag = Thread.current[:net_imap_tag] = generate_tag
1042          put_string(tag + " " + cmd)
1043          args.each do |i|
1044            put_string(" ")
1045            send_data(i)
1046          end
1047          put_string(CRLF)
1048          if cmd == "LOGOUT"
1049            @logout_command_tag = tag
1050          end
1051          if block
1052            add_response_handler(block)
1053          end
1054          begin
1055            return get_tagged_response(tag)
1056          ensure
1057            if block
1058              remove_response_handler(block)
1059            end
1060          end
1061        end
1062      end
1063 
1064      def generate_tag
1065        @tagno += 1
1066        return format("%s%04d", @tag_prefix, @tagno)
1067      end
1068      
1069      def put_string(str)
1070        @sock.print(str)
1071        if @@debug
1072          if @debug_output_bol
1073            $stderr.print("C: ")
1074          end
1075          $stderr.print(str.gsub(/\n(?!\z)/n, "\nC: "))
1076          if /\r\n\z/n.match(str)
1077            @debug_output_bol = true
1078          else
1079            @debug_output_bol = false
1080          end
1081        end
1082      end
1083 
1084      def send_data(data)
1085        case data
1086        when nil
1087          put_string("NIL")
1088        when String
1089          send_string_data(data)
1090        when Integer
1091          send_number_data(data)
1092        when Array
1093          send_list_data(data)
1094        when Time
1095          send_time_data(data)
1096        when Symbol
1097          send_symbol_data(data)
1098        else
1099          data.send_data(self)
1100        end
1101      end
1102 
1103      def send_string_data(str)
1104        case str
1105        when ""
1106          put_string('""')
1107        when /[\x80-\xff\r\n]/n
1108          # literal
1109          send_literal(str)
1110        when /[(){ \x00-\x1f\x7f%*"\\]/n
1111          # quoted string
1112          send_quoted_string(str)
1113        else
1114          put_string(str)
1115        end
1116      end
1117      
1118      def send_quoted_string(str)
1119        put_string('"' + str.gsub(/["\\]/n, "\\\\\\&") + '"')
1120      end
1121 
1122      def send_literal(str)
1123        put_string("{" + str.length.to_s + "}" + CRLF)
1124        while @continuation_request.nil? &&
1125          !@tagged_responses.key?(Thread.current[:net_imap_tag])
1126          @response_arrival.wait
1127          raise @exception if @exception
1128        end
1129        if @continuation_request.nil?
1130          pick_up_tagged_response(Thread.current[:net_imap_tag])
1131          raise ResponseError.new("expected continuation request")
1132        end
1133        @continuation_request = nil
1134        put_string(str)
1135      end
1136 
1137      def send_number_data(num)
1138        if num < 0 || num >= 4294967296
1139          raise DataFormatError, num.to_s
1140        end
1141        put_string(num.to_s)
1142      end
1143 
1144      def send_list_data(list)
1145        put_string("(")
1146        first = true
1147        list.each do |i|
1148          if first
1149            first = false
1150          else
1151            put_string(" ")
1152          end
1153          send_data(i)
1154        end
1155        put_string(")")
1156      end
1157 
1158      DATE_MONTH = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)
1159 
1160      def send_time_data(time)
1161        t = time.dup.gmtime
1162        s = format('"%2d-%3s-%4d %02d:%02d:%02d +0000"',
1163                   t.day, DATE_MONTH[t.month - 1], t.year,
1164                   t.hour, t.min, t.sec)
1165        put_string(s)
1166      end
1167 
1168      def send_symbol_data(symbol)
1169        put_string("\\" + symbol.to_s)
1170      end
1171 
1172      def search_internal(cmd, keys, charset)
1173        if keys.instance_of?(String)
1174          keys = [RawData.new(keys)]
1175        else
1176          normalize_searching_criteria(keys)
1177        end
1178        synchronize do
1179          if charset
1180            send_command(cmd, "CHARSET", charset, *keys)
1181          else
1182            send_command(cmd, *keys)
1183          end
1184          return @responses.delete("SEARCH")[-1]
1185        end
1186      end
1187 
1188      def fetch_internal(cmd, set, attr)
1189        case attr
1190        when String then
1191          attr = RawData.new(attr)
1192        when Array then
1193          attr = attr.map { |arg|
1194            arg.is_a?(String) ? RawData.new(arg) : arg
1195          }
1196        end
1197 
1198        synchronize do
1199          @responses.delete("FETCH")
1200          send_command(cmd, MessageSet.new(set), attr)
1201          return @responses.delete("FETCH")
1202        end
1203      end
1204 
1205      def store_internal(cmd, set, attr, flags)
1206        if attr.instance_of?(String)
1207          attr = RawData.new(attr)
1208        end
1209        synchronize do
1210          @responses.delete("FETCH")
1211          send_command(cmd, MessageSet.new(set), attr, flags)
1212          return @responses.delete("FETCH")
1213        end
1214      end
1215 
1216      def copy_internal(cmd, set, mailbox)
1217        send_command(cmd, MessageSet.new(set), mailbox)
1218      end
1219 
1220      def sort_internal(cmd, sort_keys, search_keys, charset)
1221        if search_keys.instance_of?(String)
1222          search_keys = [RawData.new(search_keys)]
1223        else
1224          normalize_searching_criteria(search_keys)
1225        end
1226        normalize_searching_criteria(search_keys)
1227        synchronize do
1228          send_command(cmd, sort_keys, charset, *search_keys)
1229          return @responses.delete("SORT")[-1]
1230        end
1231      end
1232 
1233      def thread_internal(cmd, algorithm, search_keys, charset)
1234        if search_keys.instance_of?(String)
1235          search_keys = [RawData.new(search_keys)]
1236        else
1237          normalize_searching_criteria(search_keys)
1238        end
1239        normalize_searching_criteria(search_keys)
1240        send_command(cmd, algorithm, charset, *search_keys)
1241        return @responses.delete("THREAD")[-1]
1242      end
1243 
1244      def normalize_searching_criteria(keys)
1245        keys.collect! do |i|
1246          case i
1247          when -1, Range, Array
1248            MessageSet.new(i)
1249          else
1250            i
1251          end
1252        end
1253      end
1254 
1255      def self.u16tou8(s)
1256        len = s.length
1257        if len < 2
1258          return ""
1259        end
1260        buf = ""
1261        i = 0
1262        while i < len
1263          c = s[i] << 8 | s[i + 1]
1264          i += 2
1265          if c == 0xfeff
1266            next
1267          elsif c < 0x0080
1268            buf.concat(c)
1269          elsif c < 0x0800
1270            b2 = c & 0x003f
1271            b1 = c >> 6
1272            buf.concat(b1 | 0xc0)
1273            buf.concat(b2 | 0x80)
1274          elsif c >= 0xdc00 && c < 0xe000
1275            raise DataFormatError, "invalid surrogate detected"
1276          elsif c >= 0xd800 && c < 0xdc00
1277            if i + 2 > len
1278              raise DataFormatError, "invalid surrogate detected"
1279            end
1280            low = s[i] << 8 | s[i + 1]
1281            i += 2
1282            if low < 0xdc00 || low > 0xdfff
1283              raise DataFormatError, "invalid surrogate detected"
1284            end
1285            c = (((c & 0x03ff)) << 10 | (low & 0x03ff)) + 0x10000
1286            b4 = c & 0x003f
1287            b3 = (c >> 6) & 0x003f
1288            b2 = (c >> 12) & 0x003f
1289            b1 = c >> 18;
1290            buf.concat(b1 | 0xf0)
1291            buf.concat(b2 | 0x80)
1292            buf.concat(b3 | 0x80)
1293            buf.concat(b4 | 0x80)
1294          else # 0x0800-0xffff
1295            b3 = c & 0x003f
1296            b2 = (c >> 6) & 0x003f
1297            b1 = c >> 12
1298            buf.concat(b1 | 0xe0)
1299            buf.concat(b2 | 0x80)
1300            buf.concat(b3 | 0x80)
1301          end
1302        end
1303        return buf
1304      end
1305      private_class_method :u16tou8
1306 
1307      def self.u8tou16(s)
1308        len = s.length
1309        buf = ""
1310        i = 0
1311        while i < len
1312          c = s[i]
1313          if (c & 0x80) == 0
1314            buf.concat(0x00)
1315            buf.concat(c)
1316            i += 1
1317          elsif (c & 0xe0) == 0xc0 &&
1318              len >= 2 &&
1319              (s[i + 1] & 0xc0) == 0x80
1320            if c == 0xc0 || c == 0xc1
1321              raise DataFormatError, format("non-shortest UTF-8 sequence (%02x)", c)
1322            end
1323            u = ((c & 0x1f) << 6) | (s[i + 1] & 0x3f)
1324            buf.concat(u >> 8)
1325            buf.concat(u & 0x00ff)
1326            i += 2
1327          elsif (c & 0xf0) == 0xe0 &&
1328              i + 2 < len &&
1329              (s[i + 1] & 0xc0) == 0x80 &&
1330              (s[i + 2] & 0xc0) == 0x80
1331            if c == 0xe0 && s[i + 1] < 0xa0
1332              raise DataFormatError, format("non-shortest UTF-8 sequence (%02x)", c)
1333            end
1334            u = ((c & 0x0f) << 12) | ((s[i + 1] & 0x3f) << 6) | (s[i + 2] & 0x3f)
1335            # surrogate chars
1336            if u >= 0xd800 && u <= 0xdfff
1337              raise DataFormatError, format("none-UTF-16 char detected (%04x)", u)
1338            end
1339            buf.concat(u >> 8)
1340            buf.concat(u & 0x00ff)
1341            i += 3
1342          elsif (c & 0xf8) == 0xf0 &&
1343              i + 3 < len &&
1344              (s[i + 1] & 0xc0) == 0x80 &&
1345              (s[i + 2] & 0xc0) == 0x80 &&
1346              (s[i + 3] & 0xc0) == 0x80
1347            if c == 0xf0 && s[i + 1] < 0x90
1348              raise DataFormatError, format("non-shortest UTF-8 sequence (%02x)", c)
1349            end
1350            u = ((c & 0x07) << 18) | ((s[i + 1] & 0x3f) << 12) |
1351              ((s[i + 2] & 0x3f) << 6) | (s[i + 3] & 0x3f)
1352            if u < 0x10000
1353              buf.concat(u >> 8)
1354              buf.concat(u & 0x00ff)
1355            elsif u < 0x110000
1356              high = ((u - 0x10000) >> 10) | 0xd800
1357              low = (u & 0x03ff) | 0xdc00
1358              buf.concat(high >> 8)
1359              buf.concat(high & 0x00ff)
1360              buf.concat(low >> 8)
1361              buf.concat(low & 0x00ff)
1362            else
1363              raise DataFormatError, format("none-UTF-16 char detected (%04x)", u)
1364            end
1365            i += 4
1366          else
1367            raise DataFormatError, format("illegal UTF-8 sequence (%02x)", c)
1368          end
1369        end
1370        return buf
1371      end
1372      private_class_method :u8tou16
1373 
1374      class RawData # :nodoc:
1375        def send_data(imap)
1376          imap.send(:put_string, @data)
1377        end
1378 
1379        private
1380 
1381        def initialize(data)
1382          @data = data
1383        end
1384      end
1385 
1386      class Atom # :nodoc:
1387        def send_data(imap)
1388          imap.send(:put_string, @data)
1389        end
1390 
1391        private
1392 
1393        def initialize(data)
1394          @data = data
1395        end
1396      end
1397 
1398      class QuotedString # :nodoc:
1399        def send_data(imap)
1400          imap.send(:send_quoted_string, @data)
1401        end
1402 
1403        private
1404 
1405        def initialize(data)
1406          @data = data
1407        end
1408      end
1409 
1410      class Literal # :nodoc:
1411        def send_data(imap)
1412          imap.send(:send_literal, @data)
1413        end
1414 
1415        private
1416 
1417        def initialize(data)
1418          @data = data
1419        end
1420      end
1421 
1422      class MessageSet # :nodoc:
1423        def send_data(imap)
1424          imap.send(:put_string, format_internal(@data))
1425        end
1426 
1427        private
1428 
1429        def initialize(data)
1430          @data = data
1431        end
1432 
1433        def format_internal(data)
1434          case data
1435          when "*"
1436            return data
1437          when Integer
1438            ensure_nz_number(data)
1439            if data == -1
1440              return "*"
1441            else
1442              return data.to_s
1443            end
1444          when Range
1445            return format_internal(data.first) +
1446              ":" + format_internal(data.last)
1447          when Array
1448            return data.collect {|i| format_internal(i)}.join(",")
1449          when ThreadMember
1450            return data.seqno.to_s +
1451              ":" + data.children.collect {|i| format_internal(i).join(",")}
1452          else
1453            raise DataFormatError, data.inspect
1454          end
1455        end
1456 
1457        def ensure_nz_number(num)
1458          if num < -1 || num == 0 || num >= 4294967296
1459            msg = "nz_number must be non-zero unsigned 32-bit integer: " +
1460                  num.inspect
1461            raise DataFormatError, msg
1462          end
1463        end
1464      end
1465 
1466      # Net::IMAP::ContinuationRequest represents command continuation requests.
1467      # 
1468      # The command continuation request response is indicated by a "+" token
1469      # instead of a tag.  This form of response indicates that the server is
1470      # ready to accept the continuation of a command from the client.  The
1471      # remainder of this response is a line of text.
1472      # 
1473      #   continue_req    ::= "+" SPACE (resp_text / base64)
1474      # 
1475      # ==== Fields:
1476      # 
1477      # data:: Returns the data (Net::IMAP::ResponseText).
1478      # 
1479      # raw_data:: Returns the raw data string.
1480      ContinuationRequest = Struct.new(:data, :raw_data)
1481 
1482      # Net::IMAP::UntaggedResponse represents untagged responses.
1483      # 
1484      # Data transmitted by the server to the client and status responses
1485      # that do not indicate command completion are prefixed with the token
1486      # "*", and are called untagged responses.
1487      # 
1488      #   response_data   ::= "*" SPACE (resp_cond_state / resp_cond_bye /
1489      #                       mailbox_data / message_data / capability_data)
1490      # 
1491      # ==== Fields:
1492      # 
1493      # name:: Returns the name such as "FLAGS", "LIST", "FETCH"....
1494      # 
1495      # data:: Returns the data such as an array of flag symbols,
1496      #         a ((<Net::IMAP::MailboxList>)) object....
1497      # 
1498      # raw_data:: Returns the raw data string.
1499      UntaggedResponse = Struct.new(:name, :data, :raw_data)
1500       
1501      # Net::IMAP::TaggedResponse represents tagged responses.
1502      # 
1503      # The server completion result response indicates the success or
1504      # failure of the operation.  It is tagged with the same tag as the
1505      # client command which began the operation.
1506      # 
1507      #   response_tagged ::= tag SPACE resp_cond_state CRLF
1508      #   
1509      #   tag             ::= 1*<any ATOM_CHAR except "+">
1510      #   
1511      #   resp_cond_state ::= ("OK" / "NO" / "BAD") SPACE resp_text
1512      # 
1513      # ==== Fields:
1514      # 
1515      # tag:: Returns the tag.
1516      # 
1517      # name:: Returns the name. the name is one of "OK", "NO", "BAD".
1518      # 
1519      # data:: Returns the data. See ((<Net::IMAP::ResponseText>)).
1520      # 
1521      # raw_data:: Returns the raw data string.
1522      #
1523      TaggedResponse = Struct.new(:tag, :name, :data, :raw_data)
1524       
1525      # Net::IMAP::ResponseText represents texts of responses.
1526      # The text may be prefixed by the response code.
1527      # 
1528      #   resp_text       ::= ["[" resp_text_code "]" SPACE] (text_mime2 / text)
1529      #                       ;; text SHOULD NOT begin with "[" or "="
1530      # 
1531      # ==== Fields:
1532      # 
1533      # code:: Returns the response code. See ((<Net::IMAP::ResponseCode>)).
1534      #       
1535      # text:: Returns the text.
1536      # 
1537      ResponseText = Struct.new(:code, :text)
1538 
1539      # 
1540      # Net::IMAP::ResponseCode represents response codes.
1541      # 
1542      #   resp_text_code  ::= "ALERT" / "PARSE" /
1543      #                       "PERMANENTFLAGS" SPACE "(" #(flag / "\*") ")" /
1544      #                       "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
1545      #                       "UIDVALIDITY" SPACE nz_number /
1546      #                       "UNSEEN" SPACE nz_number /
1547      #                       atom [SPACE 1*<any TEXT_CHAR except "]">]
1548      # 
1549      # ==== Fields:
1550      # 
1551      # name:: Returns the name such as "ALERT", "PERMANENTFLAGS", "UIDVALIDITY"....
1552      # 
1553      # data:: Returns the data if it exists.
1554      #
1555      ResponseCode = Struct.new(:name, :data)
1556 
1557      # Net::IMAP::MailboxList represents contents of the LIST response.
1558      # 
1559      #   mailbox_list    ::= "(" #("\Marked" / "\Noinferiors" /
1560      #                       "\Noselect" / "\Unmarked" / flag_extension) ")"
1561      #                       SPACE (<"> QUOTED_CHAR <"> / nil) SPACE mailbox
1562      # 
1563      # ==== Fields:
1564      # 
1565      # attr:: Returns the name attributes. Each name attribute is a symbol
1566      #        capitalized by String#capitalize, such as :Noselect (not :NoSelect).
1567      # 
1568      # delim:: Returns the hierarchy delimiter
1569      # 
1570      # name:: Returns the mailbox name.
1571      #
1572      MailboxList = Struct.new(:attr, :delim, :name)
1573 
1574      # Net::IMAP::MailboxQuota represents contents of GETQUOTA response.
1575      # This object can also be a response to GETQUOTAROOT.  In the syntax
1576      # specification below, the delimiter used with the "#" construct is a
1577      # single space (SPACE).
1578      # 
1579      #    quota_list      ::= "(" #quota_resource ")"
1580      # 
1581      #    quota_resource  ::= atom SPACE number SPACE number
1582      # 
1583      #    quota_response  ::= "QUOTA" SPACE astring SPACE quota_list
1584      # 
1585      # ==== Fields:
1586      # 
1587      # mailbox:: The mailbox with the associated quota.
1588      # 
1589      # usage:: Current storage usage of mailbox.
1590      # 
1591      # quota:: Quota limit imposed on mailbox.
1592      #
1593      MailboxQuota = Struct.new(:mailbox, :usage, :quota)
1594 
1595      # Net::IMAP::MailboxQuotaRoot represents part of the GETQUOTAROOT
1596      # response. (GETQUOTAROOT can also return Net::IMAP::MailboxQuota.)
1597      # 
1598      #    quotaroot_response ::= "QUOTAROOT" SPACE astring *(SPACE astring)
1599      # 
1600      # ==== Fields:
1601      # 
1602      # mailbox:: The mailbox with the associated quota.
1603      # 
1604      # quotaroots:: Zero or more quotaroots that effect the quota on the
1605      #              specified mailbox.
1606      #
1607      MailboxQuotaRoot = Struct.new(:mailbox, :quotaroots)
1608 
1609      # Net::IMAP::MailboxACLItem represents response from GETACL.
1610      # 
1611      #    acl_data        ::= "ACL" SPACE mailbox *(SPACE identifier SPACE rights)
1612      # 
1613      #    identifier      ::= astring
1614      # 
1615      #    rights          ::= astring
1616      # 
1617      # ==== Fields:
1618      # 
1619      # user:: Login name that has certain rights to the mailbox
1620      #        that was specified with the getacl command.
1621      # 
1622      # rights:: The access rights the indicated user has to the
1623      #          mailbox.
1624      #
1625      MailboxACLItem = Struct.new(:user, :rights)
1626 
1627      # Net::IMAP::StatusData represents contents of the STATUS response.
1628      # 
1629      # ==== Fields:
1630      # 
1631      # mailbox:: Returns the mailbox name.
1632      # 
1633      # attr:: Returns a hash. Each key is one of "MESSAGES", "RECENT", "UIDNEXT",
1634      #        "UIDVALIDITY", "UNSEEN". Each value is a number.
1635      # 
1636      StatusData = Struct.new(:mailbox, :attr)
1637 
1638      # Net::IMAP::FetchData represents contents of the FETCH response.
1639      # 
1640      # ==== Fields:
1641      # 
1642      # seqno:: Returns the message sequence number.
1643      #         (Note: not the unique identifier, even for the UID command response.)
1644      # 
1645      # attr:: Returns a hash. Each key is a data item name, and each value is
1646      #        its value.
1647      # 
1648      #        The current data items are:
1649      # 
1650      #        [BODY]
1651      #           A form of BODYSTRUCTURE without extension data.
1652      #        [BODY[<section>]<<origin_octet>>]
1653      #           A string expressing the body contents of the specified section.
1654      #        [BODYSTRUCTURE]
1655      #           An object that describes the [MIME-IMB] body structure of a message.
1656      #           See Net::IMAP::BodyTypeBasic, Net::IMAP::BodyTypeText,
1657      #           Net::IMAP::BodyTypeMessage, Net::IMAP::BodyTypeMultipart.
1658      #        [ENVELOPE]
1659      #           A Net::IMAP::Envelope object that describes the envelope
1660      #           structure of a message.
1661      #        [FLAGS]
1662      #           A array of flag symbols that are set for this message. flag symbols
1663      #           are capitalized by String#capitalize.
1664      #        [INTERNALDATE]
1665      #           A string representing the internal date of the message.
1666      #        [RFC822]
1667      #           Equivalent to BODY[].
1668      #        [RFC822.HEADER]
1669      #           Equivalent to BODY.PEEK[HEADER].
1670      #        [RFC822.SIZE]
1671      #           A number expressing the [RFC-822] size of the message.
1672      #        [RFC822.TEXT]
1673      #           Equivalent to BODY[TEXT].
1674      #        [UID]
1675      #           A number expressing the unique identifier of the message.
1676      # 
1677      FetchData = Struct.new(:seqno, :attr)
1678 
1679      # Net::IMAP::Envelope represents envelope structures of messages.
1680      # 
1681      # ==== Fields:
1682      # 
1683      # date:: Returns a string that represents the date.
1684      # 
1685      # subject:: Returns a string that represents the subject.
1686      # 
1687      # from:: Returns an array of Net::IMAP::Address that represents the from.
1688      # 
1689      # sender:: Returns an array of Net::IMAP::Address that represents the sender.
1690      # 
1691      # reply_to:: Returns an array of Net::IMAP::Address that represents the reply-to.
1692      # 
1693      # to:: Returns an array of Net::IMAP::Address that represents the to.
1694      # 
1695      # cc:: Returns an array of Net::IMAP::Address that represents the cc.
1696      # 
1697      # bcc:: Returns an array of Net::IMAP::Address that represents the bcc.
1698      # 
1699      # in_reply_to:: Returns a string that represents the in-reply-to.
1700      # 
1701      # message_id:: Returns a string that represents the message-id.
1702      # 
1703      Envelope = Struct.new(:date, :subject, :from, :sender, :reply_to,
1704                            :to, :cc, :bcc, :in_reply_to, :message_id)
1705 
1706      # 
1707      # Net::IMAP::Address represents electronic mail addresses.
1708      # 
1709      # ==== Fields:
1710      # 
1711      # name:: Returns the phrase from [RFC-822] mailbox.
1712      # 
1713      # route:: Returns the route from [RFC-822] route-addr.
1714      # 
1715      # mailbox:: nil indicates end of [RFC-822] group.
1716      #           If non-nil and host is nil, returns [RFC-822] group name.
1717      #           Otherwise, returns [RFC-822] local-part
1718      # 
1719      # host:: nil indicates [RFC-822] group syntax.
1720      #        Otherwise, returns [RFC-822] domain name.
1721      #
1722      Address = Struct.new(:name, :route, :mailbox, :host)
1723 
1724      # 
1725      # Net::IMAP::ContentDisposition represents Content-Disposition fields.
1726      # 
1727      # ==== Fields:
1728      # 
1729      # dsp_type:: Returns the disposition type.
1730      # 
1731      # param:: Returns a hash that represents parameters of the Content-Disposition
1732      #         field.
1733      # 
1734      ContentDisposition = Struct.new(:dsp_type, :param)
1735 
1736      # Net::IMAP::ThreadMember represents a thread-node returned 
1737      # by Net::IMAP#thread
1738      #
1739      # ==== Fields:
1740      #
1741      # seqno:: The sequence number of this message.
1742      #
1743      # children:: an array of Net::IMAP::ThreadMember objects for mail
1744      # items that are children of this in the thread.
1745      #
1746      ThreadMember = Struct.new(:seqno, :children)
1747 
1748      # Net::IMAP::BodyTypeBasic represents basic body structures of messages.
1749      # 
1750      # ==== Fields:
1751      # 
1752      # media_type:: Returns the content media type name as defined in [MIME-IMB].
1753      # 
1754      # subtype:: Returns the content subtype name as defined in [MIME-IMB].
1755      # 
1756      # param:: Returns a hash that represents parameters as defined in [MIME-IMB].
1757      # 
1758      # content_id:: Returns a string giving the content id as defined in [MIME-IMB].
1759      # 
1760      # description:: Returns a string giving the content description as defined in
1761      #               [MIME-IMB].
1762      # 
1763      # encoding:: Returns a string giving the content transfer encoding as defined in
1764      #            [MIME-IMB].
1765      # 
1766      # size:: Returns a number giving the size of the body in octets.
1767      # 
1768      # md5:: Returns a string giving the body MD5 value as defined in [MD5].
1769      # 
1770      # disposition:: Returns a Net::IMAP::ContentDisposition object giving
1771      #               the content disposition.
1772      # 
1773      # language:: Returns a string or an array of strings giving the body
1774      #            language value as defined in [LANGUAGE-TAGS].
1775      # 
1776      # extension:: Returns extension data.
1777      # 
1778      # multipart?:: Returns false.
1779      # 
1780      class BodyTypeBasic < Struct.new(:media_type, :subtype,
1781                                       :param, :content_id,
1782                                       :description, :encoding, :size,
1783                                       :md5, :disposition, :language,
1784                                       :extension)
1785        def multipart?
1786          return false
1787        end
1788 
1789        # Obsolete: use +subtype+ instead.  Calling this will
1790        # generate a warning message to +stderr+, then return 
1791        # the value of +subtype+.
1792        def media_subtype
1793          $stderr.printf("warning: media_subtype is obsolete.\n")
1794          $stderr.printf("         use subtype instead.\n")
1795          return subtype
1796        end
1797      end
1798 
1799      # Net::IMAP::BodyTypeText represents TEXT body structures of messages.
1800      # 
1801      # ==== Fields:
1802      # 
1803      # lines:: Returns the size of the body in text lines.
1804      # 
1805      # And Net::IMAP::BodyTypeText has all fields of Net::IMAP::BodyTypeBasic.
1806      # 
1807      class BodyTypeText < Struct.new(:media_type, :subtype,
1808                                      :param, :content_id,
1809                                      :description, :encoding, :size,
1810                                      :lines,
1811                                      :md5, :disposition, :language,
1812                                      :extension)
1813        def multipart?
1814          return false
1815        end
1816 
1817        # Obsolete: use +subtype+ instead.  Calling this will
1818        # generate a warning message to +stderr+, then return 
1819        # the value of +subtype+.
1820        def media_subtype
1821          $stderr.printf("warning: media_subtype is obsolete.\n")
1822          $stderr.printf("         use subtype instead.\n")
1823          return subtype
1824        end
1825      end
1826 
1827      # Net::IMAP::BodyTypeMessage represents MESSAGE/RFC822 body structures of messages.
1828      # 
1829      # ==== Fields:
1830      # 
1831      # envelope:: Returns a Net::IMAP::Envelope giving the envelope structure.
1832      # 
1833      # body:: Returns an object giving the body structure.
1834      # 
1835      # And Net::IMAP::BodyTypeMessage has all methods of Net::IMAP::BodyTypeText.
1836      #
1837      class BodyTypeMessage < Struct.new(:media_type, :subtype,
1838                                         :param, :content_id,
1839                                         :description, :encoding, :size,
1840                                         :envelope, :body, :lines,
1841                                         :md5, :disposition, :language,
1842                                         :extension)
1843        def multipart?
1844          return false
1845        end
1846 
1847        # Obsolete: use +subtype+ instead.  Calling this will
1848        # generate a warning message to +stderr+, then return 
1849        # the value of +subtype+.
1850        def media_subtype
1851          $stderr.printf("warning: media_subtype is obsolete.\n")
1852          $stderr.printf("         use subtype instead.\n")
1853          return subtype
1854        end
1855      end
1856 
1857      # Net::IMAP::BodyTypeMultipart represents multipart body structures 
1858      # of messages.
1859      # 
1860      # ==== Fields:
1861      # 
1862      # media_type:: Returns the content media type name as defined in [MIME-IMB].
1863      # 
1864      # subtype:: Returns the content subtype name as defined in [MIME-IMB].
1865      # 
1866      # parts:: Returns multiple parts.
1867      # 
1868      # param:: Returns a hash that represents parameters as defined in [MIME-IMB].
1869      # 
1870      # disposition:: Returns a Net::IMAP::ContentDisposition object giving
1871      #               the content disposition.
1872      # 
1873      # language:: Returns a string or an array of strings giving the body
1874      #            language value as defined in [LANGUAGE-TAGS].
1875      # 
1876      # extension:: Returns extension data.
1877      # 
1878      # multipart?:: Returns true.
1879      # 
1880      class BodyTypeMultipart < Struct.new(:media_type, :subtype,
1881                                           :parts,
1882                                           :param, :disposition, :language,
1883                                           :extension)
1884        def multipart?
1885          return true
1886        end
1887 
1888        # Obsolete: use +subtype+ instead.  Calling this will
1889        # generate a warning message to +stderr+, then return 
1890        # the value of +subtype+.
1891        def media_subtype
1892          $stderr.printf("warning: media_subtype is obsolete.\n")
1893          $stderr.printf("         use subtype instead.\n")
1894          return subtype
1895        end
1896      end
1897 
1898      class ResponseParser # :nodoc:
1899        def parse(str)
1900          @str = str
1901          @pos = 0
1902          @lex_state = EXPR_BEG
1903          @token = nil
1904          return response
1905        end
1906 
1907        private
1908 
1909        EXPR_BEG          = :EXPR_BEG
1910        EXPR_DATA         = :EXPR_DATA
1911        EXPR_TEXT         = :EXPR_TEXT
1912        EXPR_RTEXT        = :EXPR_RTEXT
1913        EXPR_CTEXT        = :EXPR_CTEXT
1914 
1915        T_SPACE   = :SPACE
1916        T_NIL     = :NIL
1917        T_NUMBER  = :NUMBER
1918        T_ATOM    = :ATOM
1919        T_QUOTED  = :QUOTED
1920        T_LPAR    = :LPAR
1921        T_RPAR    = :RPAR
1922        T_BSLASH  = :BSLASH
1923        T_STAR    = :STAR
1924        T_LBRA    = :LBRA
1925        T_RBRA    = :RBRA
1926        T_LITERAL = :LITERAL
1927        T_PLUS    = :PLUS
1928        T_PERCENT = :PERCENT
1929        T_CRLF    = :CRLF
1930        T_EOF     = :EOF
1931        T_TEXT    = :TEXT
1932 
1933        BEG_REGEXP = /\G(?:\
1934  (?# 1:  SPACE   )( +)|\
1935  (?# 2:  NIL     )(NIL)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
1936  (?# 3:  NUMBER  )(\d+)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
1937  (?# 4:  ATOM    )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+]+)|\
1938  (?# 5:  QUOTED  )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\
1939  (?# 6:  LPAR    )(\()|\
1940  (?# 7:  RPAR    )(\))|\
1941  (?# 8:  BSLASH  )(\\)|\
1942  (?# 9:  STAR    )(\*)|\
1943  (?# 10: LBRA    )(\[)|\
1944  (?# 11: RBRA    )(\])|\
1945  (?# 12: LITERAL )\{(\d+)\}\r\n|\
1946  (?# 13: PLUS    )(\+)|\
1947  (?# 14: PERCENT )(%)|\
1948  (?# 15: CRLF    )(\r\n)|\
1949  (?# 16: EOF     )(\z))/ni
1950 
1951        DATA_REGEXP = /\G(?:\
1952  (?# 1:  SPACE   )( )|\
1953  (?# 2:  NIL     )(NIL)|\
1954  (?# 3:  NUMBER  )(\d+)|\
1955  (?# 4:  QUOTED  )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\
1956  (?# 5:  LITERAL )\{(\d+)\}\r\n|\
1957  (?# 6:  LPAR    )(\()|\
1958  (?# 7:  RPAR    )(\)))/ni
1959 
1960        TEXT_REGEXP = /\G(?:\
1961  (?# 1:  TEXT    )([^\x00\r\n]*))/ni
1962 
1963        RTEXT_REGEXP = /\G(?:\
1964  (?# 1:  LBRA    )(\[)|\
1965  (?# 2:  TEXT    )([^\x00\r\n]*))/ni
1966 
1967        CTEXT_REGEXP = /\G(?:\
1968  (?# 1:  TEXT    )([^\x00\r\n\]]*))/ni
1969 
1970        Token = Struct.new(:symbol, :value)
1971 
1972        def response
1973          token = lookahead
1974          case token.symbol
1975          when T_PLUS
1976            result = continue_req
1977          when T_STAR
1978            result = response_untagged
1979          else
1980            result = response_tagged
1981          end
1982          match(T_CRLF)
1983          match(T_EOF)
1984          return result
1985        end
1986 
1987        def continue_req
1988          match(T_PLUS)
1989          match(T_SPACE)
1990          return ContinuationRequest.new(resp_text, @str)
1991        end
1992 
1993        def response_untagged
1994          match(T_STAR)
1995          match(T_SPACE)
1996          token = lookahead
1997          if token.symbol == T_NUMBER
1998            return numeric_response
1999          elsif token.symbol == T_ATOM
2000            case token.value
2001            when /\A(?:OK|NO|BAD|BYE|PREAUTH)\z/ni
2002              return response_cond
2003            when /\A(?:FLAGS)\z/ni
2004              return flags_response
2005            when /\A(?:LIST|LSUB)\z/ni
2006              return list_response
2007            when /\A(?:QUOTA)\z/ni
2008              return getquota_response
2009            when /\A(?:QUOTAROOT)\z/ni
2010              return getquotaroot_response
2011            when /\A(?:ACL)\z/ni
2012              return getacl_response
2013            when /\A(?:SEARCH|SORT)\z/ni
2014              return search_response
2015            when /\A(?:THREAD)\z/ni
2016              return thread_response
2017            when /\A(?:STATUS)\z/ni
2018              return status_response
2019            when /\A(?:CAPABILITY)\z/ni
2020              return capability_response
2021            else
2022              return text_response
2023            end
2024          else
2025            parse_error("unexpected token %s", token.symbol)
2026          end
2027        end
2028 
2029        def response_tagged
2030          tag = atom
2031          match(T_SPACE)
2032          token = match(T_ATOM)
2033          name = token.value.upcase
2034          match(T_SPACE)
2035          return TaggedResponse.new(tag, name, resp_text, @str)
2036        end
2037 
2038        def response_cond
2039          token = match(T_ATOM)
2040          name = token.value.upcase
2041          match(T_SPACE)
2042          return UntaggedResponse.new(name, resp_text, @str)
2043        end
2044 
2045        def numeric_response
2046          n = number
2047          match(T_SPACE)
2048          token = match(T_ATOM)
2049          name = token.value.upcase
2050          case name
2051          when "EXISTS", "RECENT", "EXPUNGE"
2052            return UntaggedResponse.new(name, n, @str)
2053          when "FETCH"
2054            shift_token
2055            match(T_SPACE)
2056            data = FetchData.new(n, msg_att)
2057            return UntaggedResponse.new(name, data, @str)
2058          end
2059        end
2060 
2061        def msg_att
2062          match(T_LPAR)
2063          attr = {}
2064          while true
2065            token = lookahead
2066            case token.symbol
2067            when T_RPAR
2068              shift_token
2069              break
2070            when T_SPACE
2071              shift_token
2072              token = lookahead
2073            end
2074            case token.value
2075            when /\A(?:ENVELOPE)\z/ni
2076              name, val = envelope_data
2077            when /\A(?:FLAGS)\z/ni
2078              name, val = flags_data
2079            when /\A(?:INTERNALDATE)\z/ni
2080              name, val = internaldate_data
2081            when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni
2082              name, val = rfc822_text
2083            when /\A(?:RFC822\.SIZE)\z/ni
2084              name, val = rfc822_size
2085            when /\A(?:BODY(?:STRUCTURE)?)\z/ni
2086              name, val = body_data
2087            when /\A(?:UID)\z/ni
2088              name, val = uid_data
2089            else
2090              parse_error("unknown attribute `%s'", token.value)
2091            end
2092            attr[name] = val
2093          end
2094          return attr
2095        end
2096 
2097        def envelope_data
2098          token = match(T_ATOM)
2099          name = token.value.upcase
2100          match(T_SPACE)
2101          return name, envelope
2102        end
2103 
2104        def envelope
2105          @lex_state = EXPR_DATA
2106          token = lookahead
2107          if token.symbol == T_NIL
2108            shift_token
2109            result = nil
2110          else
2111            match(T_LPAR)
2112            date = nstring
2113            match(T_SPACE)
2114            subject = nstring
2115            match(T_SPACE)
2116            from = address_list
2117            match(T_SPACE)
2118            sender = address_list
2119            match(T_SPACE)
2120            reply_to = address_list
2121            match(T_SPACE)
2122            to = address_list
2123            match(T_SPACE)
2124            cc = address_list
2125            match(T_SPACE)
2126            bcc = address_list
2127            match(T_SPACE)
2128            in_reply_to = nstring
2129            match(T_SPACE)
2130            message_id = nstring
2131            match(T_RPAR)
2132            result = Envelope.new(date, subject, from, sender, reply_to,
2133                                  to, cc, bcc, in_reply_to, message_id)
2134          end
2135          @lex_state = EXPR_BEG
2136          return result
2137        end
2138 
2139        def flags_data
2140          token = match(T_ATOM)
2141          name = token.value.upcase
2142          match(T_SPACE)
2143          return name, flag_list
2144        end
2145 
2146        def internaldate_data
2147          token = match(T_ATOM)
2148          name = token.value.upcase
2149          match(T_SPACE)
2150          token = match(T_QUOTED)
2151          return name, token.value
2152        end
2153 
2154        def rfc822_text
2155          token = match(T_ATOM)
2156          name = token.value.upcase
2157          match(T_SPACE)
2158          return name, nstring
2159        end
2160 
2161        def rfc822_size
2162          token = match(T_ATOM)
2163          name = token.value.upcase
2164          match(T_SPACE)
2165          return name, number
2166        end
2167 
2168        def body_data
2169          token = match(T_ATOM)
2170          name = token.value.upcase
2171          token = lookahead
2172          if token.symbol == T_SPACE
2173            shift_token
2174            return name, body
2175          end
2176          name.concat(section)
2177          token = lookahead
2178          if token.symbol == T_ATOM
2179            name.concat(token.value)
2180            shift_token
2181          end
2182          match(T_SPACE)
2183          data = nstring
2184          return name, data
2185        end
2186 
2187        def body
2188          @lex_state = EXPR_DATA
2189          token = lookahead
2190          if token.symbol == T_NIL
2191            shift_token
2192            result = nil
2193          else
2194            match(T_LPAR)
2195            token = lookahead
2196            if token.symbol == T_LPAR
2197              result = body_type_mpart
2198            else
2199              result = body_type_1part
2200            end
2201            match(T_RPAR)
2202          end
2203          @lex_state = EXPR_BEG
2204          return result
2205        end
2206 
2207        def body_type_1part
2208          token = lookahead
2209          case token.value
2210          when /\A(?:TEXT)\z/ni
2211            return body_type_text
2212          when /\A(?:MESSAGE)\z/ni
2213            return body_type_msg
2214          else
2215            return body_type_basic
2216          end
2217        end
2218 
2219        def body_type_basic
2220          mtype, msubtype = media_type
2221          token = lookahead
2222          if token.symbol == T_RPAR
2223            return BodyTypeBasic.new(mtype, msubtype)
2224          end
2225          match(T_SPACE)
2226          param, content_id, desc, enc, size = body_fields
2227          md5, disposition, language, extension = body_ext_1part
2228          return BodyTypeBasic.new(mtype, msubtype,
2229                                   param, content_id,
2230                                   desc, enc, size,
2231                                   md5, disposition, language, extension)
2232        end
2233 
2234        def body_type_text
2235          mtype, msubtype = media_type
2236          match(T_SPACE)
2237          param, content_id, desc, enc, size = body_fields
2238          match(T_SPACE)
2239          lines = number
2240          md5, disposition, language, extension = body_ext_1part
2241          return BodyTypeText.new(mtype, msubtype,
2242                                  param, content_id,
2243                                  desc, enc, size,
2244                                  lines,
2245                                  md5, disposition, language, extension)
2246        end
2247 
2248        def body_type_msg
2249          mtype, msubtype = media_type
2250          match(T_SPACE)
2251          param, content_id, desc, enc, size = body_fields
2252          match(T_SPACE)
2253          env = envelope
2254          match(T_SPACE)
2255          b = body
2256          match(T_SPACE)
2257          lines = number
2258          md5, disposition, language, extension = body_ext_1part
2259          return BodyTypeMessage.new(mtype, msubtype,
2260                                     param, content_id,
2261                                     desc, enc, size,
2262                                     env, b, lines,
2263                                     md5, disposition, language, extension)
2264        end
2265 
2266        def body_type_mpart
2267          parts = []
2268          while true
2269            token = lookahead
2270            if token.symbol == T_SPACE
2271              shift_token
2272              break
2273            end
2274            parts.push(body)
2275          end
2276          mtype = "MULTIPART"
2277          msubtype = case_insensitive_string
2278          param, disposition, language, extension = body_ext_mpart
2279          return BodyTypeMultipart.new(mtype, msubtype, parts,
2280                                       param, disposition, language,
2281                                       extension)
2282        end
2283 
2284        def media_type
2285          mtype = case_insensitive_string
2286          match(T_SPACE)
2287          msubtype = case_insensitive_string
2288          return mtype, msubtype
2289        end
2290 
2291        def body_fields
2292          param = body_fld_param
2293          match(T_SPACE)
2294          content_id = nstring
2295          match(T_SPACE)
2296          desc = nstring
2297          match(T_SPACE)
2298          enc = case_insensitive_string
2299          match(T_SPACE)
2300          size = number
2301          return param, content_id, desc, enc, size
2302        end
2303 
2304        def body_fld_param
2305          token = lookahead
2306          if token.symbol == T_NIL
2307            shift_token
2308            return nil
2309          end
2310          match(T_LPAR)
2311          param = {}
2312          while true
2313            token = lookahead
2314            case token.symbol
2315            when T_RPAR
2316              shift_token
2317              break
2318            when T_SPACE
2319              shift_token
2320            end
2321            name = case_insensitive_string
2322            match(T_SPACE)
2323            val = string
2324            param[name] = val
2325          end
2326          return param
2327        end
2328 
2329        def body_ext_1part
2330          token = lookahead
2331          if token.symbol == T_SPACE
2332            shift_token
2333          else
2334            return nil
2335          end
2336          md5 = nstring
2337 
2338          token = lookahead
2339          if token.symbol == T_SPACE
2340            shift_token
2341          else
2342            return md5
2343          end
2344          disposition = body_fld_dsp
2345 
2346          token = lookahead
2347          if token.symbol == T_SPACE
2348            shift_token
2349          else
2350            return md5, disposition
2351          end
2352          language = body_fld_lang
2353 
2354          token = lookahead
2355          if token.symbol == T_SPACE
2356            shift_token
2357          else
2358            return md5, disposition, language
2359          end
2360 
2361          extension = body_extensions
2362          return md5, disposition, language, extension
2363        end
2364 
2365        def body_ext_mpart
2366          token = lookahead
2367          if token.symbol == T_SPACE
2368            shift_token
2369          else
2370            return nil
2371          end
2372          param = body_fld_param
2373 
2374          token = lookahead
2375          if token.symbol == T_SPACE
2376            shift_token
2377          else
2378            return param
2379          end
2380          disposition = body_fld_dsp
2381          match(T_SPACE)
2382          language = body_fld_lang
2383 
2384          token = lookahead
2385          if token.symbol == T_SPACE
2386            shift_token
2387          else
2388            return param, disposition, language
2389          end
2390 
2391          extension = body_extensions
2392          return param, disposition, language, extension
2393        end
2394 
2395        def body_fld_dsp
2396          token = lookahead
2397          if token.symbol == T_NIL
2398            shift_token
2399            return nil
2400          end
2401          match(T_LPAR)
2402          dsp_type = case_insensitive_string
2403          match(T_SPACE)
2404          param = body_fld_param
2405          match(T_RPAR)
2406          return ContentDisposition.new(dsp_type, param)
2407        end
2408 
2409        def body_fld_lang
2410          token = lookahead
2411          if token.symbol == T_LPAR
2412            shift_token
2413            result = []
2414            while true
2415              token = lookahead
2416              case token.symbol
2417              when T_RPAR
2418                shift_token
2419                return result
2420              when T_SPACE
2421                shift_token
2422              end
2423              result.push(case_insensitive_string)
2424            end
2425          else
2426            lang = nstring
2427            if lang
2428              return lang.upcase
2429            else
2430              return lang
2431            end
2432          end
2433        end
2434 
2435        def body_extensions
2436          result = []
2437          while true
2438            token = lookahead
2439            case token.symbol
2440            when T_RPAR
2441              return result
2442            when T_SPACE
2443              shift_token
2444            end
2445            result.push(body_extension)
2446          end
2447        end
2448 
2449        def body_extension
2450          token = lookahead
2451          case token.symbol
2452          when T_LPAR
2453            shift_token
2454            result = body_extensions
2455            match(T_RPAR)
2456            return result
2457          when T_NUMBER
2458            return number
2459          else
2460            return nstring
2461          end
2462        end
2463 
2464        def section
2465          str = ""
2466          token = match(T_LBRA)
2467          str.concat(token.value)
2468          token = match(T_ATOM, T_NUMBER, T_RBRA)
2469          if token.symbol == T_RBRA
2470            str.concat(token.value)
2471            return str
2472          end
2473          str.concat(token.value)
2474          token = lookahead
2475          if token.symbol == T_SPACE
2476            shift_token
2477            str.concat(token.value)
2478            token = match(T_LPAR)
2479            str.concat(token.value)
2480            while true
2481              token = lookahead
2482              case token.symbol
2483              when T_RPAR
2484                str.concat(token.value)
2485                shift_token
2486                break
2487              when T_SPACE
2488                shift_token
2489                str.concat(token.value)
2490              end
2491              str.concat(format_string(astring))
2492            end
2493          end
2494          token = match(T_RBRA)
2495          str.concat(token.value)
2496          return str
2497        end
2498 
2499        def format_string(str)
2500          case str
2501          when ""
2502            return '""'
2503          when /[\x80-\xff\r\n]/n
2504            # literal
2505            return "{" + str.length.to_s + "}" + CRLF + str
2506          when /[(){ \x00-\x1f\x7f%*"\\]/n
2507            # quoted string
2508            return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"'
2509          else
2510            # atom
2511            return str
2512          end
2513        end
2514 
2515        def uid_data
2516          token = match(T_ATOM)
2517          name = token.value.upcase
2518          match(T_SPACE)
2519          return name, number
2520        end
2521 
2522        def text_response
2523          token = match(T_ATOM)
2524          name = token.value.upcase
2525          match(T_SPACE)
2526          @lex_state = EXPR_TEXT
2527          token = match(T_TEXT)
2528          @lex_state = EXPR_BEG
2529          return UntaggedResponse.new(name, token.value)
2530        end
2531 
2532        def flags_response
2533          token = match(T_ATOM)
2534          name = token.value.upcase
2535          match(T_SPACE)
2536          return UntaggedResponse.new(name, flag_list, @str)
2537        end
2538 
2539        def list_response
2540          token = match(T_ATOM)
2541          name = token.value.upcase
2542          match(T_SPACE)
2543          return UntaggedResponse.new(name, mailbox_list, @str)
2544        end
2545 
2546        def mailbox_list
2547          attr = flag_list
2548          match(T_SPACE)
2549          token = match(T_QUOTED, T_NIL)
2550          if token.symbol == T_NIL
2551            delim = nil
2552          else
2553            delim = token.value
2554          end
2555          match(T_SPACE)
2556          name = astring
2557          return MailboxList.new(attr, delim, name)
2558        end
2559 
2560        def getquota_response
2561          # If quota never established, get back
2562          # `NO Quota root does not exist'.
2563          # If quota removed, get `()' after the
2564          # folder spec with no mention of `STORAGE'.
2565          token = match(T_ATOM)
2566          name = token.value.upcase
2567          match(T_SPACE)
2568          mailbox = astring
2569          match(T_SPACE)
2570          match(T_LPAR)
2571          token = lookahead
2572          case token.symbol
2573          when T_RPAR
2574            shift_token
2575            data = MailboxQuota.new(mailbox, nil, nil)
2576            return UntaggedResponse.new(name, data, @str)
2577          when T_ATOM
2578            shift_token
2579            match(T_SPACE)
2580            token = match(T_NUMBER)
2581            usage = token.value
2582            match(T_SPACE)
2583            token = match(T_NUMBER)
2584            quota = token.value
2585            match(T_RPAR)
2586            data = MailboxQuota.new(mailbox, usage, quota)
2587            return UntaggedResponse.new(name, data, @str)
2588          else
2589            parse_error("unexpected token %s", token.symbol)
2590          end
2591        end
2592 
2593        def getquotaroot_response
2594          # Similar to getquota, but only admin can use getquota.
2595          token = match(T_ATOM)
2596          name = token.value.upcase
2597          match(T_SPACE)
2598          mailbox = astring
2599          quotaroots = []
2600          while true
2601            token = lookahead
2602            break unless token.symbol == T_SPACE
2603            shift_token
2604            quotaroots.push(astring)
2605          end
2606          data = MailboxQuotaRoot.new(mailbox, quotaroots)
2607          return UntaggedResponse.new(name, data, @str)
2608        end
2609 
2610        def getacl_response
2611          token = match(T_ATOM)
2612          name = token.value.upcase
2613          match(T_SPACE)
2614          mailbox = astring
2615          data = []
2616          token = lookahead
2617          if token.symbol == T_SPACE
2618            shift_token
2619            while true
2620              token = lookahead
2621              case token.symbol
2622              when T_CRLF
2623                break
2624              when T_SPACE
2625                shift_token
2626              end
2627              user = astring
2628              match(T_SPACE)
2629              rights = astring
2630              ##XXX data.push([user, rights])
2631              data.push(MailboxACLItem.new(user, rights))
2632            end
2633          end
2634          return UntaggedResponse.new(name, data, @str)
2635        end
2636 
2637        def search_response
2638          token = match(T_ATOM)
2639          name = token.value.upcase
2640          token = lookahead
2641          if token.symbol == T_SPACE
2642            shift_token
2643            data = []
2644            while true
2645              token = lookahead
2646              case token.symbol
2647              when T_CRLF
2648                break
2649              when T_SPACE
2650                shift_token
2651              end
2652              data.push(number)
2653            end
2654          else
2655            data = []
2656          end
2657          return UntaggedResponse.new(name, data, @str)
2658        end
2659 
2660        def thread_response
2661          token = match(T_ATOM)
2662          name = token.value.upcase
2663          token = lookahead
2664 
2665          if token.symbol == T_SPACE
2666            threads = []
2667 
2668            while true
2669              shift_token
2670              token = lookahead
2671 
2672              case token.symbol
2673              when T_LPAR
2674                threads << thread_branch(token)
2675              when T_CRLF
2676                break
2677              end
2678            end
2679          else
2680            # no member
2681            threads = []
2682          end
2683 
2684          return UntaggedResponse.new(name, threads, @str)
2685        end
2686 
2687        def thread_branch(token)
2688          rootmember = nil
2689          lastmember = nil
2690          
2691          while true
2692            shift_token    # ignore first T_LPAR
2693            token = lookahead
2694            
2695            case token.symbol
2696            when T_NUMBER
2697              # new member
2698              newmember = ThreadMember.new(number, [])
2699              if rootmember.nil?
2700                rootmember = newmember
2701              else    
2702                lastmember.children << newmember
2703              end     
2704              lastmember = newmember
2705            when T_SPACE 
2706              # do nothing 
2707            when T_LPAR
2708              if rootmember.nil?
2709                # dummy member
2710                lastmember = rootmember = ThreadMember.new(nil, [])
2711              end     
2712              
2713              lastmember.children << thread_branch(token)
2714            when T_RPAR
2715              break   
2716            end     
2717          end
2718          
2719          return rootmember
2720        end
2721 
2722        def status_response
2723          token = match(T_ATOM)
2724          name = token.value.upcase
2725          match(T_SPACE)
2726          mailbox = astring
2727          match(T_SPACE)
2728          match(T_LPAR)
2729          attr = {}
2730          while true
2731            token = lookahead
2732            case token.symbol
2733            when T_RPAR
2734              shift_token
2735              break
2736            when T_SPACE
2737              shift_token
2738            end
2739            token = match(T_ATOM)
2740            key = token.value.upcase
2741            match(T_SPACE)
2742            val = number
2743            attr[key] = val
2744          end
2745          data = StatusData.new(mailbox, attr)
2746          return UntaggedResponse.new(name, data, @str)
2747        end
2748 
2749        def capability_response
2750          token = match(T_ATOM)
2751          name = token.value.upcase
2752          match(T_SPACE)
2753          data = []
2754          while true
2755            token = lookahead
2756            case token.symbol
2757            when T_CRLF
2758              break
2759            when T_SPACE
2760              shift_token
2761            end
2762            data.push(atom.upcase)
2763          end
2764          return UntaggedResponse.new(name, data, @str)
2765        end
2766 
2767        def resp_text
2768          @lex_state = EXPR_RTEXT
2769          token = lookahead
2770          if token.symbol == T_LBRA
2771            code = resp_text_code
2772          else
2773            code = nil
2774          end
2775          token = match(T_TEXT)
2776          @lex_state = EXPR_BEG
2777          return ResponseText.new(code, token.value)
2778        end
2779 
2780        def resp_text_code
2781          @lex_state = EXPR_BEG
2782          match(T_LBRA)
2783          token = match(T_ATOM)
2784          name = token.value.upcase
2785          case name
2786          when /\A(?:ALERT|PARSE|READ-ONLY|READ-WRITE|TRYCREATE|NOMODSEQ)\z/n
2787            result = ResponseCode.new(name, nil)
2788          when /\A(?:PERMANENTFLAGS)\z/n
2789            match(T_SPACE)
2790            result = ResponseCode.new(name, flag_list)
2791          when /\A(?:UIDVALIDITY|UIDNEXT|UNSEEN)\z/n
2792            match(T_SPACE)
2793            result = ResponseCode.new(name, number)
2794          else
2795            token = lookahead
2796            if token.symbol == T_SPACE
2797              shift_token
2798              @lex_state = EXPR_CTEXT
2799              token = match(T_TEXT)
2800              @lex_state = EXPR_BEG
2801              result = ResponseCode.new(name, token.value)
2802            else
2803              result = ResponseCode.new(name, nil)
2804            end
2805          end
2806          match(T_RBRA)
2807          @lex_state = EXPR_RTEXT
2808          return result
2809        end
2810 
2811        def address_list
2812          token = lookahead
2813          if token.symbol == T_NIL
2814            shift_token
2815            return nil
2816          else
2817            result = []
2818            match(T_LPAR)
2819            while true
2820              token = lookahead
2821              case token.symbol
2822              when T_RPAR
2823                shift_token
2824                break
2825              when T_SPACE
2826                shift_token
2827              end
2828              result.push(address)
2829            end
2830            return result
2831          end
2832        end
2833 
2834        ADDRESS_REGEXP = /\G\
2835  (?# 1: NAME     )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
2836  (?# 2: ROUTE    )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
2837  (?# 3: MAILBOX  )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
2838  (?# 4: HOST     )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)")\
2839  \)/ni
2840 
2841        def address
2842          match(T_LPAR)
2843          if @str.index(ADDRESS_REGEXP, @pos)
2844            # address does not include literal.
2845            @pos = $~.end(0)
2846            name = $1
2847            route = $2
2848            mailbox = $3
2849            host = $4
2850            for s in [name, route, mailbox, host]
2851              if s
2852                s.gsub!(/\\(["\\])/n, "\\1")
2853              end
2854            end
2855          else
2856            name = nstring
2857            match(T_SPACE)
2858            route = nstring
2859            match(T_SPACE)
2860            mailbox = nstring
2861            match(T_SPACE)
2862            host = nstring
2863            match(T_RPAR)
2864          end
2865          return Address.new(name, route, mailbox, host)
2866        end
2867 
2868  #        def flag_list
2869  #       result = []
2870  #       match(T_LPAR)
2871  #       while true
2872  #         token = lookahead
2873  #         case token.symbol
2874  #         when T_RPAR
2875  #           shift_token
2876  #           break
2877  #         when T_SPACE
2878  #           shift_token
2879  #         end
2880  #         result.push(flag)
2881  #       end
2882  #       return result
2883  #        end
2884 
2885  #        def flag
2886  #       token = lookahead
2887  #       if token.symbol == T_BSLASH
2888  #         shift_token
2889  #         token = lookahead
2890  #         if token.symbol == T_STAR
2891  #           shift_token
2892  #           return token.value.intern
2893  #         else
2894  #           return atom.intern
2895  #         end
2896  #       else
2897  #         return atom
2898  #       end
2899  #        end
2900 
2901        FLAG_REGEXP = /\
2902  (?# FLAG        )\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)|\
2903  (?# ATOM        )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\]+)/n
2904 
2905        def flag_list
2906          if @str.index(/\(([^)]*)\)/ni, @pos)
2907            @pos = $~.end(0)
2908            return $1.scan(FLAG_REGEXP).collect { |flag, atom|
2909              atom || flag.capitalize.intern
2910            }
2911          else
2912            parse_error("invalid flag list")
2913          end
2914        end
2915 
2916        def nstring
2917          token = lookahead
2918          if token.symbol == T_NIL
2919            shift_token
2920            return nil
2921          else
2922            return string
2923          end
2924        end
2925 
2926        def astring
2927          token = lookahead
2928          if string_token?(token)
2929            return string
2930          else
2931            return atom
2932          end
2933        end
2934 
2935        def string
2936          token = lookahead
2937          if token.symbol == T_NIL
2938            shift_token
2939            return nil
2940          end
2941          token = match(T_QUOTED, T_LITERAL)
2942          return token.value
2943        end
2944 
2945        STRING_TOKENS = [T_QUOTED, T_LITERAL, T_NIL]
2946 
2947        def string_token?(token)
2948          return STRING_TOKENS.include?(token.symbol)
2949        end
2950 
2951        def case_insensitive_string
2952          token = lookahead
2953          if token.symbol == T_NIL
2954            shift_token
2955            return nil
2956          end
2957          token = match(T_QUOTED, T_LITERAL)
2958          return token.value.upcase
2959        end
2960 
2961        def atom
2962          result = ""
2963          while true
2964            token = lookahead
2965            if atom_token?(token)
2966              result.concat(token.value)
2967              shift_token
2968            else
2969              if result.empty?
2970                parse_error("unexpected token %s", token.symbol)
2971              else
2972                return result
2973              end
2974            end
2975          end
2976        end
2977 
2978        ATOM_TOKENS = [
2979          T_ATOM,
2980          T_NUMBER,
2981          T_NIL,
2982          T_LBRA,
2983          T_RBRA,
2984          T_PLUS
2985        ]
2986 
2987        def atom_token?(token)
2988          return ATOM_TOKENS.include?(token.symbol)
2989        end
2990 
2991        def number
2992          token = lookahead
2993          if token.symbol == T_NIL
2994            shift_token
2995            return nil
2996          end
2997          token = match(T_NUMBER)
2998          return token.value.to_i
2999        end
3000 
3001        def nil_atom
3002          match(T_NIL)
3003          return nil
3004        end
3005 
3006        def match(*args)
3007          token = lookahead
3008          unless args.include?(token.symbol)
3009            parse_error('unexpected token %s (expected %s)',
3010                        token.symbol.id2name,
3011                        args.collect {|i| i.id2name}.join(" or "))
3012          end
3013          shift_token
3014          return token
3015        end
3016 
3017        def lookahead
3018          unless @token
3019            @token = next_token
3020          end
3021          return @token
3022        end
3023 
3024        def shift_token
3025          @token = nil
3026        end
3027 
3028        def next_token
3029          case @lex_state
3030          when EXPR_BEG
3031            if @str.index(BEG_REGEXP, @pos)
3032              @pos = $~.end(0)
3033              if $1
3034                return Token.new(T_SPACE, $+)
3035              elsif $2
3036                return Token.new(T_NIL, $+)
3037              elsif $3
3038                return Token.new(T_NUMBER, $+)
3039              elsif $4
3040                return Token.new(T_ATOM, $+)
3041              elsif $5
3042                return Token.new(T_QUOTED,
3043                                 $+.gsub(/\\(["\\])/n, "\\1"))
3044              elsif $6
3045                return Token.new(T_LPAR, $+)
3046              elsif $7
3047                return Token.new(T_RPAR, $+)
3048              elsif $8
3049                return Token.new(T_BSLASH, $+)
3050              elsif $9
3051                return Token.new(T_STAR, $+)
3052              elsif $10
3053                return Token.new(T_LBRA, $+)
3054              elsif $11
3055                return Token.new(T_RBRA, $+)
3056              elsif $12
3057                len = $+.to_i
3058                val = @str[@pos, len]
3059                @pos += len
3060                return Token.new(T_LITERAL, val)
3061              elsif $13
3062                return Token.new(T_PLUS, $+)
3063              elsif $14
3064                return Token.new(T_PERCENT, $+)
3065              elsif $15
3066                return Token.new(T_CRLF, $+)
3067              elsif $16
3068                return Token.new(T_EOF, $+)
3069              else
3070                parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid")
3071              end
3072            else
3073              @str.index(/\S*/n, @pos)
3074              parse_error("unknown token - %s", $&.dump)
3075            end
3076          when EXPR_DATA
3077            if @str.index(DATA_REGEXP, @pos)
3078              @pos = $~.end(0)
3079              if $1
3080                return Token.new(T_SPACE, $+)
3081              elsif $2
3082                return Token.new(T_NIL, $+)
3083              elsif $3
3084                return Token.new(T_NUMBER, $+)
3085              elsif $4
3086                return Token.new(T_QUOTED,
3087                                 $+.gsub(/\\(["\\])/n, "\\1"))
3088              elsif $5
3089                len = $+.to_i
3090                val = @str[@pos, len]
3091                @pos += len
3092                return Token.new(T_LITERAL, val)
3093              elsif $6
3094                return Token.new(T_LPAR, $+)
3095              elsif $7
3096                return Token.new(T_RPAR, $+)
3097              else
3098                parse_error("[Net::IMAP BUG] DATA_REGEXP is invalid")
3099              end
3100            else
3101              @str.index(/\S*/n, @pos)
3102              parse_error("unknown token - %s", $&.dump)
3103            end
3104          when EXPR_TEXT
3105            if @str.index(TEXT_REGEXP, @pos)
3106              @pos = $~.end(0)
3107              if $1
3108                return Token.new(T_TEXT, $+)
3109              else
3110                parse_error("[Net::IMAP BUG] TEXT_REGEXP is invalid")
3111              end
3112            else
3113              @str.index(/\S*/n, @pos)
3114              parse_error("unknown token - %s", $&.dump)
3115            end
3116          when EXPR_RTEXT
3117            if @str.index(RTEXT_REGEXP, @pos)
3118              @pos = $~.end(0)
3119              if $1
3120                return Token.new(T_LBRA, $+)
3121              elsif $2
3122                return Token.new(T_TEXT, $+)
3123              else
3124                parse_error("[Net::IMAP BUG] RTEXT_REGEXP is invalid")
3125              end
3126            else
3127              @str.index(/\S*/n, @pos)
3128              parse_error("unknown token - %s", $&.dump)
3129            end
3130          when EXPR_CTEXT
3131            if @str.index(CTEXT_REGEXP, @pos)
3132              @pos = $~.end(0)
3133              if $1
3134                return Token.new(T_TEXT, $+)
3135              else
3136                parse_error("[Net::IMAP BUG] CTEXT_REGEXP is invalid")
3137              end
3138            else
3139              @str.index(/\S*/n, @pos) #/
3140              parse_error("unknown token - %s", $&.dump)
3141            end
3142          else
3143            parse_error("illegal @lex_state - %s", @lex_state.inspect)
3144          end
3145        end
3146 
3147        def parse_error(fmt, *args)
3148          if IMAP.debug
3149            $stderr.printf("@str: %s\n", @str.dump)
3150            $stderr.printf("@pos: %d\n", @pos)
3151            $stderr.printf("@lex_state: %s\n", @lex_state)
3152            if @token
3153              $stderr.printf("@token.symbol: %s\n", @token.symbol)
3154              $stderr.printf("@token.value: %s\n", @token.value.inspect)
3155            end
3156          end
3157          raise ResponseParseError, format(fmt, *args)
3158        end
3159      end
3160 
3161      # Authenticator for the "LOGIN" authentication type.  See
3162      # #authenticate().
3163      class LoginAuthenticator
3164        def process(data)
3165          case @state
3166          when STATE_USER
3167            @state = STATE_PASSWORD
3168            return @user
3169          when STATE_PASSWORD
3170            return @password
3171          end
3172        end
3173 
3174        private
3175 
3176        STATE_USER = :USER
3177        STATE_PASSWORD = :PASSWORD
3178 
3179        def initialize(user, password)
3180          @user = user
3181          @password = password
3182          @state = STATE_USER
3183        end
3184      end
3185      add_authenticator "LOGIN", LoginAuthenticator
3186 
3187      # Authenticator for the "CRAM-MD5" authentication type.  See
3188      # #authenticate().
3189      class CramMD5Authenticator
3190        def process(challenge)
3191          digest = hmac_md5(challenge, @password)
3192          return @user + " " + digest
3193        end
3194 
3195        private
3196 
3197        def initialize(user, password)
3198          @user = user
3199          @password = password
3200        end
3201 
3202        def hmac_md5(text, key)
3203          if key.length > 64
3204            key = Digest::MD5.digest(key)
3205          end
3206 
3207          k_ipad = key + "\0" * (64 - key.length)
3208          k_opad = key + "\0" * (64 - key.length)
3209          for i in 0..63
3210            k_ipad[i] ^= 0x36
3211            k_opad[i] ^= 0x5c
3212          end
3213 
3214          digest = Digest::MD5.digest(k_ipad + text)
3215 
3216          return Digest::MD5.hexdigest(k_opad + digest)
3217        end
3218      end
3219      add_authenticator "CRAM-MD5", CramMD5Authenticator
3220 
3221      # Superclass of IMAP errors.
3222      class Error < StandardError
3223      end
3224 
3225      # Error raised when data is in the incorrect format.
3226      class DataFormatError < Error
3227      end
3228 
3229      # Error raised when a response from the server is non-parseable.
3230      class ResponseParseError < Error
3231      end
3232 
3233      # Superclass of all errors used to encapsulate "fail" responses
3234      # from the server.
3235      class ResponseError < Error
3236      end
3237 
3238      # Error raised upon a "NO" response from the server, indicating
3239      # that the client command could not be completed successfully.
3240      class NoResponseError < ResponseError
3241      end
3242 
3243      # Error raised upon a "BAD" response from the server, indicating
3244      # that the client command violated the IMAP protocol, or an internal
3245      # server failure has occurred.
3246      class BadResponseError < ResponseError
3247      end
3248 
3249      # Error raised upon a "BYE" response from the server, indicating 
3250      # that the client is not being allowed to login, or has been timed
3251      # out due to inactivity.
3252      class ByeResponseError < ResponseError
3253      end
3254    end
3255  end
3256 
3257  if __FILE__ == $0
3258    # :enddoc:
3259    require "getoptlong"
3260 
3261    $stdout.sync = true
3262    $port = nil
3263    $user = ENV["USER"] || ENV["LOGNAME"]
3264    $auth = "login"
3265    $ssl = false
3266 
3267    def usage
3268      $stderr.print <<EOF
3269  usage: #{$0} [options] <host>
3270 
3271    --help                        print this message
3272    --port=PORT                   specifies port
3273    --user=USER                   specifies user
3274    --auth=AUTH                   specifies auth type
3275    --ssl                         use ssl
3276  EOF
3277    end
3278 
3279    def get_password
3280      print "password: "
3281      system("stty", "-echo")
3282      begin
3283        return gets.chop
3284      ensure
3285        system("stty", "echo")
3286        print "\n"
3287      end
3288    end
3289 
3290    def get_command
3291      printf("%s@%s> ", $user, $host)
3292      if line = gets
3293        return line.strip.split(/\s+/)
3294      else
3295        return nil
3296      end
3297    end
3298 
3299    parser = GetoptLong.new
3300    parser.set_options(['--debug', GetoptLong::NO_ARGUMENT],
3301                       ['--help', GetoptLong::NO_ARGUMENT],
3302                       ['--port', GetoptLong::REQUIRED_ARGUMENT],
3303                       ['--user', GetoptLong::REQUIRED_ARGUMENT],
3304                       ['--auth', GetoptLong::REQUIRED_ARGUMENT],
3305                       ['--ssl', GetoptLong::NO_ARGUMENT])
3306    begin
3307      parser.each_option do |name, arg|
3308        case name
3309        when "--port"
3310          $port = arg
3311        when "--user"
3312          $user = arg
3313        when "--auth"
3314          $auth = arg
3315        when "--ssl"
3316          $ssl = true
3317        when "--debug"
3318          Net::IMAP.debug = true
3319        when "--help"
3320          usage
3321          exit(1)
3322        end
3323      end
3324    rescue
3325      usage
3326      exit(1)
3327    end
3328 
3329    $host = ARGV.shift
3330    unless $host
3331      usage
3332      exit(1)
3333    end
3334    $port ||= $ssl ? 993 : 143
3335      
3336    imap = Net::IMAP.new($host, $port, $ssl)
3337    begin
3338      password = get_password
3339      imap.authenticate($auth, $user, password)
3340      while true
3341        cmd, *args = get_command
3342        break unless cmd
3343        begin
3344          case cmd
3345          when "list"
3346            for mbox in imap.list("", args[0] || "*")
3347              if mbox.attr.include?(Net::IMAP::NOSELECT)
3348                prefix = "!"
3349              elsif mbox.attr.include?(Net::IMAP::MARKED)
3350                prefix = "*"
3351              else
3352                prefix = " "
3353              end
3354              print prefix, mbox.name, "\n"
3355            end
3356          when "select"
3357            imap.select(args[0] || "inbox")
3358            print "ok\n"
3359          when "close"
3360            imap.close
3361            print "ok\n"
3362          when "summary"
3363            unless messages = imap.responses["EXISTS"][-1]
3364              puts "not selected"
3365              next
3366            end
3367            if messages > 0
3368              for data in imap.fetch(1..-1, ["ENVELOPE"])
3369                print data.seqno, ": ", data.attr["ENVELOPE"].subject, "\n"
3370              end
3371            else
3372              puts "no message"
3373            end
3374          when "fetch"
3375            if args[0]
3376              data = imap.fetch(args[0].to_i, ["RFC822.HEADER", "RFC822.TEXT"])[0]
3377              puts data.attr["RFC822.HEADER"]
3378              puts data.attr["RFC822.TEXT"]
3379            else
3380              puts "missing argument"
3381            end
3382          when "logout", "exit", "quit"
3383            break
3384          when "help", "?"
3385            print <<EOF
3386  list [pattern]                  list mailboxes
3387  select [mailbox]                select mailbox
3388  close                           close mailbox
3389  summary                         display summary
3390  fetch [msgno]                   display message
3391  logout                          logout
3392  help, ?                         display help message
3393  EOF
3394          else
3395            print "unknown command: ", cmd, "\n"
3396          end
3397        rescue Net::IMAP::Error
3398          puts $!
3399        end
3400      end
3401    ensure
3402      imap.logout
3403      imap.disconnect
3404    end
3405  end
3406