Click here to Skip to main content
15,860,844 members
Articles / Programming Languages / C#

Managed MAPI(Part 1): Logon MAPI Session and Retrieve Message Stores

Rate me:
Please Sign up or sign in to vote.
4.70/5 (8 votes)
9 Sep 2012CPOL8 min read 48.7K   1.8K   23   8
This article demonstrates how to implement pure .NET MAPI, and use this managed MAPI component to do a little WPF MAPI application.

Introduction

This article demonstrates how to implement pure .NET MAPI, and use this managed MAPI component to do a little WPF MAPI application.

What’s MAPI

Messaging API (MAPI) provides the messaging architecture, a set of interfaces, functions, and other data types to facility the development of email messaging. Currently all MAPI wrapper components are unmanaged or dependant on unmanaged MAPI wrapper DLLs. That causes a lot of problems to deploy and support a 64-bit environment. Managed MAPI is a pure .NET MAPI component based on the MAPI common interface.

Calling MAPI32 DLL in C# with P/Invoke

The interop features of the Common Language Run-time (CLR), called Platform Invoke (P/Invoke), allows developers to dip down into the underlying Operating System for some critical functionality. Mapi32.dll is a module for the Windows Messaging API (MAPI). Please don’t assume Mapi32.dll is always a 32-bit DLL from the name. It’s a 32-bit DLL on 32-bit Windows. There are both 64-bit and 32-bit Mapi32.dll on 64-bit Windows. The 64-bit one is in Windows\System32 and the 32-bit one is in Windows\SysWOW64.

Managed MAPI is based on the MAPI common interface, not calling Mapi32.dll directly. We just use some very basic functions to log on to a MAPI session and free unmanaged memory. These functions are all included in a class called MAPINative.

C#
class MAPINative
{
    [DllImport("MAPI32.dll")]
    internal static extern int MAPIInitialize(IntPtr lpMapiInit);

    [DllImport("MAPI32.dll")]
    internal static extern void MAPIUninitialize();

    [DllImport("MAPI32.dll")]
    internal static extern int MAPILogonEx(uint ulUIParam, 
            [MarshalAs(UnmanagedType.LPWStr)] string lpszProfileName,
            [MarshalAs(UnmanagedType.LPWStr)] string lpszPassword, 
            uint flFlags, out IntPtr lppSession);

    [DllImport("MAPI32.dll")]
    internal static extern int MAPIFreeBuffer(IntPtr lpBuffer);
}

Implement a .NET HRESULT enumeration

Developers who have done some Windows SDK developments all know HRESULT is a data type used in the Windows Operating System to formally lay out ranges of error codes. There is no counterpart type in .NET. When we do an Interop call, sometimes we’re very confused about the returned integer. Not sure what the error is. So we transplant HRESULT from C++ to .NET.

