egroupware/emailadmin/inc/class.imap_client.inc.php
2008-11-24 09:40:41 +00:00

790 lines
27 KiB
PHP

<?php
include_once('PEAR.php');
/**
* The IMP_IMAPClient:: class enables connection to an IMAP server through
* built-in PHP functions.
*
* TODO: This should eventually be moved to Horde 4.0/framework.
*
* $Horde: imp/lib/IMAP/Client.php,v 1.21.2.21 2006/03/30 10:15:31 selsky Exp $
*
* Copyright 2005-2006 Michael Slusarz <slusarz@horde.org>
*
* Based on code from:
* + auth.php (1.49)
* + imap_general.php (1.212)
* + strings.php (1.184.2.35)
* from the Squirrelmail project.
* Copyright (c) 1999-2005 The SquirrelMail Project Team
*
* See the enclosed file COPYING for license information (GPL). If you
* did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
*
* @author Michael Slusarz <slusarz@horde.org>
* @since IMP 4.1
* @package IMP
*/
class imap_client {
/**
* The list of capabilities of the IMAP server.
*
* @var array
*/
var $_capability = null;
/**
* The hostname of the IMAP server to connect to.
*
* @var string
*/
var $_host;
/**
* The last message returned from the server.
*
* @var string
*/
var $_message;
/**
* The namespace information.
*
* @var array
*/
var $_namespace = null;
/**
* The port number of the IMAP server to connect to.
*
* @var string
*/
var $_port;
/**
* The last response returned from the server.
*
* @var string
*/
var $_response;
/**
* The unique session identifier ID to use when making an IMAP query.
*
* @var integer
*/
var $_sessionid = 1;
/**
* The socket connection to the IMAP server.
*
* @var resource
*/
var $_stream;
/**
* Are we using SSL to connect to the IMAP server?
*
* @var string
*/
var $_usessl = false;
/**
* Are we using TLS to connect to the IMAP server?
*
* @var string
*/
var $_usetls = false;
/**
* Constructor.
*
* @param string $host The address/hostname of the IMAP server.
* @param string $port The port to connect to on the IMAP server.
* @param string $protocol The protocol string (See, e.g., servers.php).
*/
function imap_client($host, $port, $protocol)
{
$this->_host = $host;
$this->_port = $port;
/* Split apart protocol string to discover if we need to use either
* SSL or TLS. */
$tmp = explode('/', strtolower($protocol));
if (in_array('tls', $tmp)) {
$this->_usetls = true;
} elseif (in_array('ssl', $tmp)) {
$this->_usessl = true;
}
}
/**
* Are we using TLS to connect and is it supported?
*
* @return mixed Returns true if TLS is being used to connect, false if
* is not, and PEAR_Error if we are attempting to use TLS
* and this version of PHP doesn't support it.
*/
function useTLS()
{
if ($this->_usetls) {
/* There is no way in PHP 4 to open a TLS connection to a
* non-secured port. See http://bugs.php.net/bug.php?id=26040 */
if (!function_exists('stream_socket_enable_crypto')) {
return PEAR::raiseError(lang("To use a TLS connection, you must be running a version of PHP 5.1.0 or higher."), 'horde.error');
}
}
return $this->_usetls;
}
/**
* Generates a new IMAP session ID by incrementing the last one used.
*
* @access private
*
* @return string IMAP session id of the form 'A000'.
*/
function _generateSid()
{
return sprintf("A%03d", $this->_sessionid++);
}
/**
* Perform a command on the IMAP server.
* This command sets the $_response and $_message variable.
*
* @access private
*
* @param string $query IMAP command.
*
* @return mixed Returns PEAR_Error on error. On success, returns an
* array of the IMAP return text.
*/
function _runCommand($query)
{
$message = $response = array();
$sid = $this->_generateSid();
fwrite($this->_stream, $sid . ' ' . $query . "\r\n");
$tag_uid_a = explode(' ', trim($sid));
$tag = $tag_uid_a[0];
$res = $this->_retrieveIMAPResponse($tag, $response, $message);
if (is_a($res, 'PEAR_Error')) {
$this->_message = $this->_response = '';
return $res;
}
/* retrieve the response and the message */
$this->_response = $response[$tag];
$this->_message = $message[$tag];
return (!empty($res[$tag])) ? $res[$tag][0] : $res[$tag];
}
/**
* Custom fgets function - get a line from the IMAP server no matter how
* large the line may be.
*
* @access private
*
* @return string The next line in the IMAP stream.
*/
function _fgets()
{
$buffer = 4096;
$offset = 0;
$results = '';
while (strpos($results, "\r\n", $offset) === false) {
if (!($read = fgets($this->_stream, $buffer))) {
$results = '';
break;
}
if ($results != '') {
$offset = strlen($results) - 1;
}
$results .= $read;
}
return $results;
}
/**
* Reads the output from the IMAP stream.
*
* @access private
*
* @param string $tag The IMAP SID tag.
* @param array $response The response information.
* @param array $message The message information.
*
* @return mixed PEAR_Error on error, response string on success.
*/
function _retrieveIMAPResponse($tag, &$response, &$message)
{
$aResponse = $read = '';
$data = $resultlist = array();
$i = 0;
$read = $this->_fgets();
while ($read) {
$char = $read{0};
switch ($char) {
case '+':
default:
$read = $this->_fgets();
break;
case $tag{0}:
/* Get the command. */
$arg = '';
$i = strlen($tag) + 1;
$s = substr($read, $i);
if (($j = strpos($s, ' ')) || ($j = strpos($s, "\n"))) {
$arg = substr($s, 0, $j);
}
$found_tag = substr($read, 0, $i - 1);
if ($found_tag) {
$response[$found_tag] = $arg;
$message[$found_tag] = trim(substr($read, $i + strlen($arg)));
if (!empty($data)) {
$resultlist[] = $data;
}
$aResponse[$found_tag] = $resultlist;
$data = $resultlist = array();
if ($found_tag == $tag) {
break 2;
}
break;
}
$read = $this->_fgets();
if ($read === false) {
break 2; /* switch while */
}
break;
case '*':
if (preg_match('/^\*\s\d+\sFETCH/', $read)) {
/* check for literal */
$s = substr($read, -3);
$fetch_data = array();
do {
/* Outer loop: continue until next untagged fetch
or tagged reponse. */
do {
/* Innerloop for fetching literals. with this
loop we prohibit that literal responses appear
in the outer loop so we can trust the untagged
and tagged info provided by $read. */
if ($s === "}\r\n") {
$j = strrpos($read, '{');
$iLit = substr($read, $j + 1, -3);
$fetch_data[] = $read;
$sLiteral = fread($this->_stream, $iLit);
if ($sLiteral === false) { /* error */
break 4; /* while while switch while */
}
/* backwards compattibility */
$aLiteral = explode("\n", $sLiteral);
unset($sLiteral);
foreach ($aLiteral as $line) {
$fetch_data[] = $line ."\n";
}
unset($aLiteral);
/* Next fgets belongs to this fetch because
we just got the exact literalsize and data
must follow to complete the response. */
$read = $this->_fgets();
if ($read === false) { /* error */
break 4; /* while while switch while */
}
}
$fetch_data[] = $read;
/* Retrieve next line and check in the while
statements if it belongs to this fetch
response. */
$read = $this->_fgets();
if ($read === false) { /* error */
break 4; /* while while switch while */
}
/* Check for next untagged reponse and break. */
if ($read{0} == '*') {
break 2;
}
$s = substr($read, -3);
} while ($s === "}\r\n");
$s = substr($read,-3);
} while (($read{0} !== '*') &&
(substr($read, 0, strlen($tag)) !== $tag));
$resultlist[] = $fetch_data;
unset($fetch_data);
} else {
$s = substr($read, -3);
do {
if ($s === "}\r\n") {
$j = strrpos($read, '{');
$iLit = substr($read, $j + 1, -3);
$data[] = $read;
$sLiteral = fread($this->_stream, $iLit);
if ($sLiteral === false) { /* error */
$read = false;
break 3; /* while switch while */
}
$data[] = $sLiteral;
$data[] = $this->_fgets();
} else {
$data[] = $read;
}
$read = $this->_fgets();
if ($read === false) {
break 3; /* while switch while */
} elseif ($read{0} == '*') {
break;
}
$s = substr($read,-3);
} while ($s === "}\r\n");
break;
}
break;
}
}
/* Error processing in case $read is false. */
if ($read === false) {
/* Try to retrieve an untagged bye respons from the results. */
$sResponse = array_pop($data);
if (($sResponse !== NULL) &&
(strpos($sResponse,'* BYE') !== false)) {
return PEAR::raiseError(lang("IMAP server closed the connection."), 'horde.error');
} else {
return PEAR::raiseError(lang("Connection dropped by IMAP server."), 'horde.error');
}
}
switch ($response[$tag]) {
case 'OK':
return $aResponse;
break;
case 'NO':
/* Ignore this error from M$ exchange, it is not fatal (aka bug). */
if (strpos($message[$tag], 'command resulted in') === false) {
return PEAR::raiseError(sprintf(lang("Could not complete request. Reason Given: %s"), $message[$tag]), 'horde.error', null, null, $response[$tag]);
}
break;
case 'BAD':
return PEAR::raiseError(sprintf(lang("Bad or malformed request. Server Responded: %s"), $message[$tag]), 'horde.error', null, null, $response[$tag]);
break;
case 'BYE':
return PEAR::raiseError(sprintf(lang("IMAP Server closed the connection. Server Responded: %s"), $message[$tag]), 'horde.error', null, null, $response[$tag]);
break;
default:
return PEAR::raiseError(sprintf(lang("Unknown IMAP response from the server. Server Responded: %s"), $message[$tag]), 'horde.error', null, null, $response[$tag]);
break;
}
}
/**
* Connects to the IMAP server.
*
* @access private
*
* @return mixed Returns true on success, PEAR_Error on error.
*/
function _createStream()
{
if (($this->_usessl || $this->_usetls) &&
!function_exists('openssl_pkcs7_sign')) {
return PEAR::raiseError(lang("If using SSL or TLS, you must have the PHP openssl extension loaded."), 'horde.error');
}
if ($res = $this->useTLS()) {
if (is_a($res, 'PEAR_Error')) {
return $res;
} else {
$this->_host = $this->_host . ':' . $this->_port;
}
}
if ($this->_usessl) {
$this->_host = 'ssl://' . $this->_host;
}
$error_number = $error_string = '';
$timeout = 10;
if ($this->_usetls) {
$this->_stream = stream_socket_client($this->_host, $error_number, $error_string, $timeout);
if (!$this->_stream) {
return PEAR::raiseError(sprintf(lang("Error connecting to IMAP server. %s : %s."), $error_number, $error_string), 'horde.error');
}
/* Disregard any server information returned. */
fgets($this->_stream, 1024);
/* Send the STARTTLS command. */
fwrite($this->_stream, $this->_generateSid() . " STARTTLS\r\n");
/* Disregard any server information returned. */
fgets($this->_stream, 1024);
/* Switch over to a TLS connection. */
$res = stream_socket_enable_crypto($this->_stream, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
if (!$res) {
return PEAR::raiseError(lang("Could not open secure connection to the IMAP server. %s : %s."), 'horde.error');
}
} else {
$this->_stream = fsockopen($this->_host, $this->_port, $error_number, $error_string, $timeout);
}
/* Do some error correction */
if (!$this->_stream) {
return PEAR::raiseError(sprintf(lang("Error connecting to IMAP server. %s : %s."), $error_number, $error_string), 'horde.error');
}
/* Disregard any server information. */
fgets($this->_stream, 1024);
}
/**
* Log the user into the IMAP server.
*
* @param string $username Username.
* @param string $password Encrypted password.
*
* @return mixed True on success, PEAR_Error on error.
*/
function login($username, $password)
{
$res = $this->_createStream();
if (is_a($res, 'PEAR_Error')) {
#LK Horde::logMessage($res, __FILE__, __LINE__, PEAR_LOG_ERR);
return $res;
}
$imap_auth_mech = array();
/* Use md5 authentication, if available. But no need to use special
* authentication if we are already using an encrypted connection. */
$auth_methods = $this->queryCapability('AUTH');
if ((!$this->_usessl || !$this->_usetls) && !empty($auth_methods)) {
if (in_array('CRAM-MD5', $auth_methods)) {
$imap_auth_mech[] = 'cram-md5';
}
if (in_array('DIGEST-MD5', $auth_methods)) {
$imap_auth_mech[] = 'digest-md5';
}
}
/* Next, try 'PLAIN' authentication. */
if (!empty($auth_methods) && in_array('PLAIN', $auth_methods)) {
$imap_auth_mech[] = 'plain';
}
/* Fall back to 'LOGIN' if available. */
if (!$this->queryCapability('LOGINDISABLED')) {
$imap_auth_mech[] = 'login';
}
if (empty($imap_auth_mech)) {
return PEAR::raiseError(lang("No supported IMAP authentication method could be found."), 'horde.error');
}
foreach ($imap_auth_mech as $method) {
$res = $this->_login($username, $password, $method);
if (!is_a($res, 'PEAR_Error')) {
return true;
}
}
return $res;
}
/**
* Log the user into the IMAP server.
*
* @access private
*
* @param string $username Username.
* @param string $password Encrypted password.
* @param string $method IMAP login method.
*
* @return mixed True on success, PEAR_Error on error.
*/
function _login($username, $password, $method)
{
switch ($method) {
case 'cram-md5':
case 'digest-md5':
/* If we don't have Auth_SASL package install, return error. */
if (!@include_once 'Auth/SASL.php') {
return PEAR::raiseError(lang("CRAM-MD5 or DIGEST-MD5 requires the Auth_SASL package to be installed."), 'horde.error');
}
$tag = $this->_generateSid();
fwrite($this->_stream, $tag . ' AUTHENTICATE ' . strtoupper($method) . "\r\n");
$challenge = explode(' ', $this->_fgets(), 3);
if ($method == 'cram-md5') {
$auth_sasl = Auth_SASL::factory('crammd5');
$response = $auth_sasl->getResponse($username, $password, base64_decode($challenge[1]));
fwrite($this->_stream, base64_encode($response) . "\r\n");
$read = $this->_fgets();
} elseif ($method == 'digest-md5') {
$auth_sasl = Auth_SASL::factory('digestmd5');
$response = $auth_sasl->getResponse($username, $password, base64_decode($challenge[1]), $this->_host, 'imap');
fwrite($this->_stream, base64_encode($response) . "\r\n");
$response = explode(' ', $this->_fgets());
$response = base64_decode($response[1]);
if (strpos($response, 'rspauth=') === false) {
return PEAR::raiseError(lang("Unexpected response from server to Digest-MD5 response."), 'horde.error');
}
fwrite($this->_stream, "\r\n");
$read = $this->_fgets();
} else {
return PEAR::raiseError(lang("The IMAP server does not appear to support the authentication method selected. Please contact your system administrator."), 'horde.error');
}
break;
case 'login':
$tag = $this->_generateSid();
$query = $tag . " LOGIN $username {" . strlen($password) . "}\r\n";
fwrite($this->_stream, $query);
$read = $this->_fgets();
if (substr($read, 0, 1) == '+') {
fwrite($this->_stream, "$password\r\n");
$read = $this->_fgets();
} else {
return PEAR::raiseError(lang("Unexpected response from server to LOGIN command."), 'horde.error');
}
break;
case 'plain':
$tag = $this->_generateSid();
$sasl = $this->queryCapability('SASL-IR');
$auth = base64_encode("$username\0$username\0$password");
if ($sasl) {
// IMAP Extension for SASL Initial Client Response
// <draft-siemborski-imap-sasl-initial-response-01b.txt>
$query = $tag . " AUTHENTICATE PLAIN $auth\r\n";
fwrite($this->_stream, $query);
$read = $this->_fgets();
} else {
$query = $tag . " AUTHENTICATE PLAIN\r\n";
fwrite($this->_stream, $query);
$read = $this->_fgets();
if (substr($read, 0, 1) == '+') {
fwrite($this->_stream, "$auth\r\n");
$read = $this->_fgets();
} else {
return PEAR::raiseError(lang("Unexpected response from server to AUTHENTICATE command."), 'horde.error');
}
}
break;
}
/* Check for failed login. */
$results = explode(' ', $read, 3);
$response = $results[1];
if ($response != 'OK') {
$message = !empty($results[2]) ? htmlspecialchars($results[2]) : lang("No message returned.");
switch ($response) {
case 'NO':
return PEAR::raiseError(sprintf(lang("Bad login name or password."), $message), 'horde.error');
case 'BAD':
default:
return PEAR::raiseError(sprintf(lang("Bad request: %s"), $message), 'horde.error');
}
}
return true;
}
/**
* Log out of the IMAP session.
*/
function logout()
{
/* Logout is not valid until the server returns 'BYE'
* If we don't have an imap_ stream we're already logged out */
if (isset($this->_stream) && $this->_stream) {
$this->_runCommand('LOGOUT');
}
}
/**
* Get the CAPABILITY string from the IMAP server.
*
* @access private
*/
function _capability()
{
if (!is_null($this->_capability)) {
return;
}
$this->_capability = array();
$read = $this->_runCommand('CAPABILITY');
if (is_a($read, 'PEAR_Error')) {
#LK Horde::logMessage($read, __FILE__, __LINE__, PEAR_LOG_ERR);
return;
}
$c = explode(' ', trim($read[0]));
for ($i = 2; $i < count($c); $i++) {
$cap_list = explode('=', $c[$i]);
if (isset($cap_list[1])) {
if (!isset($this->_capability[$cap_list[0]])) {
$this->_capability[$cap_list[0]] = array();
}
$this->_capability[$cap_list[0]][] = $cap_list[1];
} else {
$this->_capability[$cap_list[0]] = true;
}
}
}
/**
* Returns whether the IMAP server supports the given capability.
*
* @param string $capability The capability string to query.
*
* @param mixed True if the server supports the queried capability,
* false if it doesn't, or an array if the capability can
* contain multiple values.
*/
function queryCapability($capability)
{
$this->_capability();
return isset($this->_capability[$capability]) ? $this->_capability[$capability] : false;
}
/**
* Get the NAMESPACE information from the IMAP server.
*
* @param array $additional If the server supports namespaces, any
* additional namespaces to add to the
* namespace list that are not broadcast by
* the server.
*
* @return array An array with the following format:
* <pre>
* Array
* (
* [foo] => Array
* (
* [name] => (string)
* [delimiter] => (string)
* [type] => 'personal' | 'others' | 'shared'
* [hidden] => (boolean)
* )
*
* [foo2] => Array
* (
* ...
* )
* )
* </pre>
*/
function namespace($additional = array())
{
if (!is_null($this->_namespace)) {
return $this->_namespace;
}
$namespace_array = array(
1 => 'personal',
2 => 'others',
3 => 'shared'
);
if ($this->queryCapability('NAMESPACE')) {
/*
* According to rfc2342 response from NAMESPACE command is:
* * NAMESPACE (PERSONAL NAMESPACES) (OTHER_USERS NAMESPACE) (SHARED NAMESPACES)
*/
$read = $this->_runCommand('NAMESPACE');
if (is_a($read, 'PEAR_Error')) {
#LK Horde::logMessage($read, __FILE__, __LINE__, PEAR_LOG_ERR);
return $read;
}
if (eregi('\\* NAMESPACE +(\\( *\\(.+\\) *\\)|NIL) +(\\( *\\(.+\\) *\\)|NIL) +(\\( *\\(.+\\) *\\)|NIL)', $read[0], $data)) {
for ($i = 1; $i <= 3; $i++) {
if ($data[$i] == 'NIL') {
continue;
}
$pna = explode(')(', $data[$i]);
while (list($k, $v) = each($pna)) {
$lst = explode('"', $v);
$delimiter = (isset($lst[3])) ? $lst[3] : '';
$this->_namespace[$lst[1]] = array('name' => $lst[1], 'delimiter' => $delimiter, 'type' => $namespace_array[$i], 'hidden' => false);
}
}
}
foreach ($additional as $val) {
/* Skip namespaces if we have already auto-detected them.
* Also, hidden namespaces cannot be empty. */
$val = trim($val);
if (empty($val) || isset($this->_namespace[$val])) {
continue;
}
$read = $this->_runCommand('LIST "" "' . $val . '"');
if (is_a($read, 'PEAR_Error')) {
#LK Horde::logMessage($read, __FILE__, __LINE__, PEAR_LOG_ERR);
return $res;
}
if (!empty($read) &&
preg_match("/^\* LIST \(.*\) \"(.*)\" \"?(.*?)\"?\s*$/", $read[0], $data) &&
($data[2] == $val)) {
$this->_namespace[$val] = array('name' => $val, 'delimiter' => $data[1], 'type' => $namespace_array[3], 'hidden' => true);
}
}
} else {
$res = $this->_runCommand('LIST "" ""');
if (is_a($res, 'PEAR_Error')) {
#LK Horde::logMessage($res, __FILE__, __LINE__, PEAR_LOG_ERR);
return $res;
}
$quote_position = strpos($res[0], '"');
$this->_namespace[''] = array('name' => '', 'delimiter' => substr($res[0], $quote_position + 1 , 1), 'type' => $namespace_array[1], 'hidden' => false);
}
return $this->_namespace;
}
/**
* Determines whether the IMAP search command supports the optional
* charset provided.
*
* @param string $charset The character set to test.
*
* @return boolean True if the IMAP search command supports the charset.
*/
function searchCharset($charset)
{
$this->_runCommand('SELECT INBOX');
$read = $this->_runCommand('SEARCH CHARSET ' . $charset . ' TEXT "charsettest" 1');
return !is_a($read, 'PEAR_Error');
}
}