![]() |
General Programming »
Internet / Network »
Remoting
Intermediate
Remote Mail (.NET Remoting + MAPI)By WiBThis 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. |
C#, VC7.NET 1.0, .NET 1.1, Win2K, WinXP, Win2003, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
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

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[];
};
InitializeLifetimeService method of the base class MarshalByRefObject to change default lease configurations: public:
virtual Object* InitializeLifetimeService()
{
return NULL;
}
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);
}
}
// 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");
Listing TI_MailService.exe.config:
<configuration>
<system.runtime.remoting>
<application>
<channels>
<channel ref="http" port="1234"/>
</channels>
</application>
</system.runtime.remoting>
</configuration>
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).
Since the project has installer classes we can install and uninstall service from command line using utility installutil.exe as following:

// 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.
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.
<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>
"" 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 |
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;
// 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.
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();
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.
IMsgStore pointer for further access: LPMDB __pin* pmdb = &pMdb;
hRes = OpenDefaultStore(pmdb);
if (hRes) { Logoff((ULONG) hWnd); return hRes;}
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.
PR_SUBJECT, PR_MESSAGE_CLASS, and PR_SUBMIT_FLAGS message's properties
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;
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;
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;
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.
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);
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;
WrapCompressedRTFStream: hRes = CopyStream(pMsgData->bodyStream, pUnStream);
if (FAILED(hRes)) goto err;
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;
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;
}
if (pMsgData->attachment_name != NULL &&
pMsgData->attachment_name.Length != 0)
hRes = SetMsgATTACHMENT(pMsg, pMsgData->attachStream,
pMsgData->attachment_name);
if (FAILED(hRes)) goto err;
IMessage::SubmitMessage method to save changes: hRes = pMsg->SubmitMessage(0L);
if (FAILED(hRes)) goto err;
New suggestions and ideas are welcome. Enjoy ;o)
| You must Sign In to use this message board. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 30 Jun 2003 Editor: Heath Stewart |
Copyright 2003 by WiB Everything else Copyright © CodeProject, 1999-2009 Web19 | Advertise on the Code Project |