Friday, November 6, 2009

Recipe 14.6. Reading Mail with IMAP










Recipe 14.6. Reading Mail with IMAP




Credit: John Wells



Problem


You want to connect to an
IMAP server in order to read and manipulate the messages stored there.




Solution


The
net/imap.rb
package, written by Shugo Maeda, is part of Ruby's standard library, and provides a very capable base on which to build an IMAP-oriented email application. In the following sections, I'll walk you through various ways of using this API to interact with an IMAP server.


For this recipe, let's assume you have access to an
IMAP server running at mail.myhost.com on the standard IMAP port 143. Your username is, conveniently, "username", and your password is "password".


To make the initial connection to the server, it's as simple as:



require 'net/imap'

conn = Net::IMAP.new('mail.myhost.com', 143)
conn.login('username', 'password')



Assuming no error messages were received, you now have a connection to the
IMAP server. The Net::IMAP object puts all the capabilities of IMAP at your fingertips.


Before doing anything, though, you must tell the server which mailbox you're interested in working with. On most IMAP servers, your default mailbox is called "INBOX". You can change mailboxes with Net::IMAP#examine:



conn.examine('INBOX')
# Use Net::IMAP#select instead for read-only access



A search provides a good example of how a Net::IMAP object lets you interact with the server. To search for all messages in the selected mailbox from a particular address, you can use this code:



conn.search(['FROM', 'jabba@huttfoundation.org']).each do |sequence|
fetch_result = conn.fetch(sequence, 'ENVELOPE')
envelope = fetch_result[0].attr['ENVELOPE']
printf("%s - From: %s - To: %s - Subject: %s\n", envelope.date,
envelope.from[0].name, envelope.to[0].name, envelope.subject)
end
# Wed Feb 08 14:07:21 EST 2006 - From: The Hutt Foundation - To: You - Subject: Bwah!
# Wed Feb 08 11:21:19 EST 2006 - From: The Hutt Foundation - To: You - Subject: Go to
# do wa IMAP





Discussion


The details of the IMAP protocol are a bit esoteric, and to really understand it you'll need to read the RFC. That said, the code in the solution shouldn't be too hard to understand: it uses the IMAP SEARCH command to find all messages with the FROM field set to "jabba@huttfoundation.org".


The call to Net::IMAP#search returns an array of message sequence IDs: a key to a message within the IMAP server. We iterate over these keys and send each one back to the server, using IMAP's FETCH command to ask for the envelope (the headers) of each message. Note that the Ruby method for an IMAP instruction often shares the instruction's name, only in lowercase to keep with the Ruby way.


The ENVELOPE parameter we pass to
Net::IMAP#fetch
tells the server to give us summary information about the message by parsing the RFC2822 message headers. This way we don't have to download the entire body of the message just to look at the headers.


You'll also notice that Net::
IMAP#fetch
returns an array, and that we access its first element to get the information we're after. This is because Net::
IMAP#fetch
lets you to pass an array of sequence numbers instead of just one. It returns an array of Net::IMAP::FetchData objects with an element corresponding to each number passed in. You get an array even if you only pass in one sequence number.


There are also other cool things you can do.



Check for new mail

You can see how many new messages have arrived by examining the responses sent by the server when you select a mailbox. These are stored in a hash: the responses member of your connection object. Per the IMAP spec, the value of RECENT is the number of new messages unseen by any client. EXISTS tells how many total messages are in the box. Once a client connects and opens the mailbox, the RECENT response will be unset, so you'll only see a new message count the first time you run the command:



puts "#{conn.responses["RECENT"]} new messages, #{conn.responses["EXISTS"]} total"
# 10 new messages, 1022 total





Retrieve a UID for a particular message

The sequence number is part of a relative sequential numbering of all the messages in the current mailbox. Sequence numbers get reassigned upon message deletion and other operations, so they're not reliable over the long term. The UID is more like a primary key for the message: it is assigned when a message arrives and is guaranteed not to be reassigned or reused for the life of the mailbox. This makes it a more reliable way of making sure you've got the right message:




