[PHP] PHP调用IMAP协议读取邮件类库

socket.php 为连接socket的类库

imap.php 基于socket的imap协议封装

test.php 进行测试

require_once 'socket.php';
require_once 'imap.php';
$imap=new Sina_Mail_Net_Imap("imap.sina.net:143",30,30);
$imap->capability();
$imap->id(array(
'name' => 'SinaMail OtherMail Client',
'version' => '1',
'os' => 'SinaMail OtherMail',
'os-version' => '1.0',
));
$imap->login("xxxx@xxxxx","xxxx");
$folders=$imap->getList('', '*');
var_dump($folders);
$status = $imap->select('SENT');
var_dump($status);
$ls = $imap->fetch(array(), array('uid', 'internaldate', 'rfc822.size')); foreach($ls as $k=>$i){
$info=$imap->fetch(array($k), array('rfc822'));
}

imap.php

<?php
class Sina_Mail_Net_Imap {
const MAX_READ_SIZE = 100000000;
const PATTERN_REQUEST_STRING_SEQUENCE = '/{\d+}/';
const PATTERN_RESPONSE_STRING_SEQUENCE = '/{(\d+)}$/';
const SEQUENCE_PARAM_NAME = '[]';
const PARTIAL_PARAM_NAME = '<>';
const PARAM_NO = 1;
const PARAM_SINGLE = 2;
const PARAM_PAIR = 4;
const PARAM_LIST = 8;
const PARAM_STRING = 16;
const PARAM_NUMBER = 32;
const PARAM_DATE = 64;
const PARAM_FLAG = 128;
const PARAM_SEQUENCE = 256;
const PARAM_SEARCH = 512;
const PARAM_BODY = 1024;
const PARAM_PARTIAL = 2048;
const PARAM_EXCLUSIVE = 4096;
private static $statusKeywords = array(
'MESSAGES' => self::PARAM_NO,
'RECENT' => self::PARAM_NO,
'UIDNEXT' => self::PARAM_NO,
'UIDVALIDITY' => self::PARAM_NO,
'UNSEEN' => self::PARAM_NO,
);
private static $searchKeywords = array(
'ALL' => self::PARAM_NO,
'ANSWERED' => self::PARAM_NO,
'BCC' => 18, // self::PARAM_SINGLE | self::PARAM_STRING,
'BEFORE' => 66, // self::PARAM_SINGLE | self::PARAM_DATE,
'BODY' => 18, // self::PARAM_SINGLE | self::PARAM_STRING,
'CC' => 18, // self::PARAM_SINGLE | self::PARAM_STRING,
'DELETED' => self::PARAM_NO,
'DRAFT' => self::PARAM_NO,
'FLAGGED' => self::PARAM_NO,
'FROM' => 18, // self::PARAM_SINGLE | self::PARAM_STRING,
'HEADER' => 20, // self::PARAM_PAIR | self::PARAM_STRING,
'KEYWORD' => 130, // self::PARAM_SINGLE | self::PARAM_FLAG,
'LARGER' => 34, // self::PARAM_SINGLE | self::PARAM_NUMBER,
'NEW' => self::PARAM_NO,
'NOT' => 514, // self::PARAM_SINGLE | self::PARAM_SEARCH,
'OLD' => self::PARAM_NO,
'ON' => 66, // self::PARAM_SINGLE | self::PARAM_DATE,
'OR' => 516, // self::PARAM_PAIR | self::PARAM_SEARCH,
'RECENT' => self::PARAM_NO,
'SEEN' => self::PARAM_NO,
'SENTBEFORE' => 66, // self::PARAM_SINGLE | self::PARAM_DATE,
'SENTON' => 66, // self::PARAM_SINGLE | self::PARAM_DATE,
'SENTSINCE' => 66, // self::PARAM_SINGLE | self::PARAM_DATE,
'SINCE' => 66, // self::PARAM_SINGLE | self::PARAM_DATE,
'SMALLER' => 34, // self::PARAM_SINGLE | self::PARAM_NUMBER,
'SUBJECT' => 18, // self::PARAM_SINGLE | self::PARAM_STRING,
'TEXT' => 18, // self::PARAM_SINGLE | self::PARAM_STRING,
'TO' => 18, // self::PARAM_SINGLE | self::PARAM_STRING,
'UID' => 258, // self::PARAM_SINGLE | self::PARAM_SEQUENCE,
'UNANSWERED' => self::PARAM_NO,
'UNDELETED' => self::PARAM_NO,
'UNDRAFT' => self::PARAM_NO,
'UNFLAGGED' => self::PARAM_NO,
'UNKEYWORD' => 130, // self::PARAM_SINGLE | self::PARAM_FLAG,
'UNSEEN' => self::PARAM_NO,
);
private static $fetchKeywords = array(
'ALL' => 4097, // self::PARAM_NO | self::PARAM_EXCLUSIVE,
'FAST' => 4097, // self::PARAM_NO | self::PARAM_EXCLUSIVE,
'FULL' => 4097, // self::PARAM_NO | self::PARAM_EXCLUSIVE,
'BODY' => 3075, // self::PARAM_NO | self::PARAM_SINGLE | self::PARAM_BODY | self::PARAM_PARTIAL,
'BODY.PEEK' => 3074, // self::PARAM_SINGLE | self::PARAM_BODY | self::PARAM_PARTIAL,
'BODYSTRUCTURE' => self::PARAM_NO,
'ENVELOPE' => self::PARAM_NO,
'FLAGS' => self::PARAM_NO,
'INTERNALDATE' => self::PARAM_NO,
'RFC822' => self::PARAM_NO,
'RFC822.HEADER' => self::PARAM_NO,
'RFC822.SIZE' => self::PARAM_NO,
'RFC822.TEXT' => self::PARAM_NO,
'UID' => self::PARAM_NO,
);
private $sock = null;
private $timeout = 120;
private $ts = 0;
private $tagName = 'A';
private $tagId = 0;
private $capabilities = array();
private $folders = array();
private $currentFolder = null;
private $currentCommand = null;
private $lastSend = '';
private $lastRecv = '';
public function __construct($uri, $timeout = null, $connTimeout = null) {
$this->sock = new Socket($uri, $timeout);
// $t = intval($timeout);
// if ($t > 0) {
// $this->timeout = $t;
// }
$this->connect($connTimeout);
}
public function __destruct() {
} public function connect($timeout) {
$this->sock->connect($timeout);
$this->getResponse();
} public function capability() {
$res = $this->request('capability');
if (isset($res[0][0]) && $res[0][0] == '*' &&
isset($res[0][1]) && strcasecmp($res[0][1], 'capability') == 0) {
for ($i = 2, $n = count($res[0]); $i < $n; ++$i) {
$this->capabilities[strtoupper($res[0][$i])] = true;
}
}
} public function id($data) {
if (isset($this->capabilities['ID'])) {
$this->request('id', array($data));
}
}
public function login($username, $password) {
try {
$this->request('login', array($username, $password));
} catch (Exception $ex) {
throw new Exception($ex->getMessage(), $ex->getCode());
}
} public function logout() {
$this->request('logout');
} public function getList($reference = '', $wildcard = '') {
$res = $this->request('list', array($reference, $wildcard));
foreach ($res as &$r) {
if (isset($r[0]) && $r[0] == '*' &&
isset($r[1]) && strcasecmp($r[1], 'list') == 0 &&
isset($r[4])) {
$this->folders[$r[4]] = array(
'id' => $r[4],
'name' => mb_convert_encoding($r[4], 'UTF-8', 'UTF7-IMAP'),
'path' => $r[3],
'attr' => $r[2],
);
}
}
return $this->folders;
} public function status($folder, $data) {
$args = $this->formatArgsForCommand($data, self::$statusKeywords);
$res = $this->request('status', array($folder, $args));
$status = array();
if (!empty($res)) {
foreach ($res as &$r) {
if (isset($r[0]) && $r[0] == '*' &&
isset($r[1]) && strcasecmp($r[1], 'status') == 0 &&
isset($r[3]) && is_array($r[3])) {
for ($i = 0, $n = count($r[3]); $i < $n; $i += 2) {
$status[$r[3][$i]] = $r[3][$i + 1];
}
}
}
}
return $status;
}
public function select($folder) {
$res = $this->request('select', array($folder));
$status = array();
if (!empty($res)) {
foreach ($res as $r) {
if (isset($r[0]) && $r[0] == '*') {
if (isset($r[1]) && isset($r[2])) {
if (strcasecmp($r[1], 'ok') == 0 && is_array($r[2])) {
for ($i = 0, $n = count($i); $i < $n; $i += 2) {
$status[$r[2][$i]] = $r[2][$i + 1];
}
} elseif (ctype_digit($r[1])) {
$status[$r[2]] = $r[1];
} else {
$status[$r[1]] = $r[2];
}
}
}
}
}
$this->currentFolder = $folder;
return $status;
}
public function search($data) {
$args = $this->formatArgsForCommand($data, self::$searchKeywords, true);
$res = $this->request('search', $args);
$ls = array();
foreach ($res as &$r) {
if (isset($r[0]) && $r[0] == '*' &&
isset($r[1]) && strcasecmp($r[1], 'search') == 0) {
for ($i = 2, $n = count($r); $i < $n; ++$i) {
$ls[] = $r[$i];
}
}
}
return $ls;
}
public function fetch($seq, $data) {
$seqStr = $this->formatSequence($seq);
$args = $this->formatArgsForCommand($data, self::$fetchKeywords);
$res = $this->request('fetch', array($seqStr, $args));
// var_dump($res);
$ls = array();
foreach ($res as &$r) {
if (isset($r[0]) && $r[0] == '*' &&
isset($r[1]) && is_numeric($r[1]) &&
isset($r[2]) && strcasecmp($r[2], 'fetch') == 0 &&
isset($r[3]) && is_array($r[3])) {
$a = array();
for ($i = 0, $n = count($r[3]); $i < $n; $i += 2) {
$key = $r[3][$i];
if (((strcasecmp($key, 'BODY') == 0 && isset($args['BODY']) && is_array($args['BODY'])) ||
(strcasecmp($key, 'BODY.PEEK') == 0 && isset($args['BODY.PEEK']) && is_array($args['BODY.PEEK']))) &&
is_array($r[3][$i + 1])) {
$key = trim($this->formatRequestArray(array($key => $r[3][$i + 1]), $placeHolder, 0), '()');
$i++;
} else {
$key = $r[3][$i];
}
$a[$key] = $r[3][$i + 1];
}
if (!empty($a)) {
$ls[$r[1]] = $a;
}
}
}
return $ls;
}
private function nextTag() {
$this->tagId++;
return sprintf('%s%d', $this->tagName, $this->tagId);
}
private function request($cmd, $args = array()) {
$this->currentCommand = strtoupper(trim($cmd));
$tag = $this->nextTag();
$req = $tag . ' ' . $this->currentCommand; // 格式化参数列表
$strSeqList = array();
if (is_array($args)) {
$argStr = $this->formatRequestArray($args, $strSeqList);
} else {
$argStr = $this->formatRequestString($args, $strSeqList);
}
//$argStr = $this->makeRequest($args, $strSeqList);
$subReqs = array();
if (isset($argStr[0])) {
$req .= ' ' . $argStr;
// 如果参数中包括需要序列化的数据,根据序列化标识{length}将命令拆分成多条
if (!empty($strSeqList) && preg_match_all(self::PATTERN_REQUEST_STRING_SEQUENCE, $req, $matches, PREG_OFFSET_CAPTURE)) {
$p = 0;
foreach ($matches[0] as $m) {
$e = $m[1] + strlen($m[0]);
$subReqs[] = substr($req, $p, $e - $p);
$p = $e;
}
$subReqs[] = substr($req, $p);
// 校验序列化标识与需要序列化的参数列表数量是否一致
if (count($subReqs) != count($strSeqList) + 1) {
$subReqs = null;
}
}
} if (empty($subReqs)) {
// 处理单条命令
$this->sock->writeLine($req);
$this->lastSend = $req;
$res = $this->getResponse($tag);
} else {
// 处理多条命令
$this->lastSend = '';
foreach ($subReqs as $id => $req) {
$this->sock->writeLine($req);
$this->lastSend .= $req;
$res = $this->getResponse($tag);
if (isset($res[0][0]) && $res[0][0] == '+') {
$this->sock->write($strSeqList[$id]);
$this->lastSend .= "\r\n" . $strSeqList[$id];
} else {
// 如果服务器端返回其他相应,则定制后续执行
break;
}
}
}
return $res;
} private function formatRequestString($s, &$strSeqList) {
$s = trim($s);
$needQuote = false;
if (!isset($s[0])) {
$needQuote = true;
} elseif ($this->currentCommand == 'ID') {
$needQuote = true;
} else {
// 参数包含多行时,需要进行序列化
if (strpos($s, "\r") !== false || strpos($s, "\n") !== false) {
$strSeqList[] = $s;
$s = sprintf('{%d}', strlen($s));
} else {
// 参数包含双引号或空格时,需要将使用双引号括起来
if (strpos($s, '"') !== false) {
$s = addcslashes($s, '"');
$needQuote = true;
}
if (strpos($s, ' ') !== false) {
$needQuote = true;
}
}
}
if ($needQuote) {
return sprintf('"%s"', $s);
} else {
return $s;
}
}
private function formatRequestArray($arr, &$strSeqList, $level = -1) {
$a = array();
foreach ($arr as $k => $v) {
$isBody = false;
$supportPartial = false;
$partialStr = '';
if ($this->currentCommand == 'FETCH') {
// 识别是否body命令,是否可以包含<partial>
$kw = strtoupper($k);
if (isset(self::$fetchKeywords[$kw]) && (self::$fetchKeywords[$kw] & self::PARAM_BODY) > 0) {
$isBody = true;
}
if (isset(self::$fetchKeywords[$kw]) && (self::$fetchKeywords[$kw] & self::PARAM_PARTIAL) > 0) {
$supportPartial = true;
}
}
if (is_array($v)) {
if ($supportPartial && isset($v[self::PARTIAL_PARAM_NAME]) && is_array($v[self::PARTIAL_PARAM_NAME])) {
// 处理包含<partial>的命令
foreach ($v[self::PARTIAL_PARAM_NAME] as $spos => $mlen) {
$partialStr = sprintf('<%d.%d>', $spos, $mlen);
}
unset($v[self::PARTIAL_PARAM_NAME]);
}
$s = $this->formatRequestArray($v, $strSeqList, $level + 1);
} else {
$s = $this->formatRequestString($v, $strSeqList);
}
if (!is_numeric($k)) {
// 字典方式需要包含键名
$k = $this->formatRequestString($k, $strSeqList);
if ($isBody) {
$s = $k . $s;
} else {
$s = $k . ' ' . $s;
}
// 包含<partial>
if ($supportPartial) {
$s .= $partialStr;
}
}
$a[] = $s;
}
if ($level < 0) {
return implode(' ', $a);
} elseif (($level % 2) == 0) {
return sprintf('(%s)', implode(' ', $a));
} else {
return sprintf('[%s]', implode(' ', $a));
}
}
private function formatSequence($seq) {
$n = count($seq);
if ($n == 0) {
return '1:*';
} elseif ($n == 1) {
if (isset($seq[0])) {
return strval($seq[0]);
} else {
foreach ($seq as $k => $v) {
return $k . ':' . $v;
}
}
} else {
return implode(',', $seq);
}
}
private function formatArgsForCommand(&$data, &$fields, $asList = false) {
$args = array();
foreach ($data as $k => $v) {
if (is_numeric($k)) {
// 无值参数
$name = strtoupper($v);
if (isset($fields[$name])) {
// 对于排他性属性,直接返回
if (($fields[$name] & self::PARAM_EXCLUSIVE) > 0) {
return $name;
} elseif (($fields[$name] & self::PARAM_NO) > 0) {
$args[] = $name;
}
}
} elseif ($k == self::SEQUENCE_PARAM_NAME) {
// 序列
$args[] = $this->formatSequence($v);
} else {
$name = strtoupper($k);
if (isset($fields[$name])) {
$paramType = $fields[$name];
// 格式化参数类型
if (($paramType & self::PARAM_DATE) > 0) {
$v = date('j-M-Y', $v);
} elseif (($paramType & self::PARAM_SEQUENCE) > 0) {
$v = $this->formatSequence($v);
} // 根据参数定义拼组参数列表
if (($paramType & self::PARAM_SINGLE) > 0) {
// 单值参数
if ($asList) {
$args[] = $name;
$args[] = $v;
} else {
$args[$name] = $v;
}
} elseif (($paramType & self::PARAM_PAIR) > 0) {
// 键值对参数
if (is_array($v)) {
foreach ($v as $x => $y) {
$pk = $x;
$pv = $y;
break;
}
} else {
$pk = $v;
$pv = '';
}
if ($asList) {
$args[] = $name;
$args[] = $pk;
$args[] = $pv;
} else {
$args[$name] = array($pk => $pv);
}
} elseif (($paramType & self::PARAM_LIST) > 0) {
// 列表参数
if ($asList) {
$args[] = $name;
foreach ($v as $i) {
$args[] = $i;
}
} else {
$args[$name] = $v;
}
} elseif (($paramType & self::PARAM_NO) > 0) {
// 无值参数
$args[] = $name;
}
}
}
}
return $args;
}
private function getResponse($tag = null) {
$r = array();
$readMore = true;
while ($readMore) {
$ln = trim($this->sock->readLine());
if (!isset($ln[0])) {
// connection closed or read empty string, throw exception to avoid dead loop and reconnect
throw new Exception('read response failed');
} $matches = null;
$strSeqKey = null;
$strSeq = null;
if (preg_match(self::PATTERN_RESPONSE_STRING_SEQUENCE, $ln, $matches)) {
$strSeqKey = $matches[0];
$this->readSequence($ln, $strSeq, $matches[1]);
}
$this->lastRecv = $ln; // 区分处理不同种响应
switch ($ln[0]) {
case '*':
$r[] = $this->parseLine($ln, $strSeqKey, $strSeq);
if (!$tag) {
$readMore = false;
}
break;
case $this->tagName:
$r[] = $this->parseLine($ln);
if ($tag) {
$readMore = false;
} else { }
break;
case '+':
$r[] = $this->parseLine($ln);
$readMore = false;
break;
default:
$r[] = $ln;
break;
}
} //var_dump($this->lastSend, $this->lastRecv); // 无响应数据
if (empty($r)) {
throw new Exception('no response');
} $last = $r[count($r) - 1];
if (isset($last[0]) && $last[0] == '+') {
// 继续发送请求数据
} else {
if ($tag) {
if (!isset($last[0]) || strcasecmp($last[0], $tag) != 0) {
throw new Exception('tag no match');
}
} else {
if (!isset($last[0]) || strcasecmp($last[0], '*') != 0) {
throw new Exception('untag no match');
}
}
if (isset($last[1])) {
// 处理响应出错的情况
if (strcasecmp($last[1], 'bad') == 0) {
throw new Exception(implode(' ', $last));
} elseif (strcasecmp($last[1], 'no') == 0) {
throw new Exception(implode(' ', $last));
}
}
//$this->currentCommand = null;
} return $r;
} private function readSequence(&$ln, &$strSeq, $seqLength) {
// 对于字符串序列,读取完整内容后再拼接响应
$readLen = 0;
$st = microtime(true);
// 网络请求多次读取字符串序列内容,直到读好为止
while ($readLen < $seqLength) {
$sb = $this->sock->read($seqLength - $readLen);
if (isset($sb[0])) {
$strSeq .= $sb;
$readLen = strlen($strSeq);
}
if ((microtime(true) - $st) > $this->timeout) {
throw new Exception('read sequence timeout');
}
}
// 读取字符串序列后的剩余命令
$leftLn = rtrim($this->sock->readLine());
$ln = $ln . $leftLn;
} private function parseLine($ln, $strSeqKey = null, $strSeq = null) {
$r = array();
$p =& $r;
$stack = array();
$token = '';
$escape = false;
$inQuote = false;
for ($i = 0, $n = strlen($ln); $i < $n; ++$i) {
$ch = $ln[$i];
if ($ch == '"') {
// 处理双引号括起的字符串
if (!$inQuote) {
$inQuote = true;
} else {
$inQuote = false;
}
} elseif ($inQuote) {
// 对于括起的字符串,处理双引号转义
if ($ch == '\\') {
if (!$escape && isset($ln[$i + 1]) && $ln[$i + 1] == '"') {
$token .= '"';
$i++;
} else {
$token .= $ch;
$escape = !$escape;
}
} else {
$token .= $ch;
}
} elseif ($ch == ' ' ||
$ch == '(' || $ch == ')' ||
$ch == '[' || $ch == ']') {
// 处理子列表
if (isset($token[0])) {
// 将字符串序列标识:{length},替换为真实字符串
if ($strSeqKey && $token == $strSeqKey) {
$p[] = $strSeq;
} else {
$p[] = $token;
}
$token = '';
}
if ($ch == '(' || $ch == '[') {
$p[] = array();
$stack[] =& $p;
$p =& $p[count($p) - 1];
} elseif ($ch == ')' || $ch == ']') {
$p =& $stack[count($stack) - 1];
array_pop($stack);
}
} else {
// 处理字符串字面量
$token .= $ch;
}
}
if (isset($token[0])) {
// 将字符串序列标识:{length},替换为真实字符串
if ($strSeqKey && $token == $strSeqKey) {
$p[] = $strSeq;
} else {
$p[] = $token;
}
}
return $r;
}
} // end of php

