Index: trunk/kernel/include/smtp.php =================================================================== diff -u -r437 -r1128 --- trunk/kernel/include/smtp.php (.../smtp.php) (revision 437) +++ trunk/kernel/include/smtp.php (.../smtp.php) (revision 1128) @@ -28,7 +28,33 @@ var $pass; var $debug; var $buffer; - + + /** + * List of supported authentication methods, in preferential order. + * @var array + * @access public + */ + var $auth_methods = array('DIGEST-MD5','CRAM-MD5','LOGIN','PLAIN'); + /** + * The most recent server response code. + * @var int + * @access private + */ + var $_code = -1; + + /** + * The most recent server response arguments. + * @var array + * @access private + */ + var $_arguments = array(); + /** + * Stores detected features of the SMTP server. + * @var array + * @access private + */ + var $_esmtp = array(); + /*************************************** ** Constructor function. Arguments: ** $params - An assoc array of parameters: @@ -150,7 +176,7 @@ $headers = str_replace($CRLF.'.', $CRLF.'..', trim(implode($CRLF, $this->headers))); $body = str_replace($CRLF.'.', $CRLF.'..', $this->body); $body = $body[0] == '.' ? '.'.$body : $body; - + $this->send_data($headers); $this->send_data(''); $this->send_data($body); @@ -184,40 +210,485 @@ ** Function to implement EHLO cmd ***************************************/ - function ehlo(){ - if(is_resource($this->connection) - AND $this->send_data('EHLO '.$this->helo) - AND substr(trim($error = $this->get_data()), 0, 3) === '250' ){ - - return TRUE; - - }else{ + function ehlo() + { + $ret_status=is_resource($this->connection) AND $this->send_data('EHLO '.$this->helo); + $success=$this->_parseResponse(250); + if(!$ret_status && $success !== true) + { $this->errors[] = 'EHLO command failed, output: ' . trim(substr(trim($error),3)); return FALSE; } + + foreach ($this->_arguments as $argument) { + $verb = strtok($argument, ' '); + $arguments = substr($argument, strlen($verb) + 1, + strlen($argument) - strlen($verb) - 1); + $this->_esmtp[$verb] = $arguments; + } + + return TRUE; + + } /*************************************** ** Function to implement AUTH cmd ***************************************/ + + function _getBestAuthMethod() + { + $available_methods = explode(' ', $this->_esmtp['AUTH']); + foreach ($this->auth_methods as $method) + { + if (in_array($method, $available_methods)) return $method; + } + return false; + } + + function auth(){ - if(is_resource($this->connection) - AND $this->send_data('AUTH LOGIN') - AND substr(trim($error = $this->get_data()), 0, 3) === '334' - AND $this->send_data(base64_encode($this->user)) // Send username - AND substr(trim($error = $this->get_data()),0,3) === '334' - AND $this->send_data(base64_encode($this->pass)) // Send password - AND substr(trim($error = $this->get_data()),0,3) === '235' ){ + if(is_resource($this->connection)) + { + $method=$this->_getBestAuthMethod(); + + switch ($method) { + case 'DIGEST-MD5': + $result = $this->_authDigest_MD5($this->user, $this->pass); + break; + case 'CRAM-MD5': + $result = $this->_authCRAM_MD5($this->user, $this->pass); + break; + case 'LOGIN': + $result = $this->_authLogin($this->user, $this->pass); + break; + case 'PLAIN': + $result = $this->_authPlain($this->user, $this->pass); + break; + default: + $this->errors[] = 'AUTH command failed: no supported authentication methods'; + return false; + break; + } + if($result!==true) + { + $this->errors[] = 'AUTH command failed: '.$result; + return FALSE; + } + return true; - return TRUE; - }else{ $this->errors[] = 'AUTH command failed: ' . trim(substr(trim($error),3)); return FALSE; } } +// ============= AUTH METHODS: BEGIN ========================== +/** + * Authenticates the user using the DIGEST-MD5 method. + * + * @param string The userid to authenticate as. + * @param string The password to authenticate with. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access private + * @since 1.1.0 + */ + function _authDigest_MD5($uid, $pwd) + { + $this->send_data('AUTH DIGEST-MD5'); + + /* 334: Continue authentication request */ + if(($error=$this->_parseResponse(334)) !== true) + { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + $challenge = base64_decode($this->_arguments[0]); + $auth_str = base64_encode($this->get_digestMD5Auth($uid, $pwd, $challenge, + $this->host, "smtp")); + + + $this->send_data($auth_str); + + /* 334: Continue authentication request */ + if(($error=$this->_parseResponse(334)) !== true) return $error; + + /* + * We don't use the protocol's third step because SMTP doesn't allow + * subsequent authentication, so we just silently ignore it. + */ + $this->send_data(' '); + + /* 235: Authentication successful */ + if(($error=$this->_parseResponse(235)) !== true) return $error; + } + + + /** + * Provides the (main) client response for DIGEST-MD5 + * requires a few extra parameters than the other + * mechanisms, which are unavoidable. + * + * @param string $authcid Authentication id (username) + * @param string $pass Password + * @param string $challenge The digest challenge sent by the server + * @param string $hostname The hostname of the machine you're connecting to + * @param string $service The servicename (eg. imap, pop, acap etc) + * @param string $authzid Authorization id (username to proxy as) + * @return string The digest response (NOT base64 encoded) + * @access public + */ + function get_digestMD5Auth($authcid, $pass, $challenge, $hostname, $service, $authzid = '') + { + $challenge = $this->_parseChallenge($challenge); + $authzid_string = ''; + if ($authzid != '') { + $authzid_string = ',authzid="' . $authzid . '"'; + } + + if (!empty($challenge)) { + $cnonce = $this->_getCnonce(); + $digest_uri = sprintf('%s/%s', $service, $hostname); + $response_value = $this->_getResponseValue($authcid, $pass, $challenge['realm'], $challenge['nonce'], $cnonce, $digest_uri, $authzid); + + return sprintf('username="%s",realm="%s"' . $authzid_string . ',nonce="%s",cnonce="%s",nc="00000001",qop=auth,digest-uri="%s",response=%s,%d', $authcid, $challenge['realm'], $challenge['nonce'], $cnonce, $digest_uri, $response_value, $challenge['maxbuf']); + } else { + return PEAR::raiseError('Invalid digest challenge'); + } + } + + /** + * Parses and verifies the digest challenge* + * + * @param string $challenge The digest challenge + * @return array The parsed challenge as an assoc + * array in the form "directive => value". + * @access private + */ + function _parseChallenge($challenge) + { + $tokens = array(); + while (preg_match('/^([a-z-]+)=("[^"]+(?send_data('AUTH CRAM-MD5'); + + /* 334: Continue authentication request */ + if(($error=$this->_parseResponse(334)) !== true) + { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + $challenge = base64_decode($this->_arguments[0]); + $auth_str = base64_encode($uid . ' ' . $this->_HMAC_MD5($pwd, $challenge)); + + $this->send_data($auth_str); + + /* 235: Authentication successful */ + if ( ($error = $this->_parseResponse(235)) ) { + return $error; + } + } + + /** + * Authenticates the user using the LOGIN method. + * + * @param string The userid to authenticate as. + * @param string The password to authenticate with. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access private + * @since 1.1.0 + */ + function _authLogin($uid, $pwd) + { + $this->send_data('AUTH LOGIN'); + + /* 334: Continue authentication request */ + if(($error=$this->_parseResponse(334)) !== true) + { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + $this->send_data( base64_encode($uid) ); + + /* 334: Continue authentication request */ + if(($error=$this->_parseResponse(334)) !== true) return $error; + + $this->send_data( base64_encode($pwd) ); + + /* 235: Authentication successful */ + if (($error=$this->_parseResponse(235)) !== true) return $error; + + return true; + } + + /** + * Authenticates the user using the PLAIN method. + * + * @param string The userid to authenticate as. + * @param string The password to authenticate with. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access private + * @since 1.1.0 + */ + function _authPlain($uid, $pwd) + { + $this->send_data('AUTH PLAIN'); + + /* 334: Continue authentication request */ + if(($error=$this->_parseResponse(334)) !== true) + { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + $auth_str = base64_encode(chr(0) . $uid . chr(0) . $pwd); + $this->send_data($auth_str); + + /* 235: Authentication successful */ + if (($error=$this->_parseResponse(235)) !== true) return $error; + + return true; + } +// ============= AUTH METHODS: END ========================== + + /** + * Function which implements HMAC MD5 digest + * + * @param string $key The secret key + * @param string $data The data to protect + * @return string The HMAC MD5 digest + */ + function _HMAC_MD5($key, $data) + { + if (strlen($key) > 64) { + $key = pack('H32', md5($key)); + } + + if (strlen($key) < 64) { + $key = str_pad($key, 64, chr(0)); + } + + $k_ipad = substr($key, 0, 64) ^ str_repeat(chr(0x36), 64); + $k_opad = substr($key, 0, 64) ^ str_repeat(chr(0x5C), 64); + + $inner = pack('H32', md5($k_ipad . $data)); + $digest = md5($k_opad . $inner); + + return $digest; + } + + + + /** + * Read a reply from the SMTP server. The reply consists of a response + * code and a response message. + * + * @param mixed $valid The set of valid response codes. These + * may be specified as an array of integer + * values or as a single integer value. + * + * @return mixed True if the server returned a valid response code or + * a PEAR_Error object is an error condition is reached. + * + * @access private + * @since 1.1.0 + * + * @see getResponse + */ + function _parseResponse($valid) + { + global $CRLF; + $this->_code = -1; + $this->_arguments = array(); + + if(!is_resource($this->connection)) return false; + + while ($line = fgets($this->connection, 512)) { + if ($this->debug) { + echo "DEBUG: Recv: $line\n"; + } + + /* If we receive an empty line, the connection has been closed. */ + if (empty($line)) { + $this->disconnect(); + return 'Connection was unexpectedly closed'; + } + + /* Read the code and store the rest in the arguments array. */ + $code = substr($line, 0, 3); + $this->_arguments[] = trim(substr($line, 4)); + + /* Check the syntax of the response code. */ + if (is_numeric($code)) { + $this->_code = (int)$code; + } else { + $this->_code = -1; + break; + } + + /* If this is not a multiline response, we're done. */ + if (substr($line, 3, 1) != '-') { + break; + } + } + + /* Compare the server's response code with the valid code. */ + if (is_int($valid) && ($this->_code === $valid)) { + return true; + } + + /* If we were given an array of valid response codes, check each one. */ + if (is_array($valid)) { + foreach ($valid as $valid_code) { + if ($this->_code === $valid_code) { + return true; + } + } + } + + return 'Invalid response code received from server'; + } + + /*************************************** ** Function that handles the MAIL FROM: cmd ***************************************/ @@ -340,4 +811,8 @@ } } // End of class + + + + ?> Index: trunk/kernel/include/emailmessage.php =================================================================== diff -u -r999 -r1128 --- trunk/kernel/include/emailmessage.php (.../emailmessage.php) (revision 999) +++ trunk/kernel/include/emailmessage.php (.../emailmessage.php) (revision 1128) @@ -724,16 +724,19 @@ /* $sql = "INSERT INTO ".GetTablePrefix()."EmailLog VALUES ('', '".htmlspecialchars($From)."', '".htmlspecialchars($To)."', '$Subject', $time, '')"; $conn->Execute($sql);*/ /* ensure headers are using \r\n instead of \n */ - //$headers = str_replace("\r\n","\n",$headers); - //$headers = str_replace("\n","\r\n",$headers); $headers = "Date: ".date("r")."\n".$headers; $headers = "Return-Path: ".$objConfig->Get("Smtp_AdminMailFrom")."\n".$headers; + + $headers = str_replace("\n\n","\r\n\r\n",$headers); + $headers = str_replace("\r\n","\n",$headers); + $headers = str_replace("\n","\r\n",$headers); - if (strtoupper(substr(PHP_OS, 0, 3) == 'WIN')) { + // if (strtoupper(substr(PHP_OS, 0, 3) == 'WIN')) { + $Msg = str_replace("\n\n","\r\n\r\n",$Msg); $Msg = str_replace("\r\n","\n",$Msg); $Msg = str_replace("\n","\r\n",$Msg); - } + // } //echo "
"; print_r(htmlentities($headers)); echo "
"; //echo "
"; print_r(htmlentities($Msg)); echo "
"; @@ -748,7 +751,7 @@ $headers = "Subject: ".trim($Subject)."\r\n".$headers; $send_params['recipients'] = array($To); // The recipients (can be multiple) - $send_params['from'] = $From; // This is used as in the MAIL FROM: cmd + $send_params['from'] = $From; // This is used as in the MAIL FROM: cmd $send_params['headers'] = explode("\r\n",$headers); // It should end up as the Return-Path: header $send_params['body'] = $Msg; // The body of the email @@ -776,7 +779,9 @@ } } else - $SmtpServer->debug = 1; + { + //$SmtpServer->debug = 1; + } $connected = $SmtpServer->connect(); if($connected) @@ -788,6 +793,7 @@ $res = $SmtpServer->send($send_params); } $SmtpServer->disconnect(); + if($this->LogLevel>1) { foreach($SmtpServer->buffer as $l) @@ -869,8 +875,8 @@ $OB="----=_OuterBoundary_000"; $boundary = "-----=".md5( uniqid (rand())); $f = "\"$FromName\" <".$From.">"; - $headers = "From: $f\n"; - $headers .= "MIME-Version: 1.0\n"; + $headers = "From: $f\r\n"; + $headers .= "MIME-Version: 1.0\r\n"; $conn = &GetADODBConnection(); $time = time(); @@ -890,27 +896,33 @@ if($HasFile) { //Messages start with text/html alternatives in OB - $headers.="Content-Type: multipart/mixed;\n\tboundary=\"".$OB."\"\n\n"; + $headers.="Content-Type: multipart/mixed;\r\n\tboundary=\"".$OB."\"\r\n\r\n"; $msg.="--".$OB."\n"; - $msg.="Content-Type: multipart/alternative; boundary=\"$boundary\"\n\n"; + $msg.="Content-Type: multipart/alternative; boundary=\"$boundary\"\r\n\r\n"; } else $headers .= "Content-Type: multipart/alternative; boundary=\"$boundary\""; if(is_array($extra_headers)) { for($i=0;$i0) { $msg .= "--" . $boundary . "\n"; @@ -919,7 +931,7 @@ $msg .= stripslashes($Html); $msg .= "\r\n\r\n"; } - $msg .= "--" . $boundary . "--\n"; + $msg .= "--" . $boundary . "--\n\r"; if($HasFile) { if(!strlen($FileLoc)) @@ -939,7 +951,7 @@ } $FileContent=chunk_split(base64_encode($FileContent)); $msg.=$FileContent; - $msg .= "--".$OB."\n"; + $msg .= $OB."--\r\n"; } if(strlen($ToName)>0)