Permalink
Browse files

+wait_for_new_emails

  • Loading branch information...
1 parent 8618717 commit 436d978eedcd1229c0f46864bb7931aaf2d95a24 @ConradIrwin committed Feb 13, 2013
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
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,13 +28,15 @@ For example, to connect to Gmail's IMAP server, you can use the following snippe
EM::stop
end
end
+```
### Authenticating
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.
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,13 +45,15 @@ 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).
### Mailbox-level IMAP
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,38 +127,26 @@ 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
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.
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
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."
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

0 comments on commit 436d978

Please sign in to comment.