65.9K
CodeProject 正在变化。 阅读更多。
Home

扩展 Outlook 的新功能

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (36投票s)

2004 年 6 月 2 日

CPOL

19分钟阅读

viewsIcon

375209

downloadIcon

1488

了解如何添加按钮以及如何与 Outlook 交互。

引言

首先,如果您想到的是 Outlook Express,那就忘掉它吧。与 Outlook Express 集成的唯一三种方法是(这只是我的猜测)向 Microsoft 支付巨款,并向他们提供一个非常好的理由说明您为何想要这样做,或者通过加密入口集成,或者创建一个合适的钩子并破解进去。我已经进行了破解(由于资金不足),它看起来并不美观,虽然它能工作,但一点也不干净。

所以,这不是针对 Outlook Express 的,它仅适用于 Office 中的 Outlook。

有没有想过扩展 Outlook 以添加您一直渴望的额外功能?那么,别再犹豫了。我将尝试为您提供足够的信息,以便您可以做到。扩展 Outlook 有两种方法,一种是纯 ATL,另一种只是关于普通 DLL 中的一些 COM 接口。我们可以就哪种方法进行讨论,我敢肯定会有人偏爱 ATL 版本而不是 COM 版本。

称之为 COM 版本是不正确的,好吧,也不是完全不正确,它确实是 COM,但为了方便起见,我将称之为 Exchange 方式。那么,为什么在谈论 Outlook 时要提 Exchange 呢?它始于旧的 Exchange 4.0 客户端,Microsoft 提供了一种挂钩其邮件客户端的方法,由于 Outlook 使用与 Exchange 相同的模型,因此该入口点一直保留着,我们应该对此感到庆幸,因为它经过了多年测试,并且 99.9% 没有错误,而 ATL 版本实际上没有经过那么充分的测试。

我想您现在已经明白,本文并非关于 ATL 版本……

另外,我需要通知您,本文的部分内容摘自 MSDN,但经过少量修改,以便读者能够真正理解内容。Microsoft 在解释其更复杂的 API 方面从未真正出色过,我不知道为什么,但事实就是这样……

我决定不从代码开始,而是先提供一些背景信息,一旦覆盖完毕,我们将创建一个小的插件,该插件向新工具栏添加一个按钮。此插件不会涵盖整个架构,但它不仅能让您了解插件的功能,更重要的是,它(希望)能解释基本原理,以便您能更快地继续独立开发……

背景

您至少需要了解什么是 COM 接口,不,我不是指 VC 在创建 ATL 项目时为您生成的那个派生自 IDispatch 的东西,我说的是真正的 COM,是那个古老的。您还需要了解一些关于 MAPI 的知识,不一定非要成为大师,但至少应该知道它是什么。如果您不知道我刚才说了什么,别担心,您将获得足够的信息,以便知道应该搜索什么。

在我开始之前,如果您想调试您的插件,请在 Dev-studio 中选择 Outlook.exe 作为可执行文件。

扩展 Outlook - 基础知识……

当您想扩展 Outlook 时,您基本上需要创建一个 DLL,导出一个入口点,通过修改注册表将插件注册到 Outlook。但在我开始谈论如何做到这一点之前,让我们先回顾一下插件是如何加载的,为什么以及何时被释放。

插件会加载很多次,每次都在不同的上下文中。现在,Outlook 足够智能,会向您传递足够的信息,以便您可以确定您是在什么上下文中加载的,通过简单地识别上下文,您也可以确定可以做什么和不应该做什么。

在上表中,您可以看到三条横线(总共五条),每条横线代表一个不同的上下文。每个上下文都有一个时间轴,横线的长度就是该上下文加载的时间。

在我们看这个之前,值得一提的是 Outlook 维护着一个插件数组,每个插件都有自己的加载时间映射。

