PHP

PHP & OOP - Mailversand via SMTP (PHP Tutorial)

Tutorial erstellt von Andreas W. in 5.x, letzte Änderung am 22.06.2010

In dem nachfolgenden Tutorial möchte ich euch zeigen, wie man eine Klasse programmiert und nutzt, die das Simple Mail Transfere Protocol (SMTP), das zum Versenden von Emails genutzt wird, implementiert. Am Ende des Tutorials wird ein kleines, aber dennoch erhellendes Beispiel zu finden sein, das den Einsatz dieser Implementationen in der Praxis demonstriert. Theorie und Geschichte sowie die gezeigten Anwendungen und Methoden werde ich dabei ausführlich erläutern.

Ein Überblick

Das Simple Mail Transfere Protocol ist ein textbasiertes Protokoll, welches das Versenden von Nachrichten innerhalb eines Netzwerkes ermöglicht. Zum Lesen von Nachrichten wird SMTP nicht verwendet. Diese Aufgabe übernehmen wahlweise das IMAP- oder POP3-Protokoll.
Ein Mail-Server kann im Regelfall über den Port 25 angesprochen werden. Dies kann sich jedoch von Server zu Server unterscheiden. Neuere Server bieten zum Beispiel auch einen Zugang über den Port 587 an, der es authentifizierten Nutzern ermöglicht, Emails an andere Server zu verschicken.

1982 wurde das Simple Mail Transfere Protocol unter RFC 821 veröffentlicht. Zuvor wurden Protokolle wie das im Juli 1971 veröffentlichte Mail Box Protocol (RFC 278) oder auch das im Februar 1973 veröffentlichte FTP Mail Protocol (RFC 458) verwendet.
Da SMTP ein textbasiertes Protokoll ist, können Emails auch über einen telnet-Client versendet werden. Dies ist jedoch nicht üblich. Die eigentliche Arbeit (die richtige Verwendung des Protokolls) übernimmt normalerweise ein Mail-Client, wie zum Beispiel Thunderbird oder auch Outlook.

Da ich kein großer Fan der PHP eigenen Mail-Funktion bin, werden wir uns das SMTP-Protokoll etwas genauer ansehen und eine eigene Mailer-Klasse auf Basis dieses Protokolls entwickeln. Dabei werden wir auch den generellen Umgang bzw. die Implementierung einer Protokoll-basierten Klasse erlernen. In diesem Rahmen wird es auch einen Ausblick auf mögliche Abstraktionsebenen geben, die es beispielsweise ermöglichen eine eigene FTP- oder IMAP-Klasse zu entwickeln.

Das Protokoll im Detail

Eine Email lässt sich in Header und Body teilen. Im Header findet man alle Daten wie zum Beispiel den Absender und den Empfänger der Email. Im Body findet man den eigentlichen Inhalt der Nachricht. Um eine Email zu versenden reicht es jedoch nicht, diesen Aufbau zu erzeugen.
Als erstes muss man den Server, über den man seine Nachricht verschicken möchte, begrüßen. Dies geschieht wahlweise mit den Kommandos Helo oder Ehlo mit nachgestellter Serveradresse, wobei Helo die gängigere Variante darstellt. Anschließend werden sowohl die Absender- als auch die Empfänger-Adresse angegeben, die mit dem Befehl MAIL FROM bzw. RCPT TO eingeleitet werden. Beide Angaben sind, wie bereits erwähnt, auch im Header der Email zu finden.
Die eigentliche Email ist im DATA Block, der die komplette Email (Header und Body) enthält, zu finden.
Zum Schluss verabschiedet man sich noch vom Server mit dem Befehl QUIT. Wichtig ist noch zu wissen, dass alle Befehle mit der Zeichenfolge \r\n (CRLF - carriage return line feed) abgeschlossen werden.

