Last year, the SoBig virus humbled me. My team uses Microsoft Exchange Server to manage our internal email system and host several closed external discussion lists. We use client-side spam software to filter out noise from our Inboxes, but we had nothing in place to filter email on the server.
After SoBig hit, each member of my team manually filtered roughly 1GB of email a day. Most of the volume was due to the virus and the inevitable virus-alert messages that found their way to our server. Our server not only had to process each offensive email message but also had to devote disk space to storing it and network bandwidth to transferring it to the user's Microsoft Outlook Inbox. Then, the user's Outlook filters took time and consumed resources to delete the message from the Inbox and notify the server to delete the message from the user's Exchange mailbox store. To mitigate the problems, we decided to write a Perl script to run on our Exchange server and filter out on arrival all incoming email that might be a virus.
Writing the script was remarkably easy, thanks to Microsoft Collaboration Data Objects (CDO). The code runs on any Windows machine that implements the Microsoft SMTP service, including Windows Server 2003 and Windows XP. Because Exchange Server 2003 and Exchange 2000 Server both rely on the SMTP service, the solution works just as well with them.
The Windows SMTP Service
Windows 2000 and later OSs include an SMTP (email) service, which Web pages and applications use to send email. When the SMTP service is running on your machine, Web pages and applications can simply submit outgoing email to the service, which then routes the messages to other email servers. CDO is a collection of COM objects that provide an easy way to access Windows messaging services, including SMTP mail. CDO abstracts many of the complexities of working with communication technologies, simplifying how programmers interact with these services. For details about CDO, see http://msdn.microsoft.com/library/en-us/cdo/html/_olemsg_overview_of_cdo.asp.
CDO Transport Event Sinks
The SMTP service is a fairly complex system of components that talk with one another. Upon receiving an email message, the SMTP service processes it by handing it sequentially to several components, each of which can examine and modify the message. One of those components is the CDO transport event sink.
Transport event sinks can perform an action based on a message's content. The SMTP transport event sink handles events that occur when the SMTP service receives an email message. When an incoming message arrives at the service—
triggering the OnArrival() event—
the transport event sink can call a Windows Script Host (WSH) script, load it, and call the script's ISMTPOnArrival_OnArrival() subroutine if one exists. Further details exceed the scope of this article, but you can learn more at http://msdn.microsoft.com/library/en-us/cdosys/html/_cdosys_smtp_nntp_transport_event_sinks_with_cdo.asp.
How Transport Event Sink Scripts Work
Listing 1 shows the MessageFilter.pl transport event sink script. You must put the script's code between XML tags that tell the transport event sink what scripting language engine to use. The code at callout A and callout J in Listing 1 show the tags that specify the PerlScript engine. These tags let your script use any file extension (or none at all)—
a flexibility that many administrators find attractive. The Perl comment character (#) at the beginning of each tag line lets you run the script both from the event sink and from a command line (e.g., to test the script) without triggering compile errors.
When the transport event sink executes the script, it first executes all the code in the default or main:: namespace. The event sink then searches the script for a subroutine called ISMTPOnArrival_OnArrival() and calls the subroutine, if it exists. The subroutine receives two parameters, as the code at callout E shows: $Message (a CDO message object) and $EventStatus (an event status value). The script can query the message object's properties to obtain information such as the message's sender, recipient list, and subject. For a list of all message object properties, go to http://msdn.microsoft.com/library/en-us/cdosys/html/_cdosys_imessage_interface.asp.
The script is supposed to set the CdoEventStatus value to a value that indicates the status of the script's message processing. For example, if the script determines that the message contains a virus and should be discarded, the script should be able to set the CdoEventStatus value to cdoSkipRemainingSinks (value of 1) to indicate that subsequent sinks don't need to process the message. By default, the event status value is cdoRunNextSink (value of 0), which indicates that the message is fine and that subsequent sinks should process it.
I say that the script is supposed to set the event status because that isn't what happens with Perl. In Perl, the value is passed in only as a value, not as a reference or an object. If you're running only one event sink and using a script as your CDO transport event sink event handler, not setting the CdoEventStatus value won't impact overall performance because you don't have any subsequent sinks. However, if you need to run multiple event sinks, not setting the event status causes subsequent sinks to process all messages, even those scheduled for removal, thereby creating unnecessary overhead. Thus, if you have multiple sinks and you're using Perl, you should consolidate all the sinks' logic into one monolithic event sink handler to reduce the overhead of running sinks unnecessarily.
After loading a script, the CDO transport event sink caches the resulting WSH object in memory. To optimize performance, the transport event sink uses the cached object whenever it needs to call the script. In other words, the Perl script is compiled only once but executed multiple times. If you modify the script, the CDO transport event sink detects the file's modified date and reloads the script. As a result, you get the performance of script caching with the benefits of touching the disk for each use. For information about using scripting languages to implement event sinks and how an event sink caches scripts, see http://msdn.microsoft.com/library/en-us/cdosys/html/_cdosys_implementing_sinks_with_scripting_languages.asp.