Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Remote Mail (.NET Remoting + MAPI)

0.00/5 (No votes)
30 Jun 2003 1  
This project is supposed to be a part of messaging-enabled server-client applications. Users in local network will be able to send messages without Internet access and without mail client installed and configured through server.

Table Of Contents

Introduction

Nowadays is almost impossible to find an application which does not include messaging functions as an added feature (an example of a messaging-enabled application is Microsoft Word, which can add messaging functions by adding a Send command to its File menu). But if user wants to use this feature, he needs mail client to be installed and configured. For server-client applications running in a company�s local network it�s not always suitable to install and configure Outlook in every client machine, and give an Internet access to every client machine.
  • This project is supposed to be a part of messaging-enabled server-client applications.
  • Users in local network will be able to send messages without Internet access and without mail client installed and configured through server.
  • No any prompts or warning will be shown on server because Extended MAPI is used.
  • All messages will be sent from and stored in personal folders of the only user profile on the server machine (mail client should support Extended MAPI calls).
  • You can give to Send Form look and feel of your main application.

UML Diagram

Remote Object Implementation

Passing Objects in Remote Methods

Remoting implementations distinguish between remote objects and mobile objects. Remote objects live on the server and provide the ability to execute remote method calls on the server side by passing only a reference (ObjRef) around among other machines. The second kind of objects (ByValue objects) are passed over remoting boundaries, serialized into a string or a binary representation and restored as a copy on the other side of the communications channel. Both server and client hold copies of the same object and both copies run absolutely independently.

        In our case choice is obvious: since MAPI session object depends on the application domain (context-bound object) - we derive MMapi class from MarshalByRefObject:

public __gc class MMapi: public MarshalByRefObject
{
    ...
}

In the contrary, the MSGDATA class object that holds all information about message to submit will be created on a client side and passed as a parameter across the boundaries, we mark it with the [Serializable] attribute:

[Serializable]
public __gc class MSGDATA
{
public:
    String * recipient_name __gc[];
    String * recipient_email __gc[];
    String * subject;
    String * carbon_copy __gc[];
    String * body;
    String * attachment_name __gc[];
    MemoryStream * bodyStream;
    MemoryStream * attachStream __gc[];
};

Lifetime Management

Since we need session object exists so long as the server is running, we override InitializeLifetimeService method of the base class MarshalByRefObject to change default lease configurations:
public:
virtual Object* InitializeLifetimeService()
{
    return NULL;
}

Remote Object Assemblies

Server contains two assemblies: the first assembly TI_MAPI includes Managed C++ base class MMapi and MSGDATA class, and the second assembly TI_MailService includes C# class RemoteSess that inherits from the MMapi class. The public methods of the MMapi class:
virtual Object* InitializeLifetimeService()
virtual HRESULT LogonEx(long hwnd, String* profile)    
virtual void Logoff(long hwnd)
virtual HRESULT SendMail(MSGDATA * pMsgData)
virtual HRESULT GetAddressList(String *pAddrList[], int n)

(implementation) are overridden in the RemoteSess class as follow:

class RemoteSess: MMapi
{

    public override object InitializeLifetimeService()
    {
        return base.InitializeLifetimeService();
    }
    public override int LogonEx(int hWnd, String profile)
    {
        return base.LogonEx(hWnd, profile);
    }
    public override void Logoff(int hWnd)
    {
        base.Logoff(hWnd);
    }
    public string[] AddressList(int n)
    {
        String[] pAddrList = new String[n];
        int i = base.GetAddressList(pAddrList, pAddrList.Length);
        return pAddrList;
    }
    public override int SendMail(MSGDATA pMsgData)
    {
        return base.SendMail(pMsgData);
    }
}

Server Implementation

Object on a Remote Server

On the server side we publish a single instance of pre-created object which allows all clients to work with the same single instance of remote object.
// declare global for server class variable:

private RemoteSess sess = null;

// configure remoting services:

