source: extensions/gsdl-video/trunk/installed/cmdline/lib/ruby/1.8/net/imap.rb@ 18425

Last change on this file since 18425 was 18425, checked in by davidb, 15 years ago

Video extension to Greenstone

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