Are you a world traveler? ZoneTick is a cool utility that'll help you stay in touch over multiple time zones!
 
Writing Simple Exchange Server Client  
Nik Okuntseff  MS Exchange Server Programming 

Writing Simple Exchange Server Client

One topic that is worth researching is how can we write the simplest Exchange server mail client program. How difficult is it to write a program that sends an e-mail message via Exchange? This section investigates this in detail. As usual, sample code (GAL/Client) is presented.

If we want to send an e-mail message, the following algorithm may be used:

  • Open the OUT folder of a particular mailbox.
  • Create a message there.
  • Modify its properties as required and save the changes.
  • Prepare the recipients list.
  • Insert recipients list into the message.
  • Call the SubmitMessage method.
Some operations listed above are not trivial and require several steps. I have compiled a program that sends a simple e-mail message to myself via the Exchange server. Here is the code:

#include <afxwin.h>
#include <edk.h>

int main()
{
    HRESULT hRes;
    LPMAPISESSION pSession = NULL;

    // Initialize MAPI
    hRes = MAPIInitialize(NULL);
        if (FAILED(hRes)) throw -1;

    // Obtain MAPI session
    hRes = MAPILogonEx(0,    // Handle to parent window
          "MS Exchange Settings", // Profile name
          NULL,   // Password
          MAPI_NEW_SESSION |
          MAPI_EXTENDED |
          MAPI_LOGON_UI, // Logon flags
          &pSession);  // Resulting MAPI session
    if (FAILED(hRes)) throw -1;

    // Find default message store
    ULONG cbDefStoreEid = 0;
    LPENTRYID pDefStoreEid = NULL;
    hRes = HrMAPIFindDefaultMsgStore(pSession, &cbDefStoreEid, &pDefStoreEid);
    if (FAILED(hRes)) throw -1;

    // Open default message store
    LPMDB pDefMsgStore = NULL;
    hRes = pSession->OpenMsgStore(0, cbDefStoreEid, pDefStoreEid, NULL,
        MDB_WRITE | MAPI_DEFERRED_ERRORS, &pDefMsgStore);
    if (FAILED(hRes)) throw -1;

    // Open user private information store
    LPMDB pUserMsgStore = NULL;
    hRes = HrMailboxLogon(pSession,
       pDefMsgStore,
       "/o=WRConsulting/ou=WhiteRock/cn=Configuration/cn=Servers/cn=MIG/cn=Microsoft Private MDB",
       "/o=WRConsulting/ou=WhiteRock/cn=Recipients/cn=NiKO", // Mailbox DN
       &pUserMsgStore);
    if (FAILED(hRes)) throw -1;

    // Open user message store root object
    ULONG ulObjType = 0;
    LPUNKNOWN pRoot = NULL;
    hRes = pUserMsgStore->OpenEntry(0,
         NULL,
         NULL,
         MAPI_BEST_ACCESS,
         &ulObjType,
         &pRoot);
    if (FAILED(hRes)) throw -1;

    // Obtain Entry ID for outbox.
    ULONG ulValues;
    LPSPropValue pPropValues = NULL;
    hRes = pUserMsgStore->GetProps(NULL,
         0,
         &ulValues,
         &pPropValues);
    if (FAILED(hRes)) throw -1;

    // Find PR_IPM_OUTBOX_ENTRYID
    BOOL bOutboxFound = FALSE;
    for (ULONG ul = 0; ul < ulValues; ul++)
    {
        if (PR_IPM_OUTBOX_ENTRYID == pPropValues[ul].ulPropTag)
        {
           bOutboxFound = TRUE;
           break;
        }
    }
    assert(bOutboxFound);
    // We have PR_IPM_OUTBOX_ENTRYID here

    // Try to open the Outbox
    LPUNKNOWN pOutbox = NULL;
    hRes = pUserMsgStore->OpenEntry(pPropValues[ul].Value.bin.cb,
         (ENTRYID *) pPropValues[ul].Value.bin.lpb,
         NULL,
         MAPI_MODIFY, // MAPI_BEST_ACCESS,
         &ulObjType,
         &pOutbox);
    if (FAILED(hRes)) throw -1;
    // We have the Outbox opened...

    // Create a message in there
    LPMESSAGE pMsg = NULL;
    hRes = ((IMAPIFolder *) pOutbox)->CreateMessage(NULL, 0, &pMsg);
    if (FAILED(hRes)) throw -1;

    // We need to modify message properties (introduce a few others).

    // Get properties.
    ULONG ulVals;
    LPSPropValue pPropVals = NULL;
    hRes = pMsg->GetProps(NULL,
       0,
       &ulVals,
       &pPropVals);
    if (FAILED(hRes)) throw -1;

    // Allocate another (bigger) SPropValue array
    SPropValue * pNewProps = new SPropValue[ulVals + 3];
    assert(pNewProps);

    // Copy original data in
    for (ul = 0; ul < ulVals; ul++)
    {
        pNewProps[ul].ulPropTag = pPropVals[ul].ulPropTag;
        pNewProps[ul].Value.bin = pPropVals[ul].Value.bin;
    }

    // Introduce subject
    pNewProps[ulVals].ulPropTag = PR_SUBJECT;
    pNewProps[ulVals++].Value.lpszA = "My subject";

    // Introduce body
    pNewProps[ulVals].ulPropTag = PR_BODY;
    pNewProps[ulVals++].Value.lpszA = "My body";

    // Introduce PR_DELETE_AFTER_SUBMIT
    // This is needed, otherwise a copy stays in the Outbox
    pNewProps[ulVals].ulPropTag = PR_DELETE_AFTER_SUBMIT;
    pNewProps[ulVals++].Value.b = TRUE;

    // Set props
    hRes = pMsg->SetProps(ulVals,
       pNewProps,
       NULL);
    if (FAILED(hRes)) throw -1;

    // Save changes
    hRes = pMsg->SaveChanges(KEEP_OPEN_READWRITE);
    if (FAILED(hRes)) throw -1;

    // Prepare recipients list...
    ADRLIST * pal = NULL;
    MAPIAllocateBuffer(CbNewADRLIST(1), (LPVOID *) &pal);
    pal->cEntries = 1;

    SPropValue * pPropArray = NULL;
    MAPIAllocateBuffer(3 * sizeof(SPropValue), (LPVOID*) &pPropArray);

    pPropArray[0].ulPropTag = PR_RECIPIENT_TYPE;
    pPropArray[0].Value.l = MAPI_TO;

    pPropArray[1].ulPropTag = PR_DISPLAY_NAME;
    pPropArray[1].Value.lpszA = "Nik Okuntseff";

    pPropArray[2].ulPropTag = PR_ADDRTYPE;
    pPropArray[2].Value.lpszA = "EX";  // Exchange address type

    pal->aEntries[0].ulReserved1 = 0;
    pal->aEntries[0].cValues = 3;   // This "3" is important, otherwise the ResolveName fails!
    pal->aEntries[0].rgPropVals = pPropArray;

    // We'll IAddrBook interface to resolve display names to entry IDs.
    LPADRBOOK pAdrBook = NULL;
    hRes = pSession->OpenAddressBook(0, 0, MAPI_ACCESS_MODIFY, &pAdrBook );
    if (FAILED(hRes)) throw -1;
 
    // Call IAdrBook::ResolveName method to resolve display names in our list to entry IDs.
    hRes = pAdrBook->ResolveName(0, 0, NULL, pal);
    if (FAILED(hRes)) throw -1;

    // Insert our recipients list into the message
    hRes = pMsg->ModifyRecipients(0, pal);
    if (FAILED(hRes)) throw -1;

    // Submit
    hRes = pMsg->SubmitMessage(0);
    if (FAILED(hRes)) throw -1;

    // Log off from user mailbox
    hRes = HrMailboxLogoff(&pUserMsgStore);
    if (FAILED(hRes)) throw -1;

    // Clean up and exit
    if (pAdrBook)
      pAdrBook->Release();
    if (pPropArray)
      MAPIFreeBuffer(pPropArray);
    if (pal)
      MAPIFreeBuffer(pal);
    if (pNewProps)
      MAPIFreeBuffer(pNewProps);
    if (pPropVals)
      MAPIFreeBuffer(pPropVals);
    if (pMsg)
      pMsg->Release();
    if (pOutbox)
      pOutbox->Release();
    if (pPropValues)
      MAPIFreeBuffer(pPropValues);
    if (pRoot)
      pRoot->Release();
    if (pUserMsgStore)
      pUserMsgStore->Release();
    if (pDefMsgStore)
      pDefMsgStore->Release();
    if (pDefStoreEid)
      MAPIFreeBuffer(pDefStoreEid);
    if (pSession)
      pSession->Release();

    ::MAPIUninitialize();
    return 0;
}

