Tuesday, August 28, 2007

Automating Outlook with Ruby: Inbox & Messages

In response to my series on Automating Outlook with Ruby, several readers have asked about accessing the Inbox and managing messages.

We start by using the win32ole library to create a new instance (or connect to a currently running instance) of the Outlook application object:


require 'win32ole'
outlook = WIN32OLE.new('Outlook.Application')

Next we'll get the MAPI namespace:

mapi = outlook.GetNameSpace('MAPI')

To get a reference to the Inbox folder, call the MAPI object's GetDefaultFolder method, passing it the integer 6, which represents the Inbox folder:

inbox = mapi.GetDefaultFolder(6)

To get the Personal Folders object, call the MAPI object's Folders.Item method, passing it the name of the folder.

personal_folders = mapi.Folders.Item('Personal Folders')

To reference a subfolder, call the parent folder object's Folders.Item method, passing it the name of the folder.

baseball_folder = personal_folders.Folders.Item('Baseball')

You can get a count of a folder's unread items by calling the UnreadItemCount method:

puts "#{inbox.UnreadItemCount} unread messages"

A folder object's Items method returns a collection of message objects, which you can iterate over:

inbox.Items.each do |message|
# Your code here...
end

You can also pass the Items method a (1-based) index to retrieve a single message:

first_message = inbox.Items(1)

Once your code tries to access methods and properties of a Message object, an Outlook security dialog will prompt the user to allow access to Address Book entries. The user must click a checkbox to allow access and select a time limit between 1 and 10 minutes.

Message objects have dozens of methods/properties, including:

SenderEmailAddress
SenderName
To
Cc
Subject
Body

To see a complete list, you could call the ole_methods method, as explained here. So to view a sorted list of the Message object's methods and properties, we could do this:

methods = []
inbox.Items(1).ole_methods.each do |method|
methods << method.to_s
end
puts methods.uniq.sort

To delete a message, call its Delete method:

message.Delete

To move a message to another folder, call its Move method, passing it the folder object:

baseball_folder = personal_folders.Folders.Item('Baseball')
message.Move(baseball_folder)

So, if we wanted to check our Inbox and move all messages that contain 'Cardinals' in the subject line to our 'Baseball' folder, we could do something like this:

inbox.Items.Count.downto(1) do |i|
message = inbox.Items(i)
if message.Subject =~ /cardinals/i
message.Move(baseball_folder)
end
end

As reader Ana points out, we should use the Count and downto methods to ensure that our inbox.Items index stays in sync, even as we move messages out of the Inbox. Otherwise, we run the risk of a message being skipped when the message above it is moved or deleted.

That concludes our program for today. Feel free to post a comment here or send me email with questions, comments, or suggestions.

Digg my article

15 comments:

Emmanuel said...

excellent! I'm going to try this soon. Thank you very much!

P.D.:

"inbox = mapi.GetDefaultFolder(6)"

How do you know is 6? Have you read it on Microsoft technet or something?

Ana Nelson said...

I have found that moving items out of a folder while iterating over the Items collection for that folder is problematic. Some of my messages weren't being processed, and I would have to run a script multiple times to fully process the target folder. I have instead switched to, e.g.,

inbox.Items.Count.downto(1) do |i|
  message = inbox.Items(i)
  if message.Subject =~ /cardinals/i
    message.Move(baseball_folder)
  end
end

which systematically iterates over all messages even when some messages have been moved/deleted.

Also, I use WIN32OLE.connect('Outlook.Application') rather than new() since I found that my inbox processing rules were not running. A search revealed that this can happen when there are multiple instances of Outlook running, and indeed since I switched to connect() my rules have been running normally again.

David Mullet said...

@Emmanuel:

Yes, I determined that the integer 6 represents the Inbox folder via code samples in books and online, such as Microsoft TechNet.

@Ana:

Excellent points, thanks! I updated my Inbox iteration example to use your method.

David

Greg said...

Is there a way to read the contents of the message (not just the subject). I'm also trying to access any attachments that the message might have, is this possible through through Ruby WIN32OLE?

David Mullet said...

@Greg:

The body of the message can be accessed via the Body property:

for item in inbox.Items
puts item.Body
end

The attachments are accessible via the message's Attachments collection. Each attachment has a FileName property and a SaveAsFile method:

for item in inbox.Items
for attachment in item.Attachments
attachment.SaveAsFile("C:\\Temp\\#{attachment.FileName}")
end
end

David

Anonymous said...

I would like to go into a subfolder under Inbox to get attachments.

So how would
inbox = mapi.GetDefaultFolder(6) apply in my case?

Thanks

Anonymous said...

How would you access a nested folder within outlook??

say I have a folder structure like

Inbox\Paul\todo

Thanks!! Good article!!

BTW in VBA I found this code snippet that Gets a folder based on a path if it does not exist it creates it then passes the folder object to the caller.... Is there a Ruby equiv to this??

