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
Monday, June 02, 2008 12:18:29 PM (Romance Standard Time, UTC+01:00) #    Comments [0]  | 

 

Just Released: Mollom for .NET v1.0!#

My friend Dries Buytaert - known all around the world for creating Drupal (the wildly popular open source content management system) and Axl (the incredibly cute kid he co-created with my even better friend Karlijn) - asked me a few months ago if I had any trouble with spam on my blog... It turned out he was building Mollom, a solution for fighting spam and automating content monitoring, and was looking for beta testers. I immediately jumped aboard and implemented a .NET client API for his service and integrated it into dasBlog, the blog engine I'm using.

Now that Mollom and its API and developer documentation have finally been released (in public beta), I've packaged my client library as well and published it on CodePlex: see the Mollom for .NET homepage.

Mollom's purpose is to dramatically reduce the effort of keeping your websites clean and the quality of their user-generated content high. Currently, Mollom is a spam-killing, one-two punch combination of a state-of-the-art spam filter and CAPTCHA server.

I have to say it's working really well for me, I don't get any spam at all anymore through my blog, and the XML-RPC API that Mollom provides is easy and straight-forward to use. And, of course, if you develop on .NET then it's even easier to talk to Mollom using my client API. As a very basic sample, this should give you an idea of how easy it is to have Mollom classify a piece of content:

MollomClient client = new MollomClient(privateKey, publicKey);
ContentCheck result = client.CheckContent(postTitle, postBody, authorName, authorMail, authorUrl, authorIPAddress);
if (result.Classification == ContentClassification.Spam)
{     // Handle spam here...
}

All information, downloads and documentation is available on the Mollom for .NET homepage on CodePlex, so rush out and let me know what you think!

Sunday, May 18, 2008 5:22:48 PM (Romance Standard Time, UTC+01:00) #    Comments [6]  | 

 

Updated: Setting up Source Server for TFS Builds#

Just a quick note to let you know that I've updated my guide on Setting up Source Server for TFS Builds, since I just found out that there is an issue with Build Definitions that contain spaces. The fix is fairly easy though:

  • In TFIndex.cmd (on the build server), remove the quotes around the %1 argument for SYMBOLS:
@call "%~dp0SSIndex.cmd" -SYSTEM=TF -SYMBOLS="%1" %*
@call "%~dp0SSIndex.cmd" -SYSTEM=TF -SYMBOLS=%1 %*
  • In the Team Build Script (in Source Control), add XML-escaped quotes around the $(BinariesRoot) argument:
<Exec Command="&quot;C:\Program Files\Debugging Tools for Windows\sdk\srcsrv\TFIndex.cmd&quot; $(BinariesRoot)"
      WorkingDirectory="$(SolutionRoot)" />
<Exec Command="&quot;C:\Program Files\Debugging Tools for Windows\sdk\srcsrv\TFIndex.cmd&quot; &quot;$(BinariesRoot)&quot;"
      WorkingDirectory="$(SolutionRoot)" />

For the full setup instructions, please refer to the original post on Setting up Source Server for TFS Builds.

Wednesday, April 30, 2008 11:20:47 AM (Romance Standard Time, UTC+01:00) #    Comments [0]  | 

 

Mortgage Loan Excel Sheet#

If anybody would be interested, I've recently put together an Excel sheet that calculates the payment table ("aflossingstabel") for a loan, i.e. for each month it shows you how much you need to pay, what the interest is, what the remaining capital is, ...

I've only modeled two options (that were relevant to me): fixed monthly payments ("vaste mensualiteit") - where you pay the same consant amount every month - and fixed capital ("vaste kapitaalaflossing") payments - where you pay off a fixed amount of capital but a variable amount of interest (making it a decreasing loan).

When I showed this to my bank, they were actually pretty impressed so I figured somebody else might benefit from this :-) And yes, this means we just bought a house, yay! But the examples in the Excel sheet and below are not ours, if you were wondering ;-)

Features:

  • Calculates payment tables for loans up to 40 years
  • Shows payment graphs up to 25 years (by default, you can enlarge this of course)
  • Calculates how much of your total payments are actually interest payments (try not to weep when looking at this)
  • Allows you to compare different loan options (amount, duration, interest rate), e.g. to compare different bank proposals

Download here: Loans.zip (108 KB).

