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