Donnerstag, 8. März 2012

hMailServer SpamAssassin Integration

Heute geht's um die Integration des hervorragenden Spam-Filters "SpamAssassin" in einen hMailServer.



Ich bin hierzu bereits auf einige andere Blogs und Foren gestoßen, allerdings wurde dort meist schwerpunktmäßig beschrieben, wie der eigentlich Unix-basierte SpamAssassin in eine Windows-Umgebung integriert werden kann.

In den Beiträgen wurde zumeist beschrieben, wie SpamAssassin unter Verwendung der Skript-Sprache Perl auf einem Windows-System kompiliert werden kann. Auch wenn hier der Bastellust durchaus Anerkennung zu zollen ist, birgt dieser Weg doch einige Nachteile: Zunächst benötigt man hier die komplette Laufzeitumgebung von Perl. Danach sollte man mit Tools wie nmake und gcc vertraut sein und in der Lage sein unter Zuhilfenahme des Quellcodes der offiziellen Apache Seite ein eigenes Kompilat zu erzeugen. Der große Nachteil hierbei sind die hierfür notwendigen Perl-Module: Da diese für Windows oft nicht vorkompiliert erhältlich sind, muss man wiederum selbst Hand anlegen. Das heißt Modul suchen, herunterladen, und in den allermeisten Fällen ein paar notwendige Änderungen an deren Quellcode vornehmen, damit sie unter Windows fehlerfrei kompiliert werden können. Selbst wenn dieser Schritt gelingt, sind am Quellcode von SpamAssasin noch einige Änderungen vorzunehmen, damit dieser unter einem Windows-System annähernd stabil funktioniert.

Die andere, oft zu sehende Variante ist die Verwendung von SaWin32. Dort erhält man nebst vorkompiliertem SpamAssasin samt Runtime einen netten Mail Proxy zur einfachen Handhabung für den Endanwender. Leider ist das Projekt vor 5 Jahren eingestellt worden, entsprechend veraltet ist der dort vorzufindende SpamAssassin (Version 3.2.3). Die aktuelle Version ist 3.3.2. Auch wenn die Versionsnummer etwas anderes suggeriert: Der Unterschied bei der Erkennung von Spam ist gewaltig. Ich bin nicht mal sicher ob man in der Version 3.2.3 überhaupt noch von Spam-Filterung sprechen kann. Aktuelle Anti-Spam-Regeln wurden hierfür meines Wissens jedenfalls schon seit einer Ewigkeit nicht mehr veröffentlicht.

Sparen wir uns das also alles und nutzen den einfacheren Weg: "SpamAssassin for Windows"
Hierunter verbirgt sich eine aktuelle und ebenfalls vorkompilierte Windows-Variante von SpamAssassin, die ich im Zuge eines Praxissemesters beim Unternehmen JAM Software entwickelt habe.

Der Grund zur Verwendung von hMailServer liegt in dessen nativer Unterstützung eines SpamAssassin-Interfaces. Sofern Mail-Server und Spam-Filter auf dem gleichen Host laufen, gestaltet sich eine Installation und Konfiguration der beiden Komponenten extrem einfach.

Kommen wir zur Vorgehensweise:
  1. hMailServer herunterladen und installieren:
    http://www.hmailserver.com/index.php?page=download
  2. SpamAssassin for Windows herunterladen und installieren:
    http://www.jam-software.de/spamassassin/download.shtml
  3. Nach der Installation öffnet man eine Eingabeaufforderung und wechselt zum Installationsverzeichnis von SpamAssassin for Windows.
  4. Den SpamAssassin Daemon starten: spamd.exe



    Ggf. wird hier eine Firewall-Warnung erscheinen, diese sollte bestätigt werden um die Verwendung Netzwerk-basierter Spam-Tests zu ermöglichen.


  5. Wechsel zum hMailServer und dort unter Settings --> Anti-Spam  unter dem Reiter "SpamAssassin" das Häkchen bei "Use SpamAssassin" setzen. Mit dem Button "Test" kann nun die Funktionalität des Spam-Filters überprüft werden. 





Wenn alles funktioniert hat, sollten sowohl hMailServer als auch SpamAssassin eine Rückmeldung über die Verarbeitung der Test-Mail zurückgegeben:





