| | |
| | | */ |
| | | class Net_SMTP |
| | | { |
| | | |
| | | /** |
| | | * The server to connect to. |
| | | * @var string |
| | |
| | | var $auth_methods = array('DIGEST-MD5', 'CRAM-MD5', 'LOGIN', 'PLAIN'); |
| | | |
| | | /** |
| | | * Use SMTP command pipelining (specified in RFC 2920) if the SMTP |
| | | * server supports it. |
| | | * |
| | | * When pipeling is enabled, rcptTo(), mailFrom(), sendFrom(), |
| | | * somlFrom() and samlFrom() do not wait for a response from the |
| | | * SMTP server but return immediately. |
| | | * |
| | | * @var bool |
| | | * @access public |
| | | */ |
| | | var $pipelining = false; |
| | | |
| | | /** |
| | | * Number of pipelined commands. |
| | | * @var int |
| | | * @access private |
| | | */ |
| | | var $_pipelined_commands = 0; |
| | | |
| | | /** |
| | | * Should debugging output be enabled? |
| | | * @var boolean |
| | | * @access private |
| | | */ |
| | | var $_debug = false; |
| | | |
| | | /** |
| | | * Debug output handler. |
| | | * @var callback |
| | | * @access private |
| | | */ |
| | | var $_debug_handler = null; |
| | | |
| | | /** |
| | | * The socket resource being used to connect to the SMTP server. |
| | |
| | | var $_arguments = array(); |
| | | |
| | | /** |
| | | * Stores the SMTP server's greeting string. |
| | | * @var string |
| | | * @access private |
| | | */ |
| | | var $_greeting = null; |
| | | |
| | | /** |
| | | * Stores detected features of the SMTP server. |
| | | * @var array |
| | | * @access private |
| | |
| | | * @param string $host The server to connect to. |
| | | * @param integer $port The port to connect to. |
| | | * @param string $localhost The value to give when sending EHLO or HELO. |
| | | * @param boolean $pipeling Use SMTP command pipelining |
| | | * |
| | | * @access public |
| | | * @since 1.0 |
| | | */ |
| | | function Net_SMTP($host = null, $port = null, $localhost = null) |
| | | function Net_SMTP($host = null, $port = null, $localhost = null, $pipelining = false) |
| | | { |
| | | if (isset($host)) $this->host = $host; |
| | | if (isset($port)) $this->port = $port; |
| | | if (isset($localhost)) $this->localhost = $localhost; |
| | | if (isset($host)) { |
| | | $this->host = $host; |
| | | } |
| | | if (isset($port)) { |
| | | $this->port = $port; |
| | | } |
| | | if (isset($localhost)) { |
| | | $this->localhost = $localhost; |
| | | } |
| | | $this->pipelining = $pipelining; |
| | | |
| | | $this->_socket = &new Net_Socket(); |
| | | $this->_socket = new Net_Socket(); |
| | | |
| | | /* |
| | | * Include the Auth_SASL package. If the package is not available, |
| | | * we disable the authentication methods that depend upon it. |
| | | */ |
| | | /* Include the Auth_SASL package. If the package is not |
| | | * available, we disable the authentication methods that |
| | | * depend upon it. */ |
| | | if ((@include_once 'Auth/SASL.php') === false) { |
| | | $pos = array_search('DIGEST-MD5', $this->auth_methods); |
| | | unset($this->auth_methods[$pos]); |
| | |
| | | * @access public |
| | | * @since 1.1.0 |
| | | */ |
| | | function setDebug($debug) |
| | | function setDebug($debug, $handler = null) |
| | | { |
| | | $this->_debug = $debug; |
| | | $this->_debug_handler = $handler; |
| | | } |
| | | |
| | | /** |
| | | * Write the given debug text to the current debug output handler. |
| | | * |
| | | * @param string $message Debug mesage text. |
| | | * |
| | | * @access private |
| | | * @since 1.3.3 |
| | | */ |
| | | function _debug($message) |
| | | { |
| | | if ($this->_debug) { |
| | | if ($this->_debug_handler) { |
| | | call_user_func_array($this->_debug_handler, |
| | | array(&$this, $message)); |
| | | } else { |
| | | echo "DEBUG: $message\n"; |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | function _send($data) |
| | | { |
| | | if ($this->_debug) { |
| | | echo "DEBUG: Send: $data\n"; |
| | | } |
| | | $this->_debug("Send: $data"); |
| | | |
| | | if (PEAR::isError($error = $this->_socket->write($data))) { |
| | | return PEAR::raiseError('Failed to write to socket: ' . |
| | | $error->getMessage()); |
| | | $error = $this->_socket->write($data); |
| | | if ($error === false || PEAR::isError($error)) { |
| | | $msg = ($error) ? $error->getMessage() : "unknown error"; |
| | | return PEAR::raiseError("Failed to write to socket: $msg"); |
| | | } |
| | | |
| | | return true; |
| | |
| | | * @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. |
| | | * @param bool $later Do not parse the response now, but wait |
| | | * until the last command in the pipelined |
| | | * command group |
| | | * |
| | | * @return mixed True if the server returned a valid response code or |
| | | * a PEAR_Error object is an error condition is reached. |
| | |
| | | * |
| | | * @see getResponse |
| | | */ |
| | | function _parseResponse($valid) |
| | | function _parseResponse($valid, $later = false) |
| | | { |
| | | $this->_code = -1; |
| | | $this->_arguments = array(); |
| | | |
| | | while ($line = $this->_socket->readLine()) { |
| | | 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 PEAR::raiseError('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)) { |
| | | if ($later) { |
| | | $this->_pipelined_commands++; |
| | | 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; |
| | | for ($i = 0; $i <= $this->_pipelined_commands; $i++) { |
| | | while ($line = $this->_socket->readLine()) { |
| | | $this->_debug("Recv: $line"); |
| | | |
| | | /* If we receive an empty line, the connection has been closed. */ |
| | | if (empty($line)) { |
| | | $this->disconnect(); |
| | | return PEAR::raiseError('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; |
| | | } |
| | | } |
| | | } |
| | | |
| | | return PEAR::raiseError('Invalid response code received from server'); |
| | | $this->_pipelined_commands = 0; |
| | | |
| | | /* Compare the server's response code with the valid code/codes. */ |
| | | if (is_int($valid) && ($this->_code === $valid)) { |
| | | return true; |
| | | } elseif (is_array($valid) && in_array($this->_code, $valid, true)) { |
| | | return true; |
| | | } |
| | | |
| | | return PEAR::raiseError('Invalid response code received from server', |
| | | $this->_code); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | /** |
| | | * Return the SMTP server's greeting string. |
| | | * |
| | | * @return string A string containing the greeting string, or null if a |
| | | * greeting has not been received. |
| | | * |
| | | * @access public |
| | | * @since 1.3.3 |
| | | */ |
| | | function getGreeting() |
| | | { |
| | | return $this->_greeting; |
| | | } |
| | | |
| | | /** |
| | | * Attempt to connect to the SMTP server. |
| | | * |
| | | * @param int $timeout The timeout value (in seconds) for the |
| | |
| | | */ |
| | | function connect($timeout = null, $persistent = false) |
| | | { |
| | | $this->_greeting = null; |
| | | $result = $this->_socket->connect($this->host, $this->port, |
| | | $persistent, $timeout); |
| | | if (PEAR::isError($result)) { |
| | |
| | | if (PEAR::isError($error = $this->_parseResponse(220))) { |
| | | return $error; |
| | | } |
| | | |
| | | /* Extract and store a copy of the server's greeting string. */ |
| | | list(, $this->_greeting) = $this->getResponse(); |
| | | |
| | | if (PEAR::isError($error = $this->_negotiate())) { |
| | | return $error; |
| | | } |
| | |
| | | $this->_esmtp[$verb] = $arguments; |
| | | } |
| | | |
| | | if (!isset($this->_esmtp['PIPELINING'])) { |
| | | $this->pipelining = false; |
| | | } |
| | | |
| | | return true; |
| | | } |
| | | |
| | |
| | | * @param string The password to authenticate with. |
| | | * @param string The requested authentication method. If none is |
| | | * specified, the best supported method will be used. |
| | | * @param bool Flag indicating whether or not TLS should be attempted. |
| | | * |
| | | * @return mixed Returns a PEAR_Error with an error message on any |
| | | * kind of failure, or true on success. |
| | | * @access public |
| | | * @since 1.0 |
| | | */ |
| | | function auth($uid, $pwd , $method = '') |
| | | function auth($uid, $pwd , $method = '', $tls = true) |
| | | { |
| | | /* We can only attempt a TLS connection if one has been requested, |
| | | * we're running PHP 5.1.0 or later, have access to the OpenSSL |
| | | * extension, are connected to an SMTP server which supports the |
| | | * STARTTLS extension, and aren't already connected over a secure |
| | | * (SSL) socket connection. */ |
| | | if ($tls && version_compare(PHP_VERSION, '5.1.0', '>=') && |
| | | extension_loaded('openssl') && isset($this->_esmtp['STARTTLS']) && |
| | | strncasecmp($this->host, 'ssl://', 6) !== 0) { |
| | | /* Start the TLS connection attempt. */ |
| | | if (PEAR::isError($result = $this->_put('STARTTLS'))) { |
| | | return $result; |
| | | } |
| | | if (PEAR::isError($result = $this->_parseResponse(220))) { |
| | | return $result; |
| | | } |
| | | if (PEAR::isError($result = $this->_socket->enableCrypto(true, STREAM_CRYPTO_METHOD_TLS_CLIENT))) { |
| | | return $result; |
| | | } elseif ($result !== true) { |
| | | return PEAR::raiseError('STARTTLS failed'); |
| | | } |
| | | |
| | | /* Send EHLO again to recieve the AUTH string from the |
| | | * SMTP server. */ |
| | | $this->_negotiate(); |
| | | } |
| | | |
| | | if (empty($this->_esmtp['AUTH'])) { |
| | | return PEAR::raiseError('SMTP server does no support authentication'); |
| | | return PEAR::raiseError('SMTP server does not support authentication'); |
| | | } |
| | | |
| | | /* If no method has been specified, get the name of the best |
| | |
| | | } |
| | | |
| | | switch ($method) { |
| | | case 'DIGEST-MD5': |
| | | $result = $this->_authDigest_MD5($uid, $pwd); |
| | | break; |
| | | case 'CRAM-MD5': |
| | | $result = $this->_authCRAM_MD5($uid, $pwd); |
| | | break; |
| | | case 'LOGIN': |
| | | $result = $this->_authLogin($uid, $pwd); |
| | | break; |
| | | case 'PLAIN': |
| | | $result = $this->_authPlain($uid, $pwd); |
| | | break; |
| | | default: |
| | | $result = PEAR::raiseError("$method is not a supported authentication method"); |
| | | break; |
| | | case 'DIGEST-MD5': |
| | | $result = $this->_authDigest_MD5($uid, $pwd); |
| | | break; |
| | | |
| | | case 'CRAM-MD5': |
| | | $result = $this->_authCRAM_MD5($uid, $pwd); |
| | | break; |
| | | |
| | | case 'LOGIN': |
| | | $result = $this->_authLogin($uid, $pwd); |
| | | break; |
| | | |
| | | case 'PLAIN': |
| | | $result = $this->_authPlain($uid, $pwd); |
| | | break; |
| | | |
| | | default: |
| | | $result = PEAR::raiseError("$method is not a supported authentication method"); |
| | | break; |
| | | } |
| | | |
| | | /* If an error was encountered, return the PEAR_Error object. */ |
| | |
| | | /* We don't use the protocol's third step because SMTP doesn't |
| | | * allow subsequent authentication, so we just silently ignore |
| | | * it. */ |
| | | if (PEAR::isError($error = $this->_put(' '))) { |
| | | if (PEAR::isError($error = $this->_put(''))) { |
| | | return $error; |
| | | } |
| | | /* 235: Authentication successful */ |
| | |
| | | } |
| | | |
| | | /** |
| | | * Return the list of SMTP service extensions advertised by the server. |
| | | * |
| | | * @return array The list of SMTP service extensions. |
| | | * @access public |
| | | * @since 1.3 |
| | | */ |
| | | function getServiceExtensions() |
| | | { |
| | | return $this->_esmtp; |
| | | } |
| | | |
| | | /** |
| | | * Send the MAIL FROM: command. |
| | | * |
| | | * @param string The sender (reverse path) to set. |
| | | * @param string $sender The sender (reverse path) to set. |
| | | * @param string $params String containing additional MAIL parameters, |
| | | * such as the NOTIFY flags defined by RFC 1891 |
| | | * or the VERP protocol. |
| | | * |
| | | * @param array optional arguments. Currently supported: |
| | | * verp boolean or string. If true or string |
| | | * verp is enabled. If string the characters |
| | | * are considered verp separators. |
| | | * If $params is an array, only the 'verp' option |
| | | * is supported. If 'verp' is true, the XVERP |
| | | * parameter is appended to the MAIL command. If |
| | | * the 'verp' value is a string, the full |
| | | * XVERP=value parameter is appended. |
| | | * |
| | | * @return mixed Returns a PEAR_Error with an error message on any |
| | | * kind of failure, or true on success. |
| | | * @access public |
| | | * @since 1.0 |
| | | */ |
| | | function mailFrom($sender, $args = array()) |
| | | function mailFrom($sender, $params = null) |
| | | { |
| | | $argstr = ''; |
| | | $args = "FROM:<$sender>"; |
| | | |
| | | if (isset($args['verp'])) { |
| | | /* Support the deprecated array form of $params. */ |
| | | if (is_array($params) && isset($params['verp'])) { |
| | | /* XVERP */ |
| | | if ($args['verp'] === true) { |
| | | $argstr .= ' XVERP'; |
| | | if ($params['verp'] === true) { |
| | | $args .= ' XVERP'; |
| | | |
| | | /* XVERP=something */ |
| | | } elseif (trim($args['verp'])) { |
| | | $argstr .= ' XVERP=' . $args['verp']; |
| | | } elseif (trim($params['verp'])) { |
| | | $args .= ' XVERP=' . $params['verp']; |
| | | } |
| | | } elseif (is_string($params)) { |
| | | $args .= ' ' . $params; |
| | | } |
| | | |
| | | if (PEAR::isError($error = $this->_put('MAIL', "FROM:<$sender>$argstr"))) { |
| | | if (PEAR::isError($error = $this->_put('MAIL', $args))) { |
| | | return $error; |
| | | } |
| | | if (PEAR::isError($error = $this->_parseResponse(250))) { |
| | | if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { |
| | | return $error; |
| | | } |
| | | |
| | |
| | | /** |
| | | * Send the RCPT TO: command. |
| | | * |
| | | * @param string The recipient (forward path) to add. |
| | | * @param string $recipient The recipient (forward path) to add. |
| | | * @param string $params String containing additional RCPT parameters, |
| | | * such as the NOTIFY flags defined by RFC 1891. |
| | | * |
| | | * @return mixed Returns a PEAR_Error with an error message on any |
| | | * kind of failure, or true on success. |
| | | * |
| | | * @access public |
| | | * @since 1.0 |
| | | */ |
| | | function rcptTo($recipient) |
| | | function rcptTo($recipient, $params = null) |
| | | { |
| | | if (PEAR::isError($error = $this->_put('RCPT', "TO:<$recipient>"))) { |
| | | $args = "TO:<$recipient>"; |
| | | if (is_string($params)) { |
| | | $args .= ' ' . $params; |
| | | } |
| | | |
| | | if (PEAR::isError($error = $this->_put('RCPT', $args))) { |
| | | return $error; |
| | | } |
| | | if (PEAR::isError($error = $this->_parseResponse(array(250, 251)))) { |
| | | if (PEAR::isError($error = $this->_parseResponse(array(250, 251), $this->pipelining))) { |
| | | return $error; |
| | | } |
| | | |
| | |
| | | /** |
| | | * Send the DATA command. |
| | | * |
| | | * @param string $data The message body to send. |
| | | * @param mixed $data The message data, either as a string or an open |
| | | * file resource. |
| | | * @param string $headers The message headers. If $headers is provided, |
| | | * $data is assumed to contain only body data. |
| | | * |
| | | * @return mixed Returns a PEAR_Error with an error message on any |
| | | * kind of failure, or true on success. |
| | | * @access public |
| | | * @since 1.0 |
| | | */ |
| | | function data($data) |
| | | function data($data, $headers = null) |
| | | { |
| | | /* Verify that $data is a supported type. */ |
| | | if (!is_string($data) && !is_resource($data)) { |
| | | return PEAR::raiseError('Expected a string or file resource'); |
| | | } |
| | | |
| | | /* RFC 1870, section 3, subsection 3 states "a value of zero |
| | | * indicates that no fixed maximum message size is in force". |
| | | * Furthermore, it says that if "the parameter is omitted no |
| | | * information is conveyed about the server's fixed maximum |
| | | * message size". */ |
| | | if (isset($this->_esmtp['SIZE']) && ($this->_esmtp['SIZE'] > 0)) { |
| | | if (strlen($data) >= $this->_esmtp['SIZE']) { |
| | | /* Start by considering the size of the optional headers string. |
| | | * We also account for the addition 4 character "\r\n\r\n" |
| | | * separator sequence. */ |
| | | $size = (is_null($headers)) ? 0 : strlen($headers) + 4; |
| | | |
| | | if (is_resource($data)) { |
| | | $stat = fstat($data); |
| | | if ($stat === false) { |
| | | return PEAR::raiseError('Failed to get file size'); |
| | | } |
| | | $size += $stat['size']; |
| | | } else { |
| | | $size += strlen($data); |
| | | } |
| | | |
| | | if ($size >= $this->_esmtp['SIZE']) { |
| | | $this->disconnect(); |
| | | return PEAR::raiseError('Message size excedes the server limit'); |
| | | return PEAR::raiseError('Message size exceeds server limit'); |
| | | } |
| | | } |
| | | |
| | | /* Quote the data based on the SMTP standards. */ |
| | | $this->quotedata($data); |
| | | |
| | | /* Initiate the DATA command. */ |
| | | if (PEAR::isError($error = $this->_put('DATA'))) { |
| | | return $error; |
| | | } |
| | |
| | | return $error; |
| | | } |
| | | |
| | | if (PEAR::isError($result = $this->_send($data . "\r\n.\r\n"))) { |
| | | return $result; |
| | | /* If we have a separate headers string, send it first. */ |
| | | if (!is_null($headers)) { |
| | | $this->quotedata($headers); |
| | | if (PEAR::isError($result = $this->_send($headers . "\r\n\r\n"))) { |
| | | return $result; |
| | | } |
| | | } |
| | | if (PEAR::isError($error = $this->_parseResponse(250))) { |
| | | |
| | | /* Now we can send the message body data. */ |
| | | if (is_resource($data)) { |
| | | /* Stream the contents of the file resource out over our socket |
| | | * connection, line by line. Each line must be run through the |
| | | * quoting routine. */ |
| | | while ($line = fgets($data, 1024)) { |
| | | $this->quotedata($line); |
| | | if (PEAR::isError($result = $this->_send($line))) { |
| | | return $result; |
| | | } |
| | | } |
| | | |
| | | /* Finally, send the DATA terminator sequence. */ |
| | | if (PEAR::isError($result = $this->_send("\r\n.\r\n"))) { |
| | | return $result; |
| | | } |
| | | } else { |
| | | /* Just send the entire quoted string followed by the DATA |
| | | * terminator. */ |
| | | $this->quotedata($data); |
| | | if (PEAR::isError($result = $this->_send($data . "\r\n.\r\n"))) { |
| | | return $result; |
| | | } |
| | | } |
| | | |
| | | /* Verify that the data was successfully received by the server. */ |
| | | if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { |
| | | return $error; |
| | | } |
| | | |
| | |
| | | if (PEAR::isError($error = $this->_put('SEND', "FROM:<$path>"))) { |
| | | return $error; |
| | | } |
| | | if (PEAR::isError($error = $this->_parseResponse(250))) { |
| | | if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { |
| | | return $error; |
| | | } |
| | | |
| | |
| | | if (PEAR::isError($error = $this->_put('SOML', "FROM:<$path>"))) { |
| | | return $error; |
| | | } |
| | | if (PEAR::isError($error = $this->_parseResponse(250))) { |
| | | if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { |
| | | return $error; |
| | | } |
| | | |
| | |
| | | if (PEAR::isError($error = $this->_put('SAML', "FROM:<$path>"))) { |
| | | return $error; |
| | | } |
| | | if (PEAR::isError($error = $this->_parseResponse(250))) { |
| | | if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { |
| | | return $error; |
| | | } |
| | | |
| | |
| | | if (PEAR::isError($error = $this->_put('RSET'))) { |
| | | return $error; |
| | | } |
| | | if (PEAR::isError($error = $this->_parseResponse(250))) { |
| | | if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { |
| | | return $error; |
| | | } |
| | | |