: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
return $this->sendCommand('XCLIENT', 'XCLIENT' . $xclient_options, 250);
* Send an SMTP RSET command.
* Abort any transaction that is currently in progress.
* Implements RFC 821: RSET <CRLF>.
* @return bool True on success
return $this->sendCommand('RSET', 'RSET', 250);
* 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 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");
//Reject line breaks in all commands
if ((strpos($commandstring, "\n") !== false) || (strpos($commandstring, "\r") !== false)) {
$this->setError("Command '$command' contained line breaks");
$this->client_send($commandstring . static::LE, $command);
$this->last_reply = $this->get_lines();
//Fetch SMTP code and possible error code explanation
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
($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . '/m',
//Fall back to simple parsing if regex fails
$code = (int) substr($this->last_reply, 0, 3);
$detail = substr($this->last_reply, 4);
$this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
if (!in_array($code, (array) $expect, true)) {
"$command command failed",
'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply,
//Don't clear the error store when using keepalive
if ($command !== 'RSET') {
* Send an SMTP SAML command.
* Starts a mail transaction from the email address specified in $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. 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>.
* @param string $from The address the message is from
public function sendAndMail($from)
return $this->sendCommand('SAML', "SAML FROM:$from", 250);
* Send an SMTP VRFY command.
* @param string $name The name to verify
public function verify($name)
return $this->sendCommand('VRFY', "VRFY $name", [250, 251]);
* Send an SMTP NOOP command.
* Used to keep keep-alives alive, doesn't actually do anything.
return $this->sendCommand('NOOP', 'NOOP', 250);
* 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>.
$this->setError('The SMTP TURN command is not implemented');
$this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT);
* Send raw data to the server.
* @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, $command = '')
//If SMTP transcripts are left enabled, or debug output is posted online
//it can leak credentials, so hide credentials in all but lowest level
self::DEBUG_LOWLEVEL > $this->do_debug &&
in_array($command, ['User & Password', 'Username', 'Password'], true)
$this->edebug('CLIENT -> SERVER: [credentials hidden]', self::DEBUG_CLIENT);
$this->edebug('CLIENT -> SERVER: ' . $data, self::DEBUG_CLIENT);
set_error_handler([$this, 'errorHandler']);
$result = fwrite($this->smtp_conn, $data);
public function getError()
* Get SMTP extensions available on the server.
public function getServerExtList()
return $this->server_caps;
* 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 string|bool|null
public function getServerExt($name)
if (!$this->server_caps) {
$this->setError('No HELO/EHLO was sent');
if (!array_key_exists($name, $this->server_caps)) {
return $this->server_caps['EHLO'];
if ('EHLO' === $name || array_key_exists('EHLO', $this->server_caps)) {
$this->setError('HELO handshake was used; No information about server extensions available');
return $this->server_caps[$name];
* Get the last reply from the server.
public function getLastReply()
return $this->last_reply;
* Read the SMTP server's response.
* Either before eof or socket timeout occurs on the operation.
* 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.
protected function get_lines()
//If the connection is bad, give up straight away
if (!is_resource($this->smtp_conn)) {
stream_set_timeout($this->smtp_conn, $this->Timeout);
if ($this->Timelimit > 0) {
$endtime = time() + $this->Timelimit;
$selR = [$this->smtp_conn];
while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) {
//Must pass vars in here as params are by reference
//solution for signals inspired by https://github.com/symfony/symfony/pull/6540
set_error_handler([$this, 'errorHandler']);
$n = stream_select($selR, $selW, $selW, $this->Timelimit);
$message = $this->getError()['detail'];
'SMTP -> get_lines(): select failed (' . $message . ')',
//stream_select returns false when the `select` system call is interrupted
//by an incoming signal, try the select again
if (stripos($message, 'interrupted system call') !== false) {
'SMTP -> get_lines(): retrying stream_select',
'SMTP -> get_lines(): select timed-out in (' . $this->Timelimit . ' sec)',
//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);
//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") {
//Timed-out? Log and break
$info = stream_get_meta_data($this->smtp_conn);
if ($info['timed_out']) {
'SMTP -> get_lines(): stream timed-out (' . $this->Timeout . ' sec)',
//Now check if reads took too long
if ($endtime && time() > $endtime) {
'SMTP -> get_lines(): timelimit reached (' .
$this->Timelimit . ' sec)',
* Enable or disable VERP address generation.
public function setVerp($enabled = false)
$this->do_verp = $enabled;
* Get VERP address generation mode.
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 $smtp_code_ex Extended SMTP code
protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '')
'smtp_code' => $smtp_code,
'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
public function setDebugOutput($method = 'echo')
$this->Debugoutput = $method;
* Get debug output method.
public function getDebugOutput()
return $this->Debugoutput;
* Set debug output level.
public function setDebugLevel($level = 0)
$this->do_debug = $level;
* Get debug output level.
public function getDebugLevel()
* @param int $timeout The timeout duration in seconds
public function setTimeout($timeout = 0)
$this->Timeout = $timeout;
public function getTimeout()
* 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.';
"$notice Error #$errno: $errmsg [$errfile line $errline]",
* 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();
$this->last_smtp_transaction_id = null;
$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]);
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;