| | |
| | | '*' => '\\*', |
| | | ); |
| | | |
| | | public static $mupdate; |
| | | |
| | | private $fp; |
| | | private $host; |
| | | private $logged = false; |
| | |
| | | const COMMAND_NORESPONSE = 1; |
| | | const COMMAND_CAPABILITY = 2; |
| | | const COMMAND_LASTLINE = 4; |
| | | const COMMAND_ANONYMIZED = 8; |
| | | |
| | | const DEBUG_LINE_LENGTH = 4098; // 4KB + 2B for \r\n |
| | | |
| | | /** |
| | | * Object constructor |
| | |
| | | * |
| | | * @param string $string Command string |
| | | * @param bool $endln True if CRLF need to be added at the end of command |
| | | * @param bool $anonymized Don't write the given data to log but a placeholder |
| | | * |
| | | * @param int Number of bytes sent, False on error |
| | | */ |
| | | function putLine($string, $endln=true) |
| | | function putLine($string, $endln=true, $anonymized=false) |
| | | { |
| | | if (!$this->fp) |
| | | return false; |
| | | |
| | | if ($this->_debug) { |
| | | $this->debug('C: '. rtrim($string)); |
| | | // anonymize the sent command for logging |
| | | $cut = $endln ? 2 : 0; |
| | | if ($anonymized && preg_match('/^(A\d+ (?:[A-Z]+ )+)(.+)/', $string, $m)) { |
| | | $log = $m[1] . sprintf('****** [%d]', strlen($m[2]) - $cut); |
| | | } |
| | | else if ($anonymized) { |
| | | $log = sprintf('****** [%d]', strlen($string) - $cut); |
| | | } |
| | | else { |
| | | $log = rtrim($string); |
| | | } |
| | | $this->debug('C: ' . $log); |
| | | } |
| | | |
| | | $res = fwrite($this->fp, $string . ($endln ? "\r\n" : '')); |
| | |
| | | * |
| | | * @param string $string Command string |
| | | * @param bool $endln True if CRLF need to be added at the end of command |
| | | * @param bool $anonymized Don't write the given data to log but a placeholder |
| | | * |
| | | * @return int|bool Number of bytes sent, False on error |
| | | */ |
| | | function putLineC($string, $endln=true) |
| | | function putLineC($string, $endln=true, $anonymized=false) |
| | | { |
| | | if (!$this->fp) { |
| | | return false; |
| | |
| | | $parts[$i+1] = sprintf("{%d+}\r\n", $matches[1]); |
| | | } |
| | | |
| | | $bytes = $this->putLine($parts[$i].$parts[$i+1], false); |
| | | $bytes = $this->putLine($parts[$i].$parts[$i+1], false, $anonymized); |
| | | if ($bytes === false) |
| | | return false; |
| | | $res += $bytes; |
| | |
| | | $i++; |
| | | } |
| | | else { |
| | | $bytes = $this->putLine($parts[$i], false); |
| | | $bytes = $this->putLine($parts[$i], false, $anonymized); |
| | | if ($bytes === false) |
| | | return false; |
| | | $res += $bytes; |
| | |
| | | $reply = base64_encode($user . ' ' . $hash); |
| | | |
| | | // send result |
| | | $this->putLine($reply); |
| | | $this->putLine($reply, true, true); |
| | | } |
| | | else { |
| | | // RFC2831: DIGEST-MD5 |
| | |
| | | base64_decode($challenge), $this->host, 'imap', $user)); |
| | | |
| | | // send result |
| | | $this->putLine($reply); |
| | | $this->putLine($reply, true, true); |
| | | $line = trim($this->readReply()); |
| | | |
| | | if ($line[0] == '+') { |
| | |
| | | // RFC 4959 (SASL-IR): save one round trip |
| | | if ($this->getCapability('SASL-IR')) { |
| | | list($result, $line) = $this->execute("AUTHENTICATE PLAIN", array($reply), |
| | | self::COMMAND_LASTLINE | self::COMMAND_CAPABILITY); |
| | | self::COMMAND_LASTLINE | self::COMMAND_CAPABILITY | self::COMMAND_ANONYMIZED); |
| | | } |
| | | else { |
| | | $this->putLine($this->nextTag() . " AUTHENTICATE PLAIN"); |
| | |
| | | } |
| | | |
| | | // send result, get reply and process it |
| | | $this->putLine($reply); |
| | | $this->putLine($reply, true, true); |
| | | $line = $this->readReply(); |
| | | $result = $this->parseResult($line); |
| | | } |
| | |
| | | function login($user, $password) |
| | | { |
| | | list($code, $response) = $this->execute('LOGIN', array( |
| | | $this->escape($user), $this->escape($password)), self::COMMAND_CAPABILITY); |
| | | $this->escape($user), $this->escape($password)), self::COMMAND_CAPABILITY | self::COMMAND_ANONYMIZED); |
| | | |
| | | // re-set capabilities list if untagged CAPABILITY response provided |
| | | if (preg_match('/\* CAPABILITY (.+)/i', $response, $matches)) { |
| | |
| | | */ |
| | | function connect($host, $user, $password, $options=null) |
| | | { |
| | | // set options |
| | | if (is_array($options)) { |
| | | $this->prefs = $options; |
| | | } |
| | | // set auth method |
| | | if (!empty($this->prefs['auth_type'])) { |
| | | $auth_method = strtoupper($this->prefs['auth_type']); |
| | | } else { |
| | | $auth_method = 'CHECK'; |
| | | } |
| | | // configure |
| | | $this->set_prefs($options); |
| | | |
| | | $result = false; |
| | | |
| | | // initialize connection |
| | | $this->error = ''; |
| | | $this->errornum = self::ERROR_OK; |
| | | $this->selected = null; |
| | | $this->user = $user; |
| | | $this->host = $host; |
| | | $this->user = $user; |
| | | $this->logged = false; |
| | | $this->selected = null; |
| | | |
| | | // check input |
| | | if (empty($host)) { |
| | | $this->setError(self::ERROR_BAD, "Empty host"); |
| | | return false; |
| | | } |
| | | |
| | | if (empty($user)) { |
| | | $this->setError(self::ERROR_NO, "Empty user"); |
| | | return false; |
| | | } |
| | | |
| | | if (empty($password)) { |
| | | $this->setError(self::ERROR_NO, "Empty password"); |
| | | return false; |
| | | } |
| | | |
| | | if (!$this->prefs['port']) { |
| | | $this->prefs['port'] = 143; |
| | | } |
| | | // check for SSL |
| | | if ($this->prefs['ssl_mode'] && $this->prefs['ssl_mode'] != 'tls') { |
| | | $host = $this->prefs['ssl_mode'] . '://' . $host; |
| | | } |
| | | |
| | | if ($this->prefs['timeout'] <= 0) { |
| | | $this->prefs['timeout'] = ini_get('default_socket_timeout'); |
| | | } |
| | | |
| | | // Connect |
| | | $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']); |
| | | |
| | | if (!$this->fp) { |
| | | $this->setError(self::ERROR_BAD, sprintf("Could not connect to %s:%d: %s", $host, $this->prefs['port'], $errstr)); |
| | | if (!$this->_connect($host)) { |
| | | return false; |
| | | } |
| | | |
| | | if ($this->prefs['timeout'] > 0) |
| | | stream_set_timeout($this->fp, $this->prefs['timeout']); |
| | | |
| | | $line = trim(fgets($this->fp, 8192)); |
| | | |
| | | if ($this->_debug) { |
| | | // set connection identifier for debug output |
| | | preg_match('/#([0-9]+)/', (string)$this->fp, $m); |
| | | $this->resourceid = strtoupper(substr(md5($m[1].$this->user.microtime()), 0, 4)); |
| | | |
| | | if ($line) |
| | | $this->debug('S: '. $line); |
| | | } |
| | | |
| | | // Connected to wrong port or connection error? |
| | | if (!preg_match('/^\* (OK|PREAUTH)/i', $line)) { |
| | | if ($line) |
| | | $error = sprintf("Wrong startup greeting (%s:%d): %s", $host, $this->prefs['port'], $line); |
| | | else |
| | | $error = sprintf("Empty startup greeting (%s:%d)", $host, $this->prefs['port']); |
| | | |
| | | $this->setError(self::ERROR_BAD, $error); |
| | | $this->closeConnection(); |
| | | return false; |
| | | } |
| | | |
| | | // RFC3501 [7.1] optional CAPABILITY response |
| | | if (preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) { |
| | | $this->parseCapability($matches[1], true); |
| | | } |
| | | |
| | | // TLS connection |
| | | if ($this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) { |
| | | if (version_compare(PHP_VERSION, '5.1.0', '>=')) { |
| | | $res = $this->execute('STARTTLS'); |
| | | |
| | | if ($res[0] != self::ERROR_OK) { |
| | | $this->closeConnection(); |
| | | return false; |
| | | } |
| | | |
| | | if (!stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { |
| | | $this->setError(self::ERROR_BAD, "Unable to negotiate TLS"); |
| | | $this->closeConnection(); |
| | | return false; |
| | | } |
| | | |
| | | // Now we're secure, capabilities need to be reread |
| | | $this->clearCapability(); |
| | | } |
| | | } |
| | | |
| | | // Send ID info |
| | |
| | | $this->id($this->prefs['ident']); |
| | | } |
| | | |
| | | $auth_method = $this->prefs['auth_type']; |
| | | $auth_methods = array(); |
| | | $result = null; |
| | | |
| | |
| | | } |
| | | |
| | | /** |
| | | * Connects to IMAP server. |
| | | * |
| | | * @param string $host Server hostname or IP |
| | | * |
| | | * @return bool True on success, False on failure |
| | | */ |
| | | protected function _connect($host) |
| | | { |
| | | // initialize connection |
| | | $this->error = ''; |
| | | $this->errornum = self::ERROR_OK; |
| | | |
| | | if (!$this->prefs['port']) { |
| | | $this->prefs['port'] = 143; |
| | | } |
| | | |
| | | // check for SSL |
| | | if ($this->prefs['ssl_mode'] && $this->prefs['ssl_mode'] != 'tls') { |
| | | $host = $this->prefs['ssl_mode'] . '://' . $host; |
| | | } |
| | | |
| | | if ($this->prefs['timeout'] <= 0) { |
| | | $this->prefs['timeout'] = max(0, intval(ini_get('default_socket_timeout'))); |
| | | } |
| | | |
| | | if (!empty($this->prefs['socket_options'])) { |
| | | $context = stream_context_create($this->prefs['socket_options']); |
| | | $this->fp = stream_socket_client($host . ':' . $this->prefs['port'], $errno, $errstr, |
| | | $this->prefs['timeout'], STREAM_CLIENT_CONNECT, $context); |
| | | } |
| | | else { |
| | | $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']); |
| | | } |
| | | |
| | | if (!$this->fp) { |
| | | $this->setError(self::ERROR_BAD, sprintf("Could not connect to %s:%d: %s", |
| | | $host, $this->prefs['port'], $errstr ? $errstr : "Unknown reason")); |
| | | |
| | | return false; |
| | | } |
| | | |
| | | if ($this->prefs['timeout'] > 0) { |
| | | stream_set_timeout($this->fp, $this->prefs['timeout']); |
| | | } |
| | | |
| | | $line = trim(fgets($this->fp, 8192)); |
| | | |
| | | if ($this->_debug) { |
| | | // set connection identifier for debug output |
| | | preg_match('/#([0-9]+)/', (string) $this->fp, $m); |
| | | $this->resourceid = strtoupper(substr(md5($m[1].$this->user.microtime()), 0, 4)); |
| | | |
| | | if ($line) { |
| | | $this->debug('S: '. $line); |
| | | } |
| | | } |
| | | |
| | | // Connected to wrong port or connection error? |
| | | if (!preg_match('/^\* (OK|PREAUTH)/i', $line)) { |
| | | if ($line) |
| | | $error = sprintf("Wrong startup greeting (%s:%d): %s", $host, $this->prefs['port'], $line); |
| | | else |
| | | $error = sprintf("Empty startup greeting (%s:%d)", $host, $this->prefs['port']); |
| | | |
| | | $this->setError(self::ERROR_BAD, $error); |
| | | $this->closeConnection(); |
| | | return false; |
| | | } |
| | | |
| | | // RFC3501 [7.1] optional CAPABILITY response |
| | | if (preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) { |
| | | $this->parseCapability($matches[1], true); |
| | | } |
| | | |
| | | // TLS connection |
| | | if ($this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) { |
| | | $res = $this->execute('STARTTLS'); |
| | | |
| | | if ($res[0] != self::ERROR_OK) { |
| | | $this->closeConnection(); |
| | | return false; |
| | | } |
| | | |
| | | if (!stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { |
| | | $this->setError(self::ERROR_BAD, "Unable to negotiate TLS"); |
| | | $this->closeConnection(); |
| | | return false; |
| | | } |
| | | |
| | | // Now we're secure, capabilities need to be reread |
| | | $this->clearCapability(); |
| | | } |
| | | |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Initializes environment |
| | | */ |
| | | protected function set_prefs($prefs) |
| | | { |
| | | // set preferences |
| | | if (is_array($prefs)) { |
| | | $this->prefs = $prefs; |
| | | } |
| | | |
| | | // set auth method |
| | | if (!empty($this->prefs['auth_type'])) { |
| | | $this->prefs['auth_type'] = strtoupper($this->prefs['auth_type']); |
| | | } |
| | | else { |
| | | $this->prefs['auth_type'] = 'CHECK'; |
| | | } |
| | | |
| | | // disabled capabilities |
| | | if (!empty($this->prefs['disabled_caps'])) { |
| | | $this->prefs['disabled_caps'] = array_map('strtoupper', (array)$this->prefs['disabled_caps']); |
| | | } |
| | | |
| | | // additional message flags |
| | | if (!empty($this->prefs['message_flags'])) { |
| | | $this->flags = array_merge($this->flags, $this->prefs['message_flags']); |
| | | unset($this->prefs['message_flags']); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Checks connection status |
| | | * |
| | | * @return bool True if connection is active and user is logged in, False otherwise. |
| | |
| | | */ |
| | | function closeConnection() |
| | | { |
| | | if ($this->putLine($this->nextTag() . ' LOGOUT')) { |
| | | if ($this->logged && $this->putLine($this->nextTag() . ' LOGOUT')) { |
| | | $this->readReply(); |
| | | } |
| | | |
| | |
| | | /** |
| | | * Executes EXPUNGE command |
| | | * |
| | | * @param string $mailbox Mailbox name |
| | | * @param string $messages Message UIDs to expunge |
| | | * @param string $mailbox Mailbox name |
| | | * @param string|array $messages Message UIDs to expunge |
| | | * |
| | | * @return boolean True on success, False on error |
| | | */ |
| | |
| | | } |
| | | |
| | | if (!$this->data['READ-WRITE']) { |
| | | $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'EXPUNGE'); |
| | | $this->setError(self::ERROR_READONLY, "Mailbox is read-only"); |
| | | return false; |
| | | } |
| | | |
| | | // Clear internal status cache |
| | | unset($this->data['STATUS:'.$mailbox]); |
| | | $this->clear_status_cache($mailbox); |
| | | |
| | | if ($messages) |
| | | $result = $this->execute('UID EXPUNGE', array($messages), self::COMMAND_NORESPONSE); |
| | | else |
| | | if (!empty($messages) && $messages != '*' && $this->hasCapability('UIDPLUS')) { |
| | | $messages = self::compressMessageSet($messages); |
| | | $result = $this->execute('UID EXPUNGE', array($messages), self::COMMAND_NORESPONSE); |
| | | } |
| | | else { |
| | | $result = $this->execute('EXPUNGE', null, self::COMMAND_NORESPONSE); |
| | | } |
| | | |
| | | if ($result == self::ERROR_OK) { |
| | | $this->selected = null; // state has changed, need to reselect |
| | |
| | | if ($cmd == 'LIST' || $cmd == 'LSUB') { |
| | | list($opts, $delim, $mailbox) = $this->tokenizeResponse($line, 3); |
| | | |
| | | // Remove redundant separator at the end of folder name, UW-IMAP bug? (#1488879) |
| | | if ($delim) { |
| | | $mailbox = rtrim($mailbox, $delim); |
| | | } |
| | | |
| | | // Add to result array |
| | | if (!$lstatus) { |
| | | $folders[] = $mailbox; |
| | |
| | | $folders[$mailbox] = array(); |
| | | } |
| | | |
| | | // store LSUB options only if not empty, this way |
| | | // we can detect a situation when LIST doesn't return specified folder |
| | | if (!empty($opts) || $cmd == 'LIST') { |
| | | // store folder options |
| | | if ($cmd == 'LIST') { |
| | | // Add to options array |
| | | if (empty($this->data['LIST'][$mailbox])) |
| | | $this->data['LIST'][$mailbox] = $opts; |
| | |
| | | * |
| | | * @return int Number of messages, False on error |
| | | */ |
| | | function countMessages($mailbox, $refresh = false) |
| | | function countMessages($mailbox) |
| | | { |
| | | if ($refresh) { |
| | | $this->selected = null; |
| | | } |
| | | |
| | | if ($this->selected === $mailbox) { |
| | | if ($this->selected === $mailbox && isset($this->data['EXISTS'])) { |
| | | return $this->data['EXISTS']; |
| | | } |
| | | |
| | |
| | | */ |
| | | function countRecent($mailbox) |
| | | { |
| | | if (!strlen($mailbox)) { |
| | | $mailbox = 'INBOX'; |
| | | if ($this->selected === $mailbox && isset($this->data['RECENT'])) { |
| | | return $this->data['RECENT']; |
| | | } |
| | | |
| | | $this->select($mailbox); |
| | | // Check internal cache |
| | | $cache = $this->data['STATUS:'.$mailbox]; |
| | | if (!empty($cache) && isset($cache['RECENT'])) { |
| | | return (int) $cache['RECENT']; |
| | | } |
| | | |
| | | if ($this->selected === $mailbox) { |
| | | return $this->data['RECENT']; |
| | | // Try STATUS (should be faster than SELECT) |
| | | $counts = $this->status($mailbox, array('RECENT')); |
| | | if (is_array($counts)) { |
| | | return (int) $counts['RECENT']; |
| | | } |
| | | |
| | | return false; |
| | |
| | | } |
| | | |
| | | // message IDs |
| | | if (!empty($add)) |
| | | if (!empty($add)) { |
| | | $add = $this->compressMessageSet($add); |
| | | } |
| | | |
| | | list($code, $response) = $this->execute($return_uid ? 'UID SORT' : 'SORT', |
| | | array("($field)", $encoding, 'ALL' . (!empty($add) ? ' '.$add : ''))); |
| | | array("($field)", $encoding, !empty($add) ? $add : 'ALL')); |
| | | |
| | | if ($code != self::ERROR_OK) { |
| | | $response = null; |
| | |
| | | } |
| | | |
| | | if (!empty($criteria)) { |
| | | $modseq = stripos($criteria, 'MODSEQ') !== false; |
| | | $params .= ($params ? ' ' : '') . $criteria; |
| | | } |
| | | else { |
| | |
| | | if ($skip_deleted && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) { |
| | | $flags = explode(' ', strtoupper($matches[1])); |
| | | if (in_array('\\DELETED', $flags)) { |
| | | $deleted[$id] = $id; |
| | | continue; |
| | | } |
| | | } |
| | |
| | | $result[$id] = ''; |
| | | } |
| | | } else if ($mode == 2) { |
| | | if (preg_match('/(UID|RFC822\.SIZE) ([0-9]+)/', $line, $matches)) { |
| | | $result[$id] = trim($matches[2]); |
| | | if (preg_match('/' . $index_field . ' ([0-9]+)/', $line, $matches)) { |
| | | $result[$id] = trim($matches[1]); |
| | | } else { |
| | | $result[$id] = 0; |
| | | } |
| | |
| | | */ |
| | | private function modFlag($mailbox, $messages, $flag, $mod = '+') |
| | | { |
| | | if ($mod != '+' && $mod != '-') { |
| | | $mod = '+'; |
| | | } |
| | | |
| | | if (!$this->select($mailbox)) { |
| | | return false; |
| | | } |
| | | |
| | | if (!$this->data['READ-WRITE']) { |
| | | $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'STORE'); |
| | | $this->setError(self::ERROR_READONLY, "Mailbox is read-only"); |
| | | return false; |
| | | } |
| | | |
| | | if ($this->flags[strtoupper($flag)]) { |
| | | $flag = $this->flags[strtoupper($flag)]; |
| | | } |
| | | |
| | | if (!$flag) { |
| | | return false; |
| | | } |
| | | |
| | | // if PERMANENTFLAGS is not specified all flags are allowed |
| | | if (!empty($this->data['PERMANENTFLAGS']) |
| | | && !in_array($flag, (array) $this->data['PERMANENTFLAGS']) |
| | | && !in_array('\\*', (array) $this->data['PERMANENTFLAGS']) |
| | | ) { |
| | | return false; |
| | | } |
| | | |
| | |
| | | unset($this->data['STATUS:'.$mailbox]['UNSEEN']); |
| | | } |
| | | |
| | | $flag = $this->flags[strtoupper($flag)]; |
| | | if ($mod != '+' && $mod != '-') { |
| | | $mod = '+'; |
| | | } |
| | | |
| | | $result = $this->execute('UID STORE', array( |
| | | $this->compressMessageSet($messages), $mod . 'FLAGS.SILENT', "($flag)"), |
| | | self::COMMAND_NORESPONSE); |
| | |
| | | |
| | | /** |
| | | * Moves message(s) from one folder to another. |
| | | * Original message(s) will be marked as deleted. |
| | | * |
| | | * @param string|array $messages Message UID(s) |
| | | * @param string $from Mailbox name |
| | |
| | | } |
| | | |
| | | if (!$this->data['READ-WRITE']) { |
| | | $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'STORE'); |
| | | $this->setError(self::ERROR_READONLY, "Mailbox is read-only"); |
| | | return false; |
| | | } |
| | | |
| | | $r = $this->copy($messages, $from, $to); |
| | | // use MOVE command (RFC 6851) |
| | | if ($this->hasCapability('MOVE')) { |
| | | // Clear last COPYUID data |
| | | unset($this->data['COPYUID']); |
| | | |
| | | if ($r) { |
| | | // Clear internal status cache |
| | | unset($this->data['STATUS:'.$to]); |
| | | $this->clear_status_cache($from); |
| | | |
| | | $result = $this->execute('UID MOVE', array( |
| | | $this->compressMessageSet($messages), $this->escape($to)), |
| | | self::COMMAND_NORESPONSE); |
| | | |
| | | return ($result == self::ERROR_OK); |
| | | } |
| | | |
| | | // use COPY + STORE +FLAGS.SILENT \Deleted + EXPUNGE |
| | | $result = $this->copy($messages, $from, $to); |
| | | |
| | | if ($result) { |
| | | // Clear internal status cache |
| | | unset($this->data['STATUS:'.$from]); |
| | | |
| | | return $this->flag($from, $messages, 'DELETED'); |
| | | $result = $this->flag($from, $messages, 'DELETED'); |
| | | |
| | | if ($messages == '*') { |
| | | // CLOSE+SELECT should be faster than EXPUNGE |
| | | $this->close(); |
| | | } |
| | | else { |
| | | $this->expunge($from, $messages); |
| | | } |
| | | } |
| | | return $r; |
| | | |
| | | return $result; |
| | | } |
| | | |
| | | /** |
| | |
| | | else if ($name == 'RFC822') { |
| | | $result[$id]->body = $value; |
| | | } |
| | | else if ($name == 'BODY') { |
| | | $body = $this->tokenizeResponse($line, 1); |
| | | if ($value[0] == 'HEADER.FIELDS') |
| | | $headers = $body; |
| | | else if (!empty($value)) |
| | | $result[$id]->bodypart[$value[0]] = $body; |
| | | else if (stripos($name, 'BODY[') === 0) { |
| | | $name = str_replace(']', '', substr($name, 5)); |
| | | |
| | | if ($name == 'HEADER.FIELDS') { |
| | | // skip ']' after headers list |
| | | $this->tokenizeResponse($line, 1); |
| | | $headers = $this->tokenizeResponse($line, 1); |
| | | } |
| | | else if (strlen($name)) |
| | | $result[$id]->bodypart[$name] = $value; |
| | | else |
| | | $result[$id]->body = $body; |
| | | $result[$id]->body = $value; |
| | | } |
| | | } |
| | | |
| | | // create array with header field:data |
| | | if (!empty($headers)) { |
| | | $headers = explode("\n", trim($headers)); |
| | | foreach ($headers as $hid => $resln) { |
| | | foreach ($headers as $resln) { |
| | | if (ord($resln[0]) <= 32) { |
| | | $lines[$ln] .= (empty($lines[$ln]) ? '' : "\n") . trim($resln); |
| | | } else { |
| | |
| | | } |
| | | } |
| | | |
| | | while (list($lines_key, $str) = each($lines)) { |
| | | foreach ($lines as $str) { |
| | | list($field, $string) = explode(':', $str, 2); |
| | | |
| | | $field = strtolower($field); |
| | |
| | | return $result; |
| | | } |
| | | |
| | | function fetchHeaders($mailbox, $message_set, $is_uid = false, $bodystr = false, $add = '') |
| | | /** |
| | | * Returns message(s) data (flags, headers, etc.) |
| | | * |
| | | * @param string $mailbox Mailbox name |
| | | * @param mixed $message_set Message(s) sequence identifier(s) or UID(s) |
| | | * @param bool $is_uid True if $message_set contains UIDs |
| | | * @param bool $bodystr Enable to add BODYSTRUCTURE data to the result |
| | | * @param array $add_headers List of additional headers |
| | | * |
| | | * @return bool|array List of rcube_message_header elements, False on error |
| | | */ |
| | | function fetchHeaders($mailbox, $message_set, $is_uid = false, $bodystr = false, $add_headers = array()) |
| | | { |
| | | $query_items = array('UID', 'RFC822.SIZE', 'FLAGS', 'INTERNALDATE'); |
| | | if ($bodystr) |
| | | $headers = array('DATE', 'FROM', 'TO', 'SUBJECT', 'CONTENT-TYPE', 'CC', 'REPLY-TO', |
| | | 'LIST-POST', 'DISPOSITION-NOTIFICATION-TO', 'X-PRIORITY'); |
| | | |
| | | if (!empty($add_headers)) { |
| | | $add_headers = array_map('strtoupper', $add_headers); |
| | | $headers = array_unique(array_merge($headers, $add_headers)); |
| | | } |
| | | |
| | | if ($bodystr) { |
| | | $query_items[] = 'BODYSTRUCTURE'; |
| | | $query_items[] = 'BODY.PEEK[HEADER.FIELDS (' |
| | | . 'DATE FROM TO SUBJECT CONTENT-TYPE CC REPLY-TO LIST-POST DISPOSITION-NOTIFICATION-TO X-PRIORITY' |
| | | . ($add ? ' ' . trim($add) : '') |
| | | . ')]'; |
| | | } |
| | | |
| | | $query_items[] = 'BODY.PEEK[HEADER.FIELDS (' . implode(' ', $headers) . ')]'; |
| | | |
| | | $result = $this->fetch($mailbox, $message_set, $is_uid, $query_items); |
| | | |
| | | return $result; |
| | | } |
| | | |
| | | function fetchHeader($mailbox, $id, $uidfetch=false, $bodystr=false, $add='') |
| | | /** |
| | | * Returns message data (flags, headers, etc.) |
| | | * |
| | | * @param string $mailbox Mailbox name |
| | | * @param int $id Message sequence identifier or UID |
| | | * @param bool $is_uid True if $id is an UID |
| | | * @param bool $bodystr Enable to add BODYSTRUCTURE data to the result |
| | | * @param array $add_headers List of additional headers |
| | | * |
| | | * @return bool|rcube_message_header Message data, False on error |
| | | */ |
| | | function fetchHeader($mailbox, $id, $is_uid = false, $bodystr = false, $add_headers = array()) |
| | | { |
| | | $a = $this->fetchHeaders($mailbox, $id, $uidfetch, $bodystr, $add); |
| | | $a = $this->fetchHeaders($mailbox, $id, $is_uid, $bodystr, $add_headers); |
| | | if (is_array($a)) { |
| | | return array_shift($a); |
| | | } |
| | |
| | | return false; |
| | | } |
| | | |
| | | switch ($encoding) { |
| | | case 'base64': |
| | | $mode = 1; |
| | | break; |
| | | case 'quoted-printable': |
| | | $mode = 2; |
| | | break; |
| | | case 'x-uuencode': |
| | | case 'x-uue': |
| | | case 'uue': |
| | | case 'uuencode': |
| | | $mode = 3; |
| | | break; |
| | | default: |
| | | $mode = 0; |
| | | } |
| | | |
| | | // Use BINARY extension when possible (and safe) |
| | | $binary = $mode && preg_match('/^[0-9.]+$/', $part) && $this->hasCapability('BINARY'); |
| | | $fetch_mode = $binary ? 'BINARY' : 'BODY'; |
| | | $partial = $max_bytes ? sprintf('<0.%d>', $max_bytes) : ''; |
| | | |
| | | // format request |
| | | $key = $this->nextTag(); |
| | | $request = $key . ($is_uid ? ' UID' : '') . " FETCH $id ($fetch_mode.PEEK[$part]$partial)"; |
| | | $result = false; |
| | | |
| | | // send request |
| | | if (!$this->putLine($request)) { |
| | | $this->setError(self::ERROR_COMMAND, "Unable to send command: $request"); |
| | | return false; |
| | | } |
| | | |
| | | if ($binary) { |
| | | // WARNING: Use $formatting argument with care, this may break binary data stream |
| | | $mode = -1; |
| | | } |
| | | $binary = true; |
| | | |
| | | do { |
| | | if (!$initiated) { |
| | | switch ($encoding) { |
| | | case 'base64': |
| | | $mode = 1; |
| | | break; |
| | | case 'quoted-printable': |
| | | $mode = 2; |
| | | break; |
| | | case 'x-uuencode': |
| | | case 'x-uue': |
| | | case 'uue': |
| | | case 'uuencode': |
| | | $mode = 3; |
| | | break; |
| | | default: |
| | | $mode = 0; |
| | | } |
| | | |
| | | // Use BINARY extension when possible (and safe) |
| | | $binary = $binary && $mode && preg_match('/^[0-9.]+$/', $part) && $this->hasCapability('BINARY'); |
| | | $fetch_mode = $binary ? 'BINARY' : 'BODY'; |
| | | $partial = $max_bytes ? sprintf('<0.%d>', $max_bytes) : ''; |
| | | |
| | | // format request |
| | | $key = $this->nextTag(); |
| | | $request = $key . ($is_uid ? ' UID' : '') . " FETCH $id ($fetch_mode.PEEK[$part]$partial)"; |
| | | $result = false; |
| | | $found = false; |
| | | $initiated = true; |
| | | |
| | | // send request |
| | | if (!$this->putLine($request)) { |
| | | $this->setError(self::ERROR_COMMAND, "Unable to send command: $request"); |
| | | return false; |
| | | } |
| | | |
| | | if ($binary) { |
| | | // WARNING: Use $formatted argument with care, this may break binary data stream |
| | | $mode = -1; |
| | | } |
| | | } |
| | | |
| | | $line = trim($this->readLine(1024)); |
| | | |
| | | if (!$line) { |
| | | break; |
| | | } |
| | | |
| | | if (!preg_match('/^\* ([0-9]+) FETCH (.*)$/', $line, $m)) { |
| | | // handle UNKNOWN-CTE response - RFC 3516, try again with standard BODY request |
| | | if ($binary && !$found && preg_match('/^' . $key . ' NO \[UNKNOWN-CTE\]/i', $line)) { |
| | | $binary = $initiated = false; |
| | | continue; |
| | | } |
| | | |
| | | // skip irrelevant untagged responses (we have a result already) |
| | | if ($found || !preg_match('/^\* ([0-9]+) FETCH (.*)$/', $line, $m)) { |
| | | continue; |
| | | } |
| | | |
| | | $line = $m[2]; |
| | | $last = substr($line, -1); |
| | | |
| | | // handle one line response |
| | | if ($line[0] == '(' && $last == ')') { |
| | | if ($line[0] == '(' && substr($line, -1) == ')') { |
| | | // tokenize content inside brackets |
| | | $tokens = $this->tokenizeResponse(preg_replace('/(^\(|\$)/', '', $line)); |
| | | $result = count($tokens) == 1 ? $tokens[0] : false; |
| | | // the content can be e.g.: (UID 9844 BODY[2.4] NIL) |
| | | $tokens = $this->tokenizeResponse(preg_replace('/(^\(|\)$)/', '', $line)); |
| | | |
| | | for ($i=0; $i<count($tokens); $i+=2) { |
| | | if (preg_match('/^(BODY|BINARY)/i', $tokens[$i])) { |
| | | $result = $tokens[$i+1]; |
| | | $found = true; |
| | | break; |
| | | } |
| | | } |
| | | |
| | | if ($result !== false) { |
| | | if ($mode == 1) { |
| | |
| | | else if (preg_match('/\{([0-9]+)\}$/', $line, $m)) { |
| | | $bytes = (int) $m[1]; |
| | | $prev = ''; |
| | | $found = true; |
| | | |
| | | while ($bytes > 0) { |
| | | // empty body |
| | | if (!$bytes) { |
| | | $result = ''; |
| | | } |
| | | else while ($bytes > 0) { |
| | | $line = $this->readLine(8192); |
| | | |
| | | if ($line === NULL) { |
| | |
| | | |
| | | // BASE64 |
| | | if ($mode == 1) { |
| | | $line = rtrim($line, "\t\r\n\0\x0B"); |
| | | $line = preg_replace('|[^a-zA-Z0-9+=/]|', '', $line); |
| | | // create chunks with proper length for base64 decoding |
| | | $line = $prev.$line; |
| | | $length = strlen($line); |
| | |
| | | } |
| | | } |
| | | } |
| | | } while (!$this->startsWith($line, $key, true)); |
| | | } while (!$this->startsWith($line, $key, true) || !$initiated); |
| | | |
| | | if ($result !== false) { |
| | | if ($file) { |
| | |
| | | /** |
| | | * Handler for IMAP APPEND command |
| | | * |
| | | * @param string $mailbox Mailbox name |
| | | * @param string $message Message content |
| | | * @param array $flags Message flags |
| | | * @param string $date Message internal date |
| | | * @param bool $binary Enable BINARY append (RFC3516) |
| | | * @param string $mailbox Mailbox name |
| | | * @param string|array $message The message source string or array (of strings and file pointers) |
| | | * @param array $flags Message flags |
| | | * @param string $date Message internal date |
| | | * @param bool $binary Enable BINARY append (RFC3516) |
| | | * |
| | | * @return string|bool On success APPENDUID response (if available) or True, False on failure |
| | | */ |
| | |
| | | |
| | | $binary = $binary && $this->getCapability('BINARY'); |
| | | $literal_plus = !$binary && $this->prefs['literal+']; |
| | | $len = 0; |
| | | $msg = is_array($message) ? $message : array(&$message); |
| | | $chunk_size = 512000; |
| | | |
| | | if (!$binary) { |
| | | $message = str_replace("\r", '', $message); |
| | | $message = str_replace("\n", "\r\n", $message); |
| | | for ($i=0, $cnt=count($msg); $i<$cnt; $i++) { |
| | | if (is_resource($msg[$i])) { |
| | | $stat = fstat($msg[$i]); |
| | | if ($stat === false) { |
| | | return false; |
| | | } |
| | | $len += $stat['size']; |
| | | } |
| | | else { |
| | | if (!$binary) { |
| | | $msg[$i] = str_replace("\r", '', $msg[$i]); |
| | | $msg[$i] = str_replace("\n", "\r\n", $msg[$i]); |
| | | } |
| | | |
| | | $len += strlen($msg[$i]); |
| | | } |
| | | } |
| | | |
| | | $len = strlen($message); |
| | | if (!$len) { |
| | | return false; |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | if (!$this->putLine($message)) { |
| | | foreach ($msg as $msg_part) { |
| | | // file pointer |
| | | if (is_resource($msg_part)) { |
| | | rewind($msg_part); |
| | | while (!feof($msg_part) && $this->fp) { |
| | | $buffer = fread($msg_part, $chunk_size); |
| | | $this->putLine($buffer, false); |
| | | } |
| | | fclose($msg_part); |
| | | } |
| | | // string |
| | | else { |
| | | $size = strlen($msg_part); |
| | | |
| | | // Break up the data by sending one chunk (up to 512k) at a time. |
| | | // This approach reduces our peak memory usage |
| | | for ($offset = 0; $offset < $size; $offset += $chunk_size) { |
| | | $chunk = substr($msg_part, $offset, $chunk_size); |
| | | if (!$this->putLine($chunk, false)) { |
| | | return false; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (!$this->putLine('')) { // \r\n |
| | | return false; |
| | | } |
| | | |
| | |
| | | */ |
| | | function appendFromFile($mailbox, $path, $headers=null, $flags = array(), $date = null, $binary = false) |
| | | { |
| | | unset($this->data['APPENDUID']); |
| | | |
| | | if ($mailbox === null || $mailbox === '') { |
| | | return false; |
| | | } |
| | | |
| | | // open message file |
| | | $in_fp = false; |
| | | if (file_exists(realpath($path))) { |
| | | $in_fp = fopen($path, 'r'); |
| | | $fp = fopen($path, 'r'); |
| | | } |
| | | |
| | | if (!$in_fp) { |
| | | if (!$fp) { |
| | | $this->setError(self::ERROR_UNKNOWN, "Couldn't open $path for reading"); |
| | | return false; |
| | | } |
| | | |
| | | $body_separator = "\r\n\r\n"; |
| | | $len = filesize($path); |
| | | |
| | | if (!$len) { |
| | | return false; |
| | | } |
| | | |
| | | $message = array(); |
| | | if ($headers) { |
| | | $headers = preg_replace('/[\r\n]+$/', '', $headers); |
| | | $len += strlen($headers) + strlen($body_separator); |
| | | $message[] = trim($headers, "\r\n") . "\r\n\r\n"; |
| | | } |
| | | $message[] = $fp; |
| | | |
| | | $binary = $binary && $this->getCapability('BINARY'); |
| | | $literal_plus = !$binary && $this->prefs['literal+']; |
| | | |
| | | // build APPEND command |
| | | $key = $this->nextTag(); |
| | | $request = "$key APPEND " . $this->escape($mailbox) . ' (' . $this->flagsToStr($flags) . ')'; |
| | | if (!empty($date)) { |
| | | $request .= ' ' . $this->escape($date); |
| | | } |
| | | $request .= ' ' . ($binary ? '~' : '') . '{' . $len . ($literal_plus ? '+' : '') . '}'; |
| | | |
| | | // send APPEND command |
| | | if ($this->putLine($request)) { |
| | | // Don't wait when LITERAL+ is supported |
| | | if (!$literal_plus) { |
| | | $line = $this->readReply(); |
| | | |
| | | if ($line[0] != '+') { |
| | | $this->parseResult($line, 'APPEND: '); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | // send headers with body separator |
| | | if ($headers) { |
| | | $this->putLine($headers . $body_separator, false); |
| | | } |
| | | |
| | | // send file |
| | | while (!feof($in_fp) && $this->fp) { |
| | | $buffer = fgets($in_fp, 4096); |
| | | $this->putLine($buffer, false); |
| | | } |
| | | fclose($in_fp); |
| | | |
| | | if (!$this->putLine('')) { // \r\n |
| | | return false; |
| | | } |
| | | |
| | | // read response |
| | | do { |
| | | $line = $this->readLine(); |
| | | } while (!$this->startsWith($line, $key, true, true)); |
| | | |
| | | // Clear internal status cache |
| | | unset($this->data['STATUS:'.$mailbox]); |
| | | |
| | | if ($this->parseResult($line, 'APPEND: ') != self::ERROR_OK) |
| | | return false; |
| | | else if (!empty($this->data['APPENDUID'])) |
| | | return $this->data['APPENDUID']; |
| | | else |
| | | return true; |
| | | } |
| | | else { |
| | | $this->setError(self::ERROR_COMMAND, "Unable to send command: $request"); |
| | | } |
| | | |
| | | return false; |
| | | return $this->append($mailbox, $message, $flags, $date, $binary); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | foreach ($entries as $name => $value) { |
| | | $entries[$name] = $this->escape($name) . ' ' . $this->escape($value); |
| | | $entries[$name] = $this->escape($name) . ' ' . $this->escape($value, true); |
| | | } |
| | | |
| | | $entries = implode(' ', $entries); |
| | |
| | | } |
| | | |
| | | foreach ($data as $entry) { |
| | | // Workaround cyrus-murder bug, the entry[2] string needs to be escaped |
| | | if (self::$mupdate) { |
| | | $entry[2] = addcslashes($entry[2], '\\"'); |
| | | } |
| | | |
| | | // ANNOTATEMORE drafts before version 08 require quoted parameters |
| | | $entries[] = sprintf('%s (%s %s)', $this->escape($entry[0], true), |
| | | $this->escape($entry[1], true), $this->escape($entry[2], true)); |
| | |
| | | } |
| | | |
| | | // Send command |
| | | if (!$this->putLineC($query)) { |
| | | if (!$this->putLineC($query, true, ($options & self::COMMAND_ANONYMIZED))) { |
| | | $this->setError(self::ERROR_COMMAND, "Unable to send command: $query"); |
| | | return $noresp ? self::ERROR_COMMAND : array(self::ERROR_COMMAND, ''); |
| | | } |
| | |
| | | |
| | | // Parenthesized list |
| | | case '(': |
| | | case '[': |
| | | $str = substr($str, 1); |
| | | $result[] = self::tokenizeResponse($str); |
| | | break; |
| | | case ')': |
| | | case ']': |
| | | $str = substr($str, 1); |
| | | return $result; |
| | | break; |
| | | |
| | | // String atom, number, NIL, *, % |
| | | // String atom, number, astring, NIL, *, % |
| | | default: |
| | | // empty string |
| | | if ($str === '' || $str === null) { |
| | | break 2; |
| | | } |
| | | |
| | | // excluded chars: SP, CTL, ), [, ] |
| | | if (preg_match('/^([^\x00-\x20\x29\x5B\x5D\x7F]+)/', $str, $m)) { |
| | | // excluded chars: SP, CTL, ), DEL |
| | | // we do not exclude [ and ] (#1489223) |
| | | if (preg_match('/^([^\x00-\x20\x29\x7F]+)/', $str, $m)) { |
| | | $result[] = $m[1] == 'NIL' ? NULL : $m[1]; |
| | | $str = substr($str, strlen($m[1])); |
| | | } |
| | |
| | | |
| | | if (is_array($element)) { |
| | | reset($element); |
| | | while (list($key, $value) = each($element)) { |
| | | foreach ($element as $value) { |
| | | $string .= ' ' . self::r_implode($value); |
| | | } |
| | | } |
| | |
| | | // if less than 255 bytes long, let's not bother |
| | | if (!$force && strlen($messages)<255) { |
| | | return $messages; |
| | | } |
| | | } |
| | | |
| | | // see if it's already been compressed |
| | | if (strpos($messages, ':') !== false) { |
| | |
| | | } |
| | | |
| | | /** |
| | | * Clear internal status cache |
| | | */ |
| | | protected function clear_status_cache($mailbox) |
| | | { |
| | | unset($this->data['STATUS:' . $mailbox]); |
| | | unset($this->data['EXISTS']); |
| | | unset($this->data['RECENT']); |
| | | unset($this->data['UNSEEN']); |
| | | } |
| | | |
| | | /** |
| | | * Converts flags array into string for inclusion in IMAP command |
| | | * |
| | | * @param array $flags Flags (see self::flags) |
| | |
| | | */ |
| | | static function strToTime($date) |
| | | { |
| | | // support non-standard "GMTXXXX" literal |
| | | $date = preg_replace('/GMT\s*([+-][0-9]+)/', '\\1', $date); |
| | | // Clean malformed data |
| | | $date = preg_replace( |
| | | array( |
| | | '/GMT\s*([+-][0-9]+)/', // support non-standard "GMTXXXX" literal |
| | | '/[^a-z0-9\x20\x09:+-]/i', // remove any invalid characters |
| | | '/\s*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s*/i', // remove weekday names |
| | | ), |
| | | array( |
| | | '\\1', |
| | | '', |
| | | '', |
| | | ), $date); |
| | | |
| | | $date = trim($date); |
| | | |
| | | // if date parsing fails, we have a date in non-rfc format |
| | | // remove token from the end and try again |
| | |
| | | |
| | | $this->capability = explode(' ', strtoupper($str)); |
| | | |
| | | if (!empty($this->prefs['disabled_caps'])) { |
| | | $this->capability = array_diff($this->capability, $this->prefs['disabled_caps']); |
| | | } |
| | | |
| | | if (!isset($this->prefs['literal+']) && in_array('LITERAL+', $this->capability)) { |
| | | $this->prefs['literal+'] = true; |
| | | } |
| | | |
| | | if (preg_match('/(\[| )MUPDATE=.*/', $str)) { |
| | | self::$mupdate = true; |
| | | } |
| | | |
| | | if ($trusted) { |
| | |
| | | /** |
| | | * Set the value of the debugging flag. |
| | | * |
| | | * @param boolean $debug New value for the debugging flag. |
| | | * @param boolean $debug New value for the debugging flag. |
| | | * @param callback $handler Logging handler function |
| | | * |
| | | * @since 0.5-stable |
| | | * @since 0.5-stable |
| | | */ |
| | | function setDebug($debug, $handler = null) |
| | | { |
| | |
| | | /** |
| | | * Write the given debug text to the current debug output handler. |
| | | * |
| | | * @param string $message Debug mesage text. |
| | | * @param string $message Debug mesage text. |
| | | * |
| | | * @since 0.5-stable |
| | | * @since 0.5-stable |
| | | */ |
| | | private function debug($message) |
| | | { |
| | | if (($len = strlen($message)) > self::DEBUG_LINE_LENGTH) { |
| | | $diff = $len - self::DEBUG_LINE_LENGTH; |
| | | $message = substr($message, 0, self::DEBUG_LINE_LENGTH) |
| | | . "... [truncated $diff bytes]"; |
| | | } |
| | | |
| | | if ($this->resourceid) { |
| | | $message = sprintf('[%s] %s', $this->resourceid, $message); |
| | | } |