所以,如果我们按加载顺序来。

  • 任务

    一旦您启动 Outlook,Outlook 就会在此上下文中加载您的插件。此上下文持续时间最长,它会一直将您的插件加载到 Outlook 运行期间。

  • 会话

    此上下文在用户登录后立即创建,即选择了要使用的配置文件(如果存在多个配置文件),并且用户已与 MAPI 建立会话(这可能包括建立与 Exchange 服务器的连接,但情况并非总是如此,因为您可以将 Outlook 配置为仅使用 Pop3,也称为仅限 Internet 邮件)。

    这是确定是否应继续加载的好地方,例如,您安装了插件,但只想在配置文件 X 中加载,或者满足某个条件,例如能否连接到某个外部资源(如服务器)。

  • 查看器

    这是您最常打交道的上下文,因为每次打开 Outlook 中的某个项目时都会创建此上下文,但不仅是项目,它也可以是您在 Outlook 中打开的新窗口,基本上每次看到新窗口弹出时,背后都有一个新的查看器上下文。您需要在此处同步您的资源,因为您的插件不被视为单例。

那么,Outlook 是如何知道何时加载您的插件的呢?解释此问题的最佳方法是查看您最初如何注册插件。

扩展 Outlook - 注册插件……

有两种方法可以告诉 Outlook 关于您的插件,一种是使用 ECF 文件,另一种是通过注册表。我将不描述 ECF 格式,因为它太长了,也不是本文的目的,所以请打开您的注册表并浏览到 HKEY_LOCAL_MACHINE\Software\Microsoft\Exchange\Client\Extensions

您在这里看到的是插件列表。此列表中的每个条目都是一个插件,除了名称之外,该条目还告诉 Outlook 何时想要加载,插件实现了哪些接口以及一些不太重要的信息(请参阅 MSDN:)

举个例子:

Exchange Scan - 4.0;C:\Program Files\Network Associates\VirusScan\scanemal.dll;1;11000000000000;1110000;

那么,我们能从这个值中学到什么呢?让我们将该值分解成更小的部分,我会尝试解释它们的意思。

4.0

 

DLL 中实现的扩展对象版本(始终为 4.0)。
C:\Program.......\scanemal.dll

 

DLL 的位置。
1

 

ExchExtEntryPoint 序号。
11000000000000

 

Outlook 应安装扩展对象的上下文列表。
1110000

 

扩展对象公开的接口列表。
 

 

扩展可能使用的特定邮件服务(此示例中不存在)。

现在,前三项并不难理解,所以我们来看看另外两项有趣的内容。

首先是上下文映射,它将告诉 Outlook 何时加载给定插件。

上下文名称 映射位置 描述
EE_CONTEXT_SESSION 1 已登录到 MAPI。
EE_CONTEXT_VIEWER 2 正在查看对象(可能是文件夹)。
EE_CONTEXT_REMOTEVIEWER 3 使用远程邮件功能。
EE_CONTEXT_SEARCHVIEWER 4 使用查找功能。
EE_CONTEXT_ADDRBOOK 5 地址簿已打开。
EE_CONTEXT_SENDNOTEMESSAGE 6 正在撰写发送邮件。
EE_CONTEXT_READNOTEMESSAGE 7 正在阅读邮件。
EE_CONTEXT_SENDPOSTMESSAGE 8 正在撰写帖子邮件。
EE_CONTEXT_READPOSTMESSAGE 9 正在阅读帖子邮件。
EE_CONTEXT_READREPORTMESSAGE 10 正在阅读送达报告、已读报告。
EE_CONTEXT_SENDRESENDMESSAGE 11 重新发送已退回的邮件。
EE_CONTEXT_PROPERTYSHEETS 12 正在查看对象的属性。
EE_CONTEXT_ADVANCEDCRITERIA 13 使用高级搜索。
EE_CONTEXT_TASK 14 客户端正在运行。

接下来是接口映射,此映射告诉 Outlook 您实现了哪些接口。如果您不确定(我不知道您怎么会不确定自己实现了哪个接口,但嘿,我算什么呢……),您可以始终将此映射填充为 1,Outlook 将查询您的基本接口以获取它可能需要的任何接口,而您需要正确地响应。此映射包含 9 个您可以决定是否实现的接口(其中 7 个可以映射),还有一个是强制性的,因此不能放在此映射中。

