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:

  1. 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?

    ReplyDelete
  2. 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.

    ReplyDelete
  3. @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

    ReplyDelete
  4. 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?

    ReplyDelete
  5. @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

    ReplyDelete
  6. 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

    ReplyDelete
  7. 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

    ReplyDelete
  8. 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

    ReplyDelete
  9. 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

    ReplyDelete
  10. @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

    ReplyDelete
  11. >>>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

    ReplyDelete
  12. 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.

    ReplyDelete
  13. 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!

    ReplyDelete
  14. 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.

    ReplyDelete
  15. 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

    ReplyDelete