HttpChannel chnl = new HttpChannel(1234);
ChannelServices.RegisterChannel(chnl);

// create a single instance of the RemoteSess class

// (all clients will use this instance):

sess = new RemoteSess();
// convert the object into an instance of the ObjRef class 

// (the object acts as a Singleton afterwards):

RemotingServices.Marshal(sess, "RemoteSess.soap");

Configuration Files

We use configuration file to define remoting channels for server instead of hard coding (no necessary to change the source code if we need to change configuration).

Listing TI_MailService.exe.config:

    <configuration>

      <system.runtime.remoting>

        <application>

          <channels>

             <channel ref="http" port="1234"/>

          </channels>

        </application>

      </system.runtime.remoting>

    </configuration>

Windows Service

We need the server application start automatically at a boot-time. For server application we create a Windows Service Project.

We override OnStart and OnStop service�s methods. In OnStart method we create a new instance of RemoteSess object, register channel and make logon into Outlook with default profile.

// global for service class variables:

private static RemoteSess sess = null;
private static HttpChannel chnl;

protected override void OnStart(string[] args)
{
    sess = new RemoteSess();
    RemotingServices.Marshal(sess, "RemoteSess.soap");    
        // logging on with the default Outlook account

    int i = sess.LogonEx(0, null);
    if (i == 0) 
    {
        evt.WriteEntry("MAPILogonEx OK");
        chnl = new HttpChannel(1234);
        ChannelServices.RegisterChannel(chnl);    
        }
    else 
    {
        evt.WriteEntry("MAPILogonEx failed" + i.ToString()); 
        return;
    }
}

If LogonEx is successful, we write the string �MAPILogonEx OK� into the event log and register new channel. Note, that because of speciality of the service, even if OnStart is succesful but logon is not, the service is not ready to work. In the OnStop method when service is stopped we make logoff and unregister channel:

protected override void OnStop()
{
    if (sess != null) sess.Logoff(0);
    evt.WriteEntry(".NET Remote Mail stopped");
    ChannelServices.UnregisterChannel(chnl);
}

In the service installation program we make configuartion of the process and the service:

// the ServiceInstaller class is used for the configuration of the service,

// the instance of ServiceInstaller is needed for each service

private ServiceInstaller serviceInstaller;
// ServiceProcessInstaller class is used for the configuration of the process 

// that defines values for all services in this process

private ServiceProcessInstaller processInstaller;

public ProjectInstaller()
{
    string[] a = { "Net Logon", "IIS Admin Service"};

    processInstaller = new ServiceProcessInstaller();
    serviceInstaller = new ServiceInstaller();

    processInstaller.Account = ServiceAccount.User;

    serviceInstaller.StartType = ServiceStartMode.Automatic;
    serviceInstaller.ServiceName = ".NET Remote Mail";
    serviceInstaller.ServicesDependedOn = a;

    Installers.Add(serviceInstaller);
    Installers.Add(processInstaller);
}

Normally services run under LocalSystem account, a highly privileged user account on the local system thus it don�t have rights on the network.

In our case we can�t use this Account because it doesn�t have permissions to access mailboxes of users, registered on local machine.

We could use impersonation to run the thread under the user account, then load the user hive by calling LoadUserProfile and log on into existing MAPI profile. But in this case while logging on necessary user programmatically we should pass in LogonUser function such parameters as user name, domain and user password. The problem is that service can�t interact with desktop (no way for user interface) and mentioned above parameters differs from user to user (no way to hard code).

Service Installation

Since the project has installer classes we can install and uninstall service from command line using utility installutil.exe as following:

  • installutil ti_mailservice.exe
  • installutil /u ti_mailservice.exe

Sample Image - 02.jpg

The dialog shown above will automatically be displayed at installation time. You should provide domain, user name, and password.

Client Implementation

SoapSuds-Generated Metadata

