Introduction
We continue with the previous article to
discuss how to get new mail notification from a MAPI Message Store.
Message Store
Message store providers handle the storage and retrieval of messages and other information for the users of client
applications. The message information is organized by using a hierarchical system known as a message store. The message store is implemented in multiple
levels, with containers called folders holding messages of different types. There is no limit to the number of levels in a message store; folders can contain many subfolders.
Declare the IMsgStore Interface in C#
This interface provides access to message store information and to messages and folders.
[
ComImport, ComVisible(false),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
Guid("00020306-0000-0000-C000-000000000046")
]
public interface IMsgStore
{
HRESULT GetLastError(int hResult, uint ulFlags, out IntPtr lppMAPIError);
HRESULT SaveChanges(uint ulFlags);
HRESULT GetProps([In, MarshalAs(UnmanagedType.LPArray)] uint[] lpPropTagArray,
uint ulFlags, out uint lpcValues, ref IntPtr lppPropArray);
HRESULT GetPropList(uint ulFlags, out IntPtr lppPropTagArray);
HRESULT OpenProperty(uint ulPropTag, ref Guid lpiid, uint ulInterfaceOptions, uint ulFlags, out IntPtr lppUnk);
HRESULT SetProps(uint cValues, IntPtr lpPropArray, out IntPtr lppProblems);
HRESULT DeleteProps(IntPtr lpPropTagArray, out IntPtr lppProblems);
HRESULT CopyTo(uint ciidExclude, ref Guid rgiidExclude,
[In, MarshalAs(UnmanagedType.LPArray)] uint[] lpExcludeProps, IntPtr ulUIParam,
IntPtr lpProgress, ref Guid lpInterface, IntPtr lpDestObj, uint ulFlags, IntPtr lppProblems);
HRESULT CopyProps(IntPtr lpIncludeProps, uint ulUIParam, IntPtr lpProgress, ref Guid lpInterface,
IntPtr lpDestObj, uint ulFlags, out IntPtr lppProblems);
HRESULT GetNamesFromIDs(out IntPtr lppPropTags, ref Guid lpPropSetGuid, uint ulFlags,
out uint lpcPropNames, out IntPtr lpppPropNames);
HRESULT GetIDsFromNames(uint cPropNames, ref IntPtr lppPropNames, uint ulFlags, out IntPtr lppPropTags);
[PreserveSig]
HRESULT Advise(uint cbEntryID, IntPtr lpEntryID, uint ulEventMask,
[In, MarshalAs(UnmanagedType.Interface)] IMAPIAdviseSink pAdviseSink, out uint lpulConnection);
[PreserveSig]
HRESULT Unadvise(uint ulConnection);
[PreserveSig]
HRESULT CompareEntryIDs(uint cbEntryID1, IntPtr lpEntryID1, uint cbEntryID2,
IntPtr lpEntryID2, uint ulFlags, out bool lpulResult);
[PreserveSig]
HRESULT OpenEntry(uint cbEntryID, IntPtr lpEntryID, IntPtr lpInterface,
uint ulFlags, out uint lpulObjType, out IntPtr lppUnk);
[PreserveSig]
HRESULT SetReceiveFolder(string lpszMessageClass, uint ulFlags, uint cbEntryID, IntPtr lpEntryID);
[PreserveSig]
HRESULT GetReceiveFolder([MarshalAs(UnmanagedType.LPWStr)]string lpszMessageClass, uint ulFlags,
out uint cbEntryID, out IntPtr lppEntryID, [MarshalAs(UnmanagedType.LPWStr)]StringBuilder lppszExplicitClass);
[PreserveSig]
HRESULT AbortSubmit(uint cbEntryID, IntPtr lpEntryID, uint ulFlags);
}
Open Message Store
Now we have a MAPI Session object already.
But how do we open a message store to access messages and folders?
In the previous article, we knew how to get the
message store table and retrieve properties we are interested in. Now we need to retrieve the entry ID property and pass this entry ID to the IMsgStore.OpenMessageStore
method.
First we get the message store table.
IntPtr pTable = IntPtr.Zero;
session_.GetMsgStoresTable(0, out pTable);
if (pTable != IntPtr.Zero)
{
object tableObj = null;
tableObj = Marshal.GetObjectForIUnknown(pTable);
content_ = new MAPITable(tableObj as IMAPITable);
}
Second we get the entry ID of the message store with the specified
name. If you don’t specify the name, the default store is returned.
if (Content.SetColumns(new PropTags[] { PropTags.PR_DISPLAY_NAME,
PropTags.PR_ENTRYID, PropTags.PR_DEFAULT_STORE }))
{
SRow[] sRows;
while (Content.QueryRows(1, out sRows))
{
if (sRows.Length != 1)
break;
if (string.IsNullOrEmpty(storeName))
{
if (sRows[0].propVals[2].AsBool)
bResult = true;
}
else if (sRows[0].propVals[0].AsString.IndexOf(storeName) > -1)
bResult = true;
if (bResult)
break;
}
}
Last we open the store with the entry ID.
IntPtr pStore = IntPtr.Zero;
if (session_.OpenMsgStore(0, entryId.cb, entryId.lpb, IntPtr.Zero,
(uint)MAPIFlag.BEST_ACCESS, out pStore) == HRESULT.S_OK)
{
if (pStore != IntPtr.Zero)
{
IMsgStore msgStore = Marshal.GetObjectForIUnknown(pStore) as IMsgStore;
CurrentStore = new MessageStore(this, msgStore, new EntryID(entryId.AsBytes), storeName);
}
}
EntryID class
Although we’ve introduced the SBinary
structure
in the previous article, it’s not easy enough to use. So now we introduce a new
EntryID
class to represent the MAPI entry identifier.
public class EntryID
{
const int DefaultBufferSize = 256;
private byte[] id_;
public EntryID(byte[] id)
{
id_ = id;
}
public byte[] AsByteArray { get { return this.id_; } }
public static EntryID BuildFromPtr(uint cb, IntPtr lpb)
{
byte[] b = new byte[cb];
for (int i = 0; i < cb; i++)
b[i] = Marshal.ReadByte(lpb, i);
return new EntryID(b);
}
public static EntryID GetEntryID(string entryID)
{
if (string.IsNullOrEmpty(entryID))
return null;
int count = entryID.Length / 2;
StringBuilder s = new StringBuilder(entryID);
byte[] bytes = new byte[count];
for (int i = 0; i < count; i++)
{
if ((2 * i + 2) > s.Length)
return null;
string s1 = s.ToString(2 * i, 2);
if (!Byte.TryParse(s1, System.Globalization.NumberStyles.HexNumber,
null as IFormatProvider, out bytes[i]))
return null;
}
return new EntryID(bytes);
}
public override string ToString()
{
StringBuilder s = new StringBuilder(DefaultBufferSize);
foreach (Byte b in id_)
{
s.Append(b.ToString("X2"));
}
return s.ToString();
}
}
Conversion between SBinary and EntryID
We use EntryID
at a higher level than
SBinary
. But we cannot pass EntryID
to a MAPI interface or an API function. We
need to convert EntryID
to SBinary
or vice versa.
SBinary sb = SBinary.SBinaryCreate(entryId.AsByteArray);
EntryID = new EntryID(sbinary.AsBytes);
Event Notification in MAPI
Event notification is the communication of
information between two MAPI objects. Through one of the objects, a client or
service provider registers for notification of a change or error, called an
event, which may take place in the other object. After the event occurs, the
first object is notified of the change or error. The object receiving the
notification is called the advise sink; the object responsible for the
notification is called the advise source.
Advise sinks are typically implemented by
client applications to receive address book and message store notifications and
support the IMAPIAdviseSink : IUnknown
interface. IMAPIAdviseSink
contains a
single method, IMAPIAdviseSink::OnNotify
.
Message store and address book providers
usually support object notifications on several of their objects and table
notifications on their contents and hierarchy tables. Transport providers do
not support notifications directly; they rely on alternative methods of
communication with clients.
Clients that implement advise sink objects
call Advise
when they want to register for a notification, in most cases
passing in the entry identifier of the object with which registration should
occur, and Unadvise
when they want to cancel the registration. Clients pass a
parameter to Advise
that indicates which of the several types of events they
want to monitor. Advise
returns a nonzero number that represents a successful
connection between the advise sink and the advise source.
When an event for which a client has
registered occurs, the advise source notifies the advise sink by calling its
IMAPIAdviseSink::OnNotify
method with a notification data structure that
contains information about the event. An advise sink's implementation of
OnNotify
can perform tasks in response to the notification, such as updating
data in memory or refreshing a screen display.
Define IMAPIAdviseSink interface in C#
The IMAPIAdviseSink
interface is used to
implement an Advise Sink object for handling notifications.
[
ComImport, ComVisible(false),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
Guid("00020302-0000-0000-C000-000000000046")
]
public interface IMAPIAdviseSink
{
HRESULT OnNotify(uint cNotify, IntPtr lpNotifications);
}
Implement the IMAPIAdviseSink Interface
We have to create our own advise sink
object to receive the message store event notification.
delegate void OnAdviseCallbackHandler(IntPtr pContext, uint cNotification, IntPtr lpNotifications);
class MAPIAdviseSink : IMAPIAdviseSink
{
OnAdviseCallbackHandler callbackHandler_;
IntPtr pContext_;
public MAPIAdviseSink(IntPtr pContext, OnAdviseCallbackHandler callbackHandler)
{
pContext_ = pContext;
callbackHandler_ = callbackHandler;
}
public HRESULT OnNotify(uint cNotify, IntPtr lpNotifications)
{
if (callbackHandler_ != null)
callbackHandler_(pContext_, cNotify, lpNotifications);
return HRESULT.S_OK;
}
}
Advise/ UnAdvise Message Store New Mail Notification
To register for notification, a pointer to
an Advise Sink object is passed in a call to a service provider's
IMAPISession::Advise
method.
Let’s review the advise and unadvised methods definition in the IMsgStore
interface.
HRESULT Advise(uint cbEntryID, IntPtr lpEntryID, uint ulEventMask,
[In, MarshalAs(UnmanagedType.Interface)] IMAPIAdviseSink pAdviseSink, out uint lpulConnection);
HRESULT Unadvise(uint ulConnection);
Then we advise the message store new mail
notification with the below code.
public bool RegisterEvents(EEventMask eventMask)
{
callbackHandler_ = new OnAdviseCallbackHandler(OnNotifyCallback);
HRESULT hresult = HRESULT.S_OK;
try
{
pAdviseSink_ = new MAPIAdviseSink(IntPtr.Zero, callbackHandler_);
hresult = MAPIStore.Advise(0, IntPtr.Zero, (uint)eventMask, pAdviseSink_, out ulConnection_);
}
catch (Exception ex)
{
System.Diagnostics.Trace.WriteLine(ex.Message);
return false;
}
return hresult == HRESULT.S_OK;
}
Call Unadvise
as the below.
MAPIStore.Unadvise(ulConnection_);
What’s EEventMask?
Advise
establishes a connection between the
caller's advise sink object and either the message store or an object in the
message store. This connection is used to send notifications to the advise sink
when one or more events, as specified in the ulEventMask
parameter, occur to
the advise source object.
The EEventMask
is a bitmask composed of one
or more of the flags.
public enum EEventMask : uint
{
fnevCriticalError = 0x00000001,
fnevNewMail = 0x00000002,
fnevObjectCreated = 0x00000004,
fnevObjectDeleted = 0x00000008,
fnevObjectModified = 0x00000010,
fnevObjectMoved = 0x00000020,
fnevObjectCopied = 0x00000040,
fnevSearchComplete = 0x00000080,
fnevTableModified = 0x00000100,
fnevStatusObjectModified = 0x00000200,
fnevReservedForMapi = 0x40000000,
fnevExtended = 0x80000000,
}
On Notfiy Callback function
Defines a callback function that MAPI calls
to send an event notification. This callback function can only be used when
wrapped in an advise sink object created.
The below is the C++ notify callback
function definition:
ULONG (STDAPICALLTYPE NOTIFCALLBACK)(
LPVOID lpvContext,
ULONG cNotification,
LPNOTIFICATION lpNotifications
);
We convert it to C#:
void OnNotifyCallback(IntPtr pContext, uint cNotification, IntPtr lpNotifications);
cNotification
: Count of event notifications
in the array indicated by the lpNotifications
parameter. lpNotifications
: Pointer to the location
where this function writes an array of notifications structures that contain
the event notifications.
Parsing a Notification Structure from Integer Pointer
The NOTIFICATION
structure contains
information about an event that has occurred and the data that has been
effected by the event.
Notification structure definition in C++:
struct {
ULONG ulEventType;
union {
ERROR_NOTIFICATION err;
NEWMAIL_NOTIFICATION newmail;
OBJECT_NOTIFICATION obj;
TABLE_NOTIFICATION tab;
EXTENDED_NOTIFICATION ext;
STATUS_OBJECT_NOTIFICATION statobj;
} info;
} NOTIFICATION, FAR *LPNOTIFICATION;
In this article, we only discuss new mail
notification, so we focus on the NEWMAIL_NOTIFICATION
structure. The NEWMAIL_NOTIFICATION
structure
describes information relating to the arrival of a new message.
Here is the NEWMAIL_NOTIFICATION
structure definition
in C++:
struct {
ULONG cbEntryID;
LPENTRYID lpEntryID;
ULONG cbParentID;
LPENTRYID lpParentID;
ULONG ulFlags;
LPTSTR lpszMessageClass;
ULONG ulMessageFlags;
} NEWMAIL_NOTIFICATION;
We convert this definition to C# as below:
[StructLayout(LayoutKind.Sequential)]
public struct NEWMAIL_NOTIFICATION
{
public uint cbEntryID;
public IntPtr pEntryID;
public uint cbParentID;
public IntPtr pParentID;
public uint Flags;
public IntPtr MessageClass;
public uint MessageFlags;
}
According to the definition of the NOTIFICATION
structure, we first extract an integer to determine the event type. Then we extract
the relevant structure per the event type. Shown below is the code:
EEventMask eventType = (EEventMask)Marshal.ReadInt32(lpNotifications);
int intSize = Marshal.SizeOf(typeof(int));
IntPtr sPtr = lpNotifications + intSize * 2;
switch (eventType)
{
case EEventMask.fnevNewMail:
{
Console.WriteLine("New mail");
if (this.OnNewMail == null)
break;
NEWMAIL_NOTIFICATION notification =
(NEWMAIL_NOTIFICATION)Marshal.PtrToStructure(sPtr, typeof(NEWMAIL_NOTIFICATION));
}
break;
}
Message Store NewMail Event and MsgStoreNewMailEventArgs
When we get the MAPI notification, it’s
ready to fire a Message Store NewMail event.
First we create MsgStoreNewMailEventArgs
per NEWMAIL_NOTIFICATION
. When marshalling an integer pointer to a string, we
use different Marshal
functions for ANSI and Unicode. Fortunately, the
notification flag tells us if MAPI is using ANSI or Unicode. That’s what we do
to get the message class string from the notification structure.
public class MsgStoreNewMailEventArgs : EventArgs
{
public EntryID StoreID { get; private set; }
public EntryID EntryID { get; private set; }
public EntryID ParentID { get; private set; }
public int MessageFlags { get; private set; }
public string MessageClass { get; private set; }
public MsgStoreNewMailEventArgs(EntryID storeID, NEWMAIL_NOTIFICATION notification)
{
StoreID = storeID;
SBinary sbEntry = new SBinary() { cb = notification.cbEntryID, lpb = notification.pEntryID };
SBinary sbParent = new SBinary() { cb = notification.cbParentID, lpb = notification.pParentID };
EntryID = sbEntry.cb > 0 ? new EntryID(sbEntry.AsBytes) : null;
ParentID = sbParent.cb > 0 ? new EntryID(sbParent.AsBytes) : null;
MessageFlags = (int)notification.MessageFlags;
if ((notification.Flags & (uint)CharacterSet.UNICODE) == (uint)CharacterSet.UNICODE)
MessageClass = Marshal.PtrToStringUni(notification.MessageClass);
else
MessageClass = Marshal.PtrToStringAnsi(notification.MessageClass);
}
}
Fire the Message Store OnNewMail
event:
MsgStoreNewMailEventArgs n = new MsgStoreNewMailEventArgs(StoreID, notification);
if (this.OnNewMail != null)
this.OnNewMail(this, n);
Message StoreInfo class
Although we can get all the information from the
MessageStore
class, we need a light class to store the basic information of a
message store.
public class StoreInfo : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
MAPISession session_;
public StoreInfo(MessageStore store)
{
session_ = store.Session;
Name = store.Name;
EntryId = store.StoreID;
}
public StoreInfo(MAPISession session, string name, EntryID storeId)
{
session_ = session;
Name = name;
EntryId = storeId;
}
public string Name { get; private set; }
public EntryID EntryId { get; private set; }
public bool IsDefault
{
get
{
if (session_ == null)
return false;
return Equals(session_.DefaultStore);
}
}
public bool IsOpened
{
get
{
if (session_ == null)
return false;
return Equals(new StoreInfo(session_.CurrentStore));
}
}
public override bool Equals(object obj)
{
if (obj is StoreInfo && session_ != null)
{
StoreInfo st = obj as StoreInfo;
if (session_ != st.session_)
return false;
return session_.CompareEntryIDs(EntryId, st.EntryId);
}
return base.Equals(obj);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
public void NotifyPropertiesChanged()
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("IsDefault"));
PropertyChanged(this, new PropertyChangedEventArgs("IsOpened"));
}
}
}
Because the StoreInfo
class only has the Name
and EntryId
properties, how do we determine if two StoreInfo
objects represent
the same message store? Don’t assume we can compare the EntryID
s, because different
objects may have the same entry ID in different sessions. We must use the
CompareEntryID
s method in the IMAPISession
object to check if two entry
IDs are the
same.
public bool CompareEntryIDs(EntryID entryid1, EntryID entryid2)
{
SBinary sb1 = SBinary.SBinaryCreate(entryid1.AsByteArray);
SBinary sb2 = SBinary.SBinaryCreate(entryid2.AsByteArray);
bool result;
session_.CompareEntryIDs(sb1.cb, sb1.lpb, sb2.cb, sb2.lpb, 0, out result);
SBinary.SBinaryRelease(ref sb1);
SBinary.SBinaryRelease(ref sb2);
return result;
}
WPF new mail notification application
Subscribe to Message Store OnNewMail event
We subscribe to the message store OnNewMail
event
when opening a message store.
public bool OpenMessageStore(string storeName)
{
if (session_.CurrentStore != null)
{
session_.CurrentStore.UnRegisteEvents();
session_.CurrentStore.OnNewMail -= new EventHandler<MsgStoreNewMailEventArgs>(OnNewMail);
}
bool ret = session_.OpenMessageStore(storeName);
if (ret)
{
session_.CurrentStore.RegisterEvents(EEventMask.fnevNewMail);
session_.CurrentStore.OnNewMail += new EventHandler<MsgStoreNewMailEventArgs>(OnNewMail);
}
return ret;
}
When the message store OnNewMail
event is fired,
show the balloon tips:
private void OnNewMail(object sender, MsgStoreNewMailEventArgs e)
{
ni_.BalloonTipText = "You get a new mail";
ni_.ShowBalloonTip(500);
}
ni_
is a NotifyIcon
that we introduced in the
previous article.
ni_ = new System.Windows.Forms.NotifyIcon();
Handle multiple Message Stores
Sometimes there are multiple message stores
in a MAPI profile. Although we open the default message store from the start, you
can open another store and get its new mail notification from the Settings
window.
The red flag means the currently opened
store. We use the open command to open the selected message store.
OpenStore command
Commands in MVVM are defined using the ICommand
interface which basically defines methods for determining whether a
command can be fired, and what it does when you do.
class OpenCommand : ICommand
{
private SettingsViewModel viewModel_;
public OpenCommand(SettingsViewModel viewModel)
{
viewModel_ = viewModel;
}
public bool CanExecute(object parameter)
{
if (viewModel_.SelectedStore.IsOpened)
return false;
return true;
}
public void Execute(object parameter)
{
SessionSingleton.Instance.OpenMessageStore(viewModel_.SelectedStore.Name);
viewModel_.NotifyStoreInfoPropertiesChanged();
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
The Execute
method is what the command is doing. In this case, it opens a message store and notifies GUI refresh.
The
CanExecute
method determines if the command can be fired. In this case, if the selected store has been opened already, the open command cannot be fired.
Then how about the CanExecuteChanged
event?
This event is raised by the command to notify its consumers that its CanExecute
property may have changed.
In general, this event simply exposes the CommandManager.RequerySuggested
event. The RequerySuggested
event is fired quite often, as focus is moved,
text selection is changed.
Define an Open Store command member in the ViewModel.
public OpenCommand OpenStore { get; private set; }
Binding the OpenCommand with the Open button in XAML
<Button Grid.Row="2" HorizontalAlignment="Left" Command="{Binding Path=OpenStore}">
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid>
<Grid.Resources>
<Style x:Key="CommandText" TargetType="{x:Type TextBlock}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#FF448DCA" />
<Setter Property="Foreground" Value="White" />
</Trigger>
</Style.Triggers>
</Style>
</Grid.Resources>
<DockPanel x:Name="imageContainer"
HorizontalAlignment="Center" DockPanel.Dock="Left">
<Image x:Name="image" VerticalAlignment="Center"
Source="..\Resources\openfile.png" Width="16" Height="16" />
<TextBlock Style="{StaticResource CommandText}"
VerticalAlignment="Center" Margin="2,0,0,0">Open</TextBlock>
</DockPanel>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Button.Style>
</Button>
Conclusion
In this article, we introduced how to register and unregister MAPI notification from a message store. Actually we
can even go further to show more details of the new mail message, like subject, from address, and so on, because we have the entry
ID from
MsgStoreNewMailEventArgs
. This New Mail Notification application works pretty well for Outlook Exchange accounts. For POP3, IMAP accounts, it
only works if Outlook is running. With the Exchange provider, send/receive isn’t really needed. POP3, IMAP providers depend on Outlook running in order
to perform send/receive operations. So it’s not that you’re not getting notifications – the mail isn’t arriving at all. We can solve this problem by
implementing our own Transport Provider. But that’s not what we discussed here.
A while ago when I looked at the MAPI stuff, a lot of people told me you couldn’t do it in pure .NET level. Although I’m
pretty happy with my C++ skills, I don’t like a .NET application being dependant on an unmanaged DLL. It’s not a good architecture. That’s why I
wanted to make some
adventures on the managed MAPI and publish these two articles.