Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

+wait_for_new_emails

  • Loading branch information...
commit 436d978eedcd1229c0f46864bb7931aaf2d95a24 1 parent 8618717
Conrad Irwin authored
Showing with 113 additions and 17 deletions.
  1. +39 −16 README.md
  2. +1 −1  em-imap.gemspec
  3. +73 −0 lib/em-imap/client.rb
55 README.md
View
@@ -14,6 +14,7 @@ Before you can communicate with an IMAP server, you must first connect to it. Th
For example, to connect to Gmail's IMAP server, you can use the following snippet:
+```ruby
require 'rubygems'
require 'em-imap'
@@ -27,6 +28,7 @@ For example, to connect to Gmail's IMAP server, you can use the following snippe
EM::stop
end
end
+```
### Authenticating
@@ -34,6 +36,7 @@ There are two authentication mechanisms in IMAP, `LOGIN` and `AUTHENTICATE`, exp
Extending our previous example to also log in to Gmail:
+```ruby
client = EM::IMAP.new('imap.gmail.com', 993, true)
client.connect.bind! do
client.login("conrad.irwin@gmail.com", ENV["GMAIL_PASSWORD"])
@@ -42,6 +45,7 @@ Extending our previous example to also log in to Gmail:
end.errback do |error|
puts "Connecting or logging in failed: #{error}"
end
+```
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).
@@ -49,6 +53,7 @@ The `.authenticate` method is more advanced and uses the same extensible mechani
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):
+```ruby
client = EM::IMAP.new('imap.gmail.com', 993, true)
client.connect.bind! do
client.login("conrad.irwin@gmail.com", ENV["GMAIL_PASSWORD"])
@@ -59,6 +64,7 @@ Once the authentication has completed successfully, you can perform IMAP command
end.errback do |error|
puts "Connecting, logging in or listing failed: #{error}"
end
+```
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).
@@ -68,6 +74,7 @@ In order to do useful things which actual messages, you need to first select a m
For example to search for all emails relevant to em-imap in Gmail:
+```ruby
client = EM::IMAP.new('imap.gmail.com', 993, true)
client.connect.bind! do
client.login("conrad.irwin@gmail.com", ENV["GMAIL_PASSWORD"])
@@ -80,9 +87,11 @@ For example to search for all emails relevant to em-imap in Gmail:
end.errback do |error|
puts "Something failed: #{error}"
end
+```
Once you have a list of message sequence numbers, as returned by search, you can actually read the emails with `.fetch`:
+```ruby
client = EM::IMAP.new('imap.gmail.com', 993, true)
client.connect.bind! do
client.login("conrad.irwin@gmail.com", ENV["GMAIL_PASSWORD"])
@@ -97,6 +106,7 @@ Once you have a list of message sequence numbers, as returned by search, you can
end.errback do |error|
puts "Something failed: #{error}"
end
+```
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.
@@ -106,6 +116,7 @@ IMAP has the notion of untagged responses (aka. unsolicited responses). The idea
For example, we could insert a listener into the above example to find out some interesting numbers:
+```ruby
end.bind! do
client.select('[Google Mail]/All Mail').listen do |response|
case response.name
@@ -116,22 +127,7 @@ For example, we could insert a listener into the above example to find out some
end
end
end.bind! do
-
-One IMAP command that exists solely to receive such unsolicited responses is IDLE. The IDLE command blocks the connection so that no other commands can use it, so before you can send further commands you must `stop` the IDLE command:
-
- idler = client.idle
-
- idler.listen do |response|
- if (response.name == "EXISTS" rescue nil)
- puts "Ooh, new emails!"
- idler.stop
- idler.callback do
- # ... process new emails
- end
- end
- end.errback do |e|
- puts "Idler recieved an error: #{e}"
- end
+```
### Concurrency
@@ -139,15 +135,18 @@ IMAP is an explicitly concurrent protocol: clients MAY send commands without wai
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:
+```ruby
handler = client.add_response_handler do |response|
puts "Server says: #{response}"
end.errback do |e|
puts "Connection closed?: #{e}"
end
EM::Timer.new(600){ handler.stop }
+```
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.
+```ruby
client = EM::Imap.new('imap.gmail.com', 993, true)
client.connect.callback do
logger_in = client.login('conrad.irwin@gmail.com', ENV["GMAIL_PASSWORD"])
@@ -160,6 +159,30 @@ If you want to send commands without waiting for previous replies, you can also
selecter.errback{ |e| searcher.fail e }
searcher.errback{ |e| "Something failed: #{e}" }
end
+```
+
+### IDLE
+
+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.
+
+```ruby
+ client = EM::IMAP.new('imap.gmail.com', 993, true)
+ client.connect.bind! do
+ client.login("conrad.irwin@gmail.com", ENV["GMAIL_PASSWORD"])
+ end.bind! do
+ client.select('INBOX')
+ end.bind! do
+
+ client.wait_for_new_emails do |response|
+ client.fetch(response.data).callback{ |fetched| puts fetched.inspect }
+ end
+
+ end.errback do |error|
+ puts "Something failed: #{error}"
+ end
+```
+
+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.
## TODO
2  em-imap.gemspec
View
@@ -1,6 +1,6 @@
Gem::Specification.new do |gem|
gem.name = 'em-imap'
- gem.version = '0.3.0'
+ gem.version = '0.4.0'
gem.summary = 'An EventMachine based IMAP client.'
gem.description = "Allows you to connect to an IMAP4rev1 server in a non-blocking fashion."
73 lib/em-imap/client.rb
View
@@ -375,6 +375,79 @@ def idle(&block)
end
end
+ # A Wrapper around the IDLE command that lets you wait until one email is received
+ #
+ # Returns a deferrable that succeeds when the IDLE command succeeds, or fails when
+ # the IDLE command fails.
+ #
+ # If a new email has arrived, the deferrable will succeed with the EXISTS response,
+ # otherwise it will succeed with nil.
+ #
+ # client.wait_for_one_email.bind! do |response|
+ # process_new_email(response) if response
+ # end
+ #
+ # This method will be default wait for 29minutes as suggested by the IMAP spec.
+ #
+ # WARNING: just as with IDLE, no further commands can be sent over this connection
+ # until this deferrable has succeeded. You can stop it ahead of time if needed by
+ # calling stop on the returned deferrable.
+ #
+ # idler = client.wait_for_one_email.bind! do |response|
+ # process_new_email(response) if response
+ # end
+ # idler.stop
+ #
+ # See also {wait_for_new_emails}
+ #
+ def wait_for_one_email(timeout=29 * 60)
+ exists_response = nil
+ idler = idle
+ EM::Timer.new(timeout) { idler.stop }
+ idler.listen do |response|
+ if Net::IMAP::UntaggedResponse === response && response.name =~ /\AEXISTS\z/i
+ exists_response = response
+ idler.stop
+ end
+ end.transform{ exists_response }
+ end
+
+ # Wait for new emails to arrive, and call the block when they do.
+ #
+ # This method will run until the upstream connection is closed,
+ # re-idling after every 29 minutes as implied by the IMAP spec.
+ # If you want to stop it, call .stop on the returned listener
+ #
+ # idler = client.wait_for_new_emails do |exists_response, &stop_waiting|
+ # client.fetch(exists_response.data).bind! do |response|
+ # puts response
+ # end
+ # end
+ #
+ # idler.stop
+ #
+ # NOTE: the block should return a deferrable that succeeds when you
+ # are done processing the exists_response. At that point, the idler
+ # will be turned back on again.
+ #
+ def wait_for_new_emails(wrapper=Listener.new, &block)
+ wait_for_one_email.listen do |response|
+ wrapper.receive_event response
+ end.bind! do |response|
+ block.call response if response
+ end.bind! do
+ if wrapper.stopped?
+ wrapper.succeed
+ else
+ wait_for_new_emails(wrapper, &block)
+ end
+ end.errback do |*e|
+ wrapper.fail *e
+ end
+
+ wrapper
+ end
+
def add_response_handler(&block)
@connection.add_response_handler(&block)
end
Please sign in to comment.
Something went wrong with that request. Please try again.