rubysl-net-imap 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+