Extracting OLE embedded images from emails in Outlook#

While it seemed a simple requirement, saving all attachments from emails in Outlook to disk proved to be challenging - to say the least. Using the Outlook Object Model, it's quite easy to enumerate all emails in a folder, look at their attachments and call the SaveAsFile method on them. However, for OLE-type attachments (typically images), this throws a COMException saying: "Outlook cannot do this action on this type of attachment". While looking for alternatives or workarounds, I found nothing but confirmation that this is indeed not an easy task - even from Dmitry Streblechenko, Outlook MVP and creator of the excellent and very affordable Outlook Redemption library: "If you mean embedded graphics objects in the RTF messages, there is not much you can do [...] You can look at the storage itself to figure that out, but I've never tried that".

Ultimately, after lots of trial and error, I did manage to find a fairly easy way to save these OLE embedded images by (mis)using the clipboard. Basically, I retrieve the attachment’s IStorage OLE interface (available through Redemption) and call OleLoad on it to have OLE load the contents and retrieve an IDataObject. The magic trick is to place that IDataObject on the clipboard and retrieve the actual image from the clipboard (so that the clipboard itself handles the nasty OLE details).

Great success! At least for a moment. That already worked for Device Independent Bitmaps, but Outlook also uses Enhanced Metafiles (wmf) and apparently there is a problem with the .NET Framework when it comes to handling Enhanced Metafiles from the clipboard. So I needed some additional COM interop to handle these Enhanced Metafiles as well, which made the code slightly more difficult to read but fortunately still effective. The trick here is to make sure you have a valid handle to pass to OpenClipboard. Because I didn't have access to a form or other type of existing control, I just created a dummy button and used its handle.

Finally, be aware that to access OLE functionality, you need a Single Threaded Apartment (STA) model. Of course I was in an MTA context, so from there I launched a new thread which I put to STA - after that, everything was golden.

Below is the full code using Redemption Data Objects (RDO), hopefully this will save other people a few hours in trying to achieve the same thing...

public static class Program
{
  public static void Main()
  {
    // Calling code should always ensure to be in STA.
    Thread staThread = new Thread(new ThreadStart(SaveOutlookAttachments));
    staThread.SetApartmentState(ApartmentState.STA);
    staThread.Start();
  }

  private static void SaveOutlookAttachments()
  {
    RDOSession session = new RDOSessionClass();
    RDOFolder inbox = session.GetDefaultFolder(rdoDefaultFolders.olFolderInbox);
    string attachmentRootPath = AppDomain.CurrentDomain.BaseDirectory;
    foreach (RDOMail mail in inbox.Items)
    {
      foreach (RDOAttachment attachment in mail.Attachments)
      {
        if (attachment.Type == rdoAttachmentType.olOLE)
        {
          // We don't have a filename for this type of attachment, create a unique one.
          string filename = Guid.NewGuid().ToString() + ".png";
          string attachmentPath = Path.Combine(attachmentRootPath, filename);
          // We assume here that only images will be stored as OLE attachments.
          // We save them as PNG to keep the file size small.
          SaveOleImageAttachment(attachment, attachmentPath, ImageFormat.Png);
        }
        else
        {
          string attachmentPath = Path.Combine(attachmentRootPath, attachment.FileName);
          attachment.SaveAsFile(attachmentPath);
        }
      }
    }
  }

  private static void SaveOleImageAttachment(RDOAttachment attachment, string filePath, ImageFormat format)
  {
    // Use the OLE storage interface to load the OLE document into a DataObject.
    IStorage oleStorage = (IStorage)attachment.OleStorage;
    object oleDataObject;
    OleLoad(oleStorage, ref IDataObjectGuid, null, out oleDataObject);

    // Copy the OLE DataObject to the clipboard so it can handle the internals.
    Clipboard.SetDataObject(oleDataObject, false);

    // Try to retrieve an image back from the clipboard.
    if (Clipboard.ContainsData(DataFormats.EnhancedMetafile))
    {
      // Enhanced Metafiles cannot be handled natively from .NET.
      // Use the Clipboard directly to retrieve the data.

      // We need a valid handle, otherwise this won't work.
      Button dummy = new Button();
      if (OpenClipboard(dummy.Handle))
      {
        try
        {
          if (IsClipboardFormatAvailable(CF_ENHMETAFILE))
          {
            IntPtr metafileData = GetClipboardData(CF_ENHMETAFILE);
            if (metafileData != IntPtr.Zero)
            {
              using (Metafile metafile = new Metafile(metafileData, true))
              {
                metafile.Save(filePath, format);
              }
            }
          }
        }
        finally
        {
          EmptyClipboard();
          CloseClipboard();
        }
      }
    }
    else if (Clipboard.ContainsImage())
    {
      using (Image image = Clipboard.GetImage())
      {
        if (image != null)
        {
          image.Save(filePath, format);
        }
      }
    }
  }

  [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
  private static extern bool OpenClipboard(IntPtr hWndNewOwner);
  [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
  private static extern bool CloseClipboard();
  [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
  private static extern IntPtr GetClipboardData(uint format);
  [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
  private static extern bool IsClipboardFormatAvailable(uint format);
  [DllImport("user32.dll")]
  private static extern bool EmptyClipboard();
  [DllImport("ole32.dll")]
  private static extern int OleLoad(IStorage pStg, [In] ref Guid riid, IOleClientSite pClientSite, [MarshalAs(UnmanagedType.IUnknown)] out object ppvObj);

  private static Guid IDataObjectGuid = new Guid("0000010E00000000C000000000000046");
  private const uint CF_ENHMETAFILE = 14;
}
Blog | General | Programming | .NET | Samples
Name
E-mail
Home page

Comment (HTML not allowed)  

Enter the code shown (prevents robots):

Live Comment Preview
All content © 2008, Jelle Druyts
On this page

Recent Photos
www.flickr.com
This is a Flickr badge showing public photos from Jelle Druyts. Make your own badge here.
Advertising
Top Picks
Statistics
Total Posts: 345
This Year: 8
This Month: 0
This Week: 0
Comments: 523
Archives
Sitemap
Disclaimer
This is my personal website, not my boss', not my mother's, and certainly not the pope's. My personal opinions may be irrelevant, inaccurate, boring or even plain wrong, I'm sorry if that makes you feel uncomfortable. But then again, you don't have to read them, I just hope you'll find something interesting here now and then. I'll certainly do my best. But if you don't like it, go read the pope's blog. I'm sure it's fascinating.

Powered by:
newtelligence dasBlog 2.0.7226.0

Sign In