Wie bei vielen Kommunikationsarten mit einem Server liefert dieser auch während der Verbindung Antworten, die der Client überprüfen muss, um auftretende Fehler frühzeitig zu erkennen. Alle Antworten vom Server setzen sich dabei aus einem festgelegten, dreistelligen Zahlencode und einer Server spezifischen Textnachricht zusammen. Eine verkürzte Zusammenstellung der verschiedenen Fehlercodes findet ihr nachfolgend.


  • 1XX: Anforderung akzeptiert, weitere Aktionen nötig.

  • 2XX: Anforderung erfolgreich ausgeführt.

  • 3XX: Anforderung verstanden, weitere Informationen benötigt.

  • 4XX: Temporärer Fehler, erneuter Versuch kann Fehler beheben.

  • 5XX: Fataler Fehler, Anforderung kann nicht verarbeitet werden.



Die zweite Ziffer gibt weiteren Aufschluss.


  • x0x: Syntax Fehler

  • x1x: Antwortinformation (z.B. beim Senden des Befehls HELP)

  • x2x: Status der Verbindung

  • x3x: Nicht benutzt

  • x4x: Nicht benutzt

  • x5x: Status des Mailservers



Diese Fehlercodes gilt es in unserer Klasse abzufragen und korrekt zu verarbeiten.

Eine erste Email

Da wir nun die wichtigsten Befehle und Fehlercodes, sowie den allgemeinen Aufbau einer SMTP-Anfrage kennengelernt haben, können wir uns jetzt dem Aufbau unserer SMTP-Klasse widmen. Dazu werden wir einige Vorüberlegungen anstellen.

Eine Verbindung zu einem Server über einen bestimmten Port lässt sich in PHP relativ einfach mit den Socket-Funktionen (man denke nun an fsockopen()) bewerkstelligen. Diese Möglichkeit wollen wir auch in unserer Klasse nutzen. Ein kleines Beispiel, wie man sich richtig mit einem Server mit Hilfe von Sockets verbindet, findet ihr nachfolgend.

Code:

<?php

/* connect to the smtp server */
$sock = fsockopen("smtp.my.com", 25, $errno, $errstr, 30);

/* check result */
if (!$sock)
{
    echo "$errstr ($errno)<br />\n";
}

else
{
    fwrite($sock, "HELO client.my.com\r\n");
    fwrite($sock, "MAIL FROM:<me@my.com>\r\n");
    fwrite($sock, "RCPT TO:<friend@his.com>\r\n");
    fwrite($sock, "DATA\r\n");
    fwrite($sock, "Received: from my.com by his.com ; Mon, 31 Aug 2009 12:11:00 -0700\r\n");
    fwrite($sock, "Date: Mon, 31 Aug 2009 12:11:00 -0700\r\n");
    fwrite($sock, "From: Me <me@my.com>\r\n");
    fwrite($sock, "Subject: A Testmail\r\n");
    fwrite($sock, "To: friend@his.com\r\n");
    fwrite($sock, "\r\nThis is a simple Testmail!\r\n\r\nMe.\r\n");
    fwrite($sock, ".\r\nQUIT\r\n");
}
?>


Soeben haben wir also eine Email von my.com an his.com verschickt. Ihr könnt die angegebenen Daten ja mal ändern und versuchen eine Email an euch selbst zu schicken.

Die Implementierung

Wir wollen nun einige Methoden implementieren, die uns das Erstellen bzw. Versenden einer solchen Email via PHP erleichtern. Wir benötigen also eine Möglichkeit eine Socket-Verbindung aufzubauen und innerhalb des Objekts zugänglich abzuspeichern. Diese Aufgabe sollten wir einfach dem Konstruktor unserer Klasse überlassen.
Des weiteren bietet es sich an eine einfache Methode zu schreiben, die es ermöglicht einen Befehl über unsere Verbindung unter anhängen des benötigten \r\n zu verschicken und eine Methode, die eine Antwort abfängt.
Zur Überprüfung der Serverantworten sollten wir eine Methode zur allgemeinen Prüfung sowie eine Methode zur Prüfung auf Basis der Fehlercodes einbinden. Um die Kommunikation zwischen Server und Client nachvollziehen zu können, implementieren wir sowohl die Methoden log() und getLog() als auch die entsprechende Variable, die den Sitzungstext enthält.
Abschließend implementieren wir für die wichtigsten Befehle jeweils eine Funktion. Ich denke die Erläuterungen reichen aus, um sich eine eigene kleine Klasse, wie die nachfolgende zu basteln.

