Programmer » MAPI Utils

MAPI example code for getting folders and messages

This code (c) 2002-2006 Lucian Wischik. The code is free and anyone can do with it whatever they like, including incorporating it in commercial products.


This code is concerned with helping you traverse the folders in an MAPI message-store, and retrieve the message-content either both in plain text and in HTML. Typically the message-store will be the "Personal Folders" in Microsoft Outlook. But it also works with multiple profiles and multiple message-stores, and should also (untested) work with other Extended MAPI implementations. It also helps you traverse the folders in a standalone PST file. The code works (has been tested) with Outlook'97, with Outlook2000 (both Internet Mail Only mode and Corporate/Workgroup mode), and with outlookXP.

The code consists of seven functions, written in C++ for Extended MAPI. I use it as a unit (mapi_utils.cpp, mapi_utils.h) added to my own project. This web page contains examples of how to use the seven functions plus extra example code, documentation for the functions, the header file and the source code for them. You should copy the code and paste it into your own project. The code uses STL, the standard template library. I wrote it under Borland C++Builder 5, but it should work on most compilers without too much fuss.

The rest of this page describes the code. Please add comments and bugfixes at the bottom.

How MAPI stores its stuff

The following example shows how MAPI-implementations, profiles, message-stores and folders fit together.

 +-MAPI implementation provided by MS Outlook
 |  +-Microsoft Outlook main profile
 |  |   +-My imap account
 |  |   |   +-Inbox folder
 |  |   |   +-Archive folder
 |  |   |   |   +-Work folder
 |  |   |   |   +-Play folder
 |  |   |   +-Outbox folder
 |  |   +-A second messagestore
 |  |       +-Folder
 |  +-Another profile belonging to MS Outlook
 |      +-A messagestore
 +-MAPI implementation provided by RivalVendor
    +-Main profile in RivalVendor
    |   +-Main messagestore
    |       +-Inbox folder
    +-Second profile in RivalVendor

Technically, nowadays, any implementation of Extended MAPI is supposed to put a key in HKEY_LOCAL_MACHINE\ SOFTWARE\ Clients\ Mail\ <implementation>\ DLLPathEx. Outlook XP does this. But the older versions of Outlook don't, and I don't imagine that anyone else does. This code will work with Outlook if it is installed (even if it is an old version without DLLPathEx). And also, if any other Extended MAPI implementation is installed and has a DLLPathEx, then it will work with that as well.

Note that my code loads the appropriate MAPI library at runtime. Therefore, you should not make any calls to MAPI functions directly: you should only call functions that have been GetProcAddress'd from the library. This is especially important for the MAPI utility functions (HrQueryAllRows, FreeProws &c.) which are not even implemented in Outlook'97 — if you tried to use them directly and not GetProcAddress'd, then your code would fail at load-time and wouldn't even start. The code below uses GetProcAddress for the standard MAPI functions, and provides its own implementation of the utility functions: pHrQueryAllRows, pFreeProws &c.

To retrieve the HTML content of a message is not documented by Microsoft for extended-MAPI. The code below for retrieving HTML text has worked on the thousand or so messages I've tried it on, but I can't guarantee it will work for everything.

Microsoft claim that Extended MAPI is 'unsupported' for Outlook 2000 in Internet Mail Only mode. Nevertheless, it mostly all works. The only thing that fails is loading a standalone PST file. But for Outlook'97 and OutlookXp this isn't an issue — neither of these use the crippled Internet Mail Only mode.

I acknowledge with gratitude the help from people on the newsgroup microsoft.public.win32.programmer.messaging, and especially the MS reps on that newsgroup.

Examples of how to program with it

To get a list of all message-stores.

The following code retrieves every profile/storename pair for the default x-mapi implementation. (Or, if the default email client doesn't support x-mapi, as Outlook Express doesn't, then it retrieves it for the first one that does.)

