random thoughts, formed in the twisted mind of a coder... RSS 2.0
# Wednesday, 13 August 2008

Edit: If you want to create customized tarpitting for your sink, please do not use Thread.Sleep(). Instead read the article "SMTP Sinks: Using AsyncCompletion for Tarpitting". It explains how to create custom tarpitting the right way.

For this project I’m doing I needed to write custom authentication within the SMTP protocol. I needed to write my own AUTH sink. The goal of the sink is to authenticate to a different user-base and to give the authenticated users the right to relay through this server.

This seems quite simple if you have ever written a protocol sink for IIS SMTP or Exchange, but nothing seemed more true. You cannot simply create a protocol sink and bind it to the AUTH keyword, though that is the first step.

For clarification of the process I will now first show the steps in the SMTP protocol for basic authentication as implemented through the AUTH LOGIN command.

S: 220 mymail.local Microsoft ESMTP MAIL Service
C: EHLO mydomain.com
S: 250-mydomain.com Hello [127.0.0.1]
S: 250-AUTH=LOGIN
S: 250-AUTH LOGIN
S: 250-…
S: 250 OK
C: AUTH LOGIN
S: 334 VXNlcm5hbWU6
C: dGVzdA==
S: 334 UGFzc3dvcmQ6
C: cGFzcw==
S: 235 2.7.0 Authentication successful
C: ...

As you can see in the example above, the authorization is split-up into three separate steps. For every step you need to be in control over the input and the output. But as you can only bind your sink to the AUTH command, you will not receive the remaining two input lines containing the credentials.

To overcome this problem, you have to setup your sink to receive all the input in that SMTP session, starting from the input after the AUTH LOGIN command until the credentials are passed. This has to be done from within the sink at session level and cannot be done by binding the sink another way.

Remark: From this point on there is absolutely no documentation at Microsoft that can help you find your way. Some information in this post was scraped together from the internet (references posted at the end) and the rest is from trying over and over again (with the help from an escalation engineer at Microsoft, as I’m in the lucky position to have a Microsoft Premium Support account).

To set the sink to listen to all coming input you have to use the SetCallback method of the context parameter. As parameter you have to pass an instance of an ISmtpInCallbackSink implementation.

The example below shows the first part:

private const string  EOL = " [sink]\r\n";

private string blobText = null;
private string credUser = null;
private string credPass = null;

