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.

Contents

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

Introduction

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.

How to Write a Managed Event Sink in C#: Step by Step

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

  1. From a command prompt, run \Program Files\Microsoft Visual Studio .NET\Common7\Tools\vsvars32.bat.
  2. Run nmake.exe from the directory of the interop (\ManagedSinksWP\Interop).

To build the wrappers

  1. Copy the interop DLL (Microsoft.Exchange.Transport.EventInterop.dll) to the wrappers directory (\ManagedSinksWP\Wrappers).

  2. 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

  1. Start Visual Studio .NET.

  2. On the File menu, click New, and then click Project.

  3. Under Project Types, click Visual C# Projects. Under Templates, double-click Class Library.

  4. In Solution Explorer, right-click the project, and then select Properties.

  5. 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.

  6. 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
  7. 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;
    
  8. 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)
          {
          }
       }
    }
    
  9. 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)
             {
             }
          }
    }
    
  10. 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.
          //
    
          }
       }
    }
    
  11. 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
    
  12. 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.

Click here to see larger image

Figure 1. (click picture to see larger image)

ShieldsUp Sample Managed Sink

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.

Debugging Managed Sinks

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.

  1. Start Visual Studio .NET.
  2. On the File menu, click Open Solution, and then open the solution that contains your managed sink.
  3. Click Debug, and then click Processes.
  4. Select the Show system processes check box if it is not already selected.
  5. In the list of processes, select Inetinfo.exe, and then click Attach.
  6. In Attach to Process, select the Common Language Runtime and Native check boxes, click OK, and then click Close.
  7. Set a breakpoint on any valid line in your code
  8. 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.

Managed Wrapper Interfaces

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.

Interface: SmtpInCommandContext

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.

Interface: SmtpOutCommandContext

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.

Interface: SmtpServerResponseContext

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.

Interface: MailMsgPropertyBag

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.

Interface: Message

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.

Interface: RecipsAdd

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

Additional Resources

For more information: https://msdn.microsoft.com/exchange/