📜 ⬆️ ⬇️

Expanding PHPMailer

Good day!
Probably everyone who had to send mail from the PHP code via SMTP is familiar with the PHPMailer class.
In this article, I will tell you how to teach PHPMailer to take as an additional parameter the IP address of the network interface from which we want to send. Naturally, this feature will be useful only on servers with several white IP addresses. And as a small addition, we will catch a rather unpleasant bug from the PHPMailer code.

PHPMailer Architecture Overview


The PHPMailer package consists of the frontend of the same name (the PHPMailer class) and several plug-in classes that implement the ability to send mail via SMTP protocol, including with POP3 pre-authentication.

Front-end PHPMailer provides fields and methods for setting email parameters (localhost, return-path, AddAdress (), body, from, etc.), choosing the sending method and authentication method (SMTPSecure, SMTPAuth, IsMail (), IsSendMail (), IsSMTP ( ), etc.), as well as the Send () method.
')
After setting the letter parameters and specifying the sending method (it is possible to choose from the following: mail, sendmail, qmail or smtp), you must call the class method PHPMailer Send (), which, in turn, delegates the call to an internal method responsible for sending mail . Since we are interested in exactly SMTP, then we will mainly consider the SMTP plugin from the class.smtp.php file.

When using the PHPMailer :: IsSMTP () method, the PHPMailer :: Send () method will call the protected PHPMailer :: SmtpSend ($ header, $ body) method, passing the generated headers and message body to it.

The PHPMailer :: SmtpSend () method will attempt to connect to the recipient's remote SMTP server (if this is not the first time the letter has been sent by the PHPMailer object, then the connection is most likely already established and this step will be skipped) and initiate a standard SMTP session with it (HELLO / EHLO, MAIL TO, RCPT, DATA, etc.).

The connection to the SMTP server takes place in the public method PHPMailer :: SmtpConnect (). Since there can be several MX records with different priorities for a single domain, the PHPMailer :: SmtpConnect () method will try to consistently connect to each of the SMTP servers specified when configuring PHPMailer.

Bug in code