Public Function GetFolder(strFolderPath As String) As MAPIFolder
' folder path needs to be something like
' "Public Folders\All Public Folders\Company\Sales"
Dim objApp As Outlook.Application
Dim objNS As Outlook.NameSpace
Dim colFolders As Outlook.Folders
Dim objFolder As Outlook.MAPIFolder
Dim arrFolders() As String
Dim I As Long
On Error Resume Next

strFolderPath = Replace(strFolderPath, "/", "\")
arrFolders() = Split(strFolderPath, "\")
Set objApp = CreateObject("Outlook.Application")
Set objNS = objApp.GetNamespace("MAPI")
Set objFolder = objNS.Folders.Item(arrFolders(0))
If Not objFolder Is Nothing Then
For I = 1 To UBound(arrFolders)
Set colFolders = objFolder.Folders
Set objFolder = Nothing
Set objFolder = colFolders.Item(arrFolders(I))
If objFolder Is Nothing Then
colFolders.Add (arrFolders(I))
Set objFolder = colFolders.Item(arrFolders(I))
'Exit For
End If
Next
End If

Set GetFolder = objFolder
Set colFolders = Nothing
Set objNS = Nothing
Set objApp = Nothing
End Function

David Mullet said...

To get a reference to a sub-folder, call the parent folder's Folders method with the name of the sub-folder:

subfolder1 = inbox.Folders('My SubFolder')

subfolder2 = subfolder1.Folders('My Sub-SubFolder')

David

HH said...

I am trying to write a ruby script to move an entire tree of messages from one folder to another. This is to counter automatic cleanup that exchange server does.

Unfortunately, I can only move about 150 messages before outlook flakes out. Here is the routine I am using to move messages from one folder to another:

def archiveMessages(sourceFolder, destinationFolder)
sourceFolder.Items.Count.downto(1) do |i|
message = sourceFolder.Items(i)
message.Move(destinationFolder)
end
end

Any ideas what I am doing wrong? I have tried different flavors of this, but outlook flakes out on all of them. Once it flakes out, all ole commands on the folders fail.

The error I get is :
archiver.rb:43:in `method_missing': Move (WIN32OLERuntimeError)
OLE error code:4096 in Microsoft Office Outlook
The operation failed.
HRESULT error code:0x80004005

All suggestions are welcome!

HH

David Mullet said...

@HH-

Your code worked for me, moving over 300 messages (including attachments) between folders on my Exchange server, and moving messages between my server and my local Personal Folders.

When you say "move and entire tree of messages", do you mean you are moving messages AND subfolders -- or just messages?

David

HH said...

>>>When you say "move and entire tree of messages", do you mean you are moving messages AND subfolders -- or just messages?<<<

The move only applies to the messages. So I have these folders:

\Inbox
..\foo
..\bar

Our retention policy moves these to

\Managed Folders
..\Retention Review
....\Inbox
......\foo
......\bar

For each folder under retention review, I make sure that a like folder exists back in the original tree. Then I move the messages. I keep recursing down until all messages have been moved back.

When I am done, an empty folder structure exists under Retention Review. Hope this makes sense.

I don't think any of my runs ever got to 300 before flaking out, but I should mention, I have some folders with more than 1000 messages. Smaller folders didn't seem to create as much of a problem.

HH

Anonymous said...

Thanks this was really appreciated.

Just used your code to move 6,900 messages from Outlook local sent mail to IMAP Gmail sent mail.

For anyone else looking to do it, I just added the Gmail IMAP account and then used the code above to tell Outlook to move the messages from the Outlook local folder to the Gmail IMAP connected folder in Outlook.

Add the line puts "Moved msg #{i}" to monitor progress.

Taman said...

Hi, I'm working on a project. I have to read email using Ruby program.

Could you explain how I'd go about doing that? I tried researching but it seems that there is more help using RoR.

Thanks!

Swapnil - My words in my fashion !! said...

David, I am trying to iterate over all the items in the inbox, but in a slightly different way i.e. passing the index of email as an argument to the "Items" method. In particular, I have written APIs to accomplish this:

inbox = get_inbox_folder()
count = get_email_count(inbox)
email = get_email_item(inbox,count)
...and then later
subject = get_subject(email)
sender = get_sender(email)
I am expecting that i should get the latest email in 'email'... for some reason, I always get some older email (at index (count - 5) or something.
Iterating over the inbox with the following code:
inbox.items.each{|email|
puts email.subject
}
confirms it that the email at index 'count' is indeed what I got with previous approach. Any ideas as to why this might be happening? I also followed similar approach for lotus notes and it turned out fine.

Glider said...

found a link for the microsoft ole outlook reference for the default folder type, which might be helpful to people: http://msdn.microsoft.com/en-us/library/bb208072(v=office.12).aspx