PHP ile IMAP E-posta İstemcisi Oluşturma

Yayınlanan: 2022-03-11

Geliştiriciler bazen e-posta posta kutularına erişim gerektiren görevlerle karşılaşırlar. Çoğu durumda bu, İnternet İleti Erişim Protokolü veya IMAP kullanılarak yapılır. Bir PHP geliştiricisi olarak, önce PHP'nin yerleşik IMAP kitaplığına döndüm, ancak bu kitaplık sorunlu ve hata ayıklamak veya değiştirmek imkansız. IMAP komutlarını protokolün yeteneklerini tam olarak kullanmak için özelleştirmek de mümkün değildir.

Bugün, PHP kullanarak sıfırdan çalışan bir IMAP e-posta istemcisi oluşturacağız. Gmail'in özel komutlarının nasıl kullanılacağını da göreceğiz.

IMAP'yi özel bir sınıf olan imap_driver . Sınıfı oluştururken her adımı açıklayacağım. imap_driver.php tamamını makalenin sonunda indirebilirsiniz.

Bağlantı Kurma

IMAP bağlantı tabanlı bir protokoldür ve genellikle SSL güvenliği ile TCP/IP üzerinden çalışır, bu nedenle herhangi bir IMAP araması yapmadan önce bağlantıyı açmalıyız.

Bağlanmak istediğimiz IMAP sunucusunun URL'sini ve port numarasını bilmemiz gerekiyor. Bu bilgiler genellikle hizmetin web sitesinde veya belgelerinde duyurulur. Örneğin, Gmail için URL, 993 numaralı bağlantı noktasında ssl://imap.gmail.com .

Başlatmanın başarılı olup olmadığını bilmek istediğimiz için, sınıf kurucumuzu boş bırakacağız ve tüm bağlantılar, bağlantı kurulamazsa false döndüren özel bir init() yönteminde yapılacaktır:

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

Yukarıdaki kodda, hem fsockopen() 'ın bağlantı kurması için hem de veri akışının açıldıktan sonra isteklere yanıt vermesi için 15 saniyelik bir zaman aşımı süresi ayarladım. Ağa yapılan her çağrı için bir zaman aşımına sahip olmak önemlidir, çünkü çoğu zaman sunucu yanıt vermez ve böyle bir donmayı kaldırabilmemiz gerekir.

Ayrıca akışın ilk satırını alıp görmezden geliyorum. Genellikle bu, sunucudan gelen bir karşılama mesajı veya sunucunun bağlandığına dair bir onaydır. Durumun böyle olduğundan emin olmak için özel posta hizmetinizin belgelerini kontrol edin.

Şimdi init() 'in başarılı olduğunu görmek için yukarıdaki kodu çalıştırmak istiyoruz:

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

Temel IMAP Sözdizimi

Artık IMAP sunucumuza açık aktif bir soketimiz olduğuna göre, IMAP komutlarını göndermeye başlayabiliriz. IMAP sözdizimine bir göz atalım.

Resmi belgeler, İnternet Mühendisliği Görev Gücü (IETF) RFC3501'de bulunabilir. IMAP etkileşimleri tipik olarak istemcinin komutları göndermesinden ve sunucunun istenen verilerle birlikte bir başarı göstergesiyle yanıt vermesinden oluşur.

Komutlar için temel sözdizimi şöyledir:

 line_number command arg1 arg2 ...

Satır numarası veya "etiket", sunucunun aynı anda birden çok komutu işlemesi durumunda hangi komuta yanıt verdiğini belirtmek için kullandığı komut için benzersiz bir tanımlayıcıdır.

LOGIN komutunu gösteren bir örnek:

 00000001 LOGIN [email protected] password

Sunucunun yanıtı, "etiketlenmemiş" bir veri yanıtıyla başlayabilir. Örneğin, Gmail, başarılı bir girişe, sunucunun yetenekleri ve seçenekleri hakkında bilgi içeren etiketsiz bir yanıtla yanıt verir ve bir e-posta iletisini alma komutu, ileti gövdesini içeren etiketsiz bir yanıt alır. Her iki durumda da, yanıt her zaman yanıtın geçerli olduğu komutun satır numarasını, bir tamamlanma durumu göstergesini ve varsa komutla ilgili ek meta verileri tanımlayan "etiketli" bir komut tamamlama yanıt satırıyla bitmelidir:

 line_number status metadata1 metadata2 ...

Gmail, LOGIN komutuna şu şekilde yanıt verir:

  • Başarı:
 * 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)
  • Arıza:
 00000001 NO [AUTHENTICATIONFAILED] Invalid credentials (Failure)

Durum, başarıyı gösteren OK , başarısızlığı gösteren NO veya geçersiz bir komut veya hatalı sözdizimi belirten BAD olabilir.

Temel Komutları Uygulama:

IMAP sunucusuna bir komut göndermek için bir fonksiyon yapalım ve yanıtı ve bitiş çizgisini alalım:

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

LOGIN Komutu

Artık başlık altında command() işlevimizi çağıran belirli komutlar için işlevler yazabiliriz. LOGIN komutu için bir fonksiyon yazalım:

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

Şimdi bu şekilde test edebiliriz. (Test etmek için aktif bir e-posta hesabınızın olması gerektiğini unutmayın.)

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

