Autor: Tomasz Jędrzejewski
Data publikacji: 18.12.2003, 17:52 | Ostatnia modyfikacja: 02.11.2006, 16:57
Artykuł ten demonstruje, jak napisać klienta poczty elektronicznej korzystającego z protokołu IMAP w PHP. Zawiera także wprowadzenie do modułu "imap".
Zapewne wiele razy patrzyÅ‚eÅ› z zazdroÅ›ciÄ… na interfejsy WWW do zarzÄ…dzania kontem pocztowym. Ba - być może nawet marzyÅ‚eÅ› o stworzeniu wÅ‚asnego? Tylko jak siÄ™ do tego zabrać, co jest potrzebne? Otóż do stworzenia takiego czegoÅ› potrzeba PHP, odrobiny inwencji, oraz rozszerzenia IMAP. Tak oto witam w kolejnym artykule z serii "Piszemy coÅ›tam", tym razem biorÄ…cym pod ostrzaÅ‚ klienty poczty e-mail...
Na poczÄ…tku musimy sobie zadać ważne pytanie: jak to ma wyglÄ…dać? ZdecydowaÅ‚em siÄ™ na stworzenie klienta, który:
Wiemy już, że do tego zadania wykorzystamy protokóÅ‚ pocztowy IMAP, oraz moduÅ‚ PHP pozwalajÄ…cy siÄ™ poprzez niego komunikować. Mocno zalecam także pobranie jakiegoÅ› serwera poczty e-mail, by móc spokojnie testować omawiany tu kod.
W systemach Windows jest ona (jak zwykle :)) bajecznie prosta - wystarczy otworzyć plik php.ini i usunąć średnik sprzed linii
;extension = php_imap.dll
pod Unixem należy zrekompilować PHP z opcjami
--with-imap=sciezka_do_biblioteki_klienckiej
Jak zwykle, w Unixie musimy mieć gdzieś odpowiednią bibliotekę. Tym razem odeślę do manuala PHP, gdzie opisany jest proces kompilacji jednej z nich.
Należy pamiÄ™tać, że moduÅ‚u IMAP w żadnym wypadku nie wolno nam używać razem z moduÅ‚ami Recode i YAZ, gdyż używajÄ… one tej samej sygnatury i może to powodować wiele problemów.
Aby nie mieć problemów z prezentowanym w dalszej części artykuÅ‚u kodem PHP, zalecam pobranie jakiegokolwiek serwera IMAP. JeÅ›li pracujesz na systemie UNIX/Linux, możesz sprawdzić ten adres: http://www.inter7.com/courierimap.html. Ja jednak piszÄ™ to wszystko w oparciu o system Windows, wiÄ™c znajdzie siÄ™ tu opis instalacji Windowsowego klienta. Jest nim MercuryMail, którego można pobrać ze strony http://www.pmail.com/. Instalacja jest prosta i wÅ‚aÅ›nie dlatego jÄ… opiszÄ™ :).
Uruchom pobrany plik i kliknij przycisk "Setup". Po rozpakowaniu archiwum i uruchomieniu programu instalacyjnego wskaż "New Installation". Zostaniesz zapytany o to, czy chcesz integracji z systemami Novell NetWare. Jak ktoÅ› wie, o co chodzi, to intuicja sama mu podpowie, co wybrać, natomiast reszta powinna kliknąć przycisk "No NetWare support". Kolejnym krokiem jest wskazanie katalogu, do którego ma zostać zainstalowany program. Po klikniÄ™ciu na "OK" program zapyta siÄ™ o... nastÄ™pnÄ… Å›cieżkÄ™ instalacyjnÄ… :). Jednak podejrzewam, że nie posiadasz programu "Pegasus Mail", a wiÄ™c nie musisz integrować z nim Mercurego. Kliknij wiÄ™c na "No Pegasus Mail integration", by powiedzieć o tym instalatorowi. NastÄ™pnie kolejna Å›cieżka, na nasze potrzeby wystarczy zaakceptować domyÅ›lnÄ…. Teraz zostaniesz zaprowadzony do bardzo ważnego okna, a mianowicie do wyboru protokoÅ‚ów i możliwoÅ›ci, które chcesz mieć aktywne. Nam wystarczÄ… "SMTP server module" i "IMAP4rev1 module" (na dole). BÄ™dzie trzeba jeszcze wybrać jakieÅ› duperszmity zwiÄ…zane z optymalizacjÄ… korzystania z SMTP, ale nam to niepotrzebne, wiÄ™c wybieramy "Install no SMTP client". Potem bÄ™dziesz musiaÅ‚ wypeÅ‚nić dane odnoÅ›nie sieciowej konfiguracji. Wpisz domenÄ™, w jakiej pracujesz (najprawdopodobniej wystarczy "localhost"). NastÄ™pnie program poprosi CiÄ™ o wskazanie, co ma robić z e-mailami przeznaczonymi dla innych serwerów. Jako, że my raczej nie bÄ™dziemy ich wysyÅ‚ać tak daleko, możesz wskazać "None". Potwierdź jeszcze jednÄ… Å›cieżkÄ™ dostÄ™pu i wreszcie bÄ™dzie Ci dane ujrzeć przycisk "Install Mercury/32" :). Chyba wiesz, co należy już zrobić?
Jako że chcemy porzÄ…dnie "pobawić" siÄ™ protokoÅ‚em IMAP, dodamy sobie parÄ™ skrzynek pocztowych. Uruchom Mercurego (plik mercury.exe), po czym wybierz z menu pozycjÄ™ "Configuration". Na dole jest opcja "Manage local users". Po klikniÄ™ciu na niÄ… ukaże siÄ™ okienko, które pozwoli Ci na utworzenie i zarzÄ…dzanie skrzynkami pocztowymi userów. ResztÄ™ opcji możesz zostawić w spokoju, gdyż powinna automatycznie dopasować siÄ™ do ustawieÅ„ twego komputera.
A wiÄ™c przystÄ…pmy do programowania. Z serwerem IMAP (który musi być włączony w trakcie testów) łączymy siÄ™ podobnie, jak z bazÄ…. Musimy najpierw otworzyć połączenie, a na koÅ„cu je zamknąć. NapisaÅ‚em wyżej, że nasz skrypt umożliwi logowanie siÄ™ do rozmaitych skrzynek pocztowych na serwerze, wiÄ™c podobna praktyka, jak przy łączeniu siÄ™ ze zwykłą bazÄ… danych (że nazwÄ™ użytkownika i hasÅ‚o wbudowujemy w skrypt) odpada. Musimy napisać stosowny mechanizm. BÄ™dzie on umieszczony w katalogu "includes" w pliku "polacz.php":
<?php define('CZAS_SESJI', 3600); function polacz($user, $haslo){ $r = @imap_open('{eniac.blok22}', $user, $haslo) or $r = imap_last_error(); return $r; } if($_GET['co'] == 'wyloguj'){ unset($_COOKIE['user']); unset($_COOKIE['pass']); setcookie('user', '', 0); setcookie('pass', '', 0); } if(!isset($_COOKIE['user']) || !isset($_COOKIE['pass'])){ if(isset($_POST['user']) && isset($_POST['pass'])){ $imap = polacz($_POST['user'], $_POST['pass']); if(is_string($imap)){ // Blad blad($imap); loguj(); stopka(); die(); }elseif(is_resource($imap)){ setcookie('user', $_POST['user'], time()+CZAS_SESJI); setcookie('pass', $_POST['pass'], time()+CZAS_SESJI); } }else{ loguj(); stopka(); die(); } }else{ if(isset($_COOKIE['user']) && isset($_COOKIE['pass'])){ $imap = polacz($_COOKIE['user'], $_COOKIE['pass']); if(is_string($imap)){ // Blad blad($imap); loguj(); stopka(); die(); }elseif(is_resource($imap)){ setcookie('user', $_COOKIE['user'], time()+CZAS_SESJI); setcookie('pass', $_COOKIE['pass'], time()+CZAS_SESJI); } } } ?>
Za połączenie siÄ™ odpowiada funkcja "polacz" zawierajÄ…ca odwoÅ‚anie do pierwszej poznanej przez nas funkcji obsÅ‚ugi IMAP'a - imap_open(). Jako parametry musimy podać nazwÄ™ hosta, pod jakim dostÄ™pny jest serwer IMAP (koniecznie w nawiasach klamrowych). NastÄ™pnie wskazujesz użytkownika, który posiada na serwerze skrzynkÄ™, oraz jego hasÅ‚o. Funkcja imap_last_error() pozwala zwrócić ewentualny komunikat błędu.
Dalszy kod sprawdza, czy ktoś jest zalogowany, czy też nie i w zależności od tego podejmuje stosowną akcję. Nazwa użytkownika i hasło przesyłane są poprzez ciastko. Jeśli nie jesteś wybredny/nie masz jeszcze zbyt dużych zdolności, możesz kodu tego używać jako logowania do twego serwisu (aczkolwiek bezpieczny to on nie jest...).
W powyższym przykładzie znajdują się odwołania do funkcji typu "loguj", "stopka", "blad". Są to funkcje generujące kod HTML dla naszego klienta i wysyłające go do przeglądarki. Umieszczone są one w pliku "layout.php" w katalogu "includes", a plik ten prezentuje się następująco (listing jest baaaaaaaardzo długi, ale jakoś musimy przez niego przebrnąć...):
Aby zaoszczedzić miejsce: Pobierz kod listingu.
Zauważ, że połączenie z serwerem IMAP zamyka funkcja stopka(). Używa do tego celu funkcji "imap_close()" pobierajÄ…cej identyfikator połączenia. Teraz możemy już przystÄ…pić do napisania wÅ‚aÅ›ciwych i dziaÅ‚ajÄ…cych już plików. Najpierw stworzymy plik "index.php" (wszystkie nastÄ™pne pliki umieszczaj w katalogu gÅ‚ównym skryptu) z takÄ… zawartoÅ›ciÄ…:
<?php require('./includes/layout.php'); naglowek('Statystyki skrzynki'); require('./includes/polacz.php'); pasek_narzedzi(); $skrzynka = imap_mailboxmsginfo($imap); if(is_object($skrzynka)){ statystyki( $skrzynka->Date, $skrzynka->Driver, preg_replace('/\{(.*?):([0-9]*)\/imap\/user="(.*?)"}INBOX/', '\\3@\\1', $skrzynka->Mailbox), $skrzynka->Nmsgs, $skrzynka->Recent, $skrzynka->Unread, $skrzynka->Deleted, $skrzynka->Size ); }else{ blad('Nie można pobrać statystyk skrzynki!'); } stopka(); ?>
Plik ten bÄ™dzie wyÅ›wietlać statystyki naszej skrzynki. Możemy pobrać je funkcjÄ… "imap_mailboxmsginfo()". Za parametr pobiera tylko identyfikator połączenia, a w zamian zwraca obiekt z interesujÄ…cymi nas danymi. Mamy wiÄ™c datÄ™, ilość nowych wiadomoÅ›ci, ilość usuniÄ™tych, ilość nieczytanych itp. Zwróć uwagÄ™, że pole "Mailbox" potraktowaÅ‚em dodatkowo funkcjÄ… preg_replace. Po co? Ponieważ normalnie dostajemy różne gÅ‚upoty w stylu "{localhost:port/imap/user=zyx}INBOX", lecz sÄ… one lekko niezrozumiaÅ‚e. Dlaczegóż wiÄ™c nie przerobić ich na czytelniejszÄ… postać "zyx@localhost"?
Tu dam jeszcze małą poradÄ™: jeÅ›li zależy Ci na wydajnoÅ›ci i nie masz nic przeciwko lekkiemu uszczupleniu iloÅ›ci danych prezentowanych w statystykach, użyj do pobrania danych funkcji imap_status(). W manualu znaduje siÄ™ komentarz dodany przez jednego z programistów PHP, który pokazuje różnicÄ™ wydajnoÅ›ci. Pobranie danych ze skrzynki z 3987 wiadomoÅ›ciami przy użyciu imap_mailboxmsginfo() - 6 minut i 5 sekund; przy użyciu imap_status() - 11,05 sekundy. Wynik przemawia sam za siebie.
Mimo iż funkcja wysyłająca wiadomości e-mail ma w nazwie "imap", to używa do tego celu protokołu SMTP i właściwie można uznać ją za synonim funkcji mail(). Korzystają one z tych samych ustawień w php.ini, dlatego wejdź tam i odszukaj blok (uwaga: wyciąłem komentarze, by nie marnować miejsca):
[mail function] SMTP = localhost sendmail_from = zyx@localhost ;sendmail_path = ;mail.force_extra_paramaters =
Wszyscy powinni w dyrektywie SMTP wpisać nazwÄ™ serwera SMTP. Jako że my zainstalowaliÅ›my Mercurego z obsÅ‚ugÄ… tego protokoÅ‚u, możesz tam Å›miaÅ‚o wpisać "localhost". DyrektywÄ™ "sendmail_from" wypeÅ‚niajÄ… użytkownicy Windowsa, natomiast dwie nastÄ™pne użytkownicy systemów unixowych.
A oto kod pliku "wiadomosc.php" wyświetlającego odpowiedni formularz, a następnie "wyslij.php" wysyłającego e-maila:
<?php require('./includes/layout.php'); naglowek('Edytor wiadomosci'); require('./includes/polacz.php'); pasek_narzedzi(); if(isset($_GET['odp'])){ $naglowki = imap_header($imap, $_GET['odp']); if($naglowki -> Subject == ''){ $temat = 'Brak tematu'; }else{ $temat = $naglowki -> Subject; } form_wiad($temat, $naglowki -> fromaddress); }else{ form_wiad(); } stopka(); ?>
Zapewne dostrzegÅ‚eÅ› tu nieznanÄ… funkcjÄ™ "imap_header()". Jej znaczenie poznamy dalej. Tu jest ona potrzebna, by móc prawidÅ‚owo obsÅ‚użyć odpowiadanie na wiadomoÅ›ci. Oto kod pliku "wyslij.php":
<?php require('./includes/layout.php'); naglowek('Wysylanie'); require('./includes/polacz.php'); pasek_narzedzi(); if(imap_mail($_POST['to'], $_POST['temat'], $_POST['tresc'], 'Content-type: text/plain')){ komunikat('Wiadomość została wysłana.'); }else{ blad(imap_last_error()); } stopka(); ?>
Tak więc naszą funkcją wysyłającą wiadomości był "imap_mail()". W swej podstawowej formie używa jej się tak samo, jak "mail()" i możliwe, że składnia wydaje się znajoma.
Lista wiadomości będzie wyświetlana poprzez plik "skrzynka.php". Oto jego kod:
<?php require('./includes/layout.php'); naglowek('Twoja skrzynka'); require('./includes/polacz.php'); pasek_narzedzi(); if(isset($_POST['usun'])){ if(count($_POST['dmsg']) > 0){ foreach($_POST['dmsg'] as $wiad_num){ imap_delete($imap, $wiad_num); } imap_expunge($imap); } } $ilosc = imap_num_msg($imap); if($ilosc > 0){ list_wiadomosci(); for($i = $ilosc; $i > 0; $i--){ $naglowki = imap_header($imap, $i); $struktura = imap_fetchstructure($imap, $i); if($naglowki -> Subject == ''){ $temat = 'Brak tematu'; }else{ $temat = $naglowki -> Subject; } wiadomosc($i, substr($naglowki->Date, 0, 22), htmlspecialchars($naglowki->fromaddress), $temat, ceil(($structure->bytes/1024)).' KB'); } list_wiadomosci_koniec(); }else{ komunikat('Brak wiadomości w skrzynce'); } stopka(); ?>
Każda wiadomość w protokole IMAP ma przypisany specjalny identyfikator, czyli numer wiadomoÅ›ci. DziaÅ‚a on jednak trochÄ™ inaczej od identyfikatorów w bazach danych, co pozwala nam na pobranie wiadomoÅ›ci zwykłą pÄ™tlÄ… for. Na poczÄ…tku ustalamy ilość e-maili funkcjÄ… imap_num_msg(), a później uruchamiamy pÄ™tlÄ™. Za każdym przebiegiem dane o pojedynczym liÅ›cie otrzymujemy wywoÅ‚ujÄ…c funkcje imap_header() i imap_fetchstructure(). Pierwsza z nich pozwala nam pobrać takie duperszmity, jak temat, adresat, nadawca, flaga. Z kolei druga daje nam dostÄ™p do danych technicznych e-maila: typu (np. text/plain), rozmiaru, kodowania. Nas interesuje tylko rozmiar, który potem odpowiednio przetwarzamy. Wszystko wprowadzamy do funkcji wiadomosc(), która generuje kod HTML dla pojedynczego wiersza z danymi wiadomoÅ›ci. Dodaje także tam pole checkbox pozwalajÄ…ce nam na wybranie wiadomoÅ›ci do usuniÄ™cia. Możesz teraz skoczyć do pliku "layout.php", by podpatrzeć jego nazwÄ™. Pole to zwie siÄ™ "dmsg[]". Widzisz te nawiasy klamrowe? DziÄ™ki temu do PHP (po klikniÄ™ciu na "UsuÅ„ zaznaczone") przejdzie tablica z numerami wszystkich e-maili do skasowania, a nie numer ostatniej zaznaczonej pozycji. Kod obsÅ‚ugujÄ…cy usuwanie znajduje siÄ™ tuż nad kodem wyÅ›wietlajÄ…cym. KażdÄ… wiadomość najpierw oznaczam flagÄ… "UsuniÄ™ty" używajÄ…c do tego funkcji "imap_delete()". Jednak nie oznacza to fizycznego usuniÄ™cia listu z serwera. Do tego potrzebne jest jeszcze wywoÅ‚anie funkcji imap_expunge(). Dlaczego jest to tak udziwnione? Zobacz: dziÄ™ki temu możesz stworzyć coÅ› w stylu "Elementów usuniÄ™tych". WspomniaÅ‚em wczeÅ›niej, że funkcja imap_header() zwraca m.in flagÄ™ listu. JednÄ… z możliwych flag jest "UsuniÄ™ty". Oznacza to, że w skrzynce odbiorczej możesz wyÅ›wietlać wszystkie listy, które tej flagi ustawionej nie majÄ… (można do tego użyć zwykÅ‚ej instrukcji IF). W "Elementach usuniÄ™tych" przeciwnie - flaga ta musi być ustawiona, by listy tam siÄ™ znalazÅ‚y. Dalsza przeróbka: w skrzynce odbiorczej przy usuwaniu nie wywoÅ‚ujesz imap_expunge(); jej wywoÅ‚anie przenosisz do pliku odpowiedzialnego za elementy usuniÄ™te - tam jest ona aktywowana po klikniÄ™ciu na "Opróżnij elementy usuniÄ™te".
Samo pokazanie listy wiadomości nie wystarczy, jeśli użytkownik nie może ich przeczytać. Dlatego też stworzymy teraz plik "zobacz.php" pokazujący treść wiadomości. Wykorzystałem w nim kod zaprezentowany w komentarzach do dokumentacji PHP. Daje on nam pewność, że treść zostanie porządnie zdekodowana i złożona do kupy. "Suche" wywołanie funkcji do pobierania tekstu z listu jest MOCNO niezalecane.
<?php function pobierz_mime(&$structure){ $primary_mime_type = array('TEXT', 'MULTIPART', 'MESSAGE', 'APPLICATION', 'AUDIO', 'IMAGE', 'VIDEO', 'OTHER'); if($structure->subtype){ return $primary_mime_type[(int) $structure->type] . '/' . $structure->subtype; } return 'TEXT/PLAIN'; } function pobierz_czesc($stream, $msg_number, $mime_type, $structure = 0, $part_number = 0){ if(!$structure){ $structure = imap_fetchstructure($stream, $msg_number); } if($mime_type == pobierz_mime($structure)){ if(!$part_number){ $part_number = '1'; } $text = imap_fetchbody($stream, $msg_number, $part_number); if($structure->encoding == 3){ return imap_base64($text); }elseif($structure->encoding == 4){ return imap_qprint($text); }else{ return $text; } } if($structure->type == 1){ while(list($index, $sub_structure) = each($structure->parts)){ if($part_number){ $prefix = $part_number . '.'; } $data = pobierz_czesc($stream, $msg_number, $mime_type, $sub_structure, $prefix . ($index + 1)); if($data){ return $data; } } } return 0; } require('./includes/layout.php'); naglowek('Edytor wiadomosci'); require('./includes/polacz.php'); pasek_narzedzi(); if($_GET['num'] > 0){ $naglowki = imap_header($imap, $_GET['num']); pokaz($_GET['num'], $naglowki->fromaddress, $naglowki -> Subject, nl2br(pobierz_czesc($imap, $_GET['num'], 'TEXT/PLAIN'))); } stopka(); ?>
Treść listu pobierana jest funkcjÄ… imap_fetchbody(). Podajemy do niej: identyfikator połączenia, numer wiadomoÅ›ci, oraz numer części do pobrania. PosÅ‚ugujÄ…c siÄ™ funkcjÄ… imap_fetchstructure() pobieramy dane techniczne wiadomoÅ›ci, które pozwolÄ… nam jÄ… rozkodować. JeÅ›li zostaÅ‚a ona podzielona na kilka części, używany jest mechanizm rekurencji do pobrania caÅ‚oÅ›ci.
Generowany kod HTML zawiera dwa odnośniki: "Odpowiedz" kierujący nas do pliku "wiadomosc.php"; "Usuń" dający nam możliwość skasowania wiadomości. Do tego potrzebny jest jednak plik "usun.php", a oto jego kod:
<?php require('./includes/layout.php'); naglowek('Wysylanie'); require('./includes/polacz.php'); pasek_narzedzi(); if(isset($_GET['num'])){ imap_delete($imap, $_GET['num']); imap_expunge($imap); komunikat('Wiadomosc zostala usunięta'); }else{ blad('Nie wybrano wiadomości!'); } stopka(); ?>
Krótko i przejrzyÅ›cie :). Możesz teraz przetestować caÅ‚y skrypt.
Tak oto artykuł ten dobiega powoli końca. Pozostaje mi tylko wyrażać nadzieję, że zainteresowałem Cię tym tematem i że będziesz dalej eksperymentował z przedstawionym tu kodem. Oto, co mógłbyś wykonać dla przećwiczenia praktycznego poznanych informacji:
A oto parę przydatnych adresów:
Autor: Tomasz "Zyx" Jędrzejewski, www.zyxist.com
Waszym zdaniem:
Nikt jeszcze nie dodał swojego komentarza. Możesz być pierwszy!