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