void ISmtpInCommandSink.OnSmtpInCommand(object _server, object _session, MailMsg _msg, ISmtpInCommandContext _context)
{        {
  MailMsgPropertyBag    session = null;
  SmtpInCommandContext  context = null;

  try
  {
    //get parameter wrappers
    session = new MailMsgPropertyBag(_session);
    context = new SmtpInCommandContext(_context);
        
    //check for which command this event was fired
    string command = context.CommandKeyword.ToUpper();
    if (command.Equals("RCPT"))
    {
      ...
    }
    else if (command.Equals("AUTH"))
    {
      if (context.Command.ToUpper().Equals("AUTH LOGIN"))
      {
        //clear buffers
        blobText = "";
        credUser = "";
        credPass = "";

        //set to receive next line
        context.CommandStatus = (uint)ProtocolEventConstants.EXPE_BLOB_READY;
        context.SetCallback(this);

        context.Response = "334 VXNlcm5hbWU6" + EOL;
        context.SmtpStatusCode = 334;
        context.ProtocolErrorFlag = false;

        //to return a value other than S_OK, throw a COMException.
        throw new COMException("Event is consumed", ProtocolEventConstants.EXPE_S_CONSUMED);
      }
    }
  }
  finally
  {
    //release COM objects
    if (_server != null)
      Marshal.ReleaseComObject(_server);
    if (_session != null)
      Marshal.ReleaseComObject(_session);
    if (_msg != null)
      Marshal.ReleaseComObject(_msg);
    if (_context != null)
      Marshal.ReleaseComObject(_context);
  }
}

As you can see in the code above,  at least four actions have to be taken in order to make it work.
1. Response has to be formed
2. CommandStatus has to be set
3. Callback has to be set
4. The event has to be consumed

The first step is quite easy , just fill in the parameters of the context. This has to be done in order to let the client know that you have accepted the AUTH LOGIN command.
The second step is necessary to let the SMTP server know that you will be expecting the input stream.
In the third step you’ll set the callback for when input has been received. The callback is called when new input arrives at the server.
And finally in the fourth step you will consume the entire event. The AUTH LOGIN event that is, not the next lines with the credentials. If you do not consume the event, the event is eventually passed to the normal AUTH handler which will take over the controls again leaving you with empty hands. So, simply consume it. This however comes with a penalty that I’ll discuss later on.

The implementation of the ISmtpInCallbackSink is pretty straight forward as it takes the same parameters as the other protocol sinks.

void ISmtpInCallbackSink.OnSmtpInCallback(object _server, object _session, MailMsg _msg, ISmtpInCallbackContext _context)
{
  //define ref-pointer for the blob
  IntPtr blobPtrPtr = IntPtr.Zero;

  try
  {
    //allocate memory for the ref-pointer
    blobPtrPtr = Marshal.AllocHGlobal(IntPtr.Size);
    Marshal.WriteIntPtr(blobPtrPtr, IntPtr.Zero);
    
    //get the ref-pointer
    uint blobSize = 0;
    _context.QueryBlob(blobPtrPtr, ref blobSize);
        
    //de-reference the ref-pointer to get the blob-pointer
    IntPtr blobPtr = Marshal.ReadIntPtr(blobPtrPtr);

    //read the text from the blob
    string text = Marshal.PtrToStringAnsi(blobPtr, (int)blobSize);

    //add text to buffer
    blobText += text;
        
    //check end-of-input
    if (blobText.EndsWith("\r\n"))
    {
      //remove CRLF
      blobText = blobText.Substring(0, blobText.Length-2);

      //decode base64
      string decoded = "";
      try
      {
        decoded = Encoding.ASCII.GetString(Convert.FromBase64String(blobText));
      }
      catch (Exception excpt)
      {
        //clear buffers
        blobText = "";
        credUser = "";
        credPass = "";

        //respond to user
        string response = "501 invalid input" + EOL;
        _context.SetResponse(response, (uint)response.Length);
        _context.SetSmtpStatusCode(501);
        _context.SetCommandStatus((uint)ProtocolEventConstants.EXPE_BLOB_DONE | (uint)ProtocolEventConstants.EXPE_COMPLETE_FAILURE);

        return;
      }

      //clear blob buffer
      blobText = "";

      //check if username was already passed
      if (string.IsNullOrEmpty(credUser))
      {
        //store username
        credUser = decoded;

        //respond to user
        string response = "334 UGFzc3dvcmQ6" + EOL;
        _context.SetResponse(response, (uint)response.Length);
        _context.SetSmtpStatusCode(334);
        _context.SetCommandStatus((uint)ProtocolEventConstants.EXPE_BLOB_READY);
      }
      else
      {
        //store password
        credPass = decoded;

        //check credentials
        If (credUser.Equals(“test”) && credPass.Equals(“pass”))
        {
          //respond to user
          string response = "235 2.7.0 Authentication successful" + EOL;
          _context.SetResponse(response, (uint)response.Length);
          _context.SetSmtpStatusCode(235);
          _context.SetCommandStatus((uint)ProtocolEventConstants.EXPE_BLOB_DONE);
        }
        else
        {
          //clear buffers
          blobText = "";
          credUser = "";
          credPass = "";

          //respond to user
          string response = "535 5.7.8  Authentication credentials invalid" + EOL;
          _context.SetResponse(response, (uint)response.Length);
          _context.SetSmtpStatusCode(535);
          _context.SetCommandStatus((uint)ProtocolEventConstants.EXPE_BLOB_DONE | (uint)ProtocolEventConstants.EXPE_COMPLETE_FAILURE);
        }
      }
    }
    else
    {
      _context.SetCommandStatus((uint)ProtocolEventConstants.EXPE_BLOB_READY);
    }
  }
  finally
  {
    //free the allocated blob space
    if (!blobPtrPtr.Equals(IntPtr.Zero))
      Marshal.FreeHGlobal(blobPtrPtr);

    //release COM objects
    if (_server != null)
      Marshal.ReleaseComObject(_server);
    if (_session != null)
      Marshal.ReleaseComObject(_session);
    if (_msg != null)
      Marshal.ReleaseComObject(_msg);
    if (_context != null)
      Marshal.ReleaseComObject(_context);
  }
}

Ok, so what’s happening here?
The first thing that is done, is to get access to the input. The input comes in blobs. You have to query to get a pointer to such a blob. The blob contains the actual bytes passed by the client to the server. There is no need to allocate memory for the blob itself as the pointer provided points directly in to allocated memory by IIS. When you’re done and moving on to the next line of input, IIS frees the memory itself.

When you look closely, you’ll notice that the blobs can contain parts of the whole input and not necessarily the complete line. This is due to the client who might send the line in parts to the server. A good example being the Windows telnet clients, who sends the line byte by byte.

Once that all credential lines are passed, you can check the authorization. If the credentials passed are bad or the client has not enough rights, you’ll have to respond to the client with an permanent failure. Otherwise you can respond to the client with a success response. Either way, you’ll also have to signal the SMTP server that you’re ready receiving the input and that the control is passed back to the server.

This is done by simply OR-ing the CommandStatus. This is also not documented but the CommandStatus field is a bit-field, where you can multiplex different values.

_context.SetCommandStatus((uint)ProtocolEventConstants.EXPE_BLOB_DONE | (uint)ProtocolEventConstants.EXPE_COMPLETE_FAILURE);

Why is there no multiplexed CommandStatus for the success scenario? This is because the EXPE_SUCCESS is an uint with a value of 0. As you could OR it for the sake of read-ability of your code, it won’t actually do anything.

The list of statuses and their values that I know about are these:
EXPE_SUCCESS           = 0x00000000
EXPE_NOT_PIPELINED     = 0x00000000
EXPE_PIPELINED         = 0x00000001
EXPE_REPEAT_COMMAND    = 0x00000002
EXPE_BLOB_READY        = 0x00000004
EXPE_BLOB_DONE         = 0x00000008
EXPE_DROP_SESSION      = 0x00010000
EXPE_CHANGE_STATE      = 0x00020000
EXPE_TRANSIENT_FAILURE = 0x00040000
EXPE_COMPLETE_FAILURE  = 0x00080000

Well, here you have your custom AUTH sink. You’re done, the work is finished, time for a break. But as you’re heading for the coffee-machine, you here your test application making all these Error noises. Because, as you will soon find out, your success response on the AUTH sequence is not understood by the SMTP server and you are still unable to relay any message.

As stated in the beginning of this document, the goal is to make a custom authentication sink, which enables me to relay messages for those who have logged on successfully. You know the setting “Allow all computers which successfully authenticate to relay, regardless of the list above”...

What to do? Well, this is where I got stuck too. I’ve tried dumping all server and session properties to screen in order to find that one property that says “I AM AUTHENTICATED”, but no such luck.
But, as normally, the answer is often a lot simpler than you first think.
If you’re unable to tell the server who may relay, tell the server who may NOT relay. In other words, turn it around. Set the server to relay for everyone and use an RCPT sink to disable relaying for those who are not authenticated!

Sounds simple and it relatively is. There’s only one catch, although you can bind the same sink to multiple events (different filters like MAIL, RCPT, etc.), the instance of your sink is not the same for those separate events. Not even within the same session. So, this disables you to create a shared property within your class which you can use to pass information from one event to another one.

This problem can be overcome by using the session’s property bag. You can set  numbered properties with various types (bool, string, etc…).

private const uint SESSION_PROPIDX_AUTHSUCCESS = 7;

//set custom session property
MailMsgPropertyBag session = new MailMsgPropertyBag(_session);
session.PutBool(SESSION_PROPIDX_AUTHSUCCESS, true);

//get custom session property
bool ses_authenticated = false;
try{ ses_authenticated = session.GetBool(SESSION_PROPIDX_AUTHSUCCESS); }catch (Exception excpt){;}

What you’re seeing here is first how to set the property and second hot to read it. Setting the property is done at the during the AUTH event. Reading the property is done at handling the RCPT event.
Note that when reading a property that has not been set, an exception is thrown. Hence the try-catch.
Please also note the constant that is defined to a value of 7. Why 7 do you ask? I don’t know actually. When dumping session properties to screen I found that property 7 was never set. This doesn’t mean that it’s never set. It may be set to another value in some special cases or in other versions of the SMTP server. Again there is no information available saying which property is what.
So, if 7 doesn’t work for you, try a higher one. I think the max is 20.000, so you would expect there to be at least one free property. ;-)

