%PDF- %PDF-
Direktori : /home/bitrix/www/bitrix/modules/mail/lib/ |
Current File : /home/bitrix/www/bitrix/modules/mail/lib/imap.php |
<?php namespace Bitrix\Mail; use Bitrix\Main; use Bitrix\Main\Text; use Bitrix\Main\Text\BinaryString; use Bitrix\Main\Text\Encoding; use Bitrix\Main\Localization\Loc; Loc::loadMessages(__FILE__); class Imap { const ERR_CONNECT = 101; const ERR_REJECTED = 102; const ERR_COMMUNICATE = 103; const ERR_EMPTY_RESPONSE = 104; const ERR_BAD_SERVER = 105; const ERR_STARTTLS = 201; const ERR_COMMAND_REJECTED = 202; const ERR_CAPABILITY = 203; const ERR_AUTH = 204; const ERR_AUTH_MECH = 205; const ERR_AUTH_OAUTH = 206; const ERR_LIST = 207; const ERR_SELECT = 208; const ERR_SEARCH = 209; const ERR_FETCH = 210; const ERR_APPEND = 211; const ERR_STORE = 212; protected $stream, $errors; protected $sessState, $sessCapability, $sessCounter, $sessUntagged, $sessMailbox; protected $options = array(); protected static $atomRegex = '[^\x00-\x20\x22\x25\x28-\x2a\x5c\x5d\x7b\x7f-\xff]+'; protected static $qcharRegex = '[^\x00\x0a\x0d\x22\x5c\x80-\xff]|\x5c[\x5c\x22]'; protected static $astringRegex = '[^\x00-\x20\x22\x25\x28-\x2a\x5c\x7b\x7f-\xff]+'; public function __construct($host, $port, $tls, $strict, $login, $password, $encoding = null) { $this->reset(); $strict = PHP_VERSION_ID < 50600 ? false : (bool) $strict; $this->options = array( 'host' => $host, 'port' => $port, 'tls' => $tls, 'socket' => sprintf('%s://%s:%s', ($tls ? 'ssl' : 'tcp'), $host, $port), 'timeout' => \COption::getOptionInt('mail', 'connect_timeout', B_MAIL_TIMEOUT), 'context' => stream_context_create(array( 'ssl' => array( 'verify_peer' => $strict, 'verify_peer_name' => $strict ) )), 'login' => $login, 'password' => $password, 'encoding' => $encoding ?: LANG_CHARSET, ); } public function __destruct() { $this->disconnect(); } protected function disconnect() { if (!is_null($this->stream)) @fclose($this->stream); unset($this->stream); } protected function reset() { $this->disconnect(); unset($this->errors); unset($this->sessState); unset($this->sessCapability); $this->sessCounter = 0; $this->sessUntagged = array(); $this->sessMailbox = array( 'name' => null, 'exists' => null, 'uidvalidity' => null, 'permanentflags' => null, ); $this->errors = new Main\ErrorCollection(); } public function getState() { return $this->sessState; } public function connect(&$error) { $error = null; if ($this->sessState) return true; $resource = @stream_socket_client( $this->options['socket'], $errno, $errstr, $this->options['timeout'], STREAM_CLIENT_CONNECT, $this->options['context'] ); if ($resource === false) { $error = $this->errorMessage(Imap::ERR_CONNECT, $errno ?: null); return false; } $this->stream = $resource; if ($this->options['timeout'] > 0) stream_set_timeout($this->stream, $this->options['timeout']); $prompt = $this->readLine(); if ($prompt !== false && preg_match('/^\* (OK|PREAUTH)/i', $prompt, $matches)) { if ($matches[1] == 'OK') $this->sessState = 'no_auth'; elseif ($matches[1] == 'PREAUTH') $this->sessState = 'auth'; } else { if ($prompt === false) $error = Imap::ERR_EMPTY_RESPONSE; elseif (preg_match('/^\* BYE/i', $prompt)) $error = Imap::ERR_REJECTED; else $error = Imap::ERR_BAD_SERVER; $error = $this->errorMessage(array(Imap::ERR_CONNECT, $error)); return false; } if (!$this->capability($error)) return false; if (!$this->options['tls'] && preg_match('/ \x20 STARTTLS ( \x20 | \r\n ) /ix', $this->sessCapability)) { if (!$this->starttls($error)) return false; } return true; } protected function starttls(&$error) { $error = null; if (!$this->sessState) { $error = $this->errorMessage(Imap::ERR_STARTTLS); return false; } $this->executeCommand('STARTTLS', $error); if (!$error) { if (stream_socket_enable_crypto($this->stream, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { if (!$this->capability($error)) return false; } else { $this->reset(); $error = $this->errorMessage(Imap::ERR_STARTTLS); return false; } } return true; } protected function capability(&$error) { $error = null; if (!$this->sessState) { $error = $this->errorMessage(Imap::ERR_CAPABILITY); return false; } $response = $this->executeCommand('CAPABILITY', $error); if ($error) { $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error; $error = $this->errorMessage(array(Imap::ERR_CAPABILITY, $error), $response); return false; } $regex = '/^ \* \x20 CAPABILITY /ix'; foreach ($this->getUntagged($regex, true) as $item) $this->sessCapability = $item[0]; return true; } public function authenticate(&$error) { $error = null; if (!$this->connect($error)) return false; if (in_array($this->sessState, array('auth', 'select'))) return true; $mech = false; if (preg_match('/ \x20 AUTH=XOAUTH2 ( \x20 | \r\n ) /ix', $this->sessCapability)) { $token = Helper\OAuth::getTokenByMeta($this->options['password']); if (!empty($token)) { $mech = 'oauth'; } else if (false === $token) { $error = $this->errorMessage(array(Imap::ERR_AUTH, Imap::ERR_AUTH_OAUTH)); return false; } } if ($mech == false) { if (preg_match('/ \x20 AUTH=PLAIN ( \x20 | \r\n ) /ix', $this->sessCapability)) $mech = 'plain'; elseif (!preg_match('/ \x20 LOGINDISABLED ( \x20 | \r\n ) /ix', $this->sessCapability)) $mech = 'login'; } if (!$mech) { $error = $this->errorMessage(array(Imap::ERR_AUTH, Imap::ERR_AUTH_MECH)); return false; } if ($mech == 'oauth') { $response = $this->executeCommand('AUTHENTICATE XOAUTH2', $error); if (strpos($response, '+') !== 0) { $error = $error == Imap::ERR_COMMAND_REJECTED ? Imap::ERR_AUTH_MECH : $error; $error = $this->errorMessage(array(Imap::ERR_AUTH, $error), $response); return false; } $response = $this->exchange(base64_encode(sprintf( "user=%s\x01auth=Bearer %s\x01\x01", $this->options['login'], $token )), $error); if (strpos($response, '+') === 0) $response = $this->exchange("\r\n", $error); } elseif ($mech == 'plain') { $response = $this->executeCommand('AUTHENTICATE PLAIN', $error); if (strpos($response, '+') !== 0) { $error = $error == Imap::ERR_COMMAND_REJECTED ? Imap::ERR_AUTH_MECH : $error; $error = $this->errorMessage(array(Imap::ERR_AUTH, $error), $response); return false; } $response = $this->exchange(base64_encode(sprintf( "\x00%s\x00%s", $this->options['login'], $this->options['password'] )), $error); } else // if ($mech == 'login') { $response = $this->executeCommand(sprintf( 'LOGIN %s %s', static::prepareString($this->options['login']), static::prepareString($this->options['password']) ), $error); } if ($error) { $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error; $error = $this->errorMessage(array(Imap::ERR_AUTH, $error), $response); return false; } $this->sessState = 'auth'; if (!$this->capability($error)) return false; return true; } public function select($mailbox, &$error) { $error = null; if (!$this->authenticate($error)) return false; if ($this->sessState == 'select' && $mailbox == $this->sessMailbox['name']) return $this->sessMailbox; $response = $this->executeCommand(sprintf( 'SELECT "%s"', static::escapeQuoted($this->encodeUtf7Imap($mailbox)) ), $error); if ($error) { $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error; $error = $this->errorMessage(array(Imap::ERR_SELECT, $error), $response); return false; } $this->sessState = 'select'; $this->sessMailbox = array( 'name' => $mailbox, 'exists' => null, 'uidvalidity' => null, ); $regex = '/^ \* \x20 ( \d+ ) \x20 EXISTS /ix'; foreach ($this->getUntagged($regex, true) as $item) $this->sessMailbox['exists'] = $item[1][1]; $regex = '/^ \* \x20 OK \x20 \[ UIDVALIDITY \x20 ( \d+ ) \] /ix'; foreach ($this->getUntagged($regex, true) as $item) $this->sessMailbox['uidvalidity'] = $item[1][1]; $regex = sprintf( '/^ \* \x20 OK \x20 \[ PERMANENTFLAGS \x20 \( ( ( \x5c? %1$s | \x5c \* ) ( \x20 (?2) )* )? \) \] /ix', self::$atomRegex ); foreach ($this->getUntagged($regex, true) as $item) { $this->sessMailbox['permanentflags'] = explode("\x20", $item[1][1]); } if (!$this->capability($error)) return false; return $this->sessMailbox; } public function examine($mailbox, &$error) { $error = null; if (!$this->authenticate($error)) return false; $response = $this->executeCommand(sprintf( 'EXAMINE "%s"', static::escapeQuoted($this->encodeUtf7Imap($mailbox)) ), $error); if ($error) { $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error; $error = $this->errorMessage(array(Imap::ERR_SELECT, $error), $response); return false; } $result = array(); $regex = '/^ \* \x20 ( \d+ ) \x20 EXISTS /ix'; foreach ($this->getUntagged($regex, true) as $item) $result['exists'] = $item[1][1]; $regex = '/^ \* \x20 OK \x20 \[ UIDVALIDITY \x20 ( \d+ ) \] /ix'; foreach ($this->getUntagged($regex, true) as $item) $result['uidvalidity'] = $item[1][1]; $regex = sprintf( '/^ \* \x20 OK \x20 \[ PERMANENTFLAGS \x20 \( ( ( \x5c? %1$s | \x5c \* ) ( \x20 (?2) )* )? \) \] /ix', self::$atomRegex ); foreach ($this->getUntagged($regex, true) as $item) { $result['permanentflags'] = explode("\x20", $item[1][1]); } return $result; } /** * Connects to server and authenticate client * * @param string &$error Error message. * @return boolean */ public function singin(&$error) { $error = null; return $this->authenticate($error); } public function fetch($uid = false, $mailbox, $range, $select, &$error) { $error = null; if (!preg_match('/(([1-9]\d*|\*)(:(?2))?)(,(?1))*/', $range)) { return false; } if (empty($select)) { $select = '(FLAGS)'; } else if (is_array($select)) { $select = sprintf('(%s)', join(' ', $select)); } if (!$this->select($mailbox, $error)) { return false; } $list = array(); if ($this->sessMailbox['exists'] > 0) { $fetchUntaggedRegex = '/^ \* \x20 ( \d+ ) \x20 FETCH \x20 \( ( .+ ) \) \r\n $/isx'; $this->getUntagged($fetchUntaggedRegex, true); $response = $this->executeCommand( sprintf('%sFETCH %s %s', $uid ? 'UID ' : '', $range, $select), $error ); if ($error) { $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error; $error = $this->errorMessage(array(Imap::ERR_FETCH, $error), $response); return false; } $shiftName = function (&$item) { $result = false; $regex = sprintf( '/^ ( [a-z0-9]+ (?: \. [a-z0-9]+ )* (?: \[ (?: [a-z0-9]+ (?: \. [a-z0-9]+ )* (?: \x20 %s )* )? \] )? (?: < \d+ > )? ) \x20 /ix', self::$astringRegex ); if (preg_match($regex, $item, $matches)) { $result = $matches[1]; $item = BinaryString::getSubstring($item, BinaryString::getLength($matches[0])); } return $result; }; $shiftValue = function (&$item) use (&$shiftValue) { $result = false; $tail = ' (?= [\x20)] | $ ) \x20? '; if (BinaryString::getSubstring($item, 0, 1) === '(') { $item = BinaryString::getSubstring($item, 1); $result = array(); while (BinaryString::getLength($item) > 0 && BinaryString::getSubstring($item, 0, 1) !== ')') { $subresult = $shiftValue($item); if (false !== $subresult) { $result[] = $subresult; } else { return false; } } if (preg_match('/^ \) (?= [\x20()] | $ ) \x20? /ix', $item, $matches)) { $item = BinaryString::getSubstring($item, BinaryString::getLength($matches[0])); } else { return false; } } else if (preg_match('/^ { ( \d+ ) } \r\n /ix', $item, $matches)) { $item = BinaryString::getSubstring($item, BinaryString::getLength($matches[0])); if (BinaryString::getLength($item) >= $matches[1]) { $result = BinaryString::getSubstring($item, 0, $matches[1]); $item = BinaryString::getSubstring($item, $matches[1]); if (preg_match(sprintf('/^ %s /ix', $tail), $item, $matches)) { $item = BinaryString::getSubstring($item, BinaryString::getLength($matches[0])); } else { return false; } } } else if (preg_match(sprintf('/^ NIL %s /ix', $tail), $item, $matches)) { $result = null; $item = BinaryString::getSubstring($item, BinaryString::getLength($matches[0])); } else if (preg_match(sprintf('/^ " ( (?: %s )* ) " %s /ix', self::$qcharRegex, $tail), $item, $matches)) { $result = self::unescapeQuoted($matches[1]); $item = BinaryString::getSubstring($item, BinaryString::getLength($matches[0])); } else if (preg_match(sprintf('/^ ( \x5c? %s ) %s /ix', self::$astringRegex, $tail), $item, $matches)) { $result = $matches[1]; $item = BinaryString::getSubstring($item, BinaryString::getLength($matches[0])); } return $result; }; foreach ($this->getUntagged($fetchUntaggedRegex, true) as $item) { $data = array( 'id' => $item[1][1], ); while (BinaryString::getLength($item[1][2]) > 0) { if (($name = $shiftName($item[1][2])) !== false) { if (($value = $shiftValue($item[1][2])) !== false) { $data[$name] = $value; continue; } } break; } $list[$data['id']] = $data; } } if (!preg_match('/[:,]/', $range)) { $list = reset($list); } return $list; } /** * Returns unseen messages count * * @param string $mailbox Mailbox name. * @param string &$error Error message. * @return int|false */ public function getUnseen($mailbox, &$error) { $error = null; if (!$this->select($mailbox, $error)) { return false; } $unseen = 0; if (!($this->sessMailbox['exists'] > 0)) { return $unseen; } $response = $this->executeCommand('SEARCH UNSEEN', $error); if ($error) { $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error; $error = $this->errorMessage(array(Imap::ERR_SEARCH, $error), $response); return false; } $regex = '/^ \* \x20 SEARCH \x20 ( .+ ) \r\n $ /ix'; foreach ($this->getUntagged($regex, true) as $item) { $unseen = preg_match_all('/\d+/', $item[1][1]); } return $unseen; } public function getNew($mailbox, $uidMin, $uidMax, &$error) { $error = null; if (!($uidMin <= $uidMax)) { return false; } if (!$this->select($mailbox, $error)) { return false; } $new = 0; if (!($this->sessMailbox['exists'] > 0)) { return $new; } if ($uidMax < 1) { return $this->sessMailbox['exists']; } $range = $this->getUidRange($mailbox, $error); if (empty($range)) { return false; } list($min, $max) = $range; $searches = array(); if ($uidMin > 1 && $uidMin > $min) { $searches[] = sprintf('%u:%u', $min, $uidMin - 1); } if ($uidMax > 0 && $uidMax < $max) { $searches[] = sprintf('%u:%u', $uidMax + 1, $max); } if (!empty($searches)) { $response = $this->executeCommand(sprintf('SEARCH UID %s', join(',', $searches)), $error); if ($error) { $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error; $error = $this->errorMessage(array(Imap::ERR_SEARCH, $error), $response); return false; } $regex = '/^ \* \x20 SEARCH \x20 ( .+ ) \r\n $ /ix'; foreach ($this->getUntagged($regex, true) as $item) { $new = preg_match_all('/\d+/', $item[1][1]); } } return $new; } public function getUidRange($mailbox, &$error) { $error = null; if (!$this->select($mailbox, $error)) { return false; } if (!($this->sessMailbox['exists'] > 0)) { return false; } $range = $this->fetch(false, $mailbox, '1,*', '(UID)', $error); if (empty($range) || empty($range[1])) { return false; } return array( $range[1]['UID'], end($range)['UID'], ); } public function listex($reference, $pattern, &$error) { $error = null; if (!$this->authenticate($error)) { return false; } $response = $this->executeCommand(sprintf( 'LIST "%s" "%s"', static::escapeQuoted($this->encodeUtf7Imap($reference)), static::escapeQuoted($this->encodeUtf7Imap($pattern)) ), $error); if ($error) { $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error; $error = $this->errorMessage(array(Imap::ERR_LIST, $error), $response); return false; } $list = array(); $regex = sprintf( '/^ \* \x20 LIST \x20 \( (?<flags> ( \x5c? %1$s ( \x20 \x5c? %1$s )* )? ) \) \x20 (?<delim> NIL | " ( %2$s ) " ) \x20 (?<name> \{ \d+ \} | " ( %2$s )* " | %3$s ) \r\n /ix', self::$atomRegex, self::$qcharRegex, self::$astringRegex ); foreach ($this->getUntagged($regex, true) as $item) { list($item, $matches) = $item; $sflags = $matches['flags']; $sdelim = $matches['delim']; $sname = $matches['name']; if (preg_match('/^ " ( .+ ) " $/ix', $sdelim, $quoted)) { $sdelim = static::unescapeQuoted($quoted[1]); } if (preg_match('/^ \{ ( \d+ ) \} $/ix', $sname, $literal)) { $sname = \CUtil::binSubstr($item, \CUtil::binStrlen($matches[0]), $literal[1]); } else if (preg_match('/^ " ( .* ) " $/ix', $sname, $quoted)) { $sname = static::unescapeQuoted($quoted[1]); } $sname = $this->decodeUtf7Imap($sname); // #79498 if (strtoupper($sdelim) != 'NIL') $sname = rtrim($sname, $sdelim); $list[] = array( 'name' => $sname, 'delim' => strtoupper($sdelim) == 'NIL' ? null : $sdelim, 'flags' => preg_split('/\s+/i', $sflags, -1, PREG_SPLIT_NO_EMPTY), ); } return $list; } /** * Returns mailboxes list * * @param string $pattern Mailbox name pattern. * @param string &$error Error message. * @return array|false */ public function listMailboxes($pattern, &$error, $flat = false) { $error = null; $listGetter = function ($parent = null, $level = 0) use (&$listGetter, &$delimiter, &$error) { $pattern = $parent ? sprintf('%s%s%%', $parent, $delimiter) : '%'; $list = $this->listex('', $pattern, $error); if (false === $list) { return false; } foreach ($list as $i => $item) { $item['title'] = $item['name']; $item['level'] = $level; if ($parent) { $regex = sprintf( '/^%s%s(.)/', preg_quote($parent, '/'), preg_quote($delimiter, '/') ); if (!preg_match($regex, $item['name'])) { unset($list[$i]); continue; } $item['title'] = preg_replace($regex, '\1', $item['name']); } if ($item['name'] == $parent) { continue; } if ($item['delim'] === null) { continue; } $delimiter = $item['delim']; if (!preg_grep('/^ \x5c ( Noinferiors | HasNoChildren ) $/ix', $item['flags'])) { $children = $listGetter($item['name'], $level + 1); if ($children === false) { return false; } if (!empty($children)) { $item['children'] = $children; } } $list[$i] = $item; } return array_values($list); }; $list = $listGetter(); if (false === $list) { return false; } $regex = sprintf( '/^%s$/i', preg_replace( array('/ ( \x5c \* )+ /x', '/ ( \% )+ /x'), array('.*', $delimiter ? sprintf('[^\x%s]*', bin2hex($delimiter)) : '.*'), preg_quote($pattern, '/') ) ); $listFilter = function ($list) use (&$listFilter, $regex) { foreach ($list as $i => $item) { if (!preg_match($regex, $item['name'])) { if (empty($item['children'])) { unset($list[$i]); continue; } } if (!empty($item['children'])) { $item['children'] = $listFilter($item['children']); if (empty($item['children'])) { unset($item['children']); } } $list[$i] = $item; } $list = array_values($list); for ($i = 0; $i < count($list); $i++) { $item = $list[$i]; if (!preg_match($regex, $item['name'])) { $children = empty($item['children']) ? array() : $item['children']; array_splice($list, $i, 1, $children); $i += count($children) - 1; } } return $list; }; $list = $listFilter($list); $listHandler = function ($list, $path = array()) use (&$listHandler, $regex, $flat) { for ($i = 0; $i < count($list); $i++) { $item = $list[$i]; $item['path'] = array_merge($path, array($item['title'])); if (!empty($item['children'])) { $item['children'] = $listHandler($item['children'], $item['path']); } $list[$i] = $item; if ($flat && !empty($item['children'])) { unset($list[$i]['children']); array_splice($list, $i + 1, 0, $item['children']); $i += count($item['children']); } } return array_values($list); }; $list = $listHandler($list); return $list; } public function listMessages($mailbox, &$uidtoken, &$error) { $error = null; $params = array( 'offset' => 0, 'limit' => -1, ); if (is_array($mailbox)) { $params = array_merge($params, $mailbox); $mailbox = $mailbox['mailbox']; } if (!($params['offset'] > 0)) { $params['offset'] = 0; } if (!($params['limit'] > 0)) { $params['limit'] = -1; } if (!$this->select($mailbox, $error)) { return false; } if (!($this->sessMailbox['exists'] > 0) || $params['offset'] + 1 > $this->sessMailbox['exists']) { return array(); } if ($params['limit'] > 0 && $params['offset'] + $params['limit'] > $this->sessMailbox['exists']) { $params['limit'] = $this->sessMailbox['exists'] - $params['offset']; } $uidtoken = $this->sessMailbox['uidvalidity']; $list = $this->fetch( false, $mailbox, sprintf( '%u:%s', $params['offset'] + 1, $params['limit'] > 0 ? (int) ($params['offset']+$params['limit']) : '*' ), array_merge( !is_null($uidtoken) ? array('UID') : array(), array('INTERNALDATE', 'RFC822.SIZE', 'FLAGS') ), $error ); foreach ($list as $id => $data) { $list[$id] = array( 'id' => $id, 'uid' => array_key_exists('UID', $data) ? $data['UID'] : null, 'date' => array_key_exists('INTERNALDATE', $data) ? $data['INTERNALDATE'] : null, 'size' => array_key_exists('RFC822.SIZE', $data) ? $data['RFC822.SIZE'] : null, 'flags' => array_key_exists('FLAGS', $data) ? $data['FLAGS'] : array(), ); } $list = array_filter( $list, function ($item) { return isset($item['date'], $item['size'], $item['flags']); } ); return $list; } /** * Adds message * * @param string $mailbox Mailbox name. * @param string $data Message. * @param string &$error Error message. * @return string|false */ public function addMessage($mailbox, $data, &$error) { $error = null; return $this->append($mailbox, array('\Seen'), new \DateTime, $data, $error); } public function searchByHeader($uid = false, $mailbox, array $header, &$error) { $error = null; if (!$this->select($mailbox, $error)) { return false; } if (empty($header)) { return false; } $result = array(); $response = $this->executeCommand( $ccc = sprintf( '%sSEARCH %s', $uid ? 'UID ' : '', join( ' ', array_map( function ($name, $value) { return sprintf( 'HEADER %s %s', static::prepareString($name), static::prepareString($value) ); }, array_keys($header), array_values($header) ) ) ), $error ); if ($error) { $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error; $error = $this->errorMessage(array(Imap::ERR_SEARCH, $error), $response); return false; } $regex = '/^ \* \x20 SEARCH \x20 ( .+ ) \r\n $ /ix'; foreach ($this->getUntagged($regex, true) as $item) { $result = preg_match_all('/\d+/', $item[1][1]); } return $result; } public function append($mailbox, array $flags, \DateTime $internaldate, $data, &$error) { $error = null; if (!$this->authenticate($error)) { return false; } foreach ($flags as $k => $item) { if (!preg_match(sprintf('/ ^ \x5c? %s $ /ix', self::$atomRegex), $item)) { unset($flags[$k]); } } $response = $this->executeCommand(sprintf( 'APPEND "%s" (%s) "%26s" %s', static::escapeQuoted($this->encodeUtf7Imap($mailbox)), join(' ', $flags), $internaldate->format('j-M-Y H:i:s O'), static::prepareString($data) ), $error); if ($error) { $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error; $error = $this->errorMessage(array(Imap::ERR_APPEND, $error), $response); return false; } $regex = sprintf('/^ OK \x20 \[ APPENDUID \x20 ( \d+ ) \x20 ( \d+ ) \] /ix', $this->getTag()); if (preg_match($regex, $response, $matches)) { return sprintf('%u:%u', $matches[1], $matches[2]); } return true; } public function moveMails($ids, $folderFrom, $folderTo) { $error = null; $result = new Main\Result(); if (!$this->authenticate($error)) { return $result->addError(new Main\Error('')); } if (preg_match('/ \x20 MOVE ( \x20 | \r\n ) /ix', $this->sessCapability)) { $result = $this->move($ids, $folderFrom, $folderTo); } else { $result = $this->copyMailToFolder($ids, $folderFrom, $folderTo); if ($result->isSuccess()) { $result = $this->delete($ids, $folderFrom); } } return $result; } public function move($ids, $folderFrom, $folderTo) { $error = null; $result = new Main\Result(); if (!$this->authenticate($error)) { return $result->addError(new Main\Error('')); } if (!$this->select($folderFrom, $error)) { return $result->addError(new Main\Error('')); } $response = $this->executeCommand(sprintf('UID MOVE %s "%s"', $this->prepareIdsParam($ids), static::escapeQuoted($this->encodeUtf7Imap($folderTo))), $error); if ($error) { $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error; $error = $this->errorMessage([Imap::ERR_STORE, $error], $response); return $result->addError(new Main\Error($error)); } return $result; } public function copyMailToFolder($ids, $mailboxName, $folder) { $error = null; $result = new Main\Result(); if (!$this->authenticate($error)) { return $result->addError(new Main\Error('')); } if (!$this->select($mailboxName, $error)) { return $result->addError(new Main\Error('')); } $response = $this->executeCommand(sprintf('UID COPY %s "%s"', $this->prepareIdsParam($ids), static::escapeQuoted($this->encodeUtf7Imap($folder))), $error); if ($error) { $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error; $error = $this->errorMessage(array(Imap::ERR_STORE, $error), $response); return $result->addError(new Main\Error($error)); } return $result; } public function unseen($ids, $folder) { $error = null; $result = new Main\Result(); if (!$this->authenticate($error)) { return $result->addError(new Main\Error('')); } if (!$this->select($folder, $error)) { return $result->addError(new Main\Error('')); } $response = $this->store($ids, ['\Seen'], $error, true, true); if ($error) { $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error; $error = $this->errorMessage([Imap::ERR_STORE, $error], $response); return $result->addError(new Main\Error($error)); } return $result; } public function seen($ids, $folder) { $error = null; $result = new Main\Result(); if (!$this->authenticate($error)) { return $result->addError(new Main\Error('')); } if (!$this->select($folder, $error)) { return $result->addError(new Main\Error('')); } $response = $this->store($ids, ['\Seen'], $error); if ($error) { $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error; $error = $this->errorMessage([Imap::ERR_STORE, $error], $response); return $result->addError(new Main\Error($error)); } return $result; } /** * @param $id * @param $mailboxName * @return Main\Result */ public function delete($id, $mailboxName) { $error = null; $result = new Main\Result(); if (!$this->authenticate($error)) { return $result->addError(new Main\Error('')); } if (!$this->select($mailboxName, $error)) { return $result->addError(new Main\Error('')); } $response = $this->store($id, ['\Deleted'], $error); if ($error) { $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error; $error = $this->errorMessage(array(Imap::ERR_STORE, $error), $response); return $result->addError(new Main\Error($error)); } $response = $this->expunge($id, $error); if ($error) { $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error; $error = $this->errorMessage(array(Imap::ERR_STORE, $error), $response); return $result->addError(new Main\Error($error)); } return $result; } /** * @param int|array $ids * @param $error * @return bool|null|string|string[] */ private function expunge($ids, &$error) { return $this->executeCommand(sprintf('UID EXPUNGE %s', $this->prepareIdsParam($ids)), $error); } /** * @param int|array $ids * @param $flags * @param $error * @param bool $isUid * @param bool $isRemoveFlags * @return bool|null|string|string[] */ private function store($ids, $flags, &$error, $isUid = true, $isRemoveFlags = false) { $command = sprintf('STORE %s ', $this->prepareIdsParam($ids)); $command .= $isRemoveFlags ? '-' : '+'; $command = $command . sprintf('FLAGS (%s)', join(' ', $flags)); if ($isUid) { $command = 'UID ' . $command; } return $this->executeCommand($command, $error); } public function updateMessageFlags($mailbox, $id, $flags, &$error) { $error = null; if (!$this->select($mailbox, $error)) { return false; } $addFlags = array(); $delFlags = array(); foreach ($flags as $name => $value) { if (preg_match(sprintf('/ ^ \x5c? %s $ /ix', self::$atomRegex), $name)) { if ($value) { $addFlags[] = $name; } else { $delFlags[] = $name; } } } if ($addFlags) { $response = $this->store($id, $addFlags, $error, false); } if (!$error && $delFlags) { $response = $this->store($id, $delFlags, $error, false, true); } if ($error) { $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error; $error = $this->errorMessage(array(Imap::ERR_STORE, $error), $response); return false; } $this->getUntagged(sprintf('/^ \* \x20 %u \x20 FETCH \x20 \( .+ \) \r\n $/isx', $id), true); return true; } /** * Returns message * * @param string $mailbox Mailbox name. * @param int $id Message ID. * @param string $section Message section. * @param string &$error Error message. * @return string|false */ public function getMessage($mailbox, $id, $section, &$error) { $error = null; $section = strtoupper($section); if (!in_array(strtoupper($section), array('HEADER', 'TEXT'))) { $section = ''; } if (!$this->select($mailbox, $error)) { return false; } $response = $this->fetch(false, $mailbox, (int) $id, sprintf('BODY.PEEK[%s]', $section), $error); return $response[sprintf('BODY[%s]', $section)]; } protected function getUntagged($regex, $unset = false) { $result = array(); $length = count($this->sessUntagged); for ($i = 0; $i < $length; $i++) { if (!preg_match($regex, $this->sessUntagged[$i], $matches)) continue; unset($matches[0]); $result[] = array($this->sessUntagged[$i], $matches); if ($unset) unset($this->sessUntagged[$i]); } if ($unset && !empty($result)) $this->sessUntagged = array_values($this->sessUntagged); return $result; } protected function getTag($next = false) { if ($next) $this->sessCounter++; return sprintf('A%03u', $this->sessCounter); } protected function executeCommand($command, &$error) { $error = null; $chunks = explode("\x00", sprintf('%s %s', $this->getTag(true), $command)); $k = count($chunks); foreach ($chunks as $chunk) { $k--; $response = $this->exchange($chunk, $error); if ($k > 0 && strpos($response, '+') !== 0) break; } return $response; } protected function exchange($data, &$error) { $error = null; if ($this->sendData(sprintf("%s\r\n", $data)) === false) { $error = Imap::ERR_COMMUNICATE; return false; } $response = $this->readResponse(); if ($response === false) { $error = Imap::ERR_EMPTY_RESPONSE; return false; } $response = trim($response); if (!preg_match(sprintf('/^ %s \x20 OK /ix', $this->getTag()), $response)) { if (preg_match(sprintf('/^ %s \x20 ( NO | BAD ) /ix', $this->getTag()), $response)) $error = Imap::ERR_COMMAND_REJECTED; else $error = Imap::ERR_BAD_SERVER; } return preg_replace(sprintf('/^ %s \x20 /ix', $this->getTag()), '', $response); return $response; } protected function sendData($data) { $fails = 0; while (\CUtil::binStrlen($data) > 0 && !feof($this->stream)) { $bytes = @fputs($this->stream, $data); if (false == $bytes) { if (false === $bytes || ++$fails >= 3) { break; } continue; } $fails = 0; $data = \CUtil::binSubstr($data, $bytes); } if (\CUtil::binStrlen($data) > 0) { $this->reset(); return false; } return true; } protected function readBytes($bytes) { $data = ''; while ($bytes > 0 && !feof($this->stream)) { $buffer = @fread($this->stream, $bytes); if ($buffer === false) break; $meta = $this->options['timeout'] > 0 ? stream_get_meta_data($this->stream) : array('timed_out' => false); $data .= $buffer; $bytes -= \CUtil::binStrlen($buffer); if ($meta['timed_out']) break; } if ($bytes > 0) { $this->reset(); return false; } return $data; } protected function readLine() { $line = ''; while (!feof($this->stream)) { $buffer = @fgets($this->stream, 4096); if ($buffer === false) break; $meta = $this->options['timeout'] > 0 ? stream_get_meta_data($this->stream) : array('timed_out' => false); $line .= $buffer; $eolRegex = '/ (?<literal> \{ (?<bytes> \d+ ) \} )? \r\n $ /x'; if (preg_match($eolRegex, $line, $matches)) { if (empty($matches['literal'])) break; if ($meta['timed_out']) return false; $data = $this->readBytes($matches['bytes']); if ($data === false) return false; $line .= $data; } if ($meta['timed_out']) break; } if (!preg_match('/\r\n$/', $line, $matches)) { $this->reset(); return false; } return $line; } protected function readResponse() { do { $line = $this->readLine(); if ($line === false) return false; if (strpos($line, '*') === 0) $this->sessUntagged[] = $line; } while (strpos($line, '*') === 0); if ('select' == $this->sessState) { $regex = '/^ \* \x20 ( \d+ ) \x20 EXISTS /ix'; foreach ($this->getUntagged($regex) as $item) { $this->sessMailbox['exists'] = $item[1][1]; } } return $line; } protected static function prepareString($data) { if (preg_match('/^[^\x00\x0a\x0d\x80-\xff]*$/', $data)) return sprintf('"%s"', static::escapeQuoted($data)); else return sprintf("{%u}\x00%s", \CUtil::binStrlen($data), $data); } protected static function escapeQuoted($data) { return str_replace(array('\\', '"'), array('\\\\', '\\"'), $data); } protected static function unescapeQuoted($data) { return str_replace(array('\\\\', '\\"'), array('\\', '"'), $data); } protected function encodeUtf7Imap($data) { if (!$data) return $data; $result = Encoding::convertEncoding($data, $this->options['encoding'], 'UTF7-IMAP'); if ($result === false) { $result = $data; $result = Encoding::convertEncoding($result, $this->options['encoding'], 'UTF-8'); $result = str_replace('&', '&-', $result); $result = preg_replace_callback('/[\x00-\x1f\x7f-\xff]+/', function($matches) { $result = $matches[0]; $result = Encoding::convertEncoding($result, 'UTF-8', 'UTF-16BE'); $result = base64_encode($result); $result = str_replace('/', ',', $result); $result = str_replace('=', '', $result); $result = '&' . $result . '-'; return $result; }, $result); } return $result; } protected function decodeUtf7Imap($data) { if (!$data) return $data; $result = Encoding::convertEncoding($data, 'UTF7-IMAP', $this->options['encoding']); if ($result === false) { $result = $data; $result = preg_replace_callback('/&([\x2b\x2c\x30-\x39\x41-\x5a\x61-\x7a]+)-/', function($matches) { $result = $matches[1]; $result = str_replace(',', '/', $result); $result = base64_decode($result); $result = Encoding::convertEncoding($result, 'UTF-16BE', 'UTF-8'); return $result; }, $result); $result = str_replace('&-', '&', $result); $result = Encoding::convertEncoding($result, 'UTF-8', $this->options['encoding']); } return $result; } private function prepareIdsParam($idsData) { if (is_array($idsData)) { return implode(',', array_map('intval', $idsData)); } else { return intval($idsData); } } protected function errorMessage($errors, $details = null) { $errors = array_filter((array) $errors); $details = array_filter((array) $details); foreach ($errors as $i => $error) { $errors[$i] = static::decodeError($error); $this->errors->setError(new Main\Error($errors[$i], $error > 0 ? $error : 0)); } $error = join(': ', $errors); if ($details) { $error .= sprintf(' (%s)', join(': ', $details)); foreach ($details as $item) $this->errors->setError(new Main\Error($item, -1)); } return $error; } public function getErrors() { return $this->errors; } /** * Returns error message * * @param int $code Error code. * @return string */ public static function decodeError($code) { switch ($code) { case self::ERR_CONNECT: return Loc::getMessage('MAIL_IMAP_ERR_CONNECT'); case self::ERR_REJECTED: return Loc::getMessage('MAIL_IMAP_ERR_REJECTED'); case self::ERR_COMMUNICATE: return Loc::getMessage('MAIL_IMAP_ERR_COMMUNICATE'); case self::ERR_EMPTY_RESPONSE: return Loc::getMessage('MAIL_IMAP_ERR_EMPTY_RESPONSE'); case self::ERR_BAD_SERVER: return Loc::getMessage('MAIL_IMAP_ERR_BAD_SERVER'); case self::ERR_STARTTLS: return Loc::getMessage('MAIL_IMAP_ERR_STARTTLS'); case self::ERR_COMMAND_REJECTED: return Loc::getMessage('MAIL_IMAP_ERR_COMMAND_REJECTED'); case self::ERR_CAPABILITY: return Loc::getMessage('MAIL_IMAP_ERR_CAPABILITY'); case self::ERR_AUTH: return Loc::getMessage('MAIL_IMAP_ERR_AUTH'); case self::ERR_AUTH_MECH: return Loc::getMessage('MAIL_IMAP_ERR_AUTH_MECH'); case self::ERR_AUTH_OAUTH: return Loc::getMessage('MAIL_IMAP_ERR_AUTH_OAUTH'); case self::ERR_LIST: return Loc::getMessage('MAIL_IMAP_ERR_LIST'); case self::ERR_SELECT: return Loc::getMessage('MAIL_IMAP_ERR_SELECT'); case self::ERR_SEARCH: return Loc::getMessage('MAIL_IMAP_ERR_SEARCH'); case self::ERR_FETCH: return Loc::getMessage('MAIL_IMAP_ERR_FETCH'); case self::ERR_APPEND: return Loc::getMessage('MAIL_IMAP_ERR_APPEND'); case self::ERR_STORE: return Loc::getMessage('MAIL_IMAP_ERR_STORE'); default: return Loc::getMessage('MAIL_IMAP_ERR_DEFAULT'); } } }