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