This is something I wrote long ago and have never got around to posting. Hopes this helps some poor soul that has to write an Outlook add-in using ATL/C++.
—————————————————————————————————————————————————————————————————
The information in this article pertains to COM Add-ins for Microsoft Outlook 2000, 2002 and 2003. The implementation in this article is done using ATL/C++.
Anyone who has tried building an Outlook add-in using the outlook object model and extended MAPI can tell you that it is in fact a battle. With so little information out there on extended MAPI and not many people willing to tell you about how it works, it is like stepping in front of a cannon and waiting for it to fire. Throughout the article I will provide code samples and insight into the workings of outlook and extended MAPI.
The reason for using extended MAPI is that it will help avoid version compatibility issue, security issues, and has a lot more functionality than the outlook object model does.
Outlook 2000: This is where most of the headache is! The outlook 2000 object model looks very similar to the 2002 and 2003 model but acts very different in action. The biggest downfall of Outlook 2000 is the difficulty in getting the MAPIOBJECT. The MAPIOBJECT is how you get into extended MAPI. To get the MAPIOBJECT (an IMAPISession) MAPILogonEX must be called. The only reliable way to get the MAPIOBJECT is from a Mail_Item which gives you an IMessage. This seems fine and dandy except you can not go anywhere with an IMessage except for the IMessage.
Outlook 2002: The functionality in outlook 2002 is far better than 2000 if you want to work with extended MAPI. Once you get the namespace for “MAPI" you can call the ‘get_MAPIOBJECT’ method to retrieve the MAPIOBJECT (IMAPISession). In 2002 the MAPIOBJECT can also be retrieved from an outlook folder (IMAPIFolder). As in Outlook 2000 the MAPIOBJECT can also be retrieved form a Mail_Item. The draw back of getting the MAPIOBJECT from namespace is that the function for sending email straight from within IE, Word, etc is gone. More on this later.
Outlook 2003: Definitely the easiest outlook to work against. A lot of the same functionality exists in 2003 that existed in 2002, but it just works a lot better and is more dependable. An add-in made specifically for outlook 2003 will have some interesting behavior in 2002. Like I said, all the functionality is there but just does not work quit the same.
Making an add-in to work in all three with one set of code (if you are worried about the size) is possible but comes with a lot of hidden rules.
NOTE: If the main target of your add-in is 2000 and earlier it is advised to make an exchange client instead of a COM add-in.
To start building an add-in open Visual studio .NET and choose a shared add-in. Pick Outlook for your add-in choice. The project by default makes a project for the add-in and a setup project. In the project for the add-in there is a class named ‘Connect’. Connect provides the minimum methods needed to implement an add-in. Chances are that these methods are not enough to make an add-in work the way you want it to. Here are some tips on structuring classes for your new add-in:
- Create a class for each type of events the add-in will handle. (ie. ApplicationEvents, ExplorerEvents)
- Create classes to handle simple tasks that will have to be performed. (ie. Creating command bars, creating buttons)
A class could be created that implements an interface that connects to all events outlook has. Chances are that you do not need all the events so here is how to set your add-in to use the classes made to handle each type of events.
In the ‘Connect.h’ add member variables for each class that is going to handle events. The Connect class is always alive as long as outlook is open. In the ‘OnStartUpComplete’ method of Connect.cpp get the application object from outlook and pass it to your ‘ApplicationEvents’class. The application object is needed by the ‘ApplicationEvents’ class to advise and unadvised the events that happen within the realm of the application object.
NOTE: To see what dispinterfaces a typelib has use the OLE Object viewer. This is also the way to get the hex value for the event which is needed for the SINKMAP.
This paradigm is different than all the add-in examples that I have seen to date. The reason for the change in paradigm is simple, maintainability.
The first thing that anyone building an add-in would want to do is catch outlook events. To catch events SINK_MAP’s are used in the header file. Below is a sample header file for the application events.
static _ATL_FUNC_INFO AppInfo ={CC_STDCALL,VT_EMPTY,0,VT_EMPTY};
class CApplication :
public IDispEventSimpleImpl<1,CApplication,&__uuidof(Outlook::ApplicationEvents)>,
public IDispEventSimpleImpl<2,CApplication,&__uuidof(Outlook::ApplicationEvents)>
{
public:
typedef IDispEventSimpleImpl<1,CApplication, &__uuidof(Outlook::ApplicationEvents)> AppQuitEvents;
typedef IDispEventSimpleImpl<2,CApplication, &__uuidof(Outlook::ApplicationEvents)> AppStartEvents;
BEGIN_SINK_MAP(CApplication)
SINK_ENTRY_INFO(1, __uuidof(Outlook::ApplicationEvents), 0xf007, OnApplicationQuit, &AppInfo)
SINK_ENTRY_INFO(2, __uuidof(Outlook::ApplicationEvents), 0xf006, OnApplicationStart, &AppInfo)
END_SINK_MAP()
public:
CApplication(void);
~CApplication(void);
void __stdcall Initialize( CComPtr pApp );
static CComPtr CApplication::GetApplication();
private:
void __stdcall OnApplicationQuit();
void __stdcall OnApplicationStart();
private:
static CComPtr _pApp;
CExplorerHandler _ExplorerHandler;
CInspectorsHandler _InspectorsHandler;
};
First lets look right under the class definition you see:
public IDispEventSimpleImpl<1,CApplication,&__uuidof(Outlook::ApplicationEvents)>,
public IDispEventSimpleImpl<2,CApplication,&__uuidof(Outlook::ApplicationEvents)>
All these say is that the class is deriving from IdispEventSimpleImple.
Next there is:
public:
typedef IDispEventSimpleImpl<1,CApplication, &__uuidof(Outlook::ApplicationEvents)> AppQuitEvents;
typedef IDispEventSimpleImpl<2,CApplication, &__uuidof(Outlook::ApplicationEvents)> AppStartEvents;
These typedef’s are nice because when an Advise or Unadvise is done not as much typing is required. Below is and example of an Advise.
hr = AppQuitEvents::DispEventAdvise( (IDispatch*) _pApp );
Take note of the ‘_pApp’ member variable. Any object that is advised against must not go out of scope.
The next thing to take note of is the code between the BEGIN_SINK_MAP and END_SINK_MAP. Lets brake down the first entry:
SINK _ENTRY_INFO(1, __uuidof(Outlook::ApplicationEvents), 0xf007, OnApplicationQuit, &AppInfo)
- ‘nid = 1’: this is the id to differentiate methods that may be attached to different events
- ‘__uuidof(Outlook::ApplicationEvents)’: all this says is that we are looking for application events
- ‘0xf007’: hex value for the Application quite event (find this by opening the msout.olb with the OLE object viewer.
- ‘OnApplicationQuit’: name of the method that will handle this event.
- &AppInfo: this is the address of AppInfo which is defined as:
static _ATL_FUNC_INFO AppInfo ={CC_STDCALL,VT_EMPTY,0,VT_EMPTY};
- The above says is to use the stdcall convention and it takes no arguments
There better be a declaration for ‘OnApplicationQuit’ since that is where you are telling it to go when the event is fired. You will notice there is.
private:
void __stdcall OnApplicationQuit();
Hopefully now you have a better understanding of what a SINK_MAP is and what it does.
Extended MAPI (IMAPI):
IMAPI has many different objects to be used at your disposal. Covered here will be the most important objects to be able to do basic tasks, like open a folder.
IContainer
First interface to take note of is the IContainer interface. It is very important that users of IMAPI understand that IMAPISession, IMsgStore, IMAPIFolder all implement IContainer. So whenever OpenEntry is used remember you are looking in the container in which you are calling from.
IMAPISession
An IMAPISession is equivalent to an outlook namespace. Unless the only version of office you are using is 2003 it is recommended that MAPILogonEX be used to get the IMAPISesson. Though Outlook 2002 does offer support to get the IMAPISession from the namespace it does not work in all cases. Any time an inspector is opened without the main explorer window open the get_MAPIOBJECT of the namespace will fail. This condition happens when you attempt to email directly form IE, Word, Excel, etc. by using ‘File->Send To->Email recipient’. Below is an example of MAPILogonEx:
hr = MAPILogonEx(0, NULL, NULL, MAPI_ALLOW_OTHERS | MAPI_USE_DEFAULT | MAPI_EXTENDED, &_pSession);
MAPILogonEx will get the current outlook session and put it in _pSession. When done with the session call logoff and release.
_pSession->Logoff( NULL, NULL, 0 );
_pSession.Release();
If the IMAPISession is held onto for to long COM will clean up and it will be gone. The easiest way to combat this problem is to make a class that handles all the IMAPI tasks. Put MAPILogonEX in a class constructor and logoff in a destructor. Only create an instance of the class when you need to use extended MAPI. Using the IMAPISession this way will always leave you in control and not dependent on COM.
Outlook 2000, 2002, 2003 all have a ‘get_MAPIOBJECT’ method for a Mail_Item. This is good if you need to do something with the mail item but you are basically stuck with only that MAPIOBJECT because it does not have an OpenEntry method. Outlook 2000 only has a ‘get_MAPIOBJECT’ method for a mail item while 2002 and 2003 have this method for the namespace and folder as well as the mail item.
Below is a code fragment for getting the MAPIOBJECT from a namespace if using outlook 2003 or later.
CComPtr pnamespace;
hr = pApp->GetNamespace( L"MAPI", &pnamespace );
CComPtr punk;
hr = pnamespace->get_MAPIOBJECT( &punk );
hr = punk->QueryInterface( IID_IMAPISession, (void**)&_pSession );
IMsgStore
The most likely message store that will be worked with is the default message store which is where mail is delivered to. There are three ways to get the default message store.
- - GetMsgStoresTable from the IMAPISession
- Find the row with PR_DEFAULT_STORE = true
- Use the ‘QueryRows’ method of the IMAPITable to get the one row in the table which is the default message store and put into a rowset.
- Use the IMAPISession to OpenEntry with the one row in the rowset
- What was opened is the default message store
CComPtr CIMAPIUtils::GetDefaultMsgStore()
{
HRESULT hr;
CComPtr pIStore = NULL;
if( _pSession )
{
CComPtr lpMapiTable = NULL;
hr = _pSession->GetMsgStoresTable( 0, &lpMapiTable );
CHECK_HRESULTR( hr, "_pSession->GetMsgStoresTable() failed [0x%x]\n", IDS_FATAL_ERROR );
FindMAPITableRow(lpMapiTable, BOOKMARK_BEGINNING);
LPSRowSet lprowSet;
hr = lpMapiTable->QueryRows( 1, TBL_NOADVANCE, &lprowSet );
CHECK_HRESULTR( hr, "lpMapiTable->QueryRows() failed [0x%x]\n", IDS_FATAL_ERROR );
pIStore = GetIStoreFromRow( &lprowSet->aRow[0] );
FreeProws( lprowSet );
}
return pIStore;
}
void CIMAPIUtils::FindMAPITableRow(CComPtr pMapiTable, BOOKMARK pBookMark)
{
HRESULT hr;
SPropValue pv;
pv.ulPropTag = PR_DEFAULT_STORE;
pv.Value.b = TRUE;
SPropertyRestriction pr;
pr.relop = RELOP_EQ;
pr.ulPropTag = PR_DEFAULT_STORE;
pr.lpProp = &pv;
SRestriction r;
r.rt = RES_PROPERTY;
r.res.resProperty = pr;
hr = pMapiTable->FindRow( &r, pBookMark, 0 );
CHECK_HRESULT( hr, " pMapiTable->FindRow() failed [0x%x]\n", IDS_FATAL_ERROR );
}
2. - Use IMAPISession::OpenEntry to open the Inbox (‘Special Folder’)
- Get the PR_PARENT_ENTRYID property from the Inbox
- Use the IMAPISession::OpenEntry with the parent entryid to open the default message store.
3. - From a message get the PR_STORE_ENTRYID (Get the message store that the message is in. If it is in the default message store it will give the default message stores entryid)
- Use IMAPISession::OpenEntry to open the message store with the entryid
/*
* NOTE: Callers of this method must call MAPIFreeBuffer on the return value!!
*/
LPSPropValue CIMAPIUtils::GetStoreEntryID( CComPtr pimessage )
{
LPSPropValue lpProps = NULL;
HRESULT hr;
if( pimessage )
{
hr = HrGetOneProp((LPMESSAGE)pimessage, PR_STORE_ENTRYID, &lpProps);
if( FAILED( hr ) )
return NULL;
}
return lpProps;
}
Once the Message store is retrieved the IMsgStore also has an OpenEntry method that can be used for anything inside of the message store. Really either the IMAPISession or the IMsgStore can be used to open something in the message store. As a best practice the lowest level IContainer should be used to “OpenEntry" (i.e. use the folder to open a message; use the message store to open a folder).
Outlook Object Model (OOM)
The outlook object model has many different parts to it but I will just be touching on a few that are most import when building a COM add-in. This will cover:
- Application
- Explorers Collection
- Inspectors Collection
- Explorer
- Inspector
These five components of the object model are with out a doubt the most widely used pieces of the OOM.
Application Object
The Application object provides many useful events such as:
- OnStart
- OnQuit
- OptionsPagesAdd
It is recommended that any custom class object (i.e. InspectorsHandler class) that is needed for the duration of the Outlook session be instantiated in the ‘OnStart’ event and terminated in the ‘OnQuit’ event. Terminating in the ‘OnQuit’ event is very important to avoid hangs in the shutdown of outlook.
The OptionsPagesAdd is probably what you would expect. This is the event you catch to add an options page to Outlook. This may or may not be something that is desirable depending on what the end goal is of your add-in.
Explorers Collection
The Explorers collection is a bucket where explorer’s live much like the Inspectors collection but different. It is different because when the ‘OnNewExplorer’ event is fired with only a skeleton of the new explorer object. By skeleton I mean that there are no subcollections in the explorer yet, like the command bar collection. Without the existance of these subcollections of the explorer you can not add command bars or buttons at this time. When a new explorer is created a new instance of the ExplorerHandler class should be instantiated. The ‘OnClose’ event lives in the explorer object.
Explorer
To add command bars or buttons to a new explorer many events have to be trapped and then check if the button and command exist on the explorer. An example of events to catch to do this follows:
- Activate
- FolderSwitch
- SelectionChange
- ViewSwitch
NOTE: Active does not get called until focus has been taken away from the explorer and then put back on it.
If you are not concerned about multiple explorers than the active explorer can be used. This explorer is as it says the one that has focus which is the first explorer that is seen when outlook is opened. An example of how to get the active explorer is below.
hr = pApp->ActiveExplorer(&_pExplorer);
pApp is the Outlook application object. The active explorer does contain the subcollections for command bars. So add what you would like.
As mentioned above this is where the ‘OnClose’ event lives. This is where you must clean up, if you do not Outlook will hang on close. There will be a member variable for the explorer you are working with that must be released. Also remember to delete the instance of the class where the explorer member variable lived. The explorer member variable exists because it must not go out of scope if you advise events again it. It is also a good idea to unadvise any events that are associated with the explorer member variable before it is released.
Inspectors Collection
This collections is a bucket for the inspector’s that outlook uses. This collection is user friendly unlike the explorer’s collection. When the ‘OnNewInstpector’ event is fired a complete inspector object does exists. Since the sub collections, like the command bars collection, it is possible to add command bars and button at this point. When a new inspector is created a new instance of the InspectorHandler class should be instantiated. Just like the explorer the ‘OnClose’ event lives in the inspector object.
Inspector
As mentioned above this is where the ‘OnClose’ event lives. This is where you must clean up, if you do not Outlook will hang on close. There will be a member variable for the inspector you are working with that must be released. Also, remember to delete the instance of the class where the inspector member variable lived. The inspector member variable exists because it must not go out of scope if you advise events again it. It is also a good idea to unadvise any events that are associated with the inspector member variable before it is released.
NOTE! Outlook reuses explorers and inspectors. Check if your button or command bar already exists.