Below you’ll find an example of the RCPT event part

private static Regex rexAddress = new Regex(@"[a-z0-9]{1,}(?:(?:\.|-|_)[a-z0-9]{1,}){0,}@[a-z0-9]{1,}(?:(?:\.|-|_)[a-z0-9]{1,}){0,}\.[a-z0-9]{1,}", RegexOptions.IgnoreCase);

...

//check for which command this event was fired
string command = context.CommandKeyword.ToUpper();
if (command.Equals("RCPT"))
{
  //check input
  Match rexMatch = rexAddress.Match(context.Command);
  if (rexMatch.Success)
  {
    if (rexMatch.Value.EndsWith(“@mydomain.com”))
    {
      //vmb found, address supported
      context.Response = "250 2.1.5 " + rexMatch.Value + EOL;
      context.SmtpStatusCode = 250;
      context.CommandStatus = (uint)ProtocolEventConstants.EXPE_SUCCESS; // is 0
      context.ProtocolErrorFlag = false;
    }
    else if (ses_authenticated)
    {
      //vmb notfound, relay-address supported
      context.Response = "250 2.1.5 relaying to " + rexMatch.Value + EOL;
      context.SmtpStatusCode = 250;
      context.CommandStatus = (uint)ProtocolEventConstants.EXPE_SUCCESS; // is 0
      context.ProtocolErrorFlag = false;
    }
    else
    {
      //vbm not found, address unsupported
      context.Response = "550 5.7.1 Unable to relay for " + rexMatch.Value + EOL;
      context.SmtpStatusCode = 550;
      context.CommandStatus = (uint)ProtocolEventConstants.EXPE_COMPLETE_FAILURE;
      context.ProtocolErrorFlag = true;

      //to return a value other than S_OK, throw a COMException.
      throw new COMException("Event is consumed", ProtocolEventConstants.EXPE_S_CONSUMED);
    }
  }
  else
  {
    //no address found in command
    context.Response = "501 5.5.4 Invalid Address" + EOL;
    context.SmtpStatusCode = 501;
    context.CommandStatus = (uint)ProtocolEventConstants.EXPE_COMPLETE_FAILURE;
    context.ProtocolErrorFlag = true;

    //to return a value other than S_OK, throw a COMException.
    throw new COMException("Event is consumed", ProtocolEventConstants.EXPE_S_CONSUMED);
  }
}
else if (command.Equals("AUTH"))
{
  ...

So, and this is the time where you can go and relax now. Let your test application do its work while you’re enjoying a well deserved cup-a-coffee at your well deserved coffee-break.

Edit: If you want to create customized tarpitting for your sink, please do not use Thread.Sleep(). Instead read the article "SMTP Sinks: Using AsyncCompletion for Tarpitting". It explains how to create custom tarpitting the right way.

References:

RFC of the AUTH scheme:
http://tools.ietf.org/html/rfc4954

RFC of SMTP:
http://tools.ietf.org/html/rfc2821

A question and some great findings by Jason S. Clary:
http://www.tech-archive.net/Archive/Exchange/microsoft.public.exchange.development/2006-05/msg00111.html

Files:
MailMsgPropertyBagWrapper.cs.txt (13.23 KB)
Message.cs.txt (38.35 KB)
SmtpInCommandContextWrapper.cs.txt (7.65 KB)
SmtpOutCommandContextWrapper.cs.txt (4.45 KB)
SmtpServerResponseContextWrapper.cs.txt (5.46 KB)
Wrappers.zip (58.14 KB)
 

Wednesday, 13 August 2008 20:26:56 (W. Europe Daylight Time, UTC+02:00)  #    Comments [31]
.Net | Microsoft SMTP
Thursday, 06 November 2008 18:44:06 (W. Europe Standard Time, UTC+01:00)
Hey,
nice work you've go here. The link of the source code is broken.

Meanwhile, I'm trying do the the same and triggering the AUTH command, but it seems that the event only looks for the EHLO or MAIL. What am I doing wrong?
I do something like :

cscript smtpreg.vbs /add 1 OnInboundCommand "SMTP Event Sink" SMTPEventHandler.EventSink AUTH

But all i get is EHLO command or MAIL command. Maybe my error is in using the OnInboundCommand.
I'm overiding the ISmtpInCommandSink.

Thanks in advance.
Hugo.

Hugo
Friday, 07 November 2008 10:06:55 (W. Europe Standard Time, UTC+01:00)
Hi Hugo,

Hmm, weird. Could be one of three things, I guess...

1. Be sure you're connecting to the same virtual server (1 in your case) as where you bind your sink to.
2. In your code, check if you use something like... "context.CommandKeyword.ToUpper().Equals("AUTH")"
3. You have another binding of an AUTH handler bind to your virtual server with a lower (= higher) priority.

You can check all your bindings with the command:
cscript smtpreg.vbs /enum

Also, please know that you can bind the same sink many times to the same virtual server. Thus, you can have your Debug built bound to the VS and also your Release built bound to the same VS.

To make sure you have removed all other bindings, please execute the following line until you get a failure response:
cscript smtpreg.vbs /remove 1 OnInboundCommand "SMTP Event Sink"

Hope that helps

Friday, 07 November 2008 18:31:55 (W. Europe Standard Time, UTC+01:00)
Could you provide the complete source for this sample application?
There seems to be a link in your articel, but that one is broken...

thx

br

Gerhard

Gerhard Urschler
Sunday, 09 November 2008 14:07:57 (W. Europe Standard Time, UTC+01:00)
I removed the dead link to the wrapper's source files...

And instead I've included all source files in text...
and also a zip file containing the source files, the binaries for VS2005 and the smtpreg.vbs file.

Wednesday, 19 November 2008 09:11:27 (W. Europe Standard Time, UTC+01:00)
Martijn,

I have implemented a custom OnSmtpInCommand-Sink where I'm trapping the "RCPT"-verb and I have successfully implemented a greylisting-feature. This runs on the MS Exchange-engine. I'm using the GetStringA-function to retrieve IP-Adress etc. This is fine.

Now I want to enhance the functionality of this spamfiltering tool and I want to block certain type of mail - but I want to block them on the SMTP-level, so that I can reject the mail with an appropiate SMTP-returncode (therefore the OnArrival is not an option).

To do so, I have to access the MailMsg-Object. I wanted to transfer this to a CDO.Message, but I'm already stuck at a much earlier point: in reading the content of the MailMsg-Object. THis function call:

pMsg.ReadContent(dwOffset, dwLength, out dwLengthRead, pBuffer, null);

returns the error-code
Error: System.UnauthorizedAccessException: Zugriff verweigert (Ausnahme von HRESULT: 0x80070005 (E_ACCESSDENIED))
bei Microsoft.Exchange.Transport.EventInterop.MailMsgClass.ReadContent(UInt32 dwOffset, UInt32 dwLength, UInt32& pdwLength, IntPtr pbBlock, IMailMsgNotify pNotify)
bei MailTonne.MailTonneSink.ReadContent(MailMsg pMsg, UInt32 dwOffset, UInt32 dwLength)

which is rather a full stop. Any ideas what can cause this thing? Is it possible that I'm trying this at a time where there is no content yet (I'm trying to read at the RCPT-verb)? Do I have some security-settings which are blocking me (I'm not aware of any, this is my test-machine, freshly installed with Server 2k3 R2, and Mx 6.5.7638.1 + all service packs)?

Any pointer would be appreciated! thx, Gerhard


Gerhard Urschler
Wednesday, 19 November 2008 09:34:37 (W. Europe Standard Time, UTC+01:00)
Hi Gerhard,

If I understand you correct, you want to access the email data within the SMTP protocol phase.
You can do this, but not at that point (RCPT).

Simply said, you're SMTP protocol would look something like this:
S: 220 mymail.local Microsoft ESMTP MAIL Service
C: HELO mydomain.com
S: 250 OK
C: MAIL FROM: <>
S: 250 OK
C: RCPT TO: me@you.com
S: 250 OK
C: DATA
S: 354 End with <CRLF>.<CRLF>
C: [all email data]
C: .
S: 250 OK

Now, where you want to bind your sink, is just after the email was passed by the client and just before the last server response.
You can do this by binding to the _EOD command. :-)
This is not an actual command, but it represents the <CRLF>.<CRLF>.
Your sink gets the event after the . and you'll be able to parse the whole message and respond back to the client with your own code.