Since .NET Remoting applications need to share common information about remoteable types between server and client, at least three assemblies are needed for any .NET Remoting project:
  • a shared assembly, which contains serializable objects and interfaces or base classes to MarshalByRefObjects
  • a server assembly, which implements the MarshalByRefObjects
  • a client assembly, which consumes the MarshalByRefObjects
Because for a client application isn�t really necessary the remoting object assembly, only the metadata is needed, we use SoapSuds utility to extract the metadata from the running server and generate a new assembly that contains only this meta information:

    // if you already have server running

    // you don�t need first line, otherwise replace �server.exe� with your

    // server executable name:

    start server.exe

    soapsuds -url:http://localhost:1234/RemoteSess.soap?wsdl -nowp gc

We get two assemblies TI_MAPI and TI_MailService that we add to the client project.

Listing TI_MAPI assembly:

using System;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Metadata;
using System.Runtime.Remoting.Metadata.W3cXsd2001;
namespace TI_MAPI {

    [Serializable, SoapType(XmlNamespace="http://schemas.microsoft.com/"
        + "clr/nsassem/TI_MAPI/TI_MAPI%2C%20Version%3D1.0.1271.18916%2C"
        + "%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull", 
    XmlTypeNamespace="http://schemas.microsoft.com/clr/nsassem/TI_MAPI/"
        + "TI_MAPI%2C%20Version%3D1.0.1271.18916%2C%20Culture%3Dneutral"
        + "%2C%20PublicKeyToken%3Dnull")]
    public class MSGDATA
    {
        // Class Fields

        public String[] recipient_name;
        public String[] recipient_email;
        public String subject;
        public String[] carbon_copy;
        public String body;
        public String[] attachment_name;
        public System.IO.MemoryStream bodyStream;
        public System.IO.MemoryStream[] attachStream;
    }
    [Serializable, SoapType(XmlNamespace="http://schemas.microsoft.com/clr/"
        + "nsassem/TI_MAPI/TI_MAPI%2C%20Version%3D1.0.1271.18916%2C"
        + "%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull", 
    XmlTypeNamespace="http://schemas.microsoft.com/clr/nsassem/TI_MAPI/"
        + "TI_MAPI%2C%20Version%3D1.0.1271.18916%2C%20Culture%3Dneutral"
        + "%2C%20PublicKeyToken%3Dnull")]
    public class MMapi : System.MarshalByRefObject
    {
        [SoapMethod(SoapAction="http://schemas.microsoft.com/clr/nsassem/"
            + "TI_MAPI.MMapi/TI_MAPI#GetAddressList")]
        public virtual Int32 GetAddressList(String[] pAddrList, Int32 n)
        {
            return((Int32) (Object) null);
        }
        [SoapMethod(SoapAction="http://schemas.microsoft.com/clr/nsassem/"
            + "TI_MAPI.MMapi/TI_MAPI#SendMail")]
        public virtual Int32 SendMail(MSGDATA pMsgData)
        {
            return((Int32) (Object) null);
        }
        [SoapMethod(SoapAction="http://schemas.microsoft.com/clr/nsassem/"
            + "TI_MAPI.MMapi/TI_MAPI#Logoff")]
        public virtual void Logoff(Int32 hwnd)
        {
            return;
        }
        [SoapMethod(SoapAction="http://schemas.microsoft.com/clr/nsassem/"
            + "TI_MAPI.MMapi/TI_MAPI#LogonEx")]
        public virtual Int32 LogonEx(Int32 hwnd, String profile)
        {
            return((Int32) (Object) null);
        }
        [SoapMethod(SoapAction=@"http://schemas.microsoft.com/clr/nsassem/"
            + "TI_MAPI.MMapi/TI_MAPI#InitializeLifetimeService")]
        public override Object InitializeLifetimeService()
        {
            return((Object) (Object) null);
        }

    }
}

Listing TI_MailService assembly:

using System;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Metadata;
using System.Runtime.Remoting.Metadata.W3cXsd2001;
namespace TI_MailService {

