PHP ile IMAP E-posta İstemcisi Oluşturma
Yayınlanan: 2022-03-11Geliş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
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.