Tworzenie klienta poczty e-mail IMAP za pomocą PHP

Opublikowany: 2022-03-11

Deweloperzy czasami napotykają zadania wymagające dostępu do skrzynek pocztowych. W większości przypadków odbywa się to za pomocą protokołu IMAP (Internet Message Access Protocol). Jako programista PHP najpierw zwróciłem się do wbudowanej w PHP biblioteki IMAP, ale ta biblioteka jest błędna i niemożliwa do debugowania ani modyfikowania. Nie można również dostosować poleceń IMAP, aby w pełni wykorzystać możliwości protokołu.

Dlatego dzisiaj stworzymy działającego klienta poczty e-mail IMAP od podstaw przy użyciu PHP. Zobaczymy też, jak korzystać ze specjalnych poleceń Gmaila.

Zaimplementujemy IMAP w niestandardowej klasie imap_driver . Każdy krok wyjaśnię podczas budowania klasy. Możesz pobrać cały imap_driver.php na końcu artykułu.

Nawiązywanie połączenia

IMAP jest protokołem opartym na połączeniu i zazwyczaj działa przez TCP/IP z zabezpieczeniami SSL, więc zanim będziemy mogli wykonać jakiekolwiek wywołania IMAP, musimy otworzyć połączenie.

Musimy znać adres URL i numer portu serwera IMAP, z którym chcemy się połączyć. Informacje te są zwykle ogłaszane na stronie internetowej lub w dokumentacji usługi. Na przykład w przypadku Gmaila adres URL to ssl://imap.gmail.com na porcie 993.

Ponieważ chcemy wiedzieć, czy inicjalizacja się powiodła, pozostawimy nasz konstruktor klasy pusty, a wszystkie połączenia zostaną wykonane w niestandardowej metodzie init() , która zwróci false , jeśli połączenie nie może zostać nawiązane:

 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); } ... }

W powyższym kodzie ustawiłem limit czasu na 15 sekund, zarówno dla fsockopen() , aby nawiązać połączenie, jak i dla samego strumienia danych, który odpowiada na żądania po jego otwarciu. Ważne jest, aby mieć limit czasu na każde połączenie do sieci, ponieważ dość często serwer nie odpowiada, a my musimy być w stanie poradzić sobie z takim zawieszeniem.

Chwytam też pierwszą linię strumienia i ignoruję ją. Zwykle jest to po prostu wiadomość powitalna z serwera lub potwierdzenie połączenia. Sprawdź dokumentację konkretnej usługi pocztowej, aby upewnić się, że tak właśnie jest.

Teraz chcemy uruchomić powyższy kod, aby zobaczyć, że init() się powiodło:

 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; }

Podstawowa składnia IMAP

Teraz, gdy mamy aktywne gniazdo otwarte dla naszego serwera IMAP, możemy zacząć wysyłać polecenia IMAP. Przyjrzyjmy się składni IMAP.

Oficjalną dokumentację można znaleźć w Internet Engineering Task Force (IETF) RFC3501. Interakcje IMAP zazwyczaj składają się z wysyłania poleceń przez klienta i odpowiedzi serwera ze wskazaniem powodzenia, wraz z wszelkimi żądanymi danymi.

Podstawowa składnia poleceń to:

 line_number command arg1 arg2 ...

Numer wiersza lub „znacznik” jest unikalnym identyfikatorem polecenia, którego serwer używa do wskazania, na które polecenie odpowiada, jeśli ma przetwarzać wiele poleceń jednocześnie.

Oto przykład pokazujący polecenie LOGIN :

 00000001 LOGIN [email protected] password

Odpowiedź serwera może zaczynać się od „nieotagowanej” odpowiedzi danych. Na przykład Gmail odpowiada na pomyślne logowanie nieotagowaną odpowiedzią zawierającą informacje o możliwościach i opcjach serwera, a polecenie pobrania wiadomości e-mail spowoduje otrzymanie nieotagowanej odpowiedzi zawierającej treść wiadomości. W obu przypadkach odpowiedź powinna zawsze kończyć się „oznakowanym” wierszem odpowiedzi na zakończenie polecenia, określając numer wiersza polecenia, którego dotyczy odpowiedź, wskaźnik stanu ukończenia oraz dodatkowe metadane dotyczące polecenia, jeśli takie istnieją:

 line_number status metadata1 metadata2 ...

Oto jak Gmail odpowiada na polecenie LOGIN :

  • Sukces:
 * 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)
  • Niepowodzenie:
 00000001 NO [AUTHENTICATIONFAILED] Invalid credentials (Failure)

Status może być OK , co oznacza sukces, NO , co oznacza niepowodzenie, lub BAD , co oznacza nieprawidłowe polecenie lub złą składnię.

Implementacja podstawowych poleceń:

Stwórzmy funkcję, która wyśle ​​polecenie do serwera IMAP i pobierze odpowiedź oraz linię końcową:

 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); } ... }

Polecenie LOGIN

Teraz możemy napisać funkcje dla konkretnych poleceń, które wywołują naszą funkcję command() pod maską. Napiszmy funkcję dla polecenia LOGIN :

 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; } } ... }

Teraz możemy to przetestować w ten sposób. (Pamiętaj, że musisz mieć aktywne konto e-mail, aby przeprowadzić test).

 ... // test for login() if ($imap_driver->login('[email protected]', 'password') === false) { echo "login() failed: " . $imap_driver->error . "\n"; exit; }