    [Serializable, SoapType(XmlNamespace="http://schemas.microsoft.com/clr/"
        + "nsassem/TI_MailService/TI_MailService%2C%20Version"
        + "%3D1.0.1271.18918%2C%20Culture%3Dneutral%2C%20PublicKeyToken"
        + "%3Dnull", XmlTypeNamespace="http://schemas.microsoft.com/clr/"
        + "nsassem/TI_MailService/TI_MailSMailService%2C%20Version%3D"
        + "1.0.1271.18918%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull")]
    public class RemoteSess : TI_MAPI.MMapi
    {
        [SoapMethod(SoapAction="http://schemas.microsoft.com/clr/nsassem/"
            + "TI_MailService.RemoteSess/TI_MailService#SendMail")]
        public override Int32 SendMail(TI_MAPI.MSGDATA pMsgData)
        {
            return((Int32) (Object) null);
        }
        [SoapMethod(SoapAction="http://schemas.microsoft.com/clr/nsassem/"
            + "TI_MailService.RemoteSess/TI_MailService#Logoff")]
        public override void Logoff(Int32 hWnd)
        {
            return;
        }
        [SoapMethod(SoapAction="http://schemas.microsoft.com/clr/nsassem/"
            + "TI_MailService.RemoteSess/TI_MailService#LogonEx")]
        public override Int32 LogonEx(Int32 hWnd, String profile)
        {
            return((Int32) (Object) null);
        }
        [SoapMethod(SoapAction="http://schemas.microsoft.com/clr/nsassem/"
            + "TI_MailService.RemoteSess/TI_MailService#"
            + "InitializeLifetimeService")]
        public override Object InitializeLifetimeService()
        {
            return((Object) (Object) null);
        }
        [SoapMethod(SoapAction="http://schemas.microsoft.com/clr/nsassem/"
            + "TI_MailService.RemoteSess/TI_MailService#AddressList")]
        public String[] AddressList(Int32 n)
        {
            return((String[]) (Object) null);
        }

    }
}
The server and the client can now be developed independently of each other. We include the assemblies above in the client project.

Proxy to the Remote Object

In the client code we configure remoting services using configuration file and use new operator to obtain the reference to the instance of the RemoteSess class living on server:
// use assemblies

using TI_MAPI;
using TI_MailService;

...

// declare global for MailForm class variables:

private RemoteSess sess = null; 
private MSGDATA pMsgData = null;

...

// configure remoting services:

RemotingConfiguration.Configure("mailer.exe.config");
// obtain a reference to the server�s single instance of the 

// RemoteSess object:

sess = new RemoteSess(); 
// ctreate an instance of MSGDATA object:

pMsgData = new MSGDATA();

After that we can call methods of RemoteSess class.

Client Configuration File

Listing Mailer.exe.config

    <configuration>

      <system.runtime.remoting>

       <application>

          <client>

             <wellknown type="TI_MailService.RemoteSess, Mailer" url="http://localhost:1234/RemoteSess.soap" />

          </client>

          <channels>

             <channel ref="http" port="1235" />

          </channels>

       </application>

      </system.runtime.remoting>

    </configuration>

Implementation of MMapi class

"" name=4.1>MMapi class is actually a wrapper of Extended MAPI functions implemented in Managed C++. It has four public methods:

virtual HRESULT LogonEx(long hwnd, String* profile)
Parameters hwnd � [in] Handle to the window to which the logon dialog box is modal or zero. If no dialog box is displayed during the call, the hwnd parameter is ignored.

profile � [in] Pointer to a string containing the name of the profile to use when logging on. NULL for default profile

Description � Starts MAPI session. Logs a client application on to a session with the messaging system, gets default store and outbox objects.

- Return value S_OK if the function succeeded.

