在c++中使用Outlook Object Model发送邮件
一、Outlook Object Model简介
Outlook Object Model(OOM)是outlook为开发者提供的一个COM组件,我们可以在程序中使用它来发送邮件、管理邮箱等。相关介绍可以参见以下链接:
https://msdn.microsoft.com/en-us/library/ms268893.aspx
可能有人会说用shellExcute也可以实现对outlook的操作,这里正好说到为什么要用Outlook Object Model的原因之一。如果用shellExcute来操作,必须严格保证传入的参数的编码和长宽字节符合outlook的要求,也就是说在进行实际的邮件操作之前需要对字符串参数进行繁琐的判断和处理(如果是自己写的小程序玩一玩的请随意),而用OOM来操作则不需要关心这些问题。
OOM的另外一个优点就是接口简单,使用方便。要兼容所有的outlook版本也比较方便,这个下面再说。
二、在c++中的使用方式
因为微软的官网上关于OOM的例程主要是VBA和c#的,相关的例子有很多,这里就不多介绍了,主要介绍怎么在c++中使用。
可以参见下面这个链接:
http://1code.codeplex.com/SourceControl/changeset/view/60353#585447
主要有三种方式来使用OOM:
1、在程序中用#import命令来导入OOM的类型库,然后使用c++里的智能指针来调用相关的函数和属性。例程如下:
1 #import "C:\Program Files\Common Files\Microsoft Shared\OFFICE15\mso.dll" no_namespace \ 2 rename("DocumentProperties","_DocumentProperties") \ 3 rename("RGB", "MsoRGB") 4 5 #import "D:\office\Office15\MSOUTL.OLB" \ 6 rename("GetOrganizer", "GetOrganizerAE")\ 7 rename_namespace("Outlook") 8 9 using namespace Outlook; 10 void SendMail_UsingOOM(const std::wstring &, const std::wstring &, const std::wstring &, const std::wstring &, const std::wstring &, const std::wstring &, bool); 11 int main() 12 { 13 SendMail_UsingOOM(L"a@email.com", L"aa@email.com", L"aaa@email.com", L"OOM_Test", L"It`s OOM Test.Do not care about it.", L"C:\\Users\\Desktop\\aaa.pdf", true); 14 return 0; 15 } 16 17 void SendMail_UsingOOM(const std::wstring &to,const std::wstring &cc,const std::wstring &bcc,const std::wstring &subject,const std::wstring &body,const std::wstring &attachmentPath,bool showUI) 18 { 19 CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); 20 try 21 { 22 _ApplicationPtr spApp("Outlook.Application"); 23 _NameSpacePtr pMAPI = spApp->GetNamespace("MAPI"); 24 pMAPI->Logon(); 25 26 _MailItemPtr olMail(spApp->CreateItem(olMailItem)); 27 if (0 == to.size()) 28 { 29 pMAPI->Logoff(); 30 return; 31 } 32 olMail->PutTo(_bstr_t(to.c_str())); 33 olMail->PutCC(_bstr_t(cc.c_str())); 34 olMail->PutBCC(_bstr_t(bcc.c_str())); 35 olMail->PutSubject(_bstr_t(subject.c_str())); 36 olMail->PutBody(_bstr_t(body.c_str())); 37 olMail->Save(); 38 39 if (0 != attachmentPath.size()) 40 { 41 AttachmentsPtr olAtta = olMail->GetAttachments(); 42 olAtta->Add(attachmentPath.c_str(), 1, 1, attachmentPath.c_str()); 43 } 44 HRESULT result = NULL; 45 if (showUI) 46 { 47 result = olMail->Display(showUI); 48 } 49 else 50 { 51 result = olMail->Send(); 52 } 53 pMAPI->Logoff(); 54 } 55 catch (_com_error &err) 56 { 57 wprintf(L"Outlook throws the error: %s\n", err.ErrorMessage()); 58 wprintf(L"Description: %s\n", (LPCWSTR)err.Description()); 59 } 60 CoUninitialize(); 61 }
要注意,这里的智能指针并不是你自己定义的,而是COM组件提供的。比如上面程序里的_ApplicationPtr、_NameSpacePtr等,因为是智能指针,也不需要手动释放,非常方便。
这里需要导入两个类型库文件:mso.dll和msoutl.olb,前者是office的,后者是outlook的。在导入后会生成mso.tlh、mso.tli、msoutl.tlh和msoutl.tli,tlh文件就相当于头文件,tli文件相当于cpp文件。实际上这个生成的过程只是最开始的时候执行了一次,生成的tlh、tli文件完全可以放到其他项目中包含(可能会产生重名问题,需要手动更改)。
在实际过程中,会发现import可能出现很多乱七八糟的错误,比如这样:
1 c:\OutlookAdd-In\OutlookAdd-In\debug\msoutl.tlh(20855) : error C2556: 'Outlook::AddressEntryPtr Outlook::_AppointmentItem::GetOrganizer(void)' : overloaded function differs only by return type from '_bstr_t Outlook::_AppointmentItem::GetOrganizer(void)' 2 c:\OutlookAdd-In\OutlookAdd-In\debug\msoutl.tlh(20753) : see declaration of 'Outlook::_AppointmentItem::GetOrganizer'
这其实是方法名冲突的问题,上面这个例子是GetOrganizer(void)这个方法冲突了,可以用rename关键字来修改namespace里实际生成的方法名以避免冲突。这里可以参见以下链接:
https://social.msdn.microsoft.com/Forums/office/en-US/f51cde52-0faa-42a2-bf61-c18b6f5b0e64/error-while-building-the-outlook-addin-code-with-ms-outlook-2010?forum=outlookdev
http://blog.chinaunix.net/uid-20791902-id-292054.html
2、第二种方式是使用COM组件提供的API来调用相关的函数,这种方式需要我们使用特定的API函数,将要调用的方法名和相关参数作为参数传入这些API中。例程如下:
1 // laterbind.cpp : 定义控制台应用程序的入口点。 2 // 3 4 #include "stdafx.h" 5 struct Wrap_errno 6 { 7 HRESULT hr; 8 EXCEPINFO info; 9 }; 10 Wrap_errno AutoWrap(int autoType, VARIANT *pvResult, IDispatch *pDisp, 11 LPOLESTR ptName, int cArgs...) 12 { 13 // Begin variable-argument list 14 va_list marker; 15 va_start(marker, cArgs); 16 17 Wrap_errno err; 18 memset(&err, 0, sizeof err); 19 20 if (!pDisp) 21 { 22 err.hr = E_INVALIDARG; 23 return err; 24 } 25 26 // Variables used 27 DISPPARAMS dp = { NULL, NULL, 0, 0 }; 28 DISPID dispidNamed = DISPID_PROPERTYPUT; 29 DISPID dispID; 30 HRESULT hr; 31 32 // Get DISPID for name passed 33 hr = pDisp->GetIDsOfNames(IID_NULL, &ptName, 1, LOCALE_USER_DEFAULT, &dispID); 34 if (FAILED(hr)) 35 { 36 err.hr = hr; 37 return err; 38 } 39 40 // Allocate memory for arguments 41 VARIANT *pArgs = new VARIANT[cArgs + 1]; 42 // Extract arguments... 43 for (int i = 0; i < cArgs; i++) 44 { 45 pArgs[i] = va_arg(marker, VARIANT); 46 } 47 48 // Build DISPPARAMS 49 dp.cArgs = cArgs; 50 dp.rgvarg = pArgs; 51 52 // Handle special-case for property-puts 53 if (autoType & DISPATCH_PROPERTYPUT) 54 { 55 dp.cNamedArgs = 1; 56 dp.rgdispidNamedArgs = &dispidNamed; 57 } 58 59 // Make the call 60 EXCEPINFO excepInfo; 61 memset(&excepInfo, 0, sizeof excepInfo); 62 hr = pDisp->Invoke(dispID, IID_NULL, LOCALE_SYSTEM_DEFAULT, 63 autoType, &dp, pvResult, &excepInfo, NULL); 64 if (FAILED(hr)) 65 { 66 err.hr = hr; 67 err.info = excepInfo; 68 delete[] pArgs; 69 return err; 70 } 71 72 // End variable-argument section 73 va_end(marker); 74 75 delete[] pArgs; 76 77 return err; 78 } 79 80 Wrap_errno SendMail_UsingOOM( 81 const std::wstring &to, 82 const std::wstring &cc, 83 const std::wstring &bcc, 84 const std::wstring &subject, 85 const std::wstring &body, 86 const std::wstring &attachmentPath, 87 bool showUI 88 ) 89 { 90 CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); 91 92 // Define vtMissing for optional parameters in some calls. 93 VARIANT vtMissing; 94 vtMissing.vt = VT_EMPTY; 95 96 CLSID clsid; 97 HRESULT hr; 98 99 Wrap_errno err; 100 memset(&err, 0, sizeof err); 101 102 LPCOLESTR progID = L"Outlook.Application"; 103 hr = CLSIDFromProgID(progID, &clsid); 104 if (FAILED(hr)) 105 { 106 CoUninitialize(); 107 return err; 108 } 109 IDispatch *pOutlookApp = NULL; 110 hr = CoCreateInstance( 111 clsid, // CLSID of the server 112 NULL, 113 CLSCTX_LOCAL_SERVER, // Outlook.Application is a local server 114 IID_IDispatch, // Query the IDispatch interface 115 (void **)&pOutlookApp); // Output 116 117 if (FAILED(hr)) 118 { 119 if (pOutlookApp != NULL) 120 { 121 pOutlookApp->Release(); 122 } 123 CoUninitialize(); 124 return err; 125 } 126 127 // pNS = pOutlookApp->GetNamespace("MAPI") 128 IDispatch *pNS = NULL; 129 { 130 VARIANT x; 131 x.vt = VT_BSTR; 132 x.bstrVal = SysAllocString(L"MAPI"); 133 134 VARIANT result; 135 VariantInit(&result); 136 err = AutoWrap(DISPATCH_METHOD, &result, pOutlookApp, L"GetNamespace", 1, x); 137 if (err.hr < 0) 138 { 139 VariantClear(&result); 140 VariantClear(&x); 141 if (pOutlookApp != NULL) 142 { 143 pOutlookApp->Release(); 144 } 145 if (pNS != NULL) 146 { 147 pNS->Release(); 148 } 149 CoUninitialize(); 150 return err; 151 } 152 pNS = result.pdispVal; 153 VariantClear(&x); 154 } 155 156 // pNS->Logon(vtMissing, vtMissing, true, true) 157 { 158 VARIANT vtShowDialog; 159 vtShowDialog.vt = VT_BOOL; 160 vtShowDialog.boolVal = VARIANT_TRUE; 161 VARIANT vtNewSession; 162 vtNewSession.vt = VT_BOOL; 163 vtNewSession.boolVal = VARIANT_TRUE; 164 165 AutoWrap(DISPATCH_METHOD, NULL, pNS, L"Logon", 4, vtNewSession, 166 vtShowDialog, vtMissing, vtMissing); 167 } 168 169 // pMail = pOutlookApp->CreateItem(Outlook::olMailItem); 170 IDispatch *pMail = NULL; 171 { 172 VARIANT x; 173 x.vt = VT_I4; 174 x.lVal = 0; // Outlook::olMailItem 175 176 VARIANT result; 177 VariantInit(&result); 178 err = AutoWrap(DISPATCH_METHOD, &result, pOutlookApp, L"CreateItem", 1, x); 179 if (err.hr < 0) 180 { 181 if (pMail != NULL) 182 { 183 pMail->Release(); 184 } 185 if (pNS != NULL) 186 { 187 pNS->Release(); 188 } 189 if (pOutlookApp != NULL) 190 { 191 pOutlookApp->Release(); 192 } 193 VariantClear(&x); 194 VariantClear(&result); 195 CoUninitialize(); 196 return err; 197 } 198 pMail = result.pdispVal; 199 } 200 201 // pMail->Subject = _bstr_t(L"Feedback of All-In-One Code Framework"); 202 { 203 VARIANT x; 204 x.vt = VT_BSTR; 205 x.bstrVal = SysAllocString(subject.c_str()); 206 AutoWrap(DISPATCH_PROPERTYPUT, NULL, pMail, L"Subject", 1, x); 207 VariantClear(&x); 208 } 209 210 // pMail->To = _bstr_t(L"codefxf@microsoft.com"); 211 { 212 VARIANT x; 213 x.vt = VT_BSTR; 214 x.bstrVal = SysAllocString(to.c_str()); 215 AutoWrap(DISPATCH_PROPERTYPUT, NULL, pMail, L"To", 1, x); 216 VariantClear(&x); 217 } 218 219 // pMail->cc 220 { 221 VARIANT x; 222 x.vt = VT_BSTR; 223 x.bstrVal = SysAllocString(cc.c_str()); 224 AutoWrap(DISPATCH_PROPERTYPUT, NULL, pMail, L"Cc", 1, x); 225 VariantClear(&x); 226 } 227 228 // pMail->bcc 229 { 230 VARIANT x; 231 x.vt = VT_BSTR; 232 x.bstrVal = SysAllocString(bcc.c_str()); 233 AutoWrap(DISPATCH_PROPERTYPUT, NULL, pMail, L"Bcc", 1, x); 234 VariantClear(&x); 235 } 236 237 // pMail->body 238 { 239 VARIANT x; 240 x.vt = VT_BSTR; 241 x.bstrVal = SysAllocString(body.c_str()); 242 AutoWrap(DISPATCH_PROPERTYPUT, NULL, pMail, L"Body", 1, x); 243 VariantClear(&x); 244 } 245 246 // pAtta = pMail->GetAttachments 247 // pAtta->Add(source,type,position,displayname) 248 IDispatch *pAtta = NULL; 249 { 250 VARIANT result; 251 VariantInit(&result); 252 err = AutoWrap(DISPATCH_PROPERTYGET, &result, pMail, L"Attachments", 0); 253 if (err.hr < 0) 254 { 255 if (pMail != NULL) 256 { 257 pMail->Release(); 258 } 259 if (pNS != NULL) 260 { 261 pNS->Release(); 262 } 263 if (pOutlookApp != NULL) 264 { 265 pOutlookApp->Release(); 266 } 267 VariantClear(&result); 268 CoUninitialize(); 269 return err; 270 } 271 pAtta = result.pdispVal; 272 273 VARIANT path; 274 path.vt = VT_BSTR; 275 path.bstrVal = SysAllocString(attachmentPath.c_str()); 276 VARIANT x; 277 x.vt = VT_I4; 278 x.lVal = 1; 279 err = AutoWrap(DISPATCH_METHOD, NULL, pAtta, L"Add", 4, path, x, x, path); 280 if (err.hr < 0) 281 { 282 if (pAtta != NULL) 283 { 284 pAtta->Release(); 285 } 286 if (pMail != NULL) 287 { 288 pMail->Release(); 289 } 290 if (pNS != NULL) 291 { 292 pNS->Release(); 293 } 294 if (pOutlookApp != NULL) 295 { 296 pOutlookApp->Release(); 297 } 298 VariantClear(&result); 299 VariantClear(&path); 300 CoUninitialize(); 301 return err; 302 } 303 VariantClear(&path); 304 } 305 306 // pMail->Display(true); 307 { 308 VARIANT vtModal; 309 vtModal.vt = VT_BOOL; 310 vtModal.boolVal = VARIANT_TRUE; 311 err = AutoWrap(DISPATCH_METHOD, NULL, pMail, L"Display", 1, vtModal); 312 if (err.hr < 0) 313 { 314 if (pMail != NULL) 315 { 316 pMail->Release(); 317 } 318 if (pNS != NULL) 319 { 320 pNS->Release(); 321 } 322 if (pOutlookApp != NULL) 323 { 324 pOutlookApp->Release(); 325 } 326 VariantClear(&vtModal); 327 CoUninitialize(); 328 return err; 329 } 330 } 331 332 // pNS->Logoff() 333 err = AutoWrap(DISPATCH_METHOD, NULL, pNS, L"Logoff", 0); 334 if (err.hr < 0) 335 { 336 if (pMail != NULL) 337 { 338 pMail->Release(); 339 } 340 if (pNS != NULL) 341 { 342 pNS->Release(); 343 } 344 if (pOutlookApp != NULL) 345 { 346 pOutlookApp->Release(); 347 } 348 CoUninitialize(); 349 return err; 350 } 351 352 if (pMail != NULL) 353 { 354 pMail->Release(); 355 } 356 if (pNS != NULL) 357 { 358 pNS->Release(); 359 } 360 if (pOutlookApp != NULL) 361 { 362 pOutlookApp->Release(); 363 } 364 365 CoUninitialize(); 366 err.hr = S_OK; 367 return err; 368 } 369 int main() 370 { 371 SendMail_UsingOOM(L"aaa@email.com", L"aaaa@email.com", L"aaaa@email.com", L"OOM_Test", L"It`s OOM Test.Do not care it.", L"C:\\Users\\Desktop\\aaa.pdf", true); 372 return 0; 373 }
这里人工地把调用相关的API封装在了AutoWrap函数里,通过调用该函数即可调用相关操作的函数,可以看到主要的API就是CLSIDFromProgID、CoCreateInstance、GetIDsOfNames、Invoke这些COM函数,不熟悉的同学可以去看下com操作相关的资料。
要使用上面这种方式来操作OOM,你需要首先知道OOM提供的接口以及不同接口、不同属性之间的关系,因为你是通过com的API来调用OOM,它并没有生成外部代码,所以接口的逻辑在外部是不可见的。我们可以通过微软的官网来查看OOM的接口:
https://msdn.microsoft.com/en-us/library/office/microsoft.office.interop.outlook.aspx
3、第三种使用方式是在MFC工程中使用,依次打开MFC里的类向导-添加类-类型库中的MFC类,如下:
将msoutl.olb中的相关接口导入工程中,可以用哪个就导入哪个接口,类向导会为每个接口都生成一个类。一般是在接口的名字里以“C”代替“_”来命名。如下图:
之后我们就可以以类的形式来使用这些接口了,非常方便。例程如下:
1 void CAboutDlg::SendMail_UsingOOM( 2 const std::wstring &to, 3 const std::wstring &cc, 4 const std::wstring &bcc, 5 const std::wstring &subject, 6 const std::wstring &body, 7 const std::wstring &attachmentPath, 8 bool showUI 9 ) 10 { 11 try 12 { 13 CApplication olApp; 14 COleException e; 15 if (!olApp.CreateDispatch(L"Outlook.Application", &e)) { 16 CString str; 17 str.Format(L"CreateDispatch() failed w/error 0x%08lx", e.m_sc); 18 AfxMessageBox(str, MB_SETFOREGROUND); 19 return; 20 } 21 // Logon. Doesn't hurt if you are already running and logged on... 22 CNameSpace olNs(olApp.GetNamespace(L"MAPI")); 23 COleVariant covOptional((long)DISP_E_PARAMNOTFOUND, VT_ERROR); 24 olNs.Logon(covOptional, covOptional, covOptional, covOptional); 25 26 // Prepare a new mail message 27 CMailItem olMail(olApp.CreateItem(0)); 28 29 if (0 != to.size()) 30 olMail.put_To(to.c_str()); 31 else 32 { 33 olNs.Logoff(); 34 return; 35 } 36 37 if (0 != subject.size())olMail.put_Subject(subject.c_str()); 38 if (0 != body.size())olMail.put_Body(body.c_str()); 39 if (0 != cc.size())olMail.put_CC(cc.c_str()); 40 if (0 != bcc.size())olMail.put_BCC(bcc.c_str()); 41 42 olMail.Save(); 43 44 if (0 != attachmentPath.size()) 45 { 46 CAttachments olAtta(olMail.get_Attachments()); 47 VARIANT vFile; 48 vFile = _variant_t(attachmentPath.c_str());//COleVariant(attachmentPath.c_str()); 49 VARIANT vType; 50 vType.vt = VT_I4; 51 vType.lVal = 1; 52 VARIANT vPosition; 53 vPosition.vt = VT_I4; 54 vPosition.lVal = 1; 55 VARIANT vNick; 56 vNick.vt = VT_BSTR; 57 vNick.bstrVal = SysAllocString(attachmentPath.c_str()); 58 olAtta.Add(vFile, vType, vPosition, vNick); 59 SysFreeString(vNick.bstrVal); 60 } 61 62 if (showUI) 63 { 64 VARIANT vParam; 65 vParam.vt = VT_BOOL; 66 vParam.boolVal = showUI; 67 olMail.Display(vParam); 68 } 69 else 70 { 71 // Send the message! 72 olMail.Send(); 73 } 74 olNs.Logoff(); 75 //olApp.Quit(); 76 } 77 catch (_com_error &err) 78 { 79 MessageBox(L"Outlook throws the error: %s\n", err.ErrorMessage()); 80 MessageBox(L"Description: %s\n", (LPCWSTR)err.Description()); 81 } 82 }
在COM里面传递的数据全部要通过VARIANT的类型来传递,它的vt属性表示传递的数据类型,不同的数据类型要用不同的联合属性表示,对应关系参见以下链接:
http://blog.csdn.net/heaven13483/article/details/8259110
三、eraly binding和late binding
有的同学说:在非MFC的c++项目里,用第一种方法就够了,干嘛要介绍第二种呢。的确,第一种方法有生成好的接口,方便我们理清接口逻辑关系、进行调试和维护,这种在导入后就已经生成固定接口的方式叫做early binding。程序像调用普通函数一样,在调用时知道这个函数的功能和逻辑,能够进行类型检查等安全措施。而第二种方法里,程序只是调用API告诉com组件我要调用哪个函数,至于它具体执行的功能程序并不知道,这就叫做late binding,也可以叫做动态绑定。
early binding确实有很多优点,由于接口在导入后就已经固定,它的速度比late binding快一倍;它能进行类型检查方便维护;它的逻辑代码简单......它的缺点是导入类型库并不稳定,有时候总是会出现各种错误。另外还有outlook版本兼容性的问题,这个下面再谈。
使用late binding的优点正是不需要特殊的类型库或者头文件,直接调用com的API就可以了,也就是说,这种方法在哪都能用。
详细地介绍可以参考以下链接:
https://msdn.microsoft.com/en-us/library/office/bb610234.aspx?f=255&MSPPError=-2147217396
https://support.microsoft.com/en-us/kb/245115
四、outlook版本库兼容问题
因为可能不同用户安装的的office版本是不同的,所以可能他们使用的outlook版本库是不同的。不同的outlook版本有不同的类型库,对应如下:
Outlook Version | Type Library |
---|---|
97 | msoutl8.olb |
98 | msoutl85.olb |
2000 | msoutl9.olb |
2002 | msoutl.olb |
2003 | msoutl.olb |
可以参考以下链接:
https://support.microsoft.com/en-us/kb/220600
怎么才能使我们的程序兼容不同的outlook版本呢?有以下两个方案:
1、使用early bind/MFC,使用要兼容的最早的版本的类库。因为early bind必须要选择一个类库来导入,而office版本是向下兼容的,所以如果我们最早要兼容到office 2003,就选择office 2003的版本的类型库。
2、使用late binding。这应该是late binding最大的优点了,即它不需要指定outlook版本,它会自动根据用户当前安装的office版本选择要使用的类型库。