| | |
| | | public $message; |
| | | public $rootdir; |
| | | public $delimiter; |
| | | public $permanentflags = array(); |
| | | public $data = array(); |
| | | public $flags = array( |
| | | 'SEEN' => '\\Seen', |
| | | 'DELETED' => '\\Deleted', |
| | |
| | | '*' => '\\*', |
| | | ); |
| | | |
| | | private $exists; |
| | | private $recent; |
| | | private $selected; |
| | | private $fp; |
| | | private $host; |
| | |
| | | $this->capability_readed = false; |
| | | } |
| | | |
| | | function authenticate($user, $pass, $encChallenge) |
| | | /** |
| | | * CRAM-MD5/PLAIN Authentication |
| | | * |
| | | * @param string $user |
| | | * @param string $pass |
| | | * @param string $type Authentication type (PLAIN or CRAM-MD5) |
| | | * |
| | | * @return resource Connection resourse on success, error code on error |
| | | */ |
| | | function authenticate($user, $pass, $type='PLAIN') |
| | | { |
| | | $ipad = ''; |
| | | $opad = ''; |
| | | if ($type == 'CRAM-MD5') { |
| | | $ipad = ''; |
| | | $opad = ''; |
| | | |
| | | // initialize ipad, opad |
| | | for ($i=0; $i<64; $i++) { |
| | | $ipad .= chr(0x36); |
| | | $opad .= chr(0x5C); |
| | | // initialize ipad, opad |
| | | for ($i=0; $i<64; $i++) { |
| | | $ipad .= chr(0x36); |
| | | $opad .= chr(0x5C); |
| | | } |
| | | |
| | | // pad $pass so it's 64 bytes |
| | | $padLen = 64 - strlen($pass); |
| | | for ($i=0; $i<$padLen; $i++) { |
| | | $pass .= chr(0); |
| | | } |
| | | |
| | | $this->putLine($this->next_tag() . " AUTHENTICATE CRAM-MD5"); |
| | | $line = trim($this->readLine(1024)); |
| | | |
| | | if ($line[0] == '+') { |
| | | $challenge = substr($line,2); |
| | | } |
| | | else { |
| | | return self::ERROR_BYE; |
| | | } |
| | | |
| | | // generate hash |
| | | $hash = md5($this->_xor($pass, $opad) . pack("H*", md5($this->_xor($pass, $ipad) . base64_decode($encChallenge)))); |
| | | $reply = base64_encode($user . ' ' . $hash); |
| | | |
| | | // send result, get reply and process it |
| | | $this->putLine($reply); |
| | | $line = $this->readLine(1024); |
| | | $result = $this->parseResult($line); |
| | | if ($result != self::ERROR_OK) { |
| | | $this->set_error($result, "Unble to authenticate user (CRAM-MD5): $line"); |
| | | } |
| | | } |
| | | else { // PLAIN |
| | | // proxy authentication |
| | | if (!empty($this->prefs['auth_cid'])) { |
| | | $authc = $this->prefs['auth_cid']; |
| | | $pass = $this->prefs['auth_pw']; |
| | | } |
| | | else { |
| | | $authc = $user; |
| | | } |
| | | |
| | | $reply = base64_encode($user . chr(0) . $authc . chr(0) . $pass); |
| | | |
| | | // RFC 4959 (SASL-IR): save one round trip |
| | | if ($this->getCapability('SASL-IR')) { |
| | | $result = $this->execute("AUTHENTICATE PLAIN", array($reply), self::COMMAND_NORESPONSE); |
| | | } |
| | | else { |
| | | $this->putLine($this->next_tag() . " AUTHENTICATE PLAIN"); |
| | | $line = trim($this->readLine(1024)); |
| | | |
| | | if ($line[0] != '+') { |
| | | return self::ERROR_BYE; |
| | | } |
| | | |
| | | // send result, get reply and process it |
| | | $this->putLine($reply); |
| | | $line = $this->readLine(1024); |
| | | $result = $this->parseResult($line); |
| | | if ($result != self::ERROR_OK) { |
| | | $this->set_error($result, "Unble to authenticate user (AUTH): $line"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // pad $pass so it's 64 bytes |
| | | $padLen = 64 - strlen($pass); |
| | | for ($i=0; $i<$padLen; $i++) { |
| | | $pass .= chr(0); |
| | | } |
| | | |
| | | // generate hash |
| | | $hash = md5($this->_xor($pass,$opad) . pack("H*", md5($this->_xor($pass, $ipad) . base64_decode($encChallenge)))); |
| | | |
| | | // generate reply |
| | | $reply = base64_encode($user . ' ' . $hash); |
| | | |
| | | // send result, get reply |
| | | $this->putLine($reply); |
| | | $line = $this->readLine(1024); |
| | | |
| | | // process result |
| | | $result = $this->parseResult($line); |
| | | if ($result == self::ERROR_OK) { |
| | | return $this->fp; |
| | | } |
| | | |
| | | $this->error = "Authentication for $user failed (AUTH): $line"; |
| | | |
| | | return $result; |
| | | } |
| | | |
| | | /** |
| | | * LOGIN Authentication |
| | | * |
| | | * @param string $user |
| | | * @param string $pass |
| | | * |
| | | * @return resource Connection resourse on success, error code on error |
| | | */ |
| | | function login($user, $password) |
| | | { |
| | | list($code, $response) = $this->execute('LOGIN', array( |
| | |
| | | if ($code == self::ERROR_OK) { |
| | | return $this->fp; |
| | | } |
| | | |
| | | @fclose($this->fp); |
| | | $this->fp = false; |
| | | |
| | | return $code; |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | $orig_method = $auth_method; |
| | | $auth_methods = array(); |
| | | $result = null; |
| | | |
| | | // check for supported auth methods |
| | | if ($auth_method == 'CHECK') { |
| | | // check for supported auth methods |
| | | if ($this->getCapability('AUTH=CRAM-MD5') || $this->getCapability('AUTH=CRAM_MD5')) { |
| | | $auth_method = 'AUTH'; |
| | | $auth_methods[] = 'AUTH'; |
| | | } |
| | | else { |
| | | // default to plain text auth |
| | | $auth_method = 'PLAIN'; |
| | | if ($this->getCapability('AUTH=PLAIN')) { |
| | | $auth_methods[] = 'PLAIN'; |
| | | } |
| | | // RFC 2595 (LOGINDISABLED) LOGIN disabled when connection is not secure |
| | | if (!$this->getCapability('LOGINDISABLED')) { |
| | | $auth_methods[] = 'LOGIN'; |
| | | } |
| | | } |
| | | else { |
| | | $auth_methods[] = $auth_method; |
| | | } |
| | | |
| | | // Authenticate |
| | | foreach ($auth_methods as $method) { |
| | | switch ($method) { |
| | | case 'AUTH': |
| | | $result = $this->authenticate($user, $password, 'CRAM-MD5'); |
| | | break; |
| | | case 'PLAIN': |
| | | $result = $this->authenticate($user, $password, 'PLAIN'); |
| | | break; |
| | | case 'LOGIN': |
| | | $result = $this->login($user, $password); |
| | | break; |
| | | default: |
| | | $this->set_error(self::ERROR_BAD, "Configuration error. Unknown auth method: $method"); |
| | | } |
| | | |
| | | if (is_resource($result)) { |
| | | break; |
| | | } |
| | | } |
| | | |
| | | if ($auth_method == 'AUTH') { |
| | | // do CRAM-MD5 authentication |
| | | $this->putLine($this->next_tag() . " AUTHENTICATE CRAM-MD5"); |
| | | $line = trim($this->readLine(1024)); |
| | | |
| | | if ($line[0] == '+') { |
| | | // got a challenge string, try CRAM-MD5 |
| | | $result = $this->authenticate($user, $password, substr($line,2)); |
| | | |
| | | // stop if server sent BYE response |
| | | if ($result == self::ERROR_BYE) { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | if (!is_resource($result) && $orig_method == 'CHECK') { |
| | | $auth_method = 'PLAIN'; |
| | | } |
| | | } |
| | | |
| | | if ($auth_method == 'PLAIN') { |
| | | // do plain text auth |
| | | $result = $this->login($user, $password); |
| | | } |
| | | |
| | | // Connected and authenticated |
| | | if (is_resource($result)) { |
| | | if ($this->prefs['force_caps']) { |
| | | $this->clearCapability(); |
| | |
| | | $this->logged = true; |
| | | |
| | | return true; |
| | | } else { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | // Close connection |
| | | @fclose($this->fp); |
| | | $this->fp = false; |
| | | |
| | | return false; |
| | | } |
| | | |
| | | function connected() |
| | |
| | | if (empty($mailbox)) { |
| | | return false; |
| | | } |
| | | |
| | | if ($this->selected == $mailbox) { |
| | | return true; |
| | | } |
| | | |
| | | $key = $this->next_tag(); |
| | | $command = "$key SELECT " . $this->escape($mailbox); |
| | | list($code, $response) = $this->execute('SELECT', array($this->escape($mailbox))); |
| | | |
| | | if (!$this->putLine($command)) { |
| | | $this->set_error(self::ERROR_COMMAND, "Unable to send command: $command"); |
| | | return false; |
| | | } |
| | | if ($code == self::ERROR_OK) { |
| | | $response = explode("\r\n", $response); |
| | | foreach ($response as $line) { |
| | | if (preg_match('/^\* ([0-9]+) (EXISTS|RECENT)$/i', $line, $m)) { |
| | | $this->data[strtoupper($m[2])] = (int) $m[1]; |
| | | } |
| | | else if (preg_match('/^\* OK \[(UIDNEXT|UIDVALIDITY|UNSEEN) ([0-9]+)\]/i', $line, $match)) { |
| | | $this->data[strtoupper($match[1])] = (int) $match[2]; |
| | | } |
| | | else if (preg_match('/^\* OK \[PERMANENTFLAGS \(([^\)]+)\)\]/iU', $line, $match)) { |
| | | $this->data['PERMANENTFLAGS'] = explode(' ', $match[1]); |
| | | } |
| | | } |
| | | |
| | | do { |
| | | $line = rtrim($this->readLine(512)); |
| | | |
| | | if (preg_match('/^\* ([0-9]+) (EXISTS|RECENT)$/', $line, $m)) { |
| | | $token = strtolower($m[2]); |
| | | $this->$token = (int) $m[1]; |
| | | } |
| | | else if (preg_match('/\[?PERMANENTFLAGS\s+\(([^\)]+)\)\]/U', $line, $match)) { |
| | | $this->permanentflags = explode(' ', $match[1]); |
| | | } |
| | | } while (!$this->startsWith($line, $key, true, true)); |
| | | |
| | | if ($this->parseResult($line, 'SELECT: ') == self::ERROR_OK) { |
| | | $this->selected = $mailbox; |
| | | return true; |
| | | } |
| | | |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Executes STATUS comand |
| | | * |
| | | * @param string $mailbox Mailbox name |
| | | * @param array $items Requested item names |
| | | * |
| | | * @return array Status item-value hash |
| | | * @access public |
| | | * @since 0.5-beta |
| | | */ |
| | | function status($mailbox, $items) |
| | | { |
| | | if (empty($mailbox) || empty($items)) { |
| | | return false; |
| | | } |
| | | |
| | | list($code, $response) = $this->execute('STATUS', array($this->escape($mailbox), |
| | | '(' . implode(' ', (array) $items) . ')')); |
| | | |
| | | if ($code == self::ERROR_OK && preg_match('/\* STATUS /i', $response)) { |
| | | $result = array(); |
| | | $response = substr($response, 9); // remove prefix "* STATUS " |
| | | |
| | | list($mbox, $items) = $this->tokenizeResponse($response, 2); |
| | | |
| | | for ($i=0, $len=count($items); $i<$len; $i += 2) { |
| | | $result[$items[$i]] = (int) $items[$i+1]; |
| | | } |
| | | |
| | | return $result; |
| | | } |
| | | |
| | | return false; |
| | |
| | | |
| | | $this->select($mailbox); |
| | | if ($this->selected == $mailbox) { |
| | | return $this->recent; |
| | | return $this->data['RECENT']; |
| | | } |
| | | |
| | | return false; |
| | |
| | | |
| | | $this->select($mailbox); |
| | | if ($this->selected == $mailbox) { |
| | | return $this->exists; |
| | | return $this->data['EXISTS']; |
| | | } |
| | | |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Returns count of messages without \Seen flag in a specified folder |
| | | * |
| | | * @param string $mailbox Mailbox name |
| | | * |
| | | * @return int Number of messages, False on error |
| | | * @access public |
| | | */ |
| | | function countUnseen($mailbox) |
| | | { |
| | | // Try STATUS, should be faster |
| | | $counts = $this->status($mailbox, array('UNSEEN')); |
| | | if (is_array($counts)) { |
| | | return (int) $counts['UNSEEN']; |
| | | } |
| | | |
| | | // Invoke SEARCH as a fallback |
| | | $index = $this->search($mailbox, 'ALL UNSEEN', false, array('COUNT')); |
| | | if (is_array($index)) { |
| | | return (int) $index['COUNT']; |
| | | } |
| | | |
| | | return false; |
| | | } |
| | |
| | | return false; |
| | | } |
| | | |
| | | /* Do "SELECT" command */ |
| | | if (!$this->select($mailbox)) { |
| | | return false; |
| | | } |
| | |
| | | return $result; |
| | | } |
| | | |
| | | private function compressMessageSet($message_set) |
| | | private function compressMessageSet($message_set, $force=false) |
| | | { |
| | | // given a comma delimited list of independent mid's, |
| | | // compresses by grouping sequences together |
| | | |
| | | // if less than 255 bytes long, let's not bother |
| | | if (strlen($message_set)<255) { |
| | | if (!$force && strlen($message_set)<255) { |
| | | return $message_set; |
| | | } |
| | | |
| | | // see if it's already been compress |
| | | // see if it's already been compressed |
| | | if (strpos($message_set, ':') !== false) { |
| | | return $message_set; |
| | | } |
| | |
| | | return $result; |
| | | } |
| | | |
| | | function countUnseen($folder) |
| | | { |
| | | $index = $this->search($folder, 'ALL UNSEEN'); |
| | | if (is_array($index)) |
| | | return count($index); |
| | | return false; |
| | | } |
| | | |
| | | // Don't be tempted to change $str to pass by reference to speed this up - it will slow it down by about |
| | | // 7 times instead :-) See comments on http://uk2.php.net/references and this article: |
| | | // http://derickrethans.nl/files/phparch-php-variables-article.pdf |
| | |
| | | } |
| | | |
| | | // return empty result when folder is empty and we're just after SELECT |
| | | if ($old_sel != $folder && !$this->exists) { |
| | | if ($old_sel != $folder && !$this->data['EXISTS']) { |
| | | return array(array(), array(), array()); |
| | | } |
| | | |
| | |
| | | return false; |
| | | } |
| | | |
| | | function search($folder, $criteria, $return_uid=false) |
| | | /** |
| | | * Executes SEARCH command |
| | | * |
| | | * @param string $mailbox Mailbox name |
| | | * @param string $criteria Searching criteria |
| | | * @param bool $return_uid Enable UID in result instead of sequence ID |
| | | * @param array $items Return items (MIN, MAX, COUNT, ALL) |
| | | * |
| | | * @return array Message identifiers or item-value hash |
| | | */ |
| | | function search($mailbox, $criteria, $return_uid=false, $items=array()) |
| | | { |
| | | $old_sel = $this->selected; |
| | | |
| | | if (!$this->select($folder)) { |
| | | if (!$this->select($mailbox)) { |
| | | return false; |
| | | } |
| | | |
| | | // return empty result when folder is empty and we're just after SELECT |
| | | if ($old_sel != $folder && !$this->exists) { |
| | | return array(); |
| | | if ($old_sel != $mailbox && !$this->data['EXISTS']) { |
| | | if (!empty($items)) |
| | | return array_combine($items, array_fill(0, count($items), 0)); |
| | | else |
| | | return array(); |
| | | } |
| | | |
| | | $esearch = empty($items) ? false : $this->getCapability('ESEARCH'); |
| | | $criteria = trim($criteria); |
| | | $params = ''; |
| | | |
| | | // RFC4731: ESEARCH |
| | | if (!empty($items) && $esearch) { |
| | | $params .= 'RETURN (' . implode(' ', $items) . ')'; |
| | | } |
| | | if (!empty($criteria)) { |
| | | $params .= ($params ? ' ' : '') . $criteria; |
| | | } |
| | | else { |
| | | $params .= 'ALL'; |
| | | } |
| | | |
| | | list($code, $response) = $this->execute($return_uid ? 'UID SEARCH' : 'SEARCH', |
| | | array(trim($criteria))); |
| | | array($params)); |
| | | |
| | | if ($code == self::ERROR_OK) { |
| | | // remove prefix and \r\n from raw response |
| | | $response = str_replace("\r\n", '', substr($response, 9)); |
| | | return preg_split('/\s+/', $response, -1, PREG_SPLIT_NO_EMPTY); |
| | | } |
| | | $response = substr($response, $esearch ? 10 : 9); |
| | | $response = str_replace("\r\n", '', $response); |
| | | |
| | | if ($esearch) { |
| | | // Skip prefix: ... (TAG "A285") UID ... |
| | | $this->tokenizeResponse($response, $return_uid ? 2 : 1); |
| | | |
| | | $result = array(); |
| | | for ($i=0; $i<count($items); $i++) { |
| | | // If the SEARCH results in no matches, the server MUST NOT |
| | | // include the item result option in the ESEARCH response |
| | | if ($ret = $this->tokenizeResponse($response, 2)) { |
| | | list ($name, $value) = $ret; |
| | | $result[$name] = $value; |
| | | } |
| | | } |
| | | |
| | | return $result; |
| | | } |
| | | else { |
| | | $response = preg_split('/\s+/', $response, -1, PREG_SPLIT_NO_EMPTY); |
| | | |
| | | if (!empty($items)) { |
| | | $result = array(); |
| | | if (in_array('COUNT', $items)) |
| | | $result['COUNT'] = count($response); |
| | | if (in_array('MIN', $items)) |
| | | $result['MIN'] = !empty($response) ? min($response) : 0; |
| | | if (in_array('MAX', $items)) |
| | | $result['MAX'] = !empty($response) ? max($response) : 0; |
| | | if (in_array('ALL', $items)) |
| | | $result['ALL'] = $this->compressMessageSet(implode(',', $response), true); |
| | | |
| | | return $result; |
| | | } |
| | | else { |
| | | return $response; |
| | | } |
| | | } |
| | | } |
| | | |
| | | return false; |
| | | } |
| | |
| | | return $r; |
| | | } |
| | | |
| | | function listMailboxes($ref, $mailbox) |
| | | /** |
| | | * Returns list of mailboxes |
| | | * |
| | | * @param string $ref Reference name |
| | | * @param string $mailbox Mailbox name |
| | | * @param array $status_opts (see self::_listMailboxes) |
| | | * |
| | | * @return array List of mailboxes or hash of options if $status_opts argument |
| | | * is non-empty. |
| | | * @access public |
| | | */ |
| | | function listMailboxes($ref, $mailbox, $status_opts=array()) |
| | | { |
| | | return $this->_listMailboxes($ref, $mailbox, false); |
| | | return $this->_listMailboxes($ref, $mailbox, false, $status_opts); |
| | | } |
| | | |
| | | function listSubscribed($ref, $mailbox) |
| | | /** |
| | | * Returns list of subscribed mailboxes |
| | | * |
| | | * @param string $ref Reference name |
| | | * @param string $mailbox Mailbox name |
| | | * @param array $status_opts (see self::_listMailboxes) |
| | | * |
| | | * @return array List of mailboxes or hash of options if $status_ops argument |
| | | * is non-empty. |
| | | * @access public |
| | | */ |
| | | function listSubscribed($ref, $mailbox, $status_opts=array()) |
| | | { |
| | | return $this->_listMailboxes($ref, $mailbox, true); |
| | | return $this->_listMailboxes($ref, $mailbox, true, $status_opts); |
| | | } |
| | | |
| | | private function _listMailboxes($ref, $mailbox, $subscribed=false) |
| | | /** |
| | | * IMAP LIST/LSUB command |
| | | * |
| | | * @param string $ref Reference name |
| | | * @param string $mailbox Mailbox name |
| | | * @param bool $subscribed Enables returning subscribed mailboxes only |
| | | * @param array $status_opts List of STATUS options (RFC5819: LIST-STATUS) |
| | | * Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN |
| | | * |
| | | * @return array List of mailboxes or hash of options if $status_ops argument |
| | | * is non-empty. |
| | | * @access private |
| | | */ |
| | | private function _listMailboxes($ref, $mailbox, $subscribed=false, $status_opts=array()) |
| | | { |
| | | if (empty($mailbox)) { |
| | | $mailbox = '*'; |
| | |
| | | $ref = $this->rootdir; |
| | | } |
| | | |
| | | $command = $subscribed ? 'LSUB' : 'LIST'; |
| | | $key = $this->next_tag(); |
| | | $query = sprintf("%s %s %s %s", $key, $command, |
| | | $this->escape($ref), $this->escape($mailbox)); |
| | | $args = array($this->escape($ref), $this->escape($mailbox)); |
| | | |
| | | // send command |
| | | if (!$this->putLine($query)) { |
| | | $this->set_error(self::ERROR_COMMAND, "Unable to send command: $query"); |
| | | return false; |
| | | } |
| | | if (!empty($status_opts) && $this->getCapability('LIST-STATUS')) { |
| | | $status_opts = array($status_opts); |
| | | $lstatus = true; |
| | | |
| | | // get folder list |
| | | do { |
| | | $line = $this->readLine(500); |
| | | $line = $this->multLine($line, true); |
| | | $line = trim($line); |
| | | $args[] = 'RETURN (STATUS (' . implode(' ', $status_opts) . '))'; |
| | | } |
| | | |
| | | if (preg_match('/^\* '.$command.' \(([^\)]*)\) "*([^"]+)"* (.*)$/', $line, $m)) { |
| | | // folder name |
| | | $folders[] = preg_replace(array('/^"/', '/"$/'), '', $this->unEscape($m[3])); |
| | | // attributes |
| | | // $attrib = explode(' ', $this->unEscape($m[1])); |
| | | // delimiter |
| | | // $delim = $this->unEscape($m[2]); |
| | | list($code, $response) = $this->execute($subscribed ? 'LSUB' : 'LIST', $args); |
| | | |
| | | if ($code == self::ERROR_OK) { |
| | | $folders = array(); |
| | | while ($this->tokenizeResponse($response, 1) == '*') { |
| | | $cmd = strtoupper($this->tokenizeResponse($response, 1)); |
| | | // * LIST (<options>) <delimiter> <mailbox> |
| | | if (!$lstatus || $cmd == 'LIST' || $cmd == 'LSUB') { |
| | | list($opts, $delim, $folder) = $this->tokenizeResponse($response, 3); |
| | | if (!$lstatus) { |
| | | $folders[] = $folder; |
| | | } |
| | | else { |
| | | $folders[$folder] = array(); |
| | | } |
| | | } |
| | | // * STATUS <mailbox> (<result>) |
| | | else if ($cmd == 'STATUS') { |
| | | list($folder, $status) = $this->tokenizeResponse($response, 2); |
| | | |
| | | for ($i=0, $len=count($status); $i<$len; $i += 2) { |
| | | list($name, $value) = $this->tokenizeResponse($status, 2); |
| | | $folders[$folder][$name] = $value; |
| | | } |
| | | } |
| | | } |
| | | } while (!$this->startsWith($line, $key, true)); |
| | | |
| | | if (is_array($folders)) { |
| | | return $folders; |
| | | } else if ($this->parseResult($line, $command.': ') == self::ERROR_OK) { |
| | | return array(); |
| | | return $folders; |
| | | } |
| | | |
| | | return false; |