Code:

<?php

/**
* The SmtpConnect class enables sending mails via SMTP.
* It is not very easily operated, but that is not required,
* because this class should just be the base for an comfortable
* Mail class. On the basis of some comments to this class i want
* to add that it sure supports Secure Sockets Layer connections.
* You just have to modify the host and port in the constructor.
*
* @author Andreas Wilhelm <Andreas2209@web.de>
* @copyright Andreas Wilhelm
* @version 13/02/2009
* @see http://avedo.net
*/  
class SmtpConnect
{
    /**
     * @var String $host The host you want to connect to
     * @access private
     */
    private $host;
    
    /**
     * @var Integer $port The http server port
     * @access private
     */
    private $port;

    /**
     * @var Object $sock Holds the socket connection
     * @access private
     */
    private $sock;
    
    /**
     * @var Array $authTypes Types of authetification
     * @access private
     */
    private $authTypes = array(
        'LOGIN',
        'PLAIN',
        'CRAM-MD5');

    /**
     * @var String $response The last server response
     * @access private
     */
    private $response = '';

    /**
     * @var String $log Holds the connection logging
     * @access private
     */
    private $log = '';

    /**
     * Sets host and port
     *
     * @access public
     * @param String $host Host name
     * @param Integer $port The server Port
     * @return void
     */
    public function __construct($host='localhost', $port=25)
    {
        // set server-variables
        $this->host = $host;
        $this->port = $port;
    }

    /**
     * Connects to the given server
     *
     * @access public
     * @return Boolean
     */
    public function connect()
    {
        // control-connection handle is saved to $handle
        $this->sock = @fsockopen($this->host, $this->port);
        if ( !$this->sock OR !$this->check('220') )
            throw new Exception("Connection failed.");

        // switch to non-blocking mode - just return data no response
        set_socket_blocking($this->sock, true);

        // set timeout of the server connection
        stream_set_timeout($this->sock, 0, 200000);

        return true;
    }

    /**
     * Sends greeting to secured server
     *
     * @access public
     * @return Boolean
     */
    public function ehlo()
    {
        // send EHLO -spezified in RFC 2554
        $this->cmd("EHLO " . $this->host);
        if( !$this->check('250') )
            throw new Exception("Failed to send EHLO.");

        return true;
    }

    /**
     * Sends greeting to server
     *
     * @access public
     * @return Boolean
     */
    public function helo()
    {
        // Send the RFC821 specified HELO.
        $this->cmd('HELO ' . $this->host);
        if( !$this->check('250') )
            throw new Exception("Failed to send HELO.");

        return true;
    }

    /**
     * Sends authentification
     *
     * @access public
     * @param String $user The username
     * @param String $pwd The passwort
     * @param String $type The authetification Type (LOGIN/PLAIN/CRAM-MD5)
     * @return Boolean
     */
    public function auth($user, $pwd, $type='PLAIN')
    {
        if( in_array($type, $this->authTypes) )
        {
            // send authentification-identifier
            $this->cmd("AUTH $type");

            // catch first ready response
            $response = $this->getReply();
            if( substr($response,0,1) != 3 )
            {
                throw new Exception("Failed to send AUTH.");
            }
        }

        if( $type == 'LOGIN' )
        {
            // send user-name
            $this->cmd(base64_encode($user));
            if( !$this->check('334') )
                throw new Exception("Failed to send user-name.");

            // send password
            $this->cmd(base64_encode($pwd));
        }

        elseif( $type == 'PLAIN' )
        {
            // prepare data
            $data = base64_encode($user.chr(0).$user.chr(0).$pwd);
            $this->cmd($data);
        }

        elseif( $type == 'CRAM-MD5' )
        {
            $data = explode(' ',$response);
            $data = base64_decode($data[1]);
            $key = str_pad($pwd, 64, chr(0x00));
            $ipad = str_repeat(chr(0x36), 64);
            $opad = str_repeat(chr(0x5c), 64);
            $this->cmd( base64_encode($user.' '.md5(($key ^ $opad).md5(($key ^ $ipad).$data,true))) );
        }

        else
            throw new Exception("Authentification failed.");

        if( !$this->check('235') )
        {
            throw new Exception("Authentification failed.");
        }

        return true;
    }