Note that you can only open this in Excel 2007 since it uses some financial functions only available there. And although the calculations were very accurate (just a few cents deviation on the total amounts compared to the bank's proposals), it goes without saying that you use this at your own financial risk :-)

Example payment table:

LoanFixedPaymentSheet

Example yearly graph for a fixed payment (constant) loan:

LoanFixedPayment

Example monthly graph for a fixed capital (decreasing) loan:

LoanFixedCapital

Friday, March 07, 2008 1:03:40 PM (Romance Standard Time, UTC+01:00) #    Comments [2]  | 

 

My "Deep Dive Into The Guidance Automation Toolkit" presentation now online!#

Tom's team has been kind enough to put my session of last year's TechDays (then still known as the Developer & IT Pro Days) online on MSDN Chopsticks. You can find my "Deep Dive Into The Guidance Automation Toolkit" presentation at http://www.microsoft.com/belux/msdn/nl/chopsticks/default.aspx?id=10. Everything I said back then is still relevant today, so if you missed it last year you can now catch up for free :-)

And in the light of Software Factory technologies, it also makes a nice preparation for my talk on Domain-Specific Development with Visual Studio DSL Tools next week. My session is scheduled on Thursday March 13 at 10:45. I'm really looking forward to it, and I hope to see you there!

Monday, March 03, 2008 10:39:16 AM (Romance Standard Time, UTC+01:00) #    Comments [0]  | 

 

DSL Tools session at TechDays in Belgium#

The annual Belgian tech-fest for Microsoft developers, architects and IT pro's is coming to Ghent again soon, and I'm once again proud to host a session in the Developers track for TechDays!

Domain Specific Development with Visual Studio Domain Specific Language (DSL) Tools

As one of the pillars of the Software Factories initiative, Domain Specific Languages (DSLs) provide a way to describe your business domain in a language closer to the actual problem than using traditional programming code.

The Visual Studio Domain Specific Language Tools allow developers to create their own graphical designers and code generation tools – much like the ones you can find in Visual Studio today, such as the Class Designer.

In this session, you will learn how to develop your own DSLs inside Visual Studio and see an example of a real-world DSL that simplifies your life as a developer: the Configuration Section Designer.

TechDays 2008

Heroes are Assembled { in Software Factories } :-)

Hope to see you there!

Blog | General | Programming | .NET | DSLs
Monday, February 18, 2008 8:56:39 AM (Romance Standard Time, UTC+01:00) #    Comments [3]  | 

 

Just Released: BuildCop v1.0!#

It is with great pleasure that I'm finally ready to release another open source tool on CodePlex: BuildCop.

BuildCop is a tool that analyzes MSBuild project files (interactively or during e.g. a daily build) according to a customizable set of rules and generates reports - e.g. is strong naming enabled, are certain project properties set correctly, is XML documentation being generated, are assembly references correct, are naming conventions respected, ...

This has grown out of a quick-and-dirty tool to check various build settings in a large customer project (to make sure that the developers were sticking to the guidelines), and has evolved into quite a clean, flexible and customizable tool that you can now start using as well.

All information, downloads and documentation is available on the BuildCop homepage on CodePlex, so rush out and let me know what you think!

Tuesday, February 05, 2008 10:37:20 PM (Romance Standard Time, UTC+01:00) #    Comments [0]  | 

 

Just Released: Configuration Section Designer#

I just released my first domain specific language to the public! The Configuration Section Designer is a Visual Studio add-in that allows you to graphically design .NET Configuration Sections and automatically generates all the required code and a schema definition (XSD) for them.

The Configuration Section Designer In Action

For all information, downloads, source code, work item tracking and discussions visit http://www.codeplex.com/csd.

The Configuration Section Designer is built on Visual Studio 2008 with the very excellent DSL Tools. Instead of publishing it here on the blog as I normally do with my pet projects, I've decided to host it as open source software on CodePlex so that hopefully other people will find it interesting enough to contribute and make it even more powerful. This also gives me the chance to test-drive CodePlex in a real project, and so far it's been working great so I'll probably be moving more projects to it.

Anyway, if you have Configuration Sections that you're currently writing by hand, I encourage you to try this and let me know how it works for you. I'm pretty excited about it, I've only published it yesterday evening and it's already got over 40 downloads and a good feature suggestion of someone that's been porting his hand-written code to the designer! Good times!

Blog | Programming | .NET | DSLs | CSD
Sunday, December 30, 2007 5:21:30 PM (Romance Standard Time, UTC+01:00) #    Comments [1]  | 

 

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: 344
This Year: 7
This Month: 0
This Week: 0
Comments: 522
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