|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
Table Of Contents
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 IntroductionNowadays 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.
Remote Object ImplementationPassing Objects in Remote MethodsRemoting 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 public __gc class MMapi: public MarshalByRefObject
{
...
}
In the contrary, the [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 ManagementSince 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 AssembliesServer 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 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 ImplementationObject on a Remote ServerOn 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 FilesWe 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 ServiceWe need the server application start automatically at a boot-time. For server application we create a Windows Service Project. We override // 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 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 Service InstallationSince the project has installer classes we can install and uninstall service from command line using utility installutil.exe as following:
Client ImplementationSoapSuds-Generated MetadataSince .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:
// 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 ObjectIn 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 Client Configuration FileListing 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:
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 MethodThe 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:
// 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 SendMail methodTo submit a message we perform next steps:
Creating a new messageWe 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 copyWe 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 propertiesWe 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 textMessage 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.
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;
}
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 sentCall theIMessage::SubmitMessage method to save changes: hRes = pMsg->SubmitMessage(0L);
if (FAILED(hRes)) goto err;
New suggestions and ideas are welcome. Enjoy ;o) References
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||