Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: 436d978eed
Fetching contributors…

Cannot retrieve contributors at this time

616 lines (549 sloc) 20.389 kb
module EventMachine
module IMAP
# TODO: Anything that accepts or returns a mailbox name should have UTF7 support.
class Client
include EM::Deferrable
DG.enhance!(self)
include IMAP::Authenticators
def initialize(host, port, usessl=false)
@connect_args=[host, port, usessl]
end
def connect
@connection = EM::IMAP::Connection.connect(*@connect_args)
@connection.errback{ |e| fail e }.
callback{ |*args| succeed *args }
@connection.hello_listener
end
def disconnect
@connection.close_connection
end
## 6.1 Client Commands - Any State.
# Ask the server which capabilities it supports.
#
# Succeeds with an array of capabilities.
#
def capability
one_data_response("CAPABILITY").transform{ |response| response.data }
end
# Actively do nothing.
#
# This is useful as a keep-alive, or to persuade the server to send
# any untagged responses your listeners would like.
#
# Succeeds with nil.
#
def noop
tagged_response("NOOP")
end
# Logout and close the connection.
#
# This will cause any other listeners or commands that are still active
# to fail, and render this client unusable.
#
def logout
command = tagged_response("LOGOUT").errback do |e|
if e.is_a? Net::IMAP::ByeResponseError
# RFC 3501 says the server MUST send a BYE response and then close the connection.
disconnect
command.succeed
end
end
end
## 6.2 Client Commands - Not Authenticated State
# Run a STARTTLS handshake.
#
# C: "STARTTLS\r\n"
# S: "OK go ahead\r\n"
# C: <tls handshake>
# S: <tls handshake>
#
# Succeeds with the OK response after the TLS handshake is complete.
def starttls
tagged_response("STARTTLS").bind! do |response|
@connection.start_tls.transform{ response }
end
end
# the IMAP command is STARTTLS, the eventmachine method is start_tls.
# Let's be nice to everyone and make both work.
alias_method :start_tls, :starttls
# Authenticate using a custom authenticator.
#
# By default there are two custom authenticators available:
#
# 'LOGIN', username, password
# 'CRAM-MD5', username, password (see RFC 2195)
#
# Though you can add new mechanisms using EM::IMAP.add_authenticator,
# see for example the gmail_xoauth gem.
#
def authenticate(auth_type, *args)
# Extract these first so that any exceptions can be raised
# before the command is created.
auth_type = auth_type.upcase
auth_handler = authenticator(auth_type, *args)
tagged_response('AUTHENTICATE', auth_type).tap do |command|
@connection.send_authentication_data(auth_handler, command)
end
end
# Authenticate with a username and password.
#
# NOTE: this SHOULD only work over a tls connection.
#
# If the password is wrong, the command will fail with a
# Net::IMAP::NoResponseError.
#
def login(username, password)
tagged_response("LOGIN", username, password)
end
## 6.3 Client Commands - Authenticated State
# Select a mailbox for performing commands against.
#
# This will generate untagged responses that you can subscribe to
# by adding a block to the listener with .listen, for more detail,
# see RFC 3501, section 6.3.1.
#
def select(mailbox)
tagged_response("SELECT", to_utf7(mailbox))
end
# Select a mailbox for performing read-only commands.
#
# This is exactly the same as select, except that no operation may
# cause a change to the state of the mailbox or its messages.
#
def examine(mailbox)
tagged_response("EXAMINE", to_utf7(mailbox))
end
# Create a new mailbox with the given name.
#
def create(mailbox)
tagged_response("CREATE", to_utf7(mailbox))
end
# Delete the mailbox with this name.
#
def delete(mailbox)
tagged_response("DELETE", to_utf7(mailbox))
end
# Rename the mailbox with this name.
#
def rename(oldname, newname)
tagged_response("RENAME", to_utf7(oldname), to_utf7(newname))
end
# Add this mailbox to the list of subscribed mailboxes.
#
def subscribe(mailbox)
tagged_response("SUBSCRIBE", to_utf7(mailbox))
end
# Remove this mailbox from the list of subscribed mailboxes.
#
def unsubscribe(mailbox)
tagged_response("UNSUBSCRIBE", to_utf7(mailbox))
end
# List all available mailboxes.
#
# @param: refname, an optional context in which to list.
# @param: mailbox, a which mailboxes to return.
#
# Succeeds with a list of Net::IMAP::MailboxList structs, each of which has:
# .name, the name of the mailbox (in UTF8)
# .delim, the delimeter (normally "/")
# .attr, A list of attributes, e.g. :Noselect, :Haschildren, :Hasnochildren.
#
def list(refname="", pattern="*")
list_internal("LIST", refname, pattern)
end
# List all subscribed mailboxes.
#
# This is the same as list, but restricted to mailboxes that have been subscribed to.
#
def lsub(refname, pattern)
list_internal("LSUB", refname, pattern)
end
# Get the status of a mailbox.
#
# This provides similar information to the untagged responses you would
# get by running SELECT or EXAMINE without doing so.
#
# @param mailbox, a mailbox to query
# @param attrs, a list of attributes to query for (valid values include
# MESSAGES, RECENT, UIDNEXT, UIDVALIDITY and UNSEEN — RFC3501#6.3.8)
#
# Succeeds with a hash of attribute name to value returned by the server.
#
def status(mailbox, attrs=['MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN'])
attrs = [attrs] if attrs.is_a?(String)
one_data_response("STATUS", to_utf7(mailbox), attrs).transform do |response|
response.data.attr
end
end
# Add a message to the mailbox.
#
# @param mailbox, the mailbox to add to,
# @param message, the full text (including headers) of the email to add.
# @param flags, A list of flags to set on the email.
# @param date_time, The time to be used as the internal date of the email.
#
# The tagged response with which this command succeeds contains the UID
# of the email that was appended.
#
def append(mailbox, message, flags=nil, date_time=nil)
args = [to_utf7(mailbox)]
args << flags if flags
args << date_time if date_time
args << Net::IMAP::Literal.new(message)
tagged_response("APPEND", *args)
end
# 6.4 Client Commands - Selected State
# Checkpoint the current mailbox.
#
# This is an implementation-defined operation, when in doubt, NOOP
# should be used instead.
#
def check
tagged_response("CHECK")
end
# Unselect the current mailbox.
#
# As a side-effect, permanently removes any messages that have the
# \Deleted flag. (Unless the mailbox was selected using the EXAMINE,
# in which case no side effects occur).
#
def close
tagged_response("CLOSE")
end
# Permanently remove any messages with the \Deleted flag from the current
# mailbox.
#
# Succeeds with a list of message sequence numbers that were deleted.
#
# NOTE: If you're planning to EXPUNGE and then SELECT a new mailbox,
# and you don't care which messages are removed, consider using
# CLOSE instead.
#
def expunge
multi_data_response("EXPUNGE").transform do |untagged_responses|
untagged_responses.map(&:data)
end
end
# Search for messages in the current mailbox.
#
# @param *args The arguments to search, these can be strings, arrays or ranges
# specifying sub-groups of search arguments or sets of messages.
#
# If you want to use non-ASCII characters, then the first two
# arguments should be 'CHARSET', 'UTF-8', though not all servers
# support this.
#
# @succeed A list of message sequence numbers.
#
def search(*args)
search_internal("SEARCH", *args)
end
# The same as search, but succeeding with a list of UIDs not sequence numbers.
#
def uid_search(*args)
search_internal("UID SEARCH", *args)
end
# SORT and THREAD (like SEARCH) from http://tools.ietf.org/search/rfc5256
#
def sort(sort_keys, *args)
raise NotImplementedError
end
def uid_sort(sort_keys, *args)
raise NotImplementedError
end
def thread(algorithm, *args)
raise NotImplementedError
end
def uid_thread(algorithm, *args)
raise NotImplementedError
end
# Get the contents of, or information about, a message.
#
# @param seq, a message or sequence of messages (a number, a range or an array of numbers)
# @param attr, the name of the attribute to fetch, or a list of attributes.
#
# Possible attribute names (see RFC 3501 for a full list):
#
# ALL: Gets all header information,
# FULL: Same as ALL with the addition of the BODY,
# FAST: Same as ALL without the message envelope.
#
# BODY: The body
# BODY[<section>] A particular section of the body
# BODY[<section>]<<start>,<length>> A substring of a section of the body.
# BODY.PEEK: The body (but doesn't change the \Recent flag)
# FLAGS: The flags
# INTERNALDATE: The internal date
# UID: The unique identifier
#
def fetch(seq, attr="FULL")
fetch_internal("FETCH", seq, attr)
end
# The same as fetch, but keyed of UIDs instead of sequence numbers.
#
def uid_fetch(seq, attr="FULL")
fetch_internal("UID FETCH", seq, attr)
end
# Update the flags on a message.
#
# @param seq, a message or sequence of messages (a number, a range, or an array of numbers)
# @param name, any of FLAGS FLAGS.SILENT, replace the flags
# +FLAGS, +FLAGS.SILENT, add the following flags
# -FLAGS, -FLAGS.SILENT, remove the following flags
# The .SILENT versions suppress the server's responses.
# @param value, a list of flags (symbols)
#
def store(seq, name, value)
store_internal("STORE", seq, name, value)
end
# The same as store, but keyed off UIDs instead of sequence numbers.
#
def uid_store(seq, name, value)
store_internal("UID", "STORE", seq, name, value)
end
# Copy the specified messages to another mailbox.
#
def copy(seq, mailbox)
tagged_response("COPY", Net::IMAP::MessageSet.new(seq), to_utf7(mailbox))
end
# The same as copy, but keyed off UIDs instead of sequence numbers.
#
def uid_copy(seq, mailbox)
tagged_response("UID", "COPY", Net::IMAP::MessageSet.new(seq), to_utf7(mailbox))
end
# The IDLE command allows you to wait for any untagged responses
# that give status updates about the contents of a mailbox.
#
# Until you call stop on the idler, no further commands can be sent
# over this connection.
#
# idler = connection.idle do |untagged_response|
# case untagged_response.name
# #...
# end
# end
#
# EM.timeout(60) { idler.stop }
#
def idle(&block)
send_command("IDLE").tap do |command|
@connection.prepare_idle_continuation(command)
command.listen(&block) if block_given?
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
private
# Decode a string from modified UTF-7 format to UTF-8.
#
# UTF-7 is a 7-bit encoding of Unicode [UTF7]. IMAP uses a
# slightly modified version of this to encode mailbox names
# containing non-ASCII characters; see [IMAP] section 5.1.3.
#
# Net::IMAP does _not_ automatically encode and decode
# mailbox names to and from utf7.
def to_utf8(s)
return force_encoding(s.gsub(/&(.*?)-/n) {
if $1.empty?
"&"
else
base64 = $1.tr(",", "/")
x = base64.length % 4
if x > 0
base64.concat("=" * (4 - x))
end
base64.unpack("m")[0].unpack("n*").pack("U*")
end
}, "UTF-8")
end
# Encode a string from UTF-8 format to modified UTF-7.
def to_utf7(s)
return force_encoding(force_encoding(s, 'UTF-8').gsub(/(&)|([^\x20-\x7e]+)/u) {
if $1
"&-"
else
base64 = [$&.unpack("U*").pack("n*")].pack("m")
"&" + base64.delete("=\n").tr("/", ",") + "-"
end
}, "ASCII-8BIT")
end
# FIXME: I haven't thought through the ramifications of this yet.
def force_encoding(s, encoding)
if s.respond_to?(:force_encoding)
s.force_encoding(encoding)
else
s
end
end
# Send a command that should return a deferrable that succeeds with
# a tagged_response.
#
def tagged_response(cmd, *args)
# We put in an otherwise unnecessary transform to hide the listen
# method from callers for consistency with other types of responses.
send_command(cmd, *args)
end
# Send a command that should return a deferrable that succeeds with
# a single untagged response with the same name as the command.
#
def one_data_response(cmd, *args)
multi_data_response(cmd, *args).transform do |untagged_responses|
untagged_responses.last
end
end
# Send a command that should return a deferrable that succeeds with
# multiple untagged responses with the same name as the command.
#
def multi_data_response(cmd, *args)
collect_untagged_responses(cmd, cmd, *args)
end
# Send a command that should return a deferrable that succeeds with
# multiple untagged responses with the given name.
#
def collect_untagged_responses(name, *command)
untagged_responses = []
send_command(*command).listen do |response|
if response.is_a?(Net::IMAP::UntaggedResponse) && response.name == name
untagged_responses << response
# If we observe another tagged response completeing, then we can be
# sure that the previous untagged responses were not relevant to this command.
elsif response.is_a?(Net::IMAP::TaggedResponse)
untagged_responses = []
end
end.transform do |tagged_response|
untagged_responses
end
end
def send_command(cmd, *args)
@connection.send_command(cmd, *args)
end
# Extract more useful data from the LIST and LSUB commands, see #list for details.
def list_internal(cmd, refname, pattern)
multi_data_response(cmd, to_utf7(refname), to_utf7(pattern)).transform do |untagged_responses|
untagged_responses.map(&:data).map do |data|
data.dup.tap do |new_data|
new_data.name = to_utf8(data.name)
end
end
end
end
# From Net::IMAP
def fetch_internal(cmd, set, attr)
case attr
when String then
attr = Net::IMAP::RawData.new(attr)
when Array then
attr = attr.map { |arg|
arg.is_a?(String) ? Net::IMAP::RawData.new(arg) : arg
}
end
set = Net::IMAP::MessageSet.new(set)
collect_untagged_responses('FETCH', cmd, set, attr).transform do |untagged_responses|
untagged_responses.map(&:data)
end
end
# Ensure that the flags are symbols, and that the message set is a message set.
def store_internal(cmd, set, attr, flags)
flags = flags.map(&:to_sym)
set = Net::IMAP::MessageSet.new(set)
collect_untagged_responses('FETCH', cmd, set, attr, flags).transform do |untagged_responses|
untagged_responses.map(&:data)
end
end
def search_internal(command, *args)
collect_untagged_responses('SEARCH', command, *normalize_search_criteria(args)).transform do |untagged_responses|
untagged_responses.last.data
end
end
# Recursively find all the message sets in the arguments and convert them so that
# Net::IMAP can serialize them.
def normalize_search_criteria(args)
args.map do |arg|
case arg
when "*", -1, Range
Net::IMAP::MessageSet.new(arg)
when Array
if arg.inject(true){|bool,item| bool and (item.is_a?(Integer) or item.is_a?(Range))}
Net::IMAP::MessageSet.new(arg)
else
normalize_search_criteria(arg)
end
else
arg
end
end
end
end
end
end
Jump to Line
Something went wrong with that request. Please try again.