Der unter X-Spam-Score angegebene Wert gibt Auskunft über den erreichten Spam-Score. Hier gilt, je höher de Wert, desto eher handelt es sich um Spam. In der Regel gilt eine E-Mail bei SpamAssassin ab einem Wert von 5 Punkten als Spam. Der extrem hohe Wert von 1000 wurde hier aufgrund der speziellen GTUBE-Regel erreicht, mit der Spam-Filter auf ihre Funktionalität hin überprüft werden.


Das war's soweit zur Integration des Spam-Filters. Es sei noch gesagt, dass man SpamAssassin am besten also Windows-Dienst betreiben sollte, da er mit der Zeit einen sehr hohen Speicherverbrauch aufweisen und somit instabil werden kann. Die Integration als Systemdienst soll hier allerdings nicht thematisiert werden.

Montag, 16. Januar 2012

Einfacher, selbsthostender WCF Server mit Client in C#

Dies ist mein erster Blog-Post und soll den Anfang für eine kleine HowTo-Sammlung bilden, die ich zur Archivierung für mich selbst, aber auch für alle anderen Interessierten hier ablegen möchte.

Die Themen werden sich vor allem, aber nicht ausschließlich, mit der Anwendungsentwicklung mit C# innerhalb von Visual Studio beschäftigen.

Der heutige Post wird ein extrem einfaches aber recht schönes Beispiel zur Erstellung eines selbsthostenden WCF Hosts zeigen. Daneben wird außerdem gezeigt, wie man mit einem kleinen Client auf die Methoden des Hosts zugreifen kann.

Für diejenigen, die sich noch nicht mit WCF beschäftigt haben: WCF steht für "Windows Communication Foundation" und stellt ein leistungsfähiges Framework innerhalb von .NET dar, mit welchem man auf sehr einfache Art und Weise Server/Client-Anwendungen realisieren kann.

Warum hierzu ein weiterer Blog-Post, obwohl im Internet bereits unzählige existieren, die sich mit diesem Thema beschäftigen? Ich wollte für ein kleines Privat-Projekt einen WCF Server/Client erstellen, konnte aber im Internet kein aktuelles, einfaches aber dennoch schönes Beispiel-Projekt finden. Die Quellen derer ich mich während der Recherche bedient habe waren die folgenden:

http://bebugsblog.blogspot.com/2010/01/wcf-tutorial-1.html
http://www.codeproject.com/KB/WCF/WCFexample.aspx
http://bloggingabout.net/blogs/dennis/archive/2010/06/16/wcf-simple-example-in-visual-studio-2010.aspx

Die Anforderungen, die ich an mein eigenes Projekt im voraus gestellt habe, konnten die obigen Beispiele leider nicht vollständig abbilden. Wer möchte kann den Teil auch überspringen, hier die Gründe im Detail ;)


Möglichst einfache aber solide Architektur
Im Blog-Eintrag von beBug wird mit zwei einfachen Konsolen-Anwendungen begonnen, was mir sehr gut gefallen hat. Leider wird im Verlauf des Beispiels sehr viel mit Konfigurationstools gearbeitet, die für ein einfaches und vor allem für das Verstehen der Funktionsweise von WCF gedachten Beispiels recht ungeeignet sind. Die Arbeit mit den XML-basierten Konfigurationsdateien wird in meinem Beispiel daher bewusst ausgelassen. Die zweite Quelle, ein Code-Projekt Beispiel, liefert ebenfalls einen vielversprechenden Ansatz, war aber zum Einen etwas aufgeblasen für ein möglichst einfaches Einstiegsprojekt und zum Anderen etwas veraltet, wodurch ich zunächst nicht sicher war, ob mit Visual Studio 2010 größere Änderungen notwendig wären.