Gmail'in varsayılan olarak güvenlik konusunda çok katı olduğunu unutmayın: Varsayılan ayarlarımız varsa ve hesap profilinin ülkesinden başka bir ülkeden erişmeye çalışırsak, IMAP ile bir e-posta hesabına erişmemize izin vermez. Ancak düzeltmek yeterince kolaydır; burada açıklandığı gibi, Gmail hesabınızda daha az güvenli ayarlar yapmanız yeterlidir.

SELECT Komutu

Şimdi e-postamızla faydalı bir şeyler yapmak için bir IMAP klasörünün nasıl seçileceğini görelim. command() yöntemimiz sayesinde sözdizimi LOGIN benzer. Bunun yerine SELECT komutunu kullanıyoruz ve klasörü belirliyoruz.

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

Test etmek için, GELEN KUTUSU'nu seçmeyi deneyelim:

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

Gelişmiş Komutları Uygulama

IMAP'in daha gelişmiş komutlarından birkaçının nasıl uygulanacağına bakalım.

SEARCH Komutu

E-posta analizinde yaygın bir rutin, belirli bir tarih aralığındaki e-postaları aramak veya işaretli e-postaları aramak vb.dir. Arama kriterleri, ayırıcı olarak boşlukla birlikte bir argüman olarak SEARCH komutuna iletilmelidir. Örneğin, 20 Kasım 2015 tarihinden itibaren tüm e-postaları almak istiyorsak aşağıdaki komutu geçmeliyiz:

 00000005 SEARCH SINCE 20-Nov-2015

Ve cevap şöyle bir şey olacak:

 * SEARCH 881 882 00000005 OK SEARCH completed

Olası arama terimlerinin ayrıntılı belgeleri burada bulunabilir Bir SEARCH komutunun çıktısı, boşluklarla ayrılmış e-postaların UID'lerinin bir listesidir. UID, 1'in en eski e-posta olduğu, kronolojik sırayla, kullanıcının hesabındaki bir e-postanın benzersiz tanımlayıcısıdır. SEARCH komutunu uygulamak için ortaya çıkan UID'leri döndürmemiz yeterlidir:

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

Bu komutu test etmek için son üç günden e-postalar alacağız:

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

BODY.PEEK ile FETCH Komutu

Diğer bir yaygın görev, bir e-postayı SEEN olarak işaretlemeden e-posta başlıklarını almaktır. IMAP kılavuzundan, bir e-postanın tamamını veya bir kısmını alma komutu FETCH şeklindedir. İlk argüman, hangi kısımla ilgilendiğimizi gösterir ve genellikle BODY iletilir; bu, tüm mesajı başlıklarıyla birlikte döndürecek ve SEEN olarak işaretleyecektir. BODY.PEEK alternatif argümanı, mesajı SEEN olarak işaretlemeden aynı şeyi yapacaktır.

IMAP sözdizimi, isteğimizin, almak istediğimiz e-postanın bu örnekte [HEADER] olan bölümünü köşeli parantez içinde belirtmemizi gerektiriyor. Sonuç olarak komutumuz aşağıdaki gibi olacaktır.

 00000006 FETCH 2 BODY.PEEK[HEADER]

Ve şuna benzeyen bir yanıt bekliyoruz:

 * 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

Başlıkları almak için bir işlev oluşturmak için, yanıtı bir karma yapıda (anahtar/değer çiftleri) döndürebilmemiz gerekir:

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

Ve bu kodu test etmek için ilgilendiğimiz mesajın UID'sini belirtiyoruz:

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

Gmail IMAP Uzantıları

Gmail, hayatımızı çok daha kolaylaştırabilecek özel komutların bir listesini sağlar. Gmail'in IMAP uzantısı komutlarının listesine buradan ulaşabilirsiniz. Bence en önemlisi olan bir komutu gözden geçirelim: X-GM-RAW . IMAP ile Gmail arama sözdizimini kullanmamıza izin verir. Örneğin, Birincil, Sosyal, Promosyonlar, Güncellemeler veya Forumlar kategorilerindeki e-postaları arayabiliriz.

İşlevsel olarak, X-GM-RAW , SEARCH komutunun bir uzantısıdır, bu nedenle yukarıdaki kodu SEARCH komutu için yeniden kullanabiliriz. Tek yapmamız gereken X-GM-RAW anahtar kelimesini ve kriterleri eklemek:

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

Yukarıdaki kod, "Birincil" kategorisinde listelenen tüm UID'leri döndürür.

Not: Aralık 2015 itibarıyla Gmail, bazı hesaplarda genellikle "Birincil" kategoriyi "Güncellemeler" kategorisiyle karıştırmaktadır. Bu, henüz düzeltilmemiş bir Gmail hatasıdır.

Çözüm

Mektubunuz Var. Şimdi ne olacak? PHP'de özel bir IMAP e-posta istemcisinin nasıl oluşturulacağını okuyun ve şartlarınıza göre postayı kontrol edin.
Cıvıldamak

Genel olarak, özel soket yaklaşımı geliştiriciye daha fazla özgürlük sağlar. IMAP RFC3501'deki tüm komutların uygulanmasını mümkün kılar. Ayrıca, "perde arkasında" neler olduğunu merak etmeniz gerekmediğinden, kodunuz üzerinde daha iyi kontrol sahibi olmanızı sağlar.

Bu makalede uyguladığımız tam imap_driver sınıfı burada bulunabilir. Olduğu gibi kullanılabilir ve bir geliştiricinin IMAP sunucusuna yeni bir işlev veya istek yazması yalnızca birkaç dakika sürer. Ayrıca ayrıntılı bir çıktı için sınıfa bir hata ayıklama özelliği ekledim.