Construirea unui client de e-mail IMAP cu PHP

Publicat: 2022-03-11

Dezvoltatorii se confruntă uneori cu sarcini care necesită acces la cutiile poștale de e-mail. În cele mai multe cazuri, acest lucru se face folosind protocolul de acces la mesaje Internet sau IMAP. În calitate de dezvoltator PHP, am apelat mai întâi la biblioteca PHP încorporată în IMAP, dar această bibliotecă este greșită și imposibil de depanat sau modificat. De asemenea, nu este posibilă personalizarea comenzilor IMAP pentru a utiliza pe deplin abilitățile protocolului.

Așadar, astăzi, vom crea un client de e-mail IMAP funcțional, folosind PHP. Vom vedea, de asemenea, cum să folosim comenzile speciale ale Gmail.

Vom implementa IMAP într-o clasă personalizată, imap_driver . Voi explica fiecare pas în timp ce construiesc clasa. Puteți descărca întregul imap_driver.php la sfârșitul articolului.

Stabilirea unei conexiuni

IMAP este un protocol bazat pe conexiune și funcționează de obicei prin TCP/IP cu securitate SSL, așa că înainte de a putea efectua apeluri IMAP trebuie să deschidem conexiunea.

Trebuie să știm adresa URL și numărul portului serverului IMAP la care dorim să ne conectăm. Aceste informații sunt de obicei publicate pe site-ul web sau în documentația serviciului. De exemplu, pentru Gmail, adresa URL este ssl://imap.gmail.com pe portul 993.

Deoarece vrem să știm dacă inițializarea a avut succes, vom lăsa constructorul nostru de clasă gol și toate conexiunile se vor face într-o metodă personalizată init() , care va returna false dacă conexiunea nu poate fi stabilită:

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

În codul de mai sus, am setat un timeout de 15 secunde, atât pentru fsockopen() pentru a stabili conexiunea, cât și pentru fluxul de date în sine să răspundă la solicitări odată ce este deschis. Este important să avem un timeout pentru fiecare apel către rețea, deoarece, destul de des, serverul nu va răspunde și trebuie să fim capabili să facem față unei astfel de blocări.

Iau și prima linie a fluxului și o ignor. De obicei, acesta este doar un mesaj de salut de la server sau o confirmare că este conectat. Verificați documentația serviciului dumneavoastră de corespondență pentru a vă asigura că acesta este cazul.

Acum vrem să rulăm codul de mai sus pentru a vedea că init() are succes:

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

Sintaxa IMAP de bază

Acum că avem un socket activ deschis pentru serverul nostru IMAP, putem începe să trimitem comenzi IMAP. Să aruncăm o privire la sintaxa IMAP.

Documentația oficială poate fi găsită în Internet Engineering Task Force (IETF) RFC3501. Interacțiunile IMAP constau, de obicei, în trimiterea de comenzi de către client, iar serverul răspunde cu o indicație de succes, împreună cu orice date ar fi putut fi solicitate.

Sintaxa de bază pentru comenzi este:

 line_number command arg1 arg2 ...

Numărul de linie, sau „etichetă”, este un identificator unic pentru comandă, pe care serverul îl folosește pentru a indica la ce comandă răspunde în cazul în care procesează mai multe comenzi simultan.

Iată un exemplu, care arată comanda LOGIN :

 00000001 LOGIN [email protected] password

Răspunsul serverului poate începe cu un răspuns de date „neetichetat”. De exemplu, Gmail răspunde la o conectare reușită cu un răspuns neetichetat care conține informații despre capacitățile și opțiunile serverului, iar o comandă pentru a prelua un mesaj de e-mail va primi un răspuns neetichetat care conține corpul mesajului. În ambele cazuri, un răspuns ar trebui să se încheie întotdeauna cu o linie de răspuns de finalizare a comenzii „etichetată”, identificând numărul de linie al comenzii căreia i se aplică răspunsul, un indicator de stare de finalizare și metadate suplimentare despre comandă, dacă există:

 line_number status metadata1 metadata2 ...

Iată cum răspunde Gmail la comanda LOGIN :

  • Succes:
 * 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)
  • Eșec:
 00000001 NO [AUTHENTICATIONFAILED] Invalid credentials (Failure)

Starea poate fi fie OK , indicând succes, NO , indicând eșec, fie BAD , indicând o comandă invalidă sau o sintaxă greșită.

Implementarea comenzilor de bază:

Să facem o funcție pentru a trimite o comandă către serverul IMAP și să recuperăm răspunsul și linia finală:

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

Comanda LOGIN

Acum putem scrie funcții pentru anumite comenzi care apelează funcția noastră command() sub capotă. Să scriem o funcție pentru comanda 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; } } ... }

Acum îl putem testa așa. (Rețineți că trebuie să aveți un cont de e-mail activ pentru a testa.)

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

