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