class EventMachine::IMAP::Client

TODO: Anything that accepts or returns a mailbox name should have UTF7 support.

Public Class Methods

new(host, port, usessl=false) click to toggle source
# File lib/em-imap/client.rb, line 10
def initialize(host, port, usessl=false)
  @connect_args=[host, port, usessl]
end

Public Instance Methods

add_response_handler(&block) click to toggle source
# File lib/em-imap/client.rb, line 451
def add_response_handler(&block)
  @connection.add_response_handler(&block)
end
append(mailbox, message, flags=nil, date_time=nil) click to toggle source

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.

# File lib/em-imap/client.rb, line 214
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
authenticate(auth_type, *args) click to toggle source

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.

# File lib/em-imap/client.rb, line 91
def authenticate(auth_type, *args)
  # Extract these first so that any exceptions can be raised
  # before the command is created.
  auth_type = auth_type.to_s.upcase
  auth_handler = authenticator(auth_type, *args)

  tagged_response('AUTHENTICATE', auth_type).tap do |command|
    @connection.send_authentication_data(auth_handler, command)
  end
end
capability() click to toggle source

Ask the server which capabilities it supports.

Succeeds with an array of capabilities.

# File lib/em-imap/client.rb, line 32
def capability
  one_data_response("CAPABILITY").transform{ |response| response.data }
end
check() click to toggle source

Checkpoint the current mailbox.

This is an implementation-defined operation, when in doubt, NOOP should be used instead.

# File lib/em-imap/client.rb, line 229
def check
  tagged_response("CHECK")
end
close() click to toggle source

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).

# File lib/em-imap/client.rb, line 239
def close
  tagged_response("CLOSE")
end
connect() click to toggle source
# File lib/em-imap/client.rb, line 14
def connect
  @connection = EM::IMAP::Connection.connect(*@connect_args)
  @connection.errback{ |e| fail e }.
              callback{ |*args| succeed *args }

  @connection.hello_listener
end
copy(seq, mailbox) click to toggle source

Copy the specified messages to another mailbox.

# File lib/em-imap/client.rb, line 347
def copy(seq, mailbox)
  tagged_response("COPY", Net::IMAP::MessageSet.new(seq), to_utf7(mailbox))
end
create(mailbox) click to toggle source

Create a new mailbox with the given name.

# File lib/em-imap/client.rb, line 136
def create(mailbox)
  tagged_response("CREATE", to_utf7(mailbox))
end
delete(mailbox) click to toggle source

Delete the mailbox with this name.

# File lib/em-imap/client.rb, line 142
def delete(mailbox)
  tagged_response("DELETE", to_utf7(mailbox))
end
disconnect() click to toggle source
# File lib/em-imap/client.rb, line 22
def disconnect
  @connection.close_connection
end
examine(mailbox) click to toggle source

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.

# File lib/em-imap/client.rb, line 130
def examine(mailbox)
  tagged_response("EXAMINE", to_utf7(mailbox))
end
expunge() click to toggle source

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.

# File lib/em-imap/client.rb, line 252
def expunge
  multi_data_response("EXPUNGE").transform do |untagged_responses|
    untagged_responses.map(&:data)
  end
end
fetch(seq, attr="FULL") click to toggle source

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
# File lib/em-imap/client.rb, line 316
def fetch(seq, attr="FULL")
  fetch_internal("FETCH", seq, attr)
end
idle(&block) click to toggle source

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 }

# File lib/em-imap/client.rb, line 371
def idle(&block)
  send_command("IDLE").tap do |command|
    @connection.prepare_idle_continuation(command)
    command.listen(&block) if block_given?
  end
end
list(refname="", pattern="*") click to toggle source

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.
# File lib/em-imap/client.rb, line 174
def list(refname="", pattern="*")
  list_internal("LIST", refname, pattern)
end
login(username, password) click to toggle source

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.

# File lib/em-imap/client.rb, line 109
def login(username, password)
  tagged_response("LOGIN", username, password)
end
logout() click to toggle source

Logout and close the connection.

This will cause any other listeners or commands that are still active to fail, and render this client unusable.

# File lib/em-imap/client.rb, line 52
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
lsub(refname, pattern) click to toggle source

List all subscribed mailboxes.

This is the same as list, but restricted to mailboxes that have been subscribed to.

# File lib/em-imap/client.rb, line 182
def lsub(refname, pattern)
  list_internal("LSUB", refname, pattern)
end
noop() click to toggle source

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.

# File lib/em-imap/client.rb, line 43
def noop
  tagged_response("NOOP")
end
rename(oldname, newname) click to toggle source

Rename the mailbox with this name.

# File lib/em-imap/client.rb, line 148
def rename(oldname, newname)
  tagged_response("RENAME", to_utf7(oldname), to_utf7(newname))
end
select(mailbox) click to toggle source

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.

# File lib/em-imap/client.rb, line 121
def select(mailbox)
  tagged_response("SELECT", to_utf7(mailbox))
end
sort(sort_keys, *args) click to toggle source

SORT and THREAD (like SEARCH) from tools.ietf.org/search/rfc5256

# File lib/em-imap/client.rb, line 281
def sort(sort_keys, *args)
  raise NotImplementedError
end
start_tls()

the IMAP command is STARTTLS, the eventmachine method is start_tls. Let’s be nice to everyone and make both work.

Alias for: starttls
starttls() click to toggle source

Run a STARTTLS handshake.

