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.
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__gcclass 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__gcclass 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)
virtualvoid 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
{
publicoverrideobject InitializeLifetimeService()
{
returnbase.InitializeLifetimeService();
}
publicoverrideint LogonEx(int hWnd, String profile)
{
returnbase.LogonEx(hWnd, profile);
}
publicoverridevoid Logoff(int hWnd)
{
base.Logoff(hWnd);
}
publicstring[] AddressList(int n)
{
String[] pAddrList = new String[n];
int i = base.GetAddressList(pAddrList, pAddrList.Length);
return pAddrList;
}
publicoverrideint SendMail(MSGDATA pMsgData)
{
returnbase.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.
private RemoteSess sess = null;
HttpChannel chnl = new HttpChannel(1234);
ChannelServices.RegisterChannel(chnl);
sess = new RemoteSess();
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.
privatestatic RemoteSess sess = null;
privatestatic HttpChannel chnl;
protectedoverridevoid OnStart(string[] args)
{
sess = new RemoteSess();
RemotingServices.Marshal(sess, "RemoteSess.soap");
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:
protectedoverridevoid 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:
Collapse private ServiceInstaller serviceInstaller;
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
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:
Collapse 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")]
publicclass MSGDATA
{
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")]
publicclass MMapi : System.MarshalByRefObject
{
[SoapMethod(SoapAction="http://schemas.microsoft.com/clr/nsassem/"
+ "TI_MAPI.MMapi/TI_MAPI#GetAddressList")]
publicvirtual Int32 GetAddressList(String[] pAddrList, Int32 n)
{
return((Int32) (Object) null);
}
[SoapMethod(SoapAction="http://schemas.microsoft.com/clr/nsassem/"
+ "TI_MAPI.MMapi/TI_MAPI#SendMail")]
publicvirtual Int32 SendMail(MSGDATA pMsgData)
{
return((Int32) (Object) null);
}
[SoapMethod(SoapAction="http://schemas.microsoft.com/clr/nsassem/"
+ "TI_MAPI.MMapi/TI_MAPI#Logoff")]
publicvirtualvoid Logoff(Int32 hwnd)
{
return;
}
[SoapMethod(SoapAction="http://schemas.microsoft.com/clr/nsassem/"
+ "TI_MAPI.MMapi/TI_MAPI#LogonEx")]
publicvirtual Int32 LogonEx(Int32 hwnd, String profile)
{
return((Int32) (Object) null);
}
[SoapMethod(SoapAction=@"http://schemas.microsoft.com/clr/nsassem/"
+ "TI_MAPI.MMapi/TI_MAPI#InitializeLifetimeService")]
publicoverride Object InitializeLifetimeService()
{
return((Object) (Object) null);
}
}
}
Listing TI_MailService assembly:
Collapse 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")]
publicclass RemoteSess : TI_MAPI.MMapi
{
[SoapMethod(SoapAction="http://schemas.microsoft.com/clr/nsassem/"
+ "TI_MailService.RemoteSess/TI_MailService#SendMail")]
publicoverride Int32 SendMail(TI_MAPI.MSGDATA pMsgData)
{
return((Int32) (Object) null);
}
[SoapMethod(SoapAction="http://schemas.microsoft.com/clr/nsassem/"
+ "TI_MailService.RemoteSess/TI_MailService#Logoff")]
publicoverridevoid Logoff(Int32 hWnd)
{
return;
}
[SoapMethod(SoapAction="http://schemas.microsoft.com/clr/nsassem/"
+ "TI_MailService.RemoteSess/TI_MailService#LogonEx")]
publicoverride Int32 LogonEx(Int32 hWnd, String profile)
{
return((Int32) (Object) null);
}
[SoapMethod(SoapAction="http://schemas.microsoft.com/clr/nsassem/"
+ "TI_MailService.RemoteSess/TI_MailService#"
+ "InitializeLifetimeService")]
publicoverride 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:
using TI_MAPI;
using TI_MailService;
...
private RemoteSess sess = null;
private MSGDATA pMsgData = null;
...
RemotingConfiguration.Configure("mailer.exe.config");
sess = new RemoteSess();
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. |
virtualvoid 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:
static LPMAPISESSION pSes = NULL;
static LPMDB pMdb = NULL;
static LPMAPIFOLDER pFolder = NULL;
staticlong 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:
MAPIINIT_0 MAPIINIT = { MAPI_INIT_VERSION,
MAPI_NT_SERVICE |
MAPI_NO_COINIT |
MAPI_MULTITHREAD_NOTIFICATIONS};
pMapiInit = &MAPIINIT;
hRes = MAPIInitialize(pMapiInit);
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
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 |
MAPI_NO_MAIL |
MAPI_NT_SERVICE | MAPI_EXTENDED
, pses);
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:
LPMESSAGE pMsg=NULL;
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:
hRes = SetMsgTO(pMsg, pMsgData->recipient_name, pMsgData->recipient_email,
MAPI_TO);
if (FAILED(hRes)) goto err;
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:
Collapse 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
WiB
| Click here to view WiB's online profile. |