class EventMachine::IMAP::Client
TODO: Anything that accepts or returns a mailbox name should have UTF7 support.
Public Class Methods
# File lib/em-imap/client.rb, line 10 def initialize(host, port, usessl=false) @connect_args=[host, port, usessl] end
Public Instance Methods
# File lib/em-imap/client.rb, line 451 def add_response_handler(&block) @connection.add_response_handler(&block) 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.
# 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 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
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
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
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
# 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 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 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 the mailbox with this name.
# File lib/em-imap/client.rb, line 142 def delete(mailbox) tagged_response("DELETE", to_utf7(mailbox)) end
# File lib/em-imap/client.rb, line 22 def disconnect @connection.close_connection 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.
# File lib/em-imap/client.rb, line 130 def examine(mailbox) tagged_response("EXAMINE", to_utf7(mailbox)) 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.
# File lib/em-imap/client.rb, line 252 def expunge multi_data_response("EXPUNGE").transform do |untagged_responses| untagged_responses.map(&:data) end 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
# File lib/em-imap/client.rb, line 316 def fetch(seq, attr="FULL") fetch_internal("FETCH", seq, attr) 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 }
# 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 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
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 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
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
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 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
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.
# File lib/em-imap/client.rb, line 269 def search(*args) search_internal("SEARCH", *args) end
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 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
the IMAP
command is STARTTLS, the eventmachine method is start_tls. Let’s be nice to everyone and make both work.
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
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
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
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
# File lib/em-imap/client.rb, line 289 def thread(algorithm, *args) raise NotImplementedError end
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
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
The same as search, but succeeding with a list of UIDs not sequence numbers.
# File lib/em-imap/client.rb, line 275 def uid_search(*args) search_internal("UID SEARCH", *args) end
# File lib/em-imap/client.rb, line 285 def uid_sort(sort_keys, *args) raise NotImplementedError end
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
# File lib/em-imap/client.rb, line 293 def uid_thread(algorithm, *args) raise NotImplementedError end
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 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
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
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
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
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
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
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
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
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
# 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
# File lib/em-imap/client.rb, line 547 def send_command(cmd, *args) @connection.send_command(cmd, *args) end
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
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
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
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