C: “STARTTLSrn” S: “OK go aheadrn” C: <tls handshake> S: <tls handshake>

Succeeds with the OK response after the TLS handshake is complete.

# File lib/em-imap/client.rb, line 72
def starttls
  tagged_response("STARTTLS").bind! do |response|
    @connection.start_tls.transform{ response }
  end
end
Also aliased as: start_tls
status(mailbox, attrs=['MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN']) click to toggle source

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.

# File lib/em-imap/client.rb, line 197
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
store(seq, name, value) click to toggle source

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)

# File lib/em-imap/client.rb, line 335
def store(seq, name, value)
  store_internal("STORE", seq, name, value)
end
subscribe(mailbox) click to toggle source

Add this mailbox to the list of subscribed mailboxes.

# File lib/em-imap/client.rb, line 154
def subscribe(mailbox)
  tagged_response("SUBSCRIBE", to_utf7(mailbox))
end
thread(algorithm, *args) click to toggle source
# File lib/em-imap/client.rb, line 289
def thread(algorithm, *args)
  raise NotImplementedError
end
uid_copy(seq, mailbox) click to toggle source

The same as copy, but keyed off UIDs instead of sequence numbers.

# File lib/em-imap/client.rb, line 353
def uid_copy(seq, mailbox)
  tagged_response("UID", "COPY", Net::IMAP::MessageSet.new(seq), to_utf7(mailbox))
end
uid_fetch(seq, attr="FULL") click to toggle source

The same as fetch, but keyed of UIDs instead of sequence numbers.

# File lib/em-imap/client.rb, line 322
def uid_fetch(seq, attr="FULL")
  fetch_internal("UID FETCH", seq, attr)
end
uid_sort(sort_keys, *args) click to toggle source
# File lib/em-imap/client.rb, line 285
def uid_sort(sort_keys, *args)
  raise NotImplementedError
end
uid_store(seq, name, value) click to toggle source

The same as store, but keyed off UIDs instead of sequence numbers.

# File lib/em-imap/client.rb, line 341
def uid_store(seq, name, value)
  store_internal("UID", "STORE", seq, name, value)
end
uid_thread(algorithm, *args) click to toggle source
# File lib/em-imap/client.rb, line 293
def uid_thread(algorithm, *args)
  raise NotImplementedError
end
unsubscribe(mailbox) click to toggle source

Remove this mailbox from the list of subscribed mailboxes.

# File lib/em-imap/client.rb, line 160
def unsubscribe(mailbox)
  tagged_response("UNSUBSCRIBE", to_utf7(mailbox))
end
wait_for_new_emails(wrapper=Listener.new, &block) click to toggle source

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.

# File lib/em-imap/client.rb, line 433
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
wait_for_one_email(timeout=29 * 60) click to toggle source

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}

# File lib/em-imap/client.rb, line 403
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

Private Instance Methods

collect_untagged_responses(name, *command) click to toggle source

Send a command that should return a deferrable that succeeds with multiple untagged responses with the given name.

# File lib/em-imap/client.rb, line 529
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
fetch_internal(cmd, set, attr) click to toggle source

From Net::IMAP

# File lib/em-imap/client.rb, line 563
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
force_encoding(s, encoding) click to toggle source

FIXME: I haven’t thought through the ramifications of this yet.

# File lib/em-imap/client.rb, line 493
def force_encoding(s, encoding)
  if s.respond_to?(:force_encoding)
    s.force_encoding(encoding)
  else
    s
  end
end
list_internal(cmd, refname, pattern) click to toggle source

Extract more useful data from the LIST and LSUB commands, see list for details.

# File lib/em-imap/client.rb, line 552
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
multi_data_response(cmd, *args) click to toggle source

Send a command that should return a deferrable that succeeds with multiple untagged responses with the same name as the command.

# File lib/em-imap/client.rb, line 522
def multi_data_response(cmd, *args)
  collect_untagged_responses(cmd, cmd, *args)
end
normalize_search_criteria(args) click to toggle source

Recursively find all the message sets in the arguments and convert them so that Net::IMAP can serialize them.

# File lib/em-imap/client.rb, line 597
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
one_data_response(cmd, *args) click to toggle source

Send a command that should return a deferrable that succeeds with a single untagged response with the same name as the command.

# File lib/em-imap/client.rb, line 513
def one_data_response(cmd, *args)
  multi_data_response(cmd, *args).transform do |untagged_responses|
    untagged_responses.last
  end
end
search_internal(command, *args) click to toggle source
# File lib/em-imap/client.rb, line 589
def search_internal(command, *args)
  collect_untagged_responses('SEARCH', command, *normalize_search_criteria(args)).transform do |untagged_responses|
    untagged_responses.last.data
  end
end
send_command(cmd, *args) click to toggle source
# File lib/em-imap/client.rb, line 547
def send_command(cmd, *args)
  @connection.send_command(cmd, *args)
end
store_internal(cmd, set, attr, flags) click to toggle source

Ensure that the flags are symbols, and that the message set is a message set.

# File lib/em-imap/client.rb, line 581
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
tagged_response(cmd, *args) click to toggle source

Send a command that should return a deferrable that succeeds with a tagged_response.

# File lib/em-imap/client.rb, line 504
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
to_utf7(s) click to toggle source

Encode a string from UTF-8 format to modified UTF-7.

# File lib/em-imap/client.rb, line 481
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
to_utf8(s) click to toggle source

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.

# File lib/em-imap/client.rb, line 465
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