symphony/lib/toolkit/class.smtp.php
<?php
/**
* @package toolkit
*/
/**
* A SMTP client class, for sending text/plain emails.
* This class only supports the very basic SMTP functions.
* Inspired by the SMTP class in the Zend library
*
* @author Huib Keemink <huib.keemink@creativedutchmen.com>
* @version 0.1 - 20 okt 2010
*/
class SMTP
{
const TIMEOUT = 30;
protected $_host;
protected $_port;
protected $_user = null;
protected $_pass = null;
protected $_transport = 'tcp';
protected $_secure = false;
protected $_header_fields = array();
protected $_from = null;
protected $_helo_host = null;
protected $_connection = false;
protected $_helo = false;
protected $_mail = false;
protected $_data = false;
protected $_rcpt = false;
protected $_auth = false;
/**
* Constructor.
*
* @param string $host
* Host to connect to. Defaults to localhost (127.0.0.1)
* @param integer $port
* When ssl is used, defaults to 465
* When no ssl is used, and ini_get returns no value, defaults to 25.
* @param array $options
* Currently supports 3 values:
* $options['secure'] can be ssl, tls or null.
* $options['username'] the username used to login to the server. Leave empty for no authentication.
* $options['password'] the password used to login to the server. Leave empty for no authentication.
* $options['helo_hostname'] the hostname address used in the EHLO/HELO commands. Ideally an FQDN.
* $options['local_ip'] the ip address used in the EHLO/HELO commands if no helo_hostname is given.
* @throws SMTPException
*/
public function __construct($host = '127.0.0.1', $port = null, $options = array())
{
if ($options['secure'] !== null) {
switch (strtolower($options['secure'])) {
case 'tls':
$this->_secure = 'tls';
break;
case 'ssl':
$this->_transport = 'ssl';
$this->_secure = 'ssl';
if ($port === null) {
$port = 465;
}
break;
case 'no':
break;
default:
throw new SMTPException(__('Unsupported SSL type'));
}
}
if (!empty($options['helo_hostname'])) {
$this->_helo_host = $options['helo_hostname'];
} elseif (!empty($options['local_ip'])) {
$this->_helo_host = '[' . $options['local_ip'] . ']';
} else {
$this->_helo_host = '[' . gethostbyname(php_uname('n')) . ']';
}
if ($port === null) {
$port = 25;
}
if (($options['username'] !== null) && ($options['password'] !== null)) {
$this->_user = $options['username'];
$this->_pass = $options['password'];
}
$this->_host = $host;
$this->_port = $port;
}
/**
* Checks to see if `$this->_connection` is a valid resource. Throws an
* exception if there is no connection, otherwise returns true.
*
* @throws SMTPException
* @return boolean
*/
public function checkConnection()
{
if (!is_resource($this->_connection)) {
throw new SMTPException(__('No connection has been established to %s', array($this->_host)));
}
return true;
}
/**
* The actual email sending.
* The connection to the server (connecting, EHLO, AUTH, etc) is done here,
* right before the actual email is sent. This is to make sure the connection does not time out.
*
* @param string $from
* The from string. Should have the following format: email@domain.tld
* @param string $to
* The email address to send the email to.
* @param string $subject
* The subject to send the email to.
* @param string $message
* The message to send as an email
* @param array $options (optional)
* An array of options that will be pass down to stream_context_create
* @throws SMTPException
* @throws Exception
* @return boolean
*/
public function sendMail($from, $to, $message, $options = [])
{
$this->_connect($this->_host, $this->_port, $options);
$this->mail($from);
if (!is_array($to)) {
$to = array($to);
}
foreach ($to as $recipient) {
$this->rcpt($recipient);
}
$this->data($message);
$this->rset();
}
/**
* Sets a header to be sent in the email.
*
* @throws SMTPException
* @param string $header
* @param string $value
* @return void
*/
public function setHeader($header, $value)
{
if (is_array($value)) {
throw new SMTPException(__('Header fields can only contain strings'));
}
$this->_header_fields[$header] = $value;
}
/**
* Initiates the ehlo/helo requests.
*
* @throws SMTPException
* @throws Exception
* @return void
*/
public function helo()
{
if ($this->_mail !== false) {
throw new SMTPException(__('Can not call HELO on existing session'));
}
//wait for the server to be ready
$this->_expect(220, 300);
//send ehlo or ehlo request.
try {
$this->_ehlo();
} catch (SMTPException $e) {
$this->_helo();
} catch (Exception $e) {
throw $e;
}
$this->_helo = true;
}
/**
* Calls the MAIL command on the server.
*
* @throws SMTPException
* @param string $from
* The email address to send the email from.
* @return void
*/
public function mail($from)
{
if ($this->_helo == false) {
throw new SMTPException(__('Must call EHLO (or HELO) before calling MAIL'));
} elseif ($this->_mail !== false) {
throw new SMTPException(__('Only one call to MAIL may be made at a time.'));
}
$this->_send('MAIL FROM:<' . $from . '>');
$this->_expect(250, 300);
$this->_from = $from;
$this->_mail = true;
$this->_rcpt = false;
$this->_data = false;
}
/**
* Calls the RCPT command on the server. May be called multiple times for more than one recipient.
*
* @throws SMTPException
* @param string $to
* The address to send the email to.
* @return void
*/
public function rcpt($to)
{
if ($this->_mail == false) {
throw new SMTPException(__('Must call MAIL before calling RCPT'));
}
$this->_send('RCPT TO:<' . $to . '>');
$this->_expect(array(250, 251), 300);
$this->_rcpt = true;
}
/**
* Calls the data command on the server.
* Also includes header fields in the command.
*
* @throws SMTPException
* @param string $data
* @return void
*/
public function data($data)
{
if ($this->_rcpt == false) {
throw new SMTPException(__('Must call RCPT before calling DATA'));
}
$this->_send('DATA');
$this->_expect(354, 120);
foreach ($this->_header_fields as $name => $body) {
// Every header can contain an array. Will insert multiple header fields of that type with the contents of array.
// Useful for multiple recipients, for instance.
if (!is_array($body)) {
$body = array($body);
}
foreach ($body as $val) {
$this->_send($name . ': ' . $val);
}
}
// Send an empty newline. Solves bugs with Apple Mail
$this->_send('');
// Because the message can contain \n as a newline, replace all \r\n with \n and explode on \n.
// The send() function will use the proper line ending (\r\n).
$data = str_replace("\r\n", "\n", $data);
$data_arr = explode("\n", $data);
foreach ($data_arr as $line) {
// Escape line if first character is a period (dot). http://tools.ietf.org/html/rfc2821#section-4.5.2
if (strpos($line, '.') === 0) {
$line = '.' . $line;
}
$this->_send($line);
}
$this->_send('.');
$this->_expect(250, 600);
$this->_data = true;
}
/**
* Resets the current session. This 'undoes' all rcpt, mail, etc calls.
*
* @throws SMTPException
* @return void
*/
public function rset()
{
$this->_send('RSET');
// MS ESMTP doesn't follow RFC, see [ZF-1377]
$this->_expect(array(250, 220));
$this->_mail = false;
$this->_rcpt = false;
$this->_data = false;
}
/**
* Disconnects to the server.
*
* @throws SMTPException
* @return void
*/
public function quit()
{
$this->_send('QUIT');
$this->_expect(221, 300);
$this->_connection = null;
}
/**
* Authenticates to the server.
* Currently supports the AUTH LOGIN command.
* May be extended if more methods are needed.
*
* @throws SMTPException
* @return void
*/
protected function _auth()
{
if ($this->_helo == false) {
throw new SMTPException(__('Must call EHLO (or HELO) before calling AUTH'));
} elseif ($this->_auth !== false) {
throw new SMTPException(__('Can not call AUTH again.'));
}
$this->_send('AUTH LOGIN');
$this->_expect(334);
$this->_send(base64_encode($this->_user));
$this->_expect(334);
$this->_send(base64_encode($this->_pass));
$this->_expect(235);
$this->_auth = true;
}
/**
* Calls the EHLO function.
* This is the HELO function for more modern servers.
*
* @throws SMTPException
* @return void
*/
protected function _ehlo()
{
$this->_send('EHLO ' . $this->_helo_host);
$this->_expect(array(250, 220), 300);
}
/**
* Initiates the connection by calling the HELO function.
* This function should only be used if the server does not support the HELO function.
*
* @throws SMTPException
* @return void
*/
protected function _helo()
{
$this->_send('HELO ' . $this->_helo_host);
$this->_expect(array(250, 220), 300);
}
/**
* Encrypts the current session with TLS.
*
* @throws SMTPException
* @return void
*/
protected function _tls()
{
if ($this->_secure == 'tls') {
$this->_send('STARTTLS');
$this->_expect(220, 180);
if (!stream_socket_enable_crypto($this->_connection, true, STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT)) {
throw new SMTPException(__('Unable to connect via TLS'));
}
$this->_ehlo();
}
}
/**
* Send a request to the host, appends the request with a line break.
*
* @param string $request
* @throws SMTPException
* @return boolean|integer number of characters written.
*/
protected function _send($request)
{
$this->checkConnection();
$result = fwrite($this->_connection, $request . "\r\n");
if ($result === false) {
throw new SMTPException(__('Could not send request: %s', array($request)));
}
return $result;
}
/**
* Get a line from the stream.
*
* @param integer $timeout
* Per-request timeout value if applicable. Defaults to null which
* will not set a timeout.
* @throws SMTPException
* @return string
*/
protected function _receive($timeout = null)
{
$this->checkConnection();
if ($timeout !== null) {
stream_set_timeout($this->_connection, $timeout);
}
$response = fgets($this->_connection, 1024);
$info = stream_get_meta_data($this->_connection);
if (!empty($info['timed_out'])) {
throw new SMTPException(__('%s has timed out', array($this->_host)));
} elseif ($response === false) {
throw new SMTPException(__('Could not read from %s', array($this->_host)));
}
return $response;
}
/**
* Parse server response for successful codes
*
* Read the response from the stream and check for expected return code.
*
* @throws SMTPException
* @param string|array $code
* One or more codes that indicate a successful response
* @param integer $timeout
* Per-request timeout value if applicable. Defaults to null which
* will not set a timeout.
* @return string
* Last line of response string
*/
protected function _expect($code, $timeout = null)
{
$cmd = '';
$more = '';
$msg = '';
$errMsg = '';
if (!is_array($code)) {
$code = array($code);
}
// Borrowed from the Zend Email Library
do {
$result = $this->_receive($timeout);
list($cmd, $more, $msg) = preg_split('/([\s-]+)/', $result, 2, PREG_SPLIT_DELIM_CAPTURE);
if ($errMsg !== '') {
$errMsg .= ' ' . $msg;
} elseif ($cmd === null || !in_array($cmd, $code)) {
$errMsg = $msg;
}
} while (strpos($more, '-') === 0); // The '-' message prefix indicates an information string instead of a response string.
if ($errMsg !== '') {
$this->rset();
throw new SMTPException($errMsg);
}
return $msg;
}
/**
* Connect to the host, and perform basic functions like helo and auth.
*
* @param string $host
* Host to connect to
* @param integer $port
* The port to connect to
* @param array $options (optional)
* An array of options that will be pass down to stream_context_create
* @throws SMTPException
* @throws Exception
* @return void
*/
protected function _connect($host, $port, $options = [])
{
$errorNum = 0;
$errorStr = '';
$remoteAddr = $this->_transport . '://' . $host . ':' . $port;
if (!is_resource($this->_connection)) {
$context = stream_context_create($options);
$this->_connection = stream_socket_client($remoteAddr, $errorNum, $errorStr, self::TIMEOUT, STREAM_CLIENT_CONNECT, $context);
if ($this->_connection === false) {
if ($errorNum == 0) {
throw new SMTPException(__('Unable to open socket. Unknown error'));
} else {
throw new SMTPException(__('Unable to open socket. %s', array($errorStr)));
}
}
if (@stream_set_timeout($this->_connection, self::TIMEOUT) === false) {
throw new SMTPException(__('Unable to set timeout.'));
}
$this->helo();
if ($this->_secure == 'tls') {
$this->_tls();
}
if (($this->_user !== null) && ($this->_pass !== null)) {
$this->_auth();
}
}
}
}