Keine Verwendung von svcutil oder Service Reference
In vielen Anleitungen, wie auch in der letzten Quelle, findet man die Verwendung der svcutil.exe (ServiceModel Metadata Utility Tool) oder einer Service-Referenz. Der Zweck hiervon ist es, die vom Host veröffentlichten Methoden im Client nutzen zu können. Der klare Nachteil der beiden Möglichkeiten ist ihre Inflexibilität. Wenn durch den Host eine neue Methode implementiert, oder eine vorhandene geändert wird, so muss man das svcutil erneut ausführen oder die Service-Referenz aktualisieren. Wenn man proprietäre Web-Services wie die von Amazon verwendet ist das völlig ausreichend. Für mein eigenes kleines Projekt wollte ich aber eine Lösung, bei der die Client-Anwendung die Methoden des Hosts in der Entwicklungsumgebung unmittelbar nutzen kann.



Erstellen des Projekts

Nun zum eigentlichen Beispiel: Wir beginnen mit der Erstellung der Solution. Hierfür sollte sichergestellt werden, dass Visual Studio im Administrator-Modus gestartet wurde, ansonsten schlägt das Erstellen des Netzwerk-Sockets im Verlaufe des Beispiels fehl. Sowohl für Host als auch für Client wird hierfür der Einfachheit halber eine Konsolenanwendung verwendet. Wie man die Projekte in einen Windows-Dienst und eine Windows Forms Anwendung portiert versuche ich in einem späteren Post zu zeigen. Zunächst erstellen wir also die Solution, zusammen mit dem WcfHost-Projekt:



Die Interface-Klasse

Nach dem Erzeugen der Solution fügen wir eine neue Class-Datei namens IWcfHost.cs hinzu und fügen dem Projekt eine Referenz zu System.ServiceModel hinzu. System.ServiceModel stellt viele der benötigten Methoden und Schnittstellen für die Erstellung von Server/Client basierten Architekturen bereit.


Zunächst wird aus der Klasse ein Interface gemacht und bekommt zwei einfache Methoden verpasst. Die Implementierung der Methoden wird gleich sauber in einer neuen eigenen Klasse bereitgestellt. Außerdem muss noch die System.ServiceModel Referenz in den usings hinzugefügt werden. Diese wird hier für die Attribute [ServiceContract] und [OperationContract] benötigt. Mithilfe der beiden Attribute wird bekannt gemacht, dass unser Interface und die beiden definierten Methoden Teil des Dienstvertrags zwischen Host und Client sein sollen. Der Dienstvertrag ist notwendig, damit Host und Client wissen, welche Art von Parameter und Rückgabewert durch die Gegenseite erwartet bzw. bereitgestellt werden.

using System.ServiceModel;

namespace WcfHost
{
    [ServiceContract]
    public interface IWcfHost
    {
        [OperationContract]
        string Echo(string pString);

        [OperationContract]
        int Sum(int pNum1, int pNum2);
    }
}


Implementierung der Interface-Methoden

Für die Implementierung der Interface-Methoden wird jetzt eine neue Klasse WcfHost erzeugt, welche das zuvor erstellte Interface IWcfHost implementiert. Nachdem wiederum die System.ServiceModel Assembly zu den usings hinzugefügt wurde, kann die Implementierung der zuvor definierten Methoden eingefügt werden. Das Attribut [ServiceBehavior(IncludeExceptionDetailInFaults = true)] ist optional und erleichtert eine detaillierte Fehlerdiagnose durch das Hinzufügen von Fehlerinformationen im Falle eines auftretenden Problems im Host. 

using System;
using System.ServiceModel;

namespace WcfHost
{
    [ServiceBehavior(IncludeExceptionDetailInFaults = true)]
    public class WcfHost : IWcfHost
    {
        public string Echo(string pString)
        {
            return String.Format("You've entered: {0}", pString);
        }

        public int Sum(int pNum1, int pNum2)
        {
            return pNum1 + pNum2;
        }
    }
}


Start des WCF-Hosts

Nach dem Erstellen des Interfaces und der Implementierung der Methoden kann der WCF-Host nun gestartet werden. Dafür wird in die Program.cs gewechselt und der folgende Code der Main-Methode hinzugefügt.

using System;
using System.ServiceModel;

