OK, so I've been having some fun with IIS's SMTP server these last couple of days. I've got these event sinks which I've written in C# so that I can do custom processing when email arrives for specific users (it also provides some simple spam filtering and stuff as well). Anyway, one of the requirements is that we have multiple SMTP server instances per machine, and I want them to be able to process things differently (because they're different domains).
Now, the SMTP server has two main kinds of event sinks: Command sinks and Transport sinks. Command sinks get called when a connected client issues an SMTP command (e.g. 'HELO', 'MAIL FROM', 'RCPT TO', etc). Transport sinks get called when a message is about to be delivered. Now, in the command sinks we do some simple checking that the message is for users we know about and stuff, but nothing really interesting. The interesting stuff happens in the transport sink when we take the message and actually perform our custom processing on it.
In the command sinks, you actually get passed a reference to a "context" object which gives you certain parameters about the server and you can in fact get the current server instance number from there. However, the transport sinks get a MailMsg object, and that's it. But I want to know the server instance from the transport sink, so what I'd been doing in the past was simply setting a property on the message in the command sinks, then referencing that in the transport sink.
Now, this worked OK when all your messages go through the command sinks to begin with, but not all messages do that. For example, IIS will generate delivery status notifications if it can't connect to a remote server. These DSNs will, obviously, completely bypass the command sinks because they're generated internally (and hence no commands are issued!). But the bigger problem is messages that get copied directly to the Pickup folder. These don't go through the command sinks either, so we can't work out the server instance there, either. But the Pickup folder is very useful, because it's heaps quicker to write messages there than it is to connect to the server via TCP/IP.
Now, there may be other ways a message can arrive at a transport sink, but so far I have only encountered those three. So I needed a way to get the server instance from within a transport sink, no matter how the message arrived at the sink. The IMailMsgProperies interface that is implemented by the MailMsg class has a GetProperty method which takes an integer property "ID" and returns whatever the property is. Now, the actually properties are not documented at all (that I could find) so I had to do a bit of trial and error to work out what properties are actually set on the messages. Basically, I just put a loop from 1 to 20,000 in my transport sink trying to see the value of the property with that ID.
Unfortunately, messages coming in via the three methods I mentioned all had a different set of properties. First, messages that came in via the command sink didn't have anything obviously useful to me, but remember that I explicitly put the server instance number in property #1, so I could just use that. But what I did find was that messages that came in via the Pickup folder had the default domain of the server in property #4106. The DSN messages had the RFC822 Message-ID header value in property #4128 (The Message-ID has the default domain after the @-sign).
So, I came up with the following code to work out the instance number. Notice that when I get the default domain from method #2 & #3, I have to go through the IIS metabase to get the instance number from that.
int GetServerInstance(MailMsg msg)
{
// This is case #1 - the message came in via a an inbound command sink
string propValue = GetStringA(1, msg);
if (propValue != null && propValue != string.Empty)
{
return int.Parse(propValue);
}
// This is case #2 - the message came in
// via the Pickup directory
string domain = GetStringA(4106, msg);
if (domain == null || domain == string.Empty)
{
// This is case #3 - the message came in
// via an IIS-generated DSN.
propValue = GetStringA(4128, msg);
int atSign = propValue.LastIndexOf('@');
domain = propValue.Substring(atSign);
}
// For both case #2 and #3, we need to get the
// instance by looking up the domain we found
// in the IIS metabase and working backwards.
propValue = null;
using (DirectoryEntry deSvcs =
new DirectoryEntry("IIS://localhost/SmtpSvc"))
{
foreach (DirectoryEntry de in deSvcs.Children)
{
string thisDomain = de.Properties["DefaultDomain"].Value.ToString();
if (de.SchemaClassName == "IIsSmtpServer" && thisDomain == domain)
{
propValue = de.Name;
break;
}
}
}
return int.Parse(propValue);
}
Just a note, there's no error checking here and the GetStringA method is a wrapper around the GetProperty method, which I pinched by looking at the Reflector code for the C# wrapper objects :)
So, with the code above, I am able to work out which server instance the message is from. The really good thing is that IIS creates exactly one transport sink class per server instance, and keeps that object around for ever, so I can just cache the value and this code only runs once.