PageRenderTime 64ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/net/imap.rb

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