接口(链接到 MSDN) 映射位置 描述
IExchExtCommands 1 如果您想添加一些按钮或菜单项,那么这是接口,今天您将使用不同的接口,但实现此接口可能有助于捕获标准按钮。稍后将对此进行更多介绍。
IExchExtUserEvents 2 如果您需要启用/禁用项目或捕获选择更改,那么这就是您需要的。
IExchExtSessionEvents 3 神奇的一个,只有一个函数:OnDelivery。每次收到新邮件时,都会调用此函数。
IExchExtMessageEvents 4 如果您想在发送或打开邮件之前进行一些自定义处理(以及其他事情),那么这就是您要找的。
IExchExtAttachedFileEvents 5 如果您想在将对象附加到邮件之前进行一些自定义处理,那么您应该从这里开始。
IExchExtPropertySheets 6 需要在“工具”\“选项”菜单中添加一个额外的属性表吗?那么您需要实现此接口。
IExchExtAdvancedCriteria 7 如果您需要增强 Outlook 中的高级搜索,那么这里是起点。
IExchExt 未映射 这是主入口,只有一个函数,那就是 Install
IExchExtModeless 未映射 请参阅 MSDN,我从未需要使用过这个。
IExchExtModelessCallback 未映射 请参阅 MSDN,我从未需要使用过这个。

当您将插件添加到注册表时,Outlook 会缓存大部分设置,因此您需要告诉它重新加载缓存。有三种方法可以做到这一点,我通常会使用第一种和最后一种,这样我就不会有任何意外。可能不太干净,但对我来说有效。

  1. 向注册表添加一个魔法值,Outlook 应该在找到此值时重新加载其缓存。
    • 此值的名称应为:Outlook Setup Extension。
    • 而值本身应为:4.0;Outxxx.dll;7;00000000000000;0000000;OutXXX。

    这唯一的问题是,当您安装插件时,您需要拥有完整的机器权限(或至少 HKEY_LOCAL_MACHINE 的读/写权限)。

  2. 使用 ECF 文件。(有关文件格式的说明,请参阅链接)使用此文件,您无需编辑注册表。
  3. 删除缓存(文件名为‘extend.dat’),这样您就安全了。不建议这样做,因为这可能会产生一些副作用,尽管我已经开发了 5 年多的插件,至今尚未发现任何副作用,但据称有一些。

典型的代码如下

char szAppPath[MAX_PATH]={0};
SHGetSpecialFolderPath(NULL, szAppPath, CSIDL_LOCAL_APPDATA, FALSE);
strcat(szAppPath, "\\Microsoft\\Outlook\\extend.dat"); 

好的,现在您知道如何注册您的插件以及插件是如何加载的,但您仍然不知道 Outlook 如何加载您的插件,所以我们现在来谈谈这个问题。

您的 DLL 需要导出一个函数,只有一个小函数就可以让您的插件运行起来。那么这个函数看起来是什么样的呢?

LPEXCHEXT CALLBACK ExchEntryPoint()
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState());
    CMyAddin *pExt = new CMyAddin();
    return pExt;
}

如您所见,它是一个回调函数,返回您的插件类的实例。这看起来不像 COM,对吗?嗯,它是,而且是以一种困难的方式实现的。Outlook 稍后将查询您的对象以获取不同的接口(请参见上文)。您还能从这段代码中看到什么?首先,它是一个使用 MFC 的 DLL。是否使用 MFC 完全取决于您,插件本身不需要 MFC,只是我使用它,因为它比使用 AYL 容易得多,而且工作量少得多(MFC 有 Bug,是的,我知道,但这只是我的个人观点,请不要因此攻击我……)。

扩展 Outlook - 开发插件的核心……

现在启动您的 Developer Studio,创建一个新项目,选择一个 Regular DLL (MFC),并将其命名为 OutlookAddin。

首先,打开 OutlookAddin.cpp 并创建如下函数

LPEXCHEXT CALLBACK ExchEntryPoint()
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState());
    CMyAddin *pExt = new CMyAddin();
    return pExt;
}

现在,打开您的 def 文件并将导出的函数添加进去

ExchEntryPoint                @1

