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