48 public function __construct($host, $port, $tls, $strict, $login, $password, $encoding =
null)
52 $strict = (bool) $strict;
54 $this->options = array(
58 'socket' => sprintf(
'%s://%s:%s', ($tls ?
'ssl' :
'tcp'), $host, $port),
59 'timeout' => \COption::getOptionInt(
'mail',
'connect_timeout', B_MAIL_TIMEOUT),
60 'context' => stream_context_create(array(
62 'verify_peer' => $strict,
63 'verify_peer_name' => $strict,
64 'crypto_method' => STREAM_CRYPTO_METHOD_ANY_CLIENT,
68 'password' => $password,
69 'encoding' => $encoding ?: LANG_CHARSET,
72 $logParams = Main\Config\Configuration::getValue(
'imap');
73 if(isset($logParams[
"log_level"]) && $logParams[
"log_level"] > 0)
75 $this->logLevel = $logParams[
"log_level"];
76 if(isset($logParams[
"log_path"]) && $logParams[
"log_path"] <>
'')
78 $this->logPath = $logParams[
"log_path"];
123 if (!empty($this->sessState))
126 $resource = @stream_socket_client(
127 $this->options[
'socket'], $errno, $errstr, $this->options[
'timeout'],
128 STREAM_CLIENT_CONNECT, $this->options[
'context']
131 if ($resource ===
false)
133 $error = $this->
errorMessage(Imap::ERR_CONNECT, $errno ?:
null);
137 $this->stream = $resource;
139 if ($this->options[
'timeout'] > 0)
140 stream_set_timeout($this->stream, $this->options[
'timeout']);
144 if ($prompt !==
false && preg_match(
'/^\* (OK|PREAUTH)/i', $prompt, $matches))
146 if ($matches[1] ==
'OK')
147 $this->sessState =
'no_auth';
148 elseif ($matches[1] ==
'PREAUTH')
149 $this->sessState =
'auth';
153 if ($prompt ===
false)
154 $error = Imap::ERR_EMPTY_RESPONSE;
155 elseif (preg_match(
'/^\* BYE/i', $prompt))
156 $error = Imap::ERR_REJECTED;
158 $error = Imap::ERR_BAD_SERVER;
160 $error = $this->
errorMessage(array(Imap::ERR_CONNECT, $error));
168 if (!$this->options[
'tls'] && preg_match(
'/ \x20 STARTTLS ( \x20 | \r\n ) /ix', $this->sessCapability))
248 if (in_array($this->sessState, array(
'auth',
'select')))
254 if (preg_match(
'/ \x20 AUTH=XOAUTH2 ( \x20 | \r\n ) /ix', $this->sessCapability))
256 $token = Helper\OAuth::getTokenByMeta($this->options[
'password']);
262 else if (
false === $token)
264 $error = $this->
errorMessage(array(Imap::ERR_AUTH, Imap::ERR_AUTH_OAUTH));
271 if (preg_match(
'/ \x20 AUTH=PLAIN ( \x20 | \r\n ) /ix', $this->sessCapability))
273 elseif (!preg_match(
'/ \x20 LOGINDISABLED ( \x20 | \r\n ) /ix', $this->sessCapability))
279 $error = $this->
errorMessage(array(Imap::ERR_AUTH, Imap::ERR_AUTH_MECH));
283 if ($mech ==
'oauth')
285 $response = $this->
executeCommand(
'AUTHENTICATE XOAUTH2', $error);
287 if (mb_strpos($response,
'+') !== 0)
289 $error = $error == Imap::ERR_COMMAND_REJECTED ? Imap::ERR_AUTH_MECH : $error;
290 $error = $this->
errorMessage(array(Imap::ERR_AUTH, $error), $response);
295 $response = $this->
exchange(base64_encode(sprintf(
296 "user=%s\x01auth=Bearer %s\x01\x01", $this->options[
'login'], $token
299 if (mb_strpos($response,
'+') === 0)
300 $response = $this->
exchange(
"\r\n", $error);
302 elseif ($mech ==
'plain')
306 if (mb_strpos($response,
'+') !== 0)
308 $error = $error == Imap::ERR_COMMAND_REJECTED ? Imap::ERR_AUTH_MECH : $error;
309 $error = $this->
errorMessage(array(Imap::ERR_AUTH, $error), $response);
314 $response = $this->
exchange(base64_encode(sprintf(
316 Encoding::convertEncoding($this->options[
'login'], $this->options[
'encoding'],
'UTF-8'),
317 Encoding::convertEncoding($this->options[
'password'], $this->options[
'encoding'],
'UTF-8')
324 static::prepareString($this->options[
'login']),
325 static::prepareString($this->options[
'password'])
331 $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error;
332 $error = $this->
errorMessage(array(Imap::ERR_AUTH, $error), $response);
337 $this->sessState =
'auth';
459 public function fetch($uid, $mailbox, $range, $select, &$error, $outputFormat =
'smart')
463 if (!preg_match(
'/(([1-9]\d*|\*)(:(?2))?)(,(?1))*/', $range))
472 else if (is_array($select))
474 $select = sprintf(
'(%s)', join(
' ', $select));
477 if (!$this->
select($mailbox, $error))
484 if ($this->sessMailbox[
'exists'] > 0)
486 $fetchUntaggedRegex =
'/^ \* \x20 ( \d+ ) \x20 FETCH \x20 \( ( .+ ) \) \r\n $/isx';
490 sprintf(
'%sFETCH %s %s', $uid ?
'UID ' :
'', $range, $select),
496 $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error;
497 $error = $this->
errorMessage(array(Imap::ERR_FETCH, $error), $response);
502 $shiftName =
function (&$item)
510 [a-z0-9]+ (?: \. [a-z0-9]+ )*
511 (?: \[ (?: [a-z0-9]+ (?: \. [a-z0-9]* )* (?: \x20 %s )* )? \] )?
519 if (preg_match($regex, $item, $matches))
521 $result = $matches[1];
523 $item = BinaryString::getSubstring($item, BinaryString::getLength($matches[0]));
529 $shiftValue =
function (&$item) use (&$shiftValue)
533 $tail =
' (?= [\x20)] | $ ) \x20? ';
535 if (BinaryString::getSubstring($item, 0, 1) ===
'(')
537 $item = BinaryString::getSubstring($item, 1);
541 while (BinaryString::getLength($item) > 0 && BinaryString::getSubstring($item, 0, 1) !==
')')
543 $subresult = $shiftValue($item);
545 if (
false !== $subresult)
547 $result[] = $subresult;
555 if (preg_match(
'/^ \) (?= [\x20()] | $ ) \x20? /ix', $item, $matches))
557 $item = BinaryString::getSubstring($item, BinaryString::getLength($matches[0]));
564 else if (preg_match(
'/^ { ( \d+ ) } \r\n /ix', $item, $matches))
566 $item = BinaryString::getSubstring($item, BinaryString::getLength($matches[0]));
568 if (BinaryString::getLength($item) >= $matches[1])
570 $result = BinaryString::getSubstring($item, 0, $matches[1]);
572 $item = BinaryString::getSubstring($item, $matches[1]);
574 if (preg_match(sprintf(
'/^ %s /ix', $tail), $item, $matches))
576 $item = BinaryString::getSubstring($item, BinaryString::getLength($matches[0]));
584 else if (preg_match(sprintf(
'/^ NIL %s /ix', $tail), $item, $matches))
588 $item = BinaryString::getSubstring($item, BinaryString::getLength($matches[0]));
590 else if (preg_match(sprintf(
'/^ " ( (?: %s )* ) " %s /ix', self::$qcharExtRegex, $tail), $item, $matches))
592 $result = self::unescapeQuoted($matches[1]);
594 $item = BinaryString::getSubstring($item, BinaryString::getLength($matches[0]));
596 else if (preg_match(sprintf(
'/^ ( \x5c? %s ) %s /ix', self::$astringRegex, $tail), $item, $matches))
598 $result = $matches[1];
600 $item = BinaryString::getSubstring($item, BinaryString::getLength($matches[0]));
602 else if (preg_match(sprintf(
'/^ %s /ix', $tail), $item, $matches))
606 $item = BinaryString::getSubstring($item, BinaryString::getLength($matches[0]));
612 $bodystructure =
function (&$value) use (&$bodystructure)
614 if (!is_array($value) || !is_array($value[0]))
619 $value[0] = $bodystructure($value[0]);
620 $value[0] = array($value[0]);
622 while (array_key_exists(1, $value) && is_array($value[1]))
624 $value[0][] = $bodystructure($value[1]);
626 array_splice($value, 1, 1);
629 while (count($value[0]) == 1 && count($value[0][0]) == 1)
631 $value[0] = $value[0][0];
637 foreach ($this->
getUntagged($fetchUntaggedRegex,
true) as $item)
643 while (BinaryString::getLength($item[1][2]) > 0)
645 if (($name = $shiftName($item[1][2])) !==
false)
647 if (($value = $shiftValue($item[1][2])) !==
false)
649 if (in_array(mb_strtoupper($name), array(
'BODY',
'BODYSTRUCTURE')))
651 $value = $bodystructure($value);
654 $data[$name] = $value;
663 $list[$data[
'id']] = $data;
670 if ($outputFormat ===
'smart' && !preg_match(
'/[:,]/', $range))
672 $list =
reset($list);
861 public function listex($reference, $pattern, &$error)
878 $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error;
879 $error = $this->
errorMessage(array(Imap::ERR_LIST, $error), $response);
887 '/^ \* \x20 LIST \x20
888 \( (?<flags> ( \x5c? %1$s ( \x20 \x5c? %1$s )* )? ) \) \x20
889 (?<delim> NIL | " ( %2$s ) " ) \x20
890 (?<name> \{ \d+ \} | " ( %2$s )* " | %3$s ) \r\n
893 self::$atomRegex, self::$qcharRegex, self::$astringRegex
895 foreach ($this->
getUntagged($regex,
true) as $item)
897 [$item, $matches] = $item;
899 $sflags = $matches[
'flags'];
900 $sdelim = $matches[
'delim'];
901 $sname = $matches[
'name'];
903 if (preg_match(
'/^ " ( .+ ) " $/ix', $sdelim, $quoted))
905 $sdelim = static::unescapeQuoted($quoted[1]);
908 if (preg_match(
'/^ \{ ( \d+ ) \} $/ix', $sname, $literal))
910 $sname = substr($matches[
'ext'], 0, $literal[1]);
912 else if (preg_match(
'/^ " ( .* ) " $/ix', $sname, $quoted))
914 $sname = static::unescapeQuoted($quoted[1]);
920 if (mb_strtoupper($sdelim) !=
'NIL')
921 $sname = rtrim($sname, $sdelim);
925 'delim' => mb_strtoupper($sdelim) ==
'NIL' ?
'NIL' : $sdelim,
926 'flags' => preg_split(
'/\s+/i', $sflags, -1, PREG_SPLIT_NO_EMPTY),
944 $listGetter =
function ($parent =
null, $level = 0) use (&$listGetter, &$delimiter, &$error)
946 $pattern = $parent ? sprintf(
'%s%s%%', $parent, $delimiter) :
'%';
948 $list = $this->
listex(
'', $pattern, $error);
955 foreach ($list as $i => $item)
957 $item[
'title'] = $item[
'name'];
958 $item[
'level'] = $level;
964 preg_quote($parent,
'/'),
965 preg_quote($delimiter,
'/')
968 if (!preg_match($regex, $item[
'name']))
974 $item[
'title'] = preg_replace($regex,
'\1', $item[
'name']);
977 if ($item[
'name'] == $parent)
982 if ($item[
'delim'] ===
null)
987 $delimiter = $item[
'delim'];
989 if (!preg_grep(
'/^ \x5c ( Noinferiors | HasNoChildren ) $/ix', $item[
'flags']))
991 $children = $listGetter($item[
'name'], $level + 1);
993 if ($children ===
false)
998 if (!empty($children))
1000 $item[
'children'] = $children;
1007 return array_values($list);
1010 $list = $listGetter();
1012 if (
false === $list)
1020 array(
'/ ( \x5c \* )+ /x',
'/ ( \% )+ /x'),
1021 array(
'.*', $delimiter ? sprintf(
'[^\x%s]*', bin2hex($delimiter)) :
'.*'),
1022 preg_quote($pattern,
'/')
1026 $listFilter =
function ($list) use (&$listFilter, $regex)
1028 foreach ($list as $i => $item)
1030 if (!preg_match($regex, $item[
'name']))
1032 if (empty($item[
'children']))
1039 if (!empty($item[
'children']))
1041 $item[
'children'] = $listFilter($item[
'children']);
1043 if (empty($item[
'children']))
1045 unset($item[
'children']);
1052 $list = array_values($list);
1054 for ($i = 0; $i < count($list); $i++)
1058 if (!preg_match($regex, $item[
'name']))
1060 $children = empty($item[
'children']) ? array() : $item[
'children'];
1062 array_splice($list, $i, 1, $children);
1063 $i += count($children) - 1;
1070 $list = $listFilter($list);
1072 $listHandler =
function ($list, $path = array()) use (&$listHandler, $regex, $flat)
1074 for ($i = 0; $i < count($list); $i++)
1078 $item[
'path'] = array_merge($path, array($item[
'title']));
1080 if (!empty($item[
'children']))
1082 $item[
'children'] = $listHandler($item[
'children'], $item[
'path']);
1087 if ($flat && !empty($item[
'children']))
1089 unset($list[$i][
'children']);
1091 array_splice($list, $i + 1, 0, $item[
'children']);
1092 $i += count($item[
'children']);
1096 return array_values($list);
1099 $list = $listHandler($list);
1253 public function append($mailbox, array $flags, \
DateTime $internaldate, $data, &$error)
1262 foreach ($flags as $k => $item)
1264 if (!preg_match(sprintf(
'/ ^ \x5c? %s $ /ix', self::$atomRegex), $item))
1271 'APPEND "%s" (%s) "%26s" %s',
1274 $internaldate->
format(
'j-M-Y H:i:s O'),
1275 static::prepareString($data)
1280 $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error;
1281 $error = $this->
errorMessage(array(Imap::ERR_APPEND, $error), $response);
1286 $regex = sprintf(
'/^ OK \x20 \[ APPENDUID \x20 ( \d+ ) \x20 ( \d+ ) \] /ix', $this->
getTag());
1287 if (preg_match($regex, $response, $matches))
1289 return sprintf(
'%u:%u', $matches[1], $matches[2]);
1484 if (!$this->
select($mailbox, $error))
1489 $addFlags = array();
1490 $delFlags = array();
1491 foreach ($flags as $name => $value)
1493 if (preg_match(sprintf(
'/ ^ \x5c? %s $ /ix', self::$atomRegex), $name))
1497 $addFlags[] = $name;
1501 $delFlags[] = $name;
1508 $response = $this->store($id, $addFlags, $error,
false);
1511 if (!$error && $delFlags)
1513 $response = $this->store($id, $delFlags, $error,
false,
true);
1518 $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error;
1519 $error = $this->
errorMessage(array(Imap::ERR_STORE, $error), $response);
1524 $this->
getUntagged(sprintf(
'/^ \* \x20 %u \x20 FETCH \x20 \( .+ \) \r\n $/isx', $id),
true);
1577 if (!$this->
select($mailbox, $error))
1582 if ($this->sessMailbox[
'exists'] > 0)
1587 $searchUntaggedRegex =
'/^ \* \x20 SEARCH \x20 ( .+ ) \r\n $ /ix';
1594 $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error;
1595 $error = $this->
errorMessage(array(Imap::ERR_SEARCH, $error), $response);
1601 foreach ($this->
getUntagged($searchUntaggedRegex,
true) as $item)
1603 $matches = preg_match_all(
'/\d+/', $item[1][1]);
1610 'IMAP: invalid mailbox (search>exists) (%s:%s:%s:%u)',
1611 $this->options[
'host'], $this->options[
'login'], $mailbox, $this->sessMailbox[
'uidvalidity']
1619 $fetchUntaggedRegex =
'/^ \* \x20 ( \d+ ) \x20 FETCH \x20 \( ( .+ ) \) \r\n $/isx';
1626 $error = $error == Imap::ERR_COMMAND_REJECTED ? null : $error;
1627 $error = $this->
errorMessage(array(Imap::ERR_FETCH, $error), $response);
1633 foreach ($this->
getUntagged($fetchUntaggedRegex,
true) as $item)
1635 $matches = $item[1][1];
1642 'IMAP: invalid mailbox (fetch>exists) (%s:%s:%s:%u)',
1643 $this->options[
'host'], $this->options[
'login'], $mailbox, $this->sessMailbox[
'uidvalidity']