SMTP Transport Event Sink

Achtung:
Diese Beschreibung ist keine How-To Anleitung, sondern soll ihnen schnell einen Einstieg in die eigene Entwicklung von Transport Sinks für Exchange bieten. Achten Sie auf jeden fall darauf, das Sie ihren Code in einem TestUmgebung ausführlich testen.

Mit Exchange 2007 wird es diese Schnittstelle so nicht mehr geben, da Exchange 2007 einen eigenen SMTP-Transport nutzt. Siehe auch E2K7:Hub/Transport und E2K7: Transportagents

Einer der wichtigsten und vermutlich häufig genutzten Schnittstellen sind die SMTP Event Sinks und hier der Transport Event Sink. Diese Schnittstelle erlaubt es ihnen relativ einfach alle eingehenden Nachrichten zu verändern. Dazu müssen Sie aber zuerst den Eventsink schreiben und dann registrieren. Die Erstellung von Protokoll Event Sinks,  die das SMTP-Protokoll erweitern ist jedoch etwas schwerer und selten erforderlich. Aber Exchange und das Active Directory nutzen diese Option.  Der Intelligent Message Filter hingegen ist kein SMTP-Sink.

Für die folgenden Aktionen benötigen Sie einige Hilfsprogramme, die von http://msdn.microsoft.com herunter zu laden sind oder in den Archiven der Sinkmuster auf meiner Webseite sind. Folgende Beispielprogramme nutzen SMTP Transport Event Sinks:

Die für die folgenden Schritt erforderlichen VBScript Codes finden Sie in folgendem KB-Artikel: Q324021 How to create a "catchall" mailbox sink für Exchange Server
Zusätzlich können Sie diese auch hier herunter laden.
smtpsink.zip

