** pass - Password for authentication Default: ** timeout - The timeout in seconds for the call Default: 5 ** to fsockopen() ***************************************/ function kSmtpClient($params = array()){ $this->timeout = 5; $this->status = SMTP_STATUS_NOT_CONNECTED; $this->host = 'localhost'; $this->port = 25; $this->helo = 'localhost'; $this->auth = FALSE; $this->user = ''; $this->pass = ''; $this->errors = array(); $this->buffer = array(); $this->debug=0; foreach($params as $key => $value){ $this->$key = $value; } } /*************************************** ** Connect function. This will, when called ** statically, create a new smtp object, ** call the connect function (ie this function) ** and return it. When not called statically, ** it will connect to the server and send ** the HELO command. ***************************************/ function connect($params = array()){ if(!isset($this->status)) { $obj = new kSmtpClient($params); if($obj->connect()){ $obj->status = SMTP_STATUS_CONNECTED; } return $obj; } else { foreach($params as $key => $value){ $this->$key = $value; } $this->connection = @fsockopen($this->host, $this->port, $errno, $errstr, $this->timeout); if(is_resource($this->connection)) { socket_set_timeout($this->connection, 0, 250000); socket_set_blocking($this->connection,TRUE); $greeting = $this->get_data(); $this->status = SMTP_STATUS_CONNECTED; return $this->auth ? $this->ehlo() : $this->helo(); } else { $this->errors[] = 'Failed to connect to server: '.$errstr; return FALSE; } } } function disconnect() { if(is_resource($this->connection)) fclose($this->connection); unset($this->connection); $this->status=SMTP_STATUS_NOT_CONNECTED; } /*************************************** ** Function which handles sending the mail. ** Arguments: ** $params - Optional assoc array of parameters. ** Can contain: ** recipients - Indexed array of recipients ** from - The from address. (used in MAIL FROM:), ** this will be the return path ** headers - Indexed array of headers, one header per array entry ** body - The body of the email ** It can also contain any of the parameters from the connect() ** function ***************************************/ function send($params = array()){ foreach($params as $key => $value){ $this->set($key, $value); } if($this->is_connected()){ // Do we auth or not? Note the distinction between the auth variable and auth() function if($this->auth){ if(!$this->auth()) return FALSE; } $this->mail($this->from); if(is_array($this->recipients)) foreach($this->recipients as $value) $this->rcpt($value); else $this->rcpt($this->recipients); if(!$this->data()) return FALSE; // Transparency $headers = str_replace($this->CRLF.'.', $this->CRLF.'..', trim(implode($this->CRLF, $this->headers))); $body = str_replace($this->CRLF.'.', $this->CRLF.'..', $this->body); $body = $body[0] == '.' ? '.'.$body : $body; $this->send_data($headers); $this->send_data(''); $this->send_data($body); $this->send_data($this->CRLF."."); return (substr(trim($this->get_data()), 0, 3) === '250'); }else{ $this->errors[] = 'Not connected!'; return FALSE; } } /*************************************** ** Function to implement HELO cmd ***************************************/ function helo(){ if(is_resource($this->connection) AND $this->send_data('HELO '.$this->helo) AND substr(trim($error = $this->get_data()), 0, 3) === '250' ){ return TRUE; }else{ $this->errors[] = 'HELO command failed, output: ' . trim(substr(trim($error),3)); return FALSE; } } /*************************************** ** Function to implement EHLO cmd ***************************************/ 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() { if ($this->authmethod) return $this->authmethod; if (!isset($this->_esmtp['AUTH'])) return 'NOAUTH'; $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)) { $method=$this->_getBestAuthMethod(); if ($method == 'NOAUTH') return true; 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; }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) { $this->_code = -1; $this->_arguments = array(); if(!is_resource($this->connection)) return false; while ($line = fgets($this->connection, 512)) { if ($this->debug) { $this->debugtext.= "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 ***************************************/ function mail($from){ if($this->is_connected() AND $this->send_data('MAIL FROM:'.$from.'') AND substr(trim($this->get_data()), 0, 2) === '250' ){ return TRUE; }else return FALSE; } /*************************************** ** Function that handles the RCPT TO: cmd ***************************************/ function rcpt($to){ if($this->is_connected() AND $this->send_data('RCPT TO:'.$to.'') AND substr(trim($error = $this->get_data()), 0, 2) === '25' ){ return TRUE; }else{ $this->errors[] = trim(substr(trim($error), 3)); return FALSE; } } /*************************************** ** Function that sends the DATA cmd ***************************************/ function data(){ if($this->is_connected() AND $this->send_data('DATA') AND substr(trim($error = $this->get_data()), 0, 3) === '354' ){ return TRUE; }else{ $this->errors[] = trim(substr(trim($error), 3)); return FALSE; } } /*************************************** ** Function to determine if this object ** is connected to the server or not. ***************************************/ function is_connected(){ return (is_resource($this->connection) AND ($this->status === SMTP_STATUS_CONNECTED)); } /*************************************** ** Function to send a bit of data ***************************************/ function send_data($data){ if($this->debug) { $this->buffer[] = "SEND: $data\n"; } if ($this->debug) { $this->debugtext.= "DEBUG: Send: $data\n"; } if(is_resource($this->connection)){ return fwrite($this->connection, $data.$this->CRLF, strlen($data)+2); }else return FALSE; } function bytes_left($fp) { $status = socket_get_status ($fp); //print_r($status); $bytes = $status["unread_bytes"]; return $bytes; } /*************************************** ** Function to get data. ***************************************/ function &get_data(){ $return = ''; $line = ''; if(is_resource($this->connection)) { while(strpos($return, $this->CRLF) === FALSE OR substr($line,3,1) !== ' ') { $line = fgets($this->connection, 512); $return .= $line; } if($this->debug) { $this->buffer[] = "GET: ".$return."\n"; } return $return; }else return FALSE; } /*************************************** ** Sets a variable ***************************************/ function set($var, $value){ $this->$var = $value; return TRUE; } } ?>