การสร้างไคลเอนต์อีเมล IMAP ด้วย PHP
เผยแพร่แล้ว: 2022-03-11นักพัฒนาบางครั้งอาจทำงานที่ต้องเข้าถึงเมลบ็อกซ์อีเมล ในกรณีส่วนใหญ่ ทำได้โดยใช้ Internet Message Access Protocol หรือ IMAP ในฐานะนักพัฒนา PHP ตอนแรกฉันหันไปใช้ไลบรารี IMAP ที่สร้างขึ้นใน PHP แต่ไลบรารีนี้มีข้อบกพร่องและไม่สามารถแก้ไขจุดบกพร่องหรือแก้ไขได้ นอกจากนี้ยังไม่สามารถกำหนดคำสั่ง IMAP เองเพื่อใช้ความสามารถของโปรโตคอลได้อย่างเต็มที่
ดังนั้นวันนี้ เราจะสร้างไคลเอนต์อีเมล IMAP ที่ใช้งานได้ตั้งแต่เริ่มต้นโดยใช้ PHP เราจะดูวิธีการใช้คำสั่งพิเศษของ Gmail
เราจะนำ IMAP ไปใช้ในคลาสที่กำหนดเอง imap_driver ฉันจะอธิบายแต่ละขั้นตอนขณะสร้างชั้นเรียน คุณสามารถดาวน์โหลด imap_driver.php ทั้งหมดได้ที่ท้ายบทความ
การสร้างการเชื่อมต่อ
IMAP เป็นโปรโตคอลที่ใช้การเชื่อมต่อ และโดยทั่วไปแล้วจะทำงานผ่าน TCP/IP โดยมีการรักษาความปลอดภัย SSL ดังนั้นก่อนที่เราจะสามารถเรียก IMAP ได้ เราจะต้องเปิดการเชื่อมต่อ
เราจำเป็นต้องทราบ URL และหมายเลขพอร์ตของเซิร์ฟเวอร์ IMAP ที่เราต้องการเชื่อมต่อ ข้อมูลนี้มักจะโฆษณาในเว็บไซต์หรือเอกสารของบริการ ตัวอย่างเช่น สำหรับ Gmail URL คือ ssl://imap.gmail.com บนพอร์ต 993
เนื่องจากเราต้องการทราบว่าการเริ่มต้นสำเร็จหรือไม่ เราจะปล่อยให้ class constructor ว่าง และการเชื่อมต่อทั้งหมดจะถูกสร้างขึ้นในเมธอด init() แบบกำหนดเอง ซึ่งจะคืนค่า false หากไม่สามารถสร้างการเชื่อมต่อได้:
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); } ... } ในโค้ดด้านบนนี้ ฉันได้ตั้งค่าระยะหมดเวลา 15 วินาที ทั้งสำหรับ fsockopen() เพื่อสร้างการเชื่อมต่อ และสำหรับตัวสตรีมข้อมูลเองเพื่อตอบสนองต่อคำขอเมื่อเปิดขึ้น สิ่งสำคัญคือต้องมีระยะหมดเวลาสำหรับการโทรไปยังเครือข่ายทุกครั้ง เนื่องจากบ่อยครั้งเพียงพอที่เซิร์ฟเวอร์จะไม่ตอบสนอง และเราต้องสามารถจัดการกับการหยุดทำงานดังกล่าวได้
ฉันยังคว้าบรรทัดแรกของสตรีมและเพิกเฉย โดยปกติแล้ว นี่เป็นเพียงข้อความทักทายจากเซิร์ฟเวอร์ หรือการยืนยันว่าเชื่อมต่อแล้ว ตรวจสอบเอกสารของบริการอีเมลโดยเฉพาะเพื่อให้แน่ใจว่าเป็นกรณีนี้
ตอนนี้เราต้องการเรียกใช้โค้ดด้านบนเพื่อดูว่า init() ประสบความสำเร็จ:
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; }ไวยากรณ์ IMAP พื้นฐาน
ตอนนี้เราเปิดซ็อกเก็ตที่ใช้งานอยู่สำหรับเซิร์ฟเวอร์ IMAP ของเราแล้ว เราสามารถเริ่มส่งคำสั่ง IMAP ได้ ให้เราดูที่ไวยากรณ์ IMAP
เอกสารที่เป็นทางการสามารถพบได้ใน Internet Engineering Task Force (IETF) RFC3501 การโต้ตอบ IMAP โดยทั่วไปประกอบด้วยคำสั่งที่ส่งโดยไคลเอ็นต์ และเซิร์ฟเวอร์ตอบสนองด้วยตัวบ่งชี้ความสำเร็จ พร้อมกับข้อมูลใดๆ ที่อาจได้รับการร้องขอ
ไวยากรณ์พื้นฐานสำหรับคำสั่งคือ:
line_number command arg1 arg2 ...หมายเลขบรรทัดหรือ "แท็ก" เป็นตัวระบุเฉพาะสำหรับคำสั่ง ที่เซิร์ฟเวอร์ใช้เพื่อระบุว่าคำสั่งใดตอบสนอง หากกำลังประมวลผลคำสั่งหลายคำสั่งพร้อมกัน
นี่คือตัวอย่างที่แสดงคำสั่ง LOGIN :
00000001 LOGIN [email protected] passwordการตอบสนองของเซิร์ฟเวอร์อาจเริ่มต้นด้วยการตอบกลับข้อมูล "ไม่ติดแท็ก" ตัวอย่างเช่น Gmail ตอบสนองต่อการเข้าสู่ระบบที่ประสบความสำเร็จด้วยการตอบกลับแบบไม่มีแท็กซึ่งมีข้อมูลเกี่ยวกับความสามารถและตัวเลือกของเซิร์ฟเวอร์ และคำสั่งให้ดึงข้อความอีเมลจะได้รับการตอบสนองที่ไม่ติดแท็กซึ่งมีเนื้อหาของข้อความ ไม่ว่าในกรณีใด การตอบกลับควรลงท้ายด้วยบรรทัดตอบกลับการเติมคำสั่งที่ "ติดแท็ก" เสมอ โดยระบุหมายเลขบรรทัดของคำสั่งที่ใช้การตอบกลับ ตัวบ่งชี้สถานะการเสร็จสิ้น และข้อมูลเมตาเพิ่มเติมเกี่ยวกับคำสั่ง หากมี:
line_number status metadata1 metadata2 ... นี่คือวิธีที่ Gmail ตอบสนองต่อคำสั่ง LOGIN :
- ความสำเร็จ:
* 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)- ความล้มเหลว:
00000001 NO [AUTHENTICATIONFAILED] Invalid credentials (Failure) สถานะอาจเป็นได้ OK ซึ่งบ่งชี้ความสำเร็จ NO บ่งชี้ความล้มเหลว หรือ BAD ซึ่งระบุคำสั่งที่ไม่ถูกต้องหรือไวยากรณ์ที่ไม่ถูกต้อง
การใช้คำสั่งพื้นฐาน:
มาสร้างฟังก์ชันเพื่อส่งคำสั่งไปยังเซิร์ฟเวอร์ IMAP และรับการตอบกลับและสิ้นสุด:
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 ระบบ
ตอนนี้ เราสามารถเขียนฟังก์ชันสำหรับคำสั่งเฉพาะที่เรียกใช้ฟังก์ชัน command() ของเราภายใต้ประทุน มาเขียนฟังก์ชันสำหรับคำสั่ง 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; } } ... }ตอนนี้เราสามารถทดสอบได้แบบนี้ (โปรดทราบว่าคุณต้องมีบัญชีอีเมลที่ใช้งานอยู่เพื่อทดสอบ)
... // test for login() if ($imap_driver->login('[email protected]', 'password') === false) { echo "login() failed: " . $imap_driver->error . "\n"; exit; }โปรดทราบว่า Gmail นั้นเข้มงวดมากเกี่ยวกับการรักษาความปลอดภัยโดยค่าเริ่มต้น: จะไม่อนุญาตให้เราเข้าถึงบัญชีอีเมลที่มี IMAP หากเรามีการตั้งค่าเริ่มต้นและพยายามเข้าถึงจากประเทศอื่นที่ไม่ใช่ประเทศของโปรไฟล์บัญชี แต่มันง่ายพอที่จะแก้ไข เพียงตั้งค่าความปลอดภัยน้อยกว่าในบัญชี Gmail ของคุณตามที่อธิบายไว้ที่นี่