virtual void Logoff(long hwnd)
Parameters hwnd - [in] Parent window handle or zero, indicating that if a dialog box is displayed, it is application modal. If no dialog box is displayed during the call, the hwnd parameter is ignored.
Description - Ends a session with the messaging system. Releases the default store, outbox, and session objects.
virtual HRESULT SendMail(MSGDATA * pMsgData)
Parameters pMsgData � [in] pointer to MSGDATA structure that holds all information about message to submit, declared as following:

[Serializable]

public __gc class MSGDATA

{public:

         String * recipient_name __gc[];

         String * recipient_email __gc[];

         String * subject;

         String * carbon_copy __gc[];

         String * body; String * attachment_name __gc[];

         MemoryStream * bodyStream;

         MemoryStream * attachStream __gc[];

};

Description - submits the Outlook message
virtual HRESULT GetAddressList(String *pAddrList[], int n)
Parameters pAddrList[] � [in] pointer to array of strings; each string contains a single address from the Address Book

n � [in] number of addresses to return (Address Book can contain huge amount of addresses, but in the most of cases user don�t need them all)

Description - lists addresses from the Address Book

Global variables:

private:
    // pointer to the IMAPISession interface:

    static LPMAPISESSION pSes = NULL;
    // pointer to the IMsgStore interface:

    static LPMDB pMdb = NULL;
    // pointer to the IMAPIFolder interface:

    static LPMAPIFOLDER pFolder = NULL;
    // Parent window handle, necessary if any of MAPI dialogs displayed

    // for windows service we should avoid any interaction with desktop,

    // services do not have user interfaces, always zero:

    static long hWnd = 0;

LogonEx Method

The steps we should perform to starts MAPI session, log a client application on to a session with the messaging system, and get default store and outbox objects:
    1. First we initialize global data for the session and prepare the MAPI libraries to accept calls:
    // initialize the MAPIINIT_0 structure:
    
    MAPIINIT_0 MAPIINIT = { MAPI_INIT_VERSION, 
                    // the caller is running as a windows service:
    
                    MAPI_NT_SERVICE | 
                    //bypass the call to CoInitialize:
    
                    MAPI_NO_COINIT | 
                    // create the notification window on a separate thread
    
                    // (MAPI recommends to set this flag for singlethreaded
    
                    // Windows Services):
    
                    MAPI_MULTITHREAD_NOTIFICATIONS}; 
    
    // pass the pointer to MAPIINIT_0 structure in MAPIInitialize call:
    
    pMapiInit = &MAPIINIT;
    hRes = MAPIInitialize(pMapiInit);
    // return if call failed:
    
    if (FAILED(hRes)) return hRes;
    
    Important remark: By default MAPI will try to initialize COM with a call to CoInitialize what initializes COM with a single threaded apartment model. We set the flag MAPI_MULTITHREAD_NOTIFICATIONS, but since COM has already been initialized with a single model and the threading model cannot be changed, MAPIInitialize will fail without setting the MAPI_NO_COINIT flag.
    2. If the previous call successes and no name profile provided, we look for a default profile:
    if (profile==NULL)  
    {
        hRes = FindDefaultProfile(prof);
        if (FAILED(hRes)) return hRes;
        pprof = (LPTSTR)&prof;
    } else 
    // convert the managed pointer to unmanaged:
    
    pprof = (char*)Marshal::StringToHGlobalAnsi(profile).ToPointer();
    
    3. Since we obtained the profile name we can log a client application on to a session with the messaging system:
    LPMAPISESSION __pin* pses = &pSes;
    hRes = MAPILogonEx((ULONG) hWnd, pprof, "",
            MAPI_NEW_SESSION | 
            // client is needed to make calls that require the IMAPISession 
    
            // interface; 
    
            // messages can be sent only through
    
            // a tightly coupled store and transport pair:
    
            MAPI_NO_MAIL | 
            MAPI_NT_SERVICE | MAPI_EXTENDED 
            , pses);
    
    // don�t forget to free previously allocated memory:
    
    if (profile!=NULL) Marshal::FreeHGlobal(pprof);
    
    Remark: Tightly coupling MAPI service providers means implementing the two providers such that the store provider and transport provider which can interact with each other directly (rather than by means of the MAPI spooler) what improves the performance.
    4. If the previous call successes we open a message store and get the IMsgStore pointer for further access:
    LPMDB __pin* pmdb = &pMdb;
    hRes = OpenDefaultStore(pmdb);
    if (hRes)    { Logoff((ULONG) hWnd); return hRes;}
    
    5. The last step: open the Outbox folder (where we are going to place message) and get the IMAPIFolder interface for the further access
    LPMAPIFOLDER __pin* pfolder = &pFolder;
    hRes = OpenOutFolder(pfolder);    
    if (hRes)    { Logoff((ULONG) hWnd); return hRes;}
    