Also, when you decline the message with an error code, be sure to consume the event and drop the message! If you don't, the message will still be sent by the underlying SMTP server.



Friday, 21 November 2008 11:54:09 (W. Europe Standard Time, UTC+01:00)
Thanks for your support, binding to the _EOD did the trick!
I can now retrieve the complete message >;-)

br
Gerhard
Gerhard Urschler
Wednesday, 10 December 2008 18:35:34 (W. Europe Standard Time, UTC+01:00)
Martijn,

Just got back to this issue, and I think I'm missing something. I've tried your recommendations, but still got no clue of what's happening. I'll try to explain to you what I'm trying to do.
I'm trying to get the user witch a client authenticates himself, for that I'll like to decode the string after 334 (username_encoded). So I thought of firing a sink each time an smtp message comes with AUTH LOGIN. Then I would compare the user with my active directory and if the user is there I will resume normal activity, if not I would discard the email. For that I'm using:
RegAsm.exe SMTPEventHandler.dll /codebase
and,
cscript smtpreg.vbs /add 1 OnInboundCommand "SMTP Event Sink" SMTPEventHandler.EventSink AUTH.
in my EventSink.dll I've got implemented only the ISmtpInCommandSink.OnSmtpInCommand.

I've also tried to change priority of the sink with,
cscript smtpreg.vbs /setprop 1 OnServerResponse "SMTP Event Sink" Sink priority 1
unfortunately with no sucess.

