本文概述
- 建立连接
- IMAP基本语法
- 实施基本命令
- 实施高级命令
- Gmail 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服务器写入新功能或请求。我还在类中包括了调试功能, 用于输出详细信息。
推荐阅读
- 同一台机器上的MySQL主从复制
- Android电商开发框架配置入口设计
- app开发-2
- app开发-3
- 跨app执行bindservice
- mybatis关于ORM的使用以及设计[DaoInterface 转换 Mapper代理对象]
- Android学习基础部分
- 架构模式中的Active Record和Data Mapper
- SpringBoot添加webapp目录