    /**
     * Sends specified addressor
     *
     * @access public
     * @param String $from The email-address of the addressor
     * @return Boolean
     */
    public function from($from)
    {
        // specify addressor
        $this->cmd("MAIL FROM: $from");
        if( !$this->check('250') )
            throw new Exception("Failed to send addressor.");

        return true;
    }

    /**
     * Sends specified acceptor
     *
     * @access public
     * @param String $to The email-address of the acceptor
     * @return Boolean
     */
    public function rcpt($to)
    {
        // send specified acceptor
        $this->cmd("RCPT TO: $to");
        if( !$this->check('250') )
            throw new Exception("Failed to send acceptor.");

        return true;
    }
  
    /**
     * Sends the data to the server
     *
     * @access public
     * @param String $message The message
     * @param Array $header SOme more mail header fields
     * @return void
     */      
    public function data($message, $header)
    {
        // initiate data-transfere
        $this->cmd('DATA');
        if( !$this->check('354') )
            throw new Exception("Data-transfere failed.");

        // validate header-data
        if( !is_array($header) )
            throw new Exception("Header-data must be an array.");

        // initiate counter
        $i = 0;

        // include header data
        foreach( $header as $key => $value)
        {
            // send header
            if( $i < count($header)-1 )
            {
                $this->cmd("$key: $value");
            }

            else
            {
                $this->cmd("$key: $value\r\n");
            }

            $i++;
        }

        // send the message
        $this->cmd("$message\r\n");

        // send end parameter
        $this->cmd('.');

        $this->check('250');
    }
  
    /**
     * Closes the server-connection
     *
     * @access public
     * @return void
     */      
    public function quit()
    {
        $this->cmd("QUIT");
        $this->check('221');
        fclose($this->sock);
        return true;
    }

    /**
     * Sets a ftp-command given by the user
     *
     * @access private
     * @param String $cmd A specific command
     * @return void
     */
    private function cmd($cmd)
    {
        fputs($this->sock, "$cmd\r\n");
        $this->log("> $cmd");
    }

    /**
     * Catches the reply of the server
     *
     * @access private
     * @return String
     */
    private function getReply()
    {
        $go = true;
        $message = "";

        do
        {
            $tmp = @fgets($this->sock, 1024);
            if($tmp === false)
            {
                $go = false;
            }

            else
            {
                $message .= $tmp;
                if( preg_match('/^([0-9]{3})(-(.*[\r\n]{1,2})+\\1)? [^\r\n]+[\r\n]{1,2}$/', $message) ) $go = false;
            }
        } while($go);

        $this->log($message);

        return $message;
    }

    /**
     * Checks if the response of a command is ok
     *
     * @access private
     * @return Boolean
     */
    private function valid()
    {
        // get response of the server
        $this->response = $this->getReply();

        // check the response and say if everything is allright
        return (empty($this->response) || preg_match('/^[5]/', $this->response)) ? false : true;
    }

    /**
     * Checks if the response-code is correct
     *
     * @access private
     * @param String $code
     * @return Boolean
     */
    private function check($code)
    {
        if( $this->valid() )
        {
            $pat = '/^'. $code .'/';
            if( preg_match($pat, $this->response))
            {
                return true;
            }
        }

        return false;
    }

    /**
     * Saves all request to the server and their responses into $this->log
     *
     * @access private
     * @return void
     */
    private function log($str)
    {
        $this->log .= "$str<br />";
    }

    /**
     * Prints out all requests to the server and their responses
     *
     * @access public
     * @return void
     */
    public function getLog()
    {
        return $this->log;
    }
}
?>


