Erstellen eines IMAP-E-Mail-Clients mit PHP
Veröffentlicht: 2022-03-11Entwickler stoßen manchmal auf Aufgaben, die Zugriff auf E-Mail-Postfächer erfordern. In den meisten Fällen erfolgt dies über das Internet Message Access Protocol oder IMAP. Als PHP-Entwickler habe ich mich zuerst der in PHP integrierten IMAP-Bibliothek zugewandt, aber diese Bibliothek ist fehlerhaft und kann nicht debuggt oder geändert werden. Es ist auch nicht möglich, IMAP-Befehle anzupassen, um die Fähigkeiten des Protokolls voll auszuschöpfen.
Deshalb werden wir heute einen funktionierenden IMAP-E-Mail-Client von Grund auf mit PHP erstellen. Wir werden auch sehen, wie man die speziellen Befehle von Google Mail verwendet.
Wir implementieren IMAP in einer benutzerdefinierten Klasse, imap_driver . Ich werde jeden Schritt beim Aufbau der Klasse erklären. Sie können die gesamte imap_driver.php am Ende des Artikels herunterladen.
Herstellen einer Verbindung
IMAP ist ein verbindungsbasiertes Protokoll und arbeitet normalerweise über TCP/IP mit SSL-Sicherheit. Bevor wir also IMAP-Aufrufe tätigen können, müssen wir die Verbindung öffnen.
Wir müssen die URL und die Portnummer des IMAP-Servers kennen, mit dem wir uns verbinden möchten. Diese Informationen werden normalerweise auf der Website oder Dokumentation des Dienstes beworben. Für Google Mail lautet die URL beispielsweise ssl://imap.gmail.com auf Port 993.
Da wir wissen wollen, ob die Initialisierung erfolgreich war, lassen wir unseren Klassenkonstruktor leer, und alle Verbindungen werden in einer benutzerdefinierten init() -Methode hergestellt, die false , wenn die Verbindung nicht hergestellt werden kann:
class imap_driver { private $fp; // file pointer public $error; // error message ... public function init($host, $port) { if (!($this->fp = fsockopen($host, $port, $errno, $errstr, 15))) { $this->error = "Could not connect to host ($errno) $errstr"; return false; } if (!stream_set_timeout($this->fp, 15)) { $this->error = "Could not set timeout"; return false; } $line = fgets($this->fp); // discard the first line of the stream return true; } private function close() { fclose($this->fp); } ... } Im obigen Code habe ich ein Timeout von 15 Sekunden festgelegt, sowohl für fsockopen() zum Herstellen der Verbindung als auch für den Datenstrom selbst, um auf Anfragen zu antworten, sobald er geöffnet ist. Es ist wichtig, für jeden Aufruf des Netzwerks eine Zeitüberschreitung zu haben, da der Server oft genug nicht antwortet, und wir müssen in der Lage sein, mit einem solchen Einfrieren fertig zu werden.
Ich schnappe mir auch die erste Zeile des Streams und ignoriere sie. Normalerweise ist dies nur eine Begrüßungsnachricht vom Server oder eine Bestätigung, dass er verbunden ist. Sehen Sie in der Dokumentation Ihres jeweiligen E-Mail-Dienstes nach, um sicherzustellen, dass dies der Fall ist.
Jetzt wollen wir den obigen Code ausführen, um zu sehen, dass init() erfolgreich ist:
include("imap_driver.php"); // test for init() $imap_driver = new imap_driver(); if ($imap_driver->init('ssl://imap.gmail.com', 993) === false) { echo "init() failed: " . $imap_driver->error . "\n"; exit; }Grundlegende IMAP-Syntax
Jetzt, da wir einen aktiven Socket zu unserem IMAP-Server geöffnet haben, können wir mit dem Senden von IMAP-Befehlen beginnen. Werfen wir einen Blick auf die IMAP-Syntax.
Die formale Dokumentation finden Sie in Internet Engineering Task Force (IETF) RFC3501. IMAP-Interaktionen bestehen normalerweise darin, dass der Client Befehle sendet und der Server mit einer Erfolgsmeldung antwortet, zusammen mit allen angeforderten Daten.
Die grundlegende Syntax für Befehle lautet:
line_number command arg1 arg2 ...Die Zeilennummer oder „Tag“ ist eine eindeutige Kennung für den Befehl, die der Server verwendet, um anzugeben, auf welchen Befehl er antwortet, falls er mehrere Befehle gleichzeitig verarbeitet.
Hier ist ein Beispiel, das den LOGIN Befehl zeigt:
00000001 LOGIN [email protected] passwordDie Antwort des Servers kann mit einer „unmarkierten“ Datenantwort beginnen. Beispielsweise antwortet Google Mail auf eine erfolgreiche Anmeldung mit einer Antwort ohne Tags, die Informationen über die Fähigkeiten und Optionen des Servers enthält, und ein Befehl zum Abrufen einer E-Mail-Nachricht erhält eine Antwort ohne Tags, die den Nachrichtentext enthält. In jedem Fall sollte eine Antwort immer mit einer „getaggten“ Antwortzeile zur Befehlsvervollständigung enden, die die Zeilennummer des Befehls angibt, auf den sich die Antwort bezieht, eine Statusanzeige für die Vervollständigung und zusätzliche Metadaten über den Befehl, falls vorhanden:
line_number status metadata1 metadata2 ... So antwortet Gmail auf den LOGIN Befehl:
- Erfolg:
* CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 UIDPLUS COMPRESS=DEFLATE ENABLE MOVE CONDSTORE ESEARCH UTF8=ACCEPT LIST-EXTENDED LIST-STATUS 00000001 OK [email protected] authenticated (Success)- Versagen:
00000001 NO [AUTHENTICATIONFAILED] Invalid credentials (Failure) Der Status kann entweder OK sein, was auf Erfolg hinweist, NO , was auf einen Fehler hinweist, oder BAD , was auf einen ungültigen Befehl oder eine schlechte Syntax hinweist.
Grundlegende Befehle implementieren:
Lassen Sie uns eine Funktion erstellen, um einen Befehl an den IMAP-Server zu senden und die Antwort und die Endzeile abzurufen:
class imap_driver { private $command_counter = "00000001"; public $last_response = array(); public $last_endline = ""; private function command($command) { $this->last_response = array(); $this->last_endline = ""; fwrite($this->fp, "$this->command_counter $command\r\n"); // send the command while ($line = fgets($this->fp)) { // fetch the response one line at a time $line = trim($line); // trim the response $line_arr = preg_split('/\s+/', $line, 0, PREG_SPLIT_NO_EMPTY); // split the response into non-empty pieces by whitespace if (count($line_arr) > 0) { $code = array_shift($line_arr); // take the first segment from the response, which will be the line number if (strtoupper($code) == $this->command_counter) { $this->last_endline = join(' ', $line_arr); // save the completion response line to parse later break; } else { $this->last_response[] = $line; // append the current line to the saved response } } else { $this->last_response[] = $line; } } $this->increment_counter(); } private function increment_counter() { $this->command_counter = sprintf('%08d', intval($this->command_counter) + 1); } ... } Der LOGIN Befehl
Jetzt können wir Funktionen für bestimmte Befehle schreiben, die unsere command() Funktion unter der Haube aufrufen. Schreiben wir eine Funktion für den LOGIN -Befehl:
class imap_driver { ... public function login($login, $pwd) { $this->command("LOGIN $login $pwd"); if (preg_match('~^OK~', $this->last_endline)) { return true; } else { $this->error = join(', ', $this->last_response); $this->close(); return false; } } ... }Jetzt können wir es so testen. (Beachten Sie, dass Sie zum Testen über ein aktives E-Mail-Konto verfügen müssen.)
... // test for login() if ($imap_driver->login('[email protected]', 'password') === false) { echo "login() failed: " . $imap_driver->error . "\n"; exit; }Beachten Sie, dass Google Mail standardmäßig sehr streng auf Sicherheit achtet: Es erlaubt uns nicht, mit IMAP auf ein E-Mail-Konto zuzugreifen, wenn wir Standardeinstellungen haben und versuchen, von einem anderen Land als dem Land des Kontoprofils darauf zuzugreifen. Aber es ist leicht genug zu beheben; Legen Sie einfach weniger sichere Einstellungen in Ihrem Gmail-Konto fest, wie hier beschrieben.

Der SELECT Befehl
Sehen wir uns nun an, wie Sie einen IMAP-Ordner auswählen, um etwas Nützliches mit unserer E-Mail zu tun. Dank unserer Methode command() ähnelt die Syntax der von LOGIN . Wir verwenden stattdessen den SELECT Befehl und geben den Ordner an.
class imap_driver { ... public function select_folder($folder) { $this->command("SELECT $folder"); if (preg_match('~^OK~', $this->last_endline)) { return true; } else { $this->error = join(', ', $this->last_response); $this->close(); return false; } } ... }Um es zu testen, versuchen wir, den Posteingang auszuwählen:
... // test for select_folder() if ($imap_driver->select_folder("INBOX") === false) { echo "select_folder() failed: " . $imap_driver->error . "\n"; return false; }Erweiterte Befehle implementieren
Schauen wir uns an, wie einige der fortgeschritteneren Befehle von IMAP implementiert werden.
Der SEARCH Befehl
Eine gängige Routine bei der E-Mail-Analyse ist die Suche nach E-Mails in einem bestimmten Datumsbereich oder die Suche nach gekennzeichneten E-Mails usw. Die Suchkriterien müssen als Argument mit Leerzeichen als Trennzeichen an den SEARCH -Befehl übergeben werden. Wenn wir beispielsweise alle E-Mails seit dem 20. November 2015 erhalten möchten, müssen wir den folgenden Befehl übergeben:
00000005 SEARCH SINCE 20-Nov-2015Und die Antwort wird ungefähr so aussehen:
* SEARCH 881 882 00000005 OK SEARCH completed Eine ausführliche Dokumentation möglicher Suchbegriffe finden Sie hier. Die Ausgabe eines SEARCH -Befehls ist eine durch Leerzeichen getrennte Liste von UIDs von E-Mails. Eine UID ist eine eindeutige Kennung einer E-Mail im Benutzerkonto in chronologischer Reihenfolge, wobei 1 die älteste E-Mail ist. Um den SEARCH Befehl zu implementieren, müssen wir einfach die resultierenden UIDs zurückgeben:
class imap_driver { ... public function get_uids_by_search($criteria) { $this->command("SEARCH $criteria"); if (preg_match('~^OK~', $this->last_endline) && is_array($this->last_response) && count($this->last_response) == 1) { $splitted_response = explode(' ', $this->last_response[0]); $uids = array(); foreach ($splitted_response as $item) { if (preg_match('~^\d+$~', $item)) { $uids[] = $item; // put the returned UIDs into an array } } return $uids; } else { $this->error = join(', ', $this->last_response); $this->close(); return false; } } ... }Um diesen Befehl zu testen, erhalten wir E-Mails der letzten drei Tage:
... // test for get_uids_by_search() $ids = $imap_driver->get_uids_by_search('SINCE ' . date('jM-Y', time() - 60 * 60 * 24 * 3)); if ($ids === false) { echo "get_uids_failed: " . $imap_driver->error . "\n"; exit; } Der FETCH -Befehl mit BODY.PEEK
Eine weitere häufige Aufgabe besteht darin, E-Mail-Header abzurufen, ohne eine E-Mail als SEEN zu markieren. Aus dem IMAP-Handbuch geht hervor, dass der Befehl zum Abrufen der gesamten oder eines Teils einer E-Mail FETCH lautet. Das erste Argument gibt an, an welchem Teil wir interessiert sind, und normalerweise wird BODY übergeben, wodurch die gesamte Nachricht zusammen mit ihren Headern zurückgegeben und als SEEN wird. Das alternative Argument BODY.PEEK macht dasselbe, ohne die Nachricht als SEEN zu markieren.
Die IMAP-Syntax erfordert, dass unsere Anfrage in eckigen Klammern auch den Abschnitt der E-Mail angibt, den wir abrufen möchten, in diesem Beispiel [HEADER] . Als Ergebnis sieht unser Befehl folgendermaßen aus:
00000006 FETCH 2 BODY.PEEK[HEADER]Und wir erwarten eine Antwort, die so aussieht:
* 2 FETCH (BODY[HEADER] {438} MIME-Version: 1.0 x-no-auto-attachment: 1 Received: by 10.170.97.214; Fri, 30 May 2014 09:13:45 -0700 (PDT) Date: Fri, 30 May 2014 09:13:45 -0700 Message-ID: <CACYy8gU+UFFukbE0Cih8kYRENMXcx1DTVhvg3TBbJ52D8OF6nQ@mail.gmail.com> Subject: The best of Gmail, wherever you are From: Gmail Team <[email protected]> To: Example Test <[email protected]> Content-Type: multipart/alternative; boundary=001a1139e3966e26ed04faa054f4 ) 00000006 OK SuccessUm eine Funktion zum Abrufen von Headern zu erstellen, müssen wir in der Lage sein, die Antwort in einer Hash-Struktur (Schlüssel/Wert-Paare) zurückzugeben:
class imap_driver { ... public function get_headers_from_uid($uid) { $this->command("FETCH $uid BODY.PEEK[HEADER]"); if (preg_match('~^OK~', $this->last_endline)) { array_shift($this->last_response); // skip the first line $headers = array(); $prev_match = ''; foreach ($this->last_response as $item) { if (preg_match('~^([az][a-z0-9-_]+):~is', $item, $match)) { $header_name = strtolower($match[1]); $prev_match = $header_name; $headers[$header_name] = trim(substr($item, strlen($header_name) + 1)); } else { $headers[$prev_match] .= " " . $item; } } return $headers; } else { $this->error = join(', ', $this->last_response); $this->close(); return false; } } ... }Und um diesen Code zu testen, geben wir einfach die UID der Nachricht an, an der wir interessiert sind:
... // test for get_headers_by_uid if (($headers = $imap_driver->get_headers_from_uid(2)) === false) { echo "get_headers_by_uid() failed: " . $imap_driver->error . "\n"; return false; }Google Mail-IMAP-Erweiterungen
Google Mail bietet eine Liste mit speziellen Befehlen, die unser Leben viel einfacher machen können. Die Liste der IMAP-Erweiterungsbefehle von Google Mail ist hier verfügbar. Lassen Sie uns einen Befehl wiederholen, der meiner Meinung nach der wichtigste ist: X-GM-RAW . Es ermöglicht uns, die Gmail-Suchsyntax mit IMAP zu verwenden. Beispielsweise können wir nach E-Mails suchen, die in den Kategorien „Primär“, „Soziale Netzwerke“, „Aktionen“, „Updates“ oder „Foren“ enthalten sind.
Funktional ist X-GM-RAW eine Erweiterung des SEARCH -Befehls, sodass wir den obigen Code für den SEARCH -Befehl wiederverwenden können. Alles, was wir tun müssen, ist das Schlüsselwort X-GM-RAW und Kriterien hinzuzufügen:
... // test for gmail extended search functionality $ids = $imap_driver->get_uids_by_search(' X-GM-RAW "category:primary"'); if ($ids === false) { echo "get_uids_failed: " . $imap_driver->error . "\n"; return false; }Der obige Code gibt alle UIDs zurück, die in der Kategorie „Primär“ aufgeführt sind.
Hinweis: Seit Dezember 2015 verwechselt Google Mail bei einigen Konten häufig die Kategorie „Primär“ mit der Kategorie „Updates“. Dies ist ein Google Mail-Fehler, der noch nicht behoben wurde.
Fazit
Insgesamt bietet der benutzerdefinierte Socket-Ansatz dem Entwickler mehr Freiheit. Es ermöglicht die Implementierung aller Befehle in IMAP RFC3501. Es gibt Ihnen auch eine bessere Kontrolle über Ihren Code, da Sie sich nicht fragen müssen, was „hinter den Kulissen“ passiert.
Die vollständige imap_driver -Klasse, die wir in diesem Artikel implementiert haben, finden Sie hier. Es kann so verwendet werden, wie es ist, und es dauert nur wenige Minuten, bis ein Entwickler eine neue Funktion oder Anfrage an seinen IMAP-Server geschrieben hat. Ich habe auch eine Debug-Funktion in die Klasse für eine ausführliche Ausgabe aufgenommen.
