Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / productivity / Office

Bridging the gap between .NET and Extended MAPI

4.91/5 (19 votes)
28 Jun 2007CPOL10 min read 1   2.3K  
An article about how to access Extended MAPI properties, fields and methods from your .NET applications.

Screenshot - BridgingTheGap_Result.png

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:

Screenshot - BridgingTheGap_Application.png

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.

Screenshot - BridgingTheGap_Reference.png

When you have finished with adding the references to your project, you can import the namespaces to the code files like this:

C#
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:

VB
Private Sub btnSend_Click(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles btnSend.Click

    ' get the Outlook Application Object
    Dim outlookApplication As Outlook.Application = New Outlook.Application()

    ' get the namespace object
    Dim olNamespace As Outlook.NameSpace = _
                okApplication.GetNamespace("MAPI")

    ' logon to Session, here we use an already opened Outlook
    olNamespace.Logon(, , True, True)

    ' create a new email and fill it with the info provided in the Form
    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

    ' logoff from namespace (session)
    olNamespace.Logoff()

    'release reference to Outlook object
    outlookApplication = Nothing

    ' Let the Garbagecollector do his work
    GC.Collect()
    GC.WaitForPendingFinalizers()

End Sub

C++

VB
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.

Screenshot - BridgingTheGap_AddConcubine.png

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.

Screenshot - BridgingTheGap_LinkedLibraries.png

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.

C++
#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.

C++
#pragma once

using namespace System;
using namespace System::Runtime::InteropServices;

namespace MAPIConcubine 
{

    ///
    /// The Fabric Class defines Methods and Functions to access 
    /// Extended MAPI Functions from .NET
    ///
    public ref class Fabric
    {
        // private methods and variables
        private:

        // destructor
        ~Fabric();

        ///
        /// Used to retrieve a human readable Errormessage from a 
        /// MAPI Errorcode.
        ///
        /// [in]dw - the error number returned from MAPI
        /// [returns] Returns the Errormessage to the given Errornumber.
        String^ GetErrorText(DWORD dw);


        // All public visible methods
        public:

        // construction code
        Fabric();

        ///
        /// Adds an InternetHeader to an outgoing Outlook- Email Message
        ///
        /// [in]mapiObject - The MAPIOBJECT property of the Outlook mailItem
        /// [in]headerName - The name of the header to add
        /// [in]headerValue - The value of the header to add
        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.

C++
///
/// Adds an InternetHeader to an outgoing Outlook- Email Message
///
/// [in]mapiObject - The MAPIOBJECT property of the Outlook mailItem
/// [in]headerName - The name of the header to add
/// [in]headerValue - The value of the header to add
void Fabric::AddMessageHeader(Object^ mapiObject, String^ headerName, 
    String^ headerValue)
{

    // Pointer to IUnknown Interface
    IUnknown* pUnknown = 0;

    // Pointer to IMessage Interface
    IMessage* pMessage = 0;

    // Pointer to IMAPIProp Interface
    IMAPIProp* pMAPIProp = 0;

    // a structure for the MANEDProp variable
    MAPINAMEID* pNamedProp = 0;

    // an array of PropertyTags as result for the GetIDdFromNames Method.
    SPropTagArray* lpTags = 0;

    // Convert the .Net Strings to unmanaged memory blocks
    IntPtr pHeaderName = Marshal::StringToHGlobalUni (headerName);
    IntPtr pHeaderValue = Marshal::StringToHGlobalUni  (headerValue);

    // if we have no MAPIObject everything is senseless...
    if (mapiObject == nullptr)
            throw gcnew System::ArgumentNullException 
            ("mapiObject","The MAPIObject must not be null!");
    try
    {
        // retrieve the IUnknon Interface from our MAPIObject 
        // coming from Outlook.
        pUnknown = (IUnknown*)Marshal::GetIUnknownForObject
                        (mapiObject).ToPointer ();

        // try to retrieve the IMessage interface, 
        // if we don't get it, everything else is senseless.
        if ( pUnknown->QueryInterface 
            (IID_IMessage, (void**)&pMessage) != S_OK)
           throw gcnew Exception
            ("QueryInterface failed on IUnknown for IID_Message");

        // try to retrieve the IMAPIProp interface from IMessage Interface, 
        // everything else is senseless.
        if ( pMessage->QueryInterface 
            (IID_IMAPIProp, (void**)&pMAPIProp) != S_OK)
           throw gcnew Exception
        ("QueryInterface failed on IMessage for IID_IMAPIProp");

        // double check, if we wave no pointer, exit...
        if (pMAPIProp == 0)
           throw gcnew Exception
        ("Unknown Error on receiving the Pointer to MAPI Properties.");

        // A well Google documented ID used to access the SMTP Headers 
        // of an Outlook Message
        GUID magicId = { 0x00020386, 0x0000, 0x0000, 
            { 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46 } };

        // The pointer to the namedProp structure
        if (MAPIAllocateBuffer(sizeof(MAPINAMEID), 
                (LPVOID*)&pNamedProp) != S_OK)
           throw gcnew Exception("Could not allocate memory buffer.");

        // The ID is used to access the named Property
        pNamedProp->lpguid = (LPGUID)&magicId;

        // Set the PropertyName
        pNamedProp->ulKind = MNID_STRING;
        pNamedProp->Kind.lpwstrName = (LPWSTR) pHeaderName.ToPointer();

        // Call the GetIDsFromNames to retrieve the propertyID to use
        if(pMAPIProp->GetIDsFromNames
            (1, &pNamedProp,  MAPI_CREATE, &lpTags ) != S_OK)
          throw gcnew Exception(String::Format 
        ("Error retrieving GetIDsFromNames: {0}.",headerName) );

        // the variable that will be passed to the HrSetOneProp method.
        SPropValue value;
        // the PROP_TAG macro creates the correct value from the 
        // value type and the value ID for us
        value.ulPropTag = PROP_TAG(PT_UNICODE, PROP_ID
                    (lpTags->aulPropTag[0]));
        // here we pass a Unicode string to the method and 
        // therefore we set the LPSZ property
        value.Value.LPSZ = (LPWSTR) pHeaderValue.ToPointer();

        // the HrSetOneProp method is used to set the property 
        // on the MAPI object
        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);

        // Free allocated unmanaged memory
        Marshal::FreeHGlobal (pHeaderName);
        Marshal::FreeHGlobal (pHeaderValue);

        // cleanup all references to COM Objects
        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.

C++
#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:

C++
///
/// construction code - we can initialize the MAPI Session here
///
Fabric::Fabric ()
{
    MAPIInitialize(0);
}

///
/// destructor - cleanup the MAPI session here.
///
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:

C++
///
/// This method takes an MAPI error code as parameter (GetLastError()) 
/// and returns the error message as managed string.
///
/// [in]dw - the MAPI error code.
/// [return] returns the human readable error message as string.
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.

Screenshot - BridgingTheGap_Reference2.png

Now you can change the code of your Windows Forms application and add the new functionality to it. That simply looks like this:

VB.NET
' create a new email and fill it with the info provided in the Form
Dim newMail As Outlook.MailItem = _
    outlookApplication.CreateItem(Outlook.OlItemType.olMailItem)

' here we use our C++ library
Dim fabric As MAPIConcubine.Fabric = New MAPIConcubine.Fabric()

' we pass the MAPIObject property to our library and the parameters
' note that in VB.Net you can't see the MAPIOBJECT property 
' but be sure it is there
fabric.AddMessageHeader(newMail.MAPIOBJECT, tbxHeaderName.Text, _
    tbxHeaderValue.Text)

' that's all
fabric = Nothing

newMail.To = tbxRecipientAddress.Text
newMail.Subject = tbxSubject.Text
newMail.Body = tbxMessage.Text

' send out the email
newMail.Send()

C++

VB.NET
// 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't see the MAPIOBJECT property 
// but be sure it is there
fabric.AddMessageHeader(newMail.MAPIOBJECT, tbxHeaderName.Text, 
    tbxHeaderValue.Text);

// that's all
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.

C++
/// 
/// Reads an InternetHeader from an Outlook- Email Message
/// 
/// [in] mapiObject - the MAPIOBJECT property of the outlook mailItem object.
/// [in] headerName - the name of the header to retrieve.
/// [returns] headerValue - the value of the header with the given name.
String^ Fabric::ReadMessageHeader(Object^ mapiObject, String^ headerName)
{
    
    // Pointer to IUnknown Interface
    IUnknown* pUnknown = 0;

    // Pointer to IMessage Interface
    IMessage* pMessage = 0;

    // Pointer to IMAPIProp Interface
    IMAPIProp* pMAPIProp = 0;

    // a structure for the MANEDProp variable
    MAPINAMEID* pNamedProp = 0;

    // an array of PropertyTags as result for the GetIDdFromNames Method.
    SPropTagArray* lpTags = 0;

    // pointer to a structure that receives the result from HrGetOneProp
    LPSPropValue lpSPropValue = 0;

    // Convert the .Net Strings to unmanaged memory blocks                
    IntPtr pHeaderName = Marshal::StringToHGlobalUni (headerName);
    
    // if we have no MAPIObject everything is senseless...
    if (mapiObject == nullptr) throw gcnew System::ArgumentNullException (
        "mapiObject","The MAPIObject must not be null!");
    try
    {

        // retrive the IUnknon Interface from our 
        // MAPIObject comming from Outlook.
        pUnknown = 
            (IUnknown*)Marshal::GetIUnknownForObject(mapiObject).ToPointer ();

        // try to retrieve the IMessage interface, if 
        // we don't get it, everything else is sensless.
        if ( pUnknown->QueryInterface (IID_IMessage, 
            (void**)&pMessage) != S_OK) 
            throw gcnew Exception(
                "QueryInterface failed on IUnknown for IID_Message");

        // try to retrieve the IMAPIProp interface from 
        // 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");

        // double check, if we wave no pointer, exit...
        if (pMAPIProp == 0) throw gcnew Exception(
            "Unknown Error on receiving the Pointer to MAPI Properties.");


        // A well Google documented ID used to 
        // access the SMTP Headers of an Outlook Message
        GUID magicId = 
        { 
            0x00020386, 0x0000, 0x0000, 
            { 
                0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46 
            } 
        }; 
        
        // The pointer to the namedProp structure
        if (MAPIAllocateBuffer(sizeof(MAPINAMEID), 
            (LPVOID*)&pNamedProp) != S_OK)  
            throw gcnew Exception("Could not allocate memory buffer.");

        // The ID is used to acces the named Property
        pNamedProp->lpguid = (LPGUID)&magicId; 

        // Set the PropertyName
        pNamedProp->ulKind = MNID_STRING;
        pNamedProp->Kind.lpwstrName = (LPWSTR) pHeaderName.ToPointer();

        // Call the GetIDsFromNames to retrieve the propertyID to use
        if(pMAPIProp->GetIDsFromNames(1, &pNamedProp, 
            STGM_READ, &lpTags ) != S_OK) 
            throw gcnew Exception(String::Format (
                "Error retrieving GetIDsFromNames: {0}.",headerName) );

        // The PROP_TAG macro creates the correct 
        // value for Type and PropertyID
        ULONG propTag = PROP_TAG(PT_UNICODE, PROP_ID(lpTags->aulPropTag[0]));

        // use the HrGetOneProp method to retrieve the profile name property
        // the lpPropValue pointer points to the result value 
        if (HrGetOneProp(pMAPIProp,propTag,&lpSPropValue) != S_OK)
            throw gcnew Exception("HrGetOneProp failed for named property !");
        
        // create a managed string from the unmanaged string.
        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

Screenshot - BridgingTheGap_Profile.png

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:

C++
/// 
/// Returns the MAPI profilename of the current session.
/// 
/// [in] mapiObject - the MAPIOBJECT property of the 
/// Outlook Application.Session object.
/// [returns] - the name of the currently used profile.
String^ Fabric::GetProfileName(Object^ mapiObject)
{

    // the result returned to the calling method
    String^ result = nullptr;

    // pointer to IUnknown interface
    IUnknown* pUnknown = 0;

    // pointer to the MAPI session interface
    LPMAPISESSION lpMAPISession = 0;

    // pointer to a profilesection interface
    LPPROFSECT lpProfileSection = 0;

    // pointer to a structure that receives the result from HrGetOneProp
    LPSPropValue lpSPropValue = 0;

    // if we have no MAPIObject everything is senseless...
    if (mapiObject == nullptr)
        throw gcnew System::ArgumentNullException ("mapiObject",
            "The MAPIObject must not be null!");

    try
    {
        // retrive the IUnknon Interface from our MAPIObject 
        // comming from Outlook.
        pUnknown = 
            (IUnknown*)Marshal::GetIUnknownForObject(mapiObject).ToPointer ();
        
        // try to retrieve the IMAPISession interface, if 
        // we don't get it, everything else is sensless.
        if ( pUnknown->QueryInterface (IID_IMAPISession, 
            (void**)&lpMAPISession) != S_OK)
            throw gcnew Exception(
                "QueryInterface failed on IUnknown for IID_IMAPISession");
                    
        // use the OpenProfileSection of the MAPISession 
        // object to retrieve a pointer to the current 
        // profilesection interface
        if( lpMAPISession->OpenProfileSection(
            (LPMAPIUID)GLOBAL_PROFILE_SECTION_MAPIUID,
            NULL,STGM_READ, &lpProfileSection) != S_OK)
            throw gcnew Exception("OpenProfileSection method failed!");

        // use the HrGetOneProp method to retrieve the profile name property
        // the lpPropValue pointer points to the result value 
        if (HrGetOneProp(lpProfileSection,
            PR_PROFILE_NAME_W,&lpSPropValue) != S_OK)
            throw gcnew Exception(
                "HrGetOneProp failed for property PR_PROFILE_NAME_W !");
        
        // create a managed string from the unmanaged string.
        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.

C++
/// 
/// Returns the current MAPI profilenames available in the system registry.
/// 
/// [returns] - a string array of all available profilenames.
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#:

C#
// Create an Outlook session
object missing = System.Reflection.Missing.Value;

// get the Outlook Application Object
Outlook.Application outlookApplication = new Outlook.Application();

// get the namespace object
Outlook.NameSpace olNamespace = outlookApplication.GetNamespace("MAPI");

// Logon to Session, here we use the name from 
// the comboBox to logon to a specific session.
// If there is already an Outlook instance with 
// a Session - then this profile is used.
olNamespace.Logon(comboBox1.Text ,  missing, true, true);


// Use the fabric to get the current profilename
MAPIConcubine.Fabric fabric = new MAPIConcubine.Fabric();
try
{
    // pass along the namespace.MAPIOBJECT to retrieve the profilename
    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

Screenshot - BridgingTheGap_AppointmentColor.png

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:

C++
/// Defines the available Colors for AppointmentLabels
enum class AppointmentLabelColor
{
    None = 0,              // Default Color
    Important = 1,         // Red
    Business = 2,          // Blue
    Personal = 3,          // Green
    Vacation = 4,          // Grey
    Deadline = 5,          // Orange
    Travel_Required = 6,   // Light-Blue
    Needs_Preparation = 7, // Grey
    Birthday = 8,          // violett
    Anniversary = 9,       // turquoise
    Phone_Call = 10        // Yellow
} ;

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.

C++
/// 
/// Sets the AppointmentLabelColor for a Outlook- Appointment
/// 
/// [in] mapiObject - the MAPIOBJECT property of 
/// the outlook appointmentItem object.
/// [in] the desired colortype that you wish to set 
void Fabric::SetAppointmentLabelColor(Object^ mapiObject, 
    AppointmentLabelColor color)
{
    
    // Pointer to IUnknown Interface
    IUnknown* pUnknown = 0;

    // Pointer to IMessage Interface
    IMessage* pMessage = 0;

    // Pointer to IMAPIProp Interface
    IMAPIProp* pMAPIProp = 0;

    // a structure for the NAMEDProp variable
    MAPINAMEID* pNamedProp = 0;

    // an array of PropertyTags as result for the GetIDdFromNames Method.
    SPropTagArray* lpTags = 0;

    // if we have no MAPIObject everything is senseless...
    if (mapiObject == nullptr) throw gcnew System::ArgumentNullException (
        "mapiObject","The MAPIObject must not be null!");
    try
    {

        // retrive the IUnknon Interface from our MAPIObject 
        // comming from Outlook.
        pUnknown = 
            (IUnknown*)Marshal::GetIUnknownForObject(mapiObject).ToPointer ();

        // try to retrieve the IMessage interface, if 
        // we don't get it, everything else is sensless.
        if ( pUnknown->QueryInterface (IID_IMessage, 
            (void**)&pMessage) != S_OK) 
            throw gcnew Exception(
                "QueryInterface failed on IUnknown for IID_Message");

        // try to retrieve the IMAPIProp interface from 
            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");

        // double check, if we wave no pointer, exit...
        if (pMAPIProp == 0) 
            throw gcnew Exception(
                "Unknown Error on receiving the Pointer to MAPI Properties.");


        // A well Google documented ID used to access an 
        // appointment color label
        GUID magicId = 
        { 
            0x00062002, 0x0000, 0x0000, 
            { 
                0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46 
            } 
        };
        LONG propertyName = 0x8214;
        
        // The pointer to the namedProp structure
        if (MAPIAllocateBuffer(sizeof(MAPINAMEID), 
            (LPVOID*)&pNamedProp) != S_OK)  
            throw gcnew Exception("Could not allocate memory buffer.");

        // The ID is used to acces the named Property
        pNamedProp->lpguid = (LPGUID)&magicId; 

        // Set the PropertyName - we have an ID as Property here
        pNamedProp->ulKind = MNID_ID;
        pNamedProp->Kind.lID = propertyName;

        // Call the GetIDsFromNames to retrieve the propertyID to use
        if(pMAPIProp->GetIDsFromNames(1, &pNamedProp, 
            MAPI_CREATE, &lpTags ) != S_OK)
            throw gcnew Exception(String::Format (
                "Error retrieving GetIDsFromNames: {0}.",propertyName) );

        // A structure that is used to pass the value to the property
        SPropValue value;

        // The PROP_TAG macro creates the correct 
        // value for Type and PropertyID
        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) );

        // save the changes to the Item
        pMessage->SaveChanges (KEEP_OPEN_READWRITE);

        
    }
    catch (Exception^ ex)
    {
       ...
    }
    finally
    {
       ...
    }

}

The method to retrieve the appointment label color is implemented as you can see here:

C++
// A well Google documented ID used to access an appointment color label
GUID magicId = 
{ 
    0x00062002, 0x0000, 0x0000, 
    { 
        0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46 
    } 
};
LONG propertyName = 0x8214;

// The pointer to the namedProp structure
if (MAPIAllocateBuffer(sizeof(MAPINAMEID), (LPVOID*)&pNamedProp) != S_OK)  
    throw gcnew Exception("Could not allocate memory buffer.");

// The ID is used to acces the named Property
pNamedProp->lpguid = (LPGUID)&magicId; 

// Set the PropertyName - we have an ID as Property here
pNamedProp->ulKind = MNID_ID;
pNamedProp->Kind.lID = propertyName;

// Call the GetIDsFromNames to retrieve the propertyID to use
if(pMAPIProp->GetIDsFromNames(1, &pNamedProp,  STGM_READ, &lpTags ) != S_OK) 
    throw gcnew Exception(String::Format (
        "Error retrieving GetIDsFromNames: {0}.",propertyName) );

// The PROP_TAG macro creates the correct value for Type and PropertyID
ULONG propTag = PROP_TAG(PT_LONG, PROP_ID(lpTags->aulPropTag[0]));

// use the HrGetOneProp method to retrieve the profile name property
// the lpPropValue pointer points to the result value 
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)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)