Introduction
In this article you will learn how to implement a C++ library that will help you to use Extended MAPI functions that aren't available through the Outlook Object Model. In the past, you had to buy an additional library or deal with COM-technology and create this native DLL by yourself. Another way was to reinvent the wheel and define all interfaces again on the .NET side. This would allow you to use these interfaces directly from C# or VB.NET by marshalling the .NET structures to unmanaged functions and structures. Here comes a handy C++ library where you can easily mix managed and native code in one environment.
Benefits
- All Extended MAPI interfaces and functions are already defined in the Windows SDK
- We don't have to expose a COM interface
- No external components must be deployed or registered on the target systems
- .NET classes and interfaces exposed to the outside
- No wrapper classes are needed for external COM components
- Fully included in your solution, with debug support
Drawbacks
- Some knowledge of C++ is required
- Some knowledge of Extended MAPI interfaces is required
Background
The Microsoft Outlook Object Model (OOM) is powerful and provides access to many features that use and manipulate the data stored in Outlook and the Exchange Server. However, every serious Outlook developer comes to a point where he or she needs a special function, property or field that is not available through the OOM or is blocked due to security restrictions. In the past, you had to use an external COM library like the highly recommended:
While these libraries are very powerful and flexible because they can be used from every programming language, they are still additional libraries that have to be registered and deployed with your application. This can sometimes be problematic. When using Extended MAPI from .NET, there exists another library called:
Set up the solution
Before you can start hacking into Extended MAPI, you have to install the minimum requirements on your development machine.
Prerequisites
Create a solution
To demonstrate how it works behind the scenes, you start with a simple Windows Forms application that creates a new email and adds an additional SMTP header to it. In this sample, you will find a C# and a VB.NET solution respectively. So, open Visual Studio, choose the language of your choice and create an application that looks like this:
Let's say you are an Outlook programmer and you want to use the Outlook Object Model. You naturally have to go to the project references and add the following COM libraries as reference to your project:
- Microsoft Office 1X.0 Object Library
- Microsoft Outlook 1X.0 Object Library
Note: depends on the installed Office Version.
When you have finished with adding the references to your project, you can import the namespaces to the code files like this:
Imports Office = Microsoft.Office.Core
Imports Outlook = Microsoft.Office.Interop.Outlook
using Office = Microsoft.Office.Core;
using Outlook = Microsoft.Office.Interop.Outlook;
The Outlook thing
The goal is to create a new email and attach an SMTP header to it. So, first we look at how to create a new email with the Outlook Object Model:
Private Sub btnSend_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnSend.Click
Dim outlookApplication As Outlook.Application = New Outlook.Application()
Dim olNamespace As Outlook.NameSpace = _
okApplication.GetNamespace("MAPI")
olNamespace.Logon(, , True, True)
Dim newMail As Outlook.MailItem = _
lookApplication.CreateItem(Outlook.OlItemType.olMailItem)
newMail.To = tbxRecipientAddress.Text
newMail.Subject = tbxSubject.Text
newMail.Body = tbxMessage.Text
newMail.Send()
newMail = Nothing
olNamespace.Logoff()
outlookApplication = Nothing
GC.Collect()
GC.WaitForPendingFinalizers()
End Sub
C++
private void btnSend_Click(object sender, EventArgs e)
{
object missing = System.Reflection.Missing.Value;
// get the Outlook Application Object
Outlook.Application outlookApplication = new Outlook.Application();
// get the namespace object
Outlook.NameSpace nameSpace = outlookApplication.GetNamespace("MAPI");
// Logon to Session, here we use an already opened Outlook
nameSpace.Logon(missing, missing, true, true);
// create a new email and fill it with the info provided in the Form
Outlook.MailItem newMail = _
lookApplication.CreateItem(Outlook.OlItemType.olMailItem);
newMail.To = tbxRecipientAddress.Text;
newMail.Subject = tbxSubject.Text;
newMail.Body = tbxMessage.Text;
newMail.Send();
newMail = null;
// logoff from namespace (session)
olNamespace.Logoff();
//release reference to Outlook object
outlookApplication = Nothing;
// Let the Garbagecollector do his work
GC.Collect();
GC.WaitForPendingFinalizers();
}
This small snippet should simply send out an Email to the recipient that you provided in the To: field.
The mixed C++ library
Now you must add a new project to the solution. Choose File|Add|New Project and select C++ CLR Class Library.
I named the library MAPIConcubine
because it gives me what the Outlook Object Model won't. However, feel free to give it the name of your choice. After you've added the C++ library to the solution, open the library project settings and add mapi32.lib as the input file to the linker.
The next step is to add the MAPI C++ header files to your project. Open the stdafx.h file and add the following headers to it after the #pragma once
statement.
#pragma once
#include <MAPIX.h>
#include <MapiUtil.h>
#include <MAPIGuid.h>
#include <MAPITags.h>
Now compile the project and see if it gets compiled successfully. If yes, everything goes well and you can continue. If not, you are probably missing a prerequisite like the Windows SDK containing the C++ header files defined in StdAfx.h.
Adding functionality
Now you should have a compiling .NET C++ class library with the linked Extended MAPI library. Go on and start exposing functionality to your .NET projects. Remembering the initial goal, you want to add an SMTP header to an outgoing Outlook email. Now you can continue and provide an interface to the external world that exposes the desired functionality.
The header file
C++ classes usually have a header file and a code file. Here in your library, the header file looks like you can see below. You have defined a .NET class, Fabric
, with a public
visible method called AddMessageHeader
. The method takes three parameters: the first is MPIObject
, which you can get from the Outlook Items as a property. This property provides access to the underlying MAPI IUnknown
interface.
#pragma once
using namespace System;
using namespace System::Runtime::InteropServices;
namespace MAPIConcubine
{
public ref class Fabric
{
private:
~Fabric();
String^ GetErrorText(DWORD dw);
public:
Fabric();
void AddMessageHeader(Object^ mapiObject, String^ headerName,
String^ headerValue);
};
}
You expose only .NET compliant methods to the outside. This makes it easy to use this library from any .NET project. Now you have defined the interface and can go on to implement some functionality behind it. Let's have a look at the implementation that can be found in the MAPIConcubine.cpp file.
The idea is to get the IUnknown
interface from the Outlook Object, followed by the IMessage
and, last but not least, the IMAPIProp
interface. Here, the C++ library plays a master role. You can now access the MAPI header files where all interfaces are defined and then compile against the external libraries like mapi32.lib or any other unmanaged native library. Just walk through the code, where you can see a mix between traditionally C++ pointers and unmanaged code (new and *) and .NET managed code (gcnew
and ^
). The trickiest thing that you can see here is how you pass the .NET string variables into an unmanaged LPWSTR
, a pointer to a Unicode null
terminated character array. .NET already gives you the correct methods for it in the System.Runtime.Interop
namespace.
void Fabric::AddMessageHeader(Object^ mapiObject, String^ headerName,
String^ headerValue)
{
IUnknown* pUnknown = 0;
IMessage* pMessage = 0;
IMAPIProp* pMAPIProp = 0;
MAPINAMEID* pNamedProp = 0;
SPropTagArray* lpTags = 0;
IntPtr pHeaderName = Marshal::StringToHGlobalUni (headerName);
IntPtr pHeaderValue = Marshal::StringToHGlobalUni (headerValue);
if (mapiObject == nullptr)
throw gcnew System::ArgumentNullException
("mapiObject","The MAPIObject must not be null!");
try
{
pUnknown = (IUnknown*)Marshal::GetIUnknownForObject
(mapiObject).ToPointer ();
if ( pUnknown->QueryInterface
(IID_IMessage, (void**)&pMessage) != S_OK)
throw gcnew Exception
("QueryInterface failed on IUnknown for IID_Message");
if ( pMessage->QueryInterface
(IID_IMAPIProp, (void**)&pMAPIProp) != S_OK)
throw gcnew Exception
("QueryInterface failed on IMessage for IID_IMAPIProp");
if (pMAPIProp == 0)
throw gcnew Exception
("Unknown Error on receiving the Pointer to MAPI Properties.");
GUID magicId = { 0x00020386, 0x0000, 0x0000,
{ 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46 } };
if (MAPIAllocateBuffer(sizeof(MAPINAMEID),
(LPVOID*)&pNamedProp) != S_OK)
throw gcnew Exception("Could not allocate memory buffer.");
pNamedProp->lpguid = (LPGUID)&magicId;
pNamedProp->ulKind = MNID_STRING;
pNamedProp->Kind.lpwstrName = (LPWSTR) pHeaderName.ToPointer();
if(pMAPIProp->GetIDsFromNames
(1, &pNamedProp, MAPI_CREATE, &lpTags ) != S_OK)
throw gcnew Exception(String::Format
("Error retrieving GetIDsFromNames: {0}.",headerName) );
SPropValue value;
value.ulPropTag = PROP_TAG(PT_UNICODE, PROP_ID
(lpTags->aulPropTag[0]));
value.Value.LPSZ = (LPWSTR) pHeaderValue.ToPointer();
if( HrSetOneProp(pMAPIProp, &value) != S_OK)
throw gcnew Exception(String::Format
("Error setting Header: {0}.",headerName) );
}
catch (Exception^ ex)
{
DWORD dw = GetLastError();
throw gcnew Exception(GetErrorText(dw),ex);
}
finally
{
if (pNamedProp != 0) MAPIFreeBuffer(pNamedProp);
if (lpTags != 0) MAPIFreeBuffer(lpTags);
Marshal::FreeHGlobal (pHeaderName);
Marshal::FreeHGlobal (pHeaderValue);
if (pMAPIProp!=0) pMAPIProp->Release();
if (pMessage!=0) pMessage->Release();
if (pUnknown!=0) pUnknown->Release();
}
}
The interesting thing is how you implement the try
/catch
/finally
block. You have to take care that you don't get a memory leak, so you put your cleanup code into the finally
section. You can also throw
exceptions friendlier to the calling .NET classes. Just compile and you will see that you are missing something. That's the way you must access the MAPI IDs defined in the MAPI header files. There's just one more action to do now: go to your Stdafx.h file and include the following statements. You have to include these statements for every MAPI ID you use.
#pragma once
#define INITGUID
#define USES_IID_IMAPIProp
#define USES_IID_IMessage
#include <MAPIX.h>
#include <MapiUtil.h>
#include <MAPIGuid.h>
#include <MAPITags.h>
Initialization and cleanup
Ahh! Wait; you missed something. Whenever you want anything from extended MAPI, you have to initialize it. So, you must add a construction and a cleanup routine to the library. This is how it looks:
Fabric::Fabric ()
{
MAPIInitialize(0);
}
Fabric::~Fabric ()
{
MAPIUninitialize();
}
Also, for easier error handling and debugging, you should add a method that returns a human readable error message to the caller. This method could look like the following code:
String^ Fabric::GetErrorText(DWORD dw)
{
LPVOID lpMsgBuf;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM,
NULL,
dw,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR) &lpMsgBuf,
0, NULL );
String^ result = Marshal::PtrToStringAuto (IntPtr::IntPtr (lpMsgBuf));
LocalFree(lpMsgBuf);
return result;
}
The usage
Let's go over to the .NET clients that should use this library. Open the Windows Forms application -- or even VSTO or Extensibility AddIn -- that you created earlier and add a new reference to it. Go to the Projects tab and select the existing project, MAPIConcubine
.
Now you can change the code of your Windows Forms application and add the new functionality to it. That simply looks like this:
Dim newMail As Outlook.MailItem = _
outlookApplication.CreateItem(Outlook.OlItemType.olMailItem)
Dim fabric As MAPIConcubine.Fabric = New MAPIConcubine.Fabric()
fabric.AddMessageHeader(newMail.MAPIOBJECT, tbxHeaderName.Text, _
tbxHeaderValue.Text)
fabric = Nothing
newMail.To = tbxRecipientAddress.Text
newMail.Subject = tbxSubject.Text
newMail.Body = tbxMessage.Text
newMail.Send()
C++
// create a new email and fill it with the info provided in the Form
Outlook.MailItem newMail = outlookApplication.CreateItem
(Outlook.OlItemType.olMailItem) as Outlook.MailItem;
// here we use our C++ library
MAPIConcubine.Fabric fabric = new MAPIConcubine.Fabric();
// we pass the MAPIObject property to our library and the parameters
// note that in VB.Net you can
// but be sure it is there
fabric.AddMessageHeader(newMail.MAPIOBJECT, tbxHeaderName.Text,
tbxHeaderValue.Text);
// that
fabric = null;
newMail.To = tbxRecipientAddress.Text;
newMail.Subject = tbxSubject.Text;
newMail.Body = tbxMessage.Text;
newMail.Send();
That's all; the rest is a piece of cake. You can check the Internet headers of this email at the recipient mailbox and you will see your own headers there. Within Outlook, you can view it by opening the email and showing the Options menu. Happy coding to all.
Reading the header
In this chapter, you will learn how to retrieve the saved header and how to retrieve all transport message headers. The theory is simple: you use the GUID to access the internet headers and the GetIDSFromNames
method. This allows you to retrieve the correct propTagId
that you will use in the HrGetOneProp
method. If you are in an Exchange environment, you can read the custom email header with the following code. Note that for easier reading, the catch/finally block is excluded. As above, refer to the source files for more detail.
String^ Fabric::ReadMessageHeader(Object^ mapiObject, String^ headerName)
{
IUnknown* pUnknown = 0;
IMessage* pMessage = 0;
IMAPIProp* pMAPIProp = 0;
MAPINAMEID* pNamedProp = 0;
SPropTagArray* lpTags = 0;
LPSPropValue lpSPropValue = 0;
IntPtr pHeaderName = Marshal::StringToHGlobalUni (headerName);
if (mapiObject == nullptr) throw gcnew System::ArgumentNullException (
"mapiObject","The MAPIObject must not be null!");
try
{
pUnknown =
(IUnknown*)Marshal::GetIUnknownForObject(mapiObject).ToPointer ();
if ( pUnknown->QueryInterface (IID_IMessage,
(void**)&pMessage) != S_OK)
throw gcnew Exception(
"QueryInterface failed on IUnknown for IID_Message");
if ( pMessage->QueryInterface (IID_IMAPIProp,
(void**)&pMAPIProp) != S_OK)
throw gcnew Exception(
"QueryInterface failed on IMessage for IID_IMAPIProp");
if (pMAPIProp == 0) throw gcnew Exception(
"Unknown Error on receiving the Pointer to MAPI Properties.");
GUID magicId =
{
0x00020386, 0x0000, 0x0000,
{
0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46
}
};
if (MAPIAllocateBuffer(sizeof(MAPINAMEID),
(LPVOID*)&pNamedProp) != S_OK)
throw gcnew Exception("Could not allocate memory buffer.");
pNamedProp->lpguid = (LPGUID)&magicId;
pNamedProp->ulKind = MNID_STRING;
pNamedProp->Kind.lpwstrName = (LPWSTR) pHeaderName.ToPointer();
if(pMAPIProp->GetIDsFromNames(1, &pNamedProp,
STGM_READ, &lpTags ) != S_OK)
throw gcnew Exception(String::Format (
"Error retrieving GetIDsFromNames: {0}.",headerName) );
ULONG propTag = PROP_TAG(PT_UNICODE, PROP_ID(lpTags->aulPropTag[0]));
if (HrGetOneProp(pMAPIProp,propTag,&lpSPropValue) != S_OK)
throw gcnew Exception("HrGetOneProp failed for named property !");
return gcnew String( lpSPropValue->Value.lpszW );
}
catch (Exception^ ex)
{
...
}
finally
{
...
}
}
The usage is equal to the other method. Create a fabric object and pass mailitem.MAPIOBJECT
to the method along with the name of the custom header.
Reading the current Outlook profile name
In this lesson, you will learn how to master a common problem to Outlook developers. You will learn how to read the profile name of the current MAPI session. In the Outlook Object Model, there is no way to determine the name of the profile that the user has started. With the following snippet, you can solve this issue. The idea is to get the session object and use the OpenProfileSection
method to retrieve information about the current Outlook profile. Note that for easier reading, the catch/finally block is excluded. As above, refer to the source files for more detail. Here is the code:
String^ Fabric::GetProfileName(Object^ mapiObject)
{
String^ result = nullptr;
IUnknown* pUnknown = 0;
LPMAPISESSION lpMAPISession = 0;
LPPROFSECT lpProfileSection = 0;
LPSPropValue lpSPropValue = 0;
if (mapiObject == nullptr)
throw gcnew System::ArgumentNullException ("mapiObject",
"The MAPIObject must not be null!");
try
{
pUnknown =
(IUnknown*)Marshal::GetIUnknownForObject(mapiObject).ToPointer ();
if ( pUnknown->QueryInterface (IID_IMAPISession,
(void**)&lpMAPISession) != S_OK)
throw gcnew Exception(
"QueryInterface failed on IUnknown for IID_IMAPISession");
if( lpMAPISession->OpenProfileSection(
(LPMAPIUID)GLOBAL_PROFILE_SECTION_MAPIUID,
NULL,STGM_READ, &lpProfileSection) != S_OK)
throw gcnew Exception("OpenProfileSection method failed!");
if (HrGetOneProp(lpProfileSection,
PR_PROFILE_NAME_W,&lpSPropValue) != S_OK)
throw gcnew Exception(
"HrGetOneProp failed for property PR_PROFILE_NAME_W !");
return gcnew String( lpSPropValue->Value.lpszW );
}
catch (Exception^ ex)
{
...
}
finally
{
...
}
}
Now it's easy to get information related to the profile the user is currently logged onto. Just for reference, you implement a method to enumerate all available MAPI profiles for the current user. This information is stored in the registry and so you simply open the registry key 'HKCU\Software\Microsoft\Windows NT\CurrentVersion\Windows Messaging Subsystem\Profiles' and enumerate all sub-keys. There is one sub-key for each profile.
array<string^,1 />^ Fabric::EnumProfileNames(){
String^ registryPath =
"Software\\Microsoft\\Windows NT\\CurrentVersion" +
"\\Windows Messaging Subsystem\\Profiles";
RegistryKey^ key = nullptr;
try
{
key = Registry::CurrentUser->OpenSubKey ( registryPath );
return key->GetSubKeyNames ();
}
catch(System::Exception^ ex)
{
throw ex;
}
finally
{
key->Close();
}
}
The test application provided in the solution demonstrates how to use this method. Just pass along the namespace.MAPIOBJECT
property. Here is the C#:
object missing = System.Reflection.Missing.Value;
Outlook.Application outlookApplication = new Outlook.Application();
Outlook.NameSpace olNamespace = outlookApplication.GetNamespace("MAPI");
olNamespace.Logon(comboBox1.Text , missing, true, true);
MAPIConcubine.Fabric fabric = new MAPIConcubine.Fabric();
try
{
string currentProfileName = fabric.GetProfileName(olNamespace.MAPIOBJECT);
MessageBox.Show(this, String.Format(
"The current profilename was: {0}", currentProfileName));
}
catch (System.Exception ex)
{
MessageBox.Show(this,ex.Message);
}
finally
{
fabric = null;
olNamespace.Logoff();
olNamespace = null;
outlookApplication = null;
GC.Collect();
GC.WaitForPendingFinalizers();
}
Set appointment label color
In this section, you will learn how to set and get the label color of an Outlook Appointment Item. With Outlook 2002 and later, you have the option of colorizing your Outlook appointments. See this for more information. In the past, you had to use CDO or one of the external libraries mentioned above. Here is the source code that demonstrates how to use Extended MAPI to accomplish this goal from .NET. In the header file, you define the available colors for the appointment labels:
enum class AppointmentLabelColor
{
None = 0, Important = 1, Business = 2, Personal = 3, Vacation = 4, Deadline = 5, Travel_Required = 6, Needs_Preparation = 7, Birthday = 8, Anniversary = 9, Phone_Call = 10 } ;
When you are done with the header file, go on and implement the SetAppointMentLabelColor
method as listed below. Note that for easier reading the catch/finally block is excluded. As above, refer to the source files for more detail.
void Fabric::SetAppointmentLabelColor(Object^ mapiObject,
AppointmentLabelColor color)
{
IUnknown* pUnknown = 0;
IMessage* pMessage = 0;
IMAPIProp* pMAPIProp = 0;
MAPINAMEID* pNamedProp = 0;
SPropTagArray* lpTags = 0;
if (mapiObject == nullptr) throw gcnew System::ArgumentNullException (
"mapiObject","The MAPIObject must not be null!");
try
{
pUnknown =
(IUnknown*)Marshal::GetIUnknownForObject(mapiObject).ToPointer ();
if ( pUnknown->QueryInterface (IID_IMessage,
(void**)&pMessage) != S_OK)
throw gcnew Exception(
"QueryInterface failed on IUnknown for IID_Message");
IMessage Interface, everything else is sensless.
if ( pMessage->QueryInterface (IID_IMAPIProp,
(void**)&pMAPIProp) != S_OK)
throw gcnew Exception(
"QueryInterface failed on IMessage for IID_IMAPIProp");
if (pMAPIProp == 0)
throw gcnew Exception(
"Unknown Error on receiving the Pointer to MAPI Properties.");
GUID magicId =
{
0x00062002, 0x0000, 0x0000,
{
0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46
}
};
LONG propertyName = 0x8214;
if (MAPIAllocateBuffer(sizeof(MAPINAMEID),
(LPVOID*)&pNamedProp) != S_OK)
throw gcnew Exception("Could not allocate memory buffer.");
pNamedProp->lpguid = (LPGUID)&magicId;
pNamedProp->ulKind = MNID_ID;
pNamedProp->Kind.lID = propertyName;
if(pMAPIProp->GetIDsFromNames(1, &pNamedProp,
MAPI_CREATE, &lpTags ) != S_OK)
throw gcnew Exception(String::Format (
"Error retrieving GetIDsFromNames: {0}.",propertyName) );
SPropValue value;
value.ulPropTag = PROP_TAG(PT_LONG, PROP_ID(lpTags->aulPropTag[0]));
value.Value.l = (LONG)color;
if( HrSetOneProp(pMAPIProp, &value) != S_OK)
throw gcnew Exception(String::Format (
"Error setting AppointmentLabelColor: {0}.",color) );
pMessage->SaveChanges (KEEP_OPEN_READWRITE);
}
catch (Exception^ ex)
{
...
}
finally
{
...
}
}
The method to retrieve the appointment label color is implemented as you can see here:
GUID magicId =
{
0x00062002, 0x0000, 0x0000,
{
0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46
}
};
LONG propertyName = 0x8214;
if (MAPIAllocateBuffer(sizeof(MAPINAMEID), (LPVOID*)&pNamedProp) != S_OK)
throw gcnew Exception("Could not allocate memory buffer.");
pNamedProp->lpguid = (LPGUID)&magicId;
pNamedProp->ulKind = MNID_ID;
pNamedProp->Kind.lID = propertyName;
if(pMAPIProp->GetIDsFromNames(1, &pNamedProp, STGM_READ, &lpTags ) != S_OK)
throw gcnew Exception(String::Format (
"Error retrieving GetIDsFromNames: {0}.",propertyName) );
ULONG propTag = PROP_TAG(PT_LONG, PROP_ID(lpTags->aulPropTag[0]));
if (HrGetOneProp(pMAPIProp,propTag,&lpSPropValue) != S_OK)
throw gcnew Exception("HrGetOneProp failed for named property !");
if (( lpSPropValue->Value.l > 0) && ( lpSPropValue->Value.l < 11))
return (Fabric::AppointmentLabelColor ) lpSPropValue->Value.l;
else
return Fabric::AppointmentLabelColor::Default;
Notes
When you deploy this DLL along with your application:
- The DLL should be compiled in release mode
- You should redistribute the CLR 8.0 runtime libraries with it (prerequisite in MSI Setup package)
- VSTO: for each DLL that is used from your add-in you have to set the security policy (custom action in MSI Setup package)
Helmut Obertanner - June 2007 X4U electronix
History
- V.1.0 - Initial version (11 June, 2007)
- V.1.1 - Added support for reading internet headers
- V.1.2 - Enum available profile names and get current outlook profile name
- V.1.3 - Get/set appointment label colors (28 June, 2007)