namespace WcfHost
{
    class Program
    {
        static void Main(string[] args)
        {
             using (ServiceHost host = new ServiceHost(typeof(WcfHost)))
            {
                Uri baseAddress = new Uri("net.tcp://localhost:8999/MyService");
                NetTcpBinding binding = new NetTcpBinding();
                host.AddServiceEndpoint(typeof(IWcfHost), binding, baseAddress);
                host.Open();

                Console.WriteLine("Service started, press any key to finish execution.");
                Console.ReadKey();
                host.Close();
            }
        }
    }
}

Kurz zur Erklärung: Die ServiceHost Variable wird wie der Name schon sagt für das Hosten unseres Diensts verwendet. Dabei ist die Angabe des Typs unserer zuvor erstellten Klasse notwendig. Danach wird die Adresse definiert, unter der unser Host später erreichbar sein soll. Die Kommunikation zwischen Host und Client verläuft TCP basiert, entsprechend beginnt die Uri mit "net.tcp". Das "MyService" am Ende definiert den genauen Endpunkt, auf dem der WCF-Host Anfragen entgegennimmt. Prinzipiell könnte man hier später weitere Endpunkte hinzufügen, für unser Beispiel ist das aber nicht notwendig.
Nach dem Hinzufügen des Endpunkts, kann der ServiceHost "geöffnet" werden. Mit dem Console.Readkey() wird verhindert, dass sich der Host gleich wieder beendet.

Damit ist unser Host fertiggestellt. Er kann jetzt ausgeführt werden, woraufhin eventuell eine Firewall-Anfrage aufpoppt die bestätigt werden sollte, damit Server und Client miteinander kommunizieren können.


Hinzufügen des Client-Projekts

Der Client wird genau wie der Host als Konsolen-Anwendung implementiert. In der Solution wird daher zunächst ein neues Projekt vom Typ Konsolen-Anwendung hinzugefügt und beispielsweise WcfClient genannt. Damit Host und Client gleichzeitig gestartet werden können, sollte in den Eigenschaften der Solution  außerdem die Option "Multiple startup projects" ausgewählt und die beiden Projekte auf "Start" eingestellt werden.


Als Projekt-Referenzen wird neben System.ServiceModel nun außerdem unser Host-Projekt "WcfHost" benötigt:



Die Implementierung der main-Methode des Clients ist erstaunlich simpel. Wir benötigen lediglich die Adresse des TCP-Endpunkts und eine ChannelFactory, um einen Kanal zum Host aufbauen und verwenden zu können. Die von der Factory erzeugte Instanz weisen wir einer Klassenvariable vom bereits bekannten Interface IWcfHost zu. Hiermit können jetzt die Methoden aus der Host-Klasse unmittelbar verwendet werden.

using System;
using System.Text;
using System.ServiceModel;
using WcfHost;

namespace WcfClient
{
    class Program
    {
        private static IWcfHost host = null;

        static void Main(string[] args)
        {   
            Uri baseAddress = new Uri("net.tcp://localhost:8999/MyService");
            EndpointAddress address = new EndpointAddress(baseAddress);
            NetTcpBinding binding = new NetTcpBinding();
            ChannelFactory<IWcfHost> factory = new ChannelFactory<IWcfHost>(binding, address);
            host = factory.CreateChannel();

            Console.WriteLine("Please enter some words.");
            string input = Console.ReadLine(); // read input
            string output = host.Echo(input); // send to host, receive output
            Console.WriteLine(output); // write output

            int sum = host.Sum(5, 8);
            Console.WriteLine(sum);

            Console.ReadKey();
        }
    }
}



Ergebnis

Somit ist unser Projekt fertiggestellt und kann gestartet werden. Nach dem Buildvorgang sollten beide Applikationen selbstständig starten. Der Host wartet nach dem Start auf Anfragen während der Client von uns eine Tastatureingabe erwartet die er an den Host weiterleiten möchte:




Nach der Eingabe von ein paar Wörtern erhalten wir vom Host eine Rückgabe zusammen mit dem Ergebnis aus der Beispiel-Addition aus unserer Main-Methode.


Das vollständige Projekt kann über diesen Link heruntergeladen werden:
https://skydrive.live.com/?cid=d5f85d5594cec9d9&id=D5F85D5594CEC9D9%21133#

Das war's dann für heute, ich hoffe der Post konnte dem ein oder anderem weiterhelfen!

Liebe Grüße
Daniel