C#
public enum HRESULT : uint
{
    /// <summary>
    /// False
    /// </summary>
    S_FALSE = 0x00000001,
    /// <summary>
    /// OK
    /// </summary>
    S_OK = 0x00000000,
    /// <summary>
    /// Not implemented
    /// </summary>
    E_NOTIMPL = 0x80004001,
    /// <summary>
    /// Call failed
    /// </summary>
    MAPI_E_CALL_FAILED = 0x80004005,
    /// <summary>
    /// Not enough memory
    /// </summary>
    MAPI_E_NOT_ENOUGH_MEMORY = 0x8007000E,
    /// <summary>
    /// Invalid parameter
    /// </summary>
    MAPI_E_INVALID_PARAMETER = 0x80000003,
    /// <summary>
    /// Interface not supported
    /// </summary>
    MAPI_E_INTERFACE_NOT_SUPPORTED = 0x80000004,
    /// <summary>
    /// No access
    /// </summary>
    MAPI_E_NO_ACCESS = 0x80000009,
    /// <summary>
    /// Invalid argument
    /// </summary>
    E_INVALIDARG = 0x80070057,
    /// <summary>
    /// Out of memory
    /// </summary>
    E_OUTOFMEMORY = 0x80000002,
    /// <summary>
    /// Unexpected
    /// </summary>
    E_UNEXPECTED = 0x8000FFFF,
    /// <summary>
    /// Fail
    /// </summary>
    E_FAIL = 0x80000008,
    /// <summary>
    /// No support
    /// </summary>
    MAPI_E_NO_SUPPORT = 0x80040000 | 0x102,
    /// <summary>
    /// Bar char width
    /// </summary>
    MAPI_E_BAD_CHARWIDTH = 0x80040000 | 0x103,
    /// <summary>
    /// String too long
    /// </summary>
    MAPI_E_STRING_TOO_LONG = 0x80040000 | 0x105,
    /// <summary>
    /// Unknown flags
    /// </summary>
    MAPI_E_UNKNOWN_FLAGS = 0x80040000 | 0x106,
    /// <summary>
    /// Invalid entry ID
    /// </summary>
    MAPI_E_INVALID_ENTRYID = 0x80040000 | 0x107,
    /// <summary>
    /// Invalid object
    /// </summary>
    MAPI_E_INVALID_OBJECT = 0x80040000 | 0x108,
    /// <summary>
    /// Object changed
    /// </summary>
    MAPI_E_OBJECT_CHANGED = 0x80040000 | 0x109,
    /// <summary>
    /// Object deleted
    /// </summary>
    MAPI_E_OBJECT_DELETED = 0x80040000 | 0x10A,
    /// <summary>
    /// Busy
    /// </summary>
    MAPI_E_BUSY = 0x80040000 | 0x10B,
    /// <summary>
    /// Not enough disk
    /// </summary>
    MAPI_E_NOT_ENOUGH_DISK = 0x80040000 | 0x10D,
    /// <summary>
    /// Not enough resources
    /// </summary>
    MAPI_E_NOT_ENOUGH_RESOURCES = 0x80040000 | 0x10E,
    /// <summary>
    /// Not found
    /// </summary>
    MAPI_E_NOT_FOUND = 0x80040000 | 0x10F,
    /// <summary>
    /// Version error
    /// </summary>
    MAPI_E_VERSION = 0x80040000 | 0x110,
    /// <summary>
    /// Logon failed
    /// </summary>
    MAPI_E_LOGON_FAILED = 0x80040000 | 0x111,
    /// <summary>
    /// Session limited
    /// </summary>
    MAPI_E_SESSION_LIMIT = 0x80040000 | 0x112,
    /// <summary>
    /// User cancel
    /// </summary>
    MAPI_E_USER_CANCEL = 0x80040000 | 0x113,
    /// <summary>
    /// Unable to abort
    /// </summary>
    MAPI_E_UNABLE_TO_ABORT = 0x80040000 | 0x114,
    /// <summary>
    /// Network error
    /// </summary>
    MAPI_E_NETWORK_ERROR = 0x80040000 | 0x115,
    /// <summary>
    /// Disk error
    /// </summary>
    MAPI_E_DISK_ERROR = 0x80040000 | 0x116,
    /// <summary>
    /// Too complex
    /// </summary>
    MAPI_E_TOO_COMPLEX = 0x80040000 | 0x117,
    /// <summary>
    /// Bad column
    /// </summary>
    MAPI_E_BAD_COLUMN = 0x80040000 | 0x118,
    /// <summary>
    /// Extended error
    /// </summary>
    MAPI_E_EXTENDED_ERROR = 0x80040000 | 0x119,
    /// <summary>
    /// Computed
    /// </summary>
    MAPI_E_COMPUTED = 0x80040000 | 0x11A,
    /// <summary>
    /// Corrupt data
    /// </summary>
    MAPI_E_CORRUPT_DATA = 0x80040000 | 0x11B,
    /// <summary>
    /// Unconfigured
    /// </summary>
    MAPI_E_UNCONFIGURED = 0x80040000 | 0x11C,
    /// <summary>
    /// One provider fail
    /// </summary>
    MAPI_E_FAILONEPROVIDER = 0x80040000 | 0x11D,
    /// <summary>
    /// Unknown CPID
    /// </summary>
    MAPI_E_UNKNOWN_CPID = 0x80040000 | 0x11E,
    /// <summary>
    /// Unknown LCID 
    /// </summary>
    MAPI_E_UNKNOWN_LCID = 0x80040000 | 0x11F,
    /// <summary>
    /// Corrupt store
    /// </summary>
    MAPI_E_CORRUPT_STORE = 0x80040000 | 0x600,
    /// <summary>
    /// Not in queue
    /// </summary>
    MAPI_E_NOT_IN_QUEUE = 0x80040000 | 0x601,
    /// <summary>
    /// No suppress
    /// </summary>
    MAPI_E_NO_SUPPRESS = 0x80040000 | 0x602,
    /// <summary>
    /// Collision
    /// </summary>
    MAPI_E_COLLISION = 0x80040000 | 0x604,
    /// <summary>
    /// Not Initialized
    /// </summary>
    MAPI_E_NOT_INITIALIZED = 0x80040000 | 0x605,
    /// <summary>
    /// Not standard
    /// </summary>
    MAPI_E_NON_STANDARD = 0x80040000 | 0x606,
    /// <summary>
    /// No recipients
    /// </summary>
    MAPI_E_NO_RECIPIENTS = 0x80040000 | 0x607,
    /// <summary>
    /// Submitted error
    /// </summary>
    MAPI_E_SUBMITTED = 0x80040000 | 0x608,
    /// <summary>
    /// Has folders
    /// </summary>
    MAPI_E_HAS_FOLDERS = 0x80040000 | 0x609,
    /// <summary>
    /// Has message
    /// </summary>
    MAPI_E_HAS_MESSAGES = 0x80040000 | 0x60A,
    /// <summary>
    /// Folder cycle
    /// </summary>
    MAPI_E_FOLDER_CYCLE = 0x80040000 | 0x60B,
}