这并不难,所以我们现在有了一个插件的入口点。现在我们需要添加一个类,一个通用的类,名为 CMyAddin,让 Dev-studio 为您生成它,这样它就会出现在一对新文件中。

首先更改类,使其派生自 IExchExt,同时,添加以下函数,使您的类如下所示:

class CMyAddin : public IExchExt
{
public:
   CMyAddin();
   ~CMyAddin();

   STDMETHODIMP_(ULONG)    AddRef();
   STDMETHODIMP_(ULONG)    Release(); 
   STDMETHODIMP        QueryInterface(REFIID    riid,
                      void**    ppvObj);
   STDMETHODIMP        Install(IExchExtCallback    *lpExchangeCallback,
                    ULONG        context,
                    ULONG        ulFlags);
    
   ULONG            m_ulRefCount;    // Basic COM stuff, needed to know when 
                                     // it's safe to delete ourselves
   ULONG            m_ulContext;     // Member to store the current context in.
};

值得一提的是,AddRefReleaseQueryInterface 函数来自标准的 IUnknown

现在您已经有了插件的核心,实现这些函数应该不难,所以我们来做吧:(我跳过了 Install 函数以外的所有内容,其他函数请参见本文的源代码)。

STDMETHODIMP CMyAddin::Install(IExchExtCallback    *lpExchangeCallback,
                 ULONG        mcontext,
                  ULONG        ulFlags)
{
   AFX_MANAGE_STATE(AfxGetStaticModuleState());
   HRESULT hRet = S_FALSE;
   try
   {
      m_ulContext = mcontext;
      switch(m_ulContext)
      {
         case EECONTEXT_TASK:
            {
               hRet = S_OK;
            }
            break;
         case EECONTEXT_SESSION:
            {
               hRet = S_OK;
            }
            break;
         case EECONTEXT_VIEWER:
            {
               hRet = S_OK;
            }
            break;
         case EECONTEXT_REMOTEVIEWER:
         case EECONTEXT_SENDNOTEMESSAGE:
         case EECONTEXT_SENDPOSTMESSAGE:
         case EECONTEXT_SEARCHVIEWER:
         case EECONTEXT_ADDRBOOK:        
         case EECONTEXT_READNOTEMESSAGE:        
         case EECONTEXT_READPOSTMESSAGE:
         case EECONTEXT_READREPORTMESSAGE:
         case EECONTEXT_SENDRESENDMESSAGE:
         case EECONTEXT_PROPERTYSHEETS:
         case EECONTEXT_ADVANCEDCRITERIA:
         default:
            {
               hRet = S_FALSE;
            }
            break;
      };
   }
   catch(...)
   {
   }
   return hRet;
}

您还需要创建一个新的 CPP 文件,称之为 MapiDefines.cpp,如下所示。之所以将其放在 CPP 文件中,是因为这些行不应被编译多次。

#include "stdafx.h"
#define INITGUID
#define USES_IID_IExchExt
#define USES_IID_IExchExtAdvancedCriteria
#define USES_IID_IExchExtAttachedFileEvents
#define USES_IID_IExchExtCommands
#define USES_IID_IExchExtMessageEvents
#define USES_IID_IExchExtPropertySheets
#define USES_IID_IExchExtSessionEvents
#define USES_IID_IExchExtUserEvents
#define USES_IID_IMessage
#define USES_IID_IMAPIContainer
#define USES_IID_IMAPIFolder
#define USES_IID_IMAPITable
#define USES_IID_IMAPIAdviseSink
#define USES_IID_IMsgStore
#define USES_IID_IMAPIForm
#define USES_IID_IPersistMessage
#define USES_IID_IMAPIMessageSite
#define USES_IID_IMAPIViewContext
#define USES_IID_IMAPIViewAdviseSink
#define USES_IID_IDistList
#define USES_IID_IMailUser
#include <mapix.h>

您还需要在 stdafx.h 中添加以下包含(这些文件应该在您安装 Dev-Studio 时已安装)。

#include <atlbase.h>
#include <mapix.h>
#include <mapiutil.h>
#include <mapitags.h>
#include <mapiform.h>
#include <initguid.h>
#include <mapiguid.h>
#include <exchext.h>
#include <exchform.h>
#include <mapidefs.h>
#include <mapispi.h>
#include <imessage.h>
#include <ocidl.h>