Let me go through this code and explain what happens. I initialize MAPI and obtain a MAPI session with the MAPILogonEx call. We need a MAPI session to open other objects. Then I need access to the OUT folder of someone's mailbox. The code above will work with any mailbox if you have necessary administrative access to Exchange server. Before you can open the OUT folder you need access to the mailbox itself. This is accomplished with the HrMailboxLogon call. It returns a pointer to the user's private information store.

The code above uses privileged access to private information stores. This means it can access anyone's mail. In fact, your security context should have enough rights to achieve this (otherwise the HrMailboxLogon fails). Server programs (gateways, mailbox agents and the like) often use this technique. Alternatively, you can open your own private message store with less powerful account (not necessarily using the HrMailboxLogon) and use it to send/receive mail for your own mailbox.

After logging on to the mailbox (my own mailbox in this case) I locate the Outbox and open it. Then I create a new message there and introduce three new properties for it: PR_SUBJECT, PR_BODY and PR_DELETE_AFTER_SUBMIT. I save the changes into the message object. Notice the use of KEEP_OPEN_READWRITE flag in the SaveChanges method. I need the message object open. If I were not using this flag the subsequent SubmitMessage call would fail.

Next step is to prepare recipients list. This is done in 2 steps. First, the recipients list is initialized with one entry containing the display name of one recipient (myself). Second, the list is resolved by using the IAddrBook::ResolveName method. This methods obtains entry IDs for recipients. After that the recipients list is ready to be inserted into the message with the IMessage::ModifyRecipients call.

Finally I submit the message to Exchange server for processing. The code is written for my own MS Exchange installation and assumes certain things (site names, DNs, etc.). You'll need to change them accordingly.

The code illustrates how a simple practical thing like sending a simple e-mail message may be accomplished in Exchange server environment. As you could see, the procedures are not trivial sometimes. A lot of things need to be taken care of. This code, as all other samples in this book does not care about error processing. If you add proper error processing for robustness, the issue becomes larger and more difficult.
 

[ Contents | Home ]

Send comments and suggestions to niko@wrconsulting.com
Copyright © 1997-1998 by Nik Okuntseff