net-imap 0.1.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.

Potentially problematic release.


This version of net-imap might be problematic. Click here for more details.

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