And now let's take a close look at the PHPMailer :: SmtpConnect () code:
/** * Initiates a connection to an SMTP server. * Returns false if the operation failed. * @uses SMTP * @access public * @return bool */ public function SmtpConnect() { if(is_null($this->smtp)) { $this->smtp = new SMTP(); } $this->smtp->do_debug = $this->SMTPDebug; $hosts = explode(';', $this->Host); $index = 0; $connection = $this->smtp->Connected(); // Retry while there is no connection try { while($index < count($hosts) && !$connection) { $hostinfo = array(); if (preg_match('/^(.+):([0-9]+)$/', $hosts[$index], $hostinfo)) { $host = $hostinfo[1]; $port = $hostinfo[2]; } else { $host = $hosts[$index]; $port = $this->Port; } $tls = ($this->SMTPSecure == 'tls'); $ssl = ($this->SMTPSecure == 'ssl'); if ($this->smtp->Connect(($ssl ? 'ssl://':'').$host, $port, $this->Timeout)) { $hello = ($this->Helo != '' ? $this->Helo : $this->ServerHostname()); $this->smtp->Hello($hello); if ($tls) { if (!$this->smtp->StartTLS()) { throw new phpmailerException($this->Lang('tls')); } //We must resend HELO after tls negotiation $this->smtp->Hello($hello); } $connection = true; if ($this->SMTPAuth) { if (!$this->smtp->Authenticate($this->Username, $this->Password)) { throw new phpmailerException($this->Lang('authenticate')); } } } $index++; if (!$connection) { throw new phpmailerException($this->Lang('connect_host')); } } } catch (phpmailerException $e) { $this->smtp->Reset(); if ($this->exceptions) { throw $e; } } return true; } 

In the $ this-> code, smtp is an object of the SMTP plugin class.

We will try to figure out what the authors had in mind. First, a check is made whether an internal object that can work with SMTP is created and is created if this is the first call to the SmtpConnect () method of an object of the PHPMailer class (in fact, the PHPMailer :: Close () method can turn $ this-> smtp into null).

Then the PHPMailer :: Host field is broken by the delimiter ';' and the result is an array of MX records for the recipient domain. If there was only one entry in Host (for example, 'smtp.yandex.ru'), then there will be only one element in the array.

Next, it checks whether we are already connected to the recipient's server. If this is the first SmtpConnect () call, then obviously $ connection will be false.

So we got to the most interesting. A cycle starts over all MX records, each iteration of which attempts to connect to the next MX. But what will happen if you execute the algorithm of this cycle in your head, imagining that for the first MX record if ($ this-> smtp-> Connect (($ ssl? 'Ssl: //': ''). $ Host, $ port, $ this-> Timeout)) returned false? It turns out that the cycle will throw an exception, which will be intercepted already after the cycle. Those. all other MX records will not be checked for availability and we will catch an exception.

But this is not the most unpleasant. PHPMailer can work in two modes - throwing exceptions, or silently die with writing an error message in the ErrorInfo field. So in the case of using the silent mode ($ this-> exceptions == false, and this is the default mode) SmtpConnect () returns true!

In general, this bug took me some time, the developers are notified about it. I noticed it in version 5.2.1, but older versions behave the same way.

Before moving on, I will introduce my quick fix. Before the release of the official fix from the developers, I live with him. Already a month the flight is normal.
 public function SmtpConnect() { if(is_null($this->smtp)) { $this->smtp = new SMTP(); } $this->smtp->do_debug = $this->SMTPDebug; $hosts = explode(';', $this->Host); $index = 0; $connection = $this->smtp->Connected(); // Retry while there is no connection try { while($index < count($hosts) && !$connection) { $hostinfo = array(); if (preg_match('/^(.+):([0-9]+)$/', $hosts[$index], $hostinfo)) { $host = $hostinfo[1]; $port = $hostinfo[2]; } else { $host = $hosts[$index]; $port = $this->Port; } $tls = ($this->SMTPSecure == 'tls'); $ssl = ($this->SMTPSecure == 'ssl'); $bRetVal = $this->smtp->Connect(($ssl ? 'ssl://':'').$host, $port, $this->Timeout); if ($bRetVal) { $hello = ($this->Helo != '' ? $this->Helo : $this->ServerHostname()); $this->smtp->Hello($hello); if ($tls) { if (!$this->smtp->StartTLS()) { throw new phpmailerException($this->Lang('tls')); } //We must resend HELO after tls negotiation $this->smtp->Hello($hello); } if ($this->SMTPAuth) { if (!$this->smtp->Authenticate($this->Username, $this->Password)) { throw new phpmailerException($this->Lang('authenticate')); } } $connection = true; break; } $index++; } if (!$connection) { throw new phpmailerException($this->Lang('connect_host')); } } catch (phpmailerException $e) { $this->SetError($e->getMessage()); if ($this->smtp->Connected()) $this->smtp->Reset(); if ($this->exceptions) { throw $e; } return false; } return true; } 


Expanding PHPMailer to work with multiple network interfaces


The SMTP PHPMailer plugin works with the network through fsockopen, fputs and fgets. If our machine has several network interfaces looking to the Internet, fsockopen will in any case create a socket on the first connection. We need to be able to create on any.

The first thought that came to mind is to use a standard bundle of classic sockets socket_create, socket_bind, socket_connect, which in socket_bind allows you to specify which network interface to connect the socket to by specifying its IP address. As it turned out, the idea is not entirely successful. As a result, almost the entire PHPMailer SMTP plugin had to be rewritten, replacing fputs and fgets with socket_read and socket_write, because fputs and fgets do not know how to work with the resource created by socket_create. Earned, but the soul remained sediment.

The next thought was better. There is a stream_socket_client function that creates a stream socket that can be safely read with fgets. As a result, by replacing just one method in the SMTP plugin, you can teach PHPMailer to send mail with an explicit indication of the network interface, and at the same time not to touch the developers code.

Our plugin looks like this:
 require_once 'class.smtp.php'; class SMTPX extends SMTP { public function __construct() { parent::__construct(); } public function Connect($host, $port = 0, $tval = 30, $local_ip) { // set the error val to null so there is no confusion $this->error = null; // make sure we are __not__ connected if($this->connected()) { // already connected, generate error $this->error = array("error" => "Already connected to a server"); return false; } if(empty($port)) { $port = $this->SMTP_PORT; } $opts = array( 'socket' => array( 'bindto' => "$local_ip:0", ), ); // create the context... $context = stream_context_create($opts); // connect to the smtp server $this->smtp_conn = @stream_socket_client($host.':'.$port, $errno, $errstr, $tval, // give up after ? secs STREAM_CLIENT_CONNECT, $context); // verify we connected properly if(empty($this->smtp_conn)) { $this->error = array("error" => "Failed to connect to server", "errno" => $errno, "errstr" => $errstr); if($this->do_debug >= 1) { echo "SMTP -> ERROR: " . $this->error["error"] . ": $errstr ($errno)" . $this->CRLF . '<br />'; } return false; } // 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") socket_set_timeout($this->smtp_conn, $tval, 0); // get any announcement $announce = $this->get_lines(); if($this->do_debug >= 2) { echo "SMTP -> FROM SERVER:" . $announce . $this->CRLF . '<br />'; } return true; } } 


In fact, the implementation of the Connect () method has also changed minimally. Only the strings that create the socket directly are replaced and one more parameter is added to the signature - the IP address of the network interface.

To use this plugin, you need to extend the PHPMailer class as follows:
 require_once 'class.phpmailer.php'; class MultipleInterfaceMailer extends PHPMailer { /** * IP   ,    *    SMTP-. *      SMTPX. * @var string */ public $Ip = ''; public function __construct($exceptions = false) { parent::__construct($exceptions); } /** *      SMTPX. * @param string $ip IP       . */ public function IsSMTPX($ip = '') { if ('' !== $ip) $this->Ip = $ip; $this->Mailer = 'smtpx'; } protected function PostSend() { if ('smtpx' == $this->Mailer) { $this->SmtpSend($this->MIMEHeader, $this->MIMEBody); return; } parent::PostSend(); } /** *  ,       * IP    . * @param string $header The message headers * @param string $body The message body * @uses SMTP * @access protected * @return bool */ protected function SmtpSend($header, $body) { require_once $this->PluginDir . 'class.smtpx.php'; $bad_rcpt = array(); if(!$this->SmtpConnect()) { throw new phpmailerException($this->Lang('connect_host'), self::STOP_CRITICAL); } $smtp_from = ($this->Sender == '') ? $this->From : $this->Sender; if(!$this->smtp->Mail($smtp_from)) { throw new phpmailerException($this->Lang('from_failed') . $smtp_from, self::STOP_CRITICAL); } // Attempt to send attach all recipients foreach($this->to as $to) { if (!$this->smtp->Recipient($to[0])) { $bad_rcpt[] = $to[0]; // implement call back function if it exists $isSent = 0; $this->doCallback($isSent, $to[0], '', '', $this->Subject, $body); } else { // implement call back function if it exists $isSent = 1; $this->doCallback($isSent, $to[0], '', '', $this->Subject, $body); } } foreach($this->cc as $cc) { if (!$this->smtp->Recipient($cc[0])) { $bad_rcpt[] = $cc[0]; // implement call back function if it exists $isSent = 0; $this->doCallback($isSent, '', $cc[0], '', $this->Subject, $body); } else { // implement call back function if it exists $isSent = 1; $this->doCallback($isSent, '', $cc[0], '', $this->Subject, $body); } } foreach($this->bcc as $bcc) { if (!$this->smtp->Recipient($bcc[0])) { $bad_rcpt[] = $bcc[0]; // implement call back function if it exists $isSent = 0; $this->doCallback($isSent, '', '', $bcc[0], $this->Subject, $body); } else { // implement call back function if it exists $isSent = 1; $this->doCallback($isSent, '', '', $bcc[0], $this->Subject, $body); } } if (count($bad_rcpt) > 0 ) { //Create error message for any bad addresses $badaddresses = implode(', ', $bad_rcpt); throw new phpmailerException($this->Lang('recipients_failed') . $badaddresses); } if(!$this->smtp->Data($header . $body)) { throw new phpmailerException($this->Lang('data_not_accepted'), self::STOP_CRITICAL); } if($this->SMTPKeepAlive == true) { $this->smtp->Reset(); } return true; } /** *  ,   PHPMailer  *    SMTPX. * @uses SMTP * @access public * @return bool */ public function SmtpConnect() { if(is_null($this->smtp) || !($this->smtp instanceof SMTPX)) { $this->smtp = new SMTPX(); } $this->smtp->do_debug = $this->SMTPDebug; $hosts = explode(';', $this->Host); $index = 0; $connection = $this->smtp->Connected(); // Retry while there is no connection try { while($index < count($hosts) && !$connection) { $hostinfo = array(); if (preg_match('/^(.+):([0-9]+)$/', $hosts[$index], $hostinfo)) { $host = $hostinfo[1]; $port = $hostinfo[2]; } else { $host = $hosts[$index]; $port = $this->Port; } $tls = ($this->SMTPSecure == 'tls'); $ssl = ($this->SMTPSecure == 'ssl'); $bRetVal = $this->smtp->Connect(($ssl ? 'ssl://':'').$host, $port, $this->Timeout, $this->Ip); if ($bRetVal) { $hello = ($this->Helo != '' ? $this->Helo : $this->ServerHostname()); $this->smtp->Hello($hello); if ($tls) { if (!$this->smtp->StartTLS()) { throw new phpmailerException($this->Lang('tls')); } //We must resend HELO after tls negotiation $this->smtp->Hello($hello); } if ($this->SMTPAuth) { if (!$this->smtp->Authenticate($this->Username, $this->Password)) { throw new phpmailerException($this->Lang('authenticate')); } } $connection = true; break; } $index++; } if (!$connection) { throw new phpmailerException($this->Lang('connect_host')); } } catch (phpmailerException $e) { $this->SetError($e->getMessage()); if ($this->smtp->Connected()) $this->smtp->Reset(); if ($this->exceptions) { throw $e; } return false; } return true; } } 


A new open Ip field has been added to the MultipleInterfaceMailer class, which should be set to a string representation of the IP address of the network interface from which we want to send mail. The IsSMTPX () method has also been added, indicating that letters should be sent using a new plug-in. The PostSend (), SmtpSend () and SmtpConnect () methods are also redesigned to use the SMTPX plugin. At the same time, objects of the MultipleInterfaceMailer class can be safely used with existing client code, which, for example, sends mail via sendmail or through the original SMTP plugin, since neither the usage procedure nor the class interface has changed.

Next, a small example of using a new class:
 function getSmtpHostsByDomain($sRcptDomain) { if (getmxrr($sRcptDomain, $aMxRecords, $aMxWeights)) { if (count($aMxRecords) > 0) { for ($i = 0; $i < count($aMxRecords); ++$i) { $mxs[$aMxRecords[$i]] = $aMxWeights[$i]; } asort($mxs); $aSortedMxRecords = array_keys($mxs); $sResult = ''; foreach ($aSortedMxRecords as $r) { $sResult .= $r . ';'; } return $sResult; } } // getmxrr    ,   DNS, //,  RFC 2821,      , //   $sRcptDomain      // 0. return $sRcptDomain; } require 'MultipleInterfaceMailer.php'; $mailer = new MultipleInterfaceMailer(true); $mailer->IsSMTPX('192.168.1.1'); //   IP    //$mailer->IsSMTP();      $mailer->Host = getSmtpHostsByDomain('email.net'); $mailer->Body = 'blah-blah'; $mailer->From ='no-replay@yourdomain.net'; $mailer->AddAddress('sucreface@email.net'); $mailer->Send(); 


Conclusion


Let's summarize:
  1. Fixed a bug in PHPMailer, because of which SmtpConnect () always returned true, even in case of unsuccessful attempt to connect to the SMTP server.
  2. SmtpConnect () began to honestly check all MX records transferred to it before the first successful attempt.
  3. A new plugin has been written, with which you can send mail via SMTP explicitly indicating which network interface of the sending server to use.
  4. PHPMailer is painlessly expanded for old client code to use the new SMTPX plugin.

Good luck in your endeavors, friends!

Source: https://habr.com/ru/post/138878/


All Articles