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 Rubys standard library, and provides a very capable base on which to build an IMAP-oriented email application. In the following sections, Ill walk you through various ways of using this API to interact with an IMAP server.

For this recipe, lets 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, its as simple as:

	require 
et/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 e 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
", 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 youll need to read the RFC. That said, the code in the solution shouldn 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 IMAPs FETCH command to ask for the envelope (the headers) of each message. Note that the Ruby method for an IMAP instruction often shares the instructions 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 have to download the entire body of the message just to look at the headers.

Youll also notice that Net:: IMAP#fetch returns an array, and that we access its first element to get the information we e 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 youll 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 e 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 youve 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. Weve 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 Rubys 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 its convenient. For example, in this search for all messages received in a certain date range:

	conn.search([BEFORE, 1-Jan-2006,
	 SINCE, 1-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, lets say you e waiting for a very important email from someone at huttfoundation.org. Lets also assume you have an SMTP server at the same host as your IMAP server, running on port 25.

Youd like to have a program that could check your email every five minutes. If a new message from anyone at huttfoundation.org is found, youd 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 
et/imap
	require 
et/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}
"
	 full_msg += "To: #{to}
"
	 full_msg += "Subject: Found message from #{address} (#{count})!
"
	 full_msg += "Date: #{Time.now}
"
	 full_msg += msg + "
"
	 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	 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 youll 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"


Strings

Numbers

Date and Time

Arrays

Hashes

Files and Directories

Code Blocks and Iteration

Objects and Classes8

Modules and Namespaces

Reflection and Metaprogramming

XML and HTML

Graphics and Other File Formats

Databases and Persistence

Internet Services

Web Development Ruby on Rails

Web Services and Distributed Programming

Testing, Debugging, Optimizing, and Documenting

Packaging and Distributing Software

Automating Tasks with Rake

Multitasking and Multithreading

User Interface

Extending Ruby with Other Languages

System Administration



Ruby Cookbook
Ruby Cookbook (Cookbooks (OReilly))
ISBN: 0596523696
EAN: 2147483647
Year: N/A
Pages: 399

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net