Now we change the MAPI32 P/Invoke definition to the below.

C#
class MAPINative
{
    [DllImport("MAPI32.dll")]
    internal static extern HRESULT MAPIInitialize(IntPtr lpMapiInit);

    [DllImport("MAPI32.dll")]
    internal static extern void MAPIUninitialize();

    [DllImport("MAPI32.dll")]
    internal static extern int MAPILogonEx(uint ulUIParam, 
            [MarshalAs(UnmanagedType.LPWStr)] string lpszProfileName,
            [MarshalAs(UnmanagedType.LPWStr)] string lpszPassword, 
            uint flFlags, out IntPtr lppSession);

    [DllImport("MAPI32.dll")]
    internal static extern HRESULT MAPIFreeBuffer(IntPtr lpBuffer);
}

Logon MAPI Session

Declare IMAPISession Interface in C#

This interface manages objects associated with a MAPI logon session.

C#
[
      ComImport, ComVisible(false),
      InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
      Guid("00020300-0000-0000-C000-000000000046")
]

public interface IMAPISession
{
    HRESULT GetLastError(int hResult, uint ulFlags, out IntPtr lppMAPIError);
    HRESULT GetMsgStoresTable(uint ulFlags, out IntPtr lppTable);
    HRESULT OpenMsgStore(uint ulUIParam, uint cbEntryID, IntPtr lpEntryID, 
            IntPtr lpInterface, uint ulFlags, out IntPtr lppMDB);
    HRESULT OpenAddressBook(uint ulUIParam, IntPtr lpInterface, uint ulFlags, out IntPtr lppAdrBook);
    HRESULT OpenProfileSection(ref Guid lpUID, ref Guid lpInterface, uint ulFlags, out IntPtr lppProfSect);
    HRESULT GetStatusTable(uint ulFlags, out IntPtr lppTable);
    HRESULT OpenEntry(uint cbEntryID, IntPtr lpEntryID, IntPtr lpInterface, 
            uint ulFlags, out uint lpulObjType, out IntPtr lppUnk);
    HRESULT CompareEntryIDs(uint cbEntryID1, IntPtr lpEntryID1, 
            uint cbEntryID2, IntPtr lpEntryID2, uint ulFlags, out bool lpulResult);
    [PreserveSig]
    HRESULT Advise(uint cbEntryID, IntPtr lpEntryID, uint ulEventMask, 
            IntPtr pAdviseSink, out uint lpulConnection);
    [PreserveSig]
    HRESULT Unadvise(uint ulConnection);
    /// <exclude/>
    HRESULT MessageOptions(uint ulUIParam, uint ulFlags, 
           [MarshalAs(UnmanagedType.LPWStr)] string lpszAdrType, IntPtr lpMessage);
    /// <exclude/>
    HRESULT QueryDefaultMessageOpt([MarshalAs(UnmanagedType.LPWStr)] string lpszAdrType, 
            uint ulFlags, out uint lpcValues, out IntPtr lppOptions);
    /// <exclude/>
    HRESULT EnumAdrTypes(uint ulFlags, out uint lpcAdrTypes, out IntPtr lpppszAdrTypes);
    HRESULT QueryIdentity(out uint lpcbEntryID, out IntPtr lppEntryID);
    HRESULT Logoff(uint ulUIParam, uint ulFlags, uint ulReserved);
    HRESULT SetDefaultStore(uint ulFlags, uint cbEntryID, IntPtr lpEntryID);
    HRESULT AdminServices(uint ulFlags, out IntPtr lppServiceAdmin);
    HRESULT ShowForm(uint ulUIParam, IntPtr lpMsgStore, IntPtr lpParentFolder, 
            ref Guid lpInterface, uint ulMessageToken,
    IntPtr lpMessageSent, uint ulFlags, uint ulMessageStatus, uint ulMessageFlags, 
           uint ulAccess, [MarshalAs(UnmanagedType.LPWStr)] string lpszMessageClass);
    HRESULT PrepareForm(ref Guid lpInterface, IntPtr lpMessage, out uint lpulMessageToken);
}

Logon MAPI session by MAPI32 SDK

The MAPIInitialize function increments the MAPI reference count for the MAPI subsystem, and the MAPIUninitialize function decrements the internal reference count. Thus, the number of calls to one function must equal the number of calls to the other.

MAPIInitialize must be called before making any other MAPI call. Failure to do so causes the client or service provider calls to return the MAPI_E_NOT_INITIALIZED value.

After MAPIInitialize , we call the MAPILogonEx function to log on to a session with the messaging system.

