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.
Contents
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:
HRESULT MAPIInitialize(LPVOID lpMapiInit);
In the example, it is performed in the following way:
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:
// 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:
//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.
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:
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:
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:
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:
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:
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:
//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:
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
- Use Microsoft Visual Studio 2005 or later to open PstWriter.sln in the src folder.
- Build the solution.
- Install MS Outlook 2007 or later (it provides Unicode MAPI) for the proper work.
Testing
- Build the solution using the instructions above.
- Run TestApp.exe with the command line parameter, which defines the path to the result pst file. For example, TextApp.exe โD:\1.pstโ.
- As a result, if there were no errors, a file is created in the defined folder.
- If you open it in MS Outlook, you see the following:
As it can be seen from the screenshot, the following objects and properties were successfully created:
- The folders hierarchy (Result\Inbox\asf).
- The message with To, CC, Bcc, and From fields, all dates, subject, and the text body.
- All recipients (except of the mailing address) also include the display name.
- The message is marked as read, private, and important.
Supported Windows Versions
- Windows XP (SP3);
- Windows 7 (x86, x64).