Remote Mail (.NET Remoting + MAPI)






4.73/5 (19 votes)
Jun 26, 2003
10 min read

127599

4643
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.
- Download server project - 52 Kb
- Download client project - 48 Kb
- Download Extended MAPI wrapper project - 51 Kb
Table Of Contents
- Implementation of MMapi class (Managed C++)
Setting recipients for message and carbon copy
Setting PR_SUBJECT, PR_MESSAGE_CLASS, and PR_SUBMIT_FLAGS message’s properties
Saving all of the message's properties and marking the message as ready to be sent
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.
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 overrideInitializeLifetimeService
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 classMMapi
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
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
// 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 theRemoteSess
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
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
, andPR_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 callIMAPIFolder::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 theIMessage::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'sIMAPIProp::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 theIMessage::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