إنشاء عميل بريد إلكتروني IMAP باستخدام PHP

نشرت: 2022-03-11

يعمل المطورون أحيانًا في مهام تتطلب الوصول إلى علب بريد البريد الإلكتروني. في معظم الحالات ، يتم ذلك باستخدام بروتوكول الوصول إلى الرسائل عبر الإنترنت أو 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.

نظرًا لأننا نريد معرفة ما إذا كانت التهيئة ناجحة ، فسنترك مُنشئ الصنف فارغًا ، وسيتم إجراء جميع الاتصالات بطريقة 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.

يمكن العثور على الوثائق الرسمية في فريق عمل هندسة الإنترنت (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; } } ... }

لاختباره ، دعنا نحاول تحديد صندوق الوارد:

 ... // 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 ، يجب علينا ببساطة إرجاع UIDs الناتجة:

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

قيادة BODY.PEEK مع FETCH

مهمة أخرى شائعة هي الحصول على رؤوس البريد الإلكتروني دون وضع علامة على البريد الإلكتروني كـ 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; } } ... }

ولاختبار هذا الرمز ، نحدد فقط المعرف الفريد للرسالة التي نهتم بها:

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

سيعيد الرمز أعلاه جميع UIDs المدرجة في فئة "أساسي".

ملاحظة: اعتبارًا من ديسمبر 2015 ، غالبًا ما يخلط Gmail بين الفئة "الأساسية" وفئة "التحديثات" في بعض الحسابات. هذا خطأ Gmail لم يتم إصلاحه بعد.

خاتمة

وصلك بريد. ماذا الآن؟ اقرأ كيفية إنشاء عميل بريد إلكتروني IMAP مخصص في PHP ، وتحقق من البريد وفقًا لشروطك.
سقسقة

بشكل عام ، يوفر نهج المقبس المخصص مزيدًا من الحرية للمطور. يجعل من الممكن تنفيذ جميع الأوامر في IMAP RFC3501. سوف يمنحك أيضًا تحكمًا أفضل في التعليمات البرمجية ، حيث لا يتعين عليك أن تتساءل عما يحدث "خلف الكواليس".

يمكن العثور على فئة imap_driver الكاملة التي قمنا بتنفيذها في هذه المقالة هنا. يمكن استخدامه كما هو ، وسيستغرق الأمر بضع دقائق فقط للمطور لكتابة وظيفة جديدة أو طلبها إلى خادم IMAP الخاص به. لقد قمت أيضًا بتضمين ميزة تصحيح الأخطاء في الفصل لإخراج مطول.