If I change the AUTH for EHLO it goes into my method, with AUTH, it never goes into it. What I'm doing wrong?
Should I override another method? Do I need to setup anything else?
Please help.
Thanks,
Hugo.

Hugo
Thursday, 11 December 2008 10:43:28 (W. Europe Standard Time, UTC+01:00)
Hi Hugo,

hmm, this is getting weird...

So, to recap:
* the AUTH event is not fired (before the 334)
* there were no old binding that you found using: "cscript smtpreg.vbs /enum"
* you're testing on the right virtual-instance

How did you make sure your method is not called at AUTH? Did you debug the sink?
If not, any first piece of code could have crashed the sink. This could be, for example, by writing a line to a logfile. If for some reason that fails, it would appear as if the sink wasn't called at all.

To debug your sink:
* bind the debug version of the dll to your SMTP server
* do the "iisreset"
* bind VisualStudio to inetinfo.exe (Tools->Attach to Process->) (set to "Managed Code")
* set a breakpoint before the first statement
* send a test-mail through the SMTP server (using telnet or a mail client)

If this again shows that the AUTH event is not fired, you'll probably have to send me your code, so that I can try it myself. :-)


Wednesday, 07 January 2009 00:45:25 (W. Europe Standard Time, UTC+01:00)
Martijn,


Still around this issue. I've changed my approach.
I'm sinking with MAIL, the thins is I seem to be installing the same event sink twice, and then It only works fine for the first time it's used, after that first time, the SMTP session just stays hanged in the line after the MAIL FROM. the first time I can managed to behave has the server and the client is alright with that.

Can you give me a hand? I've send you an email with my registering commands.

