net-imap 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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