C#
IntPtr pSession = IntPtr.Zero;
if (MAPINative.MAPIInitialize(IntPtr.Zero) == HRESULT.S_OK)
{
    MAPINative.MAPILogonEx(0, null, null, (uint)(MAPIFlag.EXTENDED | 
                           MAPIFlag.USE_DEFAULT), out pSession);
    if (pSession == IntPtr.Zero)
        MAPINative.MAPILogonEx(0, null, null, (uint)(MAPIFlag.EXTENDED | 
                   MAPIFlag.NEW_SESSION | MAPIFlag.USE_DEFAULT), out pSession);
}

If MAPILogonEx is successful, an integer pointer of the MAPI Session will be passed, and we get the IMAPISession object with Marshal.

C#
if (pSession != IntPtr.Zero)
{
    object sessionObj = null;
    try
    {
        sessionObj = Marshal.GetObjectForIUnknown(pSession);
          session_ = sessionObj as IMAPISession;
    }
    catch { }
}

MAPI properties and PropTag

A property is an attribute of a MAPI object. MAPI defines many properties, some to describe many objects and some that are appropriate only for an object of a particular type. Properties can be persistent or temporary. Properties that persist from session to session can be stored with their objects' data or in the profile. Temporary properties exist only for the duration of the current session.

The property type and identifier are combined into a single component called the property tag. Property tags are constants that can be used to easily refer to the property. A property tag is a 32-bit number that contains a unique property identifier in bits 16 through 31 and a property type in bits 0 through 15.

Image 1

Property tags are used to identify MAPI properties and every property must have one, regardless of whether the property is defined by MAPI, a client, or a service provider. MAPI defines a set of property tag constants for its properties; these properties are referred to as the "MAPI-defined properties".

We declare a property type enumeration as below.

C#
public enum PT : uint
{
    /// <summary>
    /// (Reserved for interface use) type doesn't matter to caller
    /// </summary>
    PT_UNSPECIFIED = 0,
    /// <summary>
    /// NULL property value
    /// </summary>
    PT_NULL = 1,
    /// <summary>
    /// Signed 16-bit value
    /// </summary>
    PT_I2 = 2,
    /// <summary>
    /// Signed 32-bit value
    /// </summary>
    PT_LONG = 3,
    /// <summary>
    /// 4-byte floating point
    /// </summary>
    PT_R4 = 4,
    /// <summary>
    /// Floating point double
    /// </summary>
    PT_DOUBLE = 5,
    /// <summary>
    /// Signed 64-bit int (decimal w/ 4 digits right of decimal pt)
    /// </summary>
    PT_CURRENCY = 6,
    /// <summary>
    /// Application time
    /// </summary>
    PT_APPTIME = 7,
    /// <summary>
    /// 32-bit error value
    /// </summary>
    PT_ERROR = 10,
    /// <summary>
    /// 16-bit boolean (non-zero true,zero false)
    /// </summary>
    PT_BOOLEAN = 11,
    /// <summary>
    /// 16-bit boolean (non-zero true)
    /// </summary>
    PT_BOOLEAN_DESKTOP = 11,
    /// <summary>
    /// Embedded object in a property
    /// </summary>
    PT_OBJECT = 13,
    /// <summary>
    /// 8-byte signed integer
    /// </summary>
    PT_I8 = 20,
    /// <summary>
    /// Null terminated 8-bit character string
    /// </summary>
    PT_STRING8 = 30,
    /// <summary>
    /// Null terminated Unicode string
    /// </summary>
    PT_UNICODE = 31,
    /// <summary>
    /// FILETIME 64-bit int w/ number of 100ns periods since Jan 1,1601
    /// </summary>
    PT_SYSTIME = 64,
    /// <summary>
    /// OLE GUID
    /// </summary>
    PT_CLSID = 72,
    /// <summary>
    /// Uninterpreted (counted byte array)
    /// </summary>
    PT_BINARY = 258,
    /// <summary>
    /// IF MAPI Unicode, PT_TString is Unicode string; otheriwse 8-bit character string
    /// </summary>
    PT_TSTRING = PT_UNICODE
}

In this part, we only need retrieve two MAPI Defined Properties.

C#
public enum PropTags : uint
{
    PR_DISPLAY_NAME = PT.PT_TSTRING | 0x3001 << 16,
    PR_DEFAULT_STORE = PT.PT_BOOLEAN | 0x3400 << 16,
}

Entry Identifier

Entry identifiers come in two types: short-term and long-term. Short-term entry identifiers are faster to construct, but their uniqueness is guaranteed only over the life of the current session on the current workstation. Long-term entry identifiers have a more prolonged lifespan. Short-term entry identifiers are used primarily for rows in tables and entries in dialog boxes, whereas long-term entry identifiers are used for many objects such as messages, folders, and distribution lists.

