使用PHP构建IMAP电子邮件客户端

本文概述

  • 建立连接
  • IMAP基本语法
  • 实施基本命令
  • 实施高级命令
  • Gmail IMAP扩展
  • 总结
开发人员有时会遇到需要访问电子邮件邮箱的任务。在大多数情况下, 这是使用Internet消息访问协议或IMAP完成的。作为PHP开发人员, 我首先转向PHP的内置IMAP库, 但是该库存在错误, 无法调试或修改。也无法自定义IMAP命令以充分利用协议的功能。
因此, 今天, 我们将使用PHP从头开始创建一个可运行的IMAP电子邮件客户端。我们还将了解如何使用Gmail的特殊命令。
我们将在自定义类imap_driver中实现IMAP。在构建课程时, 我将解释每个步骤。你可以在文章末尾下载整个imap_driver.php。
建立连接IMAP是基于连接的协议, 通常在具有SSL安全性的TCP / IP上运行, 因此在进行任何IMAP调用之前, 我们必须打开连接。
我们需要知道我们要连接的IMAP服务器的URL和端口号。此信息通常在服务的网站或文档中进行广告宣传。例如, 对于Gmail, URL在端口993上为ssl://imap.gmail.com。
因为我们想知道初始化是否成功, 所以我们将类构造函数留空, 所有连接都将在自定义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工程任务组(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-STATUS00000001 OK [email  protected] authenticated (Success)

  • 失败:
00000001 NO [AUTHENTICATIONFAILED] Invalid credentials (Failure)

状态可以是” 正常” (表示成功), “ 否” (表示失败)或” 错误” (表示命令无效或语法错误)。
实施基本命令让我们做一个函数, 将命令发送到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 commandwhile ($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 whitespaceif (count($line_arr) > 0) { $code = array_shift($line_arr); // take the first segment from the response, which will be the line numberif (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对安全性非常严格:如果我们使用默认设置, 则Gmail不允许我们使用IMAP访问电子邮件帐户, 并尝试从帐户配置文件所在国家/地区以外的国家/地区访问该电子邮件帐户。但是很容易修复。只需在Gmail帐户中设置不太安全的设置即可, 如此处所述。
SELECT命令
现在, 让我们看看如何选择一个IMAP文件夹以对我们的电子邮件做一些有用的事情。感谢我们的command()方法, 该语法与LOGIN相似。我们改用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命令, 并以空格作为分隔符。例如, 如果要获取自2015年11月20日以来的所有电子邮件, 则必须传递以下命令:
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('j-M-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: < [email  protected]om> 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('~^([a-z][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提供了一系列特殊命令, 可以使我们的生活更加轻松。 Gmail的IMAP扩展程序命令列表在此处提供。我认为最重要的命令是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年12月起, Gmail经常将某些帐户的” 主要” 类别与” 更新” 类别混淆。这是一个尚未修复的Gmail错误。
总结你有邮件。怎么办?阅读如何使用PHP构建自定义IMAP电子邮件客户端, 并按照你的条款检查邮件。
鸣叫
总体而言, 自定义套接字方法为开发人员提供了更多自由。这样就可以在IMAP RFC3501中实现所有命令。它还使你可以更好地控制代码, 因为你不必怀疑” 幕后” 发生了什么。
【使用PHP构建IMAP电子邮件客户端】在本文中可以找到我们实现的完整imap_driver类。它可以原样使用, 开发人员只需几分钟即可向其IMAP服务器写入新功能或请求。我还在类中包括了调试功能, 用于输出详细信息。

    推荐阅读