<?php
/**
* PHPMailer RFC821 SMTP email transport class.
- * PHP Version 5
- * @package PHPMailer
- * @link https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
- * @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
- * @author Jim Jagielski (jimjag) <jimjag@gmail.com>
- * @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
- * @author Brent R. Matzelle (original founder)
- * @copyright 2014 Marcus Bointon
+ * PHP Version 5.5.
+ *
+ * @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
+ *
+ * @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
+ * @author Jim Jagielski (jimjag) <jimjag@gmail.com>
+ * @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
+ * @author Brent R. Matzelle (original founder)
+ * @copyright 2012 - 2019 Marcus Bointon
* @copyright 2010 - 2012 Jim Jagielski
* @copyright 2004 - 2009 Andy Prevost
- * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
- * @note This program is distributed in the hope that it will be useful - WITHOUT
+ * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
+ * @note This program is distributed in the hope that it will be useful - WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE.
*/
+namespace PHPMailer\PHPMailer;
+
/**
* PHPMailer RFC821 SMTP email transport class.
* Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server.
- * @package PHPMailer
+ *
* @author Chris Ryan
* @author Marcus Bointon <phpmailer@synchromedia.co.uk>
*/
{
/**
* The PHPMailer SMTP version number.
+ *
* @var string
*/
- const VERSION = '5.2.16';
+ const VERSION = '6.1.4';
/**
* SMTP line break constant.
+ *
* @var string
*/
- const CRLF = "\r\n";
+ const LE = "\r\n";
/**
* The SMTP port to use if one is not specified.
- * @var integer
+ *
+ * @var int
*/
- const DEFAULT_SMTP_PORT = 25;
+ const DEFAULT_PORT = 25;
/**
- * The maximum line length allowed by RFC 2822 section 2.1.1
- * @var integer
+ * The maximum line length allowed by RFC 5321 section 4.5.3.1.6,
+ * *excluding* a trailing CRLF break.
+ *
+ * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.6
+ *
+ * @var int
*/
const MAX_LINE_LENGTH = 998;
/**
- * Debug level for no output
+ * The maximum line length allowed for replies in RFC 5321 section 4.5.3.1.5,
+ * *including* a trailing CRLF line break.
+ *
+ * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.5
+ *
+ * @var int
+ */
+ const MAX_REPLY_LENGTH = 512;
+
+ /**
+ * Debug level for no output.
+ *
+ * @var int
*/
const DEBUG_OFF = 0;
/**
- * Debug level to show client -> server messages
+ * Debug level to show client -> server messages.
+ *
+ * @var int
*/
const DEBUG_CLIENT = 1;
/**
- * Debug level to show client -> server and server -> client messages
+ * Debug level to show client -> server and server -> client messages.
+ *
+ * @var int
*/
const DEBUG_SERVER = 2;
/**
- * Debug level to show connection status, client -> server and server -> client messages
+ * Debug level to show connection status, client -> server and server -> client messages.
+ *
+ * @var int
*/
const DEBUG_CONNECTION = 3;
/**
- * Debug level to show all messages
+ * Debug level to show all messages.
+ *
+ * @var int
*/
const DEBUG_LOWLEVEL = 4;
- /**
- * The PHPMailer SMTP Version number.
- * @var string
- * @deprecated Use the `VERSION` constant instead
- * @see SMTP::VERSION
- */
- public $Version = '5.2.16';
-
- /**
- * SMTP server port number.
- * @var integer
- * @deprecated This is only ever used as a default value, so use the `DEFAULT_SMTP_PORT` constant instead
- * @see SMTP::DEFAULT_SMTP_PORT
- */
- public $SMTP_PORT = 25;
-
- /**
- * SMTP reply line ending.
- * @var string
- * @deprecated Use the `CRLF` constant instead
- * @see SMTP::CRLF
- */
- public $CRLF = "\r\n";
-
/**
* Debug output level.
* Options:
* * self::DEBUG_CLIENT (`1`) Client commands
* * self::DEBUG_SERVER (`2`) Client commands and server responses
* * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status
- * * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages
- * @var integer
+ * * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages.
+ *
+ * @var int
*/
public $do_debug = self::DEBUG_OFF;
* * `echo` Output plain-text as-is, appropriate for CLI
* * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
* * `error_log` Output to error log as configured in php.ini
- *
* Alternatively, you can provide a callable expecting two params: a message string and the debug level:
- * <code>
+ *
+ * ```php
* $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
- * </code>
- * @var string|callable
+ * ```
+ *
+ * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
+ * level output is used:
+ *
+ * ```php
+ * $mail->Debugoutput = new myPsr3Logger;
+ * ```
+ *
+ * @var string|callable|\Psr\Log\LoggerInterface
*/
public $Debugoutput = 'echo';
/**
* Whether to use VERP.
- * @link http://en.wikipedia.org/wiki/Variable_envelope_return_path
- * @link http://www.postfix.org/VERP_README.html Info on VERP
- * @var boolean
+ *
+ * @see http://en.wikipedia.org/wiki/Variable_envelope_return_path
+ * @see http://www.postfix.org/VERP_README.html Info on VERP
+ *
+ * @var bool
*/
public $do_verp = false;
/**
* The timeout value for connection, in seconds.
- * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2
+ * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
* This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure.
- * @link http://tools.ietf.org/html/rfc2821#section-4.5.3.2
- * @var integer
+ *
+ * @see http://tools.ietf.org/html/rfc2821#section-4.5.3.2
+ *
+ * @var int
*/
public $Timeout = 300;
/**
* How long to wait for commands to complete, in seconds.
- * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2
- * @var integer
+ * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
+ *
+ * @var int
*/
public $Timelimit = 300;
+ /**
+ * Patterns to extract an SMTP transaction id from reply to a DATA command.
+ * The first capture group in each regex will be used as the ID.
+ * MS ESMTP returns the message ID, which may not be correct for internal tracking.
+ *
+ * @var string[]
+ */
+ protected $smtp_transaction_id_patterns = [
+ 'exim' => '/[\d]{3} OK id=(.*)/',
+ 'sendmail' => '/[\d]{3} 2.0.0 (.*) Message/',
+ 'postfix' => '/[\d]{3} 2.0.0 Ok: queued as (.*)/',
+ 'Microsoft_ESMTP' => '/[0-9]{3} 2.[\d].0 (.*)@(?:.*) Queued mail for delivery/',
+ 'Amazon_SES' => '/[\d]{3} Ok (.*)/',
+ 'SendGrid' => '/[\d]{3} Ok: queued as (.*)/',
+ 'CampaignMonitor' => '/[\d]{3} 2.0.0 OK:([a-zA-Z\d]{48})/',
+ ];
+
+ /**
+ * The last transaction ID issued in response to a DATA command,
+ * if one was detected.
+ *
+ * @var string|bool|null
+ */
+ protected $last_smtp_transaction_id;
+
/**
* The socket for the server connection.
- * @var resource
+ *
+ * @var ?resource
*/
protected $smtp_conn;
/**
* Error information, if any, for the last SMTP command.
+ *
* @var array
*/
- protected $error = array(
+ protected $error = [
'error' => '',
'detail' => '',
'smtp_code' => '',
- 'smtp_code_ex' => ''
- );
+ 'smtp_code_ex' => '',
+ ];
/**
* The reply the server sent to us for HELO.
* If null, no HELO string has yet been received.
+ *
* @var string|null
*/
- protected $helo_rply = null;
+ protected $helo_rply;
/**
* The set of SMTP extensions sent in reply to EHLO command.
* represents the server name. In case of HELO it is the only element of the array.
* Other values can be boolean TRUE or an array containing extension options.
* If null, no HELO/EHLO string has yet been received.
+ *
* @var array|null
*/
- protected $server_caps = null;
+ protected $server_caps;
/**
* The most recent reply received from the server.
+ *
* @var string
*/
protected $last_reply = '';
/**
* Output debugging info via a user-selected method.
+ *
+ * @param string $str Debug string to output
+ * @param int $level The debug level of this message; see DEBUG_* constants
+ *
* @see SMTP::$Debugoutput
* @see SMTP::$do_debug
- * @param string $str Debug string to output
- * @param integer $level The debug level of this message; see DEBUG_* constants
- * @return void
*/
protected function edebug($str, $level = 0)
{
if ($level > $this->do_debug) {
return;
}
+ //Is this a PSR-3 logger?
+ if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
+ $this->Debugoutput->debug($str);
+
+ return;
+ }
//Avoid clash with built-in function names
- if (!in_array($this->Debugoutput, array('error_log', 'html', 'echo')) and is_callable($this->Debugoutput)) {
- call_user_func($this->Debugoutput, $str, $this->do_debug);
+ if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
+ call_user_func($this->Debugoutput, $str, $level);
+
return;
}
switch ($this->Debugoutput) {
break;
case 'html':
//Cleans up output a bit for a better looking, HTML-safe output
- echo htmlentities(
+ echo gmdate('Y-m-d H:i:s'), ' ', htmlentities(
preg_replace('/[\r\n]+/', '', $str),
ENT_QUOTES,
'UTF-8'
- )
- . "<br>\n";
+ ), "<br>\n";
break;
case 'echo':
default:
//Normalize line breaks
- $str = preg_replace('/(\r\n|\r|\n)/ms', "\n", $str);
- echo gmdate('Y-m-d H:i:s') . "\t" . str_replace(
- "\n",
- "\n \t ",
- trim($str)
- )."\n";
+ $str = preg_replace('/\r\n|\r/m', "\n", $str);
+ echo gmdate('Y-m-d H:i:s'),
+ "\t",
+ //Trim trailing space
+ trim(
+ //Indent for readability, except for trailing break
+ str_replace(
+ "\n",
+ "\n \t ",
+ trim($str)
+ )
+ ),
+ "\n";
}
}
/**
* Connect to an SMTP server.
- * @param string $host SMTP server IP or host name
- * @param integer $port The port number to connect to
- * @param integer $timeout How long to wait for the connection to open
- * @param array $options An array of options for stream_context_create()
- * @access public
- * @return boolean
+ *
+ * @param string $host SMTP server IP or host name
+ * @param int $port The port number to connect to
+ * @param int $timeout How long to wait for the connection to open
+ * @param array $options An array of options for stream_context_create()
+ *
+ * @return bool
*/
- public function connect($host, $port = null, $timeout = 30, $options = array())
+ public function connect($host, $port = null, $timeout = 30, $options = [])
{
static $streamok;
//This is enabled by default since 5.0.0 but some providers disable it
//Check this once and cache the result
- if (is_null($streamok)) {
+ if (null === $streamok) {
$streamok = function_exists('stream_socket_client');
}
// Clear errors to avoid confusion
if ($this->connected()) {
// Already connected, generate error
$this->setError('Already connected to a server');
+
return false;
}
if (empty($port)) {
- $port = self::DEFAULT_SMTP_PORT;
+ $port = self::DEFAULT_PORT;
}
// Connect to the SMTP server
$this->edebug(
- "Connection: opening to $host:$port, timeout=$timeout, options=".var_export($options, true),
+ "Connection: opening to $host:$port, timeout=$timeout, options=" .
+ (count($options) > 0 ? var_export($options, true) : 'array()'),
self::DEBUG_CONNECTION
);
$errno = 0;
$errstr = '';
if ($streamok) {
$socket_context = stream_context_create($options);
- //Suppress errors; connection failures are handled at a higher level
- $this->smtp_conn = @stream_socket_client(
- $host . ":" . $port,
+ set_error_handler([$this, 'errorHandler']);
+ $this->smtp_conn = stream_socket_client(
+ $host . ':' . $port,
$errno,
$errstr,
$timeout,
STREAM_CLIENT_CONNECT,
$socket_context
);
+ restore_error_handler();
} else {
//Fall back to fsockopen which should work in more places, but is missing some features
$this->edebug(
- "Connection: stream_socket_client not available, falling back to fsockopen",
+ 'Connection: stream_socket_client not available, falling back to fsockopen',
self::DEBUG_CONNECTION
);
+ set_error_handler([$this, 'errorHandler']);
$this->smtp_conn = fsockopen(
$host,
$port,
$errstr,
$timeout
);
+ restore_error_handler();
}
// Verify we connected properly
if (!is_resource($this->smtp_conn)) {
$this->setError(
'Failed to connect to server',
- $errno,
+ '',
+ (string) $errno,
$errstr
);
$this->edebug(
. ": $errstr ($errno)",
self::DEBUG_CLIENT
);
+
return false;
}
$this->edebug('Connection: opened', self::DEBUG_CONNECTION);
// SMTP server can take longer to respond, give longer timeout for first read
// Windows does not have support for this timeout function
- if (substr(PHP_OS, 0, 3) != 'WIN') {
- $max = ini_get('max_execution_time');
+ if (strpos(PHP_OS, 'WIN') !== 0) {
+ $max = (int) ini_get('max_execution_time');
// Don't bother if unlimited
- if ($max != 0 && $timeout > $max) {
+ if (0 !== $max && $timeout > $max) {
@set_time_limit($timeout);
}
stream_set_timeout($this->smtp_conn, $timeout, 0);
// Get any announcement
$announce = $this->get_lines();
$this->edebug('SERVER -> CLIENT: ' . $announce, self::DEBUG_SERVER);
+
return true;
}
/**
* Initiate a TLS (encrypted) session.
- * @access public
- * @return boolean
+ *
+ * @return bool
*/
public function startTLS()
{
}
// Begin encrypted connection
- if (!stream_socket_enable_crypto(
+ set_error_handler([$this, 'errorHandler']);
+ $crypto_ok = stream_socket_enable_crypto(
$this->smtp_conn,
true,
$crypto_method
- )) {
- return false;
- }
- return true;
+ );
+ restore_error_handler();
+
+ return (bool) $crypto_ok;
}
/**
* Perform SMTP authentication.
* Must be run after hello().
- * @see hello()
+ *
+ * @see hello()
+ *
* @param string $username The user name
* @param string $password The password
- * @param string $authtype The auth type (PLAIN, LOGIN, NTLM, CRAM-MD5, XOAUTH2)
- * @param string $realm The auth realm for NTLM
- * @param string $workstation The auth workstation for NTLM
- * @param null|OAuth $OAuth An optional OAuth instance (@see PHPMailerOAuth)
- * @return bool True if successfully authenticated.* @access public
+ * @param string $authtype The auth type (CRAM-MD5, PLAIN, LOGIN, XOAUTH2)
+ * @param OAuth $OAuth An optional OAuth instance for XOAUTH2 authentication
+ *
+ * @return bool True if successfully authenticated
*/
public function authenticate(
$username,
$password,
$authtype = null,
- $realm = '',
- $workstation = '',
$OAuth = null
) {
if (!$this->server_caps) {
$this->setError('Authentication is not allowed before HELO/EHLO');
+
return false;
}
if (array_key_exists('EHLO', $this->server_caps)) {
- // SMTP extensions are available. Let's try to find a proper authentication method
-
+ // SMTP extensions are available; try to find a proper authentication method
if (!array_key_exists('AUTH', $this->server_caps)) {
$this->setError('Authentication is not allowed at this stage');
// 'at this stage' means that auth may be allowed after the stage changes
// e.g. after STARTTLS
+
return false;
}
- self::edebug('Auth method requested: ' . ($authtype ? $authtype : 'UNKNOWN'), self::DEBUG_LOWLEVEL);
- self::edebug(
+ $this->edebug('Auth method requested: ' . ($authtype ?: 'UNSPECIFIED'), self::DEBUG_LOWLEVEL);
+ $this->edebug(
'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']),
self::DEBUG_LOWLEVEL
);
+ //If we have requested a specific auth type, check the server supports it before trying others
+ if (null !== $authtype && !in_array($authtype, $this->server_caps['AUTH'], true)) {
+ $this->edebug('Requested auth method not available: ' . $authtype, self::DEBUG_LOWLEVEL);
+ $authtype = null;
+ }
+
if (empty($authtype)) {
- foreach (array('CRAM-MD5', 'LOGIN', 'PLAIN', 'NTLM', 'XOAUTH2') as $method) {
- if (in_array($method, $this->server_caps['AUTH'])) {
+ //If no auth mechanism is specified, attempt to use these, in this order
+ //Try CRAM-MD5 first as it's more secure than the others
+ foreach (['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'] as $method) {
+ if (in_array($method, $this->server_caps['AUTH'], true)) {
$authtype = $method;
break;
}
}
if (empty($authtype)) {
$this->setError('No supported authentication methods found');
+
return false;
}
- self::edebug('Auth method selected: '.$authtype, self::DEBUG_LOWLEVEL);
+ $this->edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL);
}
- if (!in_array($authtype, $this->server_caps['AUTH'])) {
+ if (!in_array($authtype, $this->server_caps['AUTH'], true)) {
$this->setError("The requested authentication method \"$authtype\" is not supported by the server");
+
return false;
}
} elseif (empty($authtype)) {
if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) {
return false;
}
- if (!$this->sendCommand("Username", base64_encode($username), 334)) {
- return false;
- }
- if (!$this->sendCommand("Password", base64_encode($password), 235)) {
+ if (!$this->sendCommand('Username', base64_encode($username), 334)) {
return false;
}
- break;
- case 'XOAUTH2':
- //If the OAuth Instance is not set. Can be a case when PHPMailer is used
- //instead of PHPMailerOAuth
- if (is_null($OAuth)) {
- return false;
- }
- $oauth = $OAuth->getOauth64();
-
- // Start authentication
- if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
+ if (!$this->sendCommand('Password', base64_encode($password), 235)) {
return false;
}
break;
- case 'NTLM':
- /*
- * ntlm_sasl_client.php
- * Bundled with Permission
- *
- * How to telnet in windows:
- * http://technet.microsoft.com/en-us/library/aa995718%28EXCHG.65%29.aspx
- * PROTOCOL Docs http://curl.haxx.se/rfc/ntlm.html#ntlmSmtpAuthentication
- */
- require_once 'extras/ntlm_sasl_client.php';
- $temp = new stdClass;
- $ntlm_client = new ntlm_sasl_client_class;
- //Check that functions are available
- if (!$ntlm_client->Initialize($temp)) {
- $this->setError($temp->error);
- $this->edebug(
- 'You need to enable some modules in your php.ini file: '
- . $this->error['error'],
- self::DEBUG_CLIENT
- );
- return false;
- }
- //msg1
- $msg1 = $ntlm_client->TypeMsg1($realm, $workstation); //msg1
-
- if (!$this->sendCommand(
- 'AUTH NTLM',
- 'AUTH NTLM ' . base64_encode($msg1),
- 334
- )
- ) {
- return false;
- }
- //Though 0 based, there is a white space after the 3 digit number
- //msg2
- $challenge = substr($this->last_reply, 3);
- $challenge = base64_decode($challenge);
- $ntlm_res = $ntlm_client->NTLMResponse(
- substr($challenge, 24, 8),
- $password
- );
- //msg3
- $msg3 = $ntlm_client->TypeMsg3(
- $ntlm_res,
- $username,
- $realm,
- $workstation
- );
- // send encoded username
- return $this->sendCommand('Username', base64_encode($msg3), 235);
case 'CRAM-MD5':
// Start authentication
if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) {
// send encoded credentials
return $this->sendCommand('Username', base64_encode($response), 235);
+ case 'XOAUTH2':
+ //The OAuth instance must be set up prior to requesting auth.
+ if (null === $OAuth) {
+ return false;
+ }
+ $oauth = $OAuth->getOauth64();
+
+ // Start authentication
+ if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
+ return false;
+ }
+ break;
default:
$this->setError("Authentication method \"$authtype\" is not supported");
+
return false;
}
+
return true;
}
/**
* Calculate an MD5 HMAC hash.
* Works like hash_hmac('md5', $data, $key)
- * in case that function is not available
+ * in case that function is not available.
+ *
* @param string $data The data to hash
* @param string $key The key to hash with
- * @access protected
+ *
* @return string
*/
protected function hmac($data, $key)
/**
* Check connection state.
- * @access public
- * @return boolean True if connected.
+ *
+ * @return bool True if connected
*/
public function connected()
{
self::DEBUG_CLIENT
);
$this->close();
+
return false;
}
+
return true; // everything looks good
}
+
return false;
}
/**
* Close the socket and clean up the state of the class.
* Don't use this function without first trying to use QUIT.
+ *
* @see quit()
- * @access public
- * @return void
*/
public function close()
{
* finializing the mail transaction. $msg_data is the message
* that is to be send with the headers. Each header needs to be
* on a single line followed by a <CRLF> with the message headers
- * and the message body being separated by and additional <CRLF>.
- * Implements rfc 821: DATA <CRLF>
+ * and the message body being separated by an additional <CRLF>.
+ * Implements RFC 821: DATA <CRLF>.
+ *
* @param string $msg_data Message data to send
- * @access public
- * @return boolean
+ *
+ * @return bool
*/
public function data($msg_data)
{
}
/* The server is ready to accept data!
- * According to rfc821 we should not send more than 1000 characters on a single line (including the CRLF)
+ * According to rfc821 we should not send more than 1000 characters on a single line (including the LE)
* so we will break the data up into lines by \r and/or \n then if needed we will break each of those into
* smaller lines to fit within the limit.
* We will also look for lines that start with a '.' and prepend an additional '.'.
*/
// Normalize line breaks before exploding
- $lines = explode("\n", str_replace(array("\r\n", "\r"), "\n", $msg_data));
+ $lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $msg_data));
/* To distinguish between a complete RFC822 message and a plain message body, we check if the first field
* of the first line (':' separated) does not contain a space then it _should_ be a header and we will
}
foreach ($lines as $line) {
- $lines_out = array();
- if ($in_headers and $line == '') {
+ $lines_out = [];
+ if ($in_headers && $line === '') {
$in_headers = false;
}
//Break this line up into several smaller lines if it's too long
//Send the lines to the server
foreach ($lines_out as $line_out) {
//RFC2821 section 4.5.2
- if (!empty($line_out) and $line_out[0] == '.') {
+ if (!empty($line_out) && $line_out[0] === '.') {
$line_out = '.' . $line_out;
}
- $this->client_send($line_out . self::CRLF);
+ $this->client_send($line_out . static::LE, 'DATA');
}
}
//Message data has been sent, complete the command
//Increase timelimit for end of DATA command
$savetimelimit = $this->Timelimit;
- $this->Timelimit = $this->Timelimit * 2;
+ $this->Timelimit *= 2;
$result = $this->sendCommand('DATA END', '.', 250);
+ $this->recordLastTransactionID();
//Restore timelimit
$this->Timelimit = $savetimelimit;
+
return $result;
}
* This makes sure that client and server are in a known state.
* Implements RFC 821: HELO <SP> <domain> <CRLF>
* and RFC 2821 EHLO.
+ *
* @param string $host The host name or IP to connect to
- * @access public
- * @return boolean
+ *
+ * @return bool
*/
public function hello($host = '')
{
//Try extended hello first (RFC 2821)
- return (boolean)($this->sendHello('EHLO', $host) or $this->sendHello('HELO', $host));
+ return $this->sendHello('EHLO', $host) or $this->sendHello('HELO', $host);
}
/**
* Send an SMTP HELO or EHLO command.
- * Low-level implementation used by hello()
- * @see hello()
+ * Low-level implementation used by hello().
+ *
* @param string $hello The HELO string
- * @param string $host The hostname to say we are
- * @access protected
- * @return boolean
+ * @param string $host The hostname to say we are
+ *
+ * @return bool
+ *
+ * @see hello()
*/
protected function sendHello($hello, $host)
{
} else {
$this->server_caps = null;
}
+
return $noerror;
}
/**
* Parse a reply to HELO/EHLO command to discover server extensions.
* In case of HELO, the only parameter that can be discovered is a server name.
- * @access protected
- * @param string $type - 'HELO' or 'EHLO'
+ *
+ * @param string $type `HELO` or `EHLO`
*/
protected function parseHelloFields($type)
{
- $this->server_caps = array();
+ $this->server_caps = [];
$lines = explode("\n", $this->helo_rply);
foreach ($lines as $n => $s) {
break;
case 'AUTH':
if (!is_array($fields)) {
- $fields = array();
+ $fields = [];
}
break;
default:
* $from. Returns true if successful or false otherwise. If True
* the mail transaction is started and then one or more recipient
* commands may be called followed by a data command.
- * Implements rfc 821: MAIL <SP> FROM:<reverse-path> <CRLF>
+ * Implements RFC 821: MAIL <SP> FROM:<reverse-path> <CRLF>.
+ *
* @param string $from Source address of this message
- * @access public
- * @return boolean
+ *
+ * @return bool
*/
public function mail($from)
{
$useVerp = ($this->do_verp ? ' XVERP' : '');
+
return $this->sendCommand(
'MAIL FROM',
'MAIL FROM:<' . $from . '>' . $useVerp,
/**
* Send an SMTP QUIT command.
* Closes the socket if there is no error or the $close_on_error argument is true.
- * Implements from rfc 821: QUIT <CRLF>
- * @param boolean $close_on_error Should the connection close if an error occurs?
- * @access public
- * @return boolean
+ * Implements from RFC 821: QUIT <CRLF>.
+ *
+ * @param bool $close_on_error Should the connection close if an error occurs?
+ *
+ * @return bool
*/
public function quit($close_on_error = true)
{
$noerror = $this->sendCommand('QUIT', 'QUIT', 221);
$err = $this->error; //Save any error
- if ($noerror or $close_on_error) {
+ if ($noerror || $close_on_error) {
$this->close();
$this->error = $err; //Restore any error from the quit command
}
+
return $noerror;
}
* Send an SMTP RCPT command.
* Sets the TO argument to $toaddr.
* Returns true if the recipient was accepted false if it was rejected.
- * Implements from rfc 821: RCPT <SP> TO:<forward-path> <CRLF>
+ * Implements from RFC 821: RCPT <SP> TO:<forward-path> <CRLF>.
+ *
* @param string $address The address the message is being sent to
- * @access public
- * @return boolean
+ * @param string $dsn Comma separated list of DSN notifications. NEVER, SUCCESS, FAILURE
+ * or DELAY. If you specify NEVER all other notifications are ignored.
+ *
+ * @return bool
*/
- public function recipient($address)
+ public function recipient($address, $dsn = '')
{
+ if (empty($dsn)) {
+ $rcpt = 'RCPT TO:<' . $address . '>';
+ } else {
+ $dsn = strtoupper($dsn);
+ $notify = [];
+
+ if (strpos($dsn, 'NEVER') !== false) {
+ $notify[] = 'NEVER';
+ } else {
+ foreach (['SUCCESS', 'FAILURE', 'DELAY'] as $value) {
+ if (strpos($dsn, $value) !== false) {
+ $notify[] = $value;
+ }
+ }
+ }
+
+ $rcpt = 'RCPT TO:<' . $address . '> NOTIFY=' . implode(',', $notify);
+ }
+
return $this->sendCommand(
'RCPT TO',
- 'RCPT TO:<' . $address . '>',
- array(250, 251)
+ $rcpt,
+ [250, 251]
);
}
/**
* Send an SMTP RSET command.
* Abort any transaction that is currently in progress.
- * Implements rfc 821: RSET <CRLF>
- * @access public
- * @return boolean True on success.
+ * Implements RFC 821: RSET <CRLF>.
+ *
+ * @return bool True on success
*/
public function reset()
{
/**
* Send a command to an SMTP server and check its return code.
- * @param string $command The command name - not sent to the server
- * @param string $commandstring The actual command to send
- * @param integer|array $expect One or more expected integer success codes
- * @access protected
- * @return boolean True on success.
+ *
+ * @param string $command The command name - not sent to the server
+ * @param string $commandstring The actual command to send
+ * @param int|array $expect One or more expected integer success codes
+ *
+ * @return bool True on success
*/
protected function sendCommand($command, $commandstring, $expect)
{
if (!$this->connected()) {
$this->setError("Called $command without being connected");
+
return false;
}
//Reject line breaks in all commands
- if (strpos($commandstring, "\n") !== false or strpos($commandstring, "\r") !== false) {
+ if ((strpos($commandstring, "\n") !== false) || (strpos($commandstring, "\r") !== false)) {
$this->setError("Command '$command' contained line breaks");
+
return false;
}
- $this->client_send($commandstring . self::CRLF);
+ $this->client_send($commandstring . static::LE, $command);
$this->last_reply = $this->get_lines();
// Fetch SMTP code and possible error code explanation
- $matches = array();
- if (preg_match("/^([0-9]{3})[ -](?:([0-9]\\.[0-9]\\.[0-9]) )?/", $this->last_reply, $matches)) {
- $code = $matches[1];
+ $matches = [];
+ if (preg_match('/^([\d]{3})[ -](?:([\d]\\.[\d]\\.[\d]{1,2}) )?/', $this->last_reply, $matches)) {
+ $code = (int) $matches[1];
$code_ex = (count($matches) > 2 ? $matches[2] : null);
// Cut off error code from each response line
$detail = preg_replace(
- "/{$code}[ -]".($code_ex ? str_replace('.', '\\.', $code_ex).' ' : '')."/m",
+ "/{$code}[ -]" .
+ ($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . '/m',
'',
$this->last_reply
);
} else {
// Fall back to simple parsing if regex fails
- $code = substr($this->last_reply, 0, 3);
+ $code = (int) substr($this->last_reply, 0, 3);
$code_ex = null;
$detail = substr($this->last_reply, 4);
}
$this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
- if (!in_array($code, (array)$expect)) {
+ if (!in_array($code, (array) $expect, true)) {
$this->setError(
"$command command failed",
$detail,
'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply,
self::DEBUG_CLIENT
);
+
return false;
}
$this->setError('');
+
return true;
}
* commands may be called followed by a data command. This command
* will send the message to the users terminal if they are logged
* in and send them an email.
- * Implements rfc 821: SAML <SP> FROM:<reverse-path> <CRLF>
+ * Implements RFC 821: SAML <SP> FROM:<reverse-path> <CRLF>.
+ *
* @param string $from The address the message is from
- * @access public
- * @return boolean
+ *
+ * @return bool
*/
public function sendAndMail($from)
{
/**
* Send an SMTP VRFY command.
+ *
* @param string $name The name to verify
- * @access public
- * @return boolean
+ *
+ * @return bool
*/
public function verify($name)
{
- return $this->sendCommand('VRFY', "VRFY $name", array(250, 251));
+ return $this->sendCommand('VRFY', "VRFY $name", [250, 251]);
}
/**
* Send an SMTP NOOP command.
- * Used to keep keep-alives alive, doesn't actually do anything
- * @access public
- * @return boolean
+ * Used to keep keep-alives alive, doesn't actually do anything.
+ *
+ * @return bool
*/
public function noop()
{
* Send an SMTP TURN command.
* This is an optional command for SMTP that this class does not support.
* This method is here to make the RFC821 Definition complete for this class
- * and _may_ be implemented in future
- * Implements from rfc 821: TURN <CRLF>
- * @access public
- * @return boolean
+ * and _may_ be implemented in future.
+ * Implements from RFC 821: TURN <CRLF>.
+ *
+ * @return bool
*/
public function turn()
{
$this->setError('The SMTP TURN command is not implemented');
$this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT);
+
return false;
}
/**
* Send raw data to the server.
- * @param string $data The data to send
- * @access public
- * @return integer|boolean The number of bytes sent to the server or false on error
+ *
+ * @param string $data The data to send
+ * @param string $command Optionally, the command this is part of, used only for controlling debug output
+ *
+ * @return int|bool The number of bytes sent to the server or false on error
*/
- public function client_send($data)
+ public function client_send($data, $command = '')
{
- $this->edebug("CLIENT -> SERVER: $data", self::DEBUG_CLIENT);
- return fwrite($this->smtp_conn, $data);
+ //If SMTP transcripts are left enabled, or debug output is posted online
+ //it can leak credentials, so hide credentials in all but lowest level
+ if (self::DEBUG_LOWLEVEL > $this->do_debug &&
+ in_array($command, ['User & Password', 'Username', 'Password'], true)) {
+ $this->edebug('CLIENT -> SERVER: [credentials hidden]', self::DEBUG_CLIENT);
+ } else {
+ $this->edebug('CLIENT -> SERVER: ' . $data, self::DEBUG_CLIENT);
+ }
+ set_error_handler([$this, 'errorHandler']);
+ $result = fwrite($this->smtp_conn, $data);
+ restore_error_handler();
+
+ return $result;
}
/**
* Get the latest error.
- * @access public
+ *
* @return array
*/
public function getError()
}
/**
- * Get SMTP extensions available on the server
- * @access public
+ * Get SMTP extensions available on the server.
+ *
* @return array|null
*/
public function getServerExtList()
}
/**
- * A multipurpose method
- * The method works in three ways, dependent on argument value and current state
- * 1. HELO/EHLO was not sent - returns null and set up $this->error
- * 2. HELO was sent
- * $name = 'HELO': returns server name
- * $name = 'EHLO': returns boolean false
- * $name = any string: returns null and set up $this->error
- * 3. EHLO was sent
- * $name = 'HELO'|'EHLO': returns server name
- * $name = any string: if extension $name exists, returns boolean True
- * or its options. Otherwise returns boolean False
- * In other words, one can use this method to detect 3 conditions:
- * - null returned: handshake was not or we don't know about ext (refer to $this->error)
- * - false returned: the requested feature exactly not exists
- * - positive value returned: the requested feature exists
+ * Get metadata about the SMTP server from its HELO/EHLO response.
+ * The method works in three ways, dependent on argument value and current state:
+ * 1. HELO/EHLO has not been sent - returns null and populates $this->error.
+ * 2. HELO has been sent -
+ * $name == 'HELO': returns server name
+ * $name == 'EHLO': returns boolean false
+ * $name == any other string: returns null and populates $this->error
+ * 3. EHLO has been sent -
+ * $name == 'HELO'|'EHLO': returns the server name
+ * $name == any other string: if extension $name exists, returns True
+ * or its options (e.g. AUTH mechanisms supported). Otherwise returns False.
+ *
* @param string $name Name of SMTP extension or 'HELO'|'EHLO'
- * @return mixed
+ *
+ * @return string|bool|null
*/
public function getServerExt($name)
{
if (!$this->server_caps) {
$this->setError('No HELO/EHLO was sent');
- return null;
+
+ return;
}
- // the tight logic knot ;)
if (!array_key_exists($name, $this->server_caps)) {
- if ($name == 'HELO') {
+ if ('HELO' === $name) {
return $this->server_caps['EHLO'];
}
- if ($name == 'EHLO' || array_key_exists('EHLO', $this->server_caps)) {
+ if ('EHLO' === $name || array_key_exists('EHLO', $this->server_caps)) {
return false;
}
- $this->setError('HELO handshake was used. Client knows nothing about server extensions');
- return null;
+ $this->setError('HELO handshake was used; No information about server extensions available');
+
+ return;
}
return $this->server_caps[$name];
/**
* Get the last reply from the server.
- * @access public
+ *
* @return string
*/
public function getLastReply()
* With SMTP we can tell if we have more lines to read if the
* 4th character is '-' symbol. If it is a space then we don't
* need to read anything else.
- * @access protected
+ *
* @return string
*/
protected function get_lines()
if ($this->Timelimit > 0) {
$endtime = time() + $this->Timelimit;
}
+ $selR = [$this->smtp_conn];
+ $selW = null;
while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) {
- $str = @fgets($this->smtp_conn, 515);
- $this->edebug("SMTP -> get_lines(): \$data is \"$data\"", self::DEBUG_LOWLEVEL);
- $this->edebug("SMTP -> get_lines(): \$str is \"$str\"", self::DEBUG_LOWLEVEL);
+ //Must pass vars in here as params are by reference
+ if (!stream_select($selR, $selW, $selW, $this->Timelimit)) {
+ $this->edebug(
+ 'SMTP -> get_lines(): timed-out (' . $this->Timeout . ' sec)',
+ self::DEBUG_LOWLEVEL
+ );
+ break;
+ }
+ //Deliberate noise suppression - errors are handled afterwards
+ $str = @fgets($this->smtp_conn, self::MAX_REPLY_LENGTH);
+ $this->edebug('SMTP INBOUND: "' . trim($str) . '"', self::DEBUG_LOWLEVEL);
$data .= $str;
- // If 4th character is a space, we are done reading, break the loop, micro-optimisation over strlen
- if ((isset($str[3]) and $str[3] == ' ')) {
+ // If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled),
+ // or 4th character is a space or a line break char, we are done reading, break the loop.
+ // String array access is a significant micro-optimisation over strlen
+ if (!isset($str[3]) || $str[3] === ' ' || $str[3] === "\r" || $str[3] === "\n") {
break;
}
// Timed-out? Log and break
break;
}
// Now check if reads took too long
- if ($endtime and time() > $endtime) {
+ if ($endtime && time() > $endtime) {
$this->edebug(
- 'SMTP -> get_lines(): timelimit reached ('.
+ 'SMTP -> get_lines(): timelimit reached (' .
$this->Timelimit . ' sec)',
self::DEBUG_LOWLEVEL
);
break;
}
}
+
return $data;
}
/**
* Enable or disable VERP address generation.
- * @param boolean $enabled
+ *
+ * @param bool $enabled
*/
public function setVerp($enabled = false)
{
/**
* Get VERP address generation mode.
- * @return boolean
+ *
+ * @return bool
*/
public function getVerp()
{
/**
* Set error messages and codes.
- * @param string $message The error message
- * @param string $detail Further detail on the error
- * @param string $smtp_code An associated SMTP error code
+ *
+ * @param string $message The error message
+ * @param string $detail Further detail on the error
+ * @param string $smtp_code An associated SMTP error code
* @param string $smtp_code_ex Extended SMTP code
*/
protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '')
{
- $this->error = array(
+ $this->error = [
'error' => $message,
'detail' => $detail,
'smtp_code' => $smtp_code,
- 'smtp_code_ex' => $smtp_code_ex
- );
+ 'smtp_code_ex' => $smtp_code_ex,
+ ];
}
/**
* Set debug output method.
- * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it.
+ *
+ * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it
*/
public function setDebugOutput($method = 'echo')
{
/**
* Get debug output method.
+ *
* @return string
*/
public function getDebugOutput()
/**
* Set debug output level.
- * @param integer $level
+ *
+ * @param int $level
*/
public function setDebugLevel($level = 0)
{
/**
* Get debug output level.
- * @return integer
+ *
+ * @return int
*/
public function getDebugLevel()
{
/**
* Set SMTP timeout.
- * @param integer $timeout
+ *
+ * @param int $timeout The timeout duration in seconds
*/
public function setTimeout($timeout = 0)
{
/**
* Get SMTP timeout.
- * @return integer
+ *
+ * @return int
*/
public function getTimeout()
{
return $this->Timeout;
}
+
+ /**
+ * Reports an error number and string.
+ *
+ * @param int $errno The error number returned by PHP
+ * @param string $errmsg The error message returned by PHP
+ * @param string $errfile The file the error occurred in
+ * @param int $errline The line number the error occurred on
+ */
+ protected function errorHandler($errno, $errmsg, $errfile = '', $errline = 0)
+ {
+ $notice = 'Connection failed.';
+ $this->setError(
+ $notice,
+ $errmsg,
+ (string) $errno
+ );
+ $this->edebug(
+ "$notice Error #$errno: $errmsg [$errfile line $errline]",
+ self::DEBUG_CONNECTION
+ );
+ }
+
+ /**
+ * Extract and return the ID of the last SMTP transaction based on
+ * a list of patterns provided in SMTP::$smtp_transaction_id_patterns.
+ * Relies on the host providing the ID in response to a DATA command.
+ * If no reply has been received yet, it will return null.
+ * If no pattern was matched, it will return false.
+ *
+ * @return bool|string|null
+ */
+ protected function recordLastTransactionID()
+ {
+ $reply = $this->getLastReply();
+
+ if (empty($reply)) {
+ $this->last_smtp_transaction_id = null;
+ } else {
+ $this->last_smtp_transaction_id = false;
+ foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) {
+ if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) {
+ $this->last_smtp_transaction_id = trim($matches[1]);
+ break;
+ }
+ }
+ }
+
+ return $this->last_smtp_transaction_id;
+ }
+
+ /**
+ * Get the queue/transaction ID of the last SMTP transaction
+ * If no reply has been received yet, it will return null.
+ * If no pattern was matched, it will return false.
+ *
+ * @return bool|string|null
+ *
+ * @see recordLastTransactionID()
+ */
+ public function getLastTransactionID()
+ {
+ return $this->last_smtp_transaction_id;
+ }
}