本文概述
开发人员有时会遇到需要访问电子邮件邮箱的任务。在大多数情况下, 这是使用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-STATUS
00000001 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 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对安全性非常严格:如果我们使用默认设置, 则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中实现所有命令。它还使你可以更好地控制代码, 因为你不必怀疑”幕后”发生了什么。
在本文中可以找到我们实现的完整imap_driver类。它可以原样使用, 开发人员只需几分钟即可向其IMAP服务器写入新功能或请求。我还在类中包括了调试功能, 用于输出详细信息。
评论前必须登录!
注册