Dienstag, 14. September 2010

Emails empfangen

Hinweis: Dieser Post erklärt die Grundlagen des Empfangens von Emails. Zur Zeit des Posts war dies noch ohne SSL möglich, heutzutage ist dies jedoch meist Voraussetzung. Daher erkläre ich das Empfangen von Emails unter Benutzung einer SSL - Verbindung in diesem neuen Post. Dort wird auch die Verwendung eines IMAP - Servers beschrieben - in diesem Post wird nur auf einen POP3 - Server eingegangen.

In einem vorigen Post habe ich gezeigt, wie man mit C# Emails senden kann.
Heute geht es um das Empfangen von Emails.
Das Senden war ganz einfach, da .Net vordefinierte Methoden zum Verbinden mit einem SMTP - Server zur Verfügung stellt, über den die Emails gesendet werden können.
Zum Empfangen muss man etwas tiefer in die Trickkiste greifen, das Programm muss sich am Posteingangsserver anmelden und die richtigen Befehle zum Abholen und Lesen von Mails senden.
Clients können sich Emails vom Mailserver über das POP3 - oder das IMAP - Protokoll abholen. IMAP bietet mehr Funktionen, die meisten kostenlosen Mailanbieter wie WEB oder GMX verwenden aber das POP3 - Protokoll.
Der hier gezeigte Code arbeitet mit dem POP3 - Protkoll.
Ein POP3 - Server wartet auf einem bestimmten Port (meist 110) auf Anfragen. Ein Client kann sich mit einem Benutzernamen und Passwort dort anmelden und dann Befehle senden, die alle per Zeilenumbruch getrennt sein müssen.
Der Server schickt auf jeden Befehl eine Antwort und erwartet ggf. Eingaben.
Den POP3 - Server realisieren wir in C# mit einem TcpClient und senden die Befehle über einen NetworkStream.
Bevor ich näher auf den Code eingehe, erkläre ich kurz den grundlegenden Ablauf der Kommunikation mit dem Posteingangsserver:

Nach der Verbindungsherstellung erwartet dieser zuerst den Benutzernamen, welchen wir ihm in Form des Befehls "USER username". Danach folgt das Passwort "PASS password".
Zum Auflisten aller auf dem Server befindlichen Mails ist der Befehl "LIST" nötig.
Der Server schickt als Antwort die Nachrichten - IDs sowie die Größen der Nachrichten zurück, pro Zeile eine Nachricht, das Ende der Liste wird mit dem Zeichen "." gekennzeichnet.
Zum Auslesen einer bestimmten Emailnachricht kann der Befehl "RETR id" verwendet werden.
Der Server antwortet mit dem kompletten Mailinhalt.
Um die Verbindung zum POP3 - Server zu trennen, ist der Befehl "QUIT" nötig.
Nun zur C# Implementierung:
Zur Darstellung des POP3 - Servers wird eine Instanz der Klasse TcpClient angelegt.
Zum Senden der oben beschriebenen Befehle an den Server wird die Klasse NetworkStream verwendet, die sich in den Stream des Servers einklinkt.
Da Computer mit Binärdaten arbeiten, wir Menschen aber Wörter generell besser verstehen können, müssen die Befehle in Bytecode umgewandelt werden, was die Klasse ASCIIEncoding erledigt.
Eine Instanz der Klasse StreamReader wird schließlich zum Auslesen der vom Server gesandten Antworten benutzt.
Das folgende Programm demonstriert eine Kommunikation mit einem Mailserver, der Programmierstil mit globalen Variablen etc. ist nicht der Beste, aber ich denke, die grundlegenden Prinzipien werden so schnell und übersichtlich vermittelt.
Das Programm besteht aus 4 Methoden, wobei diese von der Methode Form1_Load() aufgerufen werden und basiert nur auf .Net Basisklassen:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

