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.
28 # Net::IMAP implements Internet Message Access Protocol (IMAP) client
29 # functionality. The protocol is described in [IMAP].
33 # An IMAP client connects to a server, and then authenticates
34 # itself using either #authenticate() or #login(). Having
35 # authenticated itself, there is a range of commands
36 # available to it. Most work with mailboxes, which may be
37 # arranged in an hierarchical namespace, and each of which
38 # contains zero or more messages. How this is implemented on
39 # the server is implementation-dependent; on a UNIX server, it
40 # will frequently be implemented as a files in mailbox format
41 # within a hierarchy of directories.
43 # To work on the messages within a mailbox, the client must
44 # first select that mailbox, using either #select() or (for
45 # read-only access) #examine(). Once the client has successfully
46 # selected a mailbox, they enter _selected_ state, and that
47 # mailbox becomes the _current_ mailbox, on which mail-item
48 # related commands implicitly operate.
50 # Messages have two sorts of identifiers: message sequence
53 # Message sequence numbers number messages within a mail box
54 # from 1 up to the number of items in the mail box. If new
55 # message arrives during a session, it receives a sequence
56 # number equal to the new size of the mail box. If messages
57 # are expunged from the mailbox, remaining messages have their
58 # sequence numbers "shuffled down" to fill the gaps.
60 # UIDs, on the other hand, are permanently guaranteed not to
61 # identify another message within the same mailbox, even if
62 # the existing message is deleted. UIDs are required to
63 # be assigned in ascending (but not necessarily sequential)
64 # order within a mailbox; this means that if a non-IMAP client
65 # rearranges the order of mailitems within a mailbox, the
66 # UIDs have to be reassigned. An IMAP client cannot thus
67 # rearrange message orders.
69 # == Examples of Usage
71 # === List sender and subject of all recent messages in the default mailbox
73 # imap = Net::IMAP.new('mail.example.com')
74 # imap.authenticate('LOGIN', 'joe_user', 'joes_password')
75 # imap.examine('INBOX')
76 # imap.search(["RECENT"]).each do |message_id|
77 # envelope = imap.fetch(message_id, "ENVELOPE")[0].attr["ENVELOPE"]
78 # puts "#{envelope.from[0].name}: \t#{envelope.subject}"
81 # === Move all messages from April 2003 from "Mail/sent-mail" to "Mail/sent-apr03"
83 # imap = Net::IMAP.new('mail.example.com')
84 # imap.authenticate('LOGIN', 'joe_user', 'joes_password')
85 # imap.select('Mail/sent-mail')
86 # if not imap.list('Mail/', 'sent-apr03')
87 # imap.create('Mail/sent-apr03')
89 # imap.search(["BEFORE", "30-Apr-2003", "SINCE", "1-Apr-2003"]).each do |message_id|
90 # imap.copy(message_id, "Mail/sent-apr03")
91 # imap.store(message_id, "+FLAGS", [:Deleted])
97 # Net::IMAP supports concurrent threads. For example,
99 # imap = Net::IMAP.new("imap.foo.net", "imap2")
100 # imap.authenticate("cram-md5", "bar", "password")
101 # imap.select("inbox")
102 # fetch_thread = Thread.start { imap.fetch(1..-1, "UID") }
103 # search_result = imap.search(["BODY", "hello"])
104 # fetch_result = fetch_thread.value
107 # This script invokes the FETCH command and the SEARCH command concurrently.
111 # An IMAP server can send three different types of responses to indicate
114 # NO:: the attempted command could not be successfully completed. For
115 # instance, the username/password used for logging in are incorrect;
116 # the selected mailbox does not exists; etc.
118 # BAD:: the request from the client does not follow the server's
119 # understanding of the IMAP protocol. This includes attempting
120 # commands from the wrong client state; for instance, attempting
121 # to perform a SEARCH command without having SELECTed a current
122 # mailbox. It can also signal an internal server
123 # failure (such as a disk crash) has occurred.
125 # BYE:: the server is saying goodbye. This can be part of a normal
126 # logout sequence, and can be used as part of a login sequence
127 # to indicate that the server is (for some reason) unwilling
128 # to accept our connection. As a response to any other command,
129 # it indicates either that the server is shutting down, or that
130 # the server is timing out the client connection due to inactivity.
132 # These three error response are represented by the errors
133 # Net::IMAP::NoResponseError, Net::IMAP::BadResponseError, and
134 # Net::IMAP::ByeResponseError, all of which are subclasses of
135 # Net::IMAP::ResponseError. Essentially, all methods that involve
136 # sending a request to the server can generate one of these errors.
137 # Only the most pertinent instances have been documented below.
139 # Because the IMAP class uses Sockets for communication, its methods
140 # are also susceptible to the various errors that can occur when
141 # working with sockets. These are generally represented as
142 # Errno errors. For instance, any method that involves sending a
143 # request to the server and/or receiving a response from it could
144 # raise an Errno::EPIPE error if the network connection unexpectedly
145 # goes down. See the socket(7), ip(7), tcp(7), socket(2), connect(2),
146 # and associated man pages.
148 # Finally, a Net::IMAP::DataFormatError is thrown if low-level data
149 # is found to be in an incorrect format (for instance, when converting
150 # between UTF-8 and UTF-16), and Net::IMAP::ResponseParseError is
151 # thrown if a server response is non-parseable.
157 # M. Crispin, "INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1",
158 # RFC 2060, December 1996. (Note: since obsoleted by RFC 3501)
161 # Alvestrand, H., "Tags for the Identification of
162 # Languages", RFC 1766, March 1995.
165 # Myers, J., and M. Rose, "The Content-MD5 Header Field", RFC
166 # 1864, October 1995.
169 # Freed, N., and N. Borenstein, "MIME (Multipurpose Internet
170 # Mail Extensions) Part One: Format of Internet Message Bodies", RFC
171 # 2045, November 1996.
174 # Crocker, D., "Standard for the Format of ARPA Internet Text
175 # Messages", STD 11, RFC 822, University of Delaware, August 1982.
178 # Myers, J., "IMAP4 QUOTA extension", RFC 2087, January 1997.
181 # Myers, J., "IMAP4 ACL extension", RFC 2086, January 1997.
184 # Klensin, J., Catoe, R., and Krumviede, P., "IMAP/POP AUTHorize Extension
185 # for Simple Challenge/Response", RFC 2195, September 1997.
187 # [[SORT-THREAD-EXT]]
188 # Crispin, M., "INTERNET MESSAGE ACCESS PROTOCOL - SORT and THREAD
189 # Extensions", draft-ietf-imapext-sort, May 2003.
192 # http://www.openssl.org
195 # http://savannah.gnu.org/projects/rubypki
198 # Goldsmith, D. and Davis, M., "UTF-7: A Mail-Safe Transformation Format of
199 # Unicode", RFC 2152, May 1997.
208 # Returns an initial greeting response from the server.
209 attr_reader :greeting
211 # Returns recorded untagged responses. For example:
213 # imap.select("inbox")
214 # p imap.responses["EXISTS"][-1]
216 # p imap.responses["UIDVALIDITY"][-1]
218 attr_reader :responses
220 # Returns all response handlers.
221 attr_reader :response_handlers
223 # The thread to receive exceptions.
224 attr_accessor :client_thread
226 # Flag indicating a message has been seen
229 # Flag indicating a message has been answered
232 # Flag indicating a message has been flagged for special or urgent
236 # Flag indicating a message has been marked for deletion. This
237 # will occur when the mailbox is closed or expunged.
240 # Flag indicating a message is only a draft or work-in-progress version.
243 # Flag indicating that the message is "recent", meaning that this
244 # session is the first session in which the client has been notified
248 # Flag indicating that a mailbox context name cannot contain
250 NOINFERIORS = :Noinferiors
252 # Flag indicating that a mailbox is not selected.
255 # Flag indicating that a mailbox has been marked "interesting" by
256 # the server; this commonly indicates that the mailbox contains
260 # Flag indicating that the mailbox does not contains new messages.
263 # Returns the debug mode.
268 # Sets the debug mode.
273 # Adds an authenticator for Net::IMAP#authenticate. +auth_type+
274 # is the type of authentication this authenticator supports
275 # (for instance, "LOGIN"). The +authenticator+ is an object
276 # which defines a process() method to handle authentication with
277 # the server. See Net::IMAP::LoginAuthenticator,
278 # Net::IMAP::CramMD5Authenticator, and Net::IMAP::DigestMD5Authenticator
282 # If +auth_type+ refers to an existing authenticator, it will be
283 # replaced by the new one.
284 def self.add_authenticator(auth_type, authenticator)
285 @@authenticators[auth_type] = authenticator
288 # Disconnects from the server.
291 # try to call SSL::SSLSocket#io.
294 # @sock is not an SSL::SSLSocket.
297 @receiver_thread.join
301 # Returns true if disconnected from the server.
306 # Sends a CAPABILITY command, and returns an array of
307 # capabilities that the server supports. Each capability
308 # is a string. See [IMAP] for a list of possible
311 # Note that the Net::IMAP class does not modify its
312 # behaviour according to the capabilities of the server;
313 # it is up to the user of the class to ensure that
314 # a certain capability is supported by a server before
318 send_command("CAPABILITY")
319 return @responses.delete("CAPABILITY")[-1]
323 # Sends a NOOP command to the server. It does nothing.
328 # Sends a LOGOUT command to inform the server that the client is
329 # done with the connection.
331 send_command("LOGOUT")
334 # Sends a STARTTLS command to start TLS session.
335 def starttls(options = {}, verify = true)
336 send_command("STARTTLS") do |resp|
337 if resp.kind_of?(TaggedResponse) && resp.name == "OK"
339 # for backward compatibility
340 certs = options.to_str
341 options = create_ssl_params(certs, verify)
344 start_tls_session(options)
349 # Sends an AUTHENTICATE command to authenticate the client.
350 # The +auth_type+ parameter is a string that represents
351 # the authentication mechanism to be used. Currently Net::IMAP
352 # supports authentication mechanisms:
354 # LOGIN:: login using cleartext user and password.
355 # CRAM-MD5:: login with cleartext user and encrypted password
356 # (see [RFC-2195] for a full description). This
357 # mechanism requires that the server have the user's
358 # password stored in clear-text password.
360 # For both these mechanisms, there should be two +args+: username
361 # and (cleartext) password. A server may not support one or other
362 # of these mechanisms; check #capability() for a capability of
363 # the form "AUTH=LOGIN" or "AUTH=CRAM-MD5".
365 # Authentication is done using the appropriate authenticator object:
366 # see @@authenticators for more information on plugging in your own
371 # imap.authenticate('LOGIN', user, password)
373 # A Net::IMAP::NoResponseError is raised if authentication fails.
374 def authenticate(auth_type, *args)
375 auth_type = auth_type.upcase
376 unless @@authenticators.has_key?(auth_type)
378 format('unknown auth type - "%s"', auth_type)
380 authenticator = @@authenticators[auth_type].new(*args)
381 send_command("AUTHENTICATE", auth_type) do |resp|
382 if resp.instance_of?(ContinuationRequest)
383 data = authenticator.process(resp.data.text.unpack("m")[0])
384 s = [data].pack("m").gsub(/\n/, "")
391 # Sends a LOGIN command to identify the client and carries
392 # the plaintext +password+ authenticating this +user+. Note
393 # that, unlike calling #authenticate() with an +auth_type+
394 # of "LOGIN", #login() does *not* use the login authenticator.
396 # A Net::IMAP::NoResponseError is raised if authentication fails.
397 def login(user, password)
398 send_command("LOGIN", user, password)
401 # Sends a SELECT command to select a +mailbox+ so that messages
402 # in the +mailbox+ can be accessed.
404 # After you have selected a mailbox, you may retrieve the
405 # number of items in that mailbox from @responses["EXISTS"][-1],
406 # and the number of recent messages from @responses["RECENT"][-1].
407 # Note that these values can change if new messages arrive
408 # during a session; see #add_response_handler() for a way of
409 # detecting this event.
411 # A Net::IMAP::NoResponseError is raised if the mailbox does not
412 # exist or is for some reason non-selectable.
416 send_command("SELECT", mailbox)
420 # Sends a EXAMINE command to select a +mailbox+ so that messages
421 # in the +mailbox+ can be accessed. Behaves the same as #select(),
422 # except that the selected +mailbox+ is identified as read-only.
424 # A Net::IMAP::NoResponseError is raised if the mailbox does not
425 # exist or is for some reason non-examinable.
429 send_command("EXAMINE", mailbox)
433 # Sends a CREATE command to create a new +mailbox+.
435 # A Net::IMAP::NoResponseError is raised if a mailbox with that name
438 send_command("CREATE", mailbox)
441 # Sends a DELETE command to remove the +mailbox+.
443 # A Net::IMAP::NoResponseError is raised if a mailbox with that name
444 # cannot be deleted, either because it does not exist or because the
445 # client does not have permission to delete it.
447 send_command("DELETE", mailbox)
450 # Sends a RENAME command to change the name of the +mailbox+ to
453 # A Net::IMAP::NoResponseError is raised if a mailbox with the
454 # name +mailbox+ cannot be renamed to +newname+ for whatever
455 # reason; for instance, because +mailbox+ does not exist, or
456 # because there is already a mailbox with the name +newname+.
457 def rename(mailbox, newname)
458 send_command("RENAME", mailbox, newname)
461 # Sends a SUBSCRIBE command to add the specified +mailbox+ name to
462 # the server's set of "active" or "subscribed" mailboxes as returned
465 # A Net::IMAP::NoResponseError is raised if +mailbox+ cannot be
466 # subscribed to, for instance because it does not exist.
467 def subscribe(mailbox)
468 send_command("SUBSCRIBE", mailbox)
471 # Sends a UNSUBSCRIBE command to remove the specified +mailbox+ name
472 # from the server's set of "active" or "subscribed" mailboxes.
474 # A Net::IMAP::NoResponseError is raised if +mailbox+ cannot be
475 # unsubscribed from, for instance because the client is not currently
477 def unsubscribe(mailbox)
478 send_command("UNSUBSCRIBE", mailbox)
481 # Sends a LIST command, and returns a subset of names from
482 # the complete set of all names available to the client.
483 # +refname+ provides a context (for instance, a base directory
484 # in a directory-based mailbox hierarchy). +mailbox+ specifies
485 # a mailbox or (via wildcards) mailboxes under that context.
486 # Two wildcards may be used in +mailbox+: '*', which matches
487 # all characters *including* the hierarchy delimiter (for instance,
488 # '/' on a UNIX-hosted directory-based mailbox hierarchy); and '%',
489 # which matches all characters *except* the hierarchy delimiter.
491 # If +refname+ is empty, +mailbox+ is used directly to determine
492 # which mailboxes to match. If +mailbox+ is empty, the root
493 # name of +refname+ and the hierarchy delimiter are returned.
495 # The return value is an array of +Net::IMAP::MailboxList+. For example:
497 # imap.create("foo/bar")
498 # imap.create("foo/baz")
499 # p imap.list("", "foo/%")
500 # #=> [#<Net::IMAP::MailboxList attr=[:Noselect], delim="/", name="foo/">, \\
501 # #<Net::IMAP::MailboxList attr=[:Noinferiors, :Marked], delim="/", name="foo/bar">, \\
502 # #<Net::IMAP::MailboxList attr=[:Noinferiors], delim="/", name="foo/baz">]
503 def list(refname, mailbox)
505 send_command("LIST", refname, mailbox)
506 return @responses.delete("LIST")
510 # Sends the GETQUOTAROOT command along with specified +mailbox+.
511 # This command is generally available to both admin and user.
512 # If mailbox exists, returns an array containing objects of
513 # Net::IMAP::MailboxQuotaRoot and Net::IMAP::MailboxQuota.
514 def getquotaroot(mailbox)
516 send_command("GETQUOTAROOT", mailbox)
518 result.concat(@responses.delete("QUOTAROOT"))
519 result.concat(@responses.delete("QUOTA"))
524 # Sends the GETQUOTA command along with specified +mailbox+.
525 # If this mailbox exists, then an array containing a
526 # Net::IMAP::MailboxQuota object is returned. This
527 # command generally is only available to server admin.
528 def getquota(mailbox)
530 send_command("GETQUOTA", mailbox)
531 return @responses.delete("QUOTA")
535 # Sends a SETQUOTA command along with the specified +mailbox+ and
536 # +quota+. If +quota+ is nil, then quota will be unset for that
537 # mailbox. Typically one needs to be logged in as server admin
538 # for this to work. The IMAP quota commands are described in
540 def setquota(mailbox, quota)
544 data = '(STORAGE ' + quota.to_s + ')'
546 send_command("SETQUOTA", mailbox, RawData.new(data))
549 # Sends the SETACL command along with +mailbox+, +user+ and the
550 # +rights+ that user is to have on that mailbox. If +rights+ is nil,
551 # then that user will be stripped of any rights to that mailbox.
552 # The IMAP ACL commands are described in [RFC-2086].
553 def setacl(mailbox, user, rights)
555 send_command("SETACL", mailbox, user, "")
557 send_command("SETACL", mailbox, user, rights)
561 # Send the GETACL command along with specified +mailbox+.
562 # If this mailbox exists, an array containing objects of
563 # Net::IMAP::MailboxACLItem will be returned.
566 send_command("GETACL", mailbox)
567 return @responses.delete("ACL")[-1]
571 # Sends a LSUB command, and returns a subset of names from the set
572 # of names that the user has declared as being "active" or
573 # "subscribed". +refname+ and +mailbox+ are interpreted as
575 # The return value is an array of +Net::IMAP::MailboxList+.
576 def lsub(refname, mailbox)
578 send_command("LSUB", refname, mailbox)
579 return @responses.delete("LSUB")
583 # Sends a STATUS command, and returns the status of the indicated
584 # +mailbox+. +attr+ is a list of one or more attributes that
585 # we are request the status of. Supported attributes include:
587 # MESSAGES:: the number of messages in the mailbox.
588 # RECENT:: the number of recent messages in the mailbox.
589 # UNSEEN:: the number of unseen messages in the mailbox.
591 # The return value is a hash of attributes. For example:
593 # p imap.status("inbox", ["MESSAGES", "RECENT"])
594 # #=> {"RECENT"=>0, "MESSAGES"=>44}
596 # A Net::IMAP::NoResponseError is raised if status values
597 # for +mailbox+ cannot be returned, for instance because it
599 def status(mailbox, attr)
601 send_command("STATUS", mailbox, attr)
602 return @responses.delete("STATUS")[-1].attr
606 # Sends a APPEND command to append the +message+ to the end of
607 # the +mailbox+. The optional +flags+ argument is an array of
608 # flags to initially passing to the new message. The optional
609 # +date_time+ argument specifies the creation time to assign to the
610 # new message; it defaults to the current time.
613 # imap.append("inbox", <<EOF.gsub(/\n/, "\r\n"), [:Seen], Time.now)
615 # From: shugo@ruby-lang.org
616 # To: shugo@ruby-lang.org
621 # A Net::IMAP::NoResponseError is raised if the mailbox does
622 # not exist (it is not created automatically), or if the flags,
623 # date_time, or message arguments contain errors.
624 def append(mailbox, message, flags = nil, date_time = nil)
629 args.push(date_time) if date_time
630 args.push(Literal.new(message))
631 send_command("APPEND", mailbox, *args)
634 # Sends a CHECK command to request a checkpoint of the currently
635 # selected mailbox. This performs implementation-specific
636 # housekeeping, for instance, reconciling the mailbox's
637 # in-memory and on-disk state.
639 send_command("CHECK")
642 # Sends a CLOSE command to close the currently selected mailbox.
643 # The CLOSE command permanently removes from the mailbox all
644 # messages that have the \Deleted flag set.
646 send_command("CLOSE")
649 # Sends a EXPUNGE command to permanently remove from the currently
650 # selected mailbox all messages that have the \Deleted flag set.
653 send_command("EXPUNGE")
654 return @responses.delete("EXPUNGE")
658 # Sends a SEARCH command to search the mailbox for messages that
659 # match the given searching criteria, and returns message sequence
660 # numbers. +keys+ can either be a string holding the entire
661 # search string, or a single-dimension array of search keywords and
662 # arguments. The following are some common search criteria;
663 # see [IMAP] section 6.4.4 for a full list.
665 # <message set>:: a set of message sequence numbers. ',' indicates
666 # an interval, ':' indicates a range. For instance,
667 # '2,10:12,15' means "2,10,11,12,15".
669 # BEFORE <date>:: messages with an internal date strictly before
670 # <date>. The date argument has a format similar
673 # BODY <string>:: messages that contain <string> within their body.
675 # CC <string>:: messages containing <string> in their CC field.
677 # FROM <string>:: messages that contain <string> in their FROM field.
679 # NEW:: messages with the \Recent, but not the \Seen, flag set.
681 # NOT <search-key>:: negate the following search key.
683 # OR <search-key> <search-key>:: "or" two search keys together.
685 # ON <date>:: messages with an internal date exactly equal to <date>,
686 # which has a format similar to 8-Aug-2002.
688 # SINCE <date>:: messages with an internal date on or after <date>.
690 # SUBJECT <string>:: messages with <string> in their subject.
692 # TO <string>:: messages with <string> in their TO field.
696 # p imap.search(["SUBJECT", "hello", "NOT", "NEW"])
698 def search(keys, charset = nil)
699 return search_internal("SEARCH", keys, charset)
702 # As for #search(), but returns unique identifiers.
703 def uid_search(keys, charset = nil)
704 return search_internal("UID SEARCH", keys, charset)
707 # Sends a FETCH command to retrieve data associated with a message
708 # in the mailbox. The +set+ parameter is a number or an array of
709 # numbers or a Range object. The number is a message sequence
710 # number. +attr+ is a list of attributes to fetch; see the
711 # documentation for Net::IMAP::FetchData for a list of valid
713 # The return value is an array of Net::IMAP::FetchData. For example:
715 # p imap.fetch(6..8, "UID")
716 # #=> [#<Net::IMAP::FetchData seqno=6, attr={"UID"=>98}>, \\
717 # #<Net::IMAP::FetchData seqno=7, attr={"UID"=>99}>, \\
718 # #<Net::IMAP::FetchData seqno=8, attr={"UID"=>100}>]
719 # p imap.fetch(6, "BODY[HEADER.FIELDS (SUBJECT)]")
720 # #=> [#<Net::IMAP::FetchData seqno=6, attr={"BODY[HEADER.FIELDS (SUBJECT)]"=>"Subject: test\r\n\r\n"}>]
721 # data = imap.uid_fetch(98, ["RFC822.SIZE", "INTERNALDATE"])[0]
724 # p data.attr["RFC822.SIZE"]
726 # p data.attr["INTERNALDATE"]
727 # #=> "12-Oct-2000 22:40:59 +0900"
731 return fetch_internal("FETCH", set, attr)
734 # As for #fetch(), but +set+ contains unique identifiers.
735 def uid_fetch(set, attr)
736 return fetch_internal("UID FETCH", set, attr)
739 # Sends a STORE command to alter data associated with messages
740 # in the mailbox, in particular their flags. The +set+ parameter
741 # is a number or an array of numbers or a Range object. Each number
742 # is a message sequence number. +attr+ is the name of a data item
743 # to store: 'FLAGS' means to replace the message's flag list
744 # with the provided one; '+FLAGS' means to add the provided flags;
745 # and '-FLAGS' means to remove them. +flags+ is a list of flags.
747 # The return value is an array of Net::IMAP::FetchData. For example:
749 # p imap.store(6..8, "+FLAGS", [:Deleted])
750 # #=> [#<Net::IMAP::FetchData seqno=6, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\
751 # #<Net::IMAP::FetchData seqno=7, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\
752 # #<Net::IMAP::FetchData seqno=8, attr={"FLAGS"=>[:Seen, :Deleted]}>]
753 def store(set, attr, flags)
754 return store_internal("STORE", set, attr, flags)
757 # As for #store(), but +set+ contains unique identifiers.
758 def uid_store(set, attr, flags)
759 return store_internal("UID STORE", set, attr, flags)
762 # Sends a COPY command to copy the specified message(s) to the end
763 # of the specified destination +mailbox+. The +set+ parameter is
764 # a number or an array of numbers or a Range object. The number is
765 # a message sequence number.
766 def copy(set, mailbox)
767 copy_internal("COPY", set, mailbox)
770 # As for #copy(), but +set+ contains unique identifiers.
771 def uid_copy(set, mailbox)
772 copy_internal("UID COPY", set, mailbox)
775 # Sends a SORT command to sort messages in the mailbox.
776 # Returns an array of message sequence numbers. For example:
778 # p imap.sort(["FROM"], ["ALL"], "US-ASCII")
779 # #=> [1, 2, 3, 5, 6, 7, 8, 4, 9]
780 # p imap.sort(["DATE"], ["SUBJECT", "hello"], "US-ASCII")
783 # See [SORT-THREAD-EXT] for more details.
784 def sort(sort_keys, search_keys, charset)
785 return sort_internal("SORT", sort_keys, search_keys, charset)
788 # As for #sort(), but returns an array of unique identifiers.
789 def uid_sort(sort_keys, search_keys, charset)
790 return sort_internal("UID SORT", sort_keys, search_keys, charset)
793 # Adds a response handler. For example, to detect when
794 # the server sends us a new EXISTS response (which normally
795 # indicates new messages being added to the mail box),
796 # you could add the following handler after selecting the
799 # imap.add_response_handler { |resp|
800 # if resp.kind_of?(Net::IMAP::UntaggedResponse) and resp.name == "EXISTS"
801 # puts "Mailbox now has #{resp.data} messages"
805 def add_response_handler(handler = Proc.new)
806 @response_handlers.push(handler)
809 # Removes the response handler.
810 def remove_response_handler(handler)
811 @response_handlers.delete(handler)
814 # As for #search(), but returns message sequence numbers in threaded
815 # format, as a Net::IMAP::ThreadMember tree. The supported algorithms
818 # ORDEREDSUBJECT:: split into single-level threads according to subject,
820 # REFERENCES:: split into threads by parent/child relationships determined
821 # by which message is a reply to which.
823 # Unlike #search(), +charset+ is a required argument. US-ASCII
824 # and UTF-8 are sample values.
826 # See [SORT-THREAD-EXT] for more details.
827 def thread(algorithm, search_keys, charset)
828 return thread_internal("THREAD", algorithm, search_keys, charset)
831 # As for #thread(), but returns unique identifiers instead of
832 # message sequence numbers.
833 def uid_thread(algorithm, search_keys, charset)
834 return thread_internal("UID THREAD", algorithm, search_keys, charset)
837 # Decode a string from modified UTF-7 format to UTF-8.
839 # UTF-7 is a 7-bit encoding of Unicode [UTF7]. IMAP uses a
840 # slightly modified version of this to encode mailbox names
841 # containing non-ASCII characters; see [IMAP] section 5.1.3.
843 # Net::IMAP does _not_ automatically encode and decode
844 # mailbox names to and from utf7.
845 def self.decode_utf7(s)
846 return s.gsub(/&(.*?)-/n) {
850 base64 = $1.tr(",", "/")
851 x = base64.length % 4
853 base64.concat("=" * (4 - x))
855 base64.unpack("m")[0].unpack("n*").pack("U*")
857 }.force_encoding("UTF-8")
860 # Encode a string from UTF-8 format to modified UTF-7.
861 def self.encode_utf7(s)
862 return s.gsub(/(&)|([^\x20-\x25\x27-\x7e]+)/u) {
866 base64 = [$&.unpack("U*").pack("n*")].pack("m")
867 "&" + base64.delete("=\n").tr("/", ",") + "-"
869 }.force_encoding("ASCII-8BIT")
874 CRLF = "\r\n" # :nodoc:
876 SSL_PORT = 993 # :nodoc:
879 @@authenticators = {}
882 # Net::IMAP.new(host, options = {})
884 # Creates a new Net::IMAP object and connects it to the specified
887 # +options+ is an option hash, each key of which is a symbol.
889 # The available options are:
891 # port:: port number (default value is 143 for imap, or 993 for imaps)
892 # ssl:: if options[:ssl] is true, then an attempt will be made
893 # to use SSL (now TLS) to connect to the server. For this to work
894 # OpenSSL [OSSL] and the Ruby OpenSSL [RSSL] extensions need to
896 # if options[:ssl] is a hash, it's passed to
897 # OpenSSL::SSL::SSLContext#set_params as parameters.
899 # The most common errors are:
901 # Errno::ECONNREFUSED:: connection refused by +host+ or an intervening
903 # Errno::ETIMEDOUT:: connection timed out (possibly due to packets
904 # being dropped by an intervening firewall).
905 # Errno::ENETUNREACH:: there is no route to that network.
906 # SocketError:: hostname not known or other socket error.
907 # Net::IMAP::ByeResponseError:: we connected to the host, but they
908 # immediately said goodbye to us.
909 def initialize(host, port_or_options = {},
910 usessl = false, certs = nil, verify = true)
914 options = port_or_options.to_hash
916 # for backward compatibility
918 options[:port] = port_or_options
920 options[:ssl] = create_ssl_params(certs, verify)
923 @port = options[:port] || (options[:ssl] ? SSL_PORT : PORT)
926 @parser = ResponseParser.new
927 @sock = TCPSocket.open(@host, @port)
929 start_tls_session(options[:ssl])
934 @responses = Hash.new([].freeze)
935 @tagged_responses = {}
936 @response_handlers = []
937 @tagged_response_arrival = new_cond
938 @continuation_request_arrival = new_cond
939 @logout_command_tag = nil
940 @debug_output_bol = true
943 @greeting = get_response
944 if @greeting.name == "BYE"
946 raise ByeResponseError, @greeting.raw_data
949 @client_thread = Thread.current
950 @receiver_thread = Thread.start {
955 def receive_responses
962 rescue Exception => e
971 @exception = EOFError.new("end of file reached")
979 @tagged_responses[resp.tag] = resp
980 @tagged_response_arrival.broadcast
981 if resp.tag == @logout_command_tag
984 when UntaggedResponse
985 record_response(resp.name, resp.data)
986 if resp.data.instance_of?(ResponseText) &&
987 (code = resp.data.code)
988 record_response(code.name, code.data)
990 if resp.name == "BYE" && @logout_command_tag.nil?
992 @exception = ByeResponseError.new(resp.raw_data)
995 when ContinuationRequest
996 @continuation_request_arrival.signal
998 @response_handlers.each do |handler|
1002 rescue Exception => e
1005 @tagged_response_arrival.broadcast
1006 @continuation_request_arrival.broadcast
1011 @tagged_response_arrival.broadcast
1012 @continuation_request_arrival.broadcast
1016 def get_tagged_response(tag, cmd)
1017 until @tagged_responses.key?(tag)
1018 raise @exception if @exception
1019 @tagged_response_arrival.wait
1021 resp = @tagged_responses.delete(tag)
1024 raise NoResponseError, resp.data.text
1025 when /\A(?:BAD)\z/ni
1026 raise BadResponseError, resp.data.text
1035 s = @sock.gets(CRLF)
1038 if /\{(\d+)\}\r\n/n =~ s
1039 s = @sock.read($1.to_i)
1045 return nil if buff.length == 0
1047 $stderr.print(buff.gsub(/^/n, "S: "))
1049 return @parser.parse(buff)
1052 def record_response(name, data)
1053 unless @responses.has_key?(name)
1054 @responses[name] = []
1056 @responses[name].push(data)
1059 def send_command(cmd, *args, &block)
1062 put_string(tag + " " + cmd)
1069 @logout_command_tag = tag
1072 add_response_handler(block)
1075 return get_tagged_response(tag, cmd)
1078 remove_response_handler(block)
1086 return format("%s%04d", @tag_prefix, @tagno)
1092 if @debug_output_bol
1093 $stderr.print("C: ")
1095 $stderr.print(str.gsub(/\n(?!\z)/n, "\nC: "))
1096 if /\r\n\z/n.match(str)
1097 @debug_output_bol = true
1099 @debug_output_bol = false
1109 send_string_data(data)
1111 send_number_data(data)
1113 send_list_data(data)
1115 send_time_data(data)
1117 send_symbol_data(data)
1119 data.send_data(self)
1123 def send_string_data(str)
1127 when /[\x80-\xff\r\n]/n
1130 when /[(){ \x00-\x1f\x7f%*"\\]/n
1132 send_quoted_string(str)
1138 def send_quoted_string(str)
1139 put_string('"' + str.gsub(/["\\]/n, "\\\\\\&") + '"')
1142 def send_literal(str)
1143 put_string("{" + str.length.to_s + "}" + CRLF)
1144 @continuation_request_arrival.wait
1145 raise @exception if @exception
1149 def send_number_data(num)
1150 if num < 0 || num >= 4294967296
1151 raise DataFormatError, num.to_s
1153 put_string(num.to_s)
1156 def send_list_data(list)
1170 DATE_MONTH = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)
1172 def send_time_data(time)
1174 s = format('"%2d-%3s-%4d %02d:%02d:%02d +0000"',
1175 t.day, DATE_MONTH[t.month - 1], t.year,
1176 t.hour, t.min, t.sec)
1180 def send_symbol_data(symbol)
1181 put_string("\\" + symbol.to_s)
1184 def search_internal(cmd, keys, charset)
1185 if keys.instance_of?(String)
1186 keys = [RawData.new(keys)]
1188 normalize_searching_criteria(keys)
1192 send_command(cmd, "CHARSET", charset, *keys)
1194 send_command(cmd, *keys)
1196 return @responses.delete("SEARCH")[-1]
1200 def fetch_internal(cmd, set, attr)
1201 if attr.instance_of?(String)
1202 attr = RawData.new(attr)
1205 @responses.delete("FETCH")
1206 send_command(cmd, MessageSet.new(set), attr)
1207 return @responses.delete("FETCH")
1211 def store_internal(cmd, set, attr, flags)
1212 if attr.instance_of?(String)
1213 attr = RawData.new(attr)
1216 @responses.delete("FETCH")
1217 send_command(cmd, MessageSet.new(set), attr, flags)
1218 return @responses.delete("FETCH")
1222 def copy_internal(cmd, set, mailbox)
1223 send_command(cmd, MessageSet.new(set), mailbox)
1226 def sort_internal(cmd, sort_keys, search_keys, charset)
1227 if search_keys.instance_of?(String)
1228 search_keys = [RawData.new(search_keys)]
1230 normalize_searching_criteria(search_keys)
1232 normalize_searching_criteria(search_keys)
1234 send_command(cmd, sort_keys, charset, *search_keys)
1235 return @responses.delete("SORT")[-1]
1239 def thread_internal(cmd, algorithm, search_keys, charset)
1240 if search_keys.instance_of?(String)
1241 search_keys = [RawData.new(search_keys)]
1243 normalize_searching_criteria(search_keys)
1245 normalize_searching_criteria(search_keys)
1246 send_command(cmd, algorithm, charset, *search_keys)
1247 return @responses.delete("THREAD")[-1]
1250 def normalize_searching_criteria(keys)
1251 keys.collect! do |i|
1253 when -1, Range, Array
1261 def create_ssl_params(certs = nil, verify = true)
1264 if File.file?(certs)
1265 params[:ca_file] = certs
1266 elsif File.directory?(certs)
1267 params[:ca_path] = certs
1271 params[:verify_mode] = VERIFY_PEER
1273 params[:verify_mode] = VERIFY_NONE
1278 def start_tls_session(params = {})
1279 unless defined?(OpenSSL)
1280 raise "SSL extension not installed"
1282 if @sock.kind_of?(OpenSSL::SSL::SSLSocket)
1283 raise RuntimeError, "already using SSL"
1286 params = params.to_hash
1287 rescue NoMethodError
1290 context = SSLContext.new
1291 context.set_params(params)
1292 if defined?(VerifyCallbackProc)
1293 context.verify_callback = VerifyCallbackProc
1295 @sock = SSLSocket.new(@sock, context)
1296 @sock.sync_close = true
1298 if context.verify_mode != VERIFY_NONE
1299 @sock.post_connection_check(@host)
1303 class RawData # :nodoc:
1305 imap.send(:put_string, @data)
1310 def initialize(data)
1315 class Atom # :nodoc:
1317 imap.send(:put_string, @data)
1322 def initialize(data)
1327 class QuotedString # :nodoc:
1329 imap.send(:send_quoted_string, @data)
1334 def initialize(data)
1339 class Literal # :nodoc:
1341 imap.send(:send_literal, @data)
1346 def initialize(data)
1351 class MessageSet # :nodoc:
1353 imap.send(:put_string, format_internal(@data))
1358 def initialize(data)
1362 def format_internal(data)
1367 ensure_nz_number(data)
1374 return format_internal(data.first) +
1375 ":" + format_internal(data.last)
1377 return data.collect {|i| format_internal(i)}.join(",")
1379 return data.seqno.to_s +
1380 ":" + data.children.collect {|i| format_internal(i).join(",")}
1382 raise DataFormatError, data.inspect
1386 def ensure_nz_number(num)
1387 if num < -1 || num == 0 || num >= 4294967296
1388 msg = "nz_number must be non-zero unsigned 32-bit integer: " +
1390 raise DataFormatError, msg
1395 # Net::IMAP::ContinuationRequest represents command continuation requests.
1397 # The command continuation request response is indicated by a "+" token
1398 # instead of a tag. This form of response indicates that the server is
1399 # ready to accept the continuation of a command from the client. The
1400 # remainder of this response is a line of text.
1402 # continue_req ::= "+" SPACE (resp_text / base64)
1406 # data:: Returns the data (Net::IMAP::ResponseText).
1408 # raw_data:: Returns the raw data string.
1409 ContinuationRequest = Struct.new(:data, :raw_data)
1411 # Net::IMAP::UntaggedResponse represents untagged responses.
1413 # Data transmitted by the server to the client and status responses
1414 # that do not indicate command completion are prefixed with the token
1415 # "*", and are called untagged responses.
1417 # response_data ::= "*" SPACE (resp_cond_state / resp_cond_bye /
1418 # mailbox_data / message_data / capability_data)
1422 # name:: Returns the name such as "FLAGS", "LIST", "FETCH"....
1424 # data:: Returns the data such as an array of flag symbols,
1425 # a ((<Net::IMAP::MailboxList>)) object....
1427 # raw_data:: Returns the raw data string.
1428 UntaggedResponse = Struct.new(:name, :data, :raw_data)
1430 # Net::IMAP::TaggedResponse represents tagged responses.
1432 # The server completion result response indicates the success or
1433 # failure of the operation. It is tagged with the same tag as the
1434 # client command which began the operation.
1436 # response_tagged ::= tag SPACE resp_cond_state CRLF
1438 # tag ::= 1*<any ATOM_CHAR except "+">
1440 # resp_cond_state ::= ("OK" / "NO" / "BAD") SPACE resp_text
1444 # tag:: Returns the tag.
1446 # name:: Returns the name. the name is one of "OK", "NO", "BAD".
1448 # data:: Returns the data. See ((<Net::IMAP::ResponseText>)).
1450 # raw_data:: Returns the raw data string.
1452 TaggedResponse = Struct.new(:tag, :name, :data, :raw_data)
1454 # Net::IMAP::ResponseText represents texts of responses.
1455 # The text may be prefixed by the response code.
1457 # resp_text ::= ["[" resp_text_code "]" SPACE] (text_mime2 / text)
1458 # ;; text SHOULD NOT begin with "[" or "="
1462 # code:: Returns the response code. See ((<Net::IMAP::ResponseCode>)).
1464 # text:: Returns the text.
1466 ResponseText = Struct.new(:code, :text)
1469 # Net::IMAP::ResponseCode represents response codes.
1471 # resp_text_code ::= "ALERT" / "PARSE" /
1472 # "PERMANENTFLAGS" SPACE "(" #(flag / "\*") ")" /
1473 # "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
1474 # "UIDVALIDITY" SPACE nz_number /
1475 # "UNSEEN" SPACE nz_number /
1476 # atom [SPACE 1*<any TEXT_CHAR except "]">]
1480 # name:: Returns the name such as "ALERT", "PERMANENTFLAGS", "UIDVALIDITY"....
1482 # data:: Returns the data if it exists.
1484 ResponseCode = Struct.new(:name, :data)
1486 # Net::IMAP::MailboxList represents contents of the LIST response.
1488 # mailbox_list ::= "(" #("\Marked" / "\Noinferiors" /
1489 # "\Noselect" / "\Unmarked" / flag_extension) ")"
1490 # SPACE (<"> QUOTED_CHAR <"> / nil) SPACE mailbox
1494 # attr:: Returns the name attributes. Each name attribute is a symbol
1495 # capitalized by String#capitalize, such as :Noselect (not :NoSelect).
1497 # delim:: Returns the hierarchy delimiter
1499 # name:: Returns the mailbox name.
1501 MailboxList = Struct.new(:attr, :delim, :name)
1503 # Net::IMAP::MailboxQuota represents contents of GETQUOTA response.
1504 # This object can also be a response to GETQUOTAROOT. In the syntax
1505 # specification below, the delimiter used with the "#" construct is a
1506 # single space (SPACE).
1508 # quota_list ::= "(" #quota_resource ")"
1510 # quota_resource ::= atom SPACE number SPACE number
1512 # quota_response ::= "QUOTA" SPACE astring SPACE quota_list
1516 # mailbox:: The mailbox with the associated quota.
1518 # usage:: Current storage usage of mailbox.
1520 # quota:: Quota limit imposed on mailbox.
1522 MailboxQuota = Struct.new(:mailbox, :usage, :quota)
1524 # Net::IMAP::MailboxQuotaRoot represents part of the GETQUOTAROOT
1525 # response. (GETQUOTAROOT can also return Net::IMAP::MailboxQuota.)
1527 # quotaroot_response ::= "QUOTAROOT" SPACE astring *(SPACE astring)
1531 # mailbox:: The mailbox with the associated quota.
1533 # quotaroots:: Zero or more quotaroots that effect the quota on the
1534 # specified mailbox.
1536 MailboxQuotaRoot = Struct.new(:mailbox, :quotaroots)
1538 # Net::IMAP::MailboxACLItem represents response from GETACL.
1540 # acl_data ::= "ACL" SPACE mailbox *(SPACE identifier SPACE rights)
1542 # identifier ::= astring
1544 # rights ::= astring
1548 # user:: Login name that has certain rights to the mailbox
1549 # that was specified with the getacl command.
1551 # rights:: The access rights the indicated user has to the
1554 MailboxACLItem = Struct.new(:user, :rights)
1556 # Net::IMAP::StatusData represents contents of the STATUS response.
1560 # mailbox:: Returns the mailbox name.
1562 # attr:: Returns a hash. Each key is one of "MESSAGES", "RECENT", "UIDNEXT",
1563 # "UIDVALIDITY", "UNSEEN". Each value is a number.
1565 StatusData = Struct.new(:mailbox, :attr)
1567 # Net::IMAP::FetchData represents contents of the FETCH response.
1571 # seqno:: Returns the message sequence number.
1572 # (Note: not the unique identifier, even for the UID command response.)
1574 # attr:: Returns a hash. Each key is a data item name, and each value is
1577 # The current data items are:
1580 # A form of BODYSTRUCTURE without extension data.
1581 # [BODY[<section>]<<origin_octet>>]
1582 # A string expressing the body contents of the specified section.
1584 # An object that describes the [MIME-IMB] body structure of a message.
1585 # See Net::IMAP::BodyTypeBasic, Net::IMAP::BodyTypeText,
1586 # Net::IMAP::BodyTypeMessage, Net::IMAP::BodyTypeMultipart.
1588 # A Net::IMAP::Envelope object that describes the envelope
1589 # structure of a message.
1591 # A array of flag symbols that are set for this message. flag symbols
1592 # are capitalized by String#capitalize.
1594 # A string representing the internal date of the message.
1596 # Equivalent to BODY[].
1598 # Equivalent to BODY.PEEK[HEADER].
1600 # A number expressing the [RFC-822] size of the message.
1602 # Equivalent to BODY[TEXT].
1604 # A number expressing the unique identifier of the message.
1606 FetchData = Struct.new(:seqno, :attr)
1608 # Net::IMAP::Envelope represents envelope structures of messages.
1612 # date:: Returns a string that represents the date.
1614 # subject:: Returns a string that represents the subject.
1616 # from:: Returns an array of Net::IMAP::Address that represents the from.
1618 # sender:: Returns an array of Net::IMAP::Address that represents the sender.
1620 # reply_to:: Returns an array of Net::IMAP::Address that represents the reply-to.
1622 # to:: Returns an array of Net::IMAP::Address that represents the to.
1624 # cc:: Returns an array of Net::IMAP::Address that represents the cc.
1626 # bcc:: Returns an array of Net::IMAP::Address that represents the bcc.
1628 # in_reply_to:: Returns a string that represents the in-reply-to.
1630 # message_id:: Returns a string that represents the message-id.
1632 Envelope = Struct.new(:date, :subject, :from, :sender, :reply_to,
1633 :to, :cc, :bcc, :in_reply_to, :message_id)
1636 # Net::IMAP::Address represents electronic mail addresses.
1640 # name:: Returns the phrase from [RFC-822] mailbox.
1642 # route:: Returns the route from [RFC-822] route-addr.
1644 # mailbox:: nil indicates end of [RFC-822] group.
1645 # If non-nil and host is nil, returns [RFC-822] group name.
1646 # Otherwise, returns [RFC-822] local-part
1648 # host:: nil indicates [RFC-822] group syntax.
1649 # Otherwise, returns [RFC-822] domain name.
1651 Address = Struct.new(:name, :route, :mailbox, :host)
1654 # Net::IMAP::ContentDisposition represents Content-Disposition fields.
1658 # dsp_type:: Returns the disposition type.
1660 # param:: Returns a hash that represents parameters of the Content-Disposition
1663 ContentDisposition = Struct.new(:dsp_type, :param)
1665 # Net::IMAP::ThreadMember represents a thread-node returned
1666 # by Net::IMAP#thread
1670 # seqno:: The sequence number of this message.
1672 # children:: an array of Net::IMAP::ThreadMember objects for mail
1673 # items that are children of this in the thread.
1675 ThreadMember = Struct.new(:seqno, :children)
1677 # Net::IMAP::BodyTypeBasic represents basic body structures of messages.
1681 # media_type:: Returns the content media type name as defined in [MIME-IMB].
1683 # subtype:: Returns the content subtype name as defined in [MIME-IMB].
1685 # param:: Returns a hash that represents parameters as defined in [MIME-IMB].
1687 # content_id:: Returns a string giving the content id as defined in [MIME-IMB].
1689 # description:: Returns a string giving the content description as defined in
1692 # encoding:: Returns a string giving the content transfer encoding as defined in
1695 # size:: Returns a number giving the size of the body in octets.
1697 # md5:: Returns a string giving the body MD5 value as defined in [MD5].
1699 # disposition:: Returns a Net::IMAP::ContentDisposition object giving
1700 # the content disposition.
1702 # language:: Returns a string or an array of strings giving the body
1703 # language value as defined in [LANGUAGE-TAGS].
1705 # extension:: Returns extension data.
1707 # multipart?:: Returns false.
1709 class BodyTypeBasic < Struct.new(:media_type, :subtype,
1710 :param, :content_id,
1711 :description, :encoding, :size,
1712 :md5, :disposition, :language,
1718 # Obsolete: use +subtype+ instead. Calling this will
1719 # generate a warning message to +stderr+, then return
1720 # the value of +subtype+.
1722 $stderr.printf("warning: media_subtype is obsolete.\n")
1723 $stderr.printf(" use subtype instead.\n")
1728 # Net::IMAP::BodyTypeText represents TEXT body structures of messages.
1732 # lines:: Returns the size of the body in text lines.
1734 # And Net::IMAP::BodyTypeText has all fields of Net::IMAP::BodyTypeBasic.
1736 class BodyTypeText < Struct.new(:media_type, :subtype,
1737 :param, :content_id,
1738 :description, :encoding, :size,
1740 :md5, :disposition, :language,
1746 # Obsolete: use +subtype+ instead. Calling this will
1747 # generate a warning message to +stderr+, then return
1748 # the value of +subtype+.
1750 $stderr.printf("warning: media_subtype is obsolete.\n")
1751 $stderr.printf(" use subtype instead.\n")
1756 # Net::IMAP::BodyTypeMessage represents MESSAGE/RFC822 body structures of messages.
1760 # envelope:: Returns a Net::IMAP::Envelope giving the envelope structure.
1762 # body:: Returns an object giving the body structure.
1764 # And Net::IMAP::BodyTypeMessage has all methods of Net::IMAP::BodyTypeText.
1766 class BodyTypeMessage < Struct.new(:media_type, :subtype,
1767 :param, :content_id,
1768 :description, :encoding, :size,
1769 :envelope, :body, :lines,
1770 :md5, :disposition, :language,
1776 # Obsolete: use +subtype+ instead. Calling this will
1777 # generate a warning message to +stderr+, then return
1778 # the value of +subtype+.
1780 $stderr.printf("warning: media_subtype is obsolete.\n")
1781 $stderr.printf(" use subtype instead.\n")
1786 # Net::IMAP::BodyTypeMultipart represents multipart body structures
1791 # media_type:: Returns the content media type name as defined in [MIME-IMB].
1793 # subtype:: Returns the content subtype name as defined in [MIME-IMB].
1795 # parts:: Returns multiple parts.
1797 # param:: Returns a hash that represents parameters as defined in [MIME-IMB].
1799 # disposition:: Returns a Net::IMAP::ContentDisposition object giving
1800 # the content disposition.
1802 # language:: Returns a string or an array of strings giving the body
1803 # language value as defined in [LANGUAGE-TAGS].
1805 # extension:: Returns extension data.
1807 # multipart?:: Returns true.
1809 class BodyTypeMultipart < Struct.new(:media_type, :subtype,
1811 :param, :disposition, :language,
1817 # Obsolete: use +subtype+ instead. Calling this will
1818 # generate a warning message to +stderr+, then return
1819 # the value of +subtype+.
1821 $stderr.printf("warning: media_subtype is obsolete.\n")
1822 $stderr.printf(" use subtype instead.\n")
1827 class ResponseParser # :nodoc:
1831 @lex_state = EXPR_BEG
1838 EXPR_BEG = :EXPR_BEG
1839 EXPR_DATA = :EXPR_DATA
1840 EXPR_TEXT = :EXPR_TEXT
1841 EXPR_RTEXT = :EXPR_RTEXT
1842 EXPR_CTEXT = :EXPR_CTEXT
1855 T_LITERAL = :LITERAL
1857 T_PERCENT = :PERCENT
1862 BEG_REGEXP = /\G(?:\
1863 (?# 1: SPACE )( +)|\
1864 (?# 2: NIL )(NIL)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
1865 (?# 3: NUMBER )(\d+)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
1866 (?# 4: ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+]+)|\
1867 (?# 5: QUOTED )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\
1870 (?# 8: BSLASH )(\\)|\
1872 (?# 10: LBRA )(\[)|\
1873 (?# 11: RBRA )(\])|\
1874 (?# 12: LITERAL )\{(\d+)\}\r\n|\
1875 (?# 13: PLUS )(\+)|\
1876 (?# 14: PERCENT )(%)|\
1877 (?# 15: CRLF )(\r\n)|\
1878 (?# 16: EOF )(\z))/ni
1880 DATA_REGEXP = /\G(?:\
1883 (?# 3: NUMBER )(\d+)|\
1884 (?# 4: QUOTED )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\
1885 (?# 5: LITERAL )\{(\d+)\}\r\n|\
1887 (?# 7: RPAR )(\)))/ni
1889 TEXT_REGEXP = /\G(?:\
1890 (?# 1: TEXT )([^\x00\r\n]*))/ni
1892 RTEXT_REGEXP = /\G(?:\
1894 (?# 2: TEXT )([^\x00\r\n]*))/ni
1896 CTEXT_REGEXP = /\G(?:\
1897 (?# 1: TEXT )([^\x00\r\n\]]*))/ni
1899 Token = Struct.new(:symbol, :value)
1905 result = continue_req
1907 result = response_untagged
1909 result = response_tagged
1919 return ContinuationRequest.new(resp_text, @str)
1922 def response_untagged
1926 if token.symbol == T_NUMBER
1927 return numeric_response
1928 elsif token.symbol == T_ATOM
1930 when /\A(?:OK|NO|BAD|BYE|PREAUTH)\z/ni
1931 return response_cond
1932 when /\A(?:FLAGS)\z/ni
1933 return flags_response
1934 when /\A(?:LIST|LSUB)\z/ni
1935 return list_response
1936 when /\A(?:QUOTA)\z/ni
1937 return getquota_response
1938 when /\A(?:QUOTAROOT)\z/ni
1939 return getquotaroot_response
1940 when /\A(?:ACL)\z/ni
1941 return getacl_response
1942 when /\A(?:SEARCH|SORT)\z/ni
1943 return search_response
1944 when /\A(?:THREAD)\z/ni
1945 return thread_response
1946 when /\A(?:STATUS)\z/ni
1947 return status_response
1948 when /\A(?:CAPABILITY)\z/ni
1949 return capability_response
1951 return text_response
1954 parse_error("unexpected token %s", token.symbol)
1961 token = match(T_ATOM)
1962 name = token.value.upcase
1964 return TaggedResponse.new(tag, name, resp_text, @str)
1968 token = match(T_ATOM)
1969 name = token.value.upcase
1971 return UntaggedResponse.new(name, resp_text, @str)
1974 def numeric_response
1977 token = match(T_ATOM)
1978 name = token.value.upcase
1980 when "EXISTS", "RECENT", "EXPUNGE"
1981 return UntaggedResponse.new(name, n, @str)
1985 data = FetchData.new(n, msg_att)
1986 return UntaggedResponse.new(name, data, @str)
2004 when /\A(?:ENVELOPE)\z/ni
2005 name, val = envelope_data
2006 when /\A(?:FLAGS)\z/ni
2007 name, val = flags_data
2008 when /\A(?:INTERNALDATE)\z/ni
2009 name, val = internaldate_data
2010 when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni
2011 name, val = rfc822_text
2012 when /\A(?:RFC822\.SIZE)\z/ni
2013 name, val = rfc822_size
2014 when /\A(?:BODY(?:STRUCTURE)?)\z/ni
2015 name, val = body_data
2016 when /\A(?:UID)\z/ni
2017 name, val = uid_data
2019 parse_error("unknown attribute `%s'", token.value)
2027 token = match(T_ATOM)
2028 name = token.value.upcase
2030 return name, envelope
2034 @lex_state = EXPR_DATA
2036 if token.symbol == T_NIL
2047 sender = address_list
2049 reply_to = address_list
2057 in_reply_to = nstring
2059 message_id = nstring
2061 result = Envelope.new(date, subject, from, sender, reply_to,
2062 to, cc, bcc, in_reply_to, message_id)
2064 @lex_state = EXPR_BEG
2069 token = match(T_ATOM)
2070 name = token.value.upcase
2072 return name, flag_list
2075 def internaldate_data
2076 token = match(T_ATOM)
2077 name = token.value.upcase
2079 token = match(T_QUOTED)
2080 return name, token.value
2084 token = match(T_ATOM)
2085 name = token.value.upcase
2087 return name, nstring
2091 token = match(T_ATOM)
2092 name = token.value.upcase
2098 token = match(T_ATOM)
2099 name = token.value.upcase
2101 if token.symbol == T_SPACE
2105 name.concat(section)
2107 if token.symbol == T_ATOM
2108 name.concat(token.value)
2117 @lex_state = EXPR_DATA
2119 if token.symbol == T_NIL
2125 if token.symbol == T_LPAR
2126 result = body_type_mpart
2128 result = body_type_1part
2132 @lex_state = EXPR_BEG
2139 when /\A(?:TEXT)\z/ni
2140 return body_type_text
2141 when /\A(?:MESSAGE)\z/ni
2142 return body_type_msg
2144 return body_type_basic
2149 mtype, msubtype = media_type
2151 if token.symbol == T_RPAR
2152 return BodyTypeBasic.new(mtype, msubtype)
2155 param, content_id, desc, enc, size = body_fields
2156 md5, disposition, language, extension = body_ext_1part
2157 return BodyTypeBasic.new(mtype, msubtype,
2160 md5, disposition, language, extension)
2164 mtype, msubtype = media_type
2166 param, content_id, desc, enc, size = body_fields
2169 md5, disposition, language, extension = body_ext_1part
2170 return BodyTypeText.new(mtype, msubtype,
2174 md5, disposition, language, extension)
2178 mtype, msubtype = media_type
2180 param, content_id, desc, enc, size = body_fields
2187 md5, disposition, language, extension = body_ext_1part
2188 return BodyTypeMessage.new(mtype, msubtype,
2192 md5, disposition, language, extension)
2199 if token.symbol == T_SPACE
2206 msubtype = case_insensitive_string
2207 param, disposition, language, extension = body_ext_mpart
2208 return BodyTypeMultipart.new(mtype, msubtype, parts,
2209 param, disposition, language,
2214 mtype = case_insensitive_string
2216 msubtype = case_insensitive_string
2217 return mtype, msubtype
2221 param = body_fld_param
2223 content_id = nstring
2227 enc = case_insensitive_string
2230 return param, content_id, desc, enc, size
2235 if token.symbol == T_NIL
2250 name = case_insensitive_string
2260 if token.symbol == T_SPACE
2268 if token.symbol == T_SPACE
2273 disposition = body_fld_dsp
2276 if token.symbol == T_SPACE
2279 return md5, disposition
2281 language = body_fld_lang
2284 if token.symbol == T_SPACE
2287 return md5, disposition, language
2290 extension = body_extensions
2291 return md5, disposition, language, extension
2296 if token.symbol == T_SPACE
2301 param = body_fld_param
2304 if token.symbol == T_SPACE
2309 disposition = body_fld_dsp
2311 language = body_fld_lang
2314 if token.symbol == T_SPACE
2317 return param, disposition, language
2320 extension = body_extensions
2321 return param, disposition, language, extension
2326 if token.symbol == T_NIL
2331 dsp_type = case_insensitive_string
2333 param = body_fld_param
2335 return ContentDisposition.new(dsp_type, param)
2340 if token.symbol == T_LPAR
2352 result.push(case_insensitive_string)
2374 result.push(body_extension)
2383 result = body_extensions
2395 token = match(T_LBRA)
2396 str.concat(token.value)
2397 token = match(T_ATOM, T_NUMBER, T_RBRA)
2398 if token.symbol == T_RBRA
2399 str.concat(token.value)
2402 str.concat(token.value)
2404 if token.symbol == T_SPACE
2406 str.concat(token.value)
2407 token = match(T_LPAR)
2408 str.concat(token.value)
2413 str.concat(token.value)
2418 str.concat(token.value)
2420 str.concat(format_string(astring))
2423 token = match(T_RBRA)
2424 str.concat(token.value)
2428 def format_string(str)
2432 when /[\x80-\xff\r\n]/n
2434 return "{" + str.length.to_s + "}" + CRLF + str
2435 when /[(){ \x00-\x1f\x7f%*"\\]/n
2437 return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"'
2445 token = match(T_ATOM)
2446 name = token.value.upcase
2452 token = match(T_ATOM)
2453 name = token.value.upcase
2455 @lex_state = EXPR_TEXT
2456 token = match(T_TEXT)
2457 @lex_state = EXPR_BEG
2458 return UntaggedResponse.new(name, token.value)
2462 token = match(T_ATOM)
2463 name = token.value.upcase
2465 return UntaggedResponse.new(name, flag_list, @str)
2469 token = match(T_ATOM)
2470 name = token.value.upcase
2472 return UntaggedResponse.new(name, mailbox_list, @str)
2478 token = match(T_QUOTED, T_NIL)
2479 if token.symbol == T_NIL
2486 return MailboxList.new(attr, delim, name)
2489 def getquota_response
2490 # If quota never established, get back
2491 # `NO Quota root does not exist'.
2492 # If quota removed, get `()' after the
2493 # folder spec with no mention of `STORAGE'.
2494 token = match(T_ATOM)
2495 name = token.value.upcase
2504 data = MailboxQuota.new(mailbox, nil, nil)
2505 return UntaggedResponse.new(name, data, @str)
2509 token = match(T_NUMBER)
2512 token = match(T_NUMBER)
2515 data = MailboxQuota.new(mailbox, usage, quota)
2516 return UntaggedResponse.new(name, data, @str)
2518 parse_error("unexpected token %s", token.symbol)
2522 def getquotaroot_response
2523 # Similar to getquota, but only admin can use getquota.
2524 token = match(T_ATOM)
2525 name = token.value.upcase
2531 break unless token.symbol == T_SPACE
2533 quotaroots.push(astring)
2535 data = MailboxQuotaRoot.new(mailbox, quotaroots)
2536 return UntaggedResponse.new(name, data, @str)
2540 token = match(T_ATOM)
2541 name = token.value.upcase
2546 if token.symbol == T_SPACE
2559 ##XXX data.push([user, rights])
2560 data.push(MailboxACLItem.new(user, rights))
2563 return UntaggedResponse.new(name, data, @str)
2567 token = match(T_ATOM)
2568 name = token.value.upcase
2570 if token.symbol == T_SPACE
2586 return UntaggedResponse.new(name, data, @str)
2590 token = match(T_ATOM)
2591 name = token.value.upcase
2594 if token.symbol == T_SPACE
2603 threads << thread_branch(token)
2613 return UntaggedResponse.new(name, threads, @str)
2616 def thread_branch(token)
2621 shift_token # ignore first T_LPAR
2627 newmember = ThreadMember.new(number, [])
2629 rootmember = newmember
2631 lastmember.children << newmember
2633 lastmember = newmember
2639 lastmember = rootmember = ThreadMember.new(nil, [])
2642 lastmember.children << thread_branch(token)
2652 token = match(T_ATOM)
2653 name = token.value.upcase
2668 token = match(T_ATOM)
2669 key = token.value.upcase
2674 data = StatusData.new(mailbox, attr)
2675 return UntaggedResponse.new(name, data, @str)
2678 def capability_response
2679 token = match(T_ATOM)
2680 name = token.value.upcase
2691 data.push(atom.upcase)
2693 return UntaggedResponse.new(name, data, @str)
2697 @lex_state = EXPR_RTEXT
2699 if token.symbol == T_LBRA
2700 code = resp_text_code
2704 token = match(T_TEXT)
2705 @lex_state = EXPR_BEG
2706 return ResponseText.new(code, token.value)
2710 @lex_state = EXPR_BEG
2712 token = match(T_ATOM)
2713 name = token.value.upcase
2715 when /\A(?:ALERT|PARSE|READ-ONLY|READ-WRITE|TRYCREATE|NOMODSEQ)\z/n
2716 result = ResponseCode.new(name, nil)
2717 when /\A(?:PERMANENTFLAGS)\z/n
2719 result = ResponseCode.new(name, flag_list)
2720 when /\A(?:UIDVALIDITY|UIDNEXT|UNSEEN)\z/n
2722 result = ResponseCode.new(name, number)
2725 @lex_state = EXPR_CTEXT
2726 token = match(T_TEXT)
2727 @lex_state = EXPR_BEG
2728 result = ResponseCode.new(name, token.value)
2731 @lex_state = EXPR_RTEXT
2737 if token.symbol == T_NIL
2752 result.push(address)
2758 ADDRESS_REGEXP = /\G\
2759 (?# 1: NAME )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
2760 (?# 2: ROUTE )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
2761 (?# 3: MAILBOX )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
2762 (?# 4: HOST )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)")\
2767 if @str.index(ADDRESS_REGEXP, @pos)
2768 # address does not include literal.
2774 for s in [name, route, mailbox, host]
2776 s.gsub!(/\\(["\\])/n, "\\1")
2789 return Address.new(name, route, mailbox, host)
2811 # if token.symbol == T_BSLASH
2814 # if token.symbol == T_STAR
2816 # return token.value.intern
2818 # return atom.intern
2826 (?# FLAG )\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)|\
2827 (?# ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\]+)/n
2830 if @str.index(/\(([^)]*)\)/ni, @pos)
2832 return $1.scan(FLAG_REGEXP).collect { |flag, atom|
2833 atom || flag.capitalize.intern
2836 parse_error("invalid flag list")
2842 if token.symbol == T_NIL
2852 if string_token?(token)
2861 if token.symbol == T_NIL
2865 token = match(T_QUOTED, T_LITERAL)
2869 STRING_TOKENS = [T_QUOTED, T_LITERAL, T_NIL]
2871 def string_token?(token)
2872 return STRING_TOKENS.include?(token.symbol)
2875 def case_insensitive_string
2877 if token.symbol == T_NIL
2881 token = match(T_QUOTED, T_LITERAL)
2882 return token.value.upcase
2889 if atom_token?(token)
2890 result.concat(token.value)
2894 parse_error("unexpected token %s", token.symbol)
2911 def atom_token?(token)
2912 return ATOM_TOKENS.include?(token.symbol)
2917 if token.symbol == T_NIL
2921 token = match(T_NUMBER)
2922 return token.value.to_i
2932 unless args.include?(token.symbol)
2933 parse_error('unexpected token %s (expected %s)',
2934 token.symbol.id2name,
2935 args.collect {|i| i.id2name}.join(" or "))
2955 if @str.index(BEG_REGEXP, @pos)
2958 return Token.new(T_SPACE, $+)
2960 return Token.new(T_NIL, $+)
2962 return Token.new(T_NUMBER, $+)
2964 return Token.new(T_ATOM, $+)
2966 return Token.new(T_QUOTED,
2967 $+.gsub(/\\(["\\])/n, "\\1"))
2969 return Token.new(T_LPAR, $+)
2971 return Token.new(T_RPAR, $+)
2973 return Token.new(T_BSLASH, $+)
2975 return Token.new(T_STAR, $+)
2977 return Token.new(T_LBRA, $+)
2979 return Token.new(T_RBRA, $+)
2982 val = @str[@pos, len]
2984 return Token.new(T_LITERAL, val)
2986 return Token.new(T_PLUS, $+)
2988 return Token.new(T_PERCENT, $+)
2990 return Token.new(T_CRLF, $+)
2992 return Token.new(T_EOF, $+)
2994 parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid")
2997 @str.index(/\S*/n, @pos)
2998 parse_error("unknown token - %s", $&.dump)
3001 if @str.index(DATA_REGEXP, @pos)
3004 return Token.new(T_SPACE, $+)
3006 return Token.new(T_NIL, $+)
3008 return Token.new(T_NUMBER, $+)
3010 return Token.new(T_QUOTED,
3011 $+.gsub(/\\(["\\])/n, "\\1"))
3014 val = @str[@pos, len]
3016 return Token.new(T_LITERAL, val)
3018 return Token.new(T_LPAR, $+)
3020 return Token.new(T_RPAR, $+)
3022 parse_error("[Net::IMAP BUG] DATA_REGEXP is invalid")
3025 @str.index(/\S*/n, @pos)
3026 parse_error("unknown token - %s", $&.dump)
3029 if @str.index(TEXT_REGEXP, @pos)
3032 return Token.new(T_TEXT, $+)
3034 parse_error("[Net::IMAP BUG] TEXT_REGEXP is invalid")
3037 @str.index(/\S*/n, @pos)
3038 parse_error("unknown token - %s", $&.dump)
3041 if @str.index(RTEXT_REGEXP, @pos)
3044 return Token.new(T_LBRA, $+)
3046 return Token.new(T_TEXT, $+)
3048 parse_error("[Net::IMAP BUG] RTEXT_REGEXP is invalid")
3051 @str.index(/\S*/n, @pos)
3052 parse_error("unknown token - %s", $&.dump)
3055 if @str.index(CTEXT_REGEXP, @pos)
3058 return Token.new(T_TEXT, $+)
3060 parse_error("[Net::IMAP BUG] CTEXT_REGEXP is invalid")
3063 @str.index(/\S*/n, @pos) #/
3064 parse_error("unknown token - %s", $&.dump)
3067 parse_error("invalid @lex_state - %s", @lex_state.inspect)
3071 def parse_error(fmt, *args)
3073 $stderr.printf("@str: %s\n", @str.dump)
3074 $stderr.printf("@pos: %d\n", @pos)
3075 $stderr.printf("@lex_state: %s\n", @lex_state)
3077 $stderr.printf("@token.symbol: %s\n", @token.symbol)
3078 $stderr.printf("@token.value: %s\n", @token.value.inspect)
3081 raise ResponseParseError, format(fmt, *args)
3085 # Authenticator for the "LOGIN" authentication type. See
3087 class LoginAuthenticator
3091 @state = STATE_PASSWORD
3101 STATE_PASSWORD = :PASSWORD
3103 def initialize(user, password)
3105 @password = password
3109 add_authenticator "LOGIN", LoginAuthenticator
3111 # Authenticator for the "PLAIN" authentication type. See
3113 class PlainAuthenticator
3115 return "\0#{@user}\0#{@password}"
3120 def initialize(user, password)
3122 @password = password
3125 add_authenticator "PLAIN", PlainAuthenticator
3127 # Authenticator for the "CRAM-MD5" authentication type. See
3129 class CramMD5Authenticator
3130 def process(challenge)
3131 digest = hmac_md5(challenge, @password)
3132 return @user + " " + digest
3137 def initialize(user, password)
3139 @password = password
3142 def hmac_md5(text, key)
3144 key = Digest::MD5.digest(key)
3147 k_ipad = key + "\0" * (64 - key.length)
3148 k_opad = key + "\0" * (64 - key.length)
3154 digest = Digest::MD5.digest(k_ipad + text)
3156 return Digest::MD5.hexdigest(k_opad + digest)
3159 add_authenticator "CRAM-MD5", CramMD5Authenticator
3161 # Authenticator for the "DIGEST-MD5" authentication type. See
3163 class DigestMD5Authenticator
3164 def process(challenge)
3169 c = StringScanner.new(challenge)
3170 while c.scan(/(?:\s*,)?\s*(\w+)=("(?:[^\\"]+|\\.)*"|[^,]+)\s*/)
3181 raise DataFormatError, "Bad Challenge: '#{challenge}'" unless c.rest.size == 0
3182 raise Error, "Server does not support auth (qop = #{sparams['qop'].join(',')})" unless sparams['qop'].include?("auth")
3185 :nonce => sparams['nonce'],
3187 :realm => sparams['realm'],
3188 :cnonce => Digest::MD5.hexdigest("%.15f:%.15f:%d" % [Time.now.to_f, rand, Process.pid.to_s]),
3189 :'digest-uri' => 'imap/' + sparams['realm'],
3192 :nc => "%08d" % nc(sparams['nonce']),
3193 :charset => sparams['charset'],
3196 response[:authzid] = @authname unless @authname.nil?
3198 # now, the real thing
3199 a0 = Digest::MD5.digest( [ response.values_at(:username, :realm), @password ].join(':') )
3201 a1 = [ a0, response.values_at(:nonce,:cnonce) ].join(':')
3202 a1 << ':' + response[:authzid] unless response[:authzid].nil?
3204 a2 = "AUTHENTICATE:" + response[:'digest-uri']
3205 a2 << ":00000000000000000000000000000000" if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/
3207 response[:response] = Digest::MD5.hexdigest(
3209 Digest::MD5.hexdigest(a1),
3210 response.values_at(:nonce, :nc, :cnonce, :qop),
3211 Digest::MD5.hexdigest(a2)
3215 return response.keys.map { |k| qdval(k.to_s, response[k]) }.join(',')
3218 # if at the second stage, return an empty string
3219 if challenge =~ /rspauth=/
3222 raise ResponseParseError, challenge
3225 raise ResponseParseError, challenge
3229 def initialize(user, password, authname = nil)
3230 @user, @password, @authname = user, password, authname
3231 @nc, @stage = {}, STAGE_ONE
3236 STAGE_ONE = :stage_one
3237 STAGE_TWO = :stage_two
3240 if @nc.has_key? nonce
3241 @nc[nonce] = @nc[nonce] + 1
3248 # some responses need quoting
3250 return if k.nil? or v.nil?
3251 if %w"username authzid realm nonce cnonce digest-uri qop".include? k
3252 v.gsub!(/([\\"])/, "\\\1")
3253 return '%s="%s"' % [k, v]
3255 return '%s=%s' % [k, v]
3259 add_authenticator "DIGEST-MD5", DigestMD5Authenticator
3261 # Superclass of IMAP errors.
3262 class Error < StandardError
3265 # Error raised when data is in the incorrect format.
3266 class DataFormatError < Error
3269 # Error raised when a response from the server is non-parseable.
3270 class ResponseParseError < Error
3273 # Superclass of all errors used to encapsulate "fail" responses
3275 class ResponseError < Error
3278 # Error raised upon a "NO" response from the server, indicating
3279 # that the client command could not be completed successfully.
3280 class NoResponseError < ResponseError
3283 # Error raised upon a "BAD" response from the server, indicating
3284 # that the client command violated the IMAP protocol, or an internal
3285 # server failure has occurred.
3286 class BadResponseError < ResponseError
3289 # Error raised upon a "BYE" response from the server, indicating
3290 # that the client is not being allowed to login, or has been timed
3291 # out due to inactivity.
3292 class ByeResponseError < ResponseError
3299 require "getoptlong"
3303 $user = ENV["USER"] || ENV["LOGNAME"]
3309 usage: #{$0} [options] <host>
3311 --help print this message
3312 --port=PORT specifies port
3313 --user=USER specifies user
3314 --auth=AUTH specifies auth type
3321 system("stty", "-echo")
3325 system("stty", "echo")
3331 printf("%s@%s> ", $user, $host)
3333 return line.strip.split(/\s+/)
3339 parser = GetoptLong.new
3340 parser.set_options(['--debug', GetoptLong::NO_ARGUMENT],
3341 ['--help', GetoptLong::NO_ARGUMENT],
3342 ['--port', GetoptLong::REQUIRED_ARGUMENT],
3343 ['--user', GetoptLong::REQUIRED_ARGUMENT],
3344 ['--auth', GetoptLong::REQUIRED_ARGUMENT],
3345 ['--ssl', GetoptLong::NO_ARGUMENT])
3347 parser.each_option do |name, arg|
3358 Net::IMAP.debug = true
3375 imap = Net::IMAP.new($host, :port => $port, :ssl => $ssl)
3377 password = get_password
3378 imap.authenticate($auth, $user, password)
3380 cmd, *args = get_command
3385 for mbox in imap.list("", args[0] || "*")
3386 if mbox.attr.include?(Net::IMAP::NOSELECT)
3388 elsif mbox.attr.include?(Net::IMAP::MARKED)
3393 print prefix, mbox.name, "\n"
3396 imap.select(args[0] || "inbox")
3402 unless messages = imap.responses["EXISTS"][-1]
3407 for data in imap.fetch(1..-1, ["ENVELOPE"])
3408 print data.seqno, ": ", data.attr["ENVELOPE"].subject, "\n"
3415 data = imap.fetch(args[0].to_i, ["RFC822.HEADER", "RFC822.TEXT"])[0]
3416 puts data.attr["RFC822.HEADER"]
3417 puts data.attr["RFC822.TEXT"]
3419 puts "missing argument"
3421 when "logout", "exit", "quit"
3425 list [pattern] list mailboxes
3426 select [mailbox] select mailbox
3428 summary display summary
3429 fetch [msgno] display message
3431 help, ? display help message
3434 print "unknown command: ", cmd, "\n"
3436 rescue Net::IMAP::Error