Jelle Druyts .NET Consultant
Just another ignorant weirdo from Antwerp, Belgium trying to make sense out of it all
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".
SaveAsFile
COMException
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).
IStorage
OleLoad
IDataObject
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.
OpenClipboard
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; }
Remember Me