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