Auch wenn der Quellcode sich eigentlich von selbst erklärt, möchte ich noch einmal kurz auf die Methoden getReply() und auth() eingehen.

Die Methode getReply() nimmt, wie der Name schon sagt, die Antwort des Servers entgegen. Da der Server nicht immer sofort antwortet, muss sichergestellt werden, dass die Antwort des Servers auf einen vorangegangenen Befehl auch wirklich abgefangen wird. Dies geschieht indem man immer und immer wieder eine Zeile aus dem Antwort-Buffer des Servers ließt. Da wir wissen, dass eine Antwort nie länger als eine Zeile ist und zumeist nur wenige Zeichen enthält, reicht es wenn wir immer nur 1024 Zeichen einlesen. Die eingelesenen Zeichen überprüfen wir auf ihre Syntax. Entspricht diese der einer Server-Antwort können wir die Schleife verlassen und die Antwort zurückgeben.

Die Methode auth() ermöglicht es dem Benutzer sich mit seinen Zugangsdaten beim Mail-Server zu authentifizieren. Unter Nutzung des SMTP Protokolls sind mehrere Wege der Authentifizierung möglich.

Standardgemäß wird eine PLAIN-Authentifizierung (RFC 4954) verwendet. Hierbei werden Benutzername und Passwort unverschlüsselt übertragen. Die Daten werden lediglich Base64-kodiert.

Alternativ ist eine Authentifizierung über LOGIN möglich. Dabei wird genauso gearbeitet wie in der Plain-Variante, wobei jedoch Passwort und Benutzername getrennt übertragen werden.

Eine weitere Methode der Authentifizierung stellt CRAM-MD5 (RFC 2195) dar, bei der die Daten md5 verschlüsselt übertragen werden. Die Authentifizierung ist im RFC 2195 standardisiert.

Wir haben diese drei eben vorgestellten Methoden der Authentifizierung implementiert. Die vierte mögliche Methode der Übertragung mittels NTLM haben wir außer Acht lassen.

Anwendung der SMTP-Klasse

Abschließend wollen wir uns noch ansehen, wie das Versenden einer Email mit dieser Klasse aussieht.

Code:

<?php

    /* assign header data */
    $date = date('r');
    $header = array(
        'Date' => $date,
        'From' => 'me@testserver.com',
        'Subject' => 'A Testmail',
        'To' => 'friend@aserver.com',
        'X-Mailer' => 'AvedoMailer',
        'Content-Type' => 'text/plain; charset=utf-8');

    /* send mail */
    $smtp = new SmtpConnect('smtp.testserver.net');
    $smtp->connect();
    $smtp->ehlo();
    $smtp->auth('username', 'password', 'PLAIN');
    $smtp->from('me@testserver.com');
    $smtp->rcpt('friend@aserver.com');
    $smtp->data('Just a short message!', $header);
    $smtp->quit();

?>


Das ist doch ganz einfach und es ist uns sogar möglich, den Header der Email so zu erweitern, dass auch HTML-Mails verschickt werden können. Dank dieser Klasse könnten wir nun beispielsweise Bestellbestätigungen oder auch die Bestelldaten an den Besucher bzw. uns selbst senden und das einfach, schnell und unkompliziert.

Fragen und Antworten

Fragen und Antworten findet ihr selbstverständlich bei uns im Forum. Ich möchte euch auch darum bitten, keine Fragen an mich persönlich zu senden aus dem einfachen Grund, dass andere Menschen dann nichts von den gegebenen Antworten haben. Zudem können euch im Forum auch viele andere Leute, die echt Ahnung haben, helfen. Natürlich freue ich mich über ein kurzes Feedback zu diesem Tutorial. Ich bedanke mich für eure Aufmerksamkeit und hoffe, dass euch mein kurzer Exkurs gefallen hat.

Liebe Grüße,

Andreas

http://www.avedo.net

>> Allgemeine Fragen oder Probleme mit dem Tutorial? Hier gehts zum Forum!

Impressum / Datenschutzerklärung          © der-Webdesigner.net 2002 - 2011           top ▲