Thanks,
Hugo.

Hugo
Thursday, 26 February 2009 19:02:23 (W. Europe Standard Time, UTC+01:00)
Hi,

thanks for this great guide. I posted the same question on "codeproject.com" but as there hasn't been any activity for the last 6 months, I'll try posting the question here as well...

I have a problem registering the event-sink-dll using the regevent.vbs script. I successfully tried to register a .vbs-event-sink to make sure that I am allowed to register the sink, that the path to the folder is correct, etc... I considered using the .vbs-script but the event-sink must be able to handle 64,000 mails per month.

But when I try to register the event-sink-dll I get the following error:

<blockquote cite="">E:\Develop\MyEventSink\bin>regevent add "onsave;ondelete" MyEventSink.ExchEventSink http://exsrv.local/exchange/Administrator/Posteingang/regevent.evt
Microsoft (R) Windows Script Host, Version 5.6
Copyright (C) Microsoft Corporation 1996-2001. Alle Rechte vorbehalten.

New Event Binding created:
Event: onsave;ondelete
Sink: MyEventSink.ExchEventSink
FullBindingUrl: http://exsrv.local/exchange/Administrator/Posteingang/regevent.evt

Error Commiting Transaction : -2141913011 Fehler bei der Ereignisregistrierung: Die angegebene Ereignissenke (progID: %1) kann nicht innerhalb von Prozessen ausgeführt werden.</blockquote>

(The error above means: "Error while registering the event: The given eventsink (progID: %1) cannot be executed from within other processes")

The dll is registered as COM+-application "MyEventSink" and the component "MyEventSink.ExchEventSink" also exists.
Are there different kinds of COM+-dlls of which some cannot be called from within the exchange server or are there some settings within the configuration, that I might've missed?

Thanks for any help.

Regards,
Kristian Scheibe, StratOz GmbH
Friday, 27 February 2009 10:49:27 (W. Europe Standard Time, UTC+01:00)
Hi Kristian,

As far as I know, you can't bind a COM+ dll directly to the IIS Server.
What you could do, is write a wrapper in C++ or C#, which calls the COM+ dll.

Also, make sure you use the correct parameter format for smtpreg.vbs

cscript smtpreg.vbs /add 1 OnInboundCommand "SMTP Event Sink" SMTPEventHandler.EventSink AUTH.

where:
/add means add a binding (use /remove to remove a binding)
1 refers to the virtual smtp server instance (usually 1 if you have only one smtp instance)
OnInboundCommand means that the event bound to (and your sink's interface) is of the type OnInboundCommand
"SMTP Event Sink" is the bind name. Be sure to have a unique name for each separate bind in order to avoid problems when removing binds or setting properties (you need this name for that).
SMTPEventHandler.EventSink refers to your registered COM (not COM+) dll
AUTH is the filter on the OnInboundCommand commands. (you can also specify filters like: MAIL=*.domain.com)



Wednesday, 04 March 2009 17:42:04 (W. Europe Standard Time, UTC+01:00)
Hello Martijn,

thanks for your help!

I switched from C# to VB6 to create a true COM-dll and I was able to catch synchonous and asynchronous OnSave and OnDelete events. But I had no success registering a transport sink event using the smtpreg.vbs script. (The registration is successful, but the event doesn't fire when I access the exchange-server via SMTP.

Also I found out, that this error I encountered was caused by some settings in the properties of the COM+-Application:
In the "Activation"-tab the type of activation has to be set to server-application.
And in the "Security"-tab the "enforce access-checking for this application"-checkbox should be unchecked.
(I have only a german server, so the labels might be a little different.)

Regards,
Kristian Scheibe, StratOz GmbH
Thursday, 05 March 2009 09:34:04 (W. Europe Standard Time, UTC+01:00)
Hi Kristian,

I'm not quite sure what you mean. What kind of sink are you building if you have an OnSave and OnDelete event?
Are you building an Store sink?

Wednesday, 22 July 2009 22:21:53 (W. Europe Daylight Time, UTC+02:00)
This article must be one of a kind on the net. How did you find THE way to properly use SMTP event sinks ? The documentation is soooo scarce.

Million thanks!
kellogs
Saturday, 25 July 2009 12:19:57 (W. Europe Daylight Time, UTC+02:00)
Only one question -

Martijn is treating his sink as if it was called for two separate rules: AUTH and RCPT. Is this really possible ?? even for an OnInboundCommand filter ???

I am struggling to accomplish the same for MAIL and RCPTO, separated by semicolons, comma, fullstop, slash, backslash or whatever. No joy.

Anyone care to comment on how might Martijn had accomplished this ?

Thanks
kellogs
Sunday, 26 July 2009 16:06:24 (W. Europe Daylight Time, UTC+02:00)
Hi Kellogs,

Let me try and answer that myself... ;-)