คำสั่ง SELECT
ตอนนี้เรามาดูวิธีการเลือกโฟลเดอร์ IMAP เพื่อทำสิ่งที่มีประโยชน์กับอีเมลของเรากัน ไวยากรณ์คล้ายกับของ LOGIN ต้องขอบคุณเมธอด command() ของเรา เราใช้คำสั่ง SELECT แทน และระบุโฟลเดอร์
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; } } ... }เพื่อทดสอบ ลองเลือก INBOX:
... // test for select_folder() if ($imap_driver->select_folder("INBOX") === false) { echo "select_folder() failed: " . $imap_driver->error . "\n"; return false; }การใช้คำสั่งขั้นสูง
มาดูวิธีการใช้คำสั่งขั้นสูงบางอย่างของ IMAP กัน
คำสั่ง SEARCH
กิจวัตรทั่วไปในการวิเคราะห์อีเมลคือการค้นหาอีเมลในช่วงวันที่ที่กำหนด หรือค้นหาอีเมลที่ถูกตั้งค่าสถานะ และอื่นๆ เกณฑ์การค้นหาต้องส่งผ่านไปยังคำสั่ง SEARCH เป็นอาร์กิวเมนต์ โดยมีช่องว่างเป็นตัวคั่น ตัวอย่างเช่น หากเราต้องการรับอีเมลทั้งหมดตั้งแต่วันที่ 20 พฤศจิกายน 2015 เราต้องส่งคำสั่งต่อไปนี้:
00000005 SEARCH SINCE 20-Nov-2015และคำตอบจะเป็นดังนี้:
* SEARCH 881 882 00000005 OK SEARCH completed เอกสารโดยละเอียดของข้อความค้นหาที่เป็นไปได้สามารถพบได้ที่นี่ ผลลัพธ์ของคำสั่ง SEARCH คือรายการ UID ของอีเมล โดยคั่นด้วยช่องว่าง UID คือตัวระบุเฉพาะของอีเมลในบัญชีของผู้ใช้ ตามลำดับเวลา โดยที่ 1 คืออีเมลที่เก่าที่สุด ในการใช้คำสั่ง SEARCH เราต้องส่งคืน UID ที่เป็นผลลัพธ์:
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; } } ... }เพื่อทดสอบคำสั่งนี้ เราจะได้รับอีเมลจากสามวันที่ผ่านมา:
... // 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; } คำสั่ง FETCH กับ BODY.PEEK
งานทั่วไปอีกประการหนึ่งคือการรับส่วนหัวของอีเมลโดยไม่ทำเครื่องหมายอีเมลว่า SEEN จากคู่มือ IMAP คำสั่งในการดึงข้อมูลอีเมลทั้งหมดหรือบางส่วนคือ FETCH อาร์กิวเมนต์แรกระบุว่าส่วนใดที่เราสนใจ และโดยทั่วไปแล้ว BODY จะถูกส่งต่อ ซึ่งจะส่งคืนข้อความทั้งหมดพร้อมกับส่วนหัว และทำเครื่องหมายเป็น SEEN อาร์กิวเมนต์ทางเลือก BODY.PEEK จะทำสิ่งเดียวกัน โดยไม่ทำเครื่องหมายข้อความว่า SEEN
ไวยากรณ์ IMAP ต้องการให้คำขอของเราระบุในวงเล็บเหลี่ยม ส่วนของอีเมลที่เราต้องการดึงข้อมูล ซึ่งในตัวอย่างนี้คือ [HEADER] เป็นผลให้คำสั่งของเราจะมีลักษณะดังนี้:
00000006 FETCH 2 BODY.PEEK[HEADER]และเราจะคาดหวังการตอบสนองที่มีลักษณะดังนี้:
* 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ในการสร้างฟังก์ชันสำหรับการดึงข้อมูลส่วนหัว เราต้องสามารถส่งคืนการตอบกลับในโครงสร้างแฮช (คู่คีย์/ค่า):
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; } } ... }และเพื่อทดสอบโค้ดนี้ เราเพียงแค่ระบุ UID ของข้อความที่เราสนใจ:
... // 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
Gmail มีรายการคำสั่งพิเศษที่ทำให้ชีวิตของเราง่ายขึ้นมาก รายการคำสั่งส่วนขยาย IMAP ของ Gmail มีอยู่ที่นี่ มาทบทวนคำสั่งที่ฉันคิดว่าสำคัญที่สุด: X-GM-RAW . ช่วยให้เราใช้ไวยากรณ์การค้นหาของ Gmail กับ IMAP ตัวอย่างเช่น เราสามารถค้นหาอีเมลที่อยู่ในหมวดหมู่หลัก โซเชียล โปรโมชัน อัปเดต หรือฟอรัม
ตามหน้าที่ X-GM-RAW เป็นส่วนขยายของคำสั่ง SEARCH ดังนั้นเราจึงสามารถนำโค้ดที่เรามีด้านบนนี้มาใช้ใหม่สำหรับคำสั่ง SEARCH สิ่งที่เราต้องทำคือเพิ่มคีย์เวิร์ด X-GM-RAW และเกณฑ์:
... // 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; }รหัสด้านบนจะส่งคืน UID ทั้งหมดที่อยู่ในหมวด "หลัก"
หมายเหตุ: ในเดือนธันวาคม 2015 Gmail มักจะสร้างความสับสนให้กับหมวดหมู่ "หลัก" กับหมวดหมู่ "อัปเดต" ในบางบัญชี นี่เป็นข้อบกพร่องของ Gmail ที่ยังไม่ได้รับการแก้ไข
บทสรุป
โดยรวมแล้ว แนวทางซ็อกเก็ตแบบกำหนดเองให้อิสระแก่นักพัฒนามากขึ้น ทำให้สามารถใช้คำสั่งทั้งหมดใน IMAP RFC3501 ได้ นอกจากนี้ยังช่วยให้คุณควบคุมโค้ดได้ดีขึ้น เนื่องจากคุณไม่ต้องสงสัยว่าเกิดอะไรขึ้น "เบื้องหลัง"
คลาส imap_driver แบบเต็มที่เราใช้ในบทความนี้ สามารถพบได้ที่นี่ สามารถใช้งานได้ตามที่เป็นอยู่ และจะใช้เวลาเพียงไม่กี่นาทีสำหรับนักพัฒนาในการเขียนฟังก์ชันใหม่หรือร้องขอไปยังเซิร์ฟเวอร์ IMAP ของตน ฉันได้รวมคุณสมบัติการดีบักในคลาสสำหรับเอาต์พุตแบบละเอียด