In C++, the ENTRYID structure is used by the message store and address book providers to construct unique identifiers for their objects.

C++
typedef struct
{
  BYTE abFlags[4];
  BYTE ab[MAPI_DIM];
} ENTRYID;

We declare the counterpart SBinary structure in C#.

C#
[StructLayout(LayoutKind.Sequential)]
internal struct SBinary
{
    public uint cb;
    public IntPtr lpb;

    public byte[] AsBytes
    {
        get
        {
            byte[] b = new byte[cb];
            for (int i = 0; i < cb; i++)
                b[i] = Marshal.ReadByte(lpb, i);
            return b;
        }
    }

    public static SBinary SBinaryCreate(byte[] data)
    {
        SBinary b;
        b.cb = (uint)data.Length;
        b.lpb = Marshal.AllocHGlobal((int)b.cb);
        for (int i = 0; i < b.cb; i++)
            Marshal.WriteByte(b.lpb, i, data[i]);
        return b;
    }

    public static void SBinaryRelease(ref SBinary b)
    {
        if (b.lpb != IntPtr.Zero)
        {
            Marshal.FreeHGlobal(b.lpb);
            b.lpb = IntPtr.Zero;
            b.cb = 0;
        }
    }
}

SPropValue structure

There are several data structures for describing properties and information about properties. The most commonly used structure is the SPropValue structure. This structure contains the three pieces of information that describe a property:

  • Data, or value, of the property.
  • Data type of the property's value, such as integer or Boolean.
  • Numeric value within a particular range that uniquely identifies the property and component responsible for maintaining it. For example, there is a range to hold message content properties defined by MAPI and another range to hold message content properties defined by a client for a custom message class.

In C++, the definition of SPropValue is like this:

C#
struct { 
  ULONG     ulPropTag;
  ULONG     dwAlignPad;
  union _PV Value;
} SPropValue;

Now we need to transplant this definition to C#.

_PV union in C#

Usage of unions in the native world is pretty common. However, the same is not true for the .NET world. However, while using interop we can work around as shown  below.

C#
[StructLayout(LayoutKind.Explicit)]
internal struct _PV
{
    [FieldOffset(0)]
    public Int16 i;
    [FieldOffset(0)]
    public int l;
    [FieldOffset(0)]
    public uint ul;
    [FieldOffset(0)]
    public float flt;
    [FieldOffset(0)]
    public double dbl;
    [FieldOffset(0)]
    public UInt16 b;
    [FieldOffset(0)]
    public double at;
    [FieldOffset(0)]
    public IntPtr lpszA;
    [FieldOffset(0)]
    public IntPtr lpszW;
    [FieldOffset(0)]
    public IntPtr lpguid;
    /*[FieldOffset(0)]
    public IntPtr bin;*/
    [FieldOffset(0)]
    public UInt64 li;
    [FieldOffset(0)]
    public SBinary bin;
}
SPropValue structure in C#
C#
[StructLayout(LayoutKind.Sequential)]
internal struct SPropValue
{
    public UInt32 ulPropTag;
    public UInt32 dwAlignPad;
    public _PV Value;
}

Construct MAPIProp class with SPropValue

Although we have declared the SPropValue structure in C# already, it’s still not easy to use. We need a direct property class to simplify our coding.

SPropValue includes a prop tag and prop value. Also we can know the value type from the prop tag. So there is enough information to build a property object.

C#
public class MAPIProp : IPropValue
{
    private uint tag;
    private Type t;
    private uint ul = 0;
    private byte[] binary;
    private string str = null;
    private UInt64 p_li;

    /// <summary>
    ///  Initializes a new instance of the MAPIProp class.
    /// </summary>
    /// <param name="prop">pSPropValue structure</param>
    internal MAPIProp(SPropValue prop)
    {
        this.tag = prop.ulPropTag;
        switch ((PT)((uint)this.tag & 0xFFFF))
        {
            case PT.PT_TSTRING:
                this.t = typeof(string);
                this.str = Marshal.PtrToStringUni(prop.Value.lpszW);
                break;
            case PT.PT_STRING8:
                this.t = typeof(string);
                this.str = Marshal.PtrToStringAnsi(prop.Value.lpszA);
                break;
            case PT.PT_LONG:
            case PT.PT_I2:
            case PT.PT_BOOLEAN:
                this.t = typeof(int);
                this.ul = prop.Value.ul;
                break;
            case PT.PT_BINARY:
                this.t = typeof(Byte[]);
                this.binary = prop.Value.bin.AsBytes;
                break;
            case PT.PT_SYSTIME:
                this.t = typeof(DateTime);
                this.p_li = prop.Value.li;
                break;
            case PT.PT_I8:
                this.t = typeof(UInt64);
                this.p_li = prop.Value.li;
                break;
            default:
                this.t = null;
                break;
        }
    }