socket.php

<?php
class Socket{
const DEFAULT_READ_SIZE = 8192;
const CRTL = "\r\n";
private $uri = null;
private $timeout = null;
private $sock = null;
private $connected = false;
public function __construct($uri, $timeout = null){
$this->uri = $uri;
$this->timeout = $this->formatTimeout($timeout);
} public function connect($timeout = null, $retryTimes = null){
if ($this->connected) {
$this->close();
} $connTimeout = $this->formatTimeout($timeout, $this->timeout);
$retryTimes = intval($retryTimes);
if ($retryTimes < 1) {
$retryTimes = 1;
}
for ($i = 0; $i < $retryTimes; ++$i) {
$this->sock = stream_socket_client(
$this->uri, $errno, $error, $connTimeout);
if ($this->sock) {
break;
}
}
if (!$this->sock) { }
stream_set_timeout($this->sock, $this->timeout);
$this->connected = true;
} public function read($size){
assert($this->connected); $buf = fread($this->sock, $size);
if ($buf === false) {
$this->handleReadError();
} return $buf;
} public function readLine(){
assert($this->connected);
$buf = '';
while (true) {
$s = fgets($this->sock, self::DEFAULT_READ_SIZE);
if ($s === false) {
$this->checkReadTimeout();
break;
}
$n = strlen($s);
if (!$n) {
break;
}
$buf .= $s;
if ($s[$n - 1] == "\n") {
break;
}
} return $buf;
} public function readAll(){
assert($this->connected);
$buf = '';
while (true) {
$s = fread($this->sock, self::DEFAULT_READ_SIZE);
if ($s === false) {
$this->handleReadError();
} if (!isset($s[0])) {
break;
} $buf .= $s;
} return $buf;
} public function write($s){
assert($this->connected);
$n = strlen($s);
$w = 0;
while ($w < $n) {
$buf = substr($s, $w, self::DEFAULT_READ_SIZE);
$r = fwrite($this->sock, $buf);
if (!$r) {
$this->close();
}
$w += $r;
}
}
public function writeLine($s){
$this->write($s . self::CRTL);
}
public function close() {
if ($this->connected) {
fclose($this->sock);
$this->connected = false;
}
}
private function formatTimeout($timeout, $default = null){
$t = intval($timeout);
if ($t <= 0) {
if (!$default) {
$t = ini_get('default_socket_timeout');
} else {
$t = $default;
}
}
return $t;
} private function checkReadTimeout(){
$meta = stream_get_meta_data($this->sock);
if (isset($meta['timed_out'])) {
$this->close();
}
} private function handleReadError(){
$this->checkReadTimeout();
$this->close();
}
}
上一篇:dctcp-ns2-patch


下一篇:Oracle计算时间差表达式