Artikel

November 2003 | Artikel

Postwendend

(Link zum Artikel: http://www.it-republik.de/dotnet/artikel/0456)

Ein POP3-Mailchecker auf Basis des .NET Frameworks, verfasst in purem C#

Text: von Sebastian Weber
  • Teilen
  • kommentieren
  • empfehlen
  • Bookmark and Share
Natürlich gibt es Mailchecker an jeder Ecke. Sicherlich auch bessere als den, den wir hier zusammenstellen - aber es geht doch nichts über einen eigenen, selbst geschriebenen. Außerdem benötigt doch jeder sein Bastelprojekt und da bietet sich in den Zeiten der Flatrate-Tarife etwas mit Socket-Programmierung doch sehr an. Dabei kann ein POP3-Mailchecker erst der Anfang sein, denn ob SMTP, IMAP oder HTTP - sie alle basieren auf den selben technischen Grundlagen und unterscheiden sich nur in ihrem Befehlssatz.

Ein POP3-Mailchecker überwacht ein eMail-Postfach auf das Eintreffen neuer Nachrichten. Dabei wird der Mail-Server in regelmäßigen Abständen nach der Anzahl der eMails im Postfach gefragt und anhand der Differenz zur vorherigen Abfrage festgestellt, ob eine oder mehrere neue Nachrichten eingetroffen sind. In diesem Artikel wird gezeigt, wie man mit Bordmitteln des .NET Frameworks unter C# einen solchen Mailchecker entwickelt. Dabei beschränken wir uns an dieser Stelle auf die Kommunikation mit dem Server und stellen ein vollständiges Programm zur Verfügung (siehe beiliegende CD), das jeder nach Belieben nutzen und ändern kann. Doch zuvor ein wenig Theorie.

Das Post Office Protocol
Mit dem Post Office Protocol in der Version 3, kurz POP3, können eMails von einem eMail-Postfach abgeholt und gelöscht werden. POP3 ist in dem RFC 1939 beschrieben, das im Internet [1] frei verfügbar ist. Eine Erweiterung des Protokolls ist im RFC 2449 spezifiziert und führt neue Kommandos und Fehlermeldungen ein. Eine gute Möglichkeit zum Kennenlernen des Protokolls ist das gute, alte Telnet, das auch unter Windows XP noch zum Standardumfang gehört. Am schnellsten erreicht man es unter Start | Ausführen ... und gibt dort direkt telnet ein. Vielleicht nehmen Sie Ihre persönlichen Anmeldedaten gleich zur Hand und versuchen den Verbindungsaufbau am lebenden Beispiel. Auf der Telnet-Konsole wird mit open die Verbindung zum Server hergestellt:
  1. Willkommen
  2. Das Escapezeichen ist 'CTRL++'
  3. Microsoft Telnet> open pop.meinprovider.de 110
Nicht zu vergessen ist die Angabe des Ports 110, auf dem der POP3-Dienst standardmäßig läuft. Alternativ kann auch POP3 ausgeschrieben werden, was Telnet wiederum in die Portnummer umsetzt. Wurde die Verbindung hergestellt, erhält man zunächst eine Begrüßung vom Server, die ungefähr so aussehen kann:
  1. +OK POP3 server ready
Jede Antwort vom Server beginnt bei einem positiven Status mit +OK und bei einem negativen mit -ERR, beides zwingend in Großbuchstaben. Als nächstes müssen wir uns authentifizieren, indem Benutzername und Kennwort mit den Befehlen USER und PASS nacheinander übermittelt werden. Auf jede Eingabe, die mit Enter (CRLF) abgeschlossen wird, erhalten wir vom Server eine Antwort:
  1. +OK POP3 Server ist bereit
  2. USER PeterP
  3. +OK Bitte Kennwort eingeben
  4. PASS absolutgeheim
  5. +OK Postfach bereit
Schlägt der Anmeldevorgang zum Beispiel aufgrund eines falschen Kennworts fehl, folgt eine Fehlermeldung:
  1. +OK POP3 Server ist bereit
  2. USER PeterP
  3. +OK Bitte Kennwort eingeben
  4. PASS falscheskennwort
  5. -ERR Benutzer oder Kennwort falsch
Aus Sicherheitsgründen erfährt der Anwender nicht, ob es am Benutzernamen oder am Kennwort liegt und bekommt erst nach dem PASS-Befehl eine echte Aussage über den Erfolg der Authentifizierung. War die Anmeldung erfolgreich, befindet man sich im so genannten Transaction-Status, in dem weitere Kommandos zur Verfügung stehen. Die gängigsten Befehle sind im Kasten POP3 Befehle zusammengestellt. Für unseren Mailchecker benötigen wir neben dem QUIT, zum Beenden der Verbindung, legendlich einen weiteren:
  1. STAT
  2. +OK 2 230
Der Befehl STAT wird ohne Argumente aufgerufen und liefert zwei Angaben zurück: die Anzahl der Nachrichten und die Größe der Mailbox in Octets (vereinfacht gesagt entspricht ein Octet genau 8-Bit). Glücklicherweise sind selbst die Abstände zwischen +OK und den beiden Werten auf genau ein Leerzeichen festgelegt, sodass unser Parser nicht allzu kompliziert ausfallen sollte. Nachdem alle Informationen gesammelt sind, wird die Kommunikation mit QUIT beendet. Hier noch einmal der gesamte Durchlauf, den es im Programm umzusetzen gilt:
  1. +OK POP3 Server ist bereit
  2. USER PeterP
  3. +OK Bitte Kennwort eingeben
  4. PASS absolutgeheim
  5. +OK Postfach bereit
  6. STAT
  7. +OK 2 230
  8. QUIT
  9. +OK
Anmeldevorgang:
USER Name Benutzername des Postfachs
PASS Kennwort Das zugehörige Kennwort
Nach erfolgter Anmeldung:
STAT Liefert die Anzahl der Nachrichten und die gesamte Mailboxgröße in Octets zurück. Als gelöscht markierte Nachrichten werden nicht berücksichtigt.
LIST [NachrichtNr] Ohne das optionale Argument werden die Größen aller nicht gelöschten Nachrichten zeilenweise aufgelistet. Wird einen Nachrichten-Nummer übergeben, erhält man die Information nur zu dieser Nachricht.
RETR NachrichtNr Liefert den Inhalt der gesamten Nachricht zurück. Die Nachrichten-Nummer darf nicht auf eine gelöschte Nachricht verweisen.
DELE NachrichtNr Markiert eine Nachricht als gelöscht. Der Server löscht die Nachricht erst nach einem QUIT.
Beenden der Kommunikation:
QUIT Beendet die Kommunikation und versetzt den Server in den Update-Status, sofern zuvor eine erfolgreiche Anmeldung stattgefunden hat. Im Update-Status werden als gelöscht markierte Nachrichten endgültig gelöscht. Sollte das Löschen fehlschlagen, wird ein Fehlerstatus -ERR, sonst ein Erfolgsstatus +OK zurückgegeben.
 
 
Die Programmierung
Was bislang nur über aufwändige API-Aufrufe möglich war, ist im .NET-Umfeld wirklich ein Kinderspiel. Zentrale Bestandteile unseres Programms sind die TcpClient- und NetworkStream-Klassen, die beide im Namensraum System.Net.Sockets liegen, sowie ein StreamReader und ein StreamWriter aus System.IO. Mehr wird nicht benötigt! Den noch nicht ausprogrammierten Klassenrumpf finden Sie im Listing 1.

Listing 1
  1. using System;
  2. using System.IO;
  3. using System.Net.Sockets;
  4. namespace Mailchecker
  5. {
  6. public class POP3
  7. {
  8. private const int POP3_PORT = 110;
  9. private string _servername = string.Empty;
  10. private string _username = string.Empty;
  11. private string _password = string.Empty;
  12. private string _lastError = string.Empty;
  13. // Initialisierung der Klasse mit Server- und Anmeldedaten
  14. public POP3(string Servername, string Username, string Password)
  15. {
  16. _servername = Servername;
  17. _username = Username;
  18. _password = Password;
  19. }
  20. // Liefert die Anzahl der Nachrichten im Postfach zurück.
  21. // Tritt ein Fehler ein, wird -1 zurückgegeben.
  22. public int Count
  23. {
  24. get { return GetMessageCount(); }
  25. }
  26. // Liefert die letzte Fehlermeldung zurück.
  27. public string LastError
  28. {
  29. get
  30. {
  31. string msg = _lastError;
  32. _lastError = string.Empty;
  33. return msg;
  34. }
  35. }
  36. // Interne Hilfsroutine zur Ermittlung der Nachrichtenanzahl
  37. private int GetMessageCount()
  38. {
  39. // TODO: Implementierung folgt!
  40. }
  41. // Ermittelt aus der Serverantwort die Anzahl der Nachrichten
  42. private int GetParsedMessageCount(string serverResponse)
  43. {
  44. // TODO: Implementierung folgt!
  45. }
  46. // Prüft, ob die Serverantwort positiv ist.
  47. private bool IsOK(string serverResponse)
  48. {
  49. // TODO: Implementierung folgt!
  50. }
  51. }
  52. }
Für drei Methoden unserer Klasse fehlt die Implementierung, die wir uns jetzt einmal genauer anschauen sollten. Die Methode GetMessageCount() soll die Verbindung zum Server aufbauen und mit Hilfe der Methoden GetParsedMessageCount() und IsOk() die Anzahl der eMails im Postfach ermitteln. Als erstes bauen wir eine Verbindung zum POP3-Server auf und greifen dabei auf die im Konstruktor übergebenen Anmeldedaten zurück. Das geschieht mittels des TcpClients und erfordert nicht mehr als eine Zeile Quellcode. Es werden Servername, beispielsweise pop.meinprovider.de und die Port-Nummer des Dienstes, in unserem Fall Port 110, übergeben.:
  1. TcpClient tcpClient = new TcpClient(_servername, POP3_PORT);
Der Datenaustausch mit dem Server geschieht über den NetworkStream der Verbindung, den man über GetStream() des TcpClients erhält. Zu diesem NetworkStream erstellen wir uns zwei weitere Streams, einen zum Schreiben und einen zum Lesen:
  1. NetworkStream networkStream = tcpClient.GetStream();
  2. StreamReader streamReader = new StreamReader(networkStream);
  3. StreamWriter streamWriter = new StreamWriter(networkStream);
  4. streamWriter.AutoFlush = true;
Durch das Setzen von AutoFlush = true wird jeder Schreibzugriff direkt an den Server übermittelt und muss nicht einzeln mit streamWriter.Flush() gesendet werden. Da wir in unserem Beispiel die Befehle nicht puffern möchten, ist diese Einstellung ganz komfortabel. Nach Aufbau der Verbindung steht im streamReader der Willkommensgruß des Servers, den wir mit ReadLine() auslesen und auswerten müssen:
  1. string response = streamReader.ReadLine();
  2. if (!IsOK(response))
  3. throw new Exception("POP3-Server nicht bereit. Servermeldung: " + response);
Die Methode IsOk() ist eine von uns implementierte Funktion, die nicht weiter macht, als den Stringanfang auf +OK zu prüfen und bei Übereinstimmung true, ansonsten false zurückliefert. Sollte der Server an dieser Stelle kein OK liefern, werfen wir eine Exception und beenden damit unser Vorhaben. Der Quellcode der durchaus überschaubaren IsOk()-Methode ist im Listing 2 abgedruckt. Als nächstes gilt es, die Authentifizierung vorzunehmen und anschließend mit dem STAT-Befehl die Anzahl der Nachrichten im Postfach zu ermitteln. Zum Senden eines Befehls nutzen wir den StreamWriter und rufen seine Methode WriteLine() auf. Nach dem Aufruf steht wiederum im StreamReader die Antwort des Servers, die wir erneut auf +OK oder -ERR prüfen:
  1. // Benutzername mit dem USER-Befehl übergeben:
  2. streamWriter.WriteLine("USER {0}", _username);
  3. response = streamReader.ReadLine();
  4. if (!IsOK(response))
  5. throw new Exception("Anmeldung fehlgeschlagen. Servermeldung: " + response);
  6. // Kennwort mit dem PASS-Befehl übergeben:
  7. streamWriter.WriteLine("PASS {0}", _password);
  8. response = streamReader.ReadLine();
  9. if (!IsOK(response))
  10. throw new Exception("Anmeldung fehlgeschlagen. Servermeldung: " + response);
Die WriteLine()-Methode bietet die Möglichkeit einen formatierten String samt Parameterliste entgegenzunehmen. Somit wird aus USER {0} der vollständige String USER PeterP an den Server gesendet. Sind wir erst einmal angemeldet und befindet sich der Server im Transaction-Status, können wir den STAT-Befehl absetzen und aus der Serverantwort die Anzahl der eMails auslesen:
  1. streamWriter.WriteLine("STAT");
  2. response = streamReader.ReadLine();
  3. if (!IsOK(response))
  4. throw new Exception("STAT-Befehl fehlgeschlagen. Servermeldung: " + response);
  5. // Server-Antwort auswerten:
  6. int count = GetParsedMessageCount(response);
Die Methode GetParsedMessageCount() ist ebenfalls eine eigens entwickelte, die aus dem Antwort-String des Servers, in der Form +OK Anzahl Größe, die Anzahl herausnimmt und zurückliefert. Die Implementierung findet sich in Listing 2. Wir beenden die Kommunikation mit QUIT und schließen die Verbindung.
  1. streamWriter.WriteLine("QUIT");
  2. streamReader.Close();
  3. streamWriter.Close();
  4. networkStream.Close();
  5. tcpClient.Close();
Der vollständige Quellcode der Methode GetMessageCount() ist noch einmal Listing 3 aufgeführt.

Listing 2
  1. private int GetParsedMessageCount(string serverResponse)
  2. {
  3. // Format: +OK Anzahl Groesse
  4. try
  5. {
  6. string temp = string.Empty;
  7. int start = -1;
  8. int end = -1;
  9. // Start- und Endpunkt finden
  10. start = serverResponse.IndexOf(" ") + 1;
  11. end = serverResponse.IndexOf(" ", start);
  12. // Anzahl ausschneiden
  13. if (end > start)
  14. temp = serverResponse.Substring(start, end - start);
  15. return Convert.ToInt32(temp);
  16. }
  17. catch
  18. {
  19. throw new Exception("Auswertung fehlgeschlagen. Response: " + serverResponse);
  20. }
  21. }
  22. private bool private bool IsOK(string serverResponse)
  23. {
  24. return serverResponse.StartsWith("+OK");
  25. }
Listing 3
  1. private int GetMessageCount()
  2. {
  3. int count = -1;
  4. string response = string.Empty;
  5. try
  6. {
  7. // Verbindung zum Server aufbauen:
  8. TcpClient tcpClient = new TcpClient(_servername, POP3_PORT);
  9. // Netzwerk-Stream geben lassen
  10. NetworkStream networkStream = tcpClient.GetStream();
  11. // Auf dem Netzwerk-Stream einen Leser und einen Schreiber erstellen:
  12. StreamReader streamReader = new StreamReader(networkStream);
  13. StreamWriter streamWriter = new StreamWriter(networkStream);
  14. // Mit AutoFlush=true wird jeder Schreibefehl direkt an den Server gesendet.
  15. streamWriter.AutoFlush = true;
  16. // Wilkommensgruss abholen:
  17. response = streamReader.ReadLine();
  18. if (!IsOK(response))
  19. throw new Exception("POP3-Server nicht bereit. Servermeldung: " + response);
  20. // Authentifizierung einleiten:
  21. streamWriter.WriteLine("USER {0}", _username);
  22. response = streamReader.ReadLine();
  23. if (!IsOK(response))
  24. throw new Exception("Anmeldung fehlgeschlagen. Servermeldung: " + response);
  25. streamWriter.WriteLine("PASS {0}", _password);
  26. response = streamReader.ReadLine();
  27. if (!IsOK(response))
  28. throw new Exception("Anmeldung fehlgeschlagen. Servermeldung: " + response);
  29. // Anzahl der Mailboxnachrichten ermitteln:
  30. streamWriter.WriteLine("STAT");
  31. response = streamReader.ReadLine();
  32. if (!IsOK(response))
  33. throw new Exception("STAT-Befehl fehlgeschlagen. Servermeldung: " + response);
  34. // Anzahl der Nachrichten ermitteln:
  35. count = GetParsedMessageCount(response);
  36. // Kommunikation beenden:
  37. streamWriter.WriteLine("QUIT");
  38. response = streamReader.ReadLine();
  39. // Verbindung schliessen:
  40. streamReader.Close();
  41. streamWriter.Close();
  42. networkStream.Close();
  43. tcpClient.Close();
  44. }
  45. catch (Exception ex)
  46. {
  47. _lastError = ex.Message;
  48. }
  49. return count;
  50. }
Damit ist die POP3-Klasse vollständig und ermittelt die Anzahl der Nachrichten eines Postfachs mit nur einem einzigen Aufruf:
  1. POP3 pop3 = new POP3("pop.meinprovider.de", "PeterP", "absolutgeheim");
  2. int count = pop3.Count;
Um den Mailchecker zu vervollständigen ist jetzt noch eine Windows-Applikation notwendig, die zeitgesteuert die Nachrichtenanzahl prüft und mit dem letzten Wert vergleicht. Sollten weitere Nachrichten eingetroffen sein, wird eine Meldung ausgegeben. Eine mögliche Umsetzung finden Sie auf der Heft-CD. Beim Testen ist unbedingt darauf zu achten, dass der POP3-Server nicht zu sehr mit Anfragen bombardiert wird. Manche Anbieter, beispielsweise Web.de, haben für ihr kostenfreies Angebot sogar Zeitsperren eingebaut, damit der Traffic im Rahmen bleibt.

Wie immer kann ein Blick hinter die Kulissen nicht schaden und viele Dinge sind gar nicht so kompliziert, wenn man sie sich erst einmal genauer angeschaut hat. Es bedarf nur wenig Magie, um sich mit einem Mailserver zu unterhalten und es scheint offensichtlich, das die Implementierung des RETR-Befehls zum Abholen der Nachrichten ebenso einfach zu schreiben ist. Wer Interesse hat, kann sich auch einmal SMTP anschauen und legt damit den Grundstein für einen eigenen, kleinen eMail-Client. Genau das richtige Bastelprojekt für die langen Abende im Herbst und Winter.
Sebastian Weber ist Software Engineer bei der Platinion GmbH und ist dort auf die Konzeption und Implementierung von .NET-Applikationen spezialisiert. Sie erreichen ihn per eMail unter: weber.sebastian@platinion.de.

Links und Literatur
[1] RFC 1939: www.ietf.org/rfc/rfc1939.txt


Anzeige

Kommentare

zurück zum Seitenanfang