Pamiętaj, że Gmail domyślnie bardzo rygorystycznie podchodzi do kwestii bezpieczeństwa: nie pozwoli nam uzyskać dostępu do konta e-mail za pomocą protokołu IMAP, jeśli mamy ustawienia domyślne i spróbujemy uzyskać do niego dostęp z kraju innego niż kraj profilu konta. Ale łatwo to naprawić; po prostu ustaw mniej bezpieczne ustawienia na swoim koncie Gmail, jak opisano tutaj.

Polecenie SELECT

Zobaczmy teraz, jak wybrać folder IMAP, aby zrobić coś przydatnego z naszym e-mailem. Składnia jest podobna do tej z LOGIN , dzięki naszej metodzie command() . Zamiast tego używamy polecenia SELECT i określamy folder.

 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; } } ... }

Aby to przetestować, spróbujmy wybrać INBOX:

 ... // test for select_folder() if ($imap_driver->select_folder("INBOX") === false) { echo "select_folder() failed: " . $imap_driver->error . "\n"; return false; }

Wdrażanie zaawansowanych poleceń

Przyjrzyjmy się, jak zaimplementować kilka bardziej zaawansowanych poleceń IMAP.

Polecenie SEARCH

Częstą rutyną w analizie wiadomości e-mail jest wyszukiwanie wiadomości e-mail w określonym zakresie dat lub wyszukiwanie oflagowanych wiadomości e-mail i tak dalej. Kryteria wyszukiwania muszą być przekazane do polecenia SEARCH jako argument, ze spacją jako separatorem. Na przykład, jeśli chcemy otrzymywać wszystkie e-maile od 20 listopada 2015 r., musimy przekazać następujące polecenie:

 00000005 SEARCH SINCE 20-Nov-2015

A odpowiedź będzie mniej więcej taka:

 * SEARCH 881 882 00000005 OK SEARCH completed

Szczegółową dokumentację możliwych terminów wyszukiwania można znaleźć tutaj. Wynikiem polecenia SEARCH jest lista UID wiadomości e-mail oddzielonych białymi znakami. UID to unikalny identyfikator wiadomości e-mail na koncie użytkownika, w porządku chronologicznym, gdzie 1 to najstarsza wiadomość e-mail. Aby zaimplementować polecenie SEARCH , musimy po prostu zwrócić wynikowe UID:

 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; } } ... }

Aby przetestować to polecenie, otrzymamy e-maile z ostatnich trzech dni:

 ... // 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; }

Polecenie FETCH z BODY.PEEK

Innym częstym zadaniem jest pobieranie nagłówków wiadomości e-mail bez oznaczania wiadomości jako SEEN . Z podręcznika IMAP polecenie do pobrania całości lub części wiadomości e-mail to FETCH . Pierwszy argument wskazuje, którą część nas interesuje, i zazwyczaj przekazywane jest BODY , co zwróci całą wiadomość wraz z jej nagłówkami i oznaczy ją jako SEEN . Alternatywny argument BODY.PEEK zrobi to samo, bez oznaczania wiadomości jako SEEN .

Składnia IMAP wymaga, aby nasze żądanie również określiło w nawiasach kwadratowych sekcję wiadomości e-mail, którą chcemy pobrać, czyli w tym przykładzie [HEADER] . W rezultacie nasze polecenie będzie wyglądać tak:

 00000006 FETCH 2 BODY.PEEK[HEADER]

I będziemy oczekiwać odpowiedzi, która wygląda tak:

 * 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 Success

Aby zbudować funkcję do pobierania nagłówków, musimy mieć możliwość zwrócenia odpowiedzi w strukturze hash (pary klucz/wartość):

 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; } } ... }

Aby przetestować ten kod, po prostu określamy UID wiadomości, która nas interesuje:

 ... // 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; }

Rozszerzenia Gmail IMAP

Gmail udostępnia listę specjalnych poleceń, które mogą znacznie ułatwić nam życie. Lista poleceń rozszerzenia IMAP Gmaila jest dostępna tutaj. Przyjrzyjmy się komendzie, która moim zdaniem jest najważniejsza: X-GM-RAW . Pozwala nam używać składni wyszukiwania Gmaila z protokołem IMAP. Na przykład możemy wyszukiwać e-maile z kategorii Główne, Społeczności, Promocje, Aktualizacje lub Fora.

Funkcjonalnie X-GM-RAW jest rozszerzeniem polecenia SEARCH , więc możemy ponownie użyć kodu, który mamy powyżej dla polecenia SEARCH . Wystarczy dodać słowo kluczowe X-GM-RAW i kryteria:

 ... // 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; }

Powyższy kod zwróci wszystkie UID, które są wymienione w kategorii „Podstawowe”.

Uwaga: od grudnia 2015 r. Gmail często myli kategorię „Podstawowa” z kategorią „Aktualizacje” na niektórych kontach. To jest błąd Gmaila, który nie został jeszcze naprawiony.

Wniosek

Masz maila. Co teraz? Przeczytaj, jak zbudować niestandardowego klienta poczty e-mail IMAP w PHP i sprawdź pocztę na swoich warunkach.
Ćwierkać

Ogólnie rzecz biorąc, niestandardowe podejście do gniazd zapewnia programistom większą swobodę. Umożliwia implementację wszystkich poleceń w IMAP RFC3501. Zapewni to również lepszą kontrolę nad kodem, ponieważ nie musisz się zastanawiać, co dzieje się „za kulisami”.

Pełną klasę imap_driver , którą zaimplementowaliśmy w tym artykule, można znaleźć tutaj. Może być używany bez zmian, a napisanie nowej funkcji lub żądania do serwera IMAP zajmie programiście tylko kilka minut. Dołączyłem również funkcję debugowania do klasy, aby uzyskać szczegółowe dane wyjściowe.