2009-02-28

Gmail Notifier using IMAP

A simple program to check if there are any new messages in our Gmail inbox.

We will connect to Gmail using IMAP protocol and get a list of new (unread) messages from mail server. Here's more or less the code that does it:
require 'net/imap' 

class GNotifier

GMAIL_IMAP="imap.gmail.com"

LOGIN="login@gmail.com" # or just "login"
PASSWORD="password"

def initialize
@envs={}
end

def check
begin
unless @imap
@imap=Net::IMAP::new(GMAIL_IMAP,993,true,nil,false)
@imap.login(LOGIN,PASSWORD)
end
@imap.select("INBOX")
ids=@imap.search(["NOT","SEEN"])
uids=ids.empty?? [] : @imap.fetch(ids,"UID").map{|e| e.attr["UID"]}
@envs.reject!{|uid,env| not uids.include?(uid)}
new_uids=uids-@envs.keys
if new_uids.empty?
new_envs=[]
else
new_envs=@imap.uid_fetch(new_uids,"ENVELOPE").map{|e| e.attr["ENVELOPE"]}
new_uids.each_with_index{|uid,i| @envs[uid]=new_envs[i]}
end
new_mail(new_envs) unless new_envs.empty?
rescue ThreadError, Errno::ECONNABORTED, Timeout::Error, IOError => e
@imap=nil
retry
end
end

def new_mail(new_m)
# ...
end

end

The code is just a draft but shows the most important part. Let's explain it a bit.

First,
@imap
is the instance of the IMAP connector. It is created once (in the first call to
check
and if nothing goes wrong, all subsequent calls to
check
do not create a new connection and do not log into the mail system, but use the previously created one. It is deleted and renewed in case of an error, though (in the
rescue
clause.

The field
@envs
is a hash holding envelopes of each new message in the inbox associated with this message's UID (unique identifier). At the beginning, it is empty.

So now how we fetch new mail: first we call
@imap.search
to get IDs (not UIDs, don't mix up the two) of all messages that do not have the SEEN flag set. Then we fetch these messages' UIDs (the ternary operator is here because
fetch
fails with empty
ids
).

So now we have the UIDs of all new messages, and we can compare it with the list of messages that we have already fetched. First, we
@envs.reject!
all messages that were new but now are not (this means that they have been deleted or marked as read, it doesn't matter for us). Then we compute the list of
new_uids
- UIDs of new messages that are new for the first time (they were not on our list) and for those messages we get some more info - the ENVELOPE - into
new_envs
and then add them to
@envs
. Finally, we call
new_mail
and pass all new new mail that arrived. This method can be also left unimplemented if we just want to know what new messages lie on the server (this info is in
@envs
of course) and do not necessarily want a notification when a new new message arrives.

Some technical details
When creating the connector, we could have written just
Net::IMAP::new(GMAIL_IMAP,993,true)
but it will not work in Ruby 1.9, where the last parameter (authenticate) is true by default.

The line
@imap.select("INBOX")
could be called within the conditional above it, but then somehow not all new messages can be accessed by IMAP. It sort of refreshes the inbox.

The ENVELOPE attribute that we download from the server contains information that would be on the envelope of a regular letter: sender, receiver(s), date, also subject. All accessed simply by method calls. Helpful link: Envelope.

If you prefer to download the whole message and not just the envelope then use the property BODY instead. If you want something more specific, look into the documentation, for example here: Net::IMAP. Note that Gmail does not support some of the commands, like
sort
for instance.

No comments: