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"
Категории