rubysl-net-imap 1.0.0

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