Friday, May 13, 2005

C# Catchall Onarrival Event sink

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();
}
}
}

27 comments:

Roberto said...

I'm trying to test your code. The problem I have is that the sink is never called.

I used the command:

cscript smtpreg.vbs /enum

to be sure that the sink was registered.

It looks like it is.
I also restarted IISAdmin.

I'm trying to activate the sink by just sending emails by using microsoft outlook.

Could you help me?

Anonymous said...

hi... i compile this code and got these errors.. :(

C:\CDOUtilities\C# SOLUCION\SMTPC\SMTPC\Class1.cs(11): 'ISMTPOnArrival.OnArrival' in explicit interface declaration is not a member of interface

C:\CDOUtilities\C# SOLUCION\SMTPC\SMTPC\Class1.cs(9): 'SMTPonarrival.Catchall' does not implement interface member 'CDO.ISMTPOnArrival.OnArrival(CDO.Message, ref CDO.CdoEventStatus)'

Jose Molina said...

Excuse me i was last post...i forgot to say i am using cdosys.dll instead cdoex.dll maybe its the problem but as i read shouldnt make errors...

Thanks in advance.

Jose Molina said...

Excuse again :P.. but i had the same problem that Roberto...even after to change the ..OnArrival(Message msg...

When i attach the project to debug option...and when i send a mail an exception is thrown:

First-chance exception at 0x7c59bbf3 in inetinfo.exe: Microsoft C++ exception: long @ 0x00f8f96c.

and the sink doesnt work!! :(

Thanks

Glen said...

With the "does not implement interface member" This sounds like you have an issue with the assembly your trying to use. CDOSYS should be okay but you may have to make other changes for this to work if you use the object browser in Visual studio you should be able to work out the changes you need to make.

Anonymous said...

Hello,

i tried your code, and my first tests were succesful(just writing to log). After that i expanded the code an my IIS did not what expected. I changed the code back to the 1. Version but now i have the problem that nothing happens. The Code now is realy simple, is just want to write the date into the logfile. It is like the sink now is never called. I did complete unregister with regasm/smtpreg.vbs and reregister it, but no change. Is there something i forgot at registering?
Another question please, i work without visual-studio. How i get a correct GUID?

Thanks
Andy

Anonymous said...

Hello,

sorry, me again. Im really sorry, but if i dont close the files no data were written. Sink is working...

Andy

Glen said...

To gernerate a GUID using guidgen.exe which you can download from http://www.microsoft.com/downloads/details.aspx?FamilyID=94551F58-484F-4A8C-BB39-ADB270833AFC&displaylang=en Select Registry format

You need to be carefull when you are making updates to the dll that the files are usually locked by the IIS process and requires that you stop the exchange services to release them and make updates

Anonymous said...

Hello Glen,

thanks for your fast response. There are a lot of questions i have, but have no fear, i will try it on my own.
No i have to rewrite the sink so it ist not searching for general catchdomain, i try to seperate existing adresses from the not existing and than redirect the mail to a catch-all-account.

Thanks again
Andy

Anonymous said...

Your zip file isn't downloadable anymore.

Glen said...

The broken link has been fixed

Tadas said...

Hello,

Maybe someone found solution to solve the problem with first-chance exception mentioned above.I had similar problem. after messeage arrives an exception is thrown First-chance exception at 0x77e55dea in inetinfo.exe: Microsoft C++ exception: long at memory location 0x0903f6bc.. and sink doesnt work

sam said...

I built your sample (I changed ProcessMessage(msg); to writing into a log file just to check if it works )and registered the dll then the event as Roberto did. I made sure it was registered.

I sent an email through outlook but the log file did not get created.
Is there anything else I need to do?

Glen said...

I would try using Outlook Express to send a mail though it via SMTP as proper test. It might be you have issue with your assemblies aren't register properly. Try putting in more diagnostic code in also if your testing it on a development machine try running the code in a debugger.

sam said...

Hi Glen,

Thanks for your response.

I have written a simple console program to send emails using System.Web.Mail.MailMessage and it seems to work fine, but I it still does not work
When I use outlook.

Glen said...

This can happen if you trying to catch outbound message sent via Mapi on the same server as the sink is installed on. It best to use this type of sink on a relay server or otherwise you can remove the Rule from the registration of the sink. eg

cscript smtpreg.vbs /add 1 onarrival CatchallSink SMTPonarrival.Catchall

Be aware that you should not attempt to change a message that has arrived via MAPI in this way see http://support.microsoft.com/default.aspx?scid=kb;en-us;273233

Anonymous said...

Hi,

Sorry I am new to this.
OnArrival/OnSubmission is used for the inbound messages.
Which events are for the outbound messages?
Can we create an event sink for outgoing messages?

thanks

Glen said...

That not exactly true to say the onarrival/onsubmission is for inbound message. It depends what rules you bind it to . I would suggest you have a read of http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnsmtps/html/transportevents.asp

sam said...

Hi Glen,

I still have the same problem with microsoft outlook (message does not get caught whether the registration is done for inbound or outbound) but it does work using System.Web.Mail.SmtpMail.

Our server has windows 2003 and exchange version 6.5.7638.1 SMTP services are installed.

I would appreciate it if you could help.

Many Thanks

sam said...

I could not find an smtpreg.vbs that accepts a registration without the rule part.
Can we do a registration without any rules?

Thanks

Ashish Srivastava said...

I apologize if I am putting a stupid question. I want to scan all incoming mails using Managed Event sink. I am implementing IMailTransportSubmission Interface.
void IMailTransportSubmission.OnMessageSubmission(
MailMsg message,
IMailTransportNotify notify,
IntPtr context)
{
try
{
// Fill details of the Message in database
FillData(message);

}
catch (Exception ex)
{
EventLog.WriteEntry(
Assembly.GetExecutingAssembly().FullName,
ex.Message + "\n" + ex.StackTrace.ToString(),
EventLogEntryType.Error);
}
finally
{
if (null != message)
Marshal.ReleaseComObject(message);

}
}

I am using following command to bind the event sink:

cscript smtpreg.vbs /add 1 OnTransportSubmission "MyEventSink.ManagedSink" MyEventSink.ManagedSink.Sink "MAIL FROM=*" 28001

I have a development exchange server. When I put a message in “pickup” directory, a entry for this message is inserted into database. That means my event sink working fine, but I am not sure how to test it for incoming message as this development exchange doesn’t have dns entries and I have to check event sink functionality for incoming mail from same development server somehow. First thing I want to know that whether I have implemented correct interface for scanning incoming mails. Two, whether I have registered correct event for incoming mails. Any response will be very helpful.

Anonymous said...

Hello,

About the 2 compiling errors that some of you wre having:

The solution is to declare the class as follows: void ISMTPOnArrival.OnArrival(CDO.Message msg, ref CdoEventStatus EventStatus)

*Notice that I changed CDO.IMessage for CDO.Message, becuase this is the correct definition of the class, therefore the compiler wasn't able to find the correct impementation of ISMTPOnArrival.OnArrival :)

C:\CDOUtilities\C# SOLUCION\SMTPC\SMTPC\Class1.cs(11): 'ISMTPOnArrival.OnArrival' in explicit interface declaration is not a member of interface

C:\CDOUtilities\C# SOLUCION\SMTPC\SMTPC\Class1.cs(9): 'SMTPonarrival.Catchall' does not implement interface member 'CDO.ISMTPOnArrival.OnArrival(CDO.Message, ref CDO.CdoEventStatus)'

John said...

I would like to add a header of VBR to each message i.e. a digital signature to say its from our mailserver

and use your type of method on the OnSubmission event
I am not exactly sure this is the best method but so far it look's like it for our exchange server
what is your opinion ?

how would you do it given the chance ?

your thoughts would be really appreciated !

regards

John Jones

Glen said...

Cryptography is pretty advanced to be honest i would go out buy a third party piece of software to do this. Basically you need to have a MIME parser that allows you add the signature information the stuff Microsoft provides and I dont think this is different on 2007 doesn't have any provisions to do this (although you can modify the raw content). The underlying MIME format is not the difficult but its risky if you build this yourself because of the amount of testing you would need to do to test all formats you might encounter.

cheers
Glen

dave.dolan said...

what do I do if I DO have another version of ADODB registered in the GAC? I copied it from the command line by going to the c:\windows\Assembly\GAC\ADODB\[version]\adodb.dll and generated the tlb from there. Is this the correct procedure? I'd assume removing it from the GAC would be a horrible idea because obviously something is requiring that it be there or it wouldn't be there...

Glen said...

This is a difficult issue to solve i would suggest having a read of http://blogs.msdn.com/junfeng/archive/2004/08/24/219691.aspx

Cheers
Glen

Anonymous said...

How do you write this in .Net framework version 4?