Intercepting Mail using IMapiAdviseSink

mailman There is a lot of interesting stuff going on with Windows Mobile right now…unfortunately, I can’t talk about much of it (yet).  Hang in there…eventually the “hush” orders will be lifted and the posts will start. 

I owe you guys some cool stuff NOW though and you have been really patient waiting for some dev posts.  Go ahead, say it.. “You OWE US”.   I know…I do.  I have been in the dark cave at Microsoft we call “figure out what you can realistically do in a day and double that.”  I’m out for air and I’m digging back in to my bag of tricks for some stuff we can talk about NOW. Let’s see…

Here’s one that comes up from time to time with no sample I am aware of…how to programmatically intercept mail.  Intercepting SMS message is pretty easy.  We’ve had support for this back in the via SDK MapiRule Sample and then added support in NETCF to simple add an event handler.  Wow, that was easy.

So the next question becomes, how do I intercept email messages programmatically to do something cool?  What if I want to play a special ringtone or kick off some automated process for workflow.  It should be really easy, like SMS right? 

The NETCF guys never added a no-brainer message handler for email like they did for SMS, so you actually have to do a little work…in native code.  If you are on NETCF 3.5 and using Exchange, you could build something around WCF like David did with the Lunch Launcher.  If you are targeting the consumer market though, not everyone is using Exchange and you would really need an approach that had zero dependencies on the back-end.  MAPI provides this and to get it done, you just need to plug into IMapiAdviseSink.

I’m always a little stand-offish at SDK docs that refer me to some COM object that I’m supposed to wire up with no example…so let’s hack it out.

First we build a simple COM object that inherits from IMAPIAdviseSink and then implements OnNotify. When OnNotify fires, we simply look for the fnevObjectCreated event to let us know that a new message has just been added and pull out the goodies.

The COM object part is standard stuff, so here’s what OnNotify might look like:

STDMETHODIMP_(ULONG) CAdviseSink::OnNotify (ULONG cNotify, LPNOTIFICATION lpNotifications)
{
HRESULT   hRes = S_OK;
 
if (!m_hWndParent) return 0;
 
// Advise events fire on a number of different events, so be sure you look for the MAPI_MESSAGE+object creation event
// and take some action when one arrives.
try
{
  for (ULONG i=0 ; i<cNotify ; i++)
  {
   hRes = S_OK;
   if (fnevObjectCreated == lpNotifications[i].ulEventType)
   {
    if ( lpNotifications[i].info.obj.ulObjType == MAPI_MESSAGE ) // Found a message, so grab it
    {
     // New message has arrived, so take some action...
     
     // The event can fire before the message is ready to read, so I inject a slight delay here to give the system
     // a second or two to process the new message before I try to read it. 
     Sleep(1500);

     // Process the new message
     GetMsg(&lpNotifications[i]);
    }
   }
  }
}
catch(...)
{
  MessageBox(NULL,L"AdviseFail", L"AdviseFail", MB_OK );
}
return 0;
}

What you do with the message is up to you.  I created a very simple GetMsg routine to throw a MessageBox with the message subject as the title.

void GetMsg(NOTIFICATION *pNote)
{
   HRESULT             hr;
   IMessage        *   pmsg        =   NULL;
   ULONG lType=0,val=0;
   ULONG  subjectprop[]    =   { 1, PR_SUBJECT };
   LPSPropValue subjectpropval;

   // Get the message stores table
   hr = g_pSession->OpenEntry(pNote->info.obj.cbEntryID, pNote->info.obj.lpEntryID,0,0,&lType,(IUnknown **)&pmsg);
   ASSERT(pmsg);

   // Double Check the props on the message for finer control of which types of messages/folders you want to handle
   // this example traps every new message...
   hr = pmsg->GetProps((LPSPropTagArray)subjectprop,MAPI_UNICODE,&val,&subjectpropval);
   if ( hr == S_OK )
   {
// *** DO SOMETHING HERE ***
// example: popup a message box with subject info, play some cool ringtone based on sender, etc.
if ( subjectpropval->Value.lpszW )
  MessageBox(NULL,subjectpropval->Value.lpszW, L"Message Intercepted", MB_OK | MB_TOPMOST | MB_SETFOREGROUND);
   }
   RELEASE_OBJ(pmsg);

   return;
}

From our main application, now we just do a simple MAPILogon, create our class, and hand it to the Advise method which tells it to listen for any newly created MAPI objects.

   hr = MAPILogonEx(NULL, NULL, NULL, NULL, &g_pSession);
   g_pSink = new CAdviseSink(hWnd,0);
   hr = g_pSession->Advise(0,NULL,fnevObjectCreated,g_pSink,&lConn);

That’s pretty much it.  I’ll attach the sample project for reference to this post if you want to play with it or see the working app.  Keep in mind, this sample is really basic and does no filtering.  That means if I send a message to myself, it fires twice!  Why?  Because a message is created when I author it, and another is created when I receive it.  You get a message on EVERY new message object.  In the real world, you would want to do some filtering.  You could modify the GetMsg routine to filter on sender, folder, etc. to make it “smarter”.  It’s up to you… go get creative.

Cheers,
Reed

This blog entry brought to you by Windows Mobile and 3G Internet Connection Sharing…because a PC shouldn’t be useless just because the WiFi is.

MapiMon.zip