    /// <summary>
    /// Gets type of value
    /// </summary>
    public Type Type { get { return this.t; } }
    /// <summary>
    /// Gets property tag
    /// </summary>
    public uint Tag { get { return this.tag; } }
    /// <summary>
    /// Gets string value
    /// </summary>
    public string AsString
    {
        get
        {
            if (this.t == typeof(string))
                return this.str;
            return null;
        }
    }
    /// <summary>
    /// Gets Int32 value
    /// </summary>
    public int AsInt32
    {
        get
        {
            if (this.t == typeof(int))
                return (int)this.ul;
            else
                throw new Exception("Invalid type request");
        }
    }

    /// <summary>
    /// Gets byte array value
    /// </summary>
    public byte[] AsBinary
    {
        get
        {
            if (this.t == typeof(byte[]))
                return this.binary;
            else
                return null;
        }
    }

    /// <summary>
    /// Gets unsigned Int64 value
    /// </summary>
    public UInt64 AsUInt64
    {
        get
        {
            if (this.t == typeof(UInt64) || this.t == typeof(DateTime))
                return this.p_li;
            else
                throw new Exception("Invalid type request");
        }
    }
    /// <summary>
    /// Gets nullable DateTime value
    /// </summary>
    public DateTime? AsDateTime
    {
        get
        {
            if (this.t == typeof(DateTime))
            {
                try
                {
                    DateTime dt = DateTime.FromFileTimeUtc((long)this.p_li);
                    dt = dt.ToLocalTime();
                    return dt;
                }
                catch { }
            }
            return null;
        }
    }
    /// <summary>
    /// Gets boolean value
    /// </summary>
    public bool AsBool
    {
        get
        {
            if (this.t == typeof(int))
                return (short)this.ul != 0;
            else
                throw new Exception("Invalid type request");
        }
    }
    /// <summary>
    /// Gets short value
    /// </summary>
    public short AsShort
    {
        get
        {
            if (this.t == typeof(int))
                return (short)(int)this.ul;
            else
                throw new Exception("Invalid type request");
        }
    }
}

MAPI Table

A MAPI table is a MAPI object that is used to view a collection of properties belonging to other MAPI objects of a particular type. MAPI tables are structured in a row and column format with each row representing an object and each column representing a property of the object.

Define IMAPITable interface in C#

IMAPITable is used by clients and service providers to manipulate the way a table appears.