#pragma comment(lib,"mapi32")
#pragma comment(lib,"Rpcrt4")
#pragma comment(lib,"Version")

好的,现在我们有了一个基本的扩展,它除了在启动时加载到 Outlook 并在 Outlook 关闭时释放之外,没有其他功能。

现在,编译项目,并在继续阅读本文的下一部分之前,解决任何错误或警告。

如果您正在使用 Visual Studio .NET 进行开发,将找不到包含文件,您需要找到 MAPI 的 Platform SDK 或 Visual Studio 6 的包含文件。


扩展 Outlook - 向插件添加一些按钮……

首先,让我们扩展我们的类,以便在启动时添加一个工具栏和一个按钮。为此,我们需要向 Outlook 请求一个名为 <name> 的对象。我们还需要导入两个 DLL,它们定义了“Outlook 对象模型”对象(也称为 OOM)的外观。

// If you have Outlook 2000 installed on your machine, then use the 
// following lines
#import "mso9.dll" 
#import "msoutl9.olb"

// If you have Outlook 2002 installed on your machine, then use the 
// following lines
#import "mso.dll" 
#import "msoutl.olb"

// If you have Outlook 2003 installed on your machine, then use the 
// following lines
#import "mso11.dll" 
#import "msoutl11.olb"

我使用了 outlook 2000 版本,但导入哪个版本并不重要(您不必在机器上安装您导入的版本,拥有这些 DLL 就足够了,但如果安装了 Outlook,调试会容易得多……无论如何,我已将导入行放在我的 stdafx.h 中。

我未将这些文件包含在 ZIP 文件中,您需要从您的 Outlook 安装文件夹中找到它们。除了版权问题,它们体积很大(Msox.dll 大约 5.3 Mb)。

您必须记住的一点是,如果您使用的是仅存在于 Outlook 2003 中的功能,那么显然,在 Outlook 2000 中将无法正常工作,但只要您不执行该代码段,您的插件仍然可以运行。另外,如果您打算在 Outlook 97 和 Outlook 98 下运行,请忽略这一切。OOM 当时就存在,但并未完全实现。您确实可以在 Outlook 98 下创建工具栏,但这并不直接,而且按钮没有事件(以及其他),所以如果您想检测按钮何时被点击,就需要进行一些破解(挂钩和子类化)。

如果您仍然想在 Outlook 97 或 98 下运行此程序,请查找 IExchExtCommands 接口,更具体地说,查找函数 InstallCommands。(我将在另一篇文章中对此进行更多介绍,但只有在需求时才会介绍,因为如今大多数用户都在运行 Outlook 2000 或更高版本,所以这不是最常见的做法)。

在获得 Outlook 对象之前,您需要添加另一个名为 OutlookInterface.h 的头文件,并在其中放入以下内容

#ifndef _OUTLOOKINTERFACE_H
#define _OUTLOOKINTERFACE_H

#if defined(WIN32) && !defined(MAC)
#ifndef __IOutlookExtCallback_FWD_DEFINED__
#define __IOutlookExtCallback_FWD_DEFINED__
typedef interface IOutlookExtCallback IOutlookExtCallback;
#endif    /* __IOutlookExtCallback_FWD_DEFINED__ */ 

// Outlook defines this interface as an alternate to IExchExtCallback.
#ifndef __IOutlookExtCallback_INTERFACE_DEFINED__
#define __IOutlookExtCallback_INTERFACE_DEFINED__

EXTERN_C const IID IID_IOutlookExtCallback;
    interface DECLSPEC_UUID("0006720D-0000-0000-C000-000000000046")
    IOutlookExtCallback : public IUnknown
    {
    public:
        virtual HRESULT STDMETHODCALLTYPE GetObject( 
            /* [out] */ IUnknown __RPC_FAR *__RPC_FAR *ppunk) = 0;
        virtual HRESULT STDMETHODCALLTYPE GetOfficeCharacter( 
            /* [out] */ void __RPC_FAR *__RPC_FAR *ppmsotfc) = 0;
    };

DEFINE_GUID(IID_IOutlookExtCallback,
    0x0006720d,
    0x0000,
    0x0000,
    0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46);

#endif    /* __IOutlookExtCallback_INTERFACE_DEFINED__ */ 
#endif // defined(WIN32) && !defined(MAC)
#endif // _OUTLOOKINTERFACE_H

所以,要获得那个宝贵的 Outlook 对象,您可以将以下内容添加到您的类中

#include "OutlookInterface.h"

class CMyAddin : public IExchExt
{
public:
   CMyAddin();
   ~CMyAddin();
   :
   :
   :
    
   HRESULT GetOutlookApp(IExchExtCallback         *pmecb,
               Outlook::_ApplicationPtr     &rOLAppPtr);
   Outlook::_ApplicationPtr     m_OLAppPtr;
};

并按如下方式实现

HRESULT CMyAddin::GetOutlookApp(IExchExtCallback         *pmecb,
                 Outlook::_ApplicationPtr     &rOLAppPtr)
{
   AFX_MANAGE_STATE(AfxGetStaticModuleState());
   try
   {
      IOutlookExtCallback *pOutlook = NULL;
      HRESULT hRes = pmecb->QueryInterface(IID_IOutlookExtCallback,
                                            (void **) &pOutlook);
      if (pOutlook)
      {
         IUnknown *pUnk = NULL;
         pOutlook->GetObject(&pUnk);
         LPDISPATCH lpMyDispatch;
         if (pUnk != NULL)
         {
            hRes = pUnk->QueryInterface(IID_IDispatch,
                                        (void **) &lpMyDispatch);
       pUnk->Release();
         }
         if (lpMyDispatch)
         {
            OLECHAR * szApplication = L"Application";
       DISPPARAMS dispparamsNoArgs = {NULL, NULL, 0, 0};
       DISPID dspid;
       VARIANT vtResult;
       lpMyDispatch->GetIDsOfNames(IID_NULL, &szApplication, 1, 
                                   LOCALE_SYSTEM_DEFAULT, &dspid);
       lpMyDispatch->Invoke(dspid, IID_NULL, LOCALE_SYSTEM_DEFAULT, 
                             DISPATCH_METHOD, 
                            &dispparamsNoArgs, &vtResult, NULL, NULL);
       lpMyDispatch->Release();

       rOLAppPtr= vtResult.pdispVal;
            return S_OK;
         }
      }
   }
   catch(...)
   {
   }
   return S_FALSE;
}

所以,回到您的 Install 函数,并将粗体显示的那一行添加进去。

STDMETHODIMP OlemAddin::Install(IExchExtCallback    *lpExchangeCallback,
                 ULONG        mcontext,
                  ULONG        ulFlags)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState());
    HRESULT hRet = S_FALSE;
    try
    {
        m_ulContext = mcontext;
        GetOutlookApp(lpExchangeCallback, m_OLAppPtr); // <--Add this line here

好的,我们这里有什么?我们有一个扩展,在‘Install’时会获取一个 Outlook 对象。

让我们看看现在我们可以用它做什么,以及我们如何实际创建一个工具栏并向其中添加一个按钮。

第一步是创建一个某种容器类来存放我们要添加的按钮,这个类必须派生自 IDispatch。这个类将挂钩到按钮本身的 IConnectionPoint,并通过这种方式在按钮被点击时收到通知。现在,这只是一个示例。所以我不会花很多精力让它看起来漂亮或非常可用,这留给您。所以,打开文件 MyAddin.h,在 #include "OutlookInterface.h" 行和 CMyAddin 类之间添加以下内容。

class COutlookButton : public IDispatch 
{
public:
    COutlookButton(CComPtr <Office::CommandBarControl> pButton);
    virtual ~COutlookButton();

    // IUnknown Implementation
    virtual HRESULT __stdcall QueryInterface(REFIID riid, void ** ppv);
    virtual ULONG   __stdcall AddRef(void);
    virtual ULONG   __stdcall Release(void);

    // Methods:

    // IDispatch Implementation
    virtual HRESULT __stdcall GetTypeInfoCount(UINT* pctinfo);
    virtual HRESULT __stdcall GetTypeInfo(UINT iTInfo, LCID lcid, 
                                          ITypeInfo** ppTInfo);
    virtual HRESULT __stdcall GetIDsOfNames(REFIID riid, LPOLESTR* rgszNames, 
                                    UINT cNames, LCID lcid, DISPID* rgDispId);
    virtual HRESULT __stdcall Invoke(DISPID dispIdMember, REFIID riid, 
                                     LCID lcid, WORD wFlags, 
                                     DISPPARAMS* pDispParams, 
                                     VARIANT* pVarResult, 
                                     EXCEPINFO* pExcepInfo, UINT* puArgErr);


    BOOL                        SetupConnection(IDispatch *pDisp);
    void                        ShutDown();

    DWORD                                m_dwCookie;
    CComPtr <Office::CommandBarControl> m_pButton;
    IConnectionPoint                    *m_pCP;
    ULONG                                m_cRef;
};

现在,为了节省本文的空间,我不会把那个类的源代码放进去……它有点长,所以如果您打开了本文的工作区,那就打开 OutlookButton.cpp 文件,我会试着解释逻辑。

当我们创建这种类型的对象时,我们会将一个按钮作为参数传递给它。这个按钮将被实例化,我们也将连接到该按钮的 IConnectionPoint。这在构造函数和 SetupConnection 函数中完成。当按钮被点击时,Invoke 函数会被调用(忍不住写下这个……),然后就看您决定做什么了。在此示例中,我们将仅显示一个消息框,以表明按钮已被点击。

为了使代码可读,我们将添加一个名为 InstallInterface 的函数,而不是将代码放在 Install 函数中,其原型如下:

class CMyAddin : public IExchExt
{
public:
   CMyAddin();
   ~CMyAddin();
   :
   :
   :
   HRESULT   InstallInterface(IExchExtCallback   *lpExchangeCallback);

函数 InstallInterface 有点长,所以您需要查阅本文的源代码。我已添加了一些注释,足以让您了解其工作原理。

而现在我们只需要调用该函数,并且我们在 Install 函数中,当处于 EECONTEXT_VIEWER 上下文时调用它。

STDMETHODIMP CMyAddin::Install(IExchExtCallback    *lpExchangeCallback,
                 ULONG        mcontext,
                  ULONG        ulFlags)
{
   AFX_MANAGE_STATE(AfxGetStaticModuleState());
   :
   :
         case EECONTEXT_VIEWER:
            {
               :
          InstallInterface(lpExchangeCallback); // <--Add this line here
            }
            break;

现在编译整个项目,再次解决(如果有的话)任何错误。

所以,您现在需要做的就是注册 DLL,为此,在本文的源代码中,您有一个注册表文件。您需要编辑它,并更改路径,使其指向此 DLL。


在前两部分中,我们创建了一个基本的插件,它添加了一个工具栏,当您点击它时,会显示一个消息框。所以在这篇文章中,我们将探讨使用 Outlook 插件可以做的更多事情。在这篇文章中,我将向您展示如何将所有传入的邮件记录到一个文件中。这并不令人印象深刻,但只是为了向您展示一些基本的 MAPI 操作。

如果您还没有阅读之前的文章,请回到那里,否则您将无法理解很多内容。

扩展 Outlook - 针对每封进来的邮件都会被调用……

首先,让我们扩展我们的类,使其派生自 IExchExtSessionEvents。此接口只有一个方法,那就是 OnDelivery。每次传递邮件时都会调用此方法。从邮件进来到调用您之间可能有一个轻微的延迟,这取决于规则和其他插件,但大多数情况下,您会在邮件实际显示在列表视图中的前一刻被调用。

有一件事极其重要,我怎么强调都不为过,那就是此函数的返回值。如果您查看 MSDN,您可以看到以下内容:

  • S_OK

    扩展对象用自己的行为替换了 Microsoft Exchange 的默认行为。Microsoft Exchange 将认为任务已处理。

  • S_FALSE

    扩展对象什么也没做或添加了额外行为。Microsoft Exchange 将继续调用扩展对象或自行完成工作。

现在,请再次阅读 S_OK,因为如果您决定返回 S_OK,那么这封邮件就可能丢失,除非您自己存储了这封邮件。我不是开玩笑,如果您返回 S_OK,邮件就会丢失。

我想您现在已经明白了。那么让我们开始写代码吧。

首先,扩展您的类,使其如下所示(添加粗体显示的内容)

class CMyAddin : public IExchExt, 
              IExchExtSessionEvents
{
public:
   CMyAddin();
   ~CMyAddin();

   STDMETHODIMP_(ULONG)    AddRef();
   STDMETHODIMP_(ULONG)    Release(); 
   STDMETHODIMP        QueryInterface(REFIID    riid,
                      void**    ppvObj);
   STDMETHODIMP        Install(IExchExtCallback    *lpExchangeCallback,
                    ULONG        context,
                    ULONG        ulFlags);
   STDMETHODIMP            OnDelivery(IExchExtCallback  *lpExchangeCallback); 

在实现 OnDelivery 之前,我们需要更改 QueryInterface 方法。当调用此方法时,REFIID 将设置为 IID_IExchExtSessionEvents,因此我们需要添加一些行来正确处理这种情况。

再次添加粗体显示的内容

STDMETHODIMP CMyAddin::QueryInterface(REFIID   riid,
                 void**     ppvObj)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState());
    *ppvObj=NULL;
    IUnknown* punk=NULL;
    if (riid == IID_IUnknown)
    {
        punk = (IExchExt*)this;
    }
    else if (riid == IID_IExchExt)
    {
        punk = (IExchExt*)this;
    }
    else if (riid == IID_IExchExtSessionEvents)
    {
        punk = (IExchExtSessionEvents*)this;
    }
    else
    {

我们在注册表中指定我们实现了 IExchExt 接口,以及 IExchExtSessionEvents 接口,因此当 Outlook 加载我们时,它会查询 IExchExtSessionEvents 接口,当它这样做时,我们需要给它一些东西。

好了,我们来看看 OnDelivery。如下添加此函数,并记住无论您是否处理了邮件,都要返回 S_FALSE。它不是一个指示您的自定义代码是否有效的返回代码。它指示您是否处理了邮件!

STDMETHODIMP CMyAddin::OnDelivery(IExchExtCallback *lpExchangeCallback)
{
   AFX_MANAGE_STATE(AfxGetStaticModuleState());
   try
   {
   }
   catch(...)
   {
   }
   return S_FALSE;
}

现在,我们想要做的是获取邮件的指针,提取一些信息,并将其记录到一个文件中。所以,我们首先通过调用 GetObject 方法来向 Outlook 请求指针。

STDMETHODIMP CMyAddin::OnDelivery(IExchExtCallback   *lpExchangeCallback)
{
   AFX_MANAGE_STATE(AfxGetStaticModuleState());
   try
   {
      // The first thing we need to do is to obtain a pointer to the message 
      // that just arrived.
      // When we do that, we also obtain a pointer to the message-store (see 
      // it as a database)
      LPMESSAGE    lpMessage = NULL;
      LPMDB    lpMdb = NULL;
      if (SUCCEEDED(lpExchangeCallback->GetObject(&lpMdb,
                                                 (LPMAPIPROP*)&lpMessage)))
      {

GetObject 方法返回指向邮件存储和刚收到的邮件的指针。我们唯一感兴趣的是邮件,所以只需忽略邮件存储指针。

在我们继续之前,让我们先看看什么是消息,以及数据是如何存储在其中的。MAPI 消息与 mime 消息完全不同。它是一个二进制大对象,而不是文本消息。数据存储在称为标签、属性标签的东西中。您可以将其与 ini 文件进行比较,其中有键和值。在这种情况下,键就是标签。

这些标签可以由 MAPI 预定义,最常见的如主题和正文是预定义标签,但您也可以使用所谓的命名标签。命名标签是您定义的数据存储标签。这并不意味着数据是加密或隐藏的,任何人都可以提取这些数据。

© . All rights reserved.