I don't combine two events in one bind. Instead, I do two separate binds on two different events to the same dll.
So, in this case, I have a binding rule on RCPT to the compiled dll, and another binding rule on AUTH to the same dll.

You could also split the code into two separate dlls.

Please know that, although it's compiled into one dll, the two events call two different instances of the dll. You cannot use static variables (or other such tricks) to pass information from one event to the other. Instead, use the session propertybag, as I do in the code above.



Saturday, 01 August 2009 14:38:53 (W. Europe Daylight Time, UTC+02:00)
Hey, thanks - yes that was what I was talking about. On my XP SP2 machine I am not able to bind both rules MAIL and RCPT to the same dll :). But that is not an issue anymore; it turned out I do not have to use such feature.

However, the properties from IMailMsgProperties do not work on just any machine, regardless of setting or reading them. Those darn IMMPID_MP_... properties first did not work on my machine, then, out of a sudden, have started working. I do not know what was it that I have done to enable them :((.

On my client's Win2003 server they do work, partially (not all of them). More than that, on my client's 2003 server even the IMailMsgProperties::QueryCommand() fails. I dont think this behaviour has anything to do with some of the properties being obsolette as they work ok on my comp. Or, if these API I am playing with are indeed obsolette, how can I get the 'rcpt to' list of recipients ?

//hr = pMsg->GetStringA(IMMPID_MP_RFC822_TO_ADDRESS, BUFSIZE, szReceivers);
//hr = pMsg->GetStringA(IMMPID_MP_RECIPIENT_LIST, BUFSIZE, szReceivers);
hr = pContext->QueryCommand(szReceivers, &dwSize); //this is a RCPT sink

These calls all fail. Last line fails with some -2147.... code (debugging on client's machines is really really hard). Any tip on this ?

Thank you, and keep us posted
kellogs
kellogs
Monday, 03 August 2009 10:57:32 (W. Europe Daylight Time, UTC+02:00)
"Hey, thanks - yes that was what I was talking about. On my XP SP2 machine I am not able to bind both rules MAIL and RCPT to the same dll :). But that is not an issue anymore; it turned out I do not have to use such feature."

"I don't combine two events in one bind. Instead, I do two separate binds on two different events to the same dll.
So, in this case, I have a binding rule on RCPT to the compiled dll, and another binding rule on AUTH to the same dll."

Okay, I am a bit slow. Now I see what you meant. It never struck my mind ^^. Thank you!

What about my other questions ? Hints ?

Best regards,
kellogs
kellogs
Monday, 03 August 2009 15:55:54 (W. Europe Daylight Time, UTC+02:00)
Hi Kellogs,

ther questions? Ah, you want to get a list of recipients.
Do you use the Message wrapperclass? Or do you use the IMailMessageProperties?

Please use the Message wrapper. It publishes a property "Recips". This provides you with the list of recipients.
Please take a look at the sourcecode of the Message wrapper: http://blog.rednael.com/content/binary/Message.cs.txt

Monday, 03 August 2009 20:30:06 (W. Europe Daylight Time, UTC+02:00)
Thnks again,

unfortunately, that wrapper class is C#, whereas my project is C++.
I have used both IMailMsgProperties and ISmtpInCommandContext interfaces with success on WinXP SP2 and without success on Win2003 x64 SP?

Regards
kellogs
Monday, 03 August 2009 21:41:42 (W. Europe Daylight Time, UTC+02:00)
But still...

If you take a look at the sourcecode of the Message wrapper-class, you will find how to get the recipient list from the IMailMsgProperties. It's coded in C# but very readable. Just follow the code...

Monday, 03 August 2009 22:44:54 (W. Europe Daylight Time, UTC+02:00)
Yes Martijn, I have noticed you are accessing the IMMPID_MP_RFC822_TO_ADDRESS proeprty for that. That is also what I am doing from C++. Latest finds revealed that the darn property works on both Win2003 and WinXP, bujt only on debug DLL. In the release version, bang , the COM call fails !

Cheers
kellogs
Monday, 03 August 2009 23:02:01 (W. Europe Daylight Time, UTC+02:00)
Ehhhh... As a matter of fact, the ONLY way to get that recipient list is using the DEBUG build of the dll which calls:

hr = pContext->QueryCommand(szReceivers, &dwSize); //this does not work in release builds

as for the debug AND release builds of the sink that call either:

hr = pMsg->GetStringA(IMMPID_MP_RECIPIENT_LIST, BUFSIZE, szReceivers);

OR

hr = pMsg->GetStringA(IMMPID_MP_RFC822_TO_ADDRESS, BUFSIZE, szReceivers);

well, they just do not effin work!!
kellogs
Monday, 03 August 2009 23:27:00 (W. Europe Daylight Time, UTC+02:00)
Dam it...trial and error on M$ APIs.. it is the first time I had to do this on Microsoft's APIs. For it to build in release mode, add
hr = pContext->QueryCommandSize(&dwSize);
before
hr = pContext->QueryCommand(szReceivers, &dwSize);

God bless,
kellogs
Saturday, 19 September 2009 07:17:43 (W. Europe Daylight Time, UTC+02:00)
How are you setting the RCPT sink via smtpreg.vbs?
I'm calling:
cscript smtpreg.vbs /add 1 OnInboundCommand "SMTP Event Sink" SMTPEventHandler.EventSink AUTH
cscript smtpreg.vbs /add 1 OnInboundCommand "SMTP Event Sink2" SMTPEventHandler.EventSink RCPT

The AUTH event fires and i get passed the user/pass checking but the RCPT never hits...

Also, is there a link to the final source code? I'm not sure where exactly you are putting the following code:

//set custom session property
MailMsgPropertyBag session = new MailMsgPropertyBag(_session);
session.PutBool(SESSION_PROPIDX_AUTHSUCCESS, true);

//get custom session property
bool ses_authenticated = false;
try{ ses_authenticated = session.GetBool(SESSION_PROPIDX_AUTHSUCCESS); }catch (Exception excpt){;}


Thanks for your assistance, this is a great article.
Monday, 21 September 2009 15:14:46 (W. Europe Daylight Time, UTC+02:00)
Hi Skeeboe,

Please take a look at the comments above. There was a discussion between me and Hugo. It might be one of several problems why your bind is invalid.

The reason why I use the session propertybag to store information, is because I use two separate bindings. To communicate between the RCPT bind and the AUTH bind we need to use the session. It's not possible to use a static variable or so...

Monday, 15 February 2010 01:11:24 (W. Europe Standard Time, UTC+01:00)
Few things...

1. Each binding creates a new instance of the dll, correct?

2. If so, if I bind on EHLO, MAIL, and RCPT IIS would create three instance of my dll?

3. If this is the case, and I want to load some data from a text file like a list of emails to check against, what would you recommend I use? I only want to load the data once when the first dll is created. I'm thinking a singleton will do the trick, but what are your thoughts?

Thanks.
Monday, 15 February 2010 13:19:26 (W. Europe Standard Time, UTC+01:00)
Hi! I'm working on SMTP event sink. I need to split the message with multiple recipients into individual messages (one message per recipient). I use IMailMsgProperties.ForkForRecipients() function, but new (forked) message stuck somewere in queue and delivered only after IIS restart. Original message is delivered without any problems.
I hope you will help.

Sample code:
void IMailTransportSubmission.OnMessageSubmission(
MailMsg pMsg,
IMailTransportNotify pNotify,
IntPtr pNotifyContext
)
{
...
Message newMsg = null;
RecipsAdd rcptAdd = null;

msg.ForkForRecipients(out newMsg, out rcptAdd);
foreach (string rcpt in forkedRecipients)
{
rcptAdd.AddSMTPRecipient(rcpt);
}
newMsg.WriteList(rcptAdd);
newMsg.RebindAfterFork(msg, null);
..
}
Alexander
Monday, 15 February 2010 15:34:52 (W. Europe Standard Time, UTC+01:00)
Hi Deciacco,

We've discovered some new findings on this subject.

If you bind one class of one dll to multiple SMTP commands and you have IsCacheable returning S_OK (default implementation), those multiple binds share the same instance of your class.
That means that you are able to use static members in your class for creating a cache.

What you could also do, is use the SessionPropertybag to store session wide cache information, or even use the ServerPropertybag to store server wide cache information.

Monday, 15 February 2010 15:46:54 (W. Europe Standard Time, UTC+01:00)
Hi Alexander,

Ah, the fork message issue...

Yes, I've tried this myself also and went into the extend of creating a service request at Microsoft to sort things out. They went looking into the source-code for me and came up with somewhat of the following answer.
You can't use ForkForRecipients in a Protocol Sink. You can only use it in a Transport Sink, but not at any bind, but only at PreCategorize.

I see you use the OnMessageSubmission. Now, my mind draws blank (after all this time of writing protocol sinks) on the timing of the OnMessageSubmission, but I think its situated before the PreCategorize. In that case it probably wouldn't be to hard to rewrite your sink to a PreCategorize sink...

It's been a while that I worked with the ForkForRecipients method, so I hope my answer is correct. I still have a nagging feeling that it might be only available in the Categorizer itself. In that case you would have to rewrite your current sink completely.

In any case, the Categorizer is the one who actually does the (re)bind of the forked message to the storedriver.
All comments require the approval of the site owner before being displayed.
Name
E-mail
Home page

Comment (HTML not allowed)  

Enter the code shown (prevents robots):

Live Comment Preview
About the author/Disclaimer

Disclaimer
The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

© Copyright 2017
Martijn Thie
Sign In
Statistics
Total Posts: 18
This Year: 0
This Month: 0
This Week: 0
Comments: 176
All Content © 2017, Martijn Thie
DasBlog theme adapted from 'Business' (originally by delarou)