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