using System.Net.Sockets;
using System.IO;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        TcpClient POP3Server = new TcpClient(); // TcpClient zur Verbindung zum Server
        NetworkStream POP3Stream; // Netzwerkstream zur Kommunikation mit dem Server
        StreamReader StreamListener; // StreamReader zur Umwandlung der Binärdaten in leserlichen Text
        byte[] CommandBuffer = new byte[1024]; // Byte-Array zum Senden von Befehlen an den Server
        ASCIIEncoding AscEncoding = new ASCIIEncoding(); // ASCII Kodierung zur Umwandlung von ASCII-Code Befehlen in Binärdaten

        private void Form1_Load(object sender, EventArgs e)
        {
            if (Connect("hosturl", port, "benutzername", "password"))
            {
                /* folgenden Kommentar entfernen um alle Mails aufzulisten */
                // ListMails();
                /* folgenden Kommentar entfernen um Mail mit der ID id auszulesen */
                // richTextBox1.Text = ReadMailContent(id);
                Quit();
            }
        }

        /// <summary>
       /// Versucht, eine Verbindung zum angegebenen POP3-Server herzustellen.
        /// </summary>
        /// <param name="server">URL des Servers</param>
        /// <param name="port">Port des Servers</param>
        /// <param name="user">Benutzername zur Authentifizierung</param>
        /// <param name="password">Passwort zur Authentifizierung</param>
        /// <returns>true wenn Verbindung erfolgreich, andernfalls nein</returns>
        private bool Connect(string server, int port, string user, string password)
        {
            POP3Server.Connect(server, port); // verbinden
            POP3Stream = POP3Server.GetStream(); // Netzwerkstream erstellen
            StreamListener = new StreamReader(POP3Stream); // StreamReader aus Netzwerkstream erstellen

            if (POP3Server.Connected) // Verbindung erfolgreich
            {
                MessageBox.Show(StreamListener.ReadLine());

                // Benutzername senden
                CommandBuffer = AscEncoding.GetBytes("USER " + user + "\r\n");
                POP3Stream.Write(CommandBuffer, 0, CommandBuffer.Length);
                MessageBox.Show(StreamListener.ReadLine());

                // Passwort senden
                CommandBuffer = AscEncoding.GetBytes("PASS " + password + "\r\n");
                POP3Stream.Write(CommandBuffer, 0, CommandBuffer.Length);
                MessageBox.Show(StreamListener.ReadLine());

                return true;
            }

            return false;
        }

        /// <summary>
       /// Verbindung zum POP3 Server beenden
        /// </summary>
        private void Quit()
        {
            // Befehl zum Trennen senden
            CommandBuffer = AscEncoding.GetBytes("QUIT\r\n");
            POP3Stream.Write(CommandBuffer, 0, CommandBuffer.Length);
            MessageBox.Show(StreamListener.ReadLine());
        }

        /// <summary>
       /// Liest den Mailinhalt der angegebenen Mail aus.
        /// </summary>
        /// <param name="mailNr">ID der auszulesenden Mail.</param>
        /// <returns>Den Inhalt der Mail.</returns>
        private string ReadMailContent(int mailNr)
        {
            // Befehl zum Auslesen der entsprechenden Mail senden.
            CommandBuffer = AscEncoding.GetBytes("RETR " + mailNr + "\r\n");
            POP3Stream.Write(CommandBuffer, 0, CommandBuffer.Length);

            // StringBuilder zur schnellen Aufnahme des Mailinhalts.
            StringBuilder FullEmailContent = new StringBuilder();

            /* Solange, wie nicht "." ausgelesen wird, ist die Email noch nicht zuende,
            ausgelesene Zeilen in den StringBuilder schreiben. */
            string TempLine = StreamListener.ReadLine();
            while (TempLine != ".")
            {
                FullEmailContent.Append(TempLine + "\r\n");
                TempLine = StreamListener.ReadLine();
            }

            return FullEmailContent.ToString();
        }

        /// <summary>
       /// Listet alle auf dem Server befindlichen Emails auf.
        /// </summary>
        private void ListMails()
        {
            // Befehl zum Auflisten der Mails senden.
            CommandBuffer = AscEncoding.GetBytes("LIST\r\n");
            POP3Stream.Write(CommandBuffer, 0, CommandBuffer.Length);

            /* Solange, wie nicht "." ausgelesen wird, sind noch Emails auf dem Server vorhanden,
            diese werden einzeln durchlaufen und deren ID + Größe ausgegeben. */
            string CurrentMessage = StreamListener.ReadLine();
            while (CurrentMessage != ".")
            {
                MessageBox.Show(CurrentMessage);
                CurrentMessage = StreamListener.ReadLine();
            }
        }

    }
}


