Messaging-enabled Programs with Exchange
Presentation 487
Bob Combs, Director of
Technology
PCSI, Inc.
3 University Plaza, Suite
600
Hackensack, NJ 07601
Tel. (201) 488-7222
Abstract
Exchange
Server is a fairly popular email system, and many application developers would
like to message-enable their applications. This paper will list the various
methods for programming to Exchange, and then it will delve into code showing
real working examples.
Microsoft uses the term message-enabled to mean that an application can access Exchange folders to augment the application's operation. Exchange Server is Microsoft's server-based email system that supplies folders for email, public information, and schedules.
Exchange is a server-based email system, which means all processing rules and primary storage of messages is maintained on the server. The client itself, while sophisticated, does not contain the main storage and rules for processing messages. The advantage of a server-base email system is that the rules and processing is maintained in one location and does not have to be distributed to every client machine that uses the email system. This was a problem with earlier email systems such as Microsoft Mail, cc:Mail, etc.
Having a more powerful and stable email system has also meant that application programs want to use email capabilities. Exchange provides several Application Programming Interface (API) sets to use in coding messaging access.
The messaging API sets available for Exchange are
bv |
Description |
Languages supported |
CMC |
Common Messaging Calls |
C++ |
Simple MAPI |
Simple Messaging API |
VB, C++ |
MAPI (Extended MAPI) |
Extended Messaging API |
C++ |
CDO |
Collaborative Data Objects |
VB, C++ |
The Simple Messaging Application Program Interface (simple MAPI) is the most straightforward set of library routines available for sending and receiving e-mail messages. However, it is severely limited in what it can do.
The Common Messaging Calls (CMC) library will allow code to be transportable between Microsoft Exchange and other e-mail systems, but adds little functionality over simple MAPI. Unfortunately, CMC can be called only from C/C++ and not from Visual Basic as simple MAPI can.
The full power and complexities of Exchange are available using Extended MAPI. Extended MAPI looks nothing like Simple MAPI, having hundreds of routines, and is very complex to code. Extended MAPI is only ever coded in C/C++.
CDO is an object-oriented interface to e-mail. It started out as OLE Messaging version 1.0. As of Exchange Server 5.0, it was renamed to Active Messaging version 1.1. It is currently named CDO for version 1.2. CDO 1.2 is included with Exchange Server 5.5. There is a slightly newer version of CDO, 1.21, that is shipped with Outlook 98.
The Exchange messaging system uses object-oriented storage. An email message is stored in the Message object, which has a long list of base properties or attributes. Here is a quick list of the typical properties that might be needed to send a message.
Property |
Description |
PR_BODY |
The message text body. |
PR_MESSAGE_CLASS |
The message class
type; for normal email this is IPM.Note |
PR_PRIORITY |
The message priority;
defaults to Normal. |
PR_SUBJECT |
The message subject
text. |
PR_ATTACH_METHOD |
The attached file
manner. Programs can use ATTACH_OLE
for object streams. |
PR_ATTACH_DATA_OBJ |
The attachment data. |
PR_DISPLAY_NAME |
The attachment displayed
filename. |
Simple MAPI has only a handful of routines. Those routines are used to send and receive mail, with or without attached files. Below is an example of a simple MAPI program to send an email.
// SendXMsg.cpp
//
// Purpose: To send a message with an attachment to an Exchange
// recipient using simple MAPI.
//
//
// 960423 original
//
/////////////////////////////////////////////////////////////////////
#include <windows.h>
#include <windowsx.h>
#include <fstream.h>
#include <mapi.h>
#include <stdio.h>
#define MAIN
#include "mapinit.h"
SYSTEMTIME LocalTime;
char msglog[80];
char szDate[10];
char szTime[10];
char buffer[1];
int retcode = 0;
LHANDLE lplhSession;
HANDLE hLibrary;
void main()
{
ULONG hr;
char sCurrentTime[40];
// Open log file
ofstream ofLogFile("c:\\temp\\SendXMsg.log",ios::out);
// date & time
GetLocalTime(&LocalTime);
GetDateFormat(NULL,0,&LocalTime,"MM'-'dd'-'yy",szDate,10);
GetTimeFormat(NULL,0,&LocalTime,"HH':'mm':'ss",szTime,10);
strncpy(msglog,"SendXMsg - Begin date/time: ",79);
strcat(msglog,szDate);
strncat(msglog," ",1);
strcat(msglog,szTime);
strcat(msglog,"\n");
ofLogFile.write(msglog,strlen(msglog));
// declare variables
MapiMessage Message, *lpMessage=&Message;
MapiRecipDesc Recip, *lpRecip=&Recip;
// Load MAPI.DLL
if (InitMAPI()) {
strncpy(msglog,"Initialization to MAPI.DLL failed.\n",79);
ofLogFile.write(msglog,strlen(msglog));
retcode = 1;
}
else {
strcpy(sCurrentTime, "1996/05/23 14:55");
lpMessage->ulReserved = 0;
lpMessage->lpszSubject = "NOTEBOOK PRODUCTION UPLOAD";
lpMessage->lpszNoteText =
"Notebook upload to production failed - see attached log.";
lpMessage->lpszMessageType = NULL;
lpMessage->lpszDateReceived = sCurrentTime;
lpMessage->lpszConversationID = "";
lpMessage->flFlags = 0;
lpMessage->lpOriginator = NULL;
lpMessage->nRecipCount = 1;
lpMessage->lpRecips = lpRecip;
lpMessage->nFileCount = 0;
lpMessage->lpFiles = NULL;
lpRecip->ulReserved = 0;
lpRecip->ulRecipClass = MAPI_TO;
lpRecip->lpszName = "Bob Combs";
lpRecip->lpszAddress = NULL;
lpRecip->ulEIDSize = 0;
lpRecip->lpEntryID = NULL;
hr = (*lpfnMAPILogon) (0L,"MyProfile",0L,
MAPI_NEW_SESSION | MAPI_LOGON_UI,0L,&lplhSession);
if (hr != SUCCESS_SUCCESS) {
strncpy(msglog,"Logon to Exchange failed.\n",79);
ofLogFile.write(msglog,strlen(msglog));
retcode = 2;
}
else {
hr = (*lpfnMAPISendMail)
(lplhSession,0L,lpMessage,MAPI_LOGON_UI,0L);
if (hr != SUCCESS_SUCCESS) {
strncpy(msglog,"SendMail to Exchange failed.\n",79);
ofLogFile.write(msglog,strlen(msglog));
retcode = 3;
}
if ((*lpfnMAPILogoff)
(lplhSession,0L,0L,0L) != SUCCESS_SUCCESS) {
strncpy(msglog,"Logoff from Exchange failed.\n",79);
ofLogFile.write(msglog,strlen(msglog));
retcode = 4;
}
}
DeInitMAPI();
strncpy(msglog,"Message sent to Exchange.\n",79);
ofLogFile.write(msglog,strlen(msglog));
}
// termination message and timestamp
// date & time
GetLocalTime(&LocalTime);
GetDateFormat(NULL,0,&LocalTime,"MM'-'dd'-'yy",szDate,10);
GetTimeFormat(NULL,0,&LocalTime,"HH':'mm':'ss",szTime,10);
strncpy(msglog,"End date/time: ",16);
strcat(msglog,szDate);
strncat(msglog," ",1);
strcat(msglog,szTime);
strcat(msglog,"\n");
ofLogFile.write(msglog,strlen(msglog));
// close log file
ofLogFile.close();
// exit with return code
// return retcode;
}
The Mapinit module defines the entry points to allow SendXMsg to load the Simple MAPI DLL (MAPI32.DLL).
// Mapinit.cpp
//
// Purpose: To load MAPI.DLL and acquire an entry point to the simple
// MAPI functions.
//
// 960418 original
//
/////////////////////////////////////////////////////////////////////
#include <windows.h>
#include <mapi.h>
#include "mapinit.h"
extern HANDLE hLibrary;
int FAR PASCAL InitMAPI()
{
// load the dll file MAPI.DLL
if ((hLibrary = LoadLibrary("MAPI32.DLL")) < (HANDLE)32)
return (ERR_LOAD_LIB);
// establish entry points to dll functions
// pointer to MAPILOGON - begin simple MAPI session function
if ((lpfnMAPILogon = (PFNMAPILOGON) GetProcAddress
(hLibrary,SZ_MAPILOGON)) == NULL)
return (ERR_LOAD_FUNC);
// pointer to MAPILOGOFF - end simple MAPI session function
if ((lpfnMAPILogoff = (PFNMAPILOGOFF) GetProcAddress
(hLibrary,SZ_MAPILOGOFF)) == NULL)
return (ERR_LOAD_FUNC);
// pointer to MAPISENDMAIL - send message function
if ((lpfnMAPISendMail = (PFNMAPISENDMAIL) GetProcAddress
(hLibrary,SZ_MAPISENDMAIL)) == NULL)
return (ERR_LOAD_FUNC);
return (0);
}
int FAR PASCAL DeInitMAPI()
{
lpfnMAPILogon = NULL;
lpfnMAPILogoff = NULL;
lpfnMAPISendMail = NULL;
FreeLibrary(hLibrary);
return (0);
}
Mapinit.h is the include file.
// Program: Mapinit.h
//
// Purpose: Header definitions file for Mapinit
//
// 960418 original
//
/////////////////////////////////////////////////////////////////////
#define SZ_MAPILOGON "MAPILogon"
#define SZ_MAPILOGOFF "MAPILogoff"
#define SZ_MAPISENDMAIL "MAPISendMail"
#define ERR_LOAD_LIB 0x02
#define ERR_LOAD_FUNC 0x04
typedef ULONG (FAR PASCAL *PFNMAPILOGON) (ULONG, LPTSTR, LPTSTR, FLAGS, ULONG, LPLHANDLE);
typedef ULONG (FAR PASCAL *PFNMAPILOGOFF) (LHANDLE, ULONG, FLAGS, ULONG);
typedef ULONG (FAR PASCAL *PFNMAPISENDMAIL) (LHANDLE, ULONG, lpMapiMessage, FLAGS, ULONG);
#ifdef MAIN
PFNMAPILOGON lpfnMAPILogon;
PFNMAPILOGOFF lpfnMAPILogoff;
PFNMAPISENDMAIL lpfnMAPISendMail;
#else
extern PFNMAPILOGON lpfnMAPILogon;
extern PFNMAPILOGOFF lpfnMAPILogoff;
extern PFNMAPISENDMAIL lpfnMAPISendMail;
#endif
int FAR PASCAL InitMAPI(void);
int FAR PASCAL DeInitMAPI(void);
The CDO libraries are actually two libraries; CDO.DLL and CDOHTML.DLL. The CDO library is used for direct client programming. The CDOHTML library is used for rendering to HTML. There is an invaluable file included with CDO named CDO.HLP. This is the programmer's help file, containing details of each object and all methods and properties available.
CDO does not install automatically from the Exchange Server CD-ROM. You must explicitly copy the CDO files onto your hard drive. Those files are CDO.DLL, CDOHTML.DLL, and CDO.HLP. Copy them to \winnt\system32 and then run regsvr32 cdo.dll to register CDO. If you're not rendering to HTML you don't need to copy the CDOHTML.DLL file. You should note that when CDO is invoked it calls MAPI32.DLL routines. Therefore, the Windows Messaging Subsystem should be installed on your client.
The base object is the Session object. All CDO objects are derived from the Session object. The first thing any program must do is create the Session object and log onto Exchange using the profile name. If a profile name is not supplied to logon then either the default profile will be used or a pop-up dialog will allow the user to select one. Since we want this to happen programmatically, we should always supply the profile name. Many of the next level objects down from Session are collections of objects, for example a folder of messages such as the Inbox.
After the Session object is created and Logon has been performed, the Message object should be instantiated. The Message object has a Recipients list object which must include the person(s) to send the e-mail to. It could additionally include CC and BC recipients as well, but a single TO recipient is minimally required. CDO supports sending attached files in the message. The message has an Attachments object that consists of one or more attachment objects.
The following table describes the basic CDO objects.
Object |
Use |
Session |
Logon session |
Address Entries |
Address book entries |
Address Lists |
Distribution lists |
Attachments |
Attachments to a message |
Fields |
Fields in a message |
Messages |
Messages |
Recipients |
Message recipients to send to |
CDO is used in VB projects by selecting Project, References
from the VB menu and checking the line that reads Microsoft CDO 1.2 Library.
The SendMsg example is a simple example of sending a message. It accepts a subject line, message body, recipients, and a filename to attach. Both the recipient and filename fields use helper controls to browse for the desired reference.
The form used is:
The source code for SendMsg is:
' references CDO and Common Dialog Control 6.0
Dim objSession As MAPI.Session
Dim objOutbox As Folder
Dim objMessages As Messages
Dim objMessage As Message
Dim objRecipients As MAPI.Recipients
Dim sAttachmentFileName As String
Private Sub CommandExit_Click()
On Error Resume Next
FormSend.MousePointer = vbHourglass
Set objMessage = Nothing
Set objMessages = Nothing
On Error GoTo 0
Set objOutbox = Nothing
objSession.Logoff
Set objSession = Nothing
FormSend.MousePointer = vbDefault
End
End Sub
Private Sub CommandFilename_Click()
CommonDialogFilename.ShowOpen
TextFilename = CommonDialogFilename.FileName
TextSubject = TextFilename
End Sub
Private Sub CommandSend_Click()
Set objMessage = objMessages.Add
If objMessage Is Nothing Then
Exit Sub
End If
objMessage.Subject = TextSubject.Text
objMessage.Text = TextBody.Text
objMessage.Recipients = objRecipients
With objMessage.Attachments
If TextFilename <> "" Then
sAttachmentFileName = TextFilename
Call .Add(TextFilename, , , sAttachmentFileName)
End If
End With
Call objMessage.Send(False, False)
Set objMessage = Nothing
Call CommandExit_Click
End Sub
Private Sub Form_Load()
Set objSession = CreateObject("MAPI.Session")
' objSession.Logon ("Bob Combs")
objSession.Logon
Set objOutbox = objSession.Outbox
If objOutbox Is Nothing Then
Exit Sub
End If
Set objMessages = objOutbox.Messages
If objMessages Is Nothing Then
Exit Sub
End If
TextClass = "myapp.file"
TextBody = "Your message here ..."
End Sub
Private Sub TextRecipient_Click()
Dim i, iCount As Integer
Set objRecipients = objSession.AddressBook(objRecipients, _
Title:="Select Recipients", _
forceResolution:=True, _
recipLists:=2)
TextRecipient.Text = ""
iCount = objRecipients.Count
For i = 1 To iCount Step 1
TextRecipient = TextRecipient & objRecipients.Item(i) & ";"
Next i
End Sub
It is possible to call CDO objects from C++, as can be seen in this example. This is not normally recommend since it is easier to use from VB. One usually uses C++ when additional functionality is needed, which results in coding in C++ for extended MAPI.
// CdoSendMsg.cpp
//
// Example of sending a message using CDO instead of MAPI.
//
///////////////////////////////////////////////////////////////
#include <windows.h>
int main()
{
BSTR bstrProfile;
CLSID clsid;
DISPPARAMS dispArgs;
DISPID dispid;
HRESULT hr;
IDispatch* pSession=NULL;
IUnknown* pUnk=NULL;
VARIANT varResult;
VARIANTARG varg;
OLECHAR* logon=L"Logon";
wchar_t wszProfile[]=L"MyProfile";
wchar_t wszProgID[]=L"MAPI.Session";
// get the class ID for MAPI.Session
hr=CLSIDFromProgID(wszProgID, &clsid);
// initialize COM
hr=CoInitialize(NULL);
if(FAILED(hr)) {
MessageBox(NULL, "Unable to find CDO object", "Error", MB_OK);
exit(1);
}
// create object
hr=CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown,
(void**)&pUnk);
hr=pUnk->QueryInterface(IID_IDispatch, (void**)&pSession);
pUnk->Release();
// find the Logon method's dispid
hr=pSession->GetIDsOfNames(IID_NULL, &logon, 1, GetUserDefaultLCID(),
&dispid);
// build variant variable structure for passing parameter(s)
bstrProfile=SysAllocString(wszProfile);
VariantInit(&varg);
varg.vt=VT_BSTR;
varg.bstrVal=bstrProfile;
dispArgs.cArgs=1;
dispArgs.rgvarg=&varg;
dispArgs.cNamedArgs=0;
dispArgs.rgdispidNamedArgs = NULL;
// use the IDispatch interface to invoke Logon routine
pSession->Invoke(dispid,
IID_NULL,
LOCALE_USER_DEFAULT,
DISPATCH_METHOD,
&dispArgs,
&varResult,
NULL,
NULL);
SysFreeString(bstrProfile);
// Place other code here
// terminate session object & COM
pSession->Release();
CoUninitialize();
return 0;
}
While we can't cover extended MAPI in a this paper, suffice it to say that extended MAPI is by far the most complex of the methods to manipulate Exchange. Below is a snippet of code that should give the general flavor of extended MAPI coding.
<< code deleted for brevity >>
// Initialize the MAPI libraries before calling ANY MAPI function.
hResult=MAPIInitialize(NULL);
if (FAILED(hResult)) {
LogErr("Failed to initialize MAPI.", hResult);
goto error_exit;
}
// Logon to the message subsystem. We are going to ask the user to
// select a profile to log into. The UI for this will be provided by
// MAPI.
hResult=MAPILogonEx(0, szProfile, NULL, 0, &pSession);
if (FAILED(hResult)) {
LogErr("Failed to logon.", hResult);
goto error_exit;
}
<< code deleted for brevity >>
// Obtain new message object (IMessage interface)
hResult=lpInbox->CreateMessage(NULL,0,&lpMessage);
if (FAILED(hResult)) {
LogErr("Create message failed.",hResult);
goto error_exit;
}
// Allocate memory for property array
cProps = 6;
if (MAPIAllocateBuffer(cProps * sizeof(SPropValue),
(LPVOID *) &lpProps) != S_OK) {
LogErr("Unable to allocate memory for properties.", nError);
goto error_exit;
}
// Setup the properties
i=0;
lpProps[i].ulPropTag = PR_MESSAGE_CLASS;
lpProps[i++].Value.lpszA = "IPM.Note";
lpProps[i].ulPropTag = PR_PRIORITY;
lpProps[i++].Value.l = 0;
lpProps[i].ulPropTag = PR_SUBJECT;
lpProps[i++].Value.lpszA = szSubject;
lpProps[i].ulPropTag = PR_COMMENT;
lpProps[i++].Value.lpszA = szMsgID;
// Set the message submission time
GetSystemTime(&SysTime);
// Convert to file time
SystemTimeToFileTime(&SysTime, &SubmitTime);
// Set up this property
lpProps[i].ulPropTag = PR_CLIENT_SUBMIT_TIME;
lpProps[i++].Value.ft = SubmitTime;
// Build the recipients list
hResult=GetRecipient(lpAdrBook, szTo, &lpAdrList);
if (FAILED(hResult)) {
LogErr("GetRecipient() failed.", hResult);
goto error_exit;
}
// Set the properties in the message object
hResult=lpMessage->SetProps(i, lpProps, &lpPropProblems);
if (FAILED(hResult)) {
LogErr("Set Props failed.",hResult);
goto error_exit;
}
if(hResult == S_OK)
if(lpPropProblems) {
MAPIFreeBuffer(lpPropProblems);
lpPropProblems=NULL;
}
// Set the recipients to the message
hResult=lpMessage->ModifyRecipients(MODRECIP_ADD, lpAdrList);
if (FAILED(hResult)) {
LogErr("Modify Recipients failed.",hResult);
goto error_exit;
}
// Open stream to file containing the attachment
strcpy(szFQN, szPath);
strcat(szFQN, szFilename);
hResult=OpenStreamOnFile(MAPIAllocateBuffer,
MAPIFreeBuffer,
STGM_READ | STGM_DIRECT,
szFQN,
NULL,
&lpSrcStream);
if (FAILED(hResult)) {
LogErr("Open stream on file failed.",hResult);
goto error_exit;
}
// Create an attachment object
hResult=lpMessage->CreateAttach(NULL,0,&lAttachNum,&lpAttach);
if (FAILED(hResult)) {
LogErr("Create attach failed.",hResult);
goto error_exit;
}
// Open stream to attachment
hResult=lpAttach->OpenProperty(PR_ATTACH_DATA_OBJ,
&(IID_IStream),
0,
MAPI_MODIFY | MAPI_CREATE,
(LPUNKNOWN*) &lpDstStream);
if (FAILED(hResult)) {
LogErr("Open Property on attach failed.",hResult);
goto error_exit;
}
// Get size of input stream file
lpSrcStream->Stat(&StatInfo, STATFLAG_NONAME);
// Copy file into attachment
hResult=lpSrcStream->CopyTo(lpDstStream,StatInfo.cbSize,NULL,NULL);
if (FAILED(hResult)) {
LogErr("Copy To stream failed.",hResult);
goto error_exit;
}
// Setup the properties for the attachment (re-use same memory area)
i=0;
// Rendering position must be set so that an icon displays in the message
// text area
lpProps[i].ulPropTag = PR_RENDERING_POSITION;
lpProps[i++].Value.l = 2;
lpProps[i].ulPropTag = PR_ATTACH_METHOD;
lpProps[i++].Value.l = ATTACH_OLE;
// Set the remaining properties for the attachment
hResult=lpAttach->SetProps(i, lpProps, &lpPropProblems);
if (FAILED(hResult)) {
LogErr("Set Props failed.",hResult);
goto error_exit;
}
if(hResult == S_OK)
if(lpPropProblems) {
MAPIFreeBuffer(lpPropProblems);
lpPropProblems=NULL;
}
// Save attachment
hResult=lpAttach->SaveChanges(KEEP_OPEN_READWRITE);
if (FAILED(hResult)) {
LogErr("Failure to save changes on attach.",hResult);
goto error_exit;
}
// Save message
hResult=lpMessage->SaveChanges(KEEP_OPEN_READWRITE);
if (FAILED(hResult)) {
LogErr("Failure to save changes on message.",hResult);
goto error_exit;
}
// Send the message
hResult=lpMessage->SubmitMessage(FORCE_SUBMIT);
if (FAILED(hResult)) {
LogErr("Submit Message failed.",hResult);
goto error_exit;
}