~ubuntu-branches/ubuntu/hardy/ruby1.8/hardy-updates

« back to all changes in this revision

Viewing changes to lib/net/imap.rb

  • Committer: Bazaar Package Importer
  • Author(s): akira yamada
  • Date: 2007-03-13 22:11:58 UTC
  • mfrom: (1.1.5 upstream)
  • Revision ID: james.westby@ubuntu.com-20070313221158-h3oql37brlaf2go2
Tags: 1.8.6-1
* new upstream version, 1.8.6.
* libruby1.8 conflicts with libopenssl-ruby1.8 (< 1.8.6) (closes: #410018)
* changed packaging style to cdbs from dbs.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#
 
2
# = net/imap.rb
 
3
#
 
4
# Copyright (C) 2000  Shugo Maeda <shugo@ruby-lang.org>
 
5
#
 
6
# This library is distributed under the terms of the Ruby license.
 
7
# You can freely distribute/modify this library.
 
8
#
 
9
# Documentation: Shugo Maeda, with RDoc conversion and overview by William
 
10
# Webber.
 
11
#
 
12
# See Net::IMAP for documentation. 
 
13
#
 
14
 
 
15
 
 
16
require "socket"
 
17
require "monitor"
 
18
require "digest/md5"
 
19
begin
 
20
  require "openssl"
 
21
rescue LoadError
 
22
end
 
23
 
 
24
module Net
 
25
 
 
26
  #
 
27
  # Net::IMAP implements Internet Message Access Protocol (IMAP) client
 
28
  # functionality.  The protocol is described in [IMAP].
 
29
  #
 
30
  # == IMAP Overview
 
31
  #
 
32
  # An IMAP client connects to a server, and then authenticates
 
33
  # itself using either #authenticate() or #login().  Having
 
34
  # authenticated itself, there is a range of commands
 
35
  # available to it.  Most work with mailboxes, which may be
 
36
  # arranged in an hierarchical namespace, and each of which
 
37
  # contains zero or more messages.  How this is implemented on
 
38
  # the server is implementation-dependent; on a UNIX server, it
 
39
  # will frequently be implemented as a files in mailbox format
 
40
  # within a hierarchy of directories.
 
41
  #
 
42
  # To work on the messages within a mailbox, the client must
 
43
  # first select that mailbox, using either #select() or (for
 
44
  # read-only access) #examine().  Once the client has successfully
 
45
  # selected a mailbox, they enter _selected_ state, and that
 
46
  # mailbox becomes the _current_ mailbox, on which mail-item
 
47
  # related commands implicitly operate.  
 
48
  #
 
49
  # Messages have two sorts of identifiers: message sequence
 
50
  # numbers, and UIDs.  
 
51
  #
 
52
  # Message sequence numbers number messages within a mail box 
 
53
  # from 1 up to the number of items in the mail box.  If new
 
54
  # message arrives during a session, it receives a sequence
 
55
  # number equal to the new size of the mail box.  If messages
 
56
  # are expunged from the mailbox, remaining messages have their
 
57
  # sequence numbers "shuffled down" to fill the gaps.
 
58
  #
 
59
  # UIDs, on the other hand, are permanently guaranteed not to
 
60
  # identify another message within the same mailbox, even if 
 
61
  # the existing message is deleted.  UIDs are required to
 
62
  # be assigned in ascending (but not necessarily sequential)
 
63
  # order within a mailbox; this means that if a non-IMAP client
 
64
  # rearranges the order of mailitems within a mailbox, the
 
65
  # UIDs have to be reassigned.  An IMAP client cannot thus
 
66
  # rearrange message orders.
 
67
  #
 
68
  # == Examples of Usage
 
69
  #
 
70
  # === List sender and subject of all recent messages in the default mailbox
 
71
  #
 
72
  #   imap = Net::IMAP.new('mail.example.com')
 
73
  #   imap.authenticate('LOGIN', 'joe_user', 'joes_password')
 
74
  #   imap.examine('INBOX')
 
75
  #   imap.search(["RECENT"]).each do |message_id|
 
76
  #     envelope = imap.fetch(message_id, "ENVELOPE")[0].attr["ENVELOPE"]
 
77
  #     puts "#{envelope.from[0].name}: \t#{envelope.subject}"
 
78
  #   end
 
79
  #
 
80
  # === Move all messages from April 2003 from "Mail/sent-mail" to "Mail/sent-apr03"
 
81
  #
 
82
  #   imap = Net::IMAP.new('mail.example.com')
 
83
  #   imap.authenticate('LOGIN', 'joe_user', 'joes_password')
 
84
  #   imap.select('Mail/sent-mail')
 
85
  #   if not imap.list('Mail/', 'sent-apr03')
 
86
  #     imap.create('Mail/sent-apr03')
 
87
  #   end
 
88
  #   imap.search(["BEFORE", "30-Apr-2003", "SINCE", "1-Apr-2003"]).each do |message_id|
 
89
  #     imap.copy(message_id, "Mail/sent-apr03")
 
90
  #     imap.store(message_id, "+FLAGS", [:Deleted])
 
91
  #   end
 
92
  #   imap.expunge
 
93
  # 
 
94
  # == Thread Safety
 
95
  #
 
96
  # Net::IMAP supports concurrent threads. For example,
 
97
  # 
 
98
  #   imap = Net::IMAP.new("imap.foo.net", "imap2")
 
99
  #   imap.authenticate("cram-md5", "bar", "password")
 
100
  #   imap.select("inbox")
 
101
  #   fetch_thread = Thread.start { imap.fetch(1..-1, "UID") }
 
102
  #   search_result = imap.search(["BODY", "hello"])
 
103
  #   fetch_result = fetch_thread.value
 
104
  #   imap.disconnect
 
105
  # 
 
106
  # This script invokes the FETCH command and the SEARCH command concurrently.
 
107
  #
 
108
  # == Errors
 
109
  #
 
110
  # An IMAP server can send three different types of responses to indicate
 
111
  # failure:
 
112
  #
 
113
  # NO:: the attempted command could not be successfully completed.  For
 
114
  #      instance, the username/password used for logging in are incorrect;
 
115
  #      the selected mailbox does not exists; etc.  
 
116
  #
 
117
  # BAD:: the request from the client does not follow the server's 
 
118
  #       understanding of the IMAP protocol.  This includes attempting
 
119
  #       commands from the wrong client state; for instance, attempting
 
120
  #       to perform a SEARCH command without having SELECTed a current
 
121
  #       mailbox.  It can also signal an internal server
 
122
  #       failure (such as a disk crash) has occurred.
 
123
  #
 
124
  # BYE:: the server is saying goodbye.  This can be part of a normal
 
125
  #       logout sequence, and can be used as part of a login sequence
 
126
  #       to indicate that the server is (for some reason) unwilling
 
127
  #       to accept our connection.  As a response to any other command,
 
128
  #       it indicates either that the server is shutting down, or that
 
129
  #       the server is timing out the client connection due to inactivity.
 
130
  #
 
131
  # These three error response are represented by the errors
 
132
  # Net::IMAP::NoResponseError, Net::IMAP::BadResponseError, and
 
133
  # Net::IMAP::ByeResponseError, all of which are subclasses of
 
134
  # Net::IMAP::ResponseError.  Essentially, all methods that involve
 
135
  # sending a request to the server can generate one of these errors.
 
136
  # Only the most pertinent instances have been documented below.
 
137
  #
 
138
  # Because the IMAP class uses Sockets for communication, its methods
 
139
  # are also susceptible to the various errors that can occur when
 
140
  # working with sockets.  These are generally represented as
 
141
  # Errno errors.  For instance, any method that involves sending a
 
142
  # request to the server and/or receiving a response from it could
 
143
  # raise an Errno::EPIPE error if the network connection unexpectedly
 
144
  # goes down.  See the socket(7), ip(7), tcp(7), socket(2), connect(2),
 
145
  # and associated man pages.
 
146
  #
 
147
  # Finally, a Net::IMAP::DataFormatError is thrown if low-level data
 
148
  # is found to be in an incorrect format (for instance, when converting
 
149
  # between UTF-8 and UTF-16), and Net::IMAP::ResponseParseError is 
 
150
  # thrown if a server response is non-parseable. 
 
151
  #
 
152
  #
 
153
  # == References
 
154
  #
 
155
  # [[IMAP]]
 
156
  #    M. Crispin, "INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1",
 
157
  #    RFC 2060, December 1996.  (Note: since obsoleted by RFC 3501)
 
158
  #
 
159
  # [[LANGUAGE-TAGS]]
 
160
  #    Alvestrand, H., "Tags for the Identification of
 
161
  #    Languages", RFC 1766, March 1995.
 
162
  #
 
163
  # [[MD5]]
 
164
  #    Myers, J., and M. Rose, "The Content-MD5 Header Field", RFC
 
165
  #    1864, October 1995.
 
166
  #
 
167
  # [[MIME-IMB]]
 
168
  #    Freed, N., and N. Borenstein, "MIME (Multipurpose Internet
 
169
  #    Mail Extensions) Part One: Format of Internet Message Bodies", RFC
 
170
  #    2045, November 1996.
 
171
  #
 
172
  # [[RFC-822]]
 
173
  #    Crocker, D., "Standard for the Format of ARPA Internet Text
 
174
  #    Messages", STD 11, RFC 822, University of Delaware, August 1982.
 
175
  #
 
176
  # [[RFC-2087]]
 
177
  #    Myers, J., "IMAP4 QUOTA extension", RFC 2087, January 1997.
 
178
  #
 
179
  # [[RFC-2086]]
 
180
  #    Myers, J., "IMAP4 ACL extension", RFC 2086, January 1997.
 
181
  #
 
182
  # [[RFC-2195]]
 
183
  #    Klensin, J., Catoe, R., and Krumviede, P., "IMAP/POP AUTHorize Extension
 
184
  #    for Simple Challenge/Response", RFC 2195, September 1997.
 
185
  #
 
186
  # [[SORT-THREAD-EXT]]
 
187
  #    Crispin, M., "INTERNET MESSAGE ACCESS PROTOCOL - SORT and THREAD
 
188
  #    Extensions", draft-ietf-imapext-sort, May 2003.
 
189
  #
 
190
  # [[OSSL]]
 
191
  #    http://www.openssl.org
 
192
  #
 
193
  # [[RSSL]]
 
194
  #    http://savannah.gnu.org/projects/rubypki
 
195
  #
 
196
  # [[UTF7]]
 
197
  #    Goldsmith, D. and Davis, M., "UTF-7: A Mail-Safe Transformation Format of
 
198
  #    Unicode", RFC 2152, May 1997.
 
199
  #
 
200
  class IMAP
 
201
    include MonitorMixin
 
202
    if defined?(OpenSSL)
 
203
      include OpenSSL
 
204
      include SSL
 
205
    end
 
206
 
 
207
    #  Returns an initial greeting response from the server.
 
208
    attr_reader :greeting
 
209
 
 
210
    # Returns recorded untagged responses.  For example:
 
211
    #
 
212
    #   imap.select("inbox")
 
213
    #   p imap.responses["EXISTS"][-1]
 
214
    #   #=> 2
 
215
    #   p imap.responses["UIDVALIDITY"][-1]
 
216
    #   #=> 968263756
 
217
    attr_reader :responses
 
218
 
 
219
    # Returns all response handlers.
 
220
    attr_reader :response_handlers
 
221
 
 
222
    # The thread to receive exceptions.
 
223
    attr_accessor :client_thread
 
224
 
 
225
    # Flag indicating a message has been seen
 
226
    SEEN = :Seen
 
227
 
 
228
    # Flag indicating a message has been answered
 
229
    ANSWERED = :Answered
 
230
 
 
231
    # Flag indicating a message has been flagged for special or urgent
 
232
    # attention
 
233
    FLAGGED = :Flagged
 
234
 
 
235
    # Flag indicating a message has been marked for deletion.  This
 
236
    # will occur when the mailbox is closed or expunged.
 
237
    DELETED = :Deleted
 
238
 
 
239
    # Flag indicating a message is only a draft or work-in-progress version.
 
240
    DRAFT = :Draft
 
241
 
 
242
    # Flag indicating that the message is "recent", meaning that this
 
243
    # session is the first session in which the client has been notified
 
244
    # of this message.
 
245
    RECENT = :Recent
 
246
 
 
247
    # Flag indicating that a mailbox context name cannot contain
 
248
    # children.
 
249
    NOINFERIORS = :Noinferiors
 
250
 
 
251
    # Flag indicating that a mailbox is not selected.
 
252
    NOSELECT = :Noselect
 
253
 
 
254
    # Flag indicating that a mailbox has been marked "interesting" by
 
255
    # the server; this commonly indicates that the mailbox contains
 
256
    # new messages.
 
257
    MARKED = :Marked
 
258
 
 
259
    # Flag indicating that the mailbox does not contains new messages.
 
260
    UNMARKED = :Unmarked
 
261
 
 
262
    # Returns the debug mode.
 
263
    def self.debug
 
264
      return @@debug
 
265
    end
 
266
 
 
267
    # Sets the debug mode.
 
268
    def self.debug=(val)
 
269
      return @@debug = val
 
270
    end
 
271
 
 
272
    # Adds an authenticator for Net::IMAP#authenticate.  +auth_type+
 
273
    # is the type of authentication this authenticator supports
 
274
    # (for instance, "LOGIN").  The +authenticator+ is an object
 
275
    # which defines a process() method to handle authentication with
 
276
    # the server.  See Net::IMAP::LoginAuthenticator and 
 
277
    # Net::IMAP::CramMD5Authenticator for examples.
 
278
    #
 
279
    # If +auth_type+ refers to an existing authenticator, it will be
 
280
    # replaced by the new one.
 
281
    def self.add_authenticator(auth_type, authenticator)
 
282
      @@authenticators[auth_type] = authenticator
 
283
    end
 
284
 
 
285
    # Disconnects from the server.
 
286
    def disconnect
 
287
      @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: shugo@ruby-lang.org
 
592
    #   To: shugo@ruby-lang.org
 
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
 
3215
end
 
3216
 
 
3217
if __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
 
3229
usage: #{$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
 
3236
EOF
 
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
 
3346
list [pattern]                  list mailboxes
 
3347
select [mailbox]                select mailbox
 
3348
close                           close mailbox
 
3349
summary                         display summary
 
3350
fetch [msgno]                   display message
 
3351
logout                          logout
 
3352
help, ?                         display help message
 
3353
EOF
 
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
 
3365
end
 
3366