C#
[Guid("00020301-0000-0000-c000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IMAPITable
{
    HRESULT GetLastError(int hResult, uint ulFlags, out IntPtr lppMAPIError);
    HRESULT Advise(uint ulEventMask, IntPtr lpAdviseSink, IntPtr lpulConnection);
    HRESULT Unadvise(uint ulConnection);
    HRESULT GetStatus(IntPtr lpulTableStatus, IntPtr lpulTableType);
    HRESULT SetColumns([MarshalAs(UnmanagedType.LPArray)] uint[] lpPropTagArray, uint ulFlags);
    HRESULT QueryColumns(uint ulFlags, IntPtr lpPropTagArray);
    HRESULT GetRowCount(uint ulFlags, out uint lpulCount);
    HRESULT SeekRow(int bkOrigin, int lRowCount, out IntPtr lplRowsSought);
    HRESULT SeekRowApprox(uint ulNumerator, uint ulDenominator);
    HRESULT QueryPosition(IntPtr lpulRow, IntPtr lpulNumerator, IntPtr lpulDenominator);
    HRESULT FindRow(out IntPtr lpRestriction, uint BkOrigin, uint ulFlags);
    HRESULT Restrict(out IntPtr lpRestriction, uint ulFlags);
    HRESULT CreateBookmark(IntPtr lpbkPosition);
    HRESULT FreeBookmark(IntPtr bkPosition);
    HRESULT SortTable(IntPtr lpSortCriteria, int ulFlags);
    HRESULT QuerySortOrder(IntPtr lppSortCriteria);
    HRESULT QueryRows(int lRowCount, uint ulFlags, out IntPtr lppRows);
    HRESULT Abort();
    HRESULT ExpandRow(uint cbInstanceKey, IntPtr pbInstanceKey, uint ulRowCount, 
            uint ulFlags, IntPtr lppRows, IntPtr lpulMoreRows);
    HRESULT CollapseRow(uint cbInstanceKey, IntPtr pbInstanceKey, uint ulFlags, IntPtr lpulRowCount);
    HRESULT WaitForCompletion(uint ulFlags, uint ulTimeout, IntPtr lpulTableStatus);
    HRESULT GetCollapseState(uint ulFlags, uint cbInstanceKey, 
            IntPtr lpbInstanceKey, IntPtr lpcbCollapseState, IntPtr lppbCollapseState);
    HRESULT SetCollapseState(uint ulFlags, uint cbCollapseState, IntPtr pbCollapseState, IntPtr lpbkLocation);
}

Implement the MAPITable class from the IMAPITable interface

The IMAPITable interface definition comes from C++. For convenience, we build the MAPITable class with the IMAPITable object. For a MAPI table, the most used functions are SetColumn and QueryRows.

SetColumn

The SetColumn function defines the particular properties and order of properties to appear as columns in the table.

C#
HRESULT SetColumns([MarshalAs(UnmanagedType.LPArray)]
uint[] lpPropTagArray, uint ulFlags);

When we call IMAPITable.SetColumn, the lpPropTagArray will be converted to a SPropTagArray structure.

C#
typedef struct _SPropTagArray
{
  ULONG cValues;
  ULONG aulPropTag[MAPI_DIM];
} SPropTagArray;

cValues is the count of property tags in the array indicated by the aulPropTag member. This structure is still an integer array, just one more member (first) than the original array.

So we wrap SetColumns in the MAPITable class like below.

C#
public bool SetColumns(PropTags[] tags)
{
    uint[] t = new uint[tags.Length + 1];
    t[0] = (uint)tags.Length;
    for (int i = 0; i < tags.Length; i++)
        t[i + 1] = (uint)tags[i];
    return tb_.SetColumns(t, 0) == HRESULT.S_OK;
}

tb_ is the IMAPITable object. Please note we rebuild the integer array and set the first member as the count of the original array.

QueryRows

The QueryRows function returns one or more rows from a table, beginning at the current cursor position.

C++
HRESULT QueryRows(int lRowCount, uint ulFlags, out IntPtr lppRows);

We get an integer pointer after call QueryRows. But what’s the content the pointer points to?

This pointer points to a SRowSet structure. Before we use Marshal to parse the memory block, we need to know the data format.

Look at the definition in C++.

C++
typedef struct _SRowSet
{
  ULONG cRows;
  SRow aRow[MAPI_DIM];
} SRowSet;

First we can get the length of the row collections.

We assume pRowSet is the integer pointer which points to the SRowSet.

C#
uint cRows = (uint)Marshal.ReadInt32(pRowSet);

Then we need check what SRow is.

C#
typedef struct _SRow
{
  ULONG ulAdrEntryPad;
  ULONG cValues;
  LPSPropValue lpProps;
} SRow;

We assume pRow is the integer pointer which points to SRow.

C#
uint cValues = (uint)Marshal.ReadInt32(pRow + IntPtr.Size)
    IntPtr pProps = Marshal.ReadIntPtr(pRow + IntPtr.Size + Marshal.SizeOf(typeof(Int32))
);

Now we can get each SPropvalue from the pProps integer pointer.

C#
for (int j = 0; j < cValues; j++) 
{
    SPropValue lpProp = (SPropValue)Marshal.PtrToStructure(pProps + 
       j * Marshal.SizeOf(typeof(SPropValue)), typeof(SPropValue));
}

Retrieve message store table to get all message stores

After we logon to a MAPI session, the very first task is to retrieve message stores in this session. Then we can decide which store needs to open.

Go back to the IMAPISession definition. There is a function to open the message store table.

C#
HRESULT OpenMsgStore(uint ulUIParam, uint cbEntryID, 
        IntPtr lpEntryID, IntPtr lpInterface, uint ulFlags, out IntPtr lppMDB);

By this function, we can get the IMAPITable object.

C#
IntPtr pTable = IntPtr.Zero;
session_.GetMsgStoresTable(0, out pTable);
IMAPITable tableObj = Marshal.GetObjectForIUnknown(pTable) as IMAPITable; 
MAPITable mb = new MAPITable(tableObj);

Then set the columns. What we need to retrieve is the store name and if it’s a default store or not.

C#
mb.SetColumns(new PropTags[] { PropTags.PR_DISPLAY_NAME, PropTags.PR_DEFAULT_STORE })

Do the query.

C#
while (mb.QueryRows(1, out sRows))
{
    if (sRows.Length != 1)
        break;
    string storeName = sRows[0].propVals[0].AsString;
    bool isDefault = sRows[0].propVals[1].AsBool;
}

WPF New Mail Notification

Now we do a WPF sample to show how easy it is to use the Managed MAPI in a .NET application.

This WPF sample application will sit in the system tray. When a new mail arrives in the specified message store, a balloon tip will display.

C#
public MainWindow()
{
    InitializeComponent();
    this.Hide();
    ni_ = new System.Windows.Forms.NotifyIcon();
    ni_.Icon = Properties.Resources.NewMailNotification;
    ni_.Visible = true;
}

Image 2

Context Menu

We add a context menu for this notification icon, one is “setting” to open the specified message store. The other is “exit” to close this application.

C#
contextMenu_ = new System.Windows.Forms.ContextMenu();
contextMenu_.MenuItems.Add("Settings", new EventHandler(OnSettingsClick));
contextMenu_.MenuItems.Add("Exit", new EventHandler(OnExitClick));
ni_.ContextMenu = contextMenu_;

Image 3

Settings

A settings dialog will pop up all stores in the current MAPI session. You can select one of them to open and register message store notifications.

Image 4

Using the MVVM Pattern

The Model-View-ViewModel pattern is a simple and effective set of guidelines for designing and implementing a WPF application. It allows you to create a strong separation between data, behavior, and presentation, making it easier to control the chaos that is software development.

MAPISessionView Model

Most ViewModel classes need the same features. They often need to implement the INotifyPropertyChanged interface, they usually need to have a user-friendly display name.

C#
public event PropertyChangedEventHandler PropertyChanged;

public void NotifyPropertyChanged(string PropertyName)
{
    if (PropertyChanged != null)
        PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}

Create a MAPISession in MAPISessionViewModel

C#
session_ = new MAPISession();

MAPISessionViewModel contains an observable collection of MessageStoreobjects, called Stores.

We get all stores from the MAPISession object.

C#
session_.GetMessageStores();

Then add them to the collection. The settings view contains a ListBox whose ItemsSource property is bound to that collection.

C#
<ListBox Grid.Row="2" Grid.ColumnSpan="2"
           Name="lbStore"
           ItemsSource="{Binding Path=Stores}"
           ItemTemplate="{StaticResource storeTemplate}"
           SelectedItem="{Binding Path=SelectedStore, Mode=TwoWay}">
</ListBox>

Each list box item has a label whose content property is bound to the corresponding MessageStore object, and a green tick image if this store is the default store.

C#
<DataTemplate x:Key="storeTemplate" DataType="{x:Type local:MessageStore}" >
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="30"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="300"/>
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <TextBlock Text="{Binding Path=Name}" FontSize="12" VerticalAlignment="Center" />
        <Image Grid.Column="1" VerticalAlignment="Center" Stretch="None">
            <Image.Style>
                <Style TargetType="{x:Type Image}">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding Path=Default}" Value="True">
                               <Setter Property="Source" Value="..\Resources\default.png"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </Image.Style>
        </Image>
       </Grid>
</DataTemplate>

Applying a View to a ViewModel

You can easily tell WPF how to render a View by using a typed static resource.

C#
<Window x:Class="NewMailNotification.SettingsWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:NewMailNotification.ViewModel"
        xmlns:vw="clr-namespace:NewMailNotification.View"
        Title="SettingsWindow" Height="400" Width="400">
    <Window.Resources>
        <vm:MAPISessionViewModel x:Key="ViewModel"/>
    </Window.Resources>
    <Grid>
        <vw:SettingsView DataContext="{Binding Source={StaticResource ViewModel}}"/>
    </Grid>
</Window>

Conclusion

In this article, we go through how to implement a managed MAPI by using the MAPI common interface. Also we do a very simple WPF sample application to show how easy it is to use the Managed MAPI. In the next article, we will see how to open a message store and get the new mail notification.

License

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


Written By
Software Developer (Senior)
Australia Australia
Fred is a senior software developer who lives in Melbourne, Australia. In 1993, he started Programming using Visual C++, Visual Basic, Java, and Oracle Developer Tools. From 2003, He started with .Net using C#, and then expertise .Net development.

Fred is often working with software projects in different business domains based on different Microsoft Technologies like SQL-Server, C#, VC++, ASP.NET, ASP.Net MVC, WCF,WPF, Silverlight, .Net Core and Angular, although he also did some development works on IBM AS400.

Comments and Discussions

 
QuestionSend email and get address Pin
Zhi Chen23-Apr-14 9:08
Zhi Chen23-Apr-14 9:08 
QuestionWhy do you not have .NET translate the HRESULT for you? Pin
.:floyd:.21-Apr-14 8:02
.:floyd:.21-Apr-14 8:02 
QuestionHow can login to exchange server without using profile? Pin
rajen shrestha22-May-13 22:08
rajen shrestha22-May-13 22:08 
AnswerRe: How can login to exchange server without using profile? Pin
charlieS201223-May-13 14:26
charlieS201223-May-13 14:26 
AnswerRe: How can login to exchange server without using profile? Pin
rajen shrestha26-May-13 17:37
rajen shrestha26-May-13 17:37 
AnswerRe: How can login to exchange server without using profile? Pin
Fred Song (Melbourne)23-May-13 14:29
mvaFred Song (Melbourne)23-May-13 14:29 
GeneralMy vote of 5 Pin
jh111110-Sep-12 8:09
jh111110-Sep-12 8:09 
GeneralMy vote of 5 Pin
ring_09-Sep-12 18:59
ring_09-Sep-12 18:59 
Nice, It is always nice to see MAPI.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.