Die Methode ReadMailContent() schreibt den Inhalt der ausgelesenen Email in eine RichTextBox (diese muss auf dem Formular vorhanden sein), denn dieses Steuerelement unterstützt bereits einige Formattierungen (wie Links und Trennlinien), wie sie in Emails vorkommen.
Beim Aufruf Methode Connect() sind die Parameter jeweils durch eigene zu ersetzen.
Für die wohl bekanntesten Freemailer WEB und GMX sind folgende Einstellungen nötig:

WEB:
hosturl: pop3.web.de
port: 110
benutzername: Emailadresse ohne die Endung @web.de
passwort: selber wissen ;-)

GMX:
hosturl: mail.gmx.net
port: 110
benutzername: komplette Emailadresse
password: selber wissen ;-)

Kommentare:

  1. Ein echt guter Beitrag! Weiter so!

    AntwortenLöschen
  2. Hy, echt gute Beiträge! Ich arbeite mich gerade durch den gesamten Blog, aber hierfür bin ich scheinbar zu dumm ;)
    Ich habe versucht, den Code zu interpretieren und selbst neu zu schreiben. Das hat nicht funktioniert. Also habe ich einfach mal stupide kopiert und es funktioniert noch immer nicht :(
    Bei der Fehlerbehandlung bin ich dann draufgekommen, dass er scheinbar nichtmal bis zum Aufruf der Funktion connect() kommt.
    Will ich den Inhalt aber testweise in eine eigene Funktion einfügen, öffnet sich das Form gar nicht.
    Was mache ich falsch? :S

    AntwortenLöschen
  3. Hallo,

    danke für dein Interesse und viel Erfolg beim C# Lernen!
    Wenn du noch relativ unerfahren mit dieser Programmiersprache bist, ist meinen Blog durchzuarbeiten vielleicht die falsche Methode die Sprache richtig zu lernen - hier gibt es einfach viele unzusammenhängende Tipps.
    Wichtig ist es, sich die Grundprinzipien anzueignen, auf die man dann leicht aufbauen kann.
    Gerade dieser Post ist von der Schwierigkeit auch schon recht hart.
    Aber probier doch einmal, den Code schrittweise zu debuggen.
    Also setz Haltepunkte oder arbeite dich mit F11 durch den Code - dann wirst du sehen, an welcher Zeile der Fehler auftritt und was genau passiert.
    Mit dem Fehler kannst du dich dann nochmal bei mir melden, so ohne Infos kann ich dir leider nicht helfen.
    Viele Grüße
    Oliver

    AntwortenLöschen
  4. Hy,
    danke für deine schnelle Hilfe! Eines vorweg: Ich habe das Problem gelöst. Der Fehler lag im gaaanz peinlichen Detail: Ein Zifferndreher beim POP3 Server... Warum ich keine Fehlermeldung bekommen habe ist mir unklar...
    Als kleine Zusatzinfo: Ich würde mich nicht als Anfänger ezeichnen, eher als fortgeschrittener Einsteiger ;)
    Ich programmiere seit etwa 6 Jahren, seit ca 3 Jahren auch C# in der Schule. Mittlerweile bin ich so weit, dass ich sage "Was ich programmieren will, schaffe ich auch."
    Leider führt die Tatsache, dass ich mich persönlich gerne damit beschäftige dazu, dass ich etwas über dem durschnittlichen Klassenniveau bin. Deshalb stoße ich dann auf interessante Blogs wie deinen, wo wirklich interessante Themen behandelt werden. Da ich derzeit mit meinem Maturaprojekt, einem äußerst umfangreichen CarPC, beginne, arbeite ich also nun deinen Blog durch, nicht um C# zu lernen, sondern um diese Themen zu lernen ;) Besonders die Server-Client Kommunikation (bereits verstanden und verarbeitet), die Sprachaus- und -eingabe (ebenso) und das iTunes einbinden (noch nicht) haben es mir sehr angetan, aber auch noch einiges andere. Daher arbeite ich einfach alles durch: Wer weiß wozu man es braucht ;)
    Demnach hoffe ich, von dir nicht gelyncht zu werden, wenn ich noch einige (vermutliche) Anfängerfragen stelle, ich werde mich auch bemühen, sie dann genauer zu formulieren ;)
    Das Debugging hatte ich natürlich versucht, aber interessanterweise kam er gar nicht zum Verbindungsaufbau. Jetzt ist der Fehler weg und ich weiß nicht wohin: Mein Albtraum ;)

    Naja in diesem Sinne vielen Dank für die interessanten Tutorials und auch für die, die hoffentlich bald und zahlreich nachfolgen werden ;) Und sorry für die dumme Frage und den langen Text ;)

    LG,
    Bernie

    AntwortenLöschen
  5. Hallo Bernie,

    sorry, da hatte ich dich falsch verstanden!
    Aber vielen Dank für deinen netten und aufschlussreichen Text, ich freue mich natürlich immer wenn ich jemandem mit meinem Blog etwas helfen kann - und keine Angst, lynchen werde ich dich für weitere Fragen ganz sicher nicht ;-)
    Ich wünsche dir hier auf dem Blog viel Spaß und viel Erfolg bei deinene Projekten!

    Viele Grüße
    Oliver

    AntwortenLöschen
  6. Hallo,
    ich bin zurzeit daran mir einen E-Mail Client zu programmieren. Da die Art des Auslesens der Inhalte mit deiner identisch ist, frage ich am Besten hier :)
    Bis jetzt habe ich die E-Mail gesplitted in Header, Body etc. Allerdings werden Umlaute und "ß" als � dargestellt. Wie kann ich die Umlaute richtig darstellen?

    AntwortenLöschen
  7. Hallo,
    sorry erstmal für die späte Antwort.
    Dein Problem sollte aber ganz leicht zu lösen sein.
    Das Umlaute verschluckt werden liegt daran, dass das Programm die falsche Textkodierung verwendet.
    Diese wird beim Initialisieren von den Lesestreams o.ä. festgelegt.
    Sie ist allerdings auch manuell als Parameter einstellbar, also einfach die richtige übergeben.
    Im obigen Beispiel zum Beispiel:

    StreamListener = new StreamReader(POP3Stream, System.Text.Encoding.Default);

    Viele Grüße
    Oliver

    AntwortenLöschen
  8. Danke für deine Antwort,
    mit System.Encoding.Default ist aus dem '?' jetzt ein '"' geworden. noch Ideen?

    AntwortenLöschen
  9. Hallo,
    es wird also kein Sonderzeichen korrekt interpretiert?
    Probiere mal die Kodierung manuell zu wählen, meines Erachtens ist System.Text.Encoding.UTF8 die umfangreichste.

    AntwortenLöschen
  10. mit utf8 bin ich dann wieder bei dem schwarzen viereck mit weißem Fragezeichen. Hier habe ich einen Thread dazu erstellt:

    http://www.mycsharp.de/wbb2/thread.php?threadid=95113

    AntwortenLöschen
  11. Hallo Oliver,
    bin beim Stöbern auf Deine Seite gestossen.
    Prima!Informativ und Interessant! Sehr gut.
    Habe das Beispiel mit email-empfangen ausprobiert. Es laueft :-).
    Wenn ich beim Empfangen aber einen Anhang habe bekomme ich leider Zeichensalat :-(.
    Nun versuche ich auch eine email mit Anhang als *.jpg oder *.doc zu empfangen.
    Hast Du dazu eine Idee oder ein code-snipped ?

    Vielen Dank aus dem schoenen Harz

    AntwortenLöschen
  12. Hi.

    Muss ich für Hotmail auch POP3 nehmen oder etwas aneres und wie sieht es mit dem Port aus?

    AntwortenLöschen
  13. Hallo
    bei
    -
    richTextBox1.Text = ReadMailContent(id);
    -
    nimmt es den Parameter nicht an.
    Was kommt dort rein, da ich 'id' offensichtlich nicht einfach so stehen lassen kann?

    AntwortenLöschen
  14. Du musst schon die ID von der Mail nehmen die du auslesen willst. Probier einfach mal die 1.

    Sprich: richTextBox1.Text = ReadMailContent(1);

    id ist eine variable für die Nummer der auszulesenen E-Mail.

    Wenn du dir die Mails mit ListMails ausgibst stehen ja die gefunden Mails dort aufgelistet.

    AntwortenLöschen
  15. Danke, werde ich probieren.

    AntwortenLöschen
  16. Hmm. es zeigt in der MessageBox folgendes:

    +OK Hello there.<8222.131usw@localhost.localdomain>

    wenn ich OK drücke kommt:

    -ERR Invalid command

    Meine frage ist:
    Woher kommt dieses Hello there? in meinem Code steht das nirgends>.<

    schonmal danke

    AntwortenLöschen
  17. bis zum ersten "if-Gebilde" funktioniert das ganze halbwegs. "HI" wird angezeigt, wenn ich es wegklicke(mit Schliessen oder OK), kommt das, was ich oben schon beschrieben habe.

    private bool Connect(string server, int port, string user, string password)
    {
    POP3Server.Connect("mail.mink.li" , 110); // verbinden
    POP3Stream = POP3Server.GetStream(); // Netzwerkstream erstellen
    StreamListener = new StreamReader(POP3Stream); // StreamReader aus Netzwerkstream erstellen
    MessageBox.Show("HI");

    if (POP3Server.Connected) // Verbindung erfolgreich
    {
    MessageBox.Show(StreamListener.ReadLine());
    ...

    AntwortenLöschen
  18. Hallo,
    seit meinem letzten Kommentar sind ja richtig viele Kommentare entstanden, das freut mich, sorry für die späte Antwort.
    Ich versuche hier mal nicht beantwortete Fragen zu trennen und zu beantworten:

    - Zuerst an Anonym: Die benötigten Daten für Hotmail (sowie für sehr viele andere Anbieter) kannst du zum Beispiel auf dieser Seite nachlesen: http://www.patshaping.de/hilfen_ta/pop3_smtp.htm

    - Dann an SomeBody: Das "Hello there" schickt dir dein Email Server, dieser sendet Strings an den Client um z.B. den aktuellen Status o.ä. anzuzeigen.
    Deinen letzten Post kann ich leider nicht zuende sehen ...

    AntwortenLöschen
  19. Weiter ging der auch nicht, sollte ich den ganzen code posten?

    AntwortenLöschen
  20. Hallo,

    ich habe auch diesen code benutzt.
    "
    while (TempLine != ".")
    {

    FullEmailContent.AppendLine(TempLine);
    TempLine = StreamListener.ReadLine();
    }
    "
    bei dieser stelle bleibt er fast immer hängen, und der FullEmailContent kriegt nichts rein...
    manchmal funktioniert es, manchmal nicht.

    Vielleicht hast du eine lösung für das problem.

    Danke im Vorraus

    AntwortenLöschen
  21. Hallo Oliver,

    vielen Dank erstmal für das hilfreiche Tutorial.

    Eine Kurze Anregung und frage habe ich noch:
    Ich bekomme auf eine Mail-Adresse immer die gleichen Nachrichtentypen, bei denen nur Schlagwörter ausgetauscht werden.

    Diese Mails möchte ich "durchleuchten" und je nach Inhalt einen Ablauf starten.

    Ich habe im Internet nichts gefunden, vielleicht kannst du mir ja helfen?

    Danke schonmal.

    AntwortenLöschen
  22. Hi,

    das Email Thema ist leider nicht so einfach und ohne am Projekt beteiligt zu sein kann ich leider nicht viele Fehler finden, tut mir leid.

    An den letzten Poster:
    Du kannst mit der obigen Funktion ListMails() über alle vorhandenen Emails iterieren und dann über GetMailContent() den Inhalt einlesen und diesen String dann nach Schlagwörtern durchsuchen.
    War das deine Frage? Ich hoffe das hilft.

    Grüße
    Oliver

    AntwortenLöschen
    Antworten
    1. Ja, das war an sich schonmal sehr hilfreich.
      Ich werde es auf jedenfall einmal versuchen, vielen Dank!

      Löschen
  23. Hallo,

    sowohl beim listen als auch bei auslesen bekomme ich immer ne IOException, dass keine Daten gelesen werden können und die Verbindung vom Hostcomputer bzw. manchmal auch vom Remotehost abgebrochen wurde. Komischerweise bringt er diese Meldung erst bei Aufruf des ReadLine()s aus der Schleife, das davor gibt mir aber auch nichts zurück, line hat immer nur null.

    public string ReceiveMail(int id)
    {
    if (Connected)
    {
    command = ae.GetBytes("RETR " + id + "\r\n");
    stream.Write(command, 0, command.Length);

    StringBuilder sb = new StringBuilder();
    string line = reader.ReadLine();

    while (line != ".")
    {
    sb.Append(line + "\r\n");
    line = reader.ReadLine();
    }

    return sb.ToString();
    }

    return string.Empty;
    }

    AntwortenLöschen
  24. Servus,

    also ich hatte auch das Problem, das die IOExeption kam: "Von der Übertragungsverbindung können keine Daten gelesen werden: Eine vorhandene Verbindung wurde vom Remotehost geschlossen."

    Die Meldung kam bei mir auch bei ReadLines(). Nach langer Suche und verschiedenen Versuchen von Timeouts bin ich auf die blendente Idee gekommen, mal die Connection Eigenschaften zu überprüfen.

    Also auf manchen Seiten wird der Port von 995 (bei gmx) angegeben. Ich hatte leider diesen Port angegeben und nicht den 110 port von gmx wie oben angegeben. Versuche mal, diese Einstellungen bei dir zu überprüfen. Ich hab den Port angepasst und es lief durch.

    Nen Versuch ist es auf alle Fälle mal Wert.

    G.

    AntwortenLöschen
  25. Hilfe!!!

    Es zeigt mir leider nicht die Mails an, aber wieso. Was habe ich falsch gemacht?

    richTextBox1.Visible = true;
    if (Connect(cmbHostUrl.Text.ToString(), Convert.ToInt16(cmbPort.Text), txtBenutzer.Text.ToString(), txtPasswort.Text.ToString()));
    ListMails();
    richTextBox1.Text = ReadMailContent(1);
    Quit();

    AntwortenLöschen
  26. Hallo Oliver S.

    bin auf der Suche nach dieser Aufgabe mal auf deine Seite gestoßen. Ist ziemlich lange her, dass dieser Thread aktiv war :)

    Freue mich trotzdem über eine Antwort,wenn möglich?

    Nachdem Passwort setzen in der Methode Connect kommt eine Fehlermeldung vom GMX-Server, dass ich SSL aktivieren soll?

    An welcher stelle oder wo genau muss oder kann man das dem Server mitteilen, dass es aktiviert sein soll?

    mfg
    Cengiz

    AntwortenLöschen