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