Writing Managed Sinks for SMTP and Transport Events
Microsoft Corporation
March 2003
Applies to:
Microsoft® Windows® 2000
Microsoft Exchange 2000 Server
Microsoft Visual Studio® .NET
Summary: This document provides developers an overview of how to write event sinks for SMTP and Transport events in managed code using wrappers that obscure some of the details of communicating with the unmanaged server. (16 printed pages)
To download this paper and accompanying code, see Writing Managed Sinks for SMTP and Transport Events at the Microsoft Download Center.
Introduction
How to Write a Managed Event Sink in C#: Step by Step
ShieldsUp Sample Managed Sink
Debugging Managed Sinks
Managed Wrapper Interfaces
Additional Resources
Writing event sinks in managed code allows the programmer to make use of the Microsoft® .NET Framework and to more efficiently write the code that is necessary for the sink. Writing managed sinks can be somewhat difficult, however, because event sink interfaces were designed primarily for C++ programmers and are therefore not very easy to use in a language such as C#. In addition, some of the methods that are imported using Tlbimp.exe will not work unless they are modified at the intermediate language (IL) level. However, these inconsistencies in the imported assemblies have been fixed in the events' primary interop assembly (PIA) provided, and other interfaces that were inconvenient to use have also been wrapped so that they are easier to use from managed code.
These managed wrappers wrap each of the methods on the original interfaces and correctly communicate with the unmanaged server. In addition, some of the original methods are exposed as properties instead of pairs of methods. For example, all pairs of Set and Query methods are exposed as properties.
This document assumes that the reader has a working knowledge of the .NET Framework, COM, COM Interop and Microsoft Windows® 2000 SMTP Service Events.
For information about event sinks in general, see Microsoft Windows 2000 SMTP Service Events. As described in that document, sinks can implement a number of interfaces to handle corresponding events on the server. The methods of those interfaces are then called when the appropriate event is triggered and certain parameters are passed in.
To write a managed event sink, you must link to the PIA that contains the necessary interfaces and implement the interfaces that correspond to the events that need to be handled. Optionally, you can link to the assembly that contains the easier-to-use wrappers for these interfaces.
The following example illustrates how to write a managed SMTP event sink in C# that handles inbound commands. Use this type of sink to handle inbound SMTP commands and messages, and to process them as needed.
This example can be run on a computer running Windows 2000 Server running the SMTP service, or on a computer running Microsoft Exchange 2000 Server. The code contained in this example must be built using Microsoft Visual Studio® .NET, and can be downloaded from https://www.microsoft.com/downloads.
To build the interop
- From a command prompt, run \Program Files\Microsoft Visual Studio .NET\Common7\Tools\vsvars32.bat.
- Run nmake.exe from the directory of the interop (\ManagedSinksWP\Interop).
To build the wrappers
Copy the interop DLL (Microsoft.Exchange.Transport.EventInterop.dll) to the wrappers directory (\ManagedSinksWP\Wrappers).
In the same environment as the interop, run the following from the wrappers directory:
Note The following code has a carriage return inserted for readability. When implementing this code, do not include the carriage return.
csc /t:library /out:Microsoft.Exchange.Transport.EventWrappers.dll
/r:Microsoft.Exchange.Transport.EventInterop.dll *.cs /unsafe
To write the event sink
Start Visual Studio .NET.
On the File menu, click New, and then click Project.
Under Project Types, click Visual C# Projects. Under Templates, double-click Class Library.
In Solution Explorer, right-click the project, and then select Properties.
In project Property Pages, click the Configuration Properties folder. Click Build, and then set the Register for COM interop setting to True. Click OK to close the project Property Pages dialog box.
In Solution Explorer, right-click References, and then select Add Reference. Add references to the following two assemblies to your project:
- Microsoft.Exchange.Transport.EventInterop.dll
- Microsoft.Exchange.Transport.EventWrappers.dll
Add using statements for the two namespaces included in those assemblies as well as the interop namespace. For example:
using System; using System.Runtime.InteropServices; using Microsoft.Exchange.Transport.EventInterop; using Microsoft.Exchange.Transport.EventWrappers;
Create a namespace, for example, ManagedSinks. Within this namespace, create a class that inherits from the ISmtpInCommandSink interface and implements its OnSmtpInCommand method (the bold section in the example). For example:
using System; using System.Runtime.InteropServices; using Microsoft.Exchange.Transport.EventInterop; using Microsoft.Exchange.Transport.EventWrappers; namespace ManagedSinks { public class SampleInboundSink : ISmtpInCommandSink { void ISmtpInCommandSink.OnSmtpInCommand( object server, object session, MailMsg message, ISmtpInCommandContext context) { } } }
Generate a new GUID and add it as a GuidAttribute of the class. To generate a new GUID, click Tools, and then click Create GUID. Select Registry Format, click New GUID, and then click Copy to copy the new GUID to the Clipboard. Paste this new GUID into your code (the bold section in the example), but make sure to exclude the enclosing braces ({ }). For example:
using System; using System.Runtime.InteropServices; using Microsoft.Exchange.Transport.EventInterop; using Microsoft.Exchange.Transport.EventWrappers; namespace ManagedSinks { [Guid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")] public class SampleInboundSink : ISmtpInCommandSink { void ISmtpInCommandSink.OnSmtpInCommand( object server, object session, MailMsg message, ISmtpInCommandContext context) { } } }
Inside the method implementation, instantiate managed wrappers (the bold section in the example) for each of the method's parameters. For example:
using System; using System.Runtime.InteropServices; using Microsoft.Exchange.Transport.EventInterop; using Microsoft.Exchange.Transport.EventWrappers; namespace ManagedSinks { [Guid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")] public class SampleInboundSink : ISmtpInCommandSink { void ISmtpInCommandSink.OnSmtpInCommand( object server, object session, MailMsg message, ISmtpInCommandContext context) { MailMsgPropertyBag Sever = new MailMsgPropertyBag(server); MailMsgPropertyBag Session = new MailMsgPropertyBag(session); Message Msg = new Message(message); SmtpInCommandContext Context = new SmtpInCommandContext(context); // //Add your code here. // } } }
Build the project, which also registers the object for COM Interop. If the object is built on a non-production server, copy the event sink object to the production server and use the RegAsm tool to register it on the server. On the production server, open up a command window and navigate to the folder containing the event sink object. Type the following from the command prompt, where EventSinkDLL is the name of the event sink object:
RegAsm.exe EventSinkDLL.dll /codebase
Register the object as an event sink by using the smtpreg.vbs script.
The syntax of the smtpreg script is as follows:
usage: cscript smtpreg.vbs <Command> <Arguments> Commands: /add <Instance> <Event> <DisplayName | Binding GUID> <SinkClass> <Rule> /remove <Instance> <Event> <DisplayName | Binding GUID> /setprop <Instance> <Event> <DisplayName | Binding GUID> <PropertyBag> <PropertyName> <PropertyValue> /delprop <Instance> <Event> <DisplayName | Binding GUID> <PropertyBag> <PropertyName> /enable <Instance> <Event> <DisplayName | Binding GUID> /disable <Instance> <Event> <DisplayName | Binding GUID> /enum Arguments: <Instance> The SMTP virtual service instance <Event> The event name. Can be one of the following: Transport Events: StoreDriver OnArrival (OnTransportSubmission) OnTransportSubmission OnPreCategorize OnCategorize OnPostCategorize OnTransportRouter MsgTrackLog DnsResolver MaxMsgSize Protocol Events: OnInboundCommand OnServerResponse OnSessionStart OnMessageStart OnPerRecipient OnBeforeData OnSessionEnd <DisplayName> The display name of the event to edit <SinkClass> The sink Programmatic identifier <Rule> The protocol rule to use for the event (ehlo=*,mail from=*, etc) <Binding GUID> The event binding GUID in registry string format: {GUID} <PropertyBag> The "Source" or "Sink" property bag <PropertyName> The name of the property to edit <PropertyValue> The value to assign to the property
In this case, an event binding needs to be added. Use the /add command to add the binding. The Instance parameter asks for a virtual server instance; use the default virtual server instance of 1. The event that is being handled by this sample sink is the OnInboundCommand protocol event. The DisplayName can be any string that describes the sink, enclosed in quotation marks. The SinkClass is the fully qualified name of the exported COM object, which is the name of the enclosing namespace followed by a period and the name of the class. The name of the object in this example would be ManagedSinks.SampleInboundSink. The final parameter describes what commands the sink will respond to, which, in this example, is the EHLO SMTP command. The following is an example of the event binding:
Note The following code has a carriage return inserted for readability. When implementing this code, do not include the carriage return.
cscript smtpreg.vbs /add 1 OnInboundCommand "Sample managed sink"
ManagedSinks.SampleInboundSink EHLO
The following screenshot shows the command and the resultant success message from the ShieldsUp managed sink sample below.
Figure 1. (click picture to see larger image)
This sink handles the OnInboundCommand protocol event, as did the example in the previous section. The ShieldsUp sink listens for EHLO commands sent to the virtual server and immediately drops the connection after responding to the sender with an error message. The managed code below can be executed by following the steps in the previous section.
using System;
using System.Runtime.InteropServices;
using Microsoft.Exchange.Transport.EventInterop;
using Microsoft.Exchange.Transport.EventWrappers;
namespace SampleManagedSink
{
[Guid("8277680B-D636-4054-8BF6-FD91F1EBC1CD")]
// ComVisible enables COM visibility of this class. The default is true.
// Explicitly setting this attribute to true, as shown below, is useful
// if ComVisible is set to false for the namespace and you want the
// classes to be accessible individually.
[ComVisible(true)]
public class ShieldsUp :
ISmtpInCommandSink,
IEventIsCacheable
{
//
// IEventIsCacheable methods
//
void IEventIsCacheable.IsCacheable()
{
// This will return S_OK by default.
}
//
// ISmtpInCommandSink methods
//
void ISmtpInCommandSink.OnSmtpInCommand(
object server,
object session,
MailMsg msg,
ISmtpInCommandContext context)
{
try
{
// Instantiate a wrapper for the context object.
SmtpInCommandContext Context = new
SmtpInCommandContext(context);
// The next line uses properties on the wrapper that
// will be translated to calls to the proper interface.
Context.Response = "Please try again later.";
// The ProtocolEventConstants class contains several constants
// that the CommandStatus can be set to, including the
// constant used here that tells the server to drop the
// session.
Context.CommandStatus = (uint)
ProtocolEventConstants.EXPE_DROP_SESSION;
// To return a value other than S_OK, throw a COMException.
throw new COMException("Event is consumed",
ProtocolEventConstants.EXPE_S_CONSUMED);
}
// Release COM objects.
finally
{
if (null != server)
{
Marshal.ReleaseComObject(server);
}
if (null != session)
{
Marshal.ReleaseComObject(session);
}
if (null != msg)
{
Marshal.ReleaseComObject(msg);
}
if (null != context)
{
Marshal.ReleaseComObject(context);
}
}
}
}
}
The following screenshot shows the message a user receives after connecting to the host computer using telnet on port 25 and typing the EHLO command.
Figure 2.
To debug a managed sink, you can use the Visual Studio .NET debugger. The following steps describe how to break into the debugger so that you can debug your sink.
- Start Visual Studio .NET.
- On the File menu, click Open Solution, and then open the solution that contains your managed sink.
- Click Debug, and then click Processes.
- Select the Show system processes check box if it is not already selected.
- In the list of processes, select Inetinfo.exe, and then click Attach.
- In Attach to Process, select the Common Language Runtime and Native check boxes, click OK, and then click Close.
- Set a breakpoint on any valid line in your code
- Make sure the sink is registered for COM Interop and as an event sink, and then trigger the event that is being captured by your sink. When the flow of execution reaches the breakpoint, it will break into the debugger and allow you to debug the sink.
The following interfaces provide wrappers for existing interfaces:
- SmtpInCommandContext
- SmtpOutCommandContext
- SmtpServerResponseContext
- MailMsgPropertyBag
- MailMsgLoggingPropertyBag
- Message
- Recip
- RecipsAdd
Each of these interfaces is described in more detail below.
Wraps: ISmtpInCommandContext
Methods:
Method | Syntax |
---|---|
NotifyAsyncCompletion | void NotifyAsyncCompletion(int hrResult) |
SetCallback | void SetCallback(ISmtpInCallbackSink callback) |
AppendNativeResponse | void AppendNativeResponse(string response) |
AppendResponse | void AppendResponse(string response) |
Properties:
Property | Type | Read/Write | Wrapped Methods |
---|---|---|---|
Command | string | read only | QueryCommand |
CommandKeyword | string | read only | QueryCommandKeyword |
CommandStatus | uint | read, write | SetCommandStatus, QueryCommandStatus |
NativeResponse | string | read, write | SetNativeResponse,
QueryNativeResponse |
ProtocolErrorFlag | bool | read, write | SetProtocolErrorFlag, QueryProtocolErrorFlag |
Response | string | read, write | SetResponse,
QueryResponse |
SmtpStatusCode | uint | read, write | SetSmtpStatusCode, QuerySmtpStatusCode |
For more information about ISmtpInCommandContext, see https://go.microsoft.com/fwlink/?LinkId=14124.
Wraps: ISmtpOutCommandContext
Methods:
Method | Syntax |
---|---|
NotifyAsyncCompletion | void NotifyAsyncCompletion(int hrResult) |
AppendCommand | void AppendCommand(string command) |
Properties:
Property | Type | Read/Write | Wrapped Methods |
---|---|---|---|
Command | string | read, write | SetCommand, QueryCommand |
CommandKeyword | string | read only | QueryCommandKeyword |
CommandStatus | uint | read, write | SetCommandStatus, QueryCommandStatus |
CurrentRecipientIndex | uint | read only | QueryCurrentRecipientIndex |
NativeCommand | string | read only | QueryNativeCommand |
For more information about ISmtpOutCommandContext, see https://go.microsoft.com/fwlink/?LinkId=14126.
Wraps: ISmtpServerResponseContext
Methods:
Method | Syntax |
---|---|
NotifyAsyncCompletion | void NotifyAsyncCompletion(int hrResult) |
Properties:
Property | Type | Read/Write | Wrapped Methods |
---|---|---|---|
Command | string | read only | QueryCommand |
CommandKeyword | string | read only | QueryCommandKeyword |
NextEventState | uint | read, write | SetNextEventState, QueryNextEventState |
PipelinedFlag | bool | read only | QueryPipelinedFlag |
Response | string | read only | QueryResponse |
ResponseStatus | uint | read, write | SetResponseStatus, QueryResponseStatus |
SmtpStatusCode | uint | read only | QuerySmtpStatusCode |
For more information about ISmtpServerResponseContext, see https://go.microsoft.com/fwlink/?LinkId=14127.
Wraps: IMailMsgPropertyBag
Methods:
Method | Syntax |
---|---|
GetBool | bool GetBool(uint propId) |
PutBool | void PutBool(uint propId, bool value) |
GetDWORD | uint GetDWORD(uint propId) |
PutDWORD | void PutDWORD(uint propId, uint value) |
GetProperty | byte[] GetProperty(uint propId) |
PutProperty | void PutProperty(uint propId, byte[] value) |
GetStringA | string GetStringA(uint propId) |
PutStringA | void PutStringA(uint propId, string value) |
GetStringW | string GetStringW(uint propId) |
PutStringW | void PutStringW(uint propId, string value |
The MailMsgPropertyBag wrapper also contains several common properties that are only available if this interface is wrapping a server object.
Server Common Properties:
Property | Type | Read/Write | Wrapped Methods |
---|---|---|---|
Server_CATBindType | string | read, write | GetStringA, PutStringA |
Server_CATDomain | string | read, write | GetStringA, PutStringA |
Server_CATDSHost | string | read, write | GetStringA, PutStringA |
Server_CATDSType | string | read, write | GetStringA, PutStringA |
Server_CATEnable | uint | read, write | GetDWORD, PutDWORD |
Server_CATFlags | uint | read, write | GetDWORD, PutDWORD |
Server_CATNamingContext | string | read, write | GetStringA, PutStringA |
Server_CATPassword | string | read, write | GetStringA, PutStringA |
Server_CATPort | uint | read, write | GetDWORD, PutDWORD |
Server_CATSchema | string | read, write | GetStringA, PutStringA |
Server_CATUser | string | read, write | GetStringA, PutStringA |
Server_DefaultDomain | string | read, write | GetStringA, PutStringA |
Server_Instance | uint | read, write | GetDWORD, PutDWORD |
For more information about IMailMsgPropertyBag, see https://go.microsoft.com/fwlink/?LinkId=14128.
Wraps: IMailMsgPropeties
IMailMsgRecipientsBase
IMailMsgRecipients
Methods:
Method | Syntax |
---|---|
AllocNewList | RecipsAdd AllocNewList() |
Commit | void Commit() |
CopyContentToStream | void CopyContentToStream(System.IO.Stream UserStream) |
ForkForRecipients | void ForkForRecipients(out Message msgNew, out RecipsAdd recipsAddNew) |
GetContentSize | uint GetContentSize() |
ReadContent | byte[] ReadContent(uint offset, uint length) |
RebindAfterFork | void RebindAfterFork(Message msgOrig, object storeDriver) |
SetContentSize | void SetContentSize(uint size) |
WriteContent | void WriteContent(uint offset, uint length, out uint lengthWritten, byte[] content) |
WriteList | void WriteList(RecipsAdd recipsAdd) |
Properties:
Property | Type | Read/Write | Wrapped Methods |
---|---|---|---|
HrCatStatus | int | read, write | GetDWORD, PutDWORD |
MessageStatus | uint | read, write | GetDWORD, PutDWORD |
MsgClass | uint | read, write | GetDWORD, PutDWORD |
Rfc822BccAddress | string | read only | GetStringA |
Rfc822CcAddress | string | read only | GetStringA |
Rfc822FromAddress | string | read only | GetStringA |
Rfc822MsgId | string | read only | GetStringA |
Rfc822MsgSubject | string | read only | GetStringA |
Rfc822ToAddress | string | read only | GetStringA |
SenderAddressSMTP | string | read, write | GetStringA, PutStringA |
SenderAddressX500 | string | read, write | GetStringA, PutStringA |
SizeHint | uint | read, write | GetDWORD, PutDWORD |
In addition to these properties, the Message class also provides a collection that contains information about every recipient of the message. The collection's type is RecipCollection and it is exposed as a property named Recips. Each Recip in the collection exposes the following properties:
Property | Type | Read/Write | Wrapped Methods |
---|---|---|---|
Domain | string | read, write | GetStringA, PutStringA |
ErrorCode | uint | read, write | GetDWORD, PutDWORD |
RecipientFlags | uint | read, write | GetDWORD, PutDWORD |
SmtpAddress | string | read only | GetStringA |
SmtpStatusString | string | read, write | GetStringA, PutStringA |
X500Address | string | read only | GetStringA |
For more information about IMailMsgPropeties, see https://go.microsoft.com/fwlink/?LinkId=14129.
For more information about IMailMsgRecipientsBase, see https://go.microsoft.com/fwlink/?LinkId=14130.
For more information about IMailMsgRecipients, see https://go.microsoft.com/fwlink/?LinkId=14131.
Wraps: IMailMsgRecipientsAdd
Methods:
Method | Syntax |
---|---|
AddSMTPRecipient | void AddSMTPRecipient(string SMTPAddress)
void AddSMTPRecipient(string SMTPAddress, Recip sourceRecip)—copy all non-address properties from an existing recipient |
AddSMTPRecipientSecondary | void AddSMTPRecipientSecondary(string SMTPAddress, Recip sourceRecip)—copy all non-address properties from an existing recipient if it is non-null |
Properties: none
For more information about IMailMsgRecipientsAdd, see https://go.microsoft.com/fwlink/?LinkId=14132.
Note Any interface that needs to be implemented by a sink does not have a wrapper because the interface needs to be implemented directly. The following interfaces are meant to be implemented by the programmer and do not have wrappers:
- ISmtpInCommandSink
- ISmtpOutCommandSink
- ISmtpServerResponseSink
- ISMTPServer
- IMailTransportSubmission
- IMailTransportOnPreCategorize
- IMailTransportOnPostCategorize
- IMessageRouter
- IMailTransportRoutingEngine
- IMsgTrackLog
- IDnsResolverRecord
- IDnsResolverRecordSink
- ISmtpMaxMsgSize
- Microsoft Windows 2000 SMTP Service Events
- smtpreg.vbs Event Management Script
- Microsoft SMTP Server Events for Windows 2000
For more information: https://msdn.microsoft.com/exchange/