Credit: John Wells
You want to connect to an IMAP server in order to read and manipulate the messages stored there.
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
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.
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
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]]
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
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.
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