If the LogonEx function succeeds, the return value is zero.

SendMail method

To submit a message we perform next steps:
  • create a new message
  • set recipients for message and carbon copy
  • set PR_SUBJECT, PR_MESSAGE_CLASS, and PR_SUBMIT_FLAGS message's properties
  • create message text
  • add attachments (optional)
  • save all of the message's properties and mark the message as ready to be sent

Creating a new message

We call IMAPIFolder::CreateMessage to create a new message:
// Pointer to IMessage interface:

LPMESSAGE           pMsg=NULL;

// message is created in Outbox folder 

// (pFolder is a pointer to the interface identifier of the Outbox folder),

// pMsg is a pointer to the newly created message:

hRes = pFolder->CreateMessage(NULL, MAPI_DEFERRED_ERRORS, &pMsg);
if (FAILED(hRes)) goto err;

Setting recipients for message and carbon copy

We call the IMessage::ModifyRecipients method with flag MODRECIP_ADD to add recipients of message and carbon copy, previously resolve recipient�s e-mail addresses if only names are provided:
// see code of SetMsgTO in source files

hRes = SetMsgTO(pMsg, pMsgData->recipient_name, pMsgData->recipient_email,
	MAPI_TO);
if (FAILED(hRes)) goto err;    

// the same process of the setting carbon copy recipients,

// the only difference is that RECIP_TYPE is MAPI_CC not MAPI_TO:

hRes = SetMsgTO(pMsg, pMsgData->carbon_copy, pMsgData->carbon_copy, MAPI_CC);
if (FAILED(hRes)) goto err;

Setting PR_SUBJECT, PR_MESSAGE_CLASS, and PR_SUBMIT_FLAGS message�s properties

We call the message's IMAPIProp::SetProps method to set PR_SUBJECT, PR_MESSAGE_CLASS, and PR_SUBMIT_FLAGS properties:
enum {SUBJECT, CLASS, FLAGS, MSG_SENT, MSG_PROPS};
SPropValue          propVal[MSG_PROPS];
char * subject = (char*)Marshal::
StringToHGlobalAnsi(pMsgData->subject).ToPointer();
propVal[SUBJECT].ulPropTag = PR_SUBJECT;
propVal[SUBJECT].Value.lpszA = subject;

propVal[CLASS].ulPropTag = PR_MESSAGE_CLASS;
propVal[CLASS].Value.lpszA = "IPM.Note";

propVal[FLAGS].ulPropTag = PR_SUBMIT_FLAGS;     
propVal[FLAGS].Value.l = SUBMITFLAG_LOCKED ;    
propVal[FLAGS].Value.b = TRUE;

            
hRes = HrGetOneProp((LPMAPIPROP) pMdb, PR_IPM_SENTMAIL_ENTRYID, &pPropValID);
if (FAILED(hRes)) goto err;
            
assert(pPropValID->ulPropTag == PR_IPM_SENTMAIL_ENTRYID);

propVal[MSG_SENT].ulPropTag = PR_SENTMAIL_ENTRYID;
propVal[MSG_SENT].Value.bin.cb = pPropValID->Value.bin.cb;
propVal[MSG_SENT].Value.bin.lpb = pPropValID->Value.bin.lpb;