Eine erweiterte Version (Angepasst durch Henning Krause. www.infinitec.de gibt weitere Details aus
smtpeventscript.zip
Henning Krause hat ebenfalls ein .NET Assembly zur einfacheren Erstellung von Sinks in .NET bereitgestellt
http://www.infinitec.de/post/2008/07/Exchange-Eventsink-Foundation.aspx

Anzeigen der registrierten Sink

Fangen wir einfach an. Mit der Option "/ENUM" werden alle aktuell registrierten SMTP EventSinks angezeigt.

cscript.exe smtpreg.vbs /enum

Das folgende Bild zeigt die Ausgabe auf einem normalen Exchange 2003 Server ohne zusätzliche Komponenten:

Sie sehen drei Sinks, die alle auf den ersten virtuellen SMTP-Server (Displayname = smtpsvc 1) gebunden sind und auf "OnArrival" angetriggert werden:

  • Exchange Transport XEXCH50 Submission sink
    Dieser Sink behandelt die erweiterten Funktionen, die Exchange Server untereinander mit dem SMTP-Verb "XEXCH50" initiieren.
  • Exchange Transport AntiVirus API
    Über diesen Sink können aktuelle Virenscanner nun auch Nachrichten bei der Übertragung scannen und damit auf Konnectoren erkennen.  Das war früher nicht möglich, bzw. nur über den Sonderweg von Sybari.
  • ISM SMTP Transport
    Dies ist die Erweiterung, die der Domain Controller durchführt, damit "Inter-Site Messaging" über SMTP genutzt werden kann. Siehe auch http://www.Microsoft.com/windows2000/techinfo/planning/activedirectory/ismsmtp.asp

Alles in allem nutzt also auch Microsoft einfach die eigenen Schnittstellen, um den Windows SMTP-Server zu erweitern.

SMTPSinks
Quelle: http://www.microsoft.com/technet/prodtechnol/exchange/DE/Guides/E2k3TechRef/eaad59e4-ccbe-4ebf-ac14-69264b1cc167.mspx

Sie sehen, dass Sie an einigen Stellen eigene Module addieren können. Allerdings ist nur der "OnArrival" Sink so einfach per VBScript oder VB zu erreichen.  für alle anderen Sinks müssen Sie C++ bemühen.

Mein erster Sink - kein "Hello World"

Die meisten Programmierbeispiele für die verschiedensten Sprachen beginnen mit der Ausgabe eines "Hello World". Dies ist bei EventSinks aber nicht sehr effektiv, da ein EventSink keine GUI hat und damit auch keine Bildschirmausgaben erzeugen kann. Natürlich könnte ein Eventsink auch eine Datei schreiben aber auch das ist kein passendes Beispiel für die Nutzung eines EventSinks. Daher habe ich ein anderes Beispiel gewählt.

Die einfachste Art einen Sink zu schreiben ist zweifelsohne VBScript. Sie benötigen einfach nur einen Texteditor wie Notepad, um den Code zu schreiben. Sie müssen etwas das CDO-Objektmodell können, damit Sie die Teilinformationen in der Nachricht ansprechen können. Dieses Beispielskript ist nun wirklich sehr einfach gehalten und hat weder eine Fehlerbehandlung, Performancecounter noch Protokollfunktionen. Es legt Mails in BADMAIL ab, die von "testUser@testdomain.tld" kommen.

Dazu fragen wie einfach den angeblichen "Sender" ab und setzen entsprechend den Message-Status (http://msdn.Microsoft.com/library/default.asp?URL=/library/en-us/cdosys/html/_cdosys_schema_smtpenvelope_messagestatus.asp)

<SCRIPT LANGUAGE="VBScript">

Sub ISMTPOnArrival_OnArrival(ByVal objMsg, EventStatus)

    if objMsg.FROM = "testUser@testdomain.tld" then  'Mail nach Badmail verschieben
        Set objFields = objMsg.EnvelopeFields
        objFields.Item("http://schemas.Microsoft.com/cdo/SMTPenvelope/messagestatus").Value = 3
        objFields.Update
    end if
    EventStatus = 1
End Sub

</SCRIPT>

Und schon ist der Sink als solches fertig. Sie müssen den Code nun nur noch auf den Exchange Server in ein beliebiges Verzeichnis kopieren.

Vergessen Sie bitte nicht <SCRIPT LANGUAGE="VBScript"> und </SCRIPT>. Ich hatte schon den Effekt, dass ein Skript ansonsten einfach nicht gestartet wurde.

Achten Sie aber darauf, dass die NTFS-Rechte dem Exchange Server selbst (Konto: SYSTEM) die Berechtigungen geben. Das ist aber normalerweise der Fall.

Ein Sink wird für jede Mail einmal aufgerufen. Enthält eine Mail mehrere Empfänger, dann wird der Sink auch nur einmal aufgerufen. Die Liste der Empfänger enthält dann alle anderen Empfänger.

Sink registrieren

Als nächstes müssen Sie dem SMTP-Server mitteilen, dass es einen neuen Sink gibt. Mit der Registrierung wird zugleich festgelegt, auf welchem SMTP-Server und für welche Adresspaarungen dieser Sink aufgerufen wird. Das reduziert die Aufrufe. wenn die Funktion nur wenige Anwender betreffen soll. So könnte z.B. ein Supportsystem einfach alle Mails die Supportadresse abfangen und an ein anderes System weiterleiten oder ein verbessertes "CatchAll"-Skript prüfen, ob die Adresse lokal vergeben wurde und unbekannte Adressen anders verarbeiten.

cscript.exe smtpreg.vbs /add 1 onarrival testscript CDO.SS_SMTPOnArrivalSink "rcpt to=*"
cscript.exe smtpreg.vbs /setprop 1 onarrival testscript Sink ScriptName "C:\testscript\testscript.vbs"
pause

Die ersten Zeile definiert einen EventSink mit dem Namen "testscript" als "OnArrival" Event für alle Empfänger (rcpt to=*)

Die zweite Zeile setzt dann das Property "ScriptName", damit der SMTP-Server auch weiß, welches Skript er aufrufen muss.

Sink deregistrieren

Wenn Sie solch einen Sink wieder entfernen wollen, sollten Sie nicht einfach die Skriptdatei löschen, sondern auch im virtuellen SMTP-Server das Skript sauber deinstallieren.

cscript.exe smtpreg.vbs /remove 1 onarrival testscript
pause

Die Verbindung ist dann gelöst und Sie können das Skript selbst nun auch entfernen.

Einige Besonderheiten zu SMTP-OnArrival Transport Eventsinks

Auch wenn so ein Sink besonders einfach und leistungsfähig erscheint, so sollten Sie folgende Besonderheiten können:

  • Startet nicht bei internen Mails
    Wenn ein Benutzer eine Mail zu einem anderen Benutzer auf dem gleichen Exchange Server sendet, dann wird bei Exchange 2000 und Exchange 2003 kein SMTP Transport Eventsink gestartet, da die Mail direkt vom Store verarbeitet wird.
  • Startet bei eingehenden Mails per SMTP
    Wird eine Mail per SMTP (Siehe auch SMTP-Telnet) eingestellt, dann wird der Transport Eventsink aufgerufen.
  • Startet bei Mails über Pickup
    Wird eine Mail über das Pickup Verzeichnis eingestellt, dann wird der Transport Eventsink aufgerufen.
  • Vorsicht bei ausgehenden Mails
    Für jede Mail, die durch den SMTP-Service versendet wird, wird der Eventsink gestartet. Es ist dabei egal, ob die Mail vom Server selbst (z.B. Unzustellbarkeit), durch Outlook oder OWA oder per Outlook Express als Relay versendet wird.
    Allerdings sind bei ausgehenden Mails die Änderungen der Mail nur beschränkt möglich:

Q273233 You cannot modify MAPI messages that are trapped in an SMTP transport event sink
"SMTP transport event sinks für Exchange 2003 or Exchange 2000 can be used to trap all outgoing e-mail messages as they are handled by the SMTP transport layer. The cause of this problem is that Exchange converts MAPI messages to a temporary SMTP version für the event sink to handle, and then discards this temporary version after the event sink code finishes executing."

As mentioned earlier, the CDO_OnArrival event is a wrapper around the transport OnSubmission event that provides a handle to the messages in the CDO message format. The major benefit is that the Message object interface has many useful methods, such as the parsing of MIME and RFC 822 headers. The major drawback is that the CDO interface adds significant overhead, and is synchronous. The CDO_OnArrival event is most appropriate für sinks that are run on very few messages. This is the only piece that is easily programmable using a high-level scripting language like VBScript.
Quelle: http://msdn2.microsoft.com/en-us/library/ms998608.aspx#transportevents_topic6

Diese Einschränkung bezieht sich nur auf den "OnArrival"-Sink. Wenn man hingegen per C++ arbeitet, kann man auch den "OnPostCategorize"-Event ohne diese Einschränkungen nutzen.

Eine Mail kommt immer dann als MAPI-Mail an, wenn Sie wie folgt übertragen wurde:

  • MAPI Client
    Sie versenden die Mail von intern mittels Outlook oder einer anderen Software, die auf MAPI aufsetzt.
  • EDK-Gateway
    Ein Gateway nutzt die "alte" Exchange 5.5 EDK-Schnittstelle mit einem Connectorpostfach.
  • Exchange 5.5
    Die Mail kommt von einem Exchange 5.5 Server in der Organisation.

In diesem Fall können Sie aber einen zweiten SMTP-Server nachschalten, wie dies auf EventDisclaimer einfügen beschrieben ist. Dies bedeutet, dass der Eventsink sowohl eingehend als auch ausgehend gestartet wird, aber ausgehend nicht alle Funktionen zur Verfügung stehen. Ihr Skript muss daher selbst prüfen, ob die Mail zu bearbeiten ist, oder vielleicht schon verarbeitet wurde.

Eine Mail kommt per SMTP, wenn diese z.B. per OWA oder ActiveSync eingestellt wird.

EventSink und MAPI Clients

Beachten Sie bitte folgende Einschränkung, wenn Sie einen SMTP-OnArrival-Eventsink in VBScript programmieren:
Sie können nur dann eine ausgehende Mail verändern, wenn diese per SMTP empfangen wurde. Diese Einschränkung gilt nicht für C++-AddIns

Um mit SMTP Transport EventSinks dennoch auch die Mails zu verarbeiten, die per MAPI von Exchange an den virtuellen SMTP-Server zum Versand übergeben werden, müssen Sie zwei virtuelle SMTP-Server miteinander koppeln. Der erste SMTP-Server sendet die Mails an den zweiten, welcher dann die eigentliche Verarbeitung durchführt.

Eine ausführliche Beschreibung finden Sie auf der Seite SingleServerSink.

Diese Konstellation ist z.B. bei folgenden Lösungen erforderlich:

Müssen ausgehende Mails jedoch nicht verändert werden, sondern reicht es diese einfach nur für eine weitere Bearbeitung zu erhalten (z.B. Tracking, Archiv etc. dann ist dieses Konstruktion nicht erforderlich. Auch "echte" kompilierte SMTP-Sinks (COM-Objekte) sind davon nicht betroffen.

Debugging

Die Fehlersuche in EventSink-Skripten ist nicht einfach, besonders wenn sie mit VBScript arbeiten. Aber es gibt auch hier ein paar Ansatzpunkte.

  • Fehler im Skript
    Starten Sie das VBScript doch einfach mal mit CSCRIPT.EXE. Große Tippfehler und andere Schnitzer werden beim Übersetzen des Skripts so schon erkannt. Da es in einem Eventsink kein normales "Hauptprogramm" gibt, sondern nur die Funktionen, wird in der Regel auch nichts ausgeführt.
  • Sysinternals Filemon
    Das Programm "FILEMON" kann ebenfalls eine gute Hilfe sein. Man sieht dort z.B. wie der inetinfo.exe die Mail erst ablegt und dann das VBScript startet:
    Debug Sinks
  • Eventlog und Dateien
    Es ist auch immer eine Gute Idee im Script selbst ein paar Ausgaben zu generieren. Es ist problemlos möglich aus VBScript eine Datei zu schreiben oder einen Eventlogeintrag zu generieren, z.B. mit:

Dim objShell
Set objShell = CreateObject ("WScript.Shell")
objShell.LogEvent 0, "Sink Started"

Dim fs, file
Set fs = CreateObject("Scripting.FileSystemObject")
Set file = fs.CreateTextFile("C:\sink.log", 8, true)
file.Write "Gestartet"

Das Debuggen ist wirklich so altbacken, da es keine integrierte EntwicklungsUmgebung oder gar einen Debugger für VBScript gibt, den man da elegant einbinden kann.

Vorsicht mit iMsg

Eine Einschränkung sollten sie können, wenn Sie den SMTP-Service z.B. als Archiv oder "Sammeldienst" mit einem Eventsink konfigurieren wollen.  Es ist ja problemlos möglich mit folgender Zeile die komplette Message in eine Datei zu schreiben.

file.write imsg.getstream.readtext

Allerdings gibt es sehr wohl einen unterschied zwischen der Mail, die per TCP/IP auf den Server gekommen ist und der Mail, die mit GetStream erhalten werden kann.

Hier erst mal die Übertragung mit einem Telnet. Beachten Sie, dass der SMTP-Sender "User1" ist und der SMTP-Empfänger "User2", aber in der Mail selbst die FROM und TO-Zeilen anders belegt sind.

Die Mail, welches dann per "file.Write iMsg.GetStream.readtext" geschrieben wird, enthält nur die folgenden Informationen.

Die Original im SMTP-Header übertragenen Daten sind nicht Bestandteil der Mail. Diese müssen erst über die EnvelopeFields mit extrahiert werden.

Performance und Memory

Sie wissen mittlerweile, dass der SMTP-Services für jede Nachricht, die er versenden muss, ein Message-Object erstellt und diese an die verschiedenen Eventsinks der Reihe nach übergibt. Das bedeutet natürlich, dass jeder Eventsink kostbaren Speicher und CPU-Leistung konsumiert und viele Sinks das System entsprechend langsamer machen.

Wenn Sie einen Sink als VBScript implementieren, dann muss das Betriebssystem den Sink sogar bei jedem Aufruf mit dem Windows Scripting Host laden, übersetzen und ausführen.

Ein Skript muss innerhalb von 60 Sekunden beendet sein, da ansonsten der IIS das Skript abbricht und die Mail ohne Bearbeitung weitergibt.

Allerdings können sie einen EventSink auch als COM-DLL z.B. mit Visual Basic entwickeln. Dann liegt der Code schon als ausführbares Programm vor, was die Kompilierung erspart. Zusätzlich können diese DLLs die Option "Cachable" setzen, so dass der SMTP-Service die DLL nur einmal lädt und die Instanzen wieder verwendet.

The default Windows 2000 protocol and transport events are only accessible by writing Component Object Model (COM) objects in Microsoft Visual C++®. These events are fast, require no extra processing, and offer access to the lowest-level properties of the messages; however, they are more complex to write. für smaller jobs that don't require high performance, you can use the CDO_OnArrival event, which can be written using Microsoft Visual Basic®, Scripting Edition (VBScript).

Quelle: MS Windows 2000 SMTP Service Events http://www.Microsoft.com/technet/prodtechnol/exchange/2000/maintain/smtpserv.mspx

Zum Thema Performance hat auch Rui J. Silva, MVP für Exchange in Portugal auf http://www.msexchange.org/articles/Disclaimer-Fun.html einen interessanten Artikel geschrieben. Seine Lösung für einen Disclaimer hat er sowohl als VisualBasic-DLL als auch als VBScript-Code implementiert und mit 5000 Testnachrichten und dem Windows Performance Monitor untersucht. Die VBScript-Version hat mehr CPU-Belastung produziert, was zu erwarten war, aber aber fast doppelt so schnell. Das ist wahrlich ein kurioses Ergebnis. Eigentlich wäre eine großer Vorsprung der DLL-Version zu erwarten gewesen. Dies kann nur bedeuten, dass Sie ihre Lösung nicht gleich in Produktion einsetzen sollten, sondern in einem Testfeld nicht nur die Funktion sondern auch die Performance testen sollten.

Weiterhin sollten Sie folgende KB-Artikel im Auge behalten, wenn Sie sehr viele Mails umschlagen:

  • 905291 The inetinfo.exe process uses lots of memory and memory usage continues to increase until the SMTP service is unresponsive in Windows Server 2003 Service Pack 1
    (Hotfix bringt Aqueue.dll Version 6.0.3790.2520)
  • 830695 Increased memory usage in the Inetinfo.exe process if delivery restrictions are set on the SMTP connector in Exchange 2000 Server (In E200 SP4 gefixt)
  • 910932 Memory usage may increase over time when a computer that is running Windows Server 2003 uses the SMTP service to process many outgoing messages
    (Hotfix bringt Aqueue.dll Version 6.0.3790.2619)
  • 910932 The SMTP service may leak domain list memory when you use the Pickup folder

Weitere Links

Hier nur eine kleine Auswahl von Links. Die MSDN-Seite ist sicher die beste Quelle: