Logo
blank Skip to main content

Writing email to the file of the PST format

C++

The aim of this article is to create the library for writing e-mail messages in the *.pst format (used by Microsoft Outlook) to the file. This article will be interesting for those developers who develop various mail applications, which include the export to *.pst format functionality. This article is divided into two parts in order to make it easier for understanding and for better structuring. The first part introduces the common notions such as initialization, writing the message properties (sender, recipient, etc.); writing the text body of the message, and creation of the folders structure. The second part deals with more complicate notions such as writing the message attachment, RTF and HTML bodies of the message. The developed library is a sample and is not intended for usage in real applications. Its aim is to demonstrate the common principles of working with MAPI.

Introduction

The structure of the article

The Extended MAPI part is devoted to the common information about the interface, its features and implementation.

The Implementation part deals with the key moments of implementation and the common architecture of the sample project.

The Example part describes how to build the project, use the library, and also provides the example of usage and the results of testing.

Used technologies

  • C++.  The library and the example are written in ะก++ language using exceptions.
  • STLPort. A standard library (for more information, see http://www.stlport.org/).
  • Extended MAPI. MAPI (Messaging Application Programming Interface) is a messaging architecture and a Component Object Model based API for Microsoft Windows.

The structure of the project

bin -binary files.

lib – library files.

obj – object files.

src – source files.

| – PstWriter โ€“ main library that provides the export functionality.

| – TestApp โ€“ test application that writes test message to the test PST file.

Brief survey of MAPI

Messaging Application Programming Interface (MAPI) is a messaging architecture and a Component Object Model based API for Microsoft Windows. MAPI allows client programs to become (e-mail) messaging-enabled, -aware, or -based by calling MAPI subsystem routines that interface with certain messaging servers. While MAPI is designed to be independent of the protocol, it is usually used with MAPI/RPC, the proprietary protocol that Microsoft Outlook uses to communicate with Microsoft Exchange.

Simple MAPI is a subset of 12 functions which enable developers to add basic messaging functionality. Extended MAPI allows complete control over the messaging system on the client computer, creation and management of messages, management of the client mailbox, service providers, and so forth. Simple MAPI ships with Microsoft Windows as part of Outlook Express/Windows Mail while the full Extended MAPI ships with Office Outlook and Exchange.

Implementation

This part describes in detail the work of the library with MAPI. The description and code pieces are also provided. The full version of the code is attached to the article. The code in the article does not include the check for errors and other parts, which are easy for understanding and do not play a significant role while working with MAPI.

MAPI initialization

First what you should do while working with MAPI is to initialize it. If you do not do this, all calls will return the error. To initialize MAPI, call the function:

C
 HRESULT MAPIInitialize(LPVOID lpMapiInit);

In the example, it is performed in the following way:

C
MAPIINIT init = { 0, MAPI_MULTITHREAD_NOTIFICATIONS};
HRESULT hr = MAPIInitialize(&init);

The MAPI_MULTITHREAD_NOTIFICATIONS parameter means that MAPI should generate notifications using a thread dedicated to notification handling instead of the first thread used to call MAPIInitialize. Itโ€™s advisable to always define this parameter in order to avoid problems with threading. Also the MAPIUninitialize call should correspond to every MAPIInitialize call. The first should be called after the work with MAPI is finished. And this pair of methods should be called for each thread that uses MAPI.

The opening of the file

The PST file is opened in several stages. All the work with the file is performed through MAPI and the application does not have the direct access to it.

First, you should get a administration object for the service access:

C
// Get a profile administration object.
hr = MAPIAdminProfiles(0, &m_pIProfAdmin);
โ€ฆ
DWORD dwStart = GetTickCount();
sprintf_s( m_profileName, 100,  "Temp%lX", dwStart );
// Creating profile
(void) m_pIProfAdmin->DeleteProfile( (LPTSTR)&m_profileName, 0 );
hr = m_pIProfAdmin->CreateProfile( (LPTSTR)&m_profileName, NULL, 0, 0 );
โ€ฆ
// Get a service administration object.
hr = m_pIProfAdmin->AdminServices((LPTSTR)&m_profileName, 0, 0, 0, &m_pSvcAdmin);

The m_profileName defines the name of the profile in this case. In order not to open the existing profile, the profile name is generated in a random way and the attempt to delete it is performed.

Then it is necessary to create and configure the service, which will work with our file:

C
//Creating service
hr = m_pSvcAdmin->CreateMsgService( "MSUPST MS", "MAPI Pst Msg Store", 0, 0 )
// Configure the MS Personal Information Store per NewPST entries.
hr = m_pSvcAdmin->GetMsgServiceTable(0, &m_ptblSvc);
COM::AutoPtr<srowset,roweraser> prows;
COM::AutoPtr<srowset,roweraser> pRows;
enum {iSvcName, iSvcUID, iEnrtyID, cptaSvc};
//Now we should find created service.
SizedSPropTagArray (cptaSvc, ptaSvc) = { cptaSvc, 
	{ PR_SERVICE_NAME, PR_SERVICE_UID, PR_ENTRYID} };
HrQueryAllRows(m_ptblSvc, (LPSPropTagArray)&ptaSvc, NULL, NULL, 0, &pRows);
int cRows = pRows->cRows;
for(LPSRow pRow = pRows->aRow; pRow < pRows->aRow + cRows; ++pRow)
{
//Here is our service
m_MsgStoreUID = *((LPMAPIUID)pRow->lpProps[iSvcUID].Value.bin.lpb);
if(strcmp(pRow->lpProps[iSvcName].Value.lpszA, pszMsgService) == 0)
{
// Configuring PST Message Store
ULONG count = 0;
const ULONG nProps = 2;
SPropValue   rgval[nProps];
rgval[count].ulPropTag = PR_DISPLAY_NAME_W;
rgval[count].Value.lpszW = const_cast<wchar_t*>(storeDisplayName.c_str());
rgval[count].ulPropTag = PR_PST_PATH;
rgval[count].Value.lpszA = const_cast<char*>(path.c_str());
// Writing parameters
m_pSvcAdmin->ConfigureMsgService( &m_MsgStoreUID, 0, 0, nProps, rgval);
}
}

If everything is fine, the new PST file is created. It has the path path and the storeDisplayName display name (the MS Outlook property).

The last thing that you should do is to get the object, which will be used for access to the so called Message Store.

C
MAPILogonEx(0, m_profileName, NULL, MAPI_NEW_SESSION | 
	MAPI_EXTENDED | MAPI_NO_MAIL | MAPI_TIMEOUT_SHORT, &m_pses);
MapiTable ptable;
m_pses->GetMsgStoresTable(0, &ptable);
SizedSPropTagArray(3, columns) = { 3, { PR_DEFAULT_STORE, PR_ENTRYID, PR_DISPLAY_NAME } };
HrQueryAllRows(ptable, (LPSPropTagArray) &columns, NULL, NULL, 0, &prows);
IMsgStore * msgStore = 0;
m_pses->OpenMsgStore(0, prows->aRow[0].lpProps[1].Value.bin.cb, 
	(LPENTRYID)prows->aRow[0].lpProps[1].Value.bin.lpb, NULL, MDB_WRITE | 
	MAPI_DEFERRED_ERRORS | MAPI_BEST_ACCESS | MDB_NO_MAIL, & msgStore);

As a result, we receive the IMsgStore * msgStore, which will be used in practically all other MAPI calls.

Data writing

The creation of the folders hierarchy

When writing the message, you should define its path like folder1\folder2\folder3. Such path defines the location in the database hierarchy, where the message will be placed. To create the folder, you should open the root folder in the database and then create the required one.

So, first you should find the root folder:

C
COM::AutoPtr<spropvalue,mapieraser> pVal_EID;
::HrGetOneProp(store->GetMsgStore(),PR_IPM_SUBTREE_ENTRYID,&pVal_EID);
// Open the IPM subtree folder
ULONG ulObjType = 0;
MapiFolder currentFolder;
store->GetMsgStore()->OpenEntry(pVal_EID->Value.bin.cb,
	(LPENTRYID)pVal_EID->Value.bin.lpb,NULL,0x10,&ulObjType,
	reinterpret_cast<iunknown**>(ยคtFolder));

The store->GetMsgStore() returns the Message Store. The previous part of the article dedicated to the initialization was aimed to get it. As a result, you obtain the currentFolder object, which refers to the root directory of the database.

To create the subdirectory, perform the following:

C
currentFolder->CreateFolder(FOLDER_GENERIC, 
(LPTSTR)name.c_str(), NULL, NULL, 1 | OPEN_IF_EXISTS | MAPI_UNICODE, &folder);

where name is the name of the subdirectory and folder is the object, which will refer to the created folder. The names of flags, which are passed to the method, are informative enough.

All created objects, which represent folders, are saved in the implementation of the library in order to improve performance. Though there is nothing to prevent you from opening them each time you write the message for memory saving.

Writing the message

Some message abstraction is used in the library. For the simplicity, it is represented as the structure with the set of properties.  To simplify the representation of the message structure, it is divided into several logical blocks. These are the common properties (flags, subject, sender), recipients, dates, and message bodies (in this article, it is the text body). Other types of bodies (HTML and RTF) and the attachments are not discussed in this article.

The creation of the message

The creation of the message is performed by the call of the corresponding method from the object, which represents the folder, in which the message should be created. In the example, it looks like the following:

C
fld->GetFolder()->CreateMessage(0,0,&pMessage);

As a result of the successful call, we get the pointer to the message object.

Writing the message properties

All message properties are written in the same way. First, the properties array is created, where properties are written as pairs โ€“ tag and value. Here is an example of writing several properties:

C
std::vector<SPropValue> propArray;
SPropValue currentProp;
currentProp.ulPropTag = PR_MESSAGE_CLASS_W;
currentProp.Value.lpszW = (LPWSTR)pMsg->szMsgClass.c_str();
propArray.push_back(currentProp);
//Message flags
currentProp.ulPropTag = PR_MESSAGE_FLAGS;
currentProp.Value.ul = pMsg->flags;
propArray.push_back(currentProp);

After the properties array is filled, you should write it to the message. It is performed in the following way:

C
if (propArray.size() != 0)
{
COM::AutoPtr<spropproblemarray,mapieraser> Problems;
return pMessage->SetProps((ULONG)propArray.size(), &propArray.at(0), &Problems);
}

All necessary properties of the message are written in the same way. For more information, see the library code. Any other non-standard properties, which are not used in the library, can be added in the similar way. You just should find its type and tag in MSDN.

Writing the message recipients

Message recipients are written in a rather different way than other properties. There is a separate ModifyRecipients method for writing recipients in the message object. This method is called in the following way:

C
//Filling property array
std::vector<spropvalue> propArray;
SPropValue currentProp;
std::wstring recipientDisplayName = !itRecip->name.empty() ? 
			itRecip->name : itRecip->address;
currentProp.ulPropTag = PR_DISPLAY_NAME_W;
currentProp.Value.lpszW = (LPWSTR)recipientDisplayName.c_str();
propArray.push_back(currentProp);
currentProp.ulPropTag = PR_EMAIL_ADDRESS_W;
currentProp.Value.lpszW = (LPWSTR)itRecip->address.c_str();
propArray.push_back(currentProp);
currentProp.ulPropTag = PR_RECIPIENT_TYPE;
currentProp.Value.ul = itRecip->type;
propArray.push_back(currentProp);
//Filling special structure for changing recipients by property array
ADRLIST adrList = { 0 };
adrList.cEntries = 1;
adrList.aEntries[0].rgPropVals = &propArray.front();
adrList.aEntries[0].ulReserved1 = 0;
adrList.aEntries[0].cValues = (ULONG)propArray.size();
//Making changes in message object
pMessage->ModifyRecipients( MODRECIP_ADD, &adrList );

Writing the message body

The message body is often represented as a rather fair-sized data. That is why it is written not as a property, but as a so called โ€œstreamโ€. First, you should create (open) a stream in order to write data to the message. This procedure is implemented in the library in the CPstWriter::WriteMessageStream method. Its code looks like the following:

C
long CPstWriter::WriteMessageStream 
    (IMessage * pMessage, const char * data, size_t dataSize, unsigned long streamTag)
{
โ€ฆ
MapiStream pStream;
pMessage->OpenProperty (streamTag, &IID_IStream, STGM_WRITE,
MAPI_CREATE | MAPI_MODIFY, ( LPUNKNOWN* ) & pStream);
ULONG ulWritten = 0;
pStream->Write (data, (ULONG)dataSize, &ulWritten);
pStream->Commit( STGC_DEFAULT );
โ€ฆ
}

It means that you just create a stream with the tie to a definite streamTag. After this, the writing of data to this stream is performed and changes are committed.

To write the text (unicode) message body, you should call the mentioned above method with the streamTag = PR_BODY_W parameter.

How to build and run this solution

  1. Use Microsoft Visual Studio 2005 or later to open PstWriter.sln in the src folder.
  2. Build the solution.
  3. Install MS Outlook 2007 or later (it provides Unicode MAPI) for the proper work.

Testing

  1. Build the solution using the instructions above.
  2. Run TestApp.exe with the command line parameter, which defines the path to the result pst file. For example, TextApp.exe โ€œD:\1.pstโ€.
test1
  1. As a result, if there were no errors, a file is created in the defined folder.
  2. If you open it in MS Outlook, you see the following:
test2

As it can be seen from the screenshot, the following objects and properties were successfully created:

  1. The folders hierarchy (Result\Inbox\asf).
  2. The message with To, CC, Bcc, and From fields, all dates, subject, and the text body.
  3. All recipients (except of the mailing address) also include the display name.
  4. The message is marked as read, private, and important.

Supported Windows Versions

  • Windows XP (SP3);
  • Windows 7 (x86, x64).

Download source files (archive 15 KB)

Have a question?

Ask our expert!

Tell us about
your project

...And our team will:

  • Process your request within 1-2 business days.
  • Get back to you with an offer based on your project's scope and requirements.
  • Set a call to discuss your future project in detail and finalize the offer.
  • Sign a contract with you to start working on your project.

Do not have any specific task for us in mind but our skills seem interesting? Get a quick Apriorit intro to better understand our team capabilities.