Membangun Klien Email IMAP dengan PHP
Diterbitkan: 2022-03-11Pengembang terkadang mengalami tugas yang memerlukan akses ke kotak surat email. Dalam kebanyakan kasus, ini dilakukan dengan menggunakan Internet Message Access Protocol, atau IMAP. Sebagai pengembang PHP, saya pertama kali beralih ke perpustakaan IMAP bawaan PHP, tetapi perpustakaan ini bermasalah dan tidak mungkin untuk di-debug atau dimodifikasi. Juga tidak mungkin untuk menyesuaikan perintah IMAP untuk memanfaatkan sepenuhnya kemampuan protokol.
Jadi hari ini, kita akan membuat klien email IMAP yang berfungsi dari awal menggunakan PHP. Kita juga akan melihat cara menggunakan perintah khusus Gmail.
Kami akan mengimplementasikan IMAP di kelas khusus, imap_driver
. Saya akan menjelaskan setiap langkah saat membangun kelas. Anda dapat mengunduh seluruh imap_driver.php
di akhir artikel.
Membangun Koneksi
IMAP adalah protokol berbasis koneksi dan biasanya beroperasi melalui TCP/IP dengan keamanan SSL, jadi sebelum kita dapat melakukan panggilan IMAP, kita harus membuka koneksi.
Kita perlu mengetahui URL dan nomor port dari server IMAP yang ingin kita sambungkan. Informasi ini biasanya diiklankan di situs web atau dokumentasi layanan. Misalnya, untuk Gmail, URL-nya adalah ssl://imap.gmail.com
pada port 993.
Karena kita ingin tahu apakah inisialisasi berhasil, kita akan membiarkan konstruktor kelas kita kosong, dan semua koneksi akan dibuat dalam metode init()
kustom, yang akan mengembalikan false
jika koneksi tidak dapat dibuat:
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); } ... }
Dalam kode di atas, saya telah menetapkan batas waktu 15 detik, baik untuk fsockopen()
untuk membuat koneksi, dan untuk aliran data itu sendiri untuk menanggapi permintaan setelah dibuka. Penting untuk memiliki batas waktu untuk setiap panggilan ke jaringan karena, cukup sering, server tidak merespons, dan kita harus mampu menangani pembekuan seperti itu.
Saya juga mengambil baris pertama aliran dan mengabaikannya. Biasanya ini hanya pesan ucapan selamat dari server, atau konfirmasi bahwa itu terhubung. Periksa dokumentasi layanan email khusus Anda untuk memastikan hal ini terjadi.
Sekarang kita ingin menjalankan kode di atas untuk melihat bahwa init()
berhasil:
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; }
Sintaks IMAP Dasar
Sekarang kita memiliki soket aktif yang terbuka ke server IMAP kita, kita dapat mulai mengirim perintah IMAP. Mari kita lihat sintaks IMAP.
Dokumentasi formal dapat ditemukan di Internet Engineering Task Force (IETF) RFC3501. Interaksi IMAP biasanya terdiri dari perintah pengiriman klien, dan server merespons dengan indikasi keberhasilan, bersama dengan data apa pun yang mungkin diminta.
Sintaks dasar untuk perintah adalah:
line_number command arg1 arg2 ...
Nomor baris, atau "tag", adalah pengidentifikasi unik untuk perintah, yang digunakan server untuk menunjukkan perintah mana yang ditanggapi jika memproses beberapa perintah sekaligus.
Berikut adalah contoh, menunjukkan perintah LOGIN
:
00000001 LOGIN [email protected] password
Respons server dapat dimulai dengan respons data yang "tidak ditandai". Misalnya, Gmail merespons login yang berhasil dengan respons tanpa tag yang berisi informasi tentang kemampuan dan opsi server, dan perintah untuk mengambil pesan email akan menerima respons tanpa tag yang berisi isi pesan. Dalam kedua kasus tersebut, respons harus selalu diakhiri dengan baris respons penyelesaian perintah yang "diberi tag", mengidentifikasi nomor baris perintah tempat respons diterapkan, indikator status penyelesaian, dan metadata tambahan tentang perintah, jika ada:
line_number status metadata1 metadata2 ...
Berikut adalah cara Gmail merespons perintah LOGIN
:
- Kesuksesan:
* 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)
- Kegagalan:
00000001 NO [AUTHENTICATIONFAILED] Invalid credentials (Failure)
Statusnya dapat berupa OK
, menunjukkan keberhasilan, NO
, menunjukkan kegagalan, atau BAD
, menunjukkan perintah yang tidak valid atau sintaks yang buruk.
Menerapkan Perintah Dasar:
Mari kita buat fungsi untuk mengirim perintah ke server IMAP, dan mengambil respons dan endline:
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); } ... }
Perintah LOGIN
Sekarang kita dapat menulis fungsi untuk perintah tertentu yang memanggil fungsi command()
kita di bawah tenda. Mari kita menulis fungsi untuk perintah 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; } } ... }
Sekarang kita bisa mengujinya seperti ini. (Perhatikan bahwa Anda harus memiliki akun email aktif untuk diuji.)
... // test for login() if ($imap_driver->login('[email protected]', 'password') === false) { echo "login() failed: " . $imap_driver->error . "\n"; exit; }
Perhatikan bahwa Gmail sangat ketat tentang keamanan secara default: Gmail tidak akan mengizinkan kami mengakses akun email dengan IMAP jika kami memiliki pengaturan default dan mencoba mengaksesnya dari negara selain negara profil akun. Tapi itu cukup mudah untuk diperbaiki; cukup atur pengaturan yang kurang aman di akun Gmail Anda, seperti yang dijelaskan di sini.
Perintah SELECT
Sekarang mari kita lihat bagaimana memilih folder IMAP untuk melakukan sesuatu yang berguna dengan email kita. Sintaksnya mirip dengan LOGIN
, berkat metode command()
kami. Kami menggunakan perintah SELECT
sebagai gantinya, dan tentukan foldernya.

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; } } ... }
Untuk mengujinya, mari kita coba pilih INBOX:
... // test for select_folder() if ($imap_driver->select_folder("INBOX") === false) { echo "select_folder() failed: " . $imap_driver->error . "\n"; return false; }
Menerapkan Perintah Tingkat Lanjut
Mari kita lihat bagaimana menerapkan beberapa perintah IMAP yang lebih canggih.
Perintah SEARCH
Rutinitas umum dalam analisis email adalah mencari email dalam rentang tanggal tertentu, atau mencari email yang ditandai, dan seterusnya. Kriteria pencarian harus diteruskan ke perintah SEARCH
sebagai argumen, dengan spasi sebagai pemisah. Misalnya, jika kita ingin mendapatkan semua email sejak 20 November 2015, kita harus melewati perintah berikut:
00000005 SEARCH SINCE 20-Nov-2015
Dan responnya akan seperti ini:
* SEARCH 881 882 00000005 OK SEARCH completed
Dokumentasi terperinci dari kemungkinan istilah pencarian dapat ditemukan di sini Output dari perintah SEARCH
adalah daftar UID email, dipisahkan oleh spasi. UID adalah pengidentifikasi unik email di akun pengguna, dalam urutan kronologis, di mana 1 adalah email tertua. Untuk mengimplementasikan perintah SEARCH
, kita harus mengembalikan UID yang dihasilkan:
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; } } ... }
Untuk menguji perintah ini, kami akan menerima email dari tiga hari terakhir:
... // 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; }
Perintah FETCH
dengan BODY.PEEK
Tugas umum lainnya adalah mendapatkan header email tanpa menandai email sebagai SEEN
. Dari manual IMAP, perintah untuk mengambil semua atau sebagian email adalah FETCH
. Argumen pertama menunjukkan bagian mana yang kita minati, dan biasanya BODY
dilewatkan, yang akan mengembalikan seluruh pesan bersama dengan header-nya, dan menandainya sebagai SEEN
. Argumen alternatif BODY.PEEK
akan melakukan hal yang sama, tanpa menandai pesan sebagai SEEN
.
Sintaks IMAP mengharuskan permintaan kami untuk juga menentukan, dalam tanda kurung siku, bagian email yang ingin kami ambil, yang dalam contoh ini adalah [HEADER]
. Hasilnya, perintah kita akan terlihat seperti ini:
00000006 FETCH 2 BODY.PEEK[HEADER]
Dan kami akan mengharapkan respons yang terlihat seperti ini:
* 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
Untuk membangun fungsi untuk mengambil header, kita harus dapat mengembalikan respons dalam struktur hash (pasangan kunci/nilai):
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; } } ... }
Dan untuk menguji kode ini, kami hanya menentukan UID dari pesan yang kami minati:
... // 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; }
Ekstensi IMAP Gmail
Gmail menyediakan daftar perintah khusus yang dapat membuat hidup kita lebih mudah. Daftar perintah ekstensi IMAP Gmail tersedia di sini. Mari kita tinjau kembali perintah yang menurut saya paling penting: X-GM-RAW
. Ini memungkinkan kita untuk menggunakan sintaks pencarian Gmail dengan IMAP. Misalnya kita bisa mencari email yang masuk dalam kategori Utama, Sosial, Promosi, Update, atau Forum.
Secara fungsional, X-GM-RAW
merupakan perpanjangan dari perintah SEARCH
, sehingga kita dapat menggunakan kembali kode yang kita miliki di atas untuk perintah SEARCH
. Yang perlu kita lakukan adalah menambahkan kata kunci X-GM-RAW
dan kriteria:
... // 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; }
Kode di atas akan mengembalikan semua UID yang terdaftar dalam kategori "Utama".
Catatan: Mulai Desember 2015, Gmail sering mengacaukan kategori “Utama” dengan kategori “Pembaruan” di beberapa akun. Ini adalah bug Gmail yang belum diperbaiki.
Kesimpulan
Secara keseluruhan, pendekatan soket kustom memberikan lebih banyak kebebasan kepada pengembang. Itu memungkinkan untuk mengimplementasikan semua perintah di IMAP RFC3501. Ini juga akan memberi Anda kontrol yang lebih baik atas kode Anda, karena Anda tidak perlu bertanya-tanya apa yang terjadi "di balik layar".
Kelas imap_driver
lengkap yang kami implementasikan dalam artikel ini dapat ditemukan di sini. Ini dapat digunakan apa adanya, dan hanya perlu beberapa menit bagi pengembang untuk menulis fungsi atau permintaan baru ke server IMAP mereka. Saya juga menyertakan fitur debug di kelas untuk keluaran verbose.