One of the Event sinks that comes in handy from time to time especially if people own a lot of domains is the catch all event sink from http://support.microsoft.com/?kbid=324021 .Now the .NET frameworks a bit more prevalent and the fact I wanted to use this on my Internal servers for something I thought I’d give converting this sink to C# ago. There are two ways you can go about writing SMTP event sinks in managed code the first is to build some wrappers as outlined in on msdn. This gives you access to all the protocol and transport events. The second way to build a SMTP event sink in managed code is to use the CDO onarrival event whose interfaces are defined in the cdoex.dll file (or cdosys.dll if you don’t have Exchange). The downside of using the CDO interface is that it adds significant overhead and is synchronous but the upside is that it that it handles most of the parsing and MIME issues. There’s a good doc here that discusses the issues . But using C# is a step up from VBS and should avoid all those nasty STA issues discussed here.
For the code itself its mostly based on the code from the KB with one major exception. I’ve added a section that checks and sets an X-header on the message if it’s processed by the sink. I did this to make sure the sink wouldn’t run multiple times on a message which was in response to an issue that I had with this sink (as well as the original script) in my environment. What happened for me was that I was a little lazy when I registered the sink and instead of just registering it to run on messages being sent to my catch all domain I registered it to run on all messages that where being processed by the server. This was fine for all normal message traffic that flowed though the server but a problem arose when I had some messages that where bound for a mail enabled public folders. I have a front-backend setup and I have a public store mounted on my front end server which contains the folder hierarchy. So when my front end server received the message bound from a mail-enabled public folder it would deliver it locally first as per its logic and then it would resubmit it once it work out where a replica for that folder existed ref . When the event sink ran on this resubmitted message even though it wasn’t making any changes to it the code still goes though the process of writing the recipient list back to the envelope field and calls datasource.save to update the message. Something was happening within this process which would then cause a message loop on my front-backend servers which would just continually bounce the message between each of the servers until I removed the sink. This may mean I have a problem somewhere else and it wouldn’t have happened if I had bound the sink correctly in the first place but it was enough to prompt me to change this sink to prevent this type of thing happening In the future. The one draw back of adding an X-header was that it invalidates any digital signatures but for a catch domain this isn’t a big deal.
Down to the coding
The first thing to do is create a new classlibrary project in visual studio
To create the sink you need to grab 3 dll’s from your server the first is the codex.dll you also should grab the seo.dll from %windir%\system32\inetsrv and the last dll you need is the PIA for ado. I used the PIA from OWA on Exchange 2003 which is the Adodb.dll file in the Exchsrvr\OMA\browse\bin directory. This is one thing you need to be careful of as there are a few PIA’s kicking around which are different versions. Before you use it you may want to list to see if any are registered in the GAC by using gacutil –l. Usually there isn’t but I had a problem on one server where I had a version of ADODB registered in the GAC which was a different version then the PIA I was trying to use which caused a muck of problems.
Once you have all the DLL’s you need to create strong named assemblies for codex and seo so you need to first create a keypair with sn.exe eg sn.exe –k :SMTPOnarrival.key
Then using Tlbimp.exe build some Interop dll’s I’ve used the namespace switch to make sure CDOEX gets assigned CDO for the namespace eg
tlbimp cdoex.dll /namespace:CDO /keyfile:SMTPOnarrival.key /out:Interop.cdoex.dll
tlbimp seo.dll /namespace:SEO /keyfile:SMTPOnarrival.key /out:Interop.seo.dll
Once you’ve done this you can then reference all three dll’s in your project and you also need to add the keypair name to AssemblyKeyFile property in the assembly info.
The other thing you need to do is in the project properties-configuration properties you need to make sure that Regsiter for Com interop is set to true
Add the code then make sure you add a new unique GUID using Tools – Create GUID (create registry format). You need to change the catch domain and replace mailbox in the code which are hard coded as well you should set a unique x-header for the server.
Once you’ve done this you need to register your dll using regasm with the /codebase switch eg regasm onarrivalesink.dll /codebase
And then finally bind your sink using SMTPreg.vbs (which comes with the Exchange SDK there is also a copy in http://support.microsoft.com/?kbid=324021). When your binding it I would make sure you bind it so it only fires on emails sent to your catch domain recipients so a registration like
cscript smtpreg.vbs /add 1 onarrival CatchallSink SMTPonarrival.Catchall "rcpt to=*@youdomain.com"
If you want to debug your code (which you should only be doing on a dev server) because SMTP event sinks run in-process (of IIS) within Visual Studio to debug you need to select tools – debug process and then attach to the inetinfo.exe process (for CLR). The only quirk that I found was that Inetinfo needs to have successfully loaded your code to allow you to connect the debugger (eg the sink needs to have fired once first) or you just can’t connect. The other small quirk that I haven’t worked out yet is that I had to keep restarting the IISadmin service (and dependants) to make it release the DLL so I could make changes.
The code I’ve used is very low on testing so I wouldn’t trust it in anything other then a test environment.
I’ve put a downloadable copy of the code here
The code itself looks like
using System;
using System.Runtime.InteropServices;
using CDO;
using ADODB;
using SEO;
namespace SMTPonarrival
{
[Guid("E045FD54-4E2D-4a8e-8431-FF351F98B14A")]
public class Catchall : ISMTPOnArrival , IEventIsCacheable
{
void ISMTPOnArrival.OnArrival(IMessage msg, ref CdoEventStatus EventStatus)
{
try
{
if (msg.Fields["urn:schemas:mailheader:X-catchall"].Value == null)
{
ProcessMessage(msg);
};
}
catch(Exception e)
{
System.IO.StreamWriter logfile = new System.IO.StreamWriter("c:\\SMTPEventerrorlog.txt",true);
logfile.WriteLine("Sink Fired : " + System.DateTime.Now);
logfile.WriteLine("Error : " + e.Message);
logfile.Close();
}
//Set Event Status to CDO_RUN_NEXT_SINK
EventStatus = CDO.CdoEventStatus.cdoRunNextSink;
}
void IEventIsCacheable.IsCacheable()
{
// This will return S_OK by default.
}
private void ProcessMessage(IMessage msg1)
{
string RECIPLIST;
RECIPLIST = "http://schemas.microsoft.com/cdo/smtpenvelope/recipientlist";
string strFixedListlc;
string searchdomain = "@catchdomain.com";
string strreplaceaddr = "SMTP:catchmailbox@yourdomain.com;";
string strFixedList = msg1.EnvelopeFields[RECIPLIST].Value.ToString();
while (strFixedList.IndexOf(searchdomain ,1) != -1 )
{
strFixedListlc = strFixedList.ToLower();
int nDomainPart = strFixedListlc.IndexOf(searchdomain,1);
int nNamePart = strFixedList.LastIndexOf(";",nDomainPart);
int nNextAddress = strFixedList.IndexOf("SMTP:",nDomainPart);
if (nNamePart == -1)
{
if (nNextAddress == -1)
{
strFixedList = strreplaceaddr;}
else
{
strFixedList = strreplaceaddr + strFixedList.Remove(0,nNextAddress);}
}
else
{
if (nNextAddress == -1)
{
strFixedList = strFixedList.Remove(nNamePart,strFixedList.Length-nNamePart) + ";" + strreplaceaddr;}
else
{
strFixedList = strFixedList.Remove(nNamePart,strFixedList.Length-nNamePart) + ";" + strreplaceaddr + strFixedList.Remove(0,nNextAddress);
}
}
}
msg1.EnvelopeFields[RECIPLIST].Value = strFixedList;
msg1.EnvelopeFields.Update();
msg1.Fields["urn:schemas:mailheader:X-catchall"].Value = "Server-CatchALL";
msg1.Fields.Update();
msg1.DataSource.Save();
}
}
}
For the code itself its mostly based on the code from the KB with one major exception. I’ve added a section that checks and sets an X-header on the message if it’s processed by the sink. I did this to make sure the sink wouldn’t run multiple times on a message which was in response to an issue that I had with this sink (as well as the original script) in my environment. What happened for me was that I was a little lazy when I registered the sink and instead of just registering it to run on messages being sent to my catch all domain I registered it to run on all messages that where being processed by the server. This was fine for all normal message traffic that flowed though the server but a problem arose when I had some messages that where bound for a mail enabled public folders. I have a front-backend setup and I have a public store mounted on my front end server which contains the folder hierarchy. So when my front end server received the message bound from a mail-enabled public folder it would deliver it locally first as per its logic and then it would resubmit it once it work out where a replica for that folder existed ref . When the event sink ran on this resubmitted message even though it wasn’t making any changes to it the code still goes though the process of writing the recipient list back to the envelope field and calls datasource.save to update the message. Something was happening within this process which would then cause a message loop on my front-backend servers which would just continually bounce the message between each of the servers until I removed the sink. This may mean I have a problem somewhere else and it wouldn’t have happened if I had bound the sink correctly in the first place but it was enough to prompt me to change this sink to prevent this type of thing happening In the future. The one draw back of adding an X-header was that it invalidates any digital signatures but for a catch domain this isn’t a big deal.
Down to the coding
The first thing to do is create a new classlibrary project in visual studio
To create the sink you need to grab 3 dll’s from your server the first is the codex.dll you also should grab the seo.dll from %windir%\system32\inetsrv and the last dll you need is the PIA for ado. I used the PIA from OWA on Exchange 2003 which is the Adodb.dll file in the Exchsrvr\OMA\browse\bin directory. This is one thing you need to be careful of as there are a few PIA’s kicking around which are different versions. Before you use it you may want to list to see if any are registered in the GAC by using gacutil –l. Usually there isn’t but I had a problem on one server where I had a version of ADODB registered in the GAC which was a different version then the PIA I was trying to use which caused a muck of problems.
Once you have all the DLL’s you need to create strong named assemblies for codex and seo so you need to first create a keypair with sn.exe eg sn.exe –k :SMTPOnarrival.key
Then using Tlbimp.exe build some Interop dll’s I’ve used the namespace switch to make sure CDOEX gets assigned CDO for the namespace eg
tlbimp cdoex.dll /namespace:CDO /keyfile:SMTPOnarrival.key /out:Interop.cdoex.dll
tlbimp seo.dll /namespace:SEO /keyfile:SMTPOnarrival.key /out:Interop.seo.dll
Once you’ve done this you can then reference all three dll’s in your project and you also need to add the keypair name to AssemblyKeyFile property in the assembly info.
The other thing you need to do is in the project properties-configuration properties you need to make sure that Regsiter for Com interop is set to true
Add the code then make sure you add a new unique GUID using Tools – Create GUID (create registry format). You need to change the catch domain and replace mailbox in the code which are hard coded as well you should set a unique x-header for the server.
Once you’ve done this you need to register your dll using regasm with the /codebase switch eg regasm onarrivalesink.dll /codebase
And then finally bind your sink using SMTPreg.vbs (which comes with the Exchange SDK there is also a copy in http://support.microsoft.com/?kbid=324021). When your binding it I would make sure you bind it so it only fires on emails sent to your catch domain recipients so a registration like
cscript smtpreg.vbs /add 1 onarrival CatchallSink SMTPonarrival.Catchall "rcpt to=*@youdomain.com"
If you want to debug your code (which you should only be doing on a dev server) because SMTP event sinks run in-process (of IIS) within Visual Studio to debug you need to select tools – debug process and then attach to the inetinfo.exe process (for CLR). The only quirk that I found was that Inetinfo needs to have successfully loaded your code to allow you to connect the debugger (eg the sink needs to have fired once first) or you just can’t connect. The other small quirk that I haven’t worked out yet is that I had to keep restarting the IISadmin service (and dependants) to make it release the DLL so I could make changes.
The code I’ve used is very low on testing so I wouldn’t trust it in anything other then a test environment.
I’ve put a downloadable copy of the code here
The code itself looks like
using System;
using System.Runtime.InteropServices;
using CDO;
using ADODB;
using SEO;
namespace SMTPonarrival
{
[Guid("E045FD54-4E2D-4a8e-8431-FF351F98B14A")]
public class Catchall : ISMTPOnArrival , IEventIsCacheable
{
void ISMTPOnArrival.OnArrival(IMessage msg, ref CdoEventStatus EventStatus)
{
try
{
if (msg.Fields["urn:schemas:mailheader:X-catchall"].Value == null)
{
ProcessMessage(msg);
};
}
catch(Exception e)
{
System.IO.StreamWriter logfile = new System.IO.StreamWriter("c:\\SMTPEventerrorlog.txt",true);
logfile.WriteLine("Sink Fired : " + System.DateTime.Now);
logfile.WriteLine("Error : " + e.Message);
logfile.Close();
}
//Set Event Status to CDO_RUN_NEXT_SINK
EventStatus = CDO.CdoEventStatus.cdoRunNextSink;
}
void IEventIsCacheable.IsCacheable()
{
// This will return S_OK by default.
}
private void ProcessMessage(IMessage msg1)
{
string RECIPLIST;
RECIPLIST = "http://schemas.microsoft.com/cdo/smtpenvelope/recipientlist";
string strFixedListlc;
string searchdomain = "@catchdomain.com";
string strreplaceaddr = "SMTP:catchmailbox@yourdomain.com;";
string strFixedList = msg1.EnvelopeFields[RECIPLIST].Value.ToString();
while (strFixedList.IndexOf(searchdomain ,1) != -1 )
{
strFixedListlc = strFixedList.ToLower();
int nDomainPart = strFixedListlc.IndexOf(searchdomain,1);
int nNamePart = strFixedList.LastIndexOf(";",nDomainPart);
int nNextAddress = strFixedList.IndexOf("SMTP:",nDomainPart);
if (nNamePart == -1)
{
if (nNextAddress == -1)
{
strFixedList = strreplaceaddr;}
else
{
strFixedList = strreplaceaddr + strFixedList.Remove(0,nNextAddress);}
}
else
{
if (nNextAddress == -1)
{
strFixedList = strFixedList.Remove(nNamePart,strFixedList.Length-nNamePart) + ";" + strreplaceaddr;}
else
{
strFixedList = strFixedList.Remove(nNamePart,strFixedList.Length-nNamePart) + ";" + strreplaceaddr + strFixedList.Remove(0,nNextAddress);
}
}
}
msg1.EnvelopeFields[RECIPLIST].Value = strFixedList;
msg1.EnvelopeFields.Update();
msg1.Fields["urn:schemas:mailheader:X-catchall"].Value = "Server-CatchALL";
msg1.Fields.Update();
msg1.DataSource.Save();
}
}
}