| <?php |
| /** |
| * File containing the ezcMailSmtpTransport class. |
| * |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * |
| * @package Mail |
| * @version //autogen// |
| * @copyright Copyright (C) 2005-2010 eZ Systems AS. All rights reserved. |
| * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0 |
| */ |
| |
| /** |
| * This class implements the Simple Mail Transfer Protocol (SMTP) |
| * with authentication support. |
| * |
| * The implementation supports most of the commands specified in: |
| * - {@link http://www.faqs.org/rfcs/rfc821.html RFC821 - SMTP} |
| * - {@link http://www.faqs.org/rfcs/rfc2554.html RFC2554 - SMTP Authentication} |
| * - {@link http://www.faqs.org/rfcs/rfc2831.html RFC2831 - DIGEST-MD5 Authentication} |
| * - {@link http://www.faqs.org/rfcs/rfc2195.html RFC2195 - CRAM-MD5 Authentication} |
| * - {@link http://davenport.sourceforge.net/ntlm.html NTLM Authentication} |
| * |
| * By default, the SMTP transport tries to login anonymously to the SMTP server |
| * (if an empty username and password have been provided), or to authenticate |
| * with the strongest method supported by the server (if username and password |
| * have been provided). The default behaviour can be changed with the option |
| * preferredAuthMethod (see {@link ezcMailSmtpTransportOptions}). |
| * |
| * If the preferred method is specified via options, only that authentication |
| * method will be attempted on the SMTP server. If it fails, an exception will |
| * be thrown. |
| * |
| * Supported authentication methods (from strongest to weakest): |
| * - DIGEST-MD5 |
| * - CRAM-MD5 |
| * - NTLM (requires the PHP mcrypt extension) |
| * - LOGIN |
| * - PLAIN |
| * |
| * Not all SMTP servers support these methods, and some SMTP servers don't |
| * support authentication at all. |
| * |
| * Example send mail: |
| * <code> |
| * $mail = new ezcMailComposer(); |
| * |
| * $mail->from = new ezcMailAddress( 'sender@example.com', 'Adrian Ripburger' ); |
| * $mail->addTo( new ezcMailAddress( 'receiver@example.com', 'Maureen Corley' ) ); |
| * $mail->subject = "This is the subject of the example mail"; |
| * $mail->plainText = "This is the body of the example mail."; |
| * $mail->build(); |
| * |
| * // Create a new SMTP transport object with an SSLv3 connection. |
| * // The port will be 465 by default, use the 4th argument to change it. |
| * // Username and password (2nd and 3rd arguments) are left blank, which means |
| * // the mail host does not need authentication. |
| * // The 5th parameter is the optional $options object. |
| * $options = new ezcMailSmtpTransportOptions(); |
| * $options->connectionType = ezcMailSmtpTransport::CONNECTION_SSLV3; |
| * |
| * $transport = new ezcMailSmtpTransport( 'mailhost.example.com', '', '', null, $options ); |
| * |
| * // Use the SMTP transport to send the created mail object |
| * $transport->send( $mail ); |
| * </code> |
| * |
| * Example require NTLM authentication: |
| * <code> |
| * // Create an SMTP transport and demand NTLM authentication. |
| * // Username and password must be specified, otherwise no authentication |
| * // will be attempted. |
| * // If NTLM authentication fails, an exception will be thrown. |
| * $options = new ezcMailSmtpTransportOptions(); |
| * $options->preferredAuthMethod = ezcMailSmtpTransport::AUTH_NTLM; |
| * |
| * $transport = new ezcMailSmtpTransport( 'mailhost.example.com', 'username', 'password', null, $options ); |
| * |
| * // The option can also be specified via the option property: |
| * $transport->options->preferredAuthMethod = ezcMailSmtpTransport::AUTH_NTLM; |
| * </code> |
| * |
| * See {@link ezcMailSmtpTransportOptions} for options you can specify for SMTP. |
| * |
| * @property string $serverHost |
| * The SMTP server host to connect to. |
| * @property int $serverPort |
| * The port of the SMTP server. Defaults to 25. |
| * @property string $username |
| * The username used for authentication. The default is blank which |
| * means no authentication. |
| * @property string $password |
| * The password used for authentication. |
| * @property int $timeout |
| * The timeout value of the connection in seconds. The default is |
| * 5 seconds. When setting/getting this option, the timeout option |
| * from $this->options {@link ezcMailTransportOptions} will be set instead. |
| * @property string $senderHost |
| * The hostname of the computer that sends the mail. The default is |
| * 'localhost'. |
| * @property ezcMailSmtpTransportOptions $options |
| * Holds the options you can set to the SMTP transport. |
| * |
| * @package Mail |
| * @version //autogen// |
| * @mainclass |
| */ |
| class ezcMailSmtpTransport implements ezcMailTransport |
| { |
| /** |
| * Plain connection. |
| */ |
| const CONNECTION_PLAIN = 'tcp'; |
| |
| /** |
| * SSL connection. |
| */ |
| const CONNECTION_SSL = 'ssl'; |
| |
| /** |
| * SSLv2 connection. |
| */ |
| const CONNECTION_SSLV2 = 'sslv2'; |
| |
| /** |
| * SSLv3 connection. |
| */ |
| const CONNECTION_SSLV3 = 'sslv3'; |
| |
| /** |
| * TLS connection. |
| */ |
| const CONNECTION_TLS = 'tls'; |
| |
| /** |
| * Authenticate with 'AUTH PLAIN'. |
| */ |
| const AUTH_PLAIN = 'PLAIN'; |
| |
| /** |
| * Authenticate with 'AUTH LOGIN'. |
| */ |
| const AUTH_LOGIN = 'LOGIN'; |
| |
| /** |
| * Authenticate with 'AUTH CRAM-MD5'. |
| */ |
| const AUTH_CRAM_MD5 = 'CRAM-MD5'; |
| |
| /** |
| * Authenticate with 'AUTH DIGEST-MD5'. |
| */ |
| const AUTH_DIGEST_MD5 = 'DIGEST-MD5'; |
| |
| /** |
| * Authenticate with 'AUTH NTLM'. |
| */ |
| const AUTH_NTLM = 'NTLM'; |
| |
| /** |
| * No authentication method. Specifies that the transport should try to |
| * authenticate using the methods supported by the SMTP server in their |
| * decreasing strength order. If one method fails an exception will be |
| * thrown. |
| */ |
| const AUTH_AUTO = null; |
| |
| /** |
| * The line-break characters to use. |
| * |
| * @access private |
| */ |
| const CRLF = "\r\n"; |
| |
| /** |
| * We are not connected to a server. |
| * |
| * @access private |
| */ |
| const STATUS_NOT_CONNECTED = 1; |
| |
| /** |
| * We are connected to the server, but not authenticated. |
| * |
| * @access private |
| */ |
| const STATUS_CONNECTED = 2; |
| |
| /** |
| * We are connected to the server and authenticated. |
| * |
| * @access private |
| */ |
| const STATUS_AUTHENTICATED = 3; |
| |
| /** |
| * The connection to the SMTP server. |
| * |
| * @var resource |
| */ |
| protected $connection; |
| |
| /** |
| * Holds the connection status. |
| * |
| * $var int {@link STATUS_NOT_CONNECTED}, |
| * {@link STATUS_CONNECTED} or |
| * {@link STATUS_AUTHENTICATED}. |
| */ |
| protected $status; |
| |
| /** |
| * True if authentication should be performed; otherwise false. |
| * |
| * This variable is set to true if a username is provided for login. |
| * |
| * @var bool |
| */ |
| protected $doAuthenticate; |
| |
| /** |
| * Holds if the connection should be kept open after sending a mail. |
| * |
| * @var bool |
| */ |
| protected $keepConnection = false; |
| |
| /** |
| * Holds the properties of this class. |
| * |
| * @var array(string=>mixed) |
| */ |
| protected $properties = array(); |
| |
| /** |
| * Holds the options of this class. |
| * |
| * @var ezcMailSmtpTransportOptions |
| */ |
| protected $options; |
| |
| /** |
| * Constructs a new ezcMailSmtpTransport. |
| * |
| * The constructor expects, at least, the hostname $host of the SMTP server. |
| * |
| * The username $user will be used for authentication if provided. |
| * If it is left blank no authentication will be performed. |
| * |
| * The password $password will be used for authentication |
| * if provided. Use this parameter always in combination with the $user |
| * parameter. |
| * |
| * The value $port specifies on which port to connect to $host. By default |
| * it is 25 for plain connections and 465 for TLS/SSL/SSLv2/SSLv3. |
| * |
| * Note: The ssl option from {@link ezcMailTransportOptions} doesn't apply to SMTP. |
| * If you want to connect to SMTP using TLS/SSL/SSLv2/SSLv3 use the connectionType |
| * option in {@link ezcMailSmtpTransportOptions}. |
| * |
| * For options you can specify for SMTP see {@link ezcMailSmtpTransportOptions}. |
| * |
| * @throws ezcBasePropertyNotFoundException |
| * if $options contains a property not defined |
| * @throws ezcBaseValueException |
| * if $options contains a property with a value not allowed |
| * @param string $host |
| * @param string $user |
| * @param string $password |
| * @param int $port |
| * @param ezcMailSmtpTransportOptions|array(string=>mixed) $options |
| */ |
| public function __construct( $host, $user = '', $password = '', $port = null, $options = array() ) |
| { |
| if ( $options instanceof ezcMailSmtpTransportOptions ) |
| { |
| $this->options = $options; |
| } |
| else if ( is_array( $options ) ) |
| { |
| $this->options = new ezcMailSmtpTransportOptions( $options ); |
| } |
| else |
| { |
| throw new ezcBaseValueException( "options", $options, "ezcMailSmtpTransportOptions|array" ); |
| } |
| |
| $this->serverHost = $host; |
| if ( $port === null ) |
| { |
| $port = ( $this->options->connectionType === self::CONNECTION_PLAIN ) ? 25 : 465; |
| } |
| $this->serverPort = $port; |
| $this->user = $user; |
| $this->password = $password; |
| $this->doAuthenticate = $user != '' ? true : false; |
| |
| $this->status = self::STATUS_NOT_CONNECTED; |
| $this->senderHost = 'localhost'; |
| } |
| |
| /** |
| * Destructs this object. |
| * |
| * Closes the connection if it is still open. |
| */ |
| public function __destruct() |
| { |
| if ( $this->status != self::STATUS_NOT_CONNECTED ) |
| { |
| $this->sendData( 'QUIT' ); |
| fclose( $this->connection ); |
| } |
| } |
| |
| /** |
| * Sets the property $name to $value. |
| * |
| * @throws ezcBasePropertyNotFoundException |
| * if the property $name does not exist |
| * @throws ezcBaseValueException |
| * if $value is not accepted for the property $name |
| * @param string $name |
| * @param mixed $value |
| * @ignore |
| */ |
| public function __set( $name, $value ) |
| { |
| switch ( $name ) |
| { |
| case 'user': |
| case 'password': |
| case 'senderHost': |
| case 'serverHost': |
| case 'serverPort': |
| $this->properties[$name] = $value; |
| break; |
| |
| case 'timeout': |
| // the timeout option from $this->options is used instead of |
| // the timeout option of this class |
| $this->options->timeout = $value; |
| break; |
| |
| case 'options': |
| if ( !( $value instanceof ezcMailSmtpTransportOptions ) ) |
| { |
| throw new ezcBaseValueException( 'options', $value, 'instanceof ezcMailSmtpTransportOptions' ); |
| } |
| $this->options = $value; |
| break; |
| |
| default: |
| throw new ezcBasePropertyNotFoundException( $name ); |
| } |
| } |
| |
| /** |
| * Returns the value of the property $name. |
| * |
| * @throws ezcBasePropertyNotFoundException |
| * if the property $name does not exist |
| * @param string $name |
| * @return mixed |
| * @ignore |
| */ |
| public function __get( $name ) |
| { |
| switch ( $name ) |
| { |
| case 'user': |
| case 'password': |
| case 'senderHost': |
| case 'serverHost': |
| case 'serverPort': |
| return $this->properties[$name]; |
| |
| case 'timeout': |
| return $this->options->timeout; |
| |
| case 'options': |
| return $this->options; |
| |
| default: |
| throw new ezcBasePropertyNotFoundException( $name ); |
| } |
| } |
| |
| /** |
| * Returns true if the property $name is set, otherwise false. |
| * |
| * @param string $name |
| * @return bool |
| * @ignore |
| */ |
| public function __isset( $name ) |
| { |
| switch ( $name ) |
| { |
| case 'user': |
| case 'password': |
| case 'senderHost': |
| case 'serverHost': |
| case 'serverPort': |
| return isset( $this->properties[$name] ); |
| |
| case 'timeout': |
| case 'options': |
| return true; |
| |
| default: |
| return false; |
| } |
| } |
| |
| /** |
| * Sets if the connection should be kept open after sending an email. |
| * |
| * This method should be called prior to the first call to send(). |
| * |
| * Keeping the connection open is useful if you are sending a lot of mail. |
| * It removes the overhead of opening the connection after each mail is |
| * sent. |
| * |
| * Use disconnect() to close the connection if you have requested to keep |
| * it open. |
| */ |
| public function keepConnection() |
| { |
| $this->keepConnection = true; |
| } |
| |
| /** |
| * Sends the ezcMail $mail using the SMTP protocol. |
| * |
| * If you want to send several emails use keepConnection() to leave the |
| * connection to the server open between each mail. |
| * |
| * @throws ezcMailTransportException |
| * if the mail could not be sent |
| * @throws ezcBaseFeatureNotFoundException |
| * if trying to use SSL and the openssl extension is not installed |
| * @param ezcMail $mail |
| */ |
| public function send( ezcMail $mail ) |
| { |
| // sanity check the e-mail |
| // need at least one recepient |
| if ( ( count( $mail->to ) + count( $mail->cc ) + count( $mail->bcc ) ) < 1 ) |
| { |
| throw new ezcMailTransportException( "Can not send e-mail with no 'to' recipients." ); |
| } |
| |
| try |
| { |
| // open connection unless we are connected already. |
| if ( $this->status != self::STATUS_AUTHENTICATED ) |
| { |
| $this->connect(); |
| } |
| |
| if ( isset( $mail->returnPath ) ) |
| { |
| $this->cmdMail( $mail->returnPath->email ); |
| } |
| else |
| { |
| $this->cmdMail( $mail->from->email ); |
| } |
| |
| // each recepient must be listed here. |
| // this controls where the mail is actually sent as SMTP does not |
| // read the headers itself |
| foreach ( $mail->to as $address ) |
| { |
| $this->cmdRcpt( $address->email ); |
| } |
| foreach ( $mail->cc as $address ) |
| { |
| $this->cmdRcpt( $address->email ); |
| } |
| foreach ( $mail->bcc as $address ) |
| { |
| $this->cmdRcpt( $address->email ); |
| } |
| // done with the from and recipients, lets send the mail itself |
| $this->cmdData(); |
| |
| // A '.' on a line ends the mail. Make sure this does not happen in |
| // the data we want to send. also called transparancy in the RFC, |
| // section 4.5.2 |
| $data = $mail->generate(); |
| $data = str_replace( self::CRLF . '.', self::CRLF . '..', $data ); |
| if ( $data[0] == '.' ) |
| { |
| $data = '.' . $data; |
| } |
| |
| $this->sendData( $data ); |
| $this->sendData( '.' ); |
| |
| if ( $this->getReplyCode( $error ) !== '250' ) |
| { |
| throw new ezcMailTransportSmtpException( "Error: {$error}" ); |
| } |
| } |
| catch ( ezcMailTransportSmtpException $e ) |
| { |
| throw new ezcMailTransportException( $e->getMessage() ); |
| // TODO: reset connection here.pin |
| } |
| |
| // close connection unless we should keep it |
| if ( $this->keepConnection === false ) |
| { |
| try |
| { |
| $this->disconnect(); |
| } |
| catch ( Exception $e ) |
| { |
| // Eat! We don't care anyway since we are aborting the connection |
| } |
| } |
| } |
| |
| /** |
| * Creates a connection to the SMTP server and initiates the login |
| * procedure. |
| * |
| * @todo The @ should be removed when PHP doesn't throw warnings for connect problems |
| * |
| * @throws ezcMailTransportSmtpException |
| * if no connection could be made |
| * or if the login failed |
| * @throws ezcBaseExtensionNotFoundException |
| * if trying to use SSL and the openssl extension is not installed |
| */ |
| protected function connect() |
| { |
| $errno = null; |
| $errstr = null; |
| if ( $this->options->connectionType !== self::CONNECTION_PLAIN && |
| !ezcBaseFeatures::hasExtensionSupport( 'openssl' ) ) |
| { |
| throw new ezcBaseExtensionNotFoundException( 'openssl', null, "PHP not configured --with-openssl." ); |
| } |
| if ( count( $this->options->connectionOptions ) > 0 ) |
| { |
| $context = stream_context_create( $this->options->connectionOptions ); |
| $this->connection = @stream_socket_client( "{$this->options->connectionType}://{$this->serverHost}:{$this->serverPort}", |
| $errno, $errstr, $this->options->timeout, STREAM_CLIENT_CONNECT, $context ); |
| } |
| else |
| { |
| $this->connection = @stream_socket_client( "{$this->options->connectionType}://{$this->serverHost}:{$this->serverPort}", |
| $errno, $errstr, $this->options->timeout ); |
| } |
| |
| if ( is_resource( $this->connection ) ) |
| { |
| stream_set_timeout( $this->connection, $this->options->timeout ); |
| $this->status = self::STATUS_CONNECTED; |
| $greeting = $this->getData(); |
| $this->login(); |
| } |
| else |
| { |
| throw new ezcMailTransportSmtpException( "Failed to connect to the smtp server: {$this->serverHost}:{$this->serverPort}." ); |
| } |
| } |
| |
| /** |
| * Performs the initial handshake with the SMTP server and |
| * authenticates the user, if login data is provided to the |
| * constructor. |
| * |
| * @throws ezcMailTransportSmtpException |
| * if the HELO/EHLO command or authentication fails |
| */ |
| protected function login() |
| { |
| if ( $this->doAuthenticate ) |
| { |
| $this->sendData( 'EHLO ' . $this->senderHost ); |
| } |
| else |
| { |
| $this->sendData( 'HELO ' . $this->senderHost ); |
| } |
| |
| if ( $this->getReplyCode( $response ) !== '250' ) |
| { |
| throw new ezcMailTransportSmtpException( "HELO/EHLO failed with error: {$response}." ); |
| } |
| |
| // do authentication |
| if ( $this->doAuthenticate ) |
| { |
| if ( $this->options->preferredAuthMethod !== self::AUTH_AUTO ) |
| { |
| $this->auth( $this->options->preferredAuthMethod ); |
| } |
| else |
| { |
| preg_match( "/250-AUTH[= ](.*)/", $response, $matches ); |
| if ( count( $matches ) > 0 ) |
| { |
| $methods = explode( ' ', trim( $matches[1] ) ); |
| } |
| if ( count( $matches ) === 0 || count( $methods ) === 0 ) |
| { |
| throw new ezcMailTransportSmtpException( 'SMTP server does not accept the AUTH command.' ); |
| } |
| |
| $authenticated = false; |
| $methods = $this->sortAuthMethods( $methods ); |
| foreach ( $methods as $method ) |
| { |
| if ( $this->auth( $method ) === true ) |
| { |
| $authenticated = true; |
| break; |
| } |
| } |
| |
| if ( $authenticated === false ) |
| { |
| throw new ezcMailTransportSmtpException( 'SMTP server did not respond correctly to any of the authentication methods ' . implode( ', ', $methods ) . '.' ); |
| } |
| } |
| } |
| $this->status = self::STATUS_AUTHENTICATED; |
| } |
| |
| /** |
| * Returns an array with the authentication methods supported by the |
| * SMTP transport class (not by the SMTP server!). |
| * |
| * The returned array has the methods sorted by their relative strengths, |
| * so stronger methods are first in the array. |
| * |
| * @return array(string) |
| */ |
| public static function getSupportedAuthMethods() |
| { |
| return array( |
| ezcMailSmtpTransport::AUTH_DIGEST_MD5, |
| ezcMailSmtpTransport::AUTH_CRAM_MD5, |
| ezcMailSmtpTransport::AUTH_NTLM, |
| ezcMailSmtpTransport::AUTH_LOGIN, |
| ezcMailSmtpTransport::AUTH_PLAIN, |
| ); |
| } |
| |
| /** |
| * Sorts the specified array of AUTH methods $methods by strength, so higher |
| * strength methods will be used first. |
| * |
| * For example, if the server supports: |
| * <code> |
| * $methods = array( 'PLAIN', 'LOGIN', 'CRAM-MD5' ); |
| * </code> |
| * |
| * then this method will return: |
| * <code> |
| * $methods = array( 'CRAM-MD5', 'LOGIN', 'PLAIN' ); |
| * </code> |
| * |
| * @param array(string) $methods |
| * @return array(string) |
| */ |
| protected function sortAuthMethods( array $methods ) |
| { |
| $result = array(); |
| $unsupported = array(); |
| $supportedAuthMethods = self::getSupportedAuthMethods(); |
| foreach ( $supportedAuthMethods as $method ) |
| { |
| if ( in_array( $method, $methods ) ) |
| { |
| $result[] = $method; |
| } |
| } |
| return $result; |
| } |
| |
| /** |
| * Calls the appropiate authentication method based on $method. |
| * |
| * @throws ezcMailTransportSmtpException |
| * if $method is not supported by the transport class |
| * @return bool |
| */ |
| protected function auth( $method ) |
| { |
| switch ( $method ) |
| { |
| case self::AUTH_DIGEST_MD5: |
| $authenticated = $this->authDigestMd5(); |
| break; |
| |
| case self::AUTH_CRAM_MD5: |
| $authenticated = $this->authCramMd5(); |
| break; |
| |
| case self::AUTH_NTLM: |
| $authenticated = $this->authNtlm(); |
| break; |
| |
| case self::AUTH_LOGIN: |
| $authenticated = $this->authLogin(); |
| break; |
| |
| case self::AUTH_PLAIN: |
| $authenticated = $this->authPlain(); |
| break; |
| |
| default: |
| throw new ezcMailTransportSmtpException( "Unsupported AUTH method '{$method}'." ); |
| } |
| |
| return $authenticated; |
| } |
| |
| /** |
| * Tries to login to the SMTP server with 'AUTH DIGEST-MD5' and returns true if |
| * successful. |
| * |
| * @todo implement auth-int and auth-conf quality of protection (qop) modes |
| * @todo support other algorithms than md5-sess? |
| * |
| * @throws ezcMailTransportSmtpException |
| * if the SMTP server returned an error |
| * @return bool |
| */ |
| protected function authDigestMd5() |
| { |
| $this->sendData( 'AUTH DIGEST-MD5' ); |
| if ( $this->getReplyCode( $serverResponse ) !== '334' ) |
| { |
| throw new ezcMailTransportSmtpException( 'SMTP server does not accept AUTH DIGEST-MD5.' ); |
| } |
| |
| $serverDigest = base64_decode( trim( substr( $serverResponse, 4 ) ) ); |
| $parts = explode( ',', $serverDigest ); |
| foreach ( $parts as $part ) |
| { |
| $args = explode( '=', $part, 2 ); |
| $params[trim( $args[0] )] = trim( $args[1] ); |
| } |
| |
| if ( !isset( $params['nonce'] ) || |
| !isset( $params['algorithm'] ) ) |
| { |
| throw new ezcMailTransportSmtpException( 'SMTP server did not send a correct DIGEST-MD5 challenge.' ); |
| } |
| |
| $nonce = trim( $params['nonce'], '"' ); |
| $algorithm = trim( $params['algorithm'], '"' ); |
| |
| $qop = 'auth'; |
| $realm = isset( $params['realm'] ) ? trim( $params['realm'], '"' ) : $this->serverHost; |
| $cnonce = $this->generateNonce( 32 ); |
| $digestUri = "smtp/{$this->serverHost}"; |
| $nc = '00000001'; |
| $charset = isset( $params['charset'] ) ? trim( $params['charset'], '"' ) : 'utf-8'; |
| $maxbuf = isset( $params['maxbuf'] ) ? trim( $params['maxbuf'], '"' ) : 65536; |
| |
| $response = ''; |
| $A2 = "AUTHENTICATE:{$digestUri}"; |
| $A1 = pack( 'H32', md5( "{$this->user}:{$realm}:{$this->password}" ) ) . ":{$nonce}:{$cnonce}"; |
| $response = md5( md5( $A1 ) . ":{$nonce}:{$nc}:{$cnonce}:{$qop}:" . md5( $A2 ) ); |
| |
| $loginParams = array( |
| 'username' => "\"{$this->user}\"", |
| 'cnonce' => "\"{$cnonce}\"", |
| 'nonce' => "\"{$nonce}\"", |
| 'nc' => $nc, |
| 'qop' => $qop, |
| 'digest-uri' => "\"{$digestUri}\"", |
| 'charset' => $charset, |
| 'realm' => "\"{$realm}\"", |
| 'response' => $response, |
| 'maxbuf' => $maxbuf |
| ); |
| |
| $parts = array(); |
| foreach ( $loginParams as $key => $value ) |
| { |
| $parts[] = "{$key}={$value}"; |
| } |
| $login = base64_encode( implode( ',', $parts ) ); |
| |
| $this->sendData( $login ); |
| if ( $this->getReplyCode( $serverResponse ) !== '334' ) |
| { |
| throw new ezcMailTransportSmtpException( 'SMTP server did not accept the provided username and password.' ); |
| } |
| |
| $serverResponse = base64_decode( trim( substr( $serverResponse, 4 ) ) ); |
| $parts = explode( '=', $serverResponse ); |
| $rspauthServer = trim( $parts[1] ); |
| |
| $A2 = ":{$digestUri}"; |
| $rspauthClient = md5( md5( $A1 ) . ":{$nonce}:{$nc}:{$cnonce}:{$qop}:" . md5( $A2 ) ); |
| |
| if ( $rspauthServer !== $rspauthClient ) |
| { |
| throw new ezcMailTransportSmtpException( 'SMTP server did not responded correctly to the DIGEST-MD5 authentication.' ); |
| } |
| |
| $this->sendData( '' ); |
| if ( $this->getReplyCode( $serverResponse ) !== '235' ) |
| { |
| throw new ezcMailTransportSmtpException( 'SMTP server did not allow DIGEST-MD5 authentication.' ); |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Tries to login to the SMTP server with 'AUTH CRAM-MD5' and returns true if |
| * successful. |
| * |
| * @throws ezcMailTransportSmtpException |
| * if the SMTP server returned an error |
| * @return bool |
| */ |
| protected function authCramMd5() |
| { |
| $this->sendData( 'AUTH CRAM-MD5' ); |
| if ( $this->getReplyCode( $response ) !== '334' ) |
| { |
| throw new ezcMailTransportSmtpException( 'SMTP server does not accept AUTH CRAM-MD5.' ); |
| } |
| |
| $serverDigest = trim( substr( $response, 4 ) ); |
| $clientDigest = hash_hmac( 'md5', base64_decode( $serverDigest ), $this->password ); |
| $login = base64_encode( "{$this->user} {$clientDigest}" ); |
| |
| $this->sendData( $login ); |
| if ( $this->getReplyCode( $error ) !== '235' ) |
| { |
| throw new ezcMailTransportSmtpException( 'SMTP server did not accept the provided username and password.' ); |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Tries to login to the SMTP server with 'AUTH NTLM' and returns true if |
| * successful. |
| * |
| * @throws ezcMailTransportSmtpException |
| * if the SMTP server returned an error |
| * @return bool |
| */ |
| protected function authNtlm() |
| { |
| if ( !ezcBaseFeatures::hasExtensionSupport( 'mcrypt' ) ) |
| { |
| throw new ezcBaseExtensionNotFoundException( 'mcrypt', null, "PHP not compiled with --with-mcrypt." ); |
| } |
| |
| // Send NTLM type 1 message |
| $msg1 = base64_encode( $this->authNtlmMessageType1( $this->senderHost, $this->serverHost ) ); |
| |
| $this->sendData( "AUTH NTLM {$msg1}" ); |
| if ( $this->getReplyCode( $serverResponse ) !== '334' ) |
| { |
| throw new ezcMailTransportSmtpException( 'SMTP server does not accept AUTH NTLM.' ); |
| } |
| |
| // Parse NTLM type 2 message |
| $msg2 = base64_decode( trim( substr( $serverResponse, 4 ) ) ); |
| $parts = array( |
| substr( $msg2, 0, 8 ), // Signature ("NTLMSSP\0") |
| substr( $msg2, 8, 4 ), // Message type |
| substr( $msg2, 12, 8 ), // Target name (security buffer) |
| substr( $msg2, 20, 4 ), // Flags |
| substr( $msg2, 24, 8 ), // Challenge |
| substr( $msg2, 32 ) // The rest of information |
| ); |
| |
| $challenge = $parts[4]; |
| |
| // Send NTLM type 3 message |
| $msg3 = base64_encode( $this->authNtlmMessageType3( $challenge, $this->user, $this->password, $this->senderHost, $this->serverHost ) ); |
| |
| $this->sendData( $msg3 ); |
| if ( $this->getReplyCode( $serverResponse ) !== '235' ) |
| { |
| throw new ezcMailTransportSmtpException( 'SMTP server did not allow NTLM authentication.' ); |
| } |
| } |
| |
| /** |
| * Tries to login to the SMTP server with 'AUTH LOGIN' and returns true if |
| * successful. |
| * |
| * @throws ezcMailTransportSmtpException |
| * if the SMTP server returned an error |
| * @return bool |
| */ |
| protected function authLogin() |
| { |
| $this->sendData( 'AUTH LOGIN' ); |
| if ( $this->getReplyCode( $error ) !== '334' ) |
| { |
| throw new ezcMailTransportSmtpException( 'SMTP server does not accept AUTH LOGIN.' ); |
| } |
| |
| $this->sendData( base64_encode( $this->user ) ); |
| if ( $this->getReplyCode( $error ) !== '334' ) |
| { |
| throw new ezcMailTransportSmtpException( "SMTP server did not accept login: {$this->user}." ); |
| } |
| |
| $this->sendData( base64_encode( $this->password ) ); |
| if ( $this->getReplyCode( $error ) !== '235' ) |
| { |
| throw new ezcMailTransportSmtpException( 'SMTP server did not accept the provided username and password.' ); |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Tries to login to the SMTP server with 'AUTH PLAIN' and returns true if |
| * successful. |
| * |
| * @throws ezcMailTransportSmtpException |
| * if the SMTP server returned an error |
| * @return bool |
| */ |
| protected function authPlain() |
| { |
| $digest = base64_encode( "\0{$this->user}\0{$this->password}" ); |
| $this->sendData( "AUTH PLAIN {$digest}" ); |
| if ( $this->getReplyCode( $error ) !== '235' ) |
| { |
| throw new ezcMailTransportSmtpException( 'SMTP server did not accept the provided username and password.' ); |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Sends the QUIT command to the server and breaks the connection. |
| * |
| * @throws ezcMailTransportSmtpException |
| * if the QUIT command failed |
| */ |
| public function disconnect() |
| { |
| if ( $this->status != self::STATUS_NOT_CONNECTED ) |
| { |
| $this->sendData( 'QUIT' ); |
| $replyCode = $this->getReplyCode( $error ) !== '221'; |
| fclose( $this->connection ); |
| $this->status = self::STATUS_NOT_CONNECTED; |
| if ( $replyCode ) |
| { |
| throw new ezcMailTransportSmtpException( "QUIT failed with error: $error." ); |
| } |
| } |
| } |
| |
| /** |
| * Returns the $email enclosed within '< >'. |
| * |
| * If $email is already enclosed within '< >' it is returned unmodified. |
| * |
| * @param string $email |
| * $return string |
| */ |
| protected function composeSmtpMailAddress( $email ) |
| { |
| if ( !preg_match( "/<.+>/", $email ) ) |
| { |
| $email = "<{$email}>"; |
| } |
| return $email; |
| } |
| |
| /** |
| * Sends the MAIL FROM command, with the sender's mail address $from. |
| * |
| * This method must be called once to tell the server the sender address. |
| * |
| * The sender's mail address $from may be enclosed in angle brackets. |
| * |
| * @throws ezcMailTransportSmtpException |
| * if there is no valid connection |
| * or if the MAIL FROM command failed |
| * @param string $from |
| */ |
| protected function cmdMail( $from ) |
| { |
| if ( $this->status === self::STATUS_AUTHENTICATED ) |
| { |
| $this->sendData( 'MAIL FROM:' . $this->composeSmtpMailAddress( $from ) . '' ); |
| if ( $this->getReplyCode( $error ) !== '250' ) |
| { |
| throw new ezcMailTransportSmtpException( "MAIL FROM failed with error: $error." ); |
| } |
| } |
| } |
| |
| /** |
| * Sends the 'RCTP TO' to the server with the address $email. |
| * |
| * This method must be called once for each recipient of the mail |
| * including cc and bcc recipients. The RCPT TO commands control |
| * where the mail is actually sent. It does not affect the headers |
| * of the email. |
| * |
| * The recipient mail address $email may be enclosed in angle brackets. |
| * |
| * @throws ezcMailTransportSmtpException |
| * if there is no valid connection |
| * or if the RCPT TO command failed |
| * @param string $email |
| */ |
| protected function cmdRcpt( $email ) |
| { |
| if ( $this->status === self::STATUS_AUTHENTICATED ) |
| { |
| $this->sendData( 'RCPT TO:' . $this->composeSmtpMailAddress( $email ) ); |
| if ( $this->getReplyCode( $error ) !== '250' ) |
| { |
| throw new ezcMailTransportSmtpException( "RCPT TO failed with error: $error." ); |
| } |
| } |
| } |
| |
| /** |
| * Sends the DATA command to the SMTP server. |
| * |
| * @throws ezcMailTransportSmtpException |
| * if there is no valid connection |
| * or if the DATA command failed |
| */ |
| protected function cmdData() |
| { |
| if ( $this->status === self::STATUS_AUTHENTICATED ) |
| { |
| $this->sendData( 'DATA' ); |
| if ( $this->getReplyCode( $error ) !== '354' ) |
| { |
| throw new ezcMailTransportSmtpException( "DATA failed with error: $error." ); |
| } |
| } |
| } |
| |
| /** |
| * Sends $data to the SMTP server through the connection. |
| * |
| * This method appends one line-break at the end of $data. |
| * |
| * @throws ezcMailTransportSmtpException |
| * if there is no valid connection |
| * @param string $data |
| */ |
| protected function sendData( $data ) |
| { |
| if ( is_resource( $this->connection ) ) |
| { |
| if ( fwrite( $this->connection, $data . self::CRLF, |
| strlen( $data ) + strlen( self::CRLF ) ) === false ) |
| { |
| throw new ezcMailTransportSmtpException( 'Could not write to SMTP stream. It was probably terminated by the host.' ); |
| } |
| } |
| } |
| |
| /** |
| * Returns data received from the connection stream. |
| * |
| * @throws ezcMailTransportSmtpException |
| * if there is no valid connection |
| * @return string |
| */ |
| protected function getData() |
| { |
| $data = ''; |
| $line = ''; |
| $loops = 0; |
| |
| if ( is_resource( $this->connection ) ) |
| { |
| while ( ( strpos( $data, self::CRLF ) === false || (string) substr( $line, 3, 1 ) !== ' ' ) && $loops < 100 ) |
| { |
| $line = fgets( $this->connection, 512 ); |
| $data .= $line; |
| $loops++; |
| } |
| return $data; |
| } |
| throw new ezcMailTransportSmtpException( 'Could not read from SMTP stream. It was probably terminated by the host.' ); |
| } |
| |
| /** |
| * Returns the reply code of the last message from the server. |
| * |
| * $line contains the complete data retrieved from the stream. This can be used to retrieve |
| * the error message in case of an error. |
| * |
| * @throws ezcMailTransportSmtpException |
| * if it could not fetch data from the stream |
| * @param string &$line |
| * @return string |
| */ |
| protected function getReplyCode( &$line ) |
| { |
| return substr( trim( $line = $this->getData() ), 0, 3 ); |
| } |
| |
| /** |
| * Generates an alpha-numeric random string with the specified $length. |
| * |
| * Used in the DIGEST-MD5 authentication method. |
| * |
| * @param int $length |
| * @return string |
| */ |
| protected function generateNonce( $length = 32 ) |
| { |
| $chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; |
| $result = ''; |
| for ( $i = 0; $i < $length; $i++ ) |
| { |
| $result .= $chars[mt_rand( 0, strlen( $chars ) - 1 )]; |
| } |
| |
| return $result; |
| } |
| |
| /** |
| * Generates an NTLM type 1 message. |
| * |
| * @param string $workstation |
| * @param string $domain |
| * @return string |
| */ |
| protected function authNtlmMessageType1( $workstation, $domain ) |
| { |
| $parts = array( |
| "NTLMSSP\x00", |
| "\x01\x00\x00\x00", |
| "\x07\x32\x00\x00", |
| $this->authNtlmSecurityBuffer( $domain, 32 + strlen( $workstation ) ), |
| $this->authNtlmSecurityBuffer( $workstation, 32 ), |
| $workstation, |
| $domain |
| ); |
| |
| return implode( "", $parts ); |
| } |
| |
| /** |
| * Generates an NTLM type 3 message from the $challenge sent by the SMTP |
| * server in an NTLM type 2 message. |
| * |
| * @param string $challenge |
| * @param string $user |
| * @param string $password |
| * @param string $workstation |
| * @param string $domain |
| * @return string |
| */ |
| protected function authNtlmMessageType3( $challenge, $user, $password, $workstation, $domain ) |
| { |
| $domain = chunk_split( $domain, 1, "\x00" ); |
| $user = chunk_split( $user, 1, "\x00" ); |
| $workstation = chunk_split( $workstation, 1, "\x00" ); |
| $lm = ''; |
| $ntlm = $this->authNtlmResponse( $challenge, $password ); |
| $session = ''; |
| |
| $domainOffset = 64; |
| $userOffset = $domainOffset + strlen( $domain ); |
| $workstationOffset = $userOffset + strlen( $user ); |
| $lmOffset = $workstationOffset + strlen( $workstation ); |
| $ntlmOffset = $lmOffset + strlen( $lm ); |
| $sessionOffset = $ntlmOffset + strlen( $ntlm ); |
| |
| $parts = array( |
| "NTLMSSP\x00", |
| "\x03\x00\x00\x00", |
| $this->authNtlmSecurityBuffer( $lm, $lmOffset ), |
| $this->authNtlmSecurityBuffer( $ntlm, $ntlmOffset ), |
| $this->authNtlmSecurityBuffer( $domain, $domainOffset ), |
| $this->authNtlmSecurityBuffer( $user, $userOffset ), |
| $this->authNtlmSecurityBuffer( $workstation, $workstationOffset ), |
| $this->authNtlmSecurityBuffer( $session, $sessionOffset ), |
| "\x01\x02\x00\x00", |
| $domain, |
| $user, |
| $workstation, |
| $lm, |
| $ntlm |
| ); |
| |
| return implode( '', $parts ); |
| } |
| |
| /** |
| * Calculates an NTLM response to be used in the creation of the NTLM type 3 |
| * message. |
| * |
| * @param string $challenge |
| * @param string $password |
| * @return string |
| */ |
| protected function authNtlmResponse( $challenge, $password ) |
| { |
| $password = chunk_split( $password, 1, "\x00" ); |
| $password = hash( 'md4', $password, true ); |
| $password .= str_repeat( "\x00", 21 - strlen( $password ) ); |
| |
| $td = mcrypt_module_open( 'des', '', 'ecb', '' ); |
| $iv = mcrypt_create_iv( mcrypt_enc_get_iv_size( $td ), MCRYPT_RAND ); |
| |
| $response = ''; |
| for ( $i = 0; $i < 21; $i += 7 ) |
| { |
| $packed = ''; |
| for ( $p = $i; $p < $i + 7; $p++ ) |
| { |
| $packed .= str_pad( decbin( ord( substr( $password, $p, 1 ) ) ), 8, '0', STR_PAD_LEFT ); |
| } |
| |
| $key = ''; |
| for ( $p = 0; $p < strlen( $packed ); $p += 7 ) |
| { |
| $s = substr( $packed, $p, 7 ); |
| $b = $s . ( ( substr_count( $s, '1' ) % 2 ) ? '0' : '1' ); |
| $key .= chr( bindec( $b ) ); |
| } |
| |
| mcrypt_generic_init( $td, $key, $iv ); |
| $response .= mcrypt_generic( $td, $challenge ); |
| mcrypt_generic_deinit( $td ); |
| } |
| mcrypt_module_close( $td ); |
| |
| return $response; |
| } |
| |
| /** |
| * Creates an NTLM security buffer information string. |
| * |
| * The structure of the security buffer is: |
| * - a short containing the length of the buffer content in bytes (may be |
| * zero). |
| * - a short containing the allocated space for the buffer in bytes (greater |
| * than or equal to the length; typically the same as the length). |
| * - a long containing the offset to the start of the buffer in bytes (from |
| * the beginning of the NTLM message). |
| * |
| * Example: |
| * - buffer content length: 1234 bytes (0xd204 in hexa) |
| * - allocated space: 1234 bytes( 0xd204 in hexa) |
| * - offset: 4321 bytes (0xe1100000 in hexa) |
| * |
| * then the security buffer would be 0xd204d204e1100000 (in hexa). |
| * |
| * @param string $text |
| * @param int $offset |
| * @return string |
| */ |
| protected function authNtlmSecurityBuffer( $text, $offset ) |
| { |
| return pack( 'v', strlen( $text ) ) . |
| pack( 'v', strlen( $text ) ) . |
| pack( 'V', $offset ); |
| } |
| } |
| ?> |