hRes = pMsg->SetProps(MSG_PROPS, propVal, NULL);
Marshal::FreeHGlobal(subject);
if (FAILED(hRes)) goto err;

Creating message text

Message text is stored in two message properties: PR_BODY and PR_RTF_COMPRESSED. If message store is an RTF-aware, we set PR_RTF_COMPRESSED property only; if message store is non-RTF-aware we set both properties.
    1. We call the message�s IMAPIProp::OpenProperty method to retrieve the PR_STORE_SUPPORT_MASK property:
    LPSTREAM            pStream=NULL;
    LPSTREAM            pUnStream=NULL;
    
    hRes = pMsg->OpenProperty(PR_RTF_COMPRESSED, &IID_IStream,
            STGM_CREATE | STGM_WRITE, MAPI_CREATE | MAPI_MODIFY,
            (LPUNKNOWN FAR *) &pStream);
    if (FAILED(hRes)) goto err;
    
    hRes = HrGetOneProp((LPMAPIPROP) pMdb, PR_STORE_SUPPORT_MASK,
            &pPropVal);
    if (FAILED(hRes)) goto err;
    
    assert(pPropVal->ulPropTag == PR_STORE_SUPPORT_MASK);
    2. Call the WrapCompressedRTFStream function passing the STORE_UNCOMPRESSED_RTF flag if the STORE_UNCOMPRESSED bit is set in the message store�s PR_STORE_SUPPORT_MASK property:
    if ((pPropVal->Value.i & STORE_UNCOMPRESSED_RTF) == 0)
        hRes = WrapCompressedRTFStream(pStream, MAPI_MODIFY, &pUnStream);
    else 
        hRes = WrapCompressedRTFStream(pStream, MAPI_MODIFY |
            STORE_UNCOMPRESSED_RTF, &pUnStream);
    if (FAILED(hRes)) goto err;
    
    3. Write the message text to the stream returned from WrapCompressedRTFStream:
    hRes = CopyStream(pMsgData->bodyStream, pUnStream);
    if (FAILED(hRes)) goto err;
    
    4. Call the Commit and Release methods on the streams to commit the changes and free memory:
    hRes = pUnStream->Commit(STGC_DEFAULT);
    pUnStream->Release();
    if (FAILED(hRes)) goto err;
    
    hRes = pStream->Commit(STGC_DEFAULT); 
    pStream->Release();
    if (FAILED(hRes)) goto err;
    
    5. If message store provider doesn�t support rtf, we must add non-RTF message content by setting PR_BODY property:
    if ((pPropVal->Value.i & STORE_RTF_OK) == 0)
    {
        hRes = pMsg->OpenProperty(PR_BODY, &IID_IStream,
                STGM_CREATE | STGM_WRITE, MAPI_CREATE | MAPI_MODIFY,
                    (LPUNKNOWN FAR *) &pStream);
        if (FAILED(hRes)) goto err;
    
        hRes = CopyStream(pMsgData->bodyStream, pStream);
        if (FAILED(hRes)) goto err;
    }

Adding attachments (optional)

Add attachments if there are any:
if (pMsgData->attachment_name != NULL &&
    pMsgData->attachment_name.Length != 0)
hRes = SetMsgATTACHMENT(pMsg, pMsgData->attachStream, 
pMsgData->attachment_name);
if (FAILED(hRes)) goto err;    

Saving all of the message's properties and marking the message as ready to be sent

Call the IMessage::SubmitMessage method to save changes:
hRes = pMsg->SubmitMessage(0L);
if (FAILED(hRes)) goto err;

New suggestions and ideas are welcome. Enjoy ;o)

References

    1. "Professional C#, 2nd Edition" by Simon Robinson, K. Scott Allen, Ollie Cornes, Jay Glynn, Zach Greenvoss, Burton Harvey, Christian Nagel, Morgan Skinner, Karli Watson
    2. "Advanced .NET Remoting" by Ingo Rammer

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here