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
12 # See Net::IMAP for documentation.
27 # Net::IMAP implements Internet Message Access Protocol (IMAP) client
28 # functionality. The protocol is described in [IMAP].
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.
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.
49 # Messages have two sorts of identifiers: message sequence
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.
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.
68 # == Examples of Usage
70 # === List sender and subject of all recent messages in the default mailbox
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}"
80 # === Move all messages from April 2003 from "Mail/sent-mail" to "Mail/sent-apr03"
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')
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])
96 # Net::IMAP supports concurrent threads. For example,
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
106 # This script invokes the FETCH command and the SEARCH command concurrently.
110 # An IMAP server can send three different types of responses to indicate
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.
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.
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.
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.
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.
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.
156 # M. Crispin, "INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1",
157 # RFC 2060, December 1996. (Note: since obsoleted by RFC 3501)
160 # Alvestrand, H., "Tags for the Identification of
161 # Languages", RFC 1766, March 1995.
164 # Myers, J., and M. Rose, "The Content-MD5 Header Field", RFC
165 # 1864, October 1995.
168 # Freed, N., and N. Borenstein, "MIME (Multipurpose Internet
169 # Mail Extensions) Part One: Format of Internet Message Bodies", RFC
170 # 2045, November 1996.
173 # Crocker, D., "Standard for the Format of ARPA Internet Text
174 # Messages", STD 11, RFC 822, University of Delaware, August 1982.
177 # Myers, J., "IMAP4 QUOTA extension", RFC 2087, January 1997.
180 # Myers, J., "IMAP4 ACL extension", RFC 2086, January 1997.
183 # Klensin, J., Catoe, R., and Krumviede, P., "IMAP/POP AUTHorize Extension
184 # for Simple Challenge/Response", RFC 2195, September 1997.
186 # [[SORT-THREAD-EXT]]
187 # Crispin, M., "INTERNET MESSAGE ACCESS PROTOCOL - SORT and THREAD
188 # Extensions", draft-ietf-imapext-sort, May 2003.
191 # http://www.openssl.org
194 # http://savannah.gnu.org/projects/rubypki
197 # Goldsmith, D. and Davis, M., "UTF-7: A Mail-Safe Transformation Format of
198 # Unicode", RFC 2152, May 1997.
207 # Returns an initial greeting response from the server.
208 attr_reader :greeting
210 # Returns recorded untagged responses. For example:
212 # imap.select("inbox")
213 # p imap.responses["EXISTS"][-1]
215 # p imap.responses["UIDVALIDITY"][-1]
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
228 # Flag indicating a message has been answered
231 # Flag indicating a message has been flagged for special or urgent
235 # Flag indicating a message has been marked for deletion. This
236 # will occur when the mailbox is closed or expunged.
239 # Flag indicating a message is only a draft or work-in-progress version.
242 # Flag indicating that the message is "recent", meaning that this
243 # session is the first session in which the client has been notified
247 # Flag indicating that a mailbox context name cannot contain
249 NOINFERIORS = :Noinferiors
251 # Flag indicating that a mailbox is not selected.
254 # Flag indicating that a mailbox has been marked "interesting" by
255 # the server; this commonly indicates that the mailbox contains
259 # Flag indicating that the mailbox does not contains new messages.
262 # Returns the debug mode.
267 # Sets the debug mode.
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.
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
285 # Disconnects from the server.
287 @sock.shutdown unless @usessl
288 @receiver_thread.join
292 # Returns true if disconnected from the server.
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
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
309 send_command("CAPABILITY")
310 return @responses.delete("CAPABILITY")[-1]
314 # Sends a NOOP command to the server. It does nothing.
319 # Sends a LOGOUT command to inform the server that the client is
320 # done with the connection.
322 send_command("LOGOUT")
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:
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.
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".
341 # Authentication is done using the appropriate authenticator object:
342 # see @@authenticators for more information on plugging in your own
347 # imap.authenticate('LOGIN', user, password)
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)
354 format('unknown auth type - "%s"', auth_type)
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/, "")
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.
372 # A Net::IMAP::NoResponseError is raised if authentication fails.
373 def login(user, password)
374 send_command("LOGIN", user, password)
377 # Sends a SELECT command to select a +mailbox+ so that messages
378 # in the +mailbox+ can be accessed.
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.
387 # A Net::IMAP::NoResponseError is raised if the mailbox does not
388 # exist or is for some reason non-selectable.
392 send_command("SELECT", mailbox)
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.
400 # A Net::IMAP::NoResponseError is raised if the mailbox does not
401 # exist or is for some reason non-examinable.
405 send_command("EXAMINE", mailbox)
409 # Sends a CREATE command to create a new +mailbox+.
411 # A Net::IMAP::NoResponseError is raised if a mailbox with that name
414 send_command("CREATE", mailbox)
417 # Sends a DELETE command to remove the +mailbox+.
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.
423 send_command("DELETE", mailbox)
426 # Sends a RENAME command to change the name of the +mailbox+ to
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)
437 # Sends a SUBSCRIBE command to add the specified +mailbox+ name to
438 # the server's set of "active" or "subscribed" mailboxes as returned
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)
447 # Sends a UNSUBSCRIBE command to remove the specified +mailbox+ name
448 # from the server's set of "active" or "subscribed" mailboxes.
450 # A Net::IMAP::NoResponseError is raised if +mailbox+ cannot be
451 # unsubscribed from, for instance because the client is not currently
453 def unsubscribe(mailbox)
454 send_command("UNSUBSCRIBE", mailbox)
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.
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.
471 # The return value is an array of +Net::IMAP::MailboxList+. For example:
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)
481 send_command("LIST", refname, mailbox)
482 return @responses.delete("LIST")
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)
492 send_command("GETQUOTAROOT", mailbox)
494 result.concat(@responses.delete("QUOTAROOT"))
495 result.concat(@responses.delete("QUOTA"))
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)
506 send_command("GETQUOTA", mailbox)
507 return @responses.delete("QUOTA")
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
516 def setquota(mailbox, quota)
520 data = '(STORAGE ' + quota.to_s + ')'
522 send_command("SETQUOTA", mailbox, RawData.new(data))
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)
531 send_command("SETACL", mailbox, user, "")
533 send_command("SETACL", mailbox, user, rights)
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.
542 send_command("GETACL", mailbox)
543 return @responses.delete("ACL")[-1]
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
551 # The return value is an array of +Net::IMAP::MailboxList+.
552 def lsub(refname, mailbox)
554 send_command("LSUB", refname, mailbox)
555 return @responses.delete("LSUB")
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:
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.
567 # The return value is a hash of attributes. For example:
569 # p imap.status("inbox", ["MESSAGES", "RECENT"])
570 # #=> {"RECENT"=>0, "MESSAGES"=>44}
572 # A Net::IMAP::NoResponseError is raised if status values
573 # for +mailbox+ cannot be returned, for instance because it
575 def status(mailbox, attr)
577 send_command("STATUS", mailbox, attr)
578 return @responses.delete("STATUS")[-1].attr
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.
589 # imap.append("inbox", <<EOF.gsub(/\n/, "\r\n"), [:Seen], Time.now)
591 # From: shugo@ruby-lang.org
592 # To: shugo@ruby-lang.org
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)
605 args.push(date_time) if date_time
606 args.push(Literal.new(message))
607 send_command("APPEND", mailbox, *args)
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.
615 send_command("CHECK")
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.
622 send_command("CLOSE")
625 # Sends a EXPUNGE command to permanently remove from the currently
626 # selected mailbox all messages that have the \Deleted flag set.
629 send_command("EXPUNGE")
630 return @responses.delete("EXPUNGE")
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.
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".
645 # BEFORE <date>:: messages with an internal date strictly before
646 # <date>. The date argument has a format similar
649 # BODY <string>:: messages that contain <string> within their body.
651 # CC <string>:: messages containing <string> in their CC field.
653 # FROM <string>:: messages that contain <string> in their FROM field.
655 # NEW:: messages with the \Recent, but not the \Seen, flag set.
657 # NOT <search-key>:: negate the following search key.
659 # OR <search-key> <search-key>:: "or" two search keys together.
661 # ON <date>:: messages with an internal date exactly equal to <date>,
662 # which has a format similar to 8-Aug-2002.
664 # SINCE <date>:: messages with an internal date on or after <date>.
666 # SUBJECT <string>:: messages with <string> in their subject.
668 # TO <string>:: messages with <string> in their TO field.
672 # p imap.search(["SUBJECT", "hello", "NOT", "NEW"])
674 def search(keys, charset = nil)
675 return search_internal("SEARCH", keys, charset)
678 # As for #search(), but returns unique identifiers.
679 def uid_search(keys, charset = nil)
680 return search_internal("UID SEARCH", keys, charset)
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
689 # The return value is an array of Net::IMAP::FetchData. For example:
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]
700 # p data.attr["RFC822.SIZE"]
702 # p data.attr["INTERNALDATE"]
703 # #=> "12-Oct-2000 22:40:59 +0900"
707 return fetch_internal("FETCH", set, attr)
710 # As for #fetch(), but +set+ contains unique identifiers.
711 def uid_fetch(set, attr)
712 return fetch_internal("UID FETCH", set, attr)
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.
723 # The return value is an array of Net::IMAP::FetchData. For example:
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)
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)
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)
746 # As for #copy(), but +set+ contains unique identifiers.
747 def uid_copy(set, mailbox)
748 copy_internal("UID COPY", set, mailbox)
751 # Sends a SORT command to sort messages in the mailbox.
752 # Returns an array of message sequence numbers. For example:
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")
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)
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)
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
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"
781 def add_response_handler(handler = Proc.new)
782 @response_handlers.push(handler)
785 # Removes the response handler.
786 def remove_response_handler(handler)
787 @response_handlers.delete(handler)
790 # As for #search(), but returns message sequence numbers in threaded
791 # format, as a Net::IMAP::ThreadMember tree. The supported algorithms
794 # ORDEREDSUBJECT:: split into single-level threads according to subject,
796 # REFERENCES:: split into threads by parent/child relationships determined
797 # by which message is a reply to which.
799 # Unlike #search(), +charset+ is a required argument. US-ASCII
800 # and UTF-8 are sample values.
802 # See [SORT-THREAD-EXT] for more details.
803 def thread(algorithm, search_keys, charset)
804 return thread_internal("THREAD", algorithm, search_keys, charset)
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)
813 # Decode a string from modified UTF-7 format to UTF-8.
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.
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) {
826 base64 = $1.tr(",", "/")
827 x = base64.length % 4
829 base64.concat("=" * (4 - x))
831 u16tou8(base64.unpack("m")[0])
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|
842 base64 = [u8tou16(x)].pack("m")
843 "&" + base64.delete("=\n").tr("/", ",") + "-"
850 CRLF = "\r\n" # :nodoc:
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.
865 # The most common errors are:
867 # Errno::ECONNREFUSED:: connection refused by +host+ or an intervening
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)
881 @parser = ResponseParser.new
882 @sock = TCPSocket.open(host, port)
884 unless defined?(OpenSSL)
885 raise "SSL extension not installed"
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
897 @sock = SSLSocket.new(@sock, context)
898 @sock.connect # start ssl session.
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"
913 raise ByeResponseError, @greeting.raw_data
916 @client_thread = Thread.current
917 @receiver_thread = Thread.start {
922 def receive_responses
928 @client_thread.raise($!)
936 @tagged_responses[resp.tag] = resp
937 @response_arrival.broadcast
938 if resp.tag == @logout_command_tag
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)
947 if resp.name == "BYE" && @logout_command_tag.nil?
949 raise ByeResponseError, resp.raw_data
951 when ContinuationRequest
952 @continuation_request = resp
953 @response_arrival.broadcast
955 @response_handlers.each do |handler|
960 @client_thread.raise($!)
965 def get_tagged_response(tag)
966 until @tagged_responses.key?(tag)
967 @response_arrival.wait
969 return pick_up_tagged_response(tag)
972 def pick_up_tagged_response(tag)
973 resp = @tagged_responses.delete(tag)
976 raise NoResponseError, resp.data.text
978 raise BadResponseError, resp.data.text
990 if /\{(\d+)\}\r\n/n =~ s
991 s = @sock.read($1.to_i)
997 return nil if buff.length == 0
999 $stderr.print(buff.gsub(/^/n, "S: "))
1001 return @parser.parse(buff)
1004 def record_response(name, data)
1005 unless @responses.has_key?(name)
1006 @responses[name] = []
1008 @responses[name].push(data)
1011 def send_command(cmd, *args, &block)
1013 tag = Thread.current[:net_imap_tag] = generate_tag
1014 put_string(tag + " " + cmd)
1021 @logout_command_tag = tag
1024 add_response_handler(block)
1027 return get_tagged_response(tag)
1030 remove_response_handler(block)
1038 return format("%s%04d", @tag_prefix, @tagno)
1044 if @debug_output_bol
1045 $stderr.print("C: ")
1047 $stderr.print(str.gsub(/\n(?!\z)/n, "\nC: "))
1048 if /\r\n\z/n.match(str)
1049 @debug_output_bol = true
1051 @debug_output_bol = false
1061 send_string_data(data)
1063 send_number_data(data)
1065 send_list_data(data)
1067 send_time_data(data)
1069 send_symbol_data(data)
1071 data.send_data(self)
1075 def send_string_data(str)
1079 when /[\x80-\xff\r\n]/n
1082 when /[(){ \x00-\x1f\x7f%*"\\]/n
1084 send_quoted_string(str)
1090 def send_quoted_string(str)
1091 put_string('"' + str.gsub(/["\\]/n, "\\\\\\&") + '"')
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
1100 if @continuation_request.nil?
1101 pick_up_tagged_response(Thread.current[:net_imap_tag])
1102 raise ResponseError.new("expected continuation request")
1104 @continuation_request = nil
1108 def send_number_data(num)
1109 if num < 0 || num >= 4294967296
1110 raise DataFormatError, num.to_s
1112 put_string(num.to_s)
1115 def send_list_data(list)
1129 DATE_MONTH = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)
1131 def send_time_data(time)
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)
1139 def send_symbol_data(symbol)
1140 put_string("\\" + symbol.to_s)
1143 def search_internal(cmd, keys, charset)
1144 if keys.instance_of?(String)
1145 keys = [RawData.new(keys)]
1147 normalize_searching_criteria(keys)
1151 send_command(cmd, "CHARSET", charset, *keys)
1153 send_command(cmd, *keys)
1155 return @responses.delete("SEARCH")[-1]
1159 def fetch_internal(cmd, set, attr)
1160 if attr.instance_of?(String)
1161 attr = RawData.new(attr)
1164 @responses.delete("FETCH")
1165 send_command(cmd, MessageSet.new(set), attr)
1166 return @responses.delete("FETCH")
1170 def store_internal(cmd, set, attr, flags)
1171 if attr.instance_of?(String)
1172 attr = RawData.new(attr)
1175 @responses.delete("FETCH")
1176 send_command(cmd, MessageSet.new(set), attr, flags)
1177 return @responses.delete("FETCH")
1181 def copy_internal(cmd, set, mailbox)
1182 send_command(cmd, MessageSet.new(set), mailbox)
1185 def sort_internal(cmd, sort_keys, search_keys, charset)
1186 if search_keys.instance_of?(String)
1187 search_keys = [RawData.new(search_keys)]
1189 normalize_searching_criteria(search_keys)
1191 normalize_searching_criteria(search_keys)
1193 send_command(cmd, sort_keys, charset, *search_keys)
1194 return @responses.delete("SORT")[-1]
1198 def thread_internal(cmd, algorithm, search_keys, charset)
1199 if search_keys.instance_of?(String)
1200 search_keys = [RawData.new(search_keys)]
1202 normalize_searching_criteria(search_keys)
1204 normalize_searching_criteria(search_keys)
1205 send_command(cmd, algorithm, charset, *search_keys)
1206 return @responses.delete("THREAD")[-1]
1209 def normalize_searching_criteria(keys)
1210 keys.collect! do |i|
1212 when -1, Range, Array
1228 c = s[i] << 8 | s[i + 1]
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
1243 raise DataFormatError, "invalid surrogate detected"
1245 low = s[i] << 8 | s[i + 1]
1247 if low < 0xdc00 || low > 0xdfff
1248 raise DataFormatError, "invalid surrogate detected"
1250 c = (((c & 0x03ff)) << 10 | (low & 0x03ff)) + 0x10000
1252 b3 = (c >> 6) & 0x003f
1253 b2 = (c >> 12) & 0x003f
1255 buf.concat(b1 | 0xf0)
1256 buf.concat(b2 | 0x80)
1257 buf.concat(b3 | 0x80)
1258 buf.concat(b4 | 0x80)
1259 else # 0x0800-0xffff
1261 b2 = (c >> 6) & 0x003f
1263 buf.concat(b1 | 0xe0)
1264 buf.concat(b2 | 0x80)
1265 buf.concat(b3 | 0x80)
1270 private_class_method :u16tou8
1282 elsif (c & 0xe0) == 0xc0 &&
1284 (s[i + 1] & 0xc0) == 0x80
1285 if c == 0xc0 || c == 0xc1
1286 raise DataFormatError, format("non-shortest UTF-8 sequence (%02x)", c)
1288 u = ((c & 0x1f) << 6) | (s[i + 1] & 0x3f)
1290 buf.concat(u & 0x00ff)
1292 elsif (c & 0xf0) == 0xe0 &&
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)
1299 u = ((c & 0x0f) << 12) | ((s[i + 1] & 0x3f) << 6) | (s[i + 2] & 0x3f)
1301 if u >= 0xd800 && u <= 0xdfff
1302 raise DataFormatError, format("none-UTF-16 char detected (%04x)", u)
1305 buf.concat(u & 0x00ff)
1307 elsif (c & 0xf8) == 0xf0 &&
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)
1315 u = ((c & 0x07) << 18) | ((s[i + 1] & 0x3f) << 12) |
1316 ((s[i + 2] & 0x3f) << 6) | (s[i + 3] & 0x3f)
1319 buf.concat(u & 0x00ff)
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)
1328 raise DataFormatError, format("none-UTF-16 char detected (%04x)", u)
1332 raise DataFormatError, format("illegal UTF-8 sequence (%02x)", c)
1337 private_class_method :u8tou16
1339 class RawData # :nodoc:
1341 imap.send(:put_string, @data)
1346 def initialize(data)
1351 class Atom # :nodoc:
1353 imap.send(:put_string, @data)
1358 def initialize(data)
1363 class QuotedString # :nodoc:
1365 imap.send(:send_quoted_string, @data)
1370 def initialize(data)
1375 class Literal # :nodoc:
1377 imap.send(:send_literal, @data)
1382 def initialize(data)
1387 class MessageSet # :nodoc:
1389 imap.send(:put_string, format_internal(@data))
1394 def initialize(data)
1398 def format_internal(data)
1403 ensure_nz_number(data)
1410 return format_internal(data.first) +
1411 ":" + format_internal(data.last)
1413 return data.collect {|i| format_internal(i)}.join(",")
1415 return data.seqno.to_s +
1416 ":" + data.children.collect {|i| format_internal(i).join(",")}
1418 raise DataFormatError, data.inspect
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: " +
1426 raise DataFormatError, msg
1431 # Net::IMAP::ContinuationRequest represents command continuation requests.
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.
1438 # continue_req ::= "+" SPACE (resp_text / base64)
1442 # data:: Returns the data (Net::IMAP::ResponseText).
1444 # raw_data:: Returns the raw data string.
1445 ContinuationRequest = Struct.new(:data, :raw_data)
1447 # Net::IMAP::UntaggedResponse represents untagged responses.
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.
1453 # response_data ::= "*" SPACE (resp_cond_state / resp_cond_bye /
1454 # mailbox_data / message_data / capability_data)
1458 # name:: Returns the name such as "FLAGS", "LIST", "FETCH"....
1460 # data:: Returns the data such as an array of flag symbols,
1461 # a ((<Net::IMAP::MailboxList>)) object....
1463 # raw_data:: Returns the raw data string.
1464 UntaggedResponse = Struct.new(:name, :data, :raw_data)
1466 # Net::IMAP::TaggedResponse represents tagged responses.
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.
1472 # response_tagged ::= tag SPACE resp_cond_state CRLF
1474 # tag ::= 1*<any ATOM_CHAR except "+">
1476 # resp_cond_state ::= ("OK" / "NO" / "BAD") SPACE resp_text
1480 # tag:: Returns the tag.
1482 # name:: Returns the name. the name is one of "OK", "NO", "BAD".
1484 # data:: Returns the data. See ((<Net::IMAP::ResponseText>)).
1486 # raw_data:: Returns the raw data string.
1488 TaggedResponse = Struct.new(:tag, :name, :data, :raw_data)
1490 # Net::IMAP::ResponseText represents texts of responses.
1491 # The text may be prefixed by the response code.
1493 # resp_text ::= ["[" resp_text_code "]" SPACE] (text_mime2 / text)
1494 # ;; text SHOULD NOT begin with "[" or "="
1498 # code:: Returns the response code. See ((<Net::IMAP::ResponseCode>)).
1500 # text:: Returns the text.
1502 ResponseText = Struct.new(:code, :text)
1505 # Net::IMAP::ResponseCode represents response codes.
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 "]">]
1516 # name:: Returns the name such as "ALERT", "PERMANENTFLAGS", "UIDVALIDITY"....
1518 # data:: Returns the data if it exists.
1520 ResponseCode = Struct.new(:name, :data)
1522 # Net::IMAP::MailboxList represents contents of the LIST response.
1524 # mailbox_list ::= "(" #("\Marked" / "\Noinferiors" /
1525 # "\Noselect" / "\Unmarked" / flag_extension) ")"
1526 # SPACE (<"> QUOTED_CHAR <"> / nil) SPACE mailbox
1530 # attr:: Returns the name attributes. Each name attribute is a symbol
1531 # capitalized by String#capitalize, such as :Noselect (not :NoSelect).
1533 # delim:: Returns the hierarchy delimiter
1535 # name:: Returns the mailbox name.
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).
1544 # quota_list ::= "(" #quota_resource ")"
1546 # quota_resource ::= atom SPACE number SPACE number
1548 # quota_response ::= "QUOTA" SPACE astring SPACE quota_list
1552 # mailbox:: The mailbox with the associated quota.
1554 # usage:: Current storage usage of mailbox.
1556 # quota:: Quota limit imposed on mailbox.
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.)
1563 # quotaroot_response ::= "QUOTAROOT" SPACE astring *(SPACE astring)
1567 # mailbox:: The mailbox with the associated quota.
1569 # quotaroots:: Zero or more quotaroots that effect the quota on the
1570 # specified mailbox.
1572 MailboxQuotaRoot = Struct.new(:mailbox, :quotaroots)
1574 # Net::IMAP::MailboxACLItem represents response from GETACL.
1576 # acl_data ::= "ACL" SPACE mailbox *(SPACE identifier SPACE rights)
1578 # identifier ::= astring
1580 # rights ::= astring
1584 # user:: Login name that has certain rights to the mailbox
1585 # that was specified with the getacl command.
1587 # rights:: The access rights the indicated user has to the
1590 MailboxACLItem = Struct.new(:user, :rights)
1592 # Net::IMAP::StatusData represents contents of the STATUS response.
1596 # mailbox:: Returns the mailbox name.
1598 # attr:: Returns a hash. Each key is one of "MESSAGES", "RECENT", "UIDNEXT",
1599 # "UIDVALIDITY", "UNSEEN". Each value is a number.
1601 StatusData = Struct.new(:mailbox, :attr)
1603 # Net::IMAP::FetchData represents contents of the FETCH response.
1607 # seqno:: Returns the message sequence number.
1608 # (Note: not the unique identifier, even for the UID command response.)
1610 # attr:: Returns a hash. Each key is a data item name, and each value is
1613 # The current data items are:
1616 # A form of BODYSTRUCTURE without extension data.
1617 # [BODY[<section>]<<origin_octet>>]
1618 # A string expressing the body contents of the specified section.
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.
1624 # A Net::IMAP::Envelope object that describes the envelope
1625 # structure of a message.
1627 # A array of flag symbols that are set for this message. flag symbols
1628 # are capitalized by String#capitalize.
1630 # A string representing the internal date of the message.
1632 # Equivalent to BODY[].
1634 # Equivalent to BODY.PEEK[HEADER].
1636 # A number expressing the [RFC-822] size of the message.
1638 # Equivalent to BODY[TEXT].
1640 # A number expressing the unique identifier of the message.
1642 FetchData = Struct.new(:seqno, :attr)
1644 # Net::IMAP::Envelope represents envelope structures of messages.
1648 # date:: Returns a string that represents the date.
1650 # subject:: Returns a string that represents the subject.
1652 # from:: Returns an array of Net::IMAP::Address that represents the from.
1654 # sender:: Returns an array of Net::IMAP::Address that represents the sender.
1656 # reply_to:: Returns an array of Net::IMAP::Address that represents the reply-to.
1658 # to:: Returns an array of Net::IMAP::Address that represents the to.
1660 # cc:: Returns an array of Net::IMAP::Address that represents the cc.
1662 # bcc:: Returns an array of Net::IMAP::Address that represents the bcc.
1664 # in_reply_to:: Returns a string that represents the in-reply-to.
1666 # message_id:: Returns a string that represents the message-id.
1668 Envelope = Struct.new(:date, :subject, :from, :sender, :reply_to,
1669 :to, :cc, :bcc, :in_reply_to, :message_id)
1672 # Net::IMAP::Address represents electronic mail addresses.
1676 # name:: Returns the phrase from [RFC-822] mailbox.
1678 # route:: Returns the route from [RFC-822] route-addr.
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
1684 # host:: nil indicates [RFC-822] group syntax.
1685 # Otherwise, returns [RFC-822] domain name.
1687 Address = Struct.new(:name, :route, :mailbox, :host)
1690 # Net::IMAP::ContentDisposition represents Content-Disposition fields.
1694 # dsp_type:: Returns the disposition type.
1696 # param:: Returns a hash that represents parameters of the Content-Disposition
1699 ContentDisposition = Struct.new(:dsp_type, :param)
1701 # Net::IMAP::ThreadMember represents a thread-node returned
1702 # by Net::IMAP#thread
1706 # seqno:: The sequence number of this message.
1708 # children:: an array of Net::IMAP::ThreadMember objects for mail
1709 # items that are children of this in the thread.
1711 ThreadMember = Struct.new(:seqno, :children)
1713 # Net::IMAP::BodyTypeBasic represents basic body structures of messages.
1717 # media_type:: Returns the content media type name as defined in [MIME-IMB].
1719 # subtype:: Returns the content subtype name as defined in [MIME-IMB].
1721 # param:: Returns a hash that represents parameters as defined in [MIME-IMB].
1723 # content_id:: Returns a string giving the content id as defined in [MIME-IMB].
1725 # description:: Returns a string giving the content description as defined in
1728 # encoding:: Returns a string giving the content transfer encoding as defined in
1731 # size:: Returns a number giving the size of the body in octets.
1733 # md5:: Returns a string giving the body MD5 value as defined in [MD5].
1735 # disposition:: Returns a Net::IMAP::ContentDisposition object giving
1736 # the content disposition.
1738 # language:: Returns a string or an array of strings giving the body
1739 # language value as defined in [LANGUAGE-TAGS].
1741 # extension:: Returns extension data.
1743 # multipart?:: Returns false.
1745 class BodyTypeBasic < Struct.new(:media_type, :subtype,
1746 :param, :content_id,
1747 :description, :encoding, :size,
1748 :md5, :disposition, :language,
1754 # Obsolete: use +subtype+ instead. Calling this will
1755 # generate a warning message to +stderr+, then return
1756 # the value of +subtype+.
1758 $stderr.printf("warning: media_subtype is obsolete.\n")
1759 $stderr.printf(" use subtype instead.\n")
1764 # Net::IMAP::BodyTypeText represents TEXT body structures of messages.
1768 # lines:: Returns the size of the body in text lines.
1770 # And Net::IMAP::BodyTypeText has all fields of Net::IMAP::BodyTypeBasic.
1772 class BodyTypeText < Struct.new(:media_type, :subtype,
1773 :param, :content_id,
1774 :description, :encoding, :size,
1776 :md5, :disposition, :language,
1782 # Obsolete: use +subtype+ instead. Calling this will
1783 # generate a warning message to +stderr+, then return
1784 # the value of +subtype+.
1786 $stderr.printf("warning: media_subtype is obsolete.\n")
1787 $stderr.printf(" use subtype instead.\n")
1792 # Net::IMAP::BodyTypeMessage represents MESSAGE/RFC822 body structures of messages.
1796 # envelope:: Returns a Net::IMAP::Envelope giving the envelope structure.
1798 # body:: Returns an object giving the body structure.
1800 # And Net::IMAP::BodyTypeMessage has all methods of Net::IMAP::BodyTypeText.
1802 class BodyTypeMessage < Struct.new(:media_type, :subtype,
1803 :param, :content_id,
1804 :description, :encoding, :size,
1805 :envelope, :body, :lines,
1806 :md5, :disposition, :language,
1812 # Obsolete: use +subtype+ instead. Calling this will
1813 # generate a warning message to +stderr+, then return
1814 # the value of +subtype+.
1816 $stderr.printf("warning: media_subtype is obsolete.\n")
1817 $stderr.printf(" use subtype instead.\n")
1822 # Net::IMAP::BodyTypeMultipart represents multipart body structures
1827 # media_type:: Returns the content media type name as defined in [MIME-IMB].
1829 # subtype:: Returns the content subtype name as defined in [MIME-IMB].
1831 # parts:: Returns multiple parts.
1833 # param:: Returns a hash that represents parameters as defined in [MIME-IMB].
1835 # disposition:: Returns a Net::IMAP::ContentDisposition object giving
1836 # the content disposition.
1838 # language:: Returns a string or an array of strings giving the body
1839 # language value as defined in [LANGUAGE-TAGS].
1841 # extension:: Returns extension data.
1843 # multipart?:: Returns true.
1845 class BodyTypeMultipart < Struct.new(:media_type, :subtype,
1847 :param, :disposition, :language,
1853 # Obsolete: use +subtype+ instead. Calling this will
1854 # generate a warning message to +stderr+, then return
1855 # the value of +subtype+.
1857 $stderr.printf("warning: media_subtype is obsolete.\n")
1858 $stderr.printf(" use subtype instead.\n")
1863 class ResponseParser # :nodoc:
1867 @lex_state = EXPR_BEG
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
1891 T_LITERAL = :LITERAL
1893 T_PERCENT = :PERCENT
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"\\]|\\["\\])*)"|\
1906 (?# 8: BSLASH )(\\)|\
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(?:\
1919 (?# 3: NUMBER )(\d+)|\
1920 (?# 4: QUOTED )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\
1921 (?# 5: LITERAL )\{(\d+)\}\r\n|\
1923 (?# 7: RPAR )(\)))/ni
1925 TEXT_REGEXP = /\G(?:\
1926 (?# 1: TEXT )([^\x00\r\n]*))/ni
1928 RTEXT_REGEXP = /\G(?:\
1930 (?# 2: TEXT )([^\x00\r\n]*))/ni
1932 CTEXT_REGEXP = /\G(?:\
1933 (?# 1: TEXT )([^\x00\r\n\]]*))/ni
1935 Token = Struct.new(:symbol, :value)
1941 result = continue_req
1943 result = response_untagged
1945 result = response_tagged
1955 return ContinuationRequest.new(resp_text, @str)
1958 def response_untagged
1962 if token.symbol == T_NUMBER
1963 return numeric_response
1964 elsif token.symbol == T_ATOM
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
1987 return text_response
1990 parse_error("unexpected token %s", token.symbol)
1997 token = match(T_ATOM)
1998 name = token.value.upcase
2000 return TaggedResponse.new(tag, name, resp_text, @str)
2004 token = match(T_ATOM)
2005 name = token.value.upcase
2007 return UntaggedResponse.new(name, resp_text, @str)
2010 def numeric_response
2013 token = match(T_ATOM)
2014 name = token.value.upcase
2016 when "EXISTS", "RECENT", "EXPUNGE"
2017 return UntaggedResponse.new(name, n, @str)
2021 data = FetchData.new(n, msg_att)
2022 return UntaggedResponse.new(name, data, @str)
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
2055 parse_error("unknown attribute `%s'", token.value)
2063 token = match(T_ATOM)
2064 name = token.value.upcase
2066 return name, envelope
2070 @lex_state = EXPR_DATA
2072 if token.symbol == T_NIL
2083 sender = address_list
2085 reply_to = address_list
2093 in_reply_to = nstring
2095 message_id = nstring
2097 result = Envelope.new(date, subject, from, sender, reply_to,
2098 to, cc, bcc, in_reply_to, message_id)
2100 @lex_state = EXPR_BEG
2105 token = match(T_ATOM)
2106 name = token.value.upcase
2108 return name, flag_list
2111 def internaldate_data
2112 token = match(T_ATOM)
2113 name = token.value.upcase
2115 token = match(T_QUOTED)
2116 return name, token.value
2120 token = match(T_ATOM)
2121 name = token.value.upcase
2123 return name, nstring
2127 token = match(T_ATOM)
2128 name = token.value.upcase
2134 token = match(T_ATOM)
2135 name = token.value.upcase
2137 if token.symbol == T_SPACE
2141 name.concat(section)
2143 if token.symbol == T_ATOM
2144 name.concat(token.value)
2153 @lex_state = EXPR_DATA
2155 if token.symbol == T_NIL
2161 if token.symbol == T_LPAR
2162 result = body_type_mpart
2164 result = body_type_1part
2168 @lex_state = EXPR_BEG
2175 when /\A(?:TEXT)\z/ni
2176 return body_type_text
2177 when /\A(?:MESSAGE)\z/ni
2178 return body_type_msg
2180 return body_type_basic
2185 mtype, msubtype = media_type
2187 if token.symbol == T_RPAR
2188 return BodyTypeBasic.new(mtype, msubtype)
2191 param, content_id, desc, enc, size = body_fields
2192 md5, disposition, language, extension = body_ext_1part
2193 return BodyTypeBasic.new(mtype, msubtype,
2196 md5, disposition, language, extension)
2200 mtype, msubtype = media_type
2202 param, content_id, desc, enc, size = body_fields
2205 md5, disposition, language, extension = body_ext_1part
2206 return BodyTypeText.new(mtype, msubtype,
2210 md5, disposition, language, extension)
2214 mtype, msubtype = media_type
2216 param, content_id, desc, enc, size = body_fields
2223 md5, disposition, language, extension = body_ext_1part
2224 return BodyTypeMessage.new(mtype, msubtype,
2228 md5, disposition, language, extension)
2235 if token.symbol == T_SPACE
2242 msubtype = case_insensitive_string
2243 param, disposition, language, extension = body_ext_mpart
2244 return BodyTypeMultipart.new(mtype, msubtype, parts,
2245 param, disposition, language,
2250 mtype = case_insensitive_string
2252 msubtype = case_insensitive_string
2253 return mtype, msubtype
2257 param = body_fld_param
2259 content_id = nstring
2263 enc = case_insensitive_string
2266 return param, content_id, desc, enc, size
2271 if token.symbol == T_NIL
2286 name = case_insensitive_string
2296 if token.symbol == T_SPACE
2304 if token.symbol == T_SPACE
2309 disposition = body_fld_dsp
2312 if token.symbol == T_SPACE
2315 return md5, disposition
2317 language = body_fld_lang
2320 if token.symbol == T_SPACE
2323 return md5, disposition, language
2326 extension = body_extensions
2327 return md5, disposition, language, extension
2332 if token.symbol == T_SPACE
2337 param = body_fld_param
2340 if token.symbol == T_SPACE
2345 disposition = body_fld_dsp
2347 language = body_fld_lang
2350 if token.symbol == T_SPACE
2353 return param, disposition, language
2356 extension = body_extensions
2357 return param, disposition, language, extension
2362 if token.symbol == T_NIL
2367 dsp_type = case_insensitive_string
2369 param = body_fld_param
2371 return ContentDisposition.new(dsp_type, param)
2376 if token.symbol == T_LPAR
2388 result.push(case_insensitive_string)
2410 result.push(body_extension)
2419 result = body_extensions
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)
2438 str.concat(token.value)
2440 if token.symbol == T_SPACE
2442 str.concat(token.value)
2443 token = match(T_LPAR)
2444 str.concat(token.value)
2449 str.concat(token.value)
2454 str.concat(token.value)
2456 str.concat(format_string(astring))
2459 token = match(T_RBRA)
2460 str.concat(token.value)
2464 def format_string(str)
2468 when /[\x80-\xff\r\n]/n
2470 return "{" + str.length.to_s + "}" + CRLF + str
2471 when /[(){ \x00-\x1f\x7f%*"\\]/n
2473 return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"'
2481 token = match(T_ATOM)
2482 name = token.value.upcase
2488 token = match(T_ATOM)
2489 name = token.value.upcase
2491 @lex_state = EXPR_TEXT
2492 token = match(T_TEXT)
2493 @lex_state = EXPR_BEG
2494 return UntaggedResponse.new(name, token.value)
2498 token = match(T_ATOM)
2499 name = token.value.upcase
2501 return UntaggedResponse.new(name, flag_list, @str)
2505 token = match(T_ATOM)
2506 name = token.value.upcase
2508 return UntaggedResponse.new(name, mailbox_list, @str)
2514 token = match(T_QUOTED, T_NIL)
2515 if token.symbol == T_NIL
2522 return MailboxList.new(attr, delim, name)
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
2540 data = MailboxQuota.new(mailbox, nil, nil)
2541 return UntaggedResponse.new(name, data, @str)
2545 token = match(T_NUMBER)
2548 token = match(T_NUMBER)
2551 data = MailboxQuota.new(mailbox, usage, quota)
2552 return UntaggedResponse.new(name, data, @str)
2554 parse_error("unexpected token %s", token.symbol)
2558 def getquotaroot_response
2559 # Similar to getquota, but only admin can use getquota.
2560 token = match(T_ATOM)
2561 name = token.value.upcase
2567 break unless token.symbol == T_SPACE
2569 quotaroots.push(astring)
2571 data = MailboxQuotaRoot.new(mailbox, quotaroots)
2572 return UntaggedResponse.new(name, data, @str)
2576 token = match(T_ATOM)
2577 name = token.value.upcase
2582 if token.symbol == T_SPACE
2595 ##XXX data.push([user, rights])
2596 data.push(MailboxACLItem.new(user, rights))
2599 return UntaggedResponse.new(name, data, @str)
2603 token = match(T_ATOM)
2604 name = token.value.upcase
2606 if token.symbol == T_SPACE
2622 return UntaggedResponse.new(name, data, @str)
2626 token = match(T_ATOM)
2627 name = token.value.upcase
2630 if token.symbol == T_SPACE
2639 threads << thread_branch(token)
2649 return UntaggedResponse.new(name, threads, @str)
2652 def thread_branch(token)
2657 shift_token # ignore first T_LPAR
2663 newmember = ThreadMember.new(number, [])
2665 rootmember = newmember
2667 lastmember.children << newmember
2669 lastmember = newmember
2675 lastmember = rootmember = ThreadMember.new(nil, [])
2678 lastmember.children << thread_branch(token)
2688 token = match(T_ATOM)
2689 name = token.value.upcase
2704 token = match(T_ATOM)
2705 key = token.value.upcase
2710 data = StatusData.new(mailbox, attr)
2711 return UntaggedResponse.new(name, data, @str)
2714 def capability_response
2715 token = match(T_ATOM)
2716 name = token.value.upcase
2727 data.push(atom.upcase)
2729 return UntaggedResponse.new(name, data, @str)
2733 @lex_state = EXPR_RTEXT
2735 if token.symbol == T_LBRA
2736 code = resp_text_code
2740 token = match(T_TEXT)
2741 @lex_state = EXPR_BEG
2742 return ResponseText.new(code, token.value)
2746 @lex_state = EXPR_BEG
2748 token = match(T_ATOM)
2749 name = token.value.upcase
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
2755 result = ResponseCode.new(name, flag_list)
2756 when /\A(?:UIDVALIDITY|UIDNEXT|UNSEEN)\z/n
2758 result = ResponseCode.new(name, number)
2761 @lex_state = EXPR_CTEXT
2762 token = match(T_TEXT)
2763 @lex_state = EXPR_BEG
2764 result = ResponseCode.new(name, token.value)
2767 @lex_state = EXPR_RTEXT
2773 if token.symbol == T_NIL
2788 result.push(address)
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"\\]|\\["\\])*)")\
2803 if @str.index(ADDRESS_REGEXP, @pos)
2804 # address does not include literal.
2810 for s in [name, route, mailbox, host]
2812 s.gsub!(/\\(["\\])/n, "\\1")
2825 return Address.new(name, route, mailbox, host)
2847 # if token.symbol == T_BSLASH
2850 # if token.symbol == T_STAR
2852 # return token.value.intern
2854 # return atom.intern
2862 (?# FLAG )\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)|\
2863 (?# ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\]+)/n
2866 if @str.index(/\(([^)]*)\)/ni, @pos)
2868 return $1.scan(FLAG_REGEXP).collect { |flag, atom|
2869 atom || flag.capitalize.intern
2872 parse_error("invalid flag list")
2878 if token.symbol == T_NIL
2888 if string_token?(token)
2897 if token.symbol == T_NIL
2901 token = match(T_QUOTED, T_LITERAL)
2905 STRING_TOKENS = [T_QUOTED, T_LITERAL, T_NIL]
2907 def string_token?(token)
2908 return STRING_TOKENS.include?(token.symbol)
2911 def case_insensitive_string
2913 if token.symbol == T_NIL
2917 token = match(T_QUOTED, T_LITERAL)
2918 return token.value.upcase
2925 if atom_token?(token)
2926 result.concat(token.value)
2930 parse_error("unexpected token %s", token.symbol)
2947 def atom_token?(token)
2948 return ATOM_TOKENS.include?(token.symbol)
2953 if token.symbol == T_NIL
2957 token = match(T_NUMBER)
2958 return token.value.to_i
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 "))
2991 if @str.index(BEG_REGEXP, @pos)
2994 return Token.new(T_SPACE, $+)
2996 return Token.new(T_NIL, $+)
2998 return Token.new(T_NUMBER, $+)
3000 return Token.new(T_ATOM, $+)
3002 return Token.new(T_QUOTED,
3003 $+.gsub(/\\(["\\])/n, "\\1"))
3005 return Token.new(T_LPAR, $+)
3007 return Token.new(T_RPAR, $+)
3009 return Token.new(T_BSLASH, $+)
3011 return Token.new(T_STAR, $+)
3013 return Token.new(T_LBRA, $+)
3015 return Token.new(T_RBRA, $+)
3018 val = @str[@pos, len]
3020 return Token.new(T_LITERAL, val)
3022 return Token.new(T_PLUS, $+)
3024 return Token.new(T_PERCENT, $+)
3026 return Token.new(T_CRLF, $+)
3028 return Token.new(T_EOF, $+)
3030 parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid")
3033 @str.index(/\S*/n, @pos)
3034 parse_error("unknown token - %s", $&.dump)
3037 if @str.index(DATA_REGEXP, @pos)
3040 return Token.new(T_SPACE, $+)
3042 return Token.new(T_NIL, $+)
3044 return Token.new(T_NUMBER, $+)
3046 return Token.new(T_QUOTED,
3047 $+.gsub(/\\(["\\])/n, "\\1"))
3050 val = @str[@pos, len]
3052 return Token.new(T_LITERAL, val)
3054 return Token.new(T_LPAR, $+)
3056 return Token.new(T_RPAR, $+)
3058 parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid")
3061 @str.index(/\S*/n, @pos)
3062 parse_error("unknown token - %s", $&.dump)
3065 if @str.index(TEXT_REGEXP, @pos)
3068 return Token.new(T_TEXT, $+)
3070 parse_error("[Net::IMAP BUG] TEXT_REGEXP is invalid")
3073 @str.index(/\S*/n, @pos)
3074 parse_error("unknown token - %s", $&.dump)
3077 if @str.index(RTEXT_REGEXP, @pos)
3080 return Token.new(T_LBRA, $+)
3082 return Token.new(T_TEXT, $+)
3084 parse_error("[Net::IMAP BUG] RTEXT_REGEXP is invalid")
3087 @str.index(/\S*/n, @pos)
3088 parse_error("unknown token - %s", $&.dump)
3091 if @str.index(CTEXT_REGEXP, @pos)
3094 return Token.new(T_TEXT, $+)
3096 parse_error("[Net::IMAP BUG] CTEXT_REGEXP is invalid")
3099 @str.index(/\S*/n, @pos) #/
3100 parse_error("unknown token - %s", $&.dump)
3103 parse_error("illegal @lex_state - %s", @lex_state.inspect)
3107 def parse_error(fmt, *args)
3109 $stderr.printf("@str: %s\n", @str.dump)
3110 $stderr.printf("@pos: %d\n", @pos)
3111 $stderr.printf("@lex_state: %s\n", @lex_state)
3113 $stderr.printf("@token.symbol: %s\n", @token.symbol)
3114 $stderr.printf("@token.value: %s\n", @token.value.inspect)
3117 raise ResponseParseError, format(fmt, *args)
3121 # Authenticator for the "LOGIN" authentication type. See
3123 class LoginAuthenticator
3127 @state = STATE_PASSWORD
3137 STATE_PASSWORD = :PASSWORD
3139 def initialize(user, password)
3141 @password = password
3145 add_authenticator "LOGIN", LoginAuthenticator
3147 # Authenticator for the "CRAM-MD5" authentication type. See
3149 class CramMD5Authenticator
3150 def process(challenge)
3151 digest = hmac_md5(challenge, @password)
3152 return @user + " " + digest
3157 def initialize(user, password)
3159 @password = password
3162 def hmac_md5(text, key)
3164 key = Digest::MD5.digest(key)
3167 k_ipad = key + "\0" * (64 - key.length)
3168 k_opad = key + "\0" * (64 - key.length)
3174 digest = Digest::MD5.digest(k_ipad + text)
3176 return Digest::MD5.hexdigest(k_opad + digest)
3179 add_authenticator "CRAM-MD5", CramMD5Authenticator
3181 # Superclass of IMAP errors.
3182 class Error < StandardError
3185 # Error raised when data is in the incorrect format.
3186 class DataFormatError < Error
3189 # Error raised when a response from the server is non-parseable.
3190 class ResponseParseError < Error
3193 # Superclass of all errors used to encapsulate "fail" responses
3195 class ResponseError < Error
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
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
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
3219 require "getoptlong"
3223 $user = ENV["USER"] || ENV["LOGNAME"]
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
3241 system("stty", "-echo")
3245 system("stty", "echo")
3251 printf("%s@%s> ", $user, $host)
3253 return line.strip.split(/\s+/)
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])
3267 parser.each_option do |name, arg|
3278 Net::IMAP.debug = true
3294 $port ||= $ssl ? 993 : 143
3296 imap = Net::IMAP.new($host, $port, $ssl)
3298 password = get_password
3299 imap.authenticate($auth, $user, password)
3301 cmd, *args = get_command
3306 for mbox in imap.list("", args[0] || "*")
3307 if mbox.attr.include?(Net::IMAP::NOSELECT)
3309 elsif mbox.attr.include?(Net::IMAP::MARKED)
3314 print prefix, mbox.name, "\n"
3317 imap.select(args[0] || "inbox")
3323 unless messages = imap.responses["EXISTS"][-1]
3328 for data in imap.fetch(1..-1, ["ENVELOPE"])
3329 print data.seqno, ": ", data.attr["ENVELOPE"].subject, "\n"
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"]
3340 puts "missing argument"
3342 when "logout", "exit", "quit"
3346 list [pattern] list mailboxes
3347 select [mailbox] select mailbox
3349 summary display summary
3350 fetch [msgno] display message
3352 help, ? display help message
3355 print "unknown command: ", cmd, "\n"
3357 rescue Net::IMAP::Error