if (mapi_Libraries.size()>0) // we require at least one mapi-x implementation
{ mapi_EnsureStores(mapi_Libraries.front().path);
  for (list<mapi_TStoreInfo>::const_iterator j=mapi_Stores.begin(); j!=mapi_Stores.end(); j++)
  { if (j->type==mstStore)
    { //... do something with j->profile and j->store

Note: it's important to call mapi_EnsureFinished() at some point (any point) after you have released all your pointers. If you fail to call mapi_EnsureFinished(), then the store won't be closed properly, and Outlook will complain.

To get default profile and message-store.

The following code retrieves the default profile/storename for the default x-mapi:

string profile="", storename="";
if (mapi_Libraries.size()>0)
{ mapi_EnsureStores(mapi_Libraries.front().path);
  list<mapi_TStoreInfo>::const_iterator j=mapi_Stores.begin(); j++;
  if (j->type==mstStore)
  { profile=j->profile; storename=j->store;
    // ... and do something with them

To iterate over all folders.

The following code code, given a profile/storename, retrieves the list of folders in it, and iterates over all the folders. It assumes a variable 'hwnd' which is the window-handle of the application's main window.

for (list<mapi_TFolderInfo>::const_iterator i=mapi_Folders.begin(); i!=mapi_Folders.end(); i++)
{ IMAPIFolder *ifolder; ULONG ftype;
  HRESULT hr = mapi_msgstore->OpenEntry(i->eid.size,i->eid.ab, NULL,0,&ftype,(IUnknown**)&ifolder);
  if (hr==S_OK)
  { if (ftype==MAPI_FOLDER)
    { // ... and do something with the ifolder, like iterating over its messages

To load a PST file.

The following code, given the filename of a PST file, retrieves the list of folders in it. As before, it assumes 'hwnd'.

for (list<mapi_TFolderInfo>::const_iterator i=mapi_Folders.begin(); i!=mapi_Folders.end(); i++)
{ IMAPIFolder *ifolder; ULONG ftype;
  HRESULT hr = mapi_msgstore->OpenEntry(i->eid.size,i->eid.ab, NULL,0,&ftype,(IUnknown**)&ifolder);
  if (hr==S_OK)
  { if (ftype==MAPI_FOLDER)
    { // ... and do something with the ifolder, like iterating over its messages

To iterate over messages in a folder.

The following code, given a pointer 'ifolder' to an IMAPIFolder, iterates over all the messages in it.

IMAPITable *contents;
HRESULT hr = ifolder->GetContentsTable(0,&contents);
if (hr==S_OK)
  SRowSet *frows;
  hr = pHrQueryAllRows(contents,(SPropTagArray*)&foldcols,NULL,NULL,0,&frows);
  if (hr==S_OK)
  { for (unsigned int m=0; m<frows->cRows; m++)
    { // We'll first retrieve the basic header information of the message.
      // We must do this now, from the contents-table, because if the
      // message is offline and we're using Outlook2000/IMO then it's not
      // possible to IMessage::GetProps.
      // NB. In this headers, "sent-representing" is available, but none of the
      // other sender properties are.
      mapi_TEntryid eid; string subj,sndr,date;
      if (frows->aRow[m].lpProps[0].ulPropTag==PR_ENTRYID) eid=&frows->aRow[m].lpProps[0];
      if (frows->aRow[m].lpProps[1].ulPropTag==PR_SUBJECT) subj=frows->aRow[m].lpProps[1].Value.lpszA;
      if (frows->aRow[m].lpProps[2].ulPropTag==PR_SENT_REPRESENTING_NAME) sndr=frows->aRow[m].lpProps[2].Value.lpszA;
      if (frows->aRow[m].lpProps[3].ulPropTag==PR_MESSAGE_DELIVERY_TIME)
      { FILETIME ft; FileTimeToLocalFileTime(&frows->aRow[m].lpProps[3].Value.ft, &ft); // Translate from UTC to current timezone
        SYSTEMTIME st; FileTimeToSystemTime(&ft,&st);
        const char *days[] = {"Sun ","Mon ","Tue ","Wed ","Thu ","Fri ","Sat "};
        const char *day=0; if (st.wDayOfWeek<=6) day=days[st.wDayOfWeek];
        const char *months[] = {"","Jan ","Feb ","Mar ","Apr ","May ","Jun ","Jul ","Aug ","Sep ","Oct ","Nov ","Dec "};
        const char *month=0; if (st.wMonth>=1 && st.wMonth<=12) month=months[st.wMonth];
        char c[20]; wsprintf(c,"%s%i %s%i",day,st.wDay,month,st.wYear); date=c;
      string message_details = sndr+" - "+subj+" - "+date;
      // Now, 'message_details' shows the important stuff about the message,
      // so we could use it in (e.g.) error reports if some other bit of code fails.
      if (!eid.isempty())
      { IMessage *imsg; ULONG msgtype;
        hr = ifolder->OpenEntry(eid.size, eid.ab, NULL, 0, &msgtype, (IUnknown**)&imsg);
        if (hr==S_OK)
        { if (msgtype==MAPI_MESSAGE)
          { //
            // do something with the imsg here, like read its body...

To retrieve message text and properties.

The following code, given a pointer 'imsg' to an IMessage, retrieves the body of the message in both text and (if present) html, and also retrieves its 'from' and 'to' information, and determines whether the message is available offline.

bool AvailableOffline=false;
SizedSPropTagArray(8, mcols) = {8,
ULONG pcount; SPropValue *props=0; HRESULT hr;
hr = imsg->GetProps((SPropTagArray*)&mcols,0,&pcount,&props);
// Outlook2000 in IMO gives this NO_ACCESS error messages that are header-only.
// But Outlook97, and Outlook2000/CWG, and OutlookXP, don't give the error...
// we'll test for them later on.
if (hr==MAPI_E_NO_ACCESS) AvailableOffline=false;
bool okay=true;
if (hr!=S_OK && hr!=MAPI_W_ERRORS_RETURNED) okay=false;
if (okay && props[0].ulPropTag!=PR_MESSAGE_CLASS) okay=false;
if (okay && (strncmp(props[0].Value.lpszA,"IPM.Note",8)!=0 && strncmp(props[0].Value.lpszA,"IPF.Note",8)!=0)) okay=false;
if (!okay) {if (props!=0) pMAPIFreeBuffer(props); return; }

// Was it a received message, or one that the user sent?
// (This isn't recorded explicitly. We'll use a heurstic instead:
// if there's any sign that this message has been over the internet,
// then presumably it's received. If there's no such sign, then
// presumably it's just the local copy put into the 'Sent' folder)
bool isrcvd = false;
if (props[4].ulPropTag==PR_RECEIVED_BY_EMAIL_ADDRESS) isrcvd=true;
if (props[5].ulPropTag==PR_RECEIVED_BY_NAME) isrcvd=true;
if (props[6].ulPropTag==PR_RECEIVED_BY_ENTRYID) isrcvd=true;
if (props[7].ulPropTag==PR_TRANSPORT_MESSAGE_HEADERS) isrcvd=true;

// Retrieve the 'from' property
// in the form Name <email@addr>
string from;
if (props[1].ulPropTag==PR_SENDER_NAME) from=props[1].Value.lpszA;
if (props[2].ulPropTag==PR_SENDER_EMAIL_ADDRESS)
{ if (from!="") from+=" ";
  from += string("<")+props[2].Value.lpszA+">";
if (from!="") AvailableOffline = true;

// Retrieve the 'to' property
// in the form Name <email@addr>, AnotherName <email@addr>
string to;
IMAPITable *rtable;
hr = imsg->GetRecipientTable(0,&rtable);
if (hr==S_OK)
  SRowSet *rrows;
  hr = pHrQueryAllRows(rtable,(SPropTagArray*)&rcols,NULL,NULL,0,&rrows);
  if (hr==S_OK)
  { for (unsigned int r=0; r<rrows->cRows; r++)
    { string recipient;
      if (rrows->aRow[r].lpProps[0].ulPropTag==PR_DISPLAY_NAME) recipient=rrows->aRow[r].lpProps[0].Value.lpszA;
      if (rrows->aRow[r].lpProps[1].ulPropTag==PR_EMAIL_ADDRESS)
      { if (recipient!="") recipient+=" ";
        recipient += string("<")+rrows->aRow[r].lpProps[1].Value.lpszA+">";
      if (recipient!="")
      { if (to!="") to+=", ";
        to += recipient;
if (to!="") AvailableOffline = true;

// Get the body of the message as plain text
// into the buffer 'bodybuf'
char *bodybuf=0; unsigned int bodysize=0;
IStream *istream;
hr = imsg->OpenProperty(PR_BODY, &IID_IStream, STGM_READ, 0, (IUnknown**)&istream);
if (hr==S_OK)
{ AvailableOffline = true;
  STATSTG stg = {0};
  hr = istream->Stat(&stg,STATFLAG_NONAME);
  if (hr==S_OK)
  { bodysize = stg.cbSize.LowPart; // won't bother checking for >2gb messages!
    bodybuf = new char[bodysize+1];
    ULONG red; hr = istream->Read(bodybuf, bodysize, &red);
    if (hr!=S_OK) bodysize=0;
    else if (red<bodysize) bodysize=red;

// Get the body of the message if it was in HTML
// into the buffer 'htmlbuf'
char *htmlbuf=0; unsigned int htmlsize=0;
hr = imsg->OpenProperty(PR_BODY_HTML, &IID_IStream, STGM_READ, 0, (IUnknown**)&istream);
if (hr==S_OK)
{ STATSTG stg = {0};
  hr = istream->Stat(&stg,STATFLAG_NONAME);
  if (hr==S_OK)
  { htmlsize = stg.cbSize.LowPart;
    htmlbuf = new char[htmlsize+1];
    ULONG red; hr = istream->Read(htmlbuf, htmlsize, &red);
    if (hr!=S_OK) htmlsize=0;
    else if (red<htmlsize) htmlsize=red;
// In actual fact, the PR_HTML_BODY that we just tested is rarely used.
// More frequently, Microsoft encode the html message source into the RTF...
// First we have to sync the rtf with the body, in case the messsagestore hasn't already.
// (nb. it would have made more sense to query the support mask just once, rather than
// for each message as we're doing here)
SPropValue *svmask; bool mustsync=true;
hr = pHrGetOneProp(mapi_msgstore, PR_STORE_SUPPORT_MASK, &svmask);
if (hr==S_OK)
{ if ((svmask->Value.ul&STORE_RTF_OK)!=0) mustsync=false;
  else if (props[3].ulPropTag==PR_RTF_IN_SYNC && props[3].Value.b!=0) mustsync=false;
if (mustsync)
{ BOOL isupdated; pRTFSync(imsg,RTF_SYNC_BODY_CHANGED,&isupdated);
  if (isupdated) imsg->SaveChanges(0);
// Now it's synced if necessary. We can retrieve the RTF property
hr=S_FALSE; if (htmlbuf==0) hr = imsg->OpenProperty(PR_RTF_COMPRESSED, &IID_IStream, STGM_READ, 0, (IUnknown**)&istream);
if (hr==S_OK)
{ AvailableOffline = true;
  IStream *iunstream; // for the uncompressed stream
  // bufsize is the size of the buffer we've allocated, and htmlsize is the
  // amount of text we've read in so far. If our buffer wasn't big enough,
  // we enlarge it and continue. We have to do this, instead of allocating
  // it up front, because Stream::Stat() doesn't work for the
  unsigned int bufsize=10240; htmlbuf = new char[bufsize];
  htmlsize=0; bool done=false;
  while (!done)
  { ULONG red; hr = iunstream->Read(htmlbuf+htmlsize, bufsize-htmlsize, &red);
    if (hr!=S_OK) {htmlbuf[htmlsize]=0; done=true;}
    { htmlsize+=red; done = (red < bufsize-htmlsize);
      if (!done)
      { unsigned int newsize=2*htmlsize; char *newbuf=new char[newsize];
        memcpy(newbuf,htmlbuf,htmlsize); delete[] htmlbuf;
        htmlbuf=newbuf; bufsize=newsize;
  // Now, assuming that this thing was an encoded RTF/HTML hybrid,
  // we can extract the original html.
  if (htmlbuf!=0)
  { if (!isrtfhtml(htmlbuf,htmlsize)) {delete[] htmlbuf; htmlbuf=0;}
    else decodertfhtml(htmlbuf,&htmlsize);

// Check if there are any attachments
IMAPITable *atable;
hr = imsg->GetAttachmentTable(0,&atable);
if (hr==S_OK)
{ SizedSPropTagArray(3,acols) = {3, {PR_ATTACH_SIZE,PR_ATTACH_NUM,PR_ATTACH_METHOD }};
  SRowSet *arows;
  hr = pHrQueryAllRows(atable,(SPropTagArray*)&acols,NULL,NULL,0,&arows);
  if (hr==S_OK)
  { if (arows->cRows>0) AvailableOffline = true;

// AvailableOffline? We have set this boolean variable through the preceeding
// code, using the following heuristic: if there is any content to the message
// at all be it To: or From: lines, or body, or attachments), then it must have
// been downloaded. The only way this could fail, is if the user composed a
// draft message but then closed it while it was still completely empty.
// Why do we resort to such a grubby heuristic? Because Microsoft do not define
// any other way to tell whether a message is offline. OutlookXP uses some
// named properties that seem to contain the information, but they're not
// documented and therefore can't be trusted.

if (AvailableOffline)
{ //
  // Now, do something with the message contents!

if (bodybuf!=0) delete[] bodybuf;
if (htmlbuf!=0) delete[] htmlbuf;

Instructions on how to use the functions

First, call 'mapi_EnsureLibraries()'. This will build a list of all the extended-MAPI implementations on the system. The list goes in the global list "mapi_Libraries". If the default email client supports extended-MAPI, then it appears first in the list.

The list might be empty, if there's no extended-MAPI implementation installed on your computer.

Second, call 'mapi_EnsureStores(lib-path)'. This will build a list of all profiles and all message-stores belonging to this implementation. This goes in the global list "mapi_Stores". This is a heterogenous list containing entries for both profiles and message-stores. It is grouped so that all the message-stores for a particular profile come immediately after the entry for that profile. Thus, for the above example, "Main-Profile, my-imap-acc, 2nd-msgstore, Another-profile, a-msgstore" The default profile comes first in the list. And within each profile, the default message-store comes first.

You can call mapi_EnsureStores with a different library at any time. This will free the previous library.

Third, call 'mapi_EnsureFolders(hwnd,prof,store)'. This will build a list of all email folders in the store. Again, the list is flattened. The above example would be "Inbox, Archive, Archive\Work, Archive\Play, Outbox".

Alternatively, 'mapi_EnsureFolders(hwnd,pst_fn)'. This will build a list of the email folders in the specified PST file. Incidentally, this will only work if the lib-path in mapi_EnsureStores(lib-path) points to an MAPI implementation that supports PST files.

Also, the "mapi_session" global variable is initialized by these calls. It has type IMAPISession*, and you can use it to open folders and stuff.

Note, incidentaly, that both mapi_EnsureFolder calls take an HWND as their first argument. This is because, perhaps, the user might have to supply a password to logon to the message-store. The password dialog will appear as a modal child of hwnd.

You can call mapi_EnsureFolders with a different store/pst at any time. This will free the previous stuff.

Fourth, you'll have to iterate over the folders yourself, to do what you want with them. To make things easier, each folder is annotated with its entry-id: this makes it a simple task to just do mapi_session->OpenEntry(eid.size,eid.ab,...,&ifolder);

Also, each folder is annotated with its type: whether it's inbox, or outbox, or whatever. Note incidentally that "Drafts" is not distinguished from the other normal mail folders.

Finally, when finished, 'mapi_EnsureFinished()' will free everything.

Note: The code uses global static variables to remember its state. You cannot have one part of your program working with one store while at the same time another part works with a different store, for instance. Also, each procedure is called "EnsureXXX" to indicate that it doesn't matter if you call it redundantly.

Header file 'mapi_utils.h'

#ifndef mapi_utilsH
#define mapi_utilsH
#include <windows.h>
#include <mapix.h>
#include <mapiutil.h>
#include <string>
#include <list>

void mapi_EnsureLibraries();
void mapi_EnsureStores(const std::string libpath);
void mapi_EnsureFolders(HWND h, const std::string profile, const std::string store);
void mapi_EnsureFolders(HWND h, const std::string pst_fn);
void mapi_EnsureFinished();

bool isrtfhtml(const char *buf,unsigned int len);
void decodertfhtml(char *buf,unsigned int *len);

class mapi_TEntryid
{ public:
  unsigned int size;
  ENTRYID *ab;
  mapi_TEntryid() {ab=0;size=0;}
  mapi_TEntryid(SPropValue *v) {ab=0;size=0; if (v->ulPropTag!=PR_ENTRYID) return; set(v->Value.bin.cb,(ENTRYID*)v->Value.bin.lpb);}
  mapi_TEntryid(const mapi_TEntryid &e) {ab=0;size=0; set(e.size,e.ab);}
  mapi_TEntryid(unsigned int asize,ENTRYID *eid) {ab=0;size=0; set(asize,eid);}
  mapi_TEntryid &operator= (const mapi_TEntryid *e) {set(e->size,e->ab); return *this;}
  mapi_TEntryid &operator= (const SPropValue *v) {set(0,0); if (PROP_TYPE(v->ulPropTag)!=PT_BINARY) return *this; set(v->Value.bin.cb,(ENTRYID*)v->Value.bin.lpb); return *this;}
  ~mapi_TEntryid() {set(0,0);}
  void set(unsigned int asize, ENTRYID *eid) {if (ab!=0) delete[] ((char*)ab); size=asize; if (eid==0) ab=0; else {ab=(ENTRYID*)(new char[size]);memcpy(ab,eid,size);}}
  void clear() {set(0,0);}
  bool isempty() const {return (ab==0 || size==0);}
  bool isequal(IMAPISession *sesh, mapi_TEntryid const &e) const
  { if (isempty() || e.isempty()) return false;
    ULONG res; HRESULT hr = sesh->CompareEntryIDs(size,ab,e.size,e.ab,0,&res);
    if (hr!=S_OK) return false;
    return (res!=0);

typedef struct {std::string name, path; bool supported;} mapi_TLibraryInfo;

enum mapi_TFolderType {mftInbox,mftOutbox,mftSent,mftDeleted,mftCalendar,mftContacts,mftJournal,mftNotes,mftTasks,mftSpecial,mftMail,mftStuff};
typedef struct {int depth; std::string name, path; mapi_TFolderType type; mapi_TEntryid eid;} mapi_TFolderInfo; // nb. path is the complete thing, and name is just the final bit of it

enum mapi_TStoreType {mstProfile, mstProfileSecret, mstStore};
typedef struct {std::string profile, store; mapi_TStoreType type;} mapi_TStoreInfo;

// These are initialized by mapi_EnsureLibraries()
extern std::list<mapi_TLibraryInfo> mapi_Libraries;
// These are initialized by mapi_EnsureStores(lib)
extern std::list<mapi_TStoreInfo> mapi_Stores;
// And so are these mapi functions
extern RTFSYNC *pRTFSync;
// These are initialized by mapi_EnsureFolders(storeinfo)
extern IMAPISession *mapi_session;    // The session
extern IMsgStore *mapi_msgstore;      // The message store
extern std::list<mapi_TFolderInfo> mapi_Folders;
// And all are freed, if necessary, by mapi_EnsureFinished.

// I must implement these utility functions myself. That's because
// they're not present in Outlook97's version of mapi32.dll.
HRESULT pHrGetOneProp(LPMAPIPROP lpMapiProp, ULONG ulPropTag, LPSPropValue FAR *lppProp);
void pFreeProws(LPSRowSet lpRows);
HRESULT pHrQueryAllRows(LPMAPITABLE lpTable, LPSPropTagArray lpPropTags, LPSRestriction lpRestriction, LPSSortOrderSet lpSortOrderSet, LONG crowsMax, LPSRowSet FAR *lppRows);

// These were omitted from the standard headers
#ifndef PR_BODY_HTML








Source-code 'mapi_utils.cpp'

#include <windows.h>
#include <shellapi.h>
#include <mapix.h>
#include <mapiutil.h>
#include <mspst.h>
#include <string>
#include <list>
#include <map>
using namespace std;
#pragma hdrstop // precompiled headers stop here
#include "mapi_utils.h"

list<mapi_TLibraryInfo> mapi_Libraries;  // from the registry, a list of ex-mapi dlls
bool got_libraries=false;                // have we built that list yet?
HINSTANCE hmapilib=0;             // for loadlibrary(mapi32.dll). If this is non-null, we must freelibrary it at the end.
string mapi_lib_path;             // this is the pathname of what we've currently loaded.
list<mapi_TStoreInfo> mapi_Stores;
IMAPISession *mapi_session=0;     // The session
string mapi_session_profile;      // This is the profile name which session is logged onto.
IMsgStore *mapi_msgstore=0;       // The message store
string mapi_msgstore_name;        // This is the name to which that message-store refers
list<mapi_TFolderInfo> mapi_Folders;  // a list of the folders
bool got_eids = false;             // a shortcut for whether or not all the following have been set
mapi_TEntryid eid_inbox, eid_outbox, eid_sent, eid_deleted;
mapi_TEntryid eid_calendar, eid_contacts, eid_journal, eid_notes, eid_tasks;

void mapi_EnsureCommonEids();
mapi_TFolderType mapi_GetFolderType(mapi_TEntryid &eid, IMAPIFolder *f);
void mapi_RecEnsureFolders(IMAPIFolder *parent, int depth, string prefix, list<mapi_TFolderInfo> *folders);
void mapi_EnsureCrazyProfileDeleted(IProfAdmin *iprofadmin);
list<string> mapi_RegQuerySubkeys(HKEY key);
string mapi_RegQueryString(HKEY key,const string name);

// MAPI_ENSURELIBRARIES -- builds up a list of all the Extended MAPI libraries
// that have been installed on this machine, in the global mapi_Libraries
// list. This is a difficult task...
// In the olden days, there didn't exist such a list anywhere on the system.
// Very recently (with the advent of Outlook XP) it has introduced the
// idea that there should be a list under HKEY_LOCAL_MACHINE\Clients\Mail
// whereby every extended-MAPI is indicated with a "DLLPathEx" value.
// But in the olden days (with Outlook2000 in both IMO and CWG modes, and
// with Outlook97) it merely has a "DLLPath" value.
// So our plan is as follows:
// (1) For any DLLPathEx keys, we'll add them.
// (2) If one of those DLLPathEx happened to be for the "Microsoft Outlook"
// service, then it must have been XP or later, and so we can return immediately.
// (3) If Microsoft Outlook was not even installed (i.e. not in the list), then we
// can also return immediately.
// (4) Otherwise, there must have been Outlook97 or 2000 installed. So we
// will set a key under HKEY_LOCAL_MACHINE\SOFTWARE\Microsft\Windows Messaging Subsystem\MSMapiApps
// to indicate that our app will use the "Microsoft Outlook" mapi. Then, we
// add "mapi32.dll" into the list. On a modern system, when you LoadLibrary(mapi32.dll),
// this is merely a stubb: it actually looks into that list and figures out which mapi
// to use from there. Therefore, this will end up loading Outlook.
void mapi_EnsureLibraries()
{ if (got_libraries) return;
  // First, if the mapi stub has been installed, then we can check in the
  // registry for which MAPI libraries are present on this machine.
  bool uses_stub=false, IsOutlookInstalled=false, IsOutlookExListed=false;
  HKEY key; LONG res;
  res = RegOpenKeyEx(HKEY_LOCAL_MACHINE,"SOFTWARE\\Clients\\Mail",0,KEY_READ,&key);
  if (res==ERROR_SUCCESS)
  { uses_stub=true;
    string defname = mapi_RegQueryString(key,"");   // Find out which one is the default
    list<string> names = mapi_RegQuerySubkeys(key); // Get the list of child keys
    for (list<string>::const_iterator i=names.begin(); i!=names.end(); i++)
    { string name = *i;
      bool thisisoutlook = (name=="Microsoft Outlook");
      IsOutlookInstalled |= thisisoutlook;
      res = RegOpenKeyEx(HKEY_LOCAL_MACHINE,("SOFTWARE\\Clients\\Mail\\"+name).c_str(),0,KEY_READ,&key);
      if (res==ERROR_SUCCESS)
      { // Get the path, stored in "DLLPathEx"
        string path = mapi_RegQueryString(key,"DLLPathEx");
        if (path!="")
        { if (thisisoutlook) IsOutlookExListed=true;
          mapi_TLibraryInfo lib;; lib.path=path; lib.supported=true;
          if (name==defname) mapi_Libraries.push_front(lib); else mapi_Libraries.push_back(lib);
  if (IsOutlookInstalled && IsOutlookExListed) return; // outlook XP is fine as it is.
  // If it uses the stub technique, and Outlook is installed, we can set the registry
  // key to tell the stub to give us outlook
  if (uses_stub && IsOutlookInstalled)
  { res = RegOpenKeyEx(HKEY_LOCAL_MACHINE,"SOFTWARE\\Microsoft\\Windows Messaging Subsystem\\MSMapiApps",0,KEY_SET_VALUE,&key);
    if (res!=ERROR_SUCCESS) return;
    char c[MAX_PATH]; GetModuleFileName(NULL,c,MAX_PATH);
    const char *d=c+strlen(c)-1; while (d>c && *d!='/' && *d!='\\' && *d!=':') d--; if (*d=='/' || *d=='\\' || *d==':') d++;
    res = RegSetValueEx(key,d,0,REG_SZ,(LPBYTE)"Microsoft Outlook",18);
    if (res!=ERROR_SUCCESS) return;
    mapi_TLibraryInfo outlib;"Microsoft Outlook"; outlib.path="mapi32.dll";
  // Otherwise, if it doesn't even use the stub, then we'll use the old-fashioned technique.
  if (!uses_stub)
  { res = RegOpenKeyEx(HKEY_LOCAL_MACHINE,"SOFTWARE\\Microsoft\\Windows Messaging Subsystem",0,KEY_READ,&key);
    if (res!=ERROR_SUCCESS) return;
    DWORD type; DWORD size=10; char c[10];
    res = RegQueryValueEx(key,"MAPIX",NULL,&type,(LPBYTE)c,&size);
    if (res!=ERROR_SUCCESS) return;
    if (strcmp(c,"1")!=0) return;
    mapi_TLibraryInfo deflib;"Default"; deflib.path="mapi32.dll";

// MAPI_ENSURESTORES -- given a path to a mapi-x DLL, this builds up a list
// of all the profiles and message-stores listed in that mapi implementation.
// The list is heterogenous. The 'type' field in each item in the list
// says whether it's a store, or a profile, or a "secret profile" (one where
// we weren't able to get a list of its stores, presumably because this
// information is protected by a password).
// Note: the PST-file support in the rest of this program uses a temporary
// profile called "Lu's Crazy Profile (democode)". Just out of neatness,
// the list we generate will not include that profile.
void mapi_EnsureStores(const string libpath)
{ if (mapi_lib_path==libpath) return;
  // Load the library
  hmapilib = LoadLibrary(libpath.c_str()); if (hmapilib==0) return;
  MAPIINITIALIZE *pMAPIInitialize = (MAPIINITIALIZE*)GetProcAddress(hmapilib,"MAPIInitialize");
  pMAPIAdminProfiles = (MAPIADMINPROFILES*)GetProcAddress(hmapilib,"MAPIAdminProfiles");
  pMAPILogonEx = (MAPILOGONEX*)GetProcAddress(hmapilib,"MAPILogonEx");
  pMAPIUninitialize = (MAPIUNINITIALIZE*)GetProcAddress(hmapilib,"MAPIUninitialize");
  pMAPIFreeBuffer = (MAPIFREEBUFFER*)GetProcAddress(hmapilib,"MAPIFreeBuffer");
  pRTFSync = (RTFSYNC*)GetProcAddress(hmapilib,"RTFSync");
  pWrapCompressedRTFStream = (WRAPCOMPRESSEDRTFSTREAM*)GetProcAddress(hmapilib,"WrapCompressedRTFStream");
  if (pMAPIInitialize==0 || pMAPIAdminProfiles==0 || pMAPILogonEx==0 || pMAPIUninitialize==0
     || pMAPIFreeBuffer==0 || pRTFSync==0 || pWrapCompressedRTFStream==0) {FreeLibrary(hmapilib);hmapilib=0;return;}
  HRESULT hr = pMAPIInitialize(NULL); if (hr!=S_OK) {FreeLibrary(hmapilib);hmapilib=0;return;}
  // List all the profiles
  IProfAdmin *iprofadmin;
  hr = pMAPIAdminProfiles(0,&iprofadmin);
  if (hr==S_OK) 
  { list<string> profiles;
    IMAPITable *proftable;
    hr = iprofadmin->GetProfileTable(0, &proftable);
    if (hr==S_OK) 
    { SizedSPropTagArray(2, proftablecols) = { 2, {PR_DISPLAY_NAME,PR_DEFAULT_PROFILE} };
      SRowSet *profrows;
      hr = pHrQueryAllRows(proftable,(SPropTagArray*)&proftablecols,NULL,NULL,0,&profrows);
      if (hr==S_OK)
      { for (unsigned int i=0; i<profrows->cRows; i++)
        { string name=""; bool isdefault=false;
          if (profrows->aRow[i].lpProps[0].ulPropTag==PR_DISPLAY_NAME) name=profrows->aRow[i].lpProps[0].Value.lpszA;
          if (profrows->aRow[i].lpProps[1].ulPropTag==PR_DEFAULT_PROFILE) isdefault=(0!=profrows->aRow[i].lpProps[1].Value.b);
          if (name!="" && name!="Lu's Crazy Profile (democode)") {if (isdefault) profiles.push_front(name); else profiles.push_back(name);}
    // For each profile we will attempt to log on and list the message-stores it contains.
    // Plan: we will build up a list of stores just for this profile. Then we will splice it
    // onto the main global list of stores -- either at the beginning (if this is the default
    // profile) or at the end.
    for (list<string>::const_iterator profile=profiles.begin(); profile!=profiles.end(); profile++)
    { list<mapi_TStoreInfo> thesestores;
      IMAPISession *isession; bool issecret=true;
      hr = pMAPILogonEx(0,(char*)profile->c_str(),NULL,MAPI_NEW_SESSION|MAPI_EXTENDED|MAPI_NO_MAIL,&isession);
      if (hr==S_OK) 
      { IMAPITable *mstable;
        hr = isession->GetMsgStoresTable(0, &mstable);
        if (hr==S_OK) 
        { issecret=false;
          SizedSPropTagArray(2, mstablecols) = { 2, {PR_RESOURCE_FLAGS,PR_DISPLAY_NAME} };
          SRowSet *msrows;
          hr = pHrQueryAllRows(mstable,(SPropTagArray*)&mstablecols,NULL,NULL,0,&msrows);
          if (hr==S_OK) 
          { for (unsigned int i=0; i<msrows->cRows; i++)
            { string name=""; bool isdefault=false;
              if (msrows->aRow[i].lpProps[0].ulPropTag==PR_RESOURCE_FLAGS) isdefault=(msrows->aRow[i].lpProps[0].Value.ul&STATUS_DEFAULT_STORE)!=0;
              if (msrows->aRow[i].lpProps[1].ulPropTag==PR_DISPLAY_NAME) name=msrows->aRow[i].lpProps[1].Value.lpszA;
              if (name!="")
              { mapi_TStoreInfo sinfo; sinfo.profile=*profile;; sinfo.type=mstStore;
                if (isdefault) thesestores.push_front(sinfo); else thesestores.push_back(sinfo);
      mapi_TStoreInfo sinfo;  sinfo.profile=*profile;"";
      if (issecret) sinfo.type=mstProfileSecret; else sinfo.type=mstProfile;

// MAPI_ENSUREFOLDERS -- given a profile name and a store name, this builds
// up a list of all the folders in that store. Note that, in truth, the folders
// form a tree. We will flatten the tree to obtain our list. For each item
// we will record its depth, and its complete path, as well as just its name.
// To open a message-store might involve displaying a password dialog.
// For this reason, once we have opened a message-store and a profile, we
// leave both of them open, in global static variables mapi_session and mapi_msgstore.
// That means that subsequent calls for the same profile/store will not
// require another login.
void mapi_EnsureFolders(HWND hwnd, const string profile, const string store)
  if (mapi_session_profile==profile && mapi_msgstore_name==store) return;
  // First thing we do is clean out anything we've allocated before.
  if (mapi_session_profile!=profile || mapi_msgstore_name!=store)
  { mapi_msgstore_name=""; got_eids=false; if (mapi_msgstore!=0) mapi_msgstore->Release(); mapi_msgstore=0;
  if (mapi_session_profile!=profile)
  { mapi_session_profile=""; if (mapi_session!=0) {mapi_session->Logoff(0,0,0); mapi_session->Release();}
  if (hmapilib==0 || pMAPILogonEx==0) return;
  // Now we can log on to the specified profile...
  if (mapi_session_profile!=profile)
  { hr = pMAPILogonEx(PtrToUlong(hwnd),(char*)profile.c_str(),NULL,MAPI_NEW_SESSION|MAPI_EXTENDED|MAPI_NO_MAIL|MAPI_PASSWORD_UI,&mapi_session);
    if (hr!=S_OK) return;
  // ... and find the specified store.
  if (mapi_msgstore_name!=store)
  { // task is to get the store we know by name. We do this by enumerating
    // all the message-stores in the table, and picking out the one with the right name.
    IMAPITable *mstable=0;
    hr = mapi_session->GetMsgStoresTable(0, &mstable);
    if (hr==S_OK)
    { SizedSPropTagArray(2, mstablecols) = { 2, {PR_ENTRYID,PR_DISPLAY_NAME} };
      SRowSet *msrows;
      hr = pHrQueryAllRows(mstable,(SPropTagArray*)&mstablecols,NULL,NULL,0,&msrows);
      if (hr==S_OK)
      { for (unsigned int i=0; i<msrows->cRows && mapi_msgstore==0; i++)
        { string name=""; mapi_TEntryid eid;
          if (msrows->aRow[i].lpProps[0].ulPropTag==PR_ENTRYID) eid=&msrows->aRow[i].lpProps[0];
          if (msrows->aRow[i].lpProps[1].ulPropTag==PR_DISPLAY_NAME) name=msrows->aRow[i].lpProps[1].Value.lpszA;
          if (name==store && !eid.isempty())
          { hr = mapi_session->OpenMsgStore(PtrToUlong(hwnd),msrows->aRow[i].lpProps[0].Value.bin.cb,
                 (LPENTRYID)msrows->aRow[i].lpProps[0].Value.bin.lpb, NULL, MDB_NO_MAIL, &mapi_msgstore);
            if (hr!=S_OK) mapi_msgstore=0;

    if (mapi_msgstore==0) {mapi_session->Logoff(0,0,0);mapi_session->Release();mapi_session=0;mapi_session_profile="";return;}
  // Now we have the msgstore. Let's get the human (intepersonal) subtree.
  // All the email folders are children of the human subtree.
  IMAPIFolder *ipmroot=0; mapi_TEntryid eid;
  SPropValue *ipm_eid;
  hr = pHrGetOneProp(mapi_msgstore, PR_IPM_SUBTREE_ENTRYID, &ipm_eid);
  if (hr==S_OK)
  { eid = ipm_eid;
  if (!eid.isempty())
  { ULONG ipmroottype;
    hr = mapi_msgstore->OpenEntry(eid.size,eid.ab,NULL,0,&ipmroottype,(IUnknown**)&ipmroot);
    if (hr==S_OK)
    { if (ipmroottype!=MAPI_FOLDER) {ipmroot->Release(); ipmroot=0;}
  if (ipmroot==0) return;
  // the following recursive call does the work! puts the tree under "ipmroot" into mapi_Folders.

// MAPI_ENSUREFOLDERS(pst) -- Given the filename of a pst file, we create our
// own temporary profile, and add an MS-PST store to this profile, and configure
// the store to point to the specified PST file. Then, given our own profile-name
// and the name of this store, we get the previous EnsureFolders() call to build
// up a list of all the folders.
// Although we call it a temporary profile, there's not anything intrinsically
// temporary about it. Its temporariness substists in the fact that a call to
// EnsureFinished will delete it. Also, I've given it a silly name, so that
// it doesn't get mistaken for anything important!
void mapi_EnsureFolders(HWND hwnd, const string pst_fn)
  if (mapi_session_profile=="Lu's Crazy Profile (democode)" && mapi_msgstore_name==pst_fn) return;
  // First thing we do is clean out anything we've allocated before.
  mapi_msgstore_name=""; got_eids=false; if (mapi_msgstore!=0) mapi_msgstore->Release(); mapi_msgstore=0;
  mapi_session_profile=""; if (mapi_session!=0) {mapi_session->Logoff(0,0,0); mapi_session->Release();}
  if (hmapilib==0 || pMAPIAdminProfiles==0) return;
  // Plan: create a temporary profile, and add a PST service to it, configured for that filename.
  IProfAdmin *iprofadmin;
  hr = pMAPIAdminProfiles(0,&iprofadmin);
  if (hr!=S_OK) return;
  hr = iprofadmin->CreateProfile("Lu's Crazy Profile (democode)",NULL,PtrToUlong(hwnd),0);
  if (hr!=S_OK) {iprofadmin->Release(); return;}
  IMsgServiceAdmin *imsadmin;
  hr = iprofadmin->AdminServices("Lu's Crazy Profile (democode)",NULL,PtrToUlong(hwnd),0,&imsadmin);
  if (hr!=S_OK) {iprofadmin->Release(); return;}
  // Now we create the message-store-service. Read <mspst.h> for more details.
  hr = imsadmin->CreateMsgService("MSPST MS","Lu's Zany Message Store",PtrToUlong(hwnd),SERVICE_UI_ALLOWED);
  if (hr==MAPI_E_UNKNOWN_FLAGS) // Outlook97 doesn't understand those two flags at the end...
  { hr = imsadmin->CreateMsgService("MSPST MS","Lu's Zany Message Store",0,0);
  if (hr!=S_OK) {imsadmin->Release(); mapi_EnsureCrazyProfileDeleted(iprofadmin); iprofadmin->Release(); return;}
  // We need to get hold of the MAPIUID for this message-service. We do this
  // by enumerating the message-stores (there will be only one!) and picking it up.
  // Actually, we set up 'mscols' to retrieve the name as well as the MAPIUID, for
  // reasons that will become apparent in just a moment.
  IMAPITable *mstable;
  hr = imsadmin->GetMsgServiceTable(0,&mstable);
  if (hr!=S_OK) {imsadmin->Release(); mapi_EnsureCrazyProfileDeleted(iprofadmin); iprofadmin->Release(); return;}
  SizedSPropTagArray(2, mscols) = { 2, {PR_SERVICE_UID,PR_DISPLAY_NAME} };
  SRowSet *msrows;
  hr = mstable->QueryRows(1,0,&msrows);
  if (hr!=S_OK) {imsadmin->Release(); mapi_EnsureCrazyProfileDeleted(iprofadmin); iprofadmin->Release(); return;}
  MAPIUID msuid = *((MAPIUID*)msrows->aRow[0].lpProps[0].Value.bin.lpb);
  // Now we wish to configure our message-store to use the PST filename.
  SPropValue msprops[1];
  msprops[0].ulPropTag=PR_PST_PATH; msprops[0].Value.lpszA=(char*)pst_fn.c_str();
  // That will have changed the message-store's display-name. Let's get it again.
  // The 'mscols' was already set up to retrieve names. How fortunate! We won't
  // bother checking for errors here: if we could query the table before, then
  // certainly we'll be able to query it again.
  string name="";
  if (msrows->aRow[0].lpProps[1].ulPropTag==PR_DISPLAY_NAME) name=msrows->aRow[0].lpProps[1].Value.lpszA;
  // all done!
  // Finally, retrieve its folders, just as normal
  mapi_EnsureFolders(hwnd,"Lu's Crazy Profile (democode)",name);

// REC-ENSURE-FOLDERS -- Given an IMAPIFolder, we recursively retrieve
// all the folders it contains, and stick them into the 'folders' list.
// Here, "depth" and "prefix" are straightforward recursive counters
// of how deep we are in the tree.
void mapi_RecEnsureFolders(IMAPIFolder *parent, int depth, string prefix, list<mapi_TFolderInfo> *folders)
{ if (folders==0) return; if (parent==0) return;

  // NB. We cannot call parent->GetHierarchyTable. That's because GetHierarchyTable
  // tries to open it will full (read/write) access, but Outlook2000/IMO only supports
  // readonly access, hence giving an MAPI_E_NO_ACCESS error. Therefore, we get
  // the hierarchy in this roundabout way, in readonly mode.
  IMAPITable *hierarchy; HRESULT hr;
  const GUID local_IID_IMAPITable = {0x00020301,0,0, {0xC0,0,0,0,0,0,0,0x46}};
  hr = parent->OpenProperty(PR_CONTAINER_HIERARCHY,&local_IID_IMAPITable,0,0,(IUnknown**)&hierarchy);
  if (hr!=S_OK) return;
  // and query for all the rows
  SizedSPropTagArray(3, cols) = {3, {PR_ENTRYID,PR_DISPLAY_NAME,PR_SUBFOLDERS} };
  SRowSet *rows;
  hr = pHrQueryAllRows(hierarchy,(SPropTagArray*)&cols, NULL, NULL, 0, &rows);
  if (hr!=S_OK) {pFreeProws(rows); return;}
  // Note: the entry-ids returned by the list are just short-term list-specific
  // entry-ids. But we want to put long-term entry-ids in our 'folder' list.
  // That's why it's necessary to open the folder...

  // Go through all the rows. For each entry, if it is a message-folder add it, and potentially recurse
  for (unsigned int i=0; i<rows->cRows; i++)
  { BOOL subfolders = rows->aRow[i].lpProps[2].Value.b;
    string name(rows->aRow[i].lpProps[1].Value.lpszA);
    IMAPIFolder *subf; ULONG subftype;
    hr = parent->OpenEntry(rows->aRow[i].lpProps[0].Value.bin.cb,
        (LPENTRYID)rows->aRow[i].lpProps[0].Value.bin.lpb, NULL,
        0, &subftype, (IUnknown**)&subf);
    if (hr==S_OK)
    { if (subftype == MAPI_FOLDER)
      { SPropValue *veid=0;
        hr = pHrGetOneProp(subf, PR_ENTRYID, &veid); // get its long-term eid
        if (hr==S_OK)
        { mapi_TFolderInfo f;
          f.depth=depth;; f.path=prefix+name;
          f.eid = veid;
          f.type = mapi_GetFolderType(f.eid,subf);
          bool usefolder = (f.type==mftInbox||f.type==mftSent||f.type==mftMail||f.type==mftStuff);
          if (usefolder) folders->push_back(f);
          if (usefolder && subfolders) mapi_RecEnsureFolders(subf,depth+1,prefix+name+"\\",folders);


// GET-FOLDER-TYPE -- given a folder and its long-term entry-id,
// returns its type (inbox/outbox/calendar/...). There are three
// techniques for doing this; we do them all, in order of preference.
mapi_TFolderType mapi_GetFolderType(mapi_TEntryid &eid, IMAPIFolder *f)
{ // 1. Most assured way to get the type of a folder is to check
  // whether it's long-term ENTRYID is the same as one of the
  // standard ones. See the EnsureCommonEids() routine for an
  // explanation of how we retrieve the standard ones.
  if (eid.isequal(mapi_session,eid_inbox)) return mftInbox;
  if (eid.isequal(mapi_session,eid_outbox)) return mftOutbox;
  if (eid.isequal(mapi_session,eid_sent)) return mftSent;
  if (eid.isequal(mapi_session,eid_deleted)) return mftDeleted;
  // 2. Second best way, specific to Outlook, is to see if
  // it's equal to one of the Outlook specific ones.
  if (eid.isequal(mapi_session,eid_calendar)) return mftCalendar;
  if (eid.isequal(mapi_session,eid_contacts)) return mftContacts;
  if (eid.isequal(mapi_session,eid_journal)) return mftJournal;
  if (eid.isequal(mapi_session,eid_notes)) return mftNotes;
  if (eid.isequal(mapi_session,eid_tasks)) return mftTasks;
  // 3. Third best way (and the only other way) is to check
  // it's PR_CONTAINER_CLASS property. The documentation says
  // that this shouldn't be used, but apparently the documentation
  // is out of date. The documentation says that everything
  // begins with IPM, but I'm sure I've seen an IPF somewhere.
  SPropValue *sp;
  HRESULT hr = pHrGetOneProp(f, PR_CONTAINER_CLASS, &sp);
  if (hr!=S_OK) return mftStuff;
  string s(sp->Value.lpszA);
  if (s.length()>3 && s[0]=='I' && s[1]=='P') s[2]='.'; // not sure IPM or IPF.
  const char *c=s.c_str();
  if (strncmp(c,"IP..Note",8)==0) return mftMail;
  else if (strncmp(c,"IP..Imap",9)==0) return mftMail;
  else if (strncmp(c,"IP..Appointment",15)==0) return mftCalendar;
  else if (strncmp(c,"IP..Contact",11)==0) return mftContacts;
  else if (strncmp(c,"IP..Journal",11)==0) return mftJournal;
  else if (strncmp(c,"IP..StickyNote",14)==0) return mftNotes;
  else if (strncmp(c,"IP..Task",8)==0) return mftTasks;
  else if (strncmp(c,"IP..",4)==0) return mftSpecial;
  else return mftStuff;

// ENSURE-COMMON-EIDS -- There are some standard ENTRYIDs for some standard
// folders. This function sets up these in global variables. Note that they
// are specific to the current library, profile and message-store. For an
// explanation of how to retrieve each type, see inside the procedure.
void mapi_EnsureCommonEids()
{ if (got_eids) return;
  eid_inbox.clear(); eid_outbox.clear(); eid_sent.clear(); eid_deleted.clear();
  eid_calendar.clear(); eid_contacts.clear(); eid_journal.clear(); eid_notes.clear(); eid_tasks.clear();
  if (mapi_msgstore==0) return;
  DWORD size; ENTRYID *eid; HRESULT hr;
  // 1. INBOX special folder -- in fact, the user can designate any folder as
  // an inbox. All we can do is check where incoming IPM.Note messages (i.e. emails)
  // are placed.
  hr = mapi_msgstore->GetReceiveFolder("IPM.Note",0,&size,&eid,NULL);
  if (hr==S_OK) eid_inbox.set(size,eid);
  // 2. Other special folders. The message-store has properties for these.
  ULONG pcount; SPropValue *props;
  hr = mapi_msgstore->GetProps((SPropTagArray*)&cols,0,&pcount,&props);
  if (hr==S_OK || hr==MAPI_W_ERRORS_RETURNED)
  { LONG mask; if (props[0].ulPropTag==PR_VALID_FOLDER_MASK) mask=props[0].Value.ul; else mask=0;
    if ((mask&FOLDER_IPM_OUTBOX_VALID) && props[1].ulPropTag==PR_IPM_OUTBOX_ENTRYID) eid_outbox.set(props[1].Value.bin.cb, (ENTRYID*)props[1].Value.bin.lpb);
    if ((mask&FOLDER_IPM_SENTMAIL_VALID) && props[2].ulPropTag==PR_IPM_SENTMAIL_ENTRYID) eid_sent.set(props[2].Value.bin.cb, (ENTRYID*)props[2].Value.bin.lpb);
    if ((mask&FOLDER_IPM_WASTEBASKET_VALID) && props[3].ulPropTag==PR_IPM_WASTEBASKET_ENTRYID) eid_deleted.set(props[3].Value.bin.cb, (ENTRYID*)props[3].Value.bin.lpb);
  // 3. The outlook specials. The inbox has properties for these.
  if (eid_inbox.isempty()) return;
  ULONG intype; IMAPIFolder *infolder;
  hr = mapi_msgstore->OpenEntry(eid_inbox.size, eid_inbox.ab, NULL, 0, &intype, (IUnknown**)&infolder);
  if (hr!=S_OK) return;
  if (intype!=MAPI_FOLDER) {infolder->Release();return;}
  SizedSPropTagArray(5, spec) = {5, {0x36D00102, 0x36D10102, 0x36D20102, 0x36D30102, 0x36D40102}};
  hr = infolder->GetProps((SPropTagArray*)&spec,0,&pcount,&props);
  if (hr==S_OK || hr==MAPI_W_ERRORS_RETURNED)
  { if (props[0].ulPropTag==0x36D00102) eid_calendar.set(props[0].Value.bin.cb, (ENTRYID*)props[0].Value.bin.lpb);
    if (props[1].ulPropTag==0x36D10102) eid_contacts.set(props[1].Value.bin.cb, (ENTRYID*)props[1].Value.bin.lpb);
    if (props[2].ulPropTag==0x36D20102) eid_journal.set(props[2].Value.bin.cb, (ENTRYID*)props[2].Value.bin.lpb);
    if (props[3].ulPropTag==0x36D30102) eid_notes.set(props[3].Value.bin.cb, (ENTRYID*)props[3].Value.bin.lpb);
    if (props[4].ulPropTag==0x36D40102) eid_tasks.set(props[4].Value.bin.cb, (ENTRYID*)props[4].Value.bin.lpb);

// MAPI_ENSUREFINISHED -- releases any interfaces that we might have obtained
// through the other functions.
void mapi_EnsureFinished()
{ got_eids=false;
  mapi_msgstore_name=""; if (mapi_msgstore!=0) mapi_msgstore->Release(); mapi_msgstore=0;
  mapi_session_profile=""; if (mapi_session!=0) {mapi_session->Logoff(0,0,0);mapi_session->Release();mapi_session=0;}
  if (hmapilib!=0)
  { IProfAdmin *iprofadmin;
    HRESULT hr = pMAPIAdminProfiles(0,&iprofadmin);
    if (hr==S_OK)
    { mapi_EnsureCrazyProfileDeleted(iprofadmin);
  mapi_lib_path=""; if (hmapilib!=0) {pMAPIUninitialize();FreeLibrary(hmapilib);hmapilib=0;}

// MAPI_ENSURECRAZYPROFILEDELETED -- In case we had created a temporary profile,
// this function ensures that it is deleted.
// Note: it is a *SEVERE* error if you try to delete a profile that's not there.
// Under Outlook2000 in IMO, attempting to do this will delete every message-store
// of the main profile. Gulp! Hence, our care in this function...
void mapi_EnsureCrazyProfileDeleted(IProfAdmin *iprofadmin)
{ bool gotcrazy=false;
  IMAPITable *proftable;
  HRESULT hr = iprofadmin->GetProfileTable(0, &proftable);
  if (hr==S_OK)
  { SizedSPropTagArray(2, proftablecols) = { 2, {PR_DISPLAY_NAME,PR_DEFAULT_PROFILE} };
    SRowSet *profrows;
    hr = pHrQueryAllRows(proftable,(SPropTagArray*)&proftablecols,NULL,NULL,0,&profrows);
    if (hr==S_OK)
    { for (unsigned int i=0; i<profrows->cRows; i++)
      { string name="";
        if (profrows->aRow[i].lpProps[0].ulPropTag==PR_DISPLAY_NAME) name=profrows->aRow[i].lpProps[0].Value.lpszA;
        if (name=="Lu's Crazy Profile (democode)") gotcrazy=true;
  if (gotcrazy) iprofadmin->DeleteProfile("Lu's Crazy Profile (democode)",0);

// DECODERTFHTML -- Given an uncompressed RTF body of the message,
// and assuming that it contains encoded-html, this function
// turns it onto regular html.
// [in] (buf,*len) indicate the start and length of the uncompressed RTF body.
// [out] the buffer is overwritten with the HTML version, null-terminated,
// and *len indicates the length of this HTML.
// Notes: (1) because of how the encoding works, the HTML version is necessarily
// shorter than the encoded version. That's why it's safe for the function to
// place the decoded html in the same buffer that formerly held the encoded stuff.
// (2) Some messages include characters \'XX, where XX is a hexedecimal number.
// This function simply converts this into ASCII. The conversion will only make
// sense if the right code-page is being used. I don't know how rtf specifies which
// code page it wants.
// (3) By experiment, I discovered that \pntext{..} and \liN and \fi-N are RTF
// markup that should be removed. There might be other RTF markup that should
// also be removed. But I don't know what else.
void decodertfhtml(char *buf,unsigned int *len)
{ // c -- pointer to where we're reading from
  // d -- pointer to where we're writing to. Invariant: d<c
  // max -- how far we can read from (i.e. to the end of the original rtf)
  // ignore_tag -- stores 'N': after \mhtmlN, we will ignore the subsequent \htmlN.
  char *c=buf, *max=buf+*len, *d=buf; int ignore_tag=-1;
  // First, we skip forwards to the first \htmltag.
  while (c<max && strncmp(c,"{\\*\\htmltag",11)!=0) c++;
  // Now work through the document. Our plan is as follows:
  // * Ignore { and }. These are part of RTF markup.
  // * Ignore \htmlrtf...\htmlrtf0. This is how RTF keeps its equivalent markup separate from the html.
  // * Ignore \r and \n. The real carriage returns are stored in \par tags.
  // * Ignore \pntext{..} and \liN and \fi-N. These are RTF junk.
  // * Convert \par and \tab into \r\n and \t
  // * Convert \'XX into the ascii character indicated by the hex number XX
  // * Convert \{ and \} into { and }. This is how RTF escapes its curly braces.
  // * When we get \*\mhtmltagN, keep the tag, but ignore the subsequent \*\htmltagN
  // * When we get \*\htmltagN, keep the tag as long as it isn't subsequent to a \*\mhtmltagN
  // * All other text should be kept as it is.
  while (c<max)
  { if (*c=='{') c++;
    else if (*c=='}') c++;
    else if (strncmp(c,"\\*\\htmltag",10)==0)
    { c+=10; int tag=0; while (*c>='0' && *c<='9') {tag=tag*10+*c-'0'; c++;}
      if (*c==' ') c++;
      if (tag==ignore_tag) {while (c<max && *c!='}') c++; if (*c=='}') c++;}
    else if (strncmp(c,"\\*\\mhtmltag",11)==0)
    { c+=11; int tag=0; while (*c>='0' && *c<='9') {tag=tag*10+*c-'0'; c++;}
      if (*c==' ') c++;
    else if (strncmp(c,"\\par",4)==0) {strcpy(d,"\r\n"); d+=2; c+=4; if (*c==' ') c++;}
    else if (strncmp(c,"\\tab",4)==0) {strcpy(d,"   "); d+=3; c+=4; if (*c==' ') c++;}
    else if (strncmp(c,"\\li",3)==0)
    { c+=3; while (*c>='0' && *c<='9') c++; if (*c==' ') c++;
    else if (strncmp(c,"\\fi-",4)==0)
    { c+=4; while (*c>='0' && *c<='9') c++; if (*c==' ') c++;
    else if (strncmp(c,"\\'",2)==0)
    { unsigned int hi=c[2], lo=c[3];
      if (hi>='0' && hi<='9') hi-='0'; else if (hi>='A' && hi<='Z') hi=hi-'A'+10; else if (hi>='a' && hi<='z') hi=hi-'a'+10;
      if (lo>='0' && lo<='9') lo-='0'; else if (lo>='A' && lo<='Z') lo=lo-'A'+10; else if (lo>='a' && lo<='z') lo=lo-'a'+10;
      *((unsigned char*)d) = (unsigned char)(hi*16+lo);
      c+=4; d++;
    else if (strncmp(c,"\\pntext",7)==0) {c+=7; while (c<max && *c!='}') c++;}
    else if (strncmp(c,"\\htmlrtf",8)==0)
    { c++; while (c<max && strncmp(c,"\\htmlrtf0",9)!=0) c++;
      if (c<max) c+=9; if (*c==' ') c++;
    else if (*c=='\r' || *c=='\n') c++;
    else if (strncmp(c,"\\{",2)==0) {*d='{'; d++; c+=2;}
    else if (strncmp(c,"\\}",2)==0) {*d='}'; d++; c+=2;}
    else {*d=*c; c++; d++;}
  *d=0; d++;
  *len = (unsigned int)(d-buf);

bool isrtfhtml(const char *buf,unsigned int len)
{ // We look for the words "\fromhtml" somewhere in the file.
  // If the rtf encodes text rather than html, then instead
  // it will only find "\fromtext".
  for (const char *c=buf; c<buf+len; c++)
  { if (strncmp(c,"\\from",5)==0) return strncmp(c,"\\fromhtml",9)==0;
  } return false;

// These three utility functions are not provided with Outlook97. That's
// why I'm reimplementing them here.
HRESULT pHrGetOneProp(IMAPIProp *obj, ULONG tag, SPropValue **pProp)
{ if (pProp==0) return E_POINTER;
  SizedSPropTagArray(1, cols) = { 1, {tag}};
  ULONG pcount; HRESULT hr;
  hr = obj->GetProps((SPropTagArray*)&cols,0,&pcount,pProp);
  if (hr==S_OK) return S_OK;
  if (hr==MAPI_W_ERRORS_RETURNED) {pMAPIFreeBuffer(*pProp); return MAPI_E_NOT_FOUND;}
  return hr;

void pFreeProws(SRowSet *r)
{ if (r==0) return;
  for (unsigned int i=0; i<r->cRows; i++)
  { SPropValue *pv = r->aRow[i].lpProps;

HRESULT pHrQueryAllRows(IMAPITable *table, SPropTagArray *tags, SRestriction *res,
   SSortOrderSet *sort, LONG crowsMax, SRowSet **rows)
{ if (tags!=0) table->SetColumns(tags,0);
  if (res!=0) table->Restrict(res,0);
  if (sort!=0) table->SortTable(sort,0);
  if (crowsMax==0) crowsMax=0x0FFFFFFF;
  while (hr==MAPI_E_BUSY)
  { hr = table->QueryRows(crowsMax,TBL_NOADVANCE,rows);
    if (hr==MAPI_E_BUSY)
    { hr=table->WaitForCompletion(0,1000,NULL);
      if (hr!=MAPI_E_NO_SUPPORT) hr=MAPI_E_BUSY;
  return hr;

// Here are just some internal functions for accessing the registry...
list<string> mapi_RegQuerySubkeys(HKEY key)
{ list<string> ss; LONG res; DWORD index=0; char buf[1024]; FILETIME ft;
  while (true)
  { DWORD size=1024;
    res = RegEnumKeyEx(key,index,buf,&size,NULL,NULL,NULL,&ft);
    if (res!=ERROR_SUCCESS) return ss;
string mapi_RegQueryString(HKEY key,const string name)
{ DWORD type, size=0; LONG res;
  res = RegQueryValueEx(key,name.c_str(),NULL,&type,NULL,&size);
  if (res!=ERROR_SUCCESS) return "";
  if (type!=REG_SZ && type!=REG_EXPAND_SZ) return "";
  char *c=new char[size+1];
  res = RegQueryValueEx(key,name.c_str(),NULL,&type,(BYTE*)c,&size);
  if (res!=ERROR_SUCCESS) {delete[] c; return "";}
  if (type==REG_EXPAND_SZ)
  { char dummy[1]; DWORD esize=ExpandEnvironmentStrings(c,dummy,0);
    if (esize!=0)
    { char *d=new char[esize+1];
      delete[] c; c=d;
  string r(c);
  delete[] c;
  return r;


The code on this page demonstrates the following interfaces, functions and structures:

(functions) MAPIInitialize, MAPIUninitialize, MAPIAdminProfiles, MAPILogonEx, MAPIFreeBuffer, HrQueryAllRows, FreeProws, HrGetOneProp, RTFSync, WrapCompressedRTFStream


(structures) ENTRYID, MAPIUID, SizedSPropTagArray, SPropTagArray, SRowSet

(interfaces) IMAPISession, IMsgStore, IProfAdmin, IMAPITable, IMAPIFolder, IMsgServiceAdmin, IStream, IMAPFolder, IMessage

(methods) SetColumns, GetMsgStoresTable, OpenMsgStore, GetProfileTable, Logoff, OpenEntry, CreateProfile, DeleteProfile, AdminServices, CreateMsgService, ConfigureMsgService, GetMsgServiceTable, GetHierarchyTable, GetReceiveFolder, SaveChanges, GetRecipientTable

(header files) mapix.h, mapiutil.h, mspst.h

(registry) SOFTWARE\Clients\Mail, DLLPathEx, SOFTWARE\Microsoft\Windows Messaging Subsystem, MAPIX, mapi32.dll, LoadLibrary

(mapi stuff) MSPST MS, IPM.Note, IPM.Appointment, IPM.Contact, IPM.Journal, IPM.StickyNote, IPM.Task, rtfhtml

Just a quick question: Any idea on how to decrypt emails using MAPI?
too good ..
very simple interface to MAPI dll.
helped me a lot.
Is there any simillar Utils for exchange message store

too good ..
very simple interface to MAPI dll.
helped me a lot.
Is there any simillar Utils for exchange message store
help detail
good one
awesome tutorial.
This got me going.
cordial thanks
Awesome Tutorial, truly helpful :)
Thanks a Ton
thnx a ton...this is of of d best tutorial i hv read. :) thnx again...
Nice Work.
hi, i have a small problem...
i have outlook XP installed, but the program lists folders of outlook XP, pls have you got any idea how to solve this?
^^^ shold be: "i have outlook xp installed, but the program lists outlook express" sorry
hi, Nice tutorial dude ..
i have another doubt ..
i am trying to send mail using mapiex.dll in outlook 2002, but its not working other than that outlook 2007 , 2003 its all working fine ...plz suggest me how to rectify this problem ...
Ultimate tutorial on MAPI. Thanks for such a nice work.
Hello Lucian,
Are you interested in contributions to your code? We use your code in our MSG viewer, together with the MsgReader code project ( To make it properly display foreign characters, I have added code page support. We have tested this with various European languages and Chinese text.
The updated code can be downloaded here:
If anyone finds issues, comments are welcome at b.engelbrecht AT gmail DOT com. If the issue is related to a specific Outlook message, please attach a zipped copy of the .msg file.
Today I have uploaded a minor change to, to remove \rtlch and \ltrch RTF codes. It was reported that these codes were visible in the html output for mixed arabic/european text pages (same issue would occur for other right-to-left scripts).
Is it possible to send a message using the MAPI API without already being logged into the Outlook front end? I have found that if I call OpenAddressBook without being logged into the Outlook front end
the call fails and GetLastError is:
The connection to the Microsoft Exchange Server is unavailable.
Outlook must be online or connected to complete this action.
MAPI was unable to load the information service EMSABP.DLL.
Be sure the service is correctly installed and configured
Do you think there is any way around this?
Hello, how can I find out the server name of a MAPI account for Microsoft Exchange Server? Thank you!
Hi Lucian,
thanks for that great tutorial and sample code.
Just one issue:
When an HTML email is received and the HTML text contains a <pre> tag, all line feeds within <pre> and </pre> are replaced by the Exchange server with the RTF tag "\line". Since the decodertfhtml() function removes "\li", it leaves the string "ne" in the HTML text.
So I addeded the lines
else if (strncmp(c,"\\line",5)==0) {strcpy(d,"\r\n"); d+=2; c+=5;}
just above the handling of "\li".
Now the HTML texts of such emails look fine.
Btw, I heard from a collegue that he also saw such "ne" strings in emails on his I-phone. It seems that a lot of people are using your code.
peter.huber [at]
I have tested the decodertfhtml function on a large email (2.5MB) and it performs quite slow (~30s) on a Core 2 Duo processor. Do you have any suggestion how this can be improved?
Thank you Lucian for your code.
Best regards
Koldo (
How do you read the body of the message? Like where it says:
// do something with the imsg here, like read its body...
Can you import the content into a text file?
Only the first 10 messages are shown for me... HELP please
The code works fine on ANSI. Can anyone helps me to run this code on Unicode and IPSwitch ? I will be grateful.
add comment  edit commentPlease add comments here