Rețineți că Gmail este foarte strict în ceea ce privește securitatea în mod implicit: nu ne va permite să accesăm un cont de e-mail cu IMAP dacă avem setări implicite și încercăm să îl accesăm dintr-o altă țară decât țara profilului contului. Dar este destul de ușor de reparat; trebuie doar să setați setări mai puțin sigure în contul dvs. Gmail, așa cum este descris aici.

Comanda SELECT

Acum să vedem cum să selectăm un folder IMAP pentru a face ceva util cu e-mailul nostru. Sintaxa este similară cu cea a LOGIN , datorită metodei noastre command() . În schimb, folosim comanda SELECT și specificăm folderul.

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

Pentru a-l testa, să încercăm să selectăm INBOX:

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

Implementarea comenzilor avansate

Să vedem cum să implementăm câteva dintre comenzile mai avansate ale IMAP.

Comanda SEARCH

O rutină obișnuită în analiza e-mailurilor este căutarea e-mailurilor într-un anumit interval de date sau căutarea e-mailurilor semnalate și așa mai departe. Criteriile de căutare trebuie transmise comenzii SEARCH ca argument, cu spațiu ca separator. De exemplu, dacă dorim să primim toate e-mailurile începând cu 20 noiembrie 2015, trebuie să transmitem următoarea comandă:

 00000005 SEARCH SINCE 20-Nov-2015

Și răspunsul va fi cam așa:

 * SEARCH 881 882 00000005 OK SEARCH completed

Documentația detaliată a termenilor posibili de căutare poate fi găsită aici. Rezultatul unei comenzi SEARCH este o listă de UID-uri ale e-mailurilor, separate prin spații albe. Un UID este un identificator unic al unui e-mail din contul utilizatorului, în ordine cronologică, unde 1 este cel mai vechi e-mail. Pentru a implementa comanda SEARCH trebuie pur și simplu să returnăm UID-urile rezultate:

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

Pentru a testa această comandă, vom primi e-mailuri din ultimele trei zile:

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

Comanda FETCH cu BODY.PEEK

O altă sarcină comună este să obțineți anteturi de e-mail fără a marca un e-mail ca SEEN . Din manualul IMAP, comanda pentru preluarea integrală sau parțială a unui e-mail este FETCH . Primul argument indică ce parte ne interesează și, de obicei, se trece BODY , care va returna întregul mesaj împreună cu anteturile sale și îl va marca ca SEEN . Argumentul alternativ BODY.PEEK va face același lucru, fără a marca mesajul ca SEEN .

Sintaxa IMAP necesită ca solicitarea noastră să specificăm, de asemenea, între paranteze drepte, secțiunea de e-mail pe care dorim să o preluam, care în acest exemplu este [HEADER] . Ca rezultat, comanda noastră va arăta astfel:

 00000006 FETCH 2 BODY.PEEK[HEADER]

Și ne așteptăm la un răspuns care arată astfel:

 * 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

Pentru a construi o funcție pentru preluarea antetelor, trebuie să fim capabili să returnăm răspunsul într-o structură hash (perechi cheie/valoare):

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

Și pentru a testa acest cod trebuie doar să specificăm UID-ul mesajului care ne interesează:

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

Extensii IMAP Gmail

Gmail oferă o listă de comenzi speciale care ne pot face viața mult mai ușoară. Lista comenzilor de extensie IMAP din Gmail este disponibilă aici. Să trecem în revistă o comandă care, după părerea mea, este cea mai importantă: X-GM-RAW . Ne permite să folosim sintaxa de căutare Gmail cu IMAP. De exemplu, putem căuta e-mailuri care se află în categoriile Principal, Social, Promoții, Actualizări sau Forumuri.

Din punct de vedere funcțional, X-GM-RAW este o extensie a comenzii SEARCH , astfel încât să putem reutiliza codul pe care îl avem mai sus pentru comanda SEARCH . Tot ce trebuie să facem este să adăugăm cuvântul cheie X-GM-RAW și criteriile:

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

Codul de mai sus va returna toate UID-urile care sunt listate în categoria „Primar”.

Notă: din decembrie 2015, Gmail confundă adesea categoria „Principală” cu categoria „Actualizări” pentru unele conturi. Aceasta este o eroare Gmail care nu a fost încă remediată.

Concluzie

Aveți corespondență. Acum ce? Citiți cum să creați un client de e-mail IMAP personalizat în PHP și verificați e-mailul în condițiile dvs.
Tweet

În general, abordarea socket personalizat oferă mai multă libertate dezvoltatorului. Face posibilă implementarea tuturor comenzilor în IMAP RFC3501. De asemenea, vă va oferi un control mai bun asupra codului dvs., deoarece nu trebuie să vă întrebați ce se întâmplă „în culise”.

Clasa completă imap_driver pe care am implementat-o ​​în acest articol poate fi găsită aici. Poate fi folosit ca atare și va dura doar câteva minute pentru ca un dezvoltator să scrie o nouă funcție sau o cerere pe serverul său IMAP. Am inclus, de asemenea, o caracteristică de depanare în clasă pentru o ieșire detaliată.