Introduction
Recently, I wrote an Outlook2003 COM add-in for one of my clients. While coding the project, I�ve had a lot of issues to which I couldn�t find any solution for C++ especially. I found it quite tricky to code an add-in in ATL for beginners, and so to help others I thought to put down some information, since most of the Office related examples I found on the Internet were VB/VBA related and almost none with ATL.
The code in this article is not really optimistic, and the sample attached with this article may have some memory leaks or some poor COM implementation, but the general approach has been kept simple for the reader to follow. Though I took quite sometime to write this, in case of any errors or bugs, kindly drop me a mail.
Overview
If you are a beginner in COM/ATL, I�ll suggest you read Amit Dey�s article for building an Outlook 2000 add-in.
My article will describe the following techniques:
- Basic Outlook 2003 add-in
- Sinking Explorer Events
- Mixing CDO and Outlook object model
- Using CDO to see if a message has security
- Using CDO to add custom fields to Outlook items
- Programmatically customizing the grouping and sorting of items
- Adding items to right click menu
- How to use MSI to install CDO programmatically
An Office COM add-in has to implement the IDTExtensibility2
interface. The IDTExtensibility2
dispinterface is defined in the MSADDIN Designer typelibrary (MSADDNDR.dll/MSADDNDR.tlb). All COM add-ins inherited from IDTExtensibility2
interface must implement five methods:
OnConnection
OnDisconnection
OnAddinUpdate
OnBeginShutDown
OnStartupComplete
Registering
All of Office add-ins are registerted to the following registry entry, where Outlook is replaced with the application name.
HKEY_CURRENT_USER\Software\Microsoft\Office\Outlook\Addins\<ProgID>
There are some other entries through which add-ins are identified to Outlook.
Getting Started
Let's start by writing a basic functional COM add-in. Then, we�ll move ahead to each step and will create an add-in that will group simple and encrypted emails to different groups.
This article assumes that you are a VC++ COM programmer, and have had some experience with ATL based component development and OLE/Automation, although this is not strictly necessary. The add-in is designed for Outlook 2003, so you must have Office 2003 with CDO installed on your machine. The project code has been built with VC++ 6.0 SP3+/ATL3.0, and tested on WinXP Pro SP1 with Office 2003 installed.
Fire up your VC IDE. Create a new project and chose ATL COM AppWizard as your project type, give it a name Outlook Addin, click OK. Choose Dynamic Link Library as your server type, and click Finish.
Now, Add a New ATL Object using the Insert menu. Select Simple Object and give it a name OAddin, move to Attributes and check the Support ISupportErrorInfo checkbox, and choose the rest of the options as default.
Now, it's time to actually implement the IDTExtensibility2
interface. Right click the COAddin
class, and chose Implement Interface. This will bring up Browse Type library wizard. Select Microsoft Add-in Designer(1.0), and click OK. If it's not listed there, you might have to browse to the folder <drive>/Program Files/Common Files/Designer.
This wizard implements the selected interface and adds a default implementation for each of the five methods of IDTExtensibility2
interface. For now, a basic automation compatible COM object is ready. By adding registry entries to the rgs file, we can register this COM add-in with Outlook. Open your OAddin.rgs file, and embed the following at the end of the file:
HKCU
{
Software
{
Microsoft
{
Office
{
Outlook
{
Addins
{
'OAddin.OAddin'
{
val FriendlyName = s 'SMIME Addin'
val Description = s 'ATLCOM Outlook Addin'
val LoadBehavior = d '00000003'
val CommandLineSafe = d '00000000'
}
}
}
}
}
}
}
The registry entries look pretty simple The LoadBehaviour
tells Outlook when to load this COM add-in; since we want our add-in to be loaded at startup, its value is given as 3. So, you can now build your project and you will be able to see the entry in Outlook as following: (Tools -> Options -> Other -> Advanced Options -> COM Addins):
So, the next step is to sink the Explorer events.
Sinking Explorer Events
Outlook Object Model presents Explorer objects that wrap the functionality of Outlook. These Explorer objects can be used to program Outlook. Explorer objects wrap the CommandBars, Panes, Views, and Folders. For this tutorial, we'll interact with ExplorerEvents
interface to sink the FolderSwitch
event of Active Explorer.
In the Outlook Object Model, the Application
object is at the top of the object hierarchy that represents the entire application. Through its ActiveExplorer
method, we get the Explorer
object that represents the current window of Outlook.
Explorer objects expose an event which is triggered whenever user switches from one folder to other. For this example, we'll only be concerned about folders for Mailbox, and will skip the Contacts, Tasks and Calendars FolderSwitch
events. Before moving to the actual coding, we'll need to import Office and Outlook Type libraries. To do this, open the project's stdafx.h file, and add the following #import
directive.
#import "C:\Program Files\Microsoft Office\Office\mso9.dll" \
rename_namespace( "Office" ) named_guids using namespace Office;
#import "C:\Program Files\Microsoft Office\Office\MSOUTL9.olb"
rename_namespace( "Outlook" ), raw_interfaces_only, \
named_guids using namespace Outlook;
You may need to change these paths according to your OS and Office installation.
Compile the project to import the type libraries.
Now, we'll have to implement the event sink interface that will be called by the source event through connection points.
ATL provides IDispEventImpl<>
and IDispEventSimpleImpl<>
for ATL COM objects. I've used IDispEventSimpleImpl
class to set up a sink map. So, we'll have to derive our COAddin
class from IDispEventSimpleImpl
and setup params using _ATL_SINK_INFO
structure. Setup our sink interface and call DispEventAdvise
and DispEventUnAdvise
to initiate and terminate connection with the source interface.
This is how the code looks:
Derive your class from IDispEventSimpleImpl
.
class ATL_NO_VTABLE COAddin :
public CComObjectRootEx<CComSingleThreadModel>,
...
...
public IDispEventSimpleImpl<1,COAddin,&__uuidof(Outlook::ExplorerEvents)>
_ATL_SINK_INFO
structure is used to describe the callback parameters. Open the header file of the add-in object (
OAddin.h) and add the following line at the very top:
extern _ATL_FUNC_INFO OnSimpleEventInfo;
Open the cpp file of the class and add the following line to the top:
_ATL_FUNC_INFO OnSimpleEventInfo ={CC_STDCALL,VT_EMPTY,0};
Create a callback function:
void __stdcall OnFolderChange();
void __stdcall COAddin::OnFolderChange()
{
MessageBoxW(NULL,L"Hello folder Change Event",L"Outlook Addin",MB_OK);
}
Now, to setup the sink map, we'll use ATL BEGIN_SINK_MAP()
and END_SINK_MAP()
. Each entry will be represented by SINK_ENTRY_INFO
, the dispid
is the event's DISPID. You can find these IDs in type libraries or using Outlook Spy.
BEGIN_SINK_MAP(COAddin)
SINK_ENTRY_INFO(1,__uuidof(Outlook::ExplorerEvents),0xf002
,OnFolderChange,&OnSimpleEventInfo)
END_SINK_MAP()
Now, move to your OnConnection
function which was added when we implemented the interface using the wizard, and modify the code as follows:
STDMETHOD(OnConnection)(IDispatch * Application, ext_ConnectMode ConnectMode,
IDispatch * AddInInst, SAFEARRAY * * custom)
{
omQIPtr <Outlook::_Application> spApp(Application);
TLASSERT(spApp);
_spApp = spApp;
ComPtr<Outlook::_Explorer> spExplorer;
pApp->ActiveExplorer(&spExplorer);
_spExplorer = spExplorer;
RESULT hr = NULL
/Sink Explorer Events to be notified for FolderChange Event
/DispEventAdvise((IDispatch*)spExplorer);
r = ExpEvents::DispEventAdvise((IDispatch*)m_spExplorer,
&__uuidof(Outlook::ExplorerEvents));
}
Till here, we've mapped the FolderSwitch
event of the Explorer
object. Compile the project if everything goes OK. When you change your folder from Inbox to Sent or any other folder, the message box will pop up. If you switch folder Other, then folder Mail, you'll still receive this message box. We'll control this later using our programming logic.
Mixing CDO and Outlook Object Model
CDO (Collaboration Data Objects) is a technology for building messaging or collaboration applications. CDO can be used separately or in connection with Outlook Object Model to gain more access over Outlook.
This example deals with the "Custom Grouping of emails", depending on their nature SMIME (encrypted) or Simple emails. Outlook Object Model doesn't provide any interface to interact with signed and encrypted emails. In fact, if you look into MailItem
object representing an encrypted email, around 80% of its properties and methods are unavailable. We'll use CDO to find out the availability of security on messages.
In order to customize the grouping of emails, the only way is to add custom field 'X' to the MailItem
and force Outlook to group emails according to 'X'. But unfortunately, the "Fields
" property of a signed/encrypted email is not valid, and we can't add any field to it using Outlook Object Model.
On the other hand, CDO is not a part of Outlook Object Model and it doesn't provide any event based functionality nor can you manipulate Outlook objects using CDO. So, to access currently selected folder in CDO, you have to use Outlook Object Model to retrieve the currently selected folder, get its unique identifier, and pass it to CDO to return the folder.
Preparing your code to Use CDO
The very first statement about CDO is "CDO is not installed during the default installation of Office XP and later", and so you've to make sure that your client does have it installed. Moreover, going further down this tutorial will also require you to have CDO installed on your machines. CDO is not available in redistributable form nor can you deploy CDO.dll with your application. CDO is only available in Office CD and can only be installed using the Microsoft Office MSI setup.
There is an example at the end of this tutorial that shows how to invoke MSI programmatically to automatically install the CDO.
We'll use the following CDO objects: Session
, Messages
, Message
, Folder
, Fields
and Field
. To have these objects available to your application, you will need to import CDO into your application. Move to "stdafx.h" and add the following line:
#import "F:\\Program Files\\Common Files\\System\\MSMAPI\\1033\\cdo.dll" \
rename_namespace("CDO")
We can access the current folder of Outlook using Outlook Object Model using GetCurrentFolder
of the Active Explorer. Here, we can stop execution if the retrieved folder's DefaultItemType
property is not olMailItem
. The EntryID
property of the retrieved folder identifies it uniquely from other folders. We'll pass this property to CDO to retrieve the current folder.
First, move back to OnFolderChange
and add the following lines of code:
void __stdcall COAddin::OnFolderChange()
{
if(!m_bEnableSorting)
return;
CDO::_SessionPtr Session("MAPI.Session");
Session->Logon("Outlook","",VARIANT_FALSE,VARIANT_FALSE);
CComPtr<Outlook::MAPIFolder> MFolder;
m_spExplorer->get_CurrentFolder(&MFolder);
OlItemType o;
MFolder->get_DefaultItemType(&o);
if(o != olMailItem)
return;
BSTR entryID;
MFolder->get_EntryID(&entryID);
CDO::MessagesPtr pcdoMessages;
CDO::FolderPtr pFolder= Session->GetFolder(entryID);
if(pFolder)
{
}
}
Ok, till here, we've got the CDO Folder, we can now get Messages
collection.
Using CDO to see if a Message has security
Now, let's look at how we can see if a message is encrypted/signed. The following KB presents the whole theory: "KB 194623", but I couldn't find it working for my client as there are a lot of email clients and not all of them set the bits described by this KB. It, in fact, states "using these properties to programmatically determine if a message has security on it is unreliable". In order to achieve the results, the only way I am able to find is that every email that has security on it has a special attachment. The attachment contains the encrypted/signed contents that Outlook decrypts/verifies and displays. This attachment has a "p7m" extension with a MIME type of "application/x-pkcs7-mime". And for our solution, this is going to be in the following way:
- Get the
Messages
collection of the folder
- Iterate the collection and get the
Message
- Iterate the
Attachments
- Get the
Fields
of each Attachment
- Iterate the
Fields
to find a Field
(H370E001E) (it is the MIME type of the Attachment
)
- Test the field value with "application/x-pkcs7-mime"
Now, add a new member function to your class named IsCDOEn
crypted. The function will accept a single CDO Message
object and will return a boolean indicating the state of the message. Following is the code snippet covering all of the above theory.
BOOL COAddin::IsCDOEncrypteD(CDO::MessagePtr pMessageRemote)
{
BOOL bEncrypted = false;
CDO::MessagePtr pMessage;
pMessage = pMessageRemote;
CDO::AttachmentsPtr pAttachments;
pAttachments = pMessage->Attachments;
_variant_t nCount =pAttachments->Count;
long nTotal = nCount.operator long();
for(int i = 0; i < nTotal; i++)
{
CDO::AttachmentPtr pAttachment;
CComVariant nItem(i+1);
pAttachment = pAttachments->Item[nItem];
CDO::FieldsPtr pFields;
pFields = pAttachment->Fields;
_variant_t nVFields = pFields->Count;
for(int z = 0; z < nVFields.operator long() ; z++)
{
CComVariant nFieldItem(z+1);
CDO::FieldPtr pField;
pField = pFields->Item[nFieldItem];
BSTR bstrFieldID;
bstrFieldID = pField->GetID().operator _bstr_t();
if(wcscmp(bstrFieldID,L"923664414")==0)
{
if(wcscmp(pField->Value.operator _bstr_t(),
L"application/x-pkcs7-mime")==0)
{
bEncrypted = true;
break;
}
}
}
}
pAttachments->Release();
pAttachments = NULL;
pMessage->Update();
return bEncrypted;
}
Using CDO to add custom fields to Outlook items
Outlook Object Model exposes Fields
properties of its items, you can add Custom fields to these items. We'll use CDO to add custom fields to mail items.
OK, now let's use IsCDOEncrypted
function during the enumeration of messages in our OnFolderChange
function. Move back to OnFolderChange
code.
The following code snippet adds custom fields to each item.
.....
.....
CDO::FolderPtr pFolder= Session->GetFolder(entryID);
if(pFolder)
{
pcdoMessages = pFolder->Messages;
CDO::MessagePtr pMessage = pcdoMessages->GetFirst();
while(pMessage)
{
BOOL bEncrypted = IsCDOEncrypteD(pMessage);
if(bEncrypted)
{
CDO::FieldsPtr pMessageFields = pMessage->Fields;
pMessageFields->Add(L"Encrypted",
CComVariant(8),L"SMIME Emails");
pMessage->Update();
}
else
{
CDO::FieldsPtr pMessageFields = pMessage->Fields;
pMessageFields->Add(L"Encrypted",CComVariant(8),L"Simple Emails");
pMessage->Update();
}
pMessage = pcdoMessages->GetNext();
}
}
Customizing the grouping and sorting of items
Outlook now exposes new XML based Views system. You can create your own Views using XML, or you can modify existing Views by altering the XML. The following is the standard XML of Inbox:
//
="1.0"
<view type="table">
<viewname>Messages</viewname>
<viewstyle>table-layout:fixed;width:100%;font-family:Tahoma;
font-style:normal;font-weight:normal;font-size:8pt;
color:Black;font-charset:0</viewstyle>
<viewtime>0</viewtime>
<linecolor>8421504</linecolor>
<linestyle>3</linestyle>
<usequickflags>1</usequickflags>
<collapsestate></collapsestate>
<rowstyle>background-color:#FFFFFF</rowstyle>
<headerstyle>background-color:#D3D3D3</headerstyle>
<previewstyle>color:Blue</previewstyle>
<arrangement>
<autogroup>1</autogroup>
<collapseclient></collapseclient>
</arrangement>
<column>
<name>HREF</name>
<prop>DAV:href</prop>
<checkbox>1</checkbox>
</column>
<column>
<heading>Importance</heading>
<prop>urn:schemas:httpmail:importance</prop>
<type>i4</type>
<bitmap>1</bitmap>
<width>10</width>
<style>padding-left:3px;;text-align:center</style>
</column>
<column>
<heading>Icon</heading>
<prop>http://schemas.microsoft.com/mapi/proptag/0x0fff0102</prop>
<bitmap>1</bitmap>
<width>18</width>
<style>padding-left:3px;;text-align:center</style>
</column>
<column>
<heading>Flag Status</heading>
<prop>http://schemas.microsoft.com/mapi/proptag/0x10900003</prop>
<type>i4</type>
<bitmap>1</bitmap>
<width>18</width>
<style>padding-left:3px;;text-align:center</style>
</column>
<column>
<format>boolicon</format>
<heading>Attachment</heading>
<prop>urn:schemas:httpmail:hasattachment</prop>
<type>boolean</type>
<bitmap>1</bitmap>
<width>10</width>
<style>padding-left:3px;;text-align:center</style>
<displayformat>3</displayformat>
</column>
<column>
<heading>From</heading>
<prop>urn:schemas:httpmail:fromname</prop>
<type>string</type>
<width>49</width>
<style>padding-left:3px;;text-align:left</style>
<displayformat>1</displayformat>
</column>
<column>
<heading>Subject</heading>
<prop>urn:schemas:httpmail:subject</prop>
<type>string</type>
<width>236</width>
<style>padding-left:3px;;text-align:left</style>
</column>
<column>
<heading>Received</heading>
<prop>urn:schemas:httpmail:datereceived</prop>
<type>datetime</type>
<width>59</width>
<style>padding-left:3px;;text-align:left</style>
<format>M/d/yyyy||h:mm tt</format>
<displayformat>2</displayformat>
</column>
<column>
<heading>Size</heading>
<prop>http://schemas.microsoft.com/mapi/id
/{00020328-0000-0000-C000-000000000046}/8ff00003</prop>
<type>i4</type>
<width>30</width>
<style>padding-left:3px;;text-align:left</style>
<displayformat>3</displayformat>
</column>
<groupby>
<order>
<heading>Received</heading>
<prop>urn:schemas:httpmail:datereceived</prop>
<type>datetime</type>
<sort>desc</sort>
</order>
</groupby>
<orderby>
<order>
<heading>Received</heading>
<prop>urn:schemas:httpmail:datereceived</prop>
<type>datetime</type>
<sort>desc</sort>
</order>
</orderby>
<groupbydefault>2</groupbydefault>
<previewpane>
<visible>1</visible>
<markasread>0</markasread>
</previewpane>
</view>
We are concerned of the two nodes <groupby>
and <orderby>
for this tutorial. I am going to customize the grouping of items only. You can reuse the same code to customize the sorting of items.
In order to customize the grouping of items, you can use "User Defined fields" in the "Customize Current View" option in Outlook, and to do it programmatically, we've already added a custom field to items. We just need to change the <groupby>
element as follows:
//
<groupby>
<order>
<heading>Encrytped</heading>
<prop>http://schemas.microsoft.com/mapi/string/
{00020329-0000-0000-C000-000000000046}/Encrypted</prop>
<type>string</type>
<sort>asc</sort>
</order>
</groupby>
So, let's get back to code and add a new member function called ChangeView(Outlook::ViewPtr pView)
. The function receives an Outlook View, retrieves its XML, and modifies it accordingly. CurrentView
property of Outlook MAPIFolder
object returns the Current View. The XML
property of Outlook::View
returns the XML of the current view. Here, I've used MSXML parser to modify the XML. You can use any convenient way. The following is the code snippet:
void COAddin::ChangeView(Outlook::ViewPtr pView)
{
HRESULT hr;
IXMLDOMDocument2 * pXMLDoc;
IXMLDOMNode * pXDN;
hr = CoInitialize(NULL);
hr = CoCreateInstance(CLSID_DOMDocument30, NULL, CLSCTX_INPROC_SERVER,
IID_IXMLDOMDocument2, (void**)&pXMLDoc);
hr = pXMLDoc->QueryInterface(IID_IXMLDOMNode, (void **)&pXDN);
BSTR XML;
pView->get_XML(&XML);
VARIANT_BOOL bSuccess=false;
pXMLDoc->loadXML(XML,&bSuccess);
CComPtr<IXMLDOMNodeList> pNodes;
pXMLDoc->getElementsByTagName(L"groupby",&pNodes);
long length = 0;
pNodes->get_length(&length);
if(length> 0)
{
HRESULT hr = pNodes->get_item(0,&pXDN);
IXMLDOMNode *pXDNTemp,*pXDNTemp2;
pXDN->get_firstChild(&pXDNTemp);
pXDNTemp->get_firstChild(&pXDNTemp2);
_variant_t vtHeading("Encrypted"),vtType("string"),
vtProp("http://schemas.microsoft.com/mapi/string/ \
{00020329-0000-0000-C000-000000000046}/Encrypted");
pXDNTemp2->put_nodeTypedValue(vtHeading);
pXDNTemp2->get_nextSibling(&pXDNTemp2);
pXDNTemp2->put_nodeTypedValue(vtProp);
pXDNTemp2->get_nextSibling(&pXDNTemp2);
pXDNTemp2->put_nodeTypedValue(vtType);
}else
{
IXMLDOMElement *pGroupByElement;
pXMLDoc->createElement(L"groupby",&pGroupByElement);
IXMLDOMElement *pOrderElement;
IXMLDOMNode *pOrderNode;
pXMLDoc->createElement(L"order",&pOrderElement);
pGroupByElement->appendChild(pOrderElement,&pOrderNode);
IXMLDOMElement *pHeadingElement,*pPropElement,*pTypeElement,
*pSortElement;
IXMLDOMNode *pHeadingNode,*pPropNode,*pTypeNode, *pSortNode;
_variant_t vtHeading("Encrypted"),vtSort("asc"),vtType("string"),
vtProp("http://schemas.microsoft.com/mapi/string//
{00020329-0000-0000-C000-000000000046}/Encrypted");
pXMLDoc->createElement(L"heading",&pHeadingElement);
pOrderNode->appendChild(pHeadingElement,&pHeadingNode);
pHeadingNode->put_nodeTypedValue(vtHeading);
pXMLDoc->createElement(L"prop",&pPropElement);
pOrderNode->appendChild(pPropElement,&pPropNode);
pPropNode->put_nodeTypedValue(vtProp);
pXMLDoc->createElement(L"type",&pTypeElement);
pOrderNode->appendChild(pTypeElement,&pTypeNode);
pTypeNode->put_nodeTypedValue(vtType);
pXMLDoc->createElement(L"sort",&pSortElement);
pOrderNode->appendChild(pSortElement,&pSortNode);
pSortNode->put_nodeTypedValue(vtSort);
HRESULT hr;
IXMLDOMElement *pXMLRootElement;
if(!FAILED(pXMLDoc->get_documentElement(&pXMLRootElement)))
{
_variant_t _vt;
hr= pXMLRootElement->insertBefore(pGroupByElement,_vt,NULL);
}
}
pXMLDoc->get_xml(&XML);
pView->put_XML(XML);
pView->Save();
}
Add the following code to the OnFolderChange
function to enable SMIME grouping.
pMessageFields->Add(L"Encrypted",CComVariant(8),L"Simple Emails");
pMessage->Update();
}
pMessage = pcdoMessages->GetNext();
}
CComPtr<Outlook::View> pV;
HRESULT hr = MFolder->get_CurrentView(&pV);
ChangeView(pV);
}
Woof, I m tired of typing :O.
Well now, it's time to compile, compile your project and if everything goes OK, you'll have your emails sorted in two groups.
Adding items to right click menu
Well, I think this is the most awaited section of this tutorial, and most of you will be concerned of this part rather then digging into cryptography logic :).
Amit Dey has already explained a lot about adding items to menu and toolbars. If you've not read that article, it's time to read it as I m not explaining the details of CommandBars and all.
In order to add an item to the right click menu of Outlook, we'll have to map the OnUpdate
event of Command Bars. We can acquire Command Bars object using the CommandBars
property of the Explorer
and sink its OnUpdate
event.
First of all, add private members to hold the CommandBars
objects. Move to OAddin.h header file and add the following lines:
CComPtr<Office::_CommandBars> m_spCommandbars;
CComPtr<Office::CommandBarControl> m_pSortButton;
Let's first make necessary changes to our COAddin
class to be able to sink the OnUpdate
event. Move to OAddin.h class and derive your class from CommandBarsEvents
class.
class ATL_NO_VTABLE COAddin :
public CComObjectRootEx<CComSingleThreadModel>,
...
...
public IDispEventSimpleImpl<1,COAddin,&__uuidof(Outlook::ExplorerEvents)>,
public IDispEventSimpleImpl<2,COAddin,&__uuidof(Office::_CommandBarsEvents)>
now create a call back function declartion in your OAddin.h file as follows
void __stdcall OnContextMenuUpdate();
Move to your cpp file and add the definition for the function.
void __stdcall COAddin::OnContextMenuUpdate()
{
MessageBoxW(NULL,L"Hello Menu Update Event",L"Outlook Addin",MB_OK);
}
Now setup the sink map.
BEGIN_SINK_MAP(COAddin)
SINK_ENTRY_INFO(1,__uuidof(Outlook::ExplorerEvents),0xf002
,OnFolderChange,&OnSimpleEventInfo)
SINK_ENTRY_INFO(2,__uuidof(Office::_CommandBarsEvents),0x1,
OnContextMenuUpdate,&OnSimpleEventInfo)
END_SINK_MAP()
Now, we've settled up everything to sink the interface from the source interface. The best place to sink events is OnConnection
. Move back to your OnConnection
function and add the following line of code. As described earlier, we can retrieve the CommandBars
from the Explorer
object.
hr = ExpEvents::DispEventAdvise((IDispatch*)m_spExplorer,
&__uuidof(Outlook::ExplorerEvents));
CComPtr < Office::_CommandBars> spCmdBars;
hr = spExplorer->get_CommandBars(&spCmdBars);
if(FAILED(hr))
return hr;
m_spCommandbars = spCmdBars;
hr = CmdBarsEvents::DispEventAdvise((IDispatch*)m_spCommandbars,
&__uuidof(Office::_CommandBarsEvents));
And finally, you are done. OnUpdate
event is fired whenever a CommandBar
has to update, and so we'll have to find the right click menu and add item to it. The right click menu has a fixed name "Context Menu". Here, we'll have to enumerate CommandBars
to find Context Menu.
CommandBars
object contains child CommandBar
controls, and each CommandBar
control can contain child CommandBarControl
and CommandBarButtons
. We'll add a CommandBarControl
to the menu. We can use Add
method of CommandBar
control to insert an item to CommandBar
, and so our task is:
- Enumerate
CommandBars
to find CommandBar
named "Context Menu".
- Get the
CommandBar
controls.
- Call
Add
method on controls to insert item to CommandBar
.
OK, one of the most important thing is that CommandBar
objects are Locked by Outlook; in order to call the Add
method, we'll have to remove the protection of the CommandBarControl
object, or the call to Add
method will fail with an assertion. We can use the Protection
property of CommandBar
to remove the protection.
Let's change the code of OnContextMenuUpdate
. The code snippet is as follows:
void __stdcall COAddin::OnContextMenuUpdate()
{
CComPtr<Office::CommandBar> pCmdBar;
BOOL bFound =false;
for(long i = 0; i < m_spCommandbars->Count ; i++)
{
CComPtr<Office::CommandBar> pTempBar;
CComVariant vItem(i+1);
m_spCommandbars->get_Item(vItem,&pTempBar);
if((pTempBar) && (!wcscmp(L"Context Menu",pTempBar->Name)))
{
pCmdBar = pTempBar;
bFound = true;
break;
}
}
if(!bFound)
return;
if(pCmdBar)
{
soBarProtection oldProtectionLevel = pCmdBar->Protection ;
pCmdBar->Protection = msoBarNoProtection;
CComVariant vtType(msoControlButton);
m_pSortButton = pCmdBar->Controls->Add(vtType);
m_pSortButton ->Tag = L"SORT_ITEM";
m_pSortButton ->Caption = L"Sort By SMIME";
m_pSortButton ->Priority =1 ;
m_pSortButton ->Visible = true;
}
}
Till now, if you compile the project, you'll be able to see an item in the right click menu of Outlook. But a control is useless until you add a handler to it. So now, let's get back to our OAddin
class header file to derive it from _CommandBarButtonEvents
to sink the events.
class ATL_NO_VTABLE COAddin :
public CComObjectRootEx<CComSingleThreadModel>,
...
...
public IDispEventSimpleImpl<1,COAddin,&__uuidof(Outlook::ExplorerEvents)>,
public IDispEventSimpleImpl<2,COAddin,&__uuidof(Office::_CommandBarsEvents)>
,
public IDispEventSimpleImpl<3,COAddin,
&__uuidof(Office::_CommandBarButtonEvents)>,
Again, use the ATL_SINK_INFO
structure to describe the callback parameters. Open the header file of the add-in object (OAddin.h) and add the following line at the very top:
extern _ATL_FUNC_INFO OnClickButtonInfo;
Open the cpp file of the class and add the following line to the top:
_ATL_FUNC_INFO OnClickButtonInfo =
{CC_STDCALL,VT_EMPTY,2,{VT_DISPATCH,VT_BYREF | VT_BOOL}};
Create a callback function.
void __stdcall OnClickButtonSort(IDispatch* Ctrl,VARIANT_BOOL * CancelDefault);
void __stdcall COAddin::OnClickButtonSort(IDispatch* Ctrl,VARIANT_BOOL * CancelDefault)
{
if(!m_bEnableSorting)
{
m_bEnableSorting =true;
OnFolderChange();
}
else
{
m_bEnableSorting = false;
}
}
Now, let's sink the event now. We'll sink the event as soon as the new CommandBarControl
is created.
m_pSortButton ->Visible = true;
hr = CommandButtonEvents::DispEventAdvise((IDispatch*)m_pSortButton);
if(hr != S_OK)
{
MessageBoxW(NULL,L"Menu Event Sinking failed",L"Error",MB_OK);
}
CComQIPtr < Office::_CommandBarButton> spCmdMenuButton(m_pSortButton);
OK now, compile your project, and play with your menu. If you click it once, the Mail items are sorted automatically, and if you click it again, Mail items are not sorted again.
How about changing the state of the menu to checked?
Oh well, more to write :S, and well, more to read as well.
OK, earlier while I was developing a solution for my client, a Microsoft MVP (I'm not naming YOU ;)) replied to me that it's not possible to either add an icon to the right click menu items or change their state to checked, and I had to ship the project without these features to my client. Later, I discovered it using Outlook Spy and Amit Dey's article that it's a little tricky but not impossible.
OK, in order to change the state of the Menu item to checked, you just need to change the State
property of the newly inserted CommandBarControl
to Office::msoButtonDown
, and you are done. The State
property is not available to the newly inserted CommandBarControl
and we'll have to convert it to CommandButton
.
Following is the code of OnUpdate
:
m_pSortButton ->Visible = true;
CComQIPtr < Office::_CommandBarButton> spCmdMenuButton(m_pSortButton);
if(m_bEnableSorting)
{
ATLASSERT(spCmdMenuButton);
spCmdMenuButton->State = Office::msoButtonDown;
m_bEnableSorting = true;
}
hr = CommandButtonEvents::DispEventAdvise((IDispatch*)m_pSortButton);
if(hr != S_OK)
{
MessageBoxW(NULL,L"Menu Event Sinking failed",L"Error",MB_OK);
}
NOTE: in order to add an ICON to the Menu item, you can use the PasteFace
method to put an image to the button. The sample source code does it. The following is the code to put an icon on the button.
HBITMAP hBmp =(HBITMAP)::LoadImage(_Module.GetResourceInstance(),
MAKEINTRESOURCE(IDB_BITMAP1),IMAGE_BITMAP,0,0,LR_LOADMAP3DCOLORS);
::OpenClipboard(NULL);
::EmptyClipboard();
::SetClipboardData(CF_BITMAP, (HANDLE)hBmp);
::CloseClipboard();
::DeleteObject(hBmp);
spCmdMenuButton->PutStyle(Office::msoButtonIconAndCaption);
HRESULT hr = spCmdMenuButton->PasteFace();
if (FAILED(hr))
return ;
If you compile your project, you'll have a complete plug-in that sorts the elements according to security availability on the message.
How to use MSI to install CDO programmatically.
MSI can be used to install CDO programmatically. In order to use MSI in your C++ project, you'll have to import the MSI.dll into your project.
#import "msi.dll" rename_namespace("MSI")
MSI requires the feature name and the product code in order to install CDO. The product code can be retrieved from Applicaton
object of Outlook that is passed to OnConnection
function. Following is the code snippet of MSI.
BSTR bstrCode;
spApp->get_ProductCode(&bstrCode);
MSI::InstallerPtr pInstaller(__uuidof(MSI::Installer));
MSI::MsiInstallState o = pInstaller->GetFeatureState(bstrCode,L"OutlookCDO");
if(o != MSI::msiInstallStateLocal)
{
pInstaller->ConfigureFeature(bstrCode,L"OutlookCDO",MSI::msiInstallStateLocal);
}
OK, that's it :).
I've tried to give as much description as possible in the tutorial.
The sample code provided is a C++ sample of what I did in VB. It is not optimized to give you the best results, moreover you may find some poor COM implementations in the code, but comments are invited :). Thanks. Cheers :).
~Blowin' me up with her love~
References and Acknowledgements
MSKB Articles:
- Q220600 - How to automate Outlook using VC++.
- Q238228 - Build an Office2000 COM add-in in VB.
- Q259298 - How to use Outlook Object Model through #import.
- Q173604 - How to use CommandBars in Outlook solutions.
- Q182394 - How to use CommandBars in Outlook solutions.
MSDN Articles:
- Building COM add-ins for Outlook 2000 - Thomas Rizzo.
- Developing COM add-ins for MS Office 2000.
Books:
- WROX Beginning ATL 3.0 COM Programming - Richard Grimes.
Other: