"Fossies" - the Fresh Open Source Software Archive

Member "ruby-2.7.4/lib/net/imap.rb" (7 Jul 2021, 113759 Bytes) of package /linux/misc/ruby-2.7.4.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Ruby source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "imap.rb": 2.7.3_vs_2.7.4.

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