Thursday, March 22, 2007

CDO and MAPI vs. C#

In a wild side-project I have started investigating how to extract appointment information from an Exchange server. To be honest this work is more like archeology (often resulting in dead links to some long forgotten Microsoft or MSDN page) than any other form of... -logy.

After having looked at the MAPI implementation on the Windows side (mapi32.dll) I found myself reeeeally reluctant to do Interop against this .dll. Any documentation and/or samples are at best provided for C++ (not really a problem, just boring after having idled around in managed programming the last few years) and MCF (now this is a problem).

At first I glanced at the Mapi33 wrapper around Mapi32.dll, the price tag however quickly persuaded me to start looking in another direction. $1000 might seem like a lot of money for a wrapper around one single dll, but I'm sure the guy deserves it, personally I'm not that into this side-project yet.

Turns out I don't need that. The CDO (Collaboration Data Objects) provides a wrapper around the MAPI interface and exposes a lot of the objects. A simple interop to CDO.dll I can live with.

Only problem now is that we are back in Interop/COM-hell. At first I used the as operator in C# to cast stuff to the right type (since all types are object when you interop). What I did forget was that a new instance is created every time. This did take some while to set straight.

So, what I did at first was:

// Create session and logon
MAPI.Session session = new MAPI.Session();
session.Logon(name, password, true, true, this.Handle.ToInt64(), false,
String.Format("{0}\n{1}", server, mailbox));

// Get the folder
MAPI.Folder inboxFolder = session.Inbox as MAPI.Folder;

// Enumerate messages
MAPI.Message message = (MAPI.Message)(inboxFolder.Messages as MAPI.Messages).GetFirst(null);
while (message != null)
{
// do stuff with the message
message = (MAPI.Message)(inboxFolder.Messages as MAPI.Messages).GetNext();
}

Do yo see whats wrong here? Cause I didn't at first. Hint, as I said above, a new object is created every time you call get on a property. So, the MAPI.Messages object I get in the GetFirst-line, is not the same as I get in the GetNext-line. In this case it makes no difference, but when we start playing around with the Messagefilter we have a problem. The GetFirst-call creates a forward only pointer in the messages object that is then used by the GetNext-call. By calling the inboxFolder.Messages for every step, we get an infinite loop here (since every call to messages creates a new instance of and that new instance has a pointer that points to the first message in the list)....

The code above should be:

// Create session and logon
MAPI.Session session = new MAPI.Session();
session.Logon(name, password, true, true, this.Handle.ToInt64(), false,
String.Format("{0}\n{1}", server, mailbox));

// Get the folder
MAPI.Folder inboxFolder = session.Inbox as MAPI.Folder;

// Enumerate messages
MAPI.Messages messages = (MAPI.Messages)inboxFolder.Messages;
message = (MAPI.Message)messages.GetFirst(null);
while (message != null)
{
// do stuff with the message
message = (MAPI.Message)messages.GetNext();
}

A subtle but important difference that is.

So, the moral of the story is:

  • You get lazy after having played around in Managed space for too long.
  • I still don't like COM.

5 comments:

Anonymous said...

I cannot even get the first line to work. I included a reference to CDO.dll in my project, and guessed( you did not include this in your code ) the "using" statement should be "using MAPI" .

However, this line

MAPI.Session session = new MAPI.Session();


Still causes the error:
Unhandled Exception: System.Runtime.InteropServices.COMException (0x80010106): Retrieving the COM class factory for component with CLSID {3F
A7DEB3-6438-101B-ACC1-00AA00423326} failed due to the following error: 80010106.
at MailboxMonitor.Program.Main(String[] args) in C:\Documents and Settings\John.Gooch\My Documents\Visual Studio 2005\Projects\MailboxMon
itor\MailboxMonitor\Program.cs:line 26

Any ideas?

snands said...

Hi,

I am using the same code and everything was fine till I used it as a console app. But the moment I run the code as a windows service, I noticed that the data from the body is getting truncated.

Any resolution.
Also, is there a way to mark a particular message as read??

Thanks,
snands

Unknown said...

Hello;

You call either MAPI or CDO from .NET you are setting up yourself to failure. See,

Unknown said...

Hello;

If you use CDO 1.21 or MAPI under .NET, then you are setting yourself up for a bad story in the future. .NET garbage collection will stomp on MAPI. MAPI has its own way of handling memory (its "special"). CDO 1.21 and MAPI are not supported by Microsoft running under a managed thread (ie .NET) and there are good reasons why. While you can wrap CDO 1.21 and MAPI, you will run into problems in the future. If you need to use CDO 1.21 or MAPI, consider using unmaged code. If your application is in .NET, then you put the CDO 1.21/MAPI code into a component and call it out of process (put it in COM+, etc) - this should work and be a better solution and is what MS suggests as a work-around. If your application is a desktop application, you should consider Outlook Object Model (OOM).

Unknown said...

Here is a link on mapi/CDO 1.21 under .net...
http://support.microsoft.com/kb/813349