[PHP] PHP调用IMAP协议读取邮件类库
- 2019 年 10 月 10 日
- 笔记
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 .= "rn" . $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 = "rn"; 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(); } }