uids = conn.search(["FROM", "jabba@huttfoundation.org"]).collect do |sequence|
fetch_result = conn.fetch(sequence, "UID")
puts "UID: #{fetch_result[0].attr["UID"]}"
end
# UID: 203
# UID: 206



Why are message UIDs useful? Consider the following scenario. We've just retrieved message information
for messages between January 2000 and January 2006. While viewing the output, we saw a message that looked interesting, and noted the UID was 203.


To view the message body, we use code like this:



puts conn.uid_fetch(203, 'BODY[TEXT]')[0].attr['BODY[TEXT]']





Reading headers made easy



In our first example in this recipe, we accessed message headers through use of the IMAP ENVELOPE parameter. Because displaying envelope information is such a common task, I prefer to take advantage of Ruby's open classes and add this functionality directly to Net::
IMAP
:



class Net::IMAP
def get_msg_info(msg_sequence_num)
# code we used above
fetch_result = fetch(msg_sequence_num, '(UID ENVELOPE)')
envelope = fetch_result[0].attr['ENVELOPE']
uid = fetch_result[0].attr['UID']
info = {'UID' => uid,
'Date' => envelope.date,
'From' => envelope.from[0].name,
'To' => envelope.to[0].name,
'Subject' => envelope.subject}
end
end



Now, we can make use of this code wherever it's convenient. For example, in this search for all messages received in a certain date range:



conn.search(['BEFORE', '01-Jan-2006',
'SINCE', '01-Jan-2000']).each do |sequence|
conn.get_msg_info(sequence).each {|key, val| puts "#{key}: #{val}" }
end





Forwarding mail to a cell phone



As a final, somewhat practical example, let's say you're waiting for a very important email from someone at huttfoundation.org. Let's also assume you have an SMTP server at the same host as your
IMAP server, running on port 25.


You'd like to have a program that could check your email every five minutes. If a new message from anyone at huttfoundation.org is found, you'd like to forward that message to your cell phone via SMS. The email address of your cell phone is 5555555555@mycellphoneprovider.com.



#!/usr/bin/ruby -w
# forward_important_messages.rb

require 'net/imap'
require 'net/smtp'

address = 'huttfoundation.org'
from = 'myhomeemail@my.mailhost.com'
to = '5555555555@mycellphoneprovider.com'
smtp_server = 'my.mailhost.com'
imap_server = 'my.mailhost.com'
username = 'username'
password = 'password'

while true do
conn = imap = Net::IMAP.new(imap_server, 143)
conn.login(username, password)
conn.select('INBOX')
uids = conn.search(['FROM', address, 'UNSEEN']).each do |sequence|
fetch_result = conn.fetch(sequence, 'BODY[TEXT]')
text = fetch_result[0].attr['BODY[TEXT]']
count = 1
while(text.size > 0) do
# SMS messages limited to 160 characters
msg = text.slice!(0, 159)
full_msg = "From: #{from}\n"
full_msg += "To: #{to}\n"
full_msg += "Subject: Found message from #{address} (#{count})!\n"
full_msg += "Date: #{Time.now}\n"
full_msg += msg + "\n"
Net::SMTP.start(smtp_server, 25) do |smtp|
smtp.send_message full_msg, from, to
end
count += 1
end
# set Seen flag, so our search won't find the message again
conn.store(sequence, '+FLAGS', [:Seen])
end
conn.disconnect
# Sleep for 5 minutes.
sleep (60*60*5)
end



This recipe should give you a hint of the power you have when you access
IMAP mailboxes. Please note that to really understand
IMAP, you need to read the IMAP RFC, as well as RFC2822, which describes the Internet Message Format. Multipart messages and MIME types are beyond of the scope of this recipe, but are both something you'll deal with regularly when accessing mailboxes.





See Also


  • ri Net::IMAP

  • The IMAP RFC (RFC3501) (http://www.faqs.org/rfcs/rfc3501.html)

  • The Internet Message Format RFC (RFC2822) (http://www.faqs.org/rfcs/rfc2822.html)

  • Recipe 3.12, "Running a Code Block Periodically"

  • Recipe 14.5, "Sending Mail"













No comments:

Post a Comment