PHPMailer機能の拡張

こんにちは

おそらく、SMTPを介してPHPコードからメールを送信する必要があった人は誰でも、 PHPMailerクラスに精通しているでしょう

この記事では、追加パラメーターとして送信したいネットワークインターフェースのIPアドレスを受け入れるようにPHPMailerに教える方法について説明します。 当然、この機能は複数の白いIPアドレスを持つサーバーでのみ役立ちます。 また、ちょっとした追加として、PHPMailerコードからのやや不快なバグをキャッチします。



PHPMailerアーキテクチャの概要



PHPMailerパッケージは、同じ名前のフロントエンド(PHPMailerクラス)と、POP3事前認証など、SMTP経由でメールを送信する機能を実装するいくつかのプラグインクラスで構成されています。



PHPMailerフロントエンドは、メッセージパラメータ(localhost、return-path、AddAdress()、body、fromなど)を設定し、送信方法と認証方法(SMTPSecure、SMTPAuth、IsMail()、IsSendMail()、IsSMTP( )など)、およびSend()メソッド。



レターのパラメーターを設定し、送信方法(mail、sendmail、qmail、smtpから選択可能)を指定したら、PHPMailer Send()クラスメソッドを呼び出す必要があります。このメソッドは、何らかの方法でメールを送信する内部メソッドに呼び出しを委任します。 。 SMTPに興味があるため、主にclass.smtp.phpファイルのSMTPプラグインを検討します。



PHPMailer :: IsSMTP()メソッドを使用する場合、PHPMailer :: Send()メソッドは、保護されたPHPMailer :: SmtpSend($ header、$ body)メソッドを呼び出し、生成されたヘッダーとメッセージ本文を渡します。



PHPMailer :: SmtpSend()メソッドは、受信者のリモートSMTPサーバーへの接続を試み(これがPHPMailerオブジェクトによって送信された最初のメッセージでない場合、おそらく接続が既に確立されており、このステップはスキップされます)、標準SMTPセッションを開始します(HELLO / EHLO、MAIL TO、RCPT、DATAなど)。



SMTPサーバーへの接続は、パブリックメソッドPHPMailer :: SmtpConnect()で行われます。 一度に1つのドメインに対して異なる優先順位を持つ複数のMXレコードが存在する可能性があるため、PHPMailer :: SmtpConnect()メソッドは、PHPMailerの構成中に指定された各SMTPサーバーに順番に接続しようとします。



コードのバグ



次に、PHPMailer :: SmtpConnect()コードをよく見てください。

/** * 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; }
      
      





コードで$ this-> smtpはSMTPプラグインクラスのオブジェクトです。



著者が何を念頭に置いていたかを理解しようとします。 まず、SMTPで機能する内部オブジェクトが作成されているかどうかを確認し、これがPHPMailerクラスのオブジェクトのSmtpConnect()メソッドの最初の呼び出しである場合に作成されます(実際、PHPMailer :: Close()メソッドは$ this-> smtpに変換できますnull)。



次に、PHPMailer :: Hostフィールドは、区切り文字「;」で分割されます 結果は、受信者ドメインのMXレコードの配列です。 Hostにエントリが1つしかない場合(「smtp.yandex.ru」など)、配列には要素が1つしかありません。



次に、受信者サーバーに既に接続されているかどうかを確認します。 これがSmtpConnect()の最初の呼び出しである場合、$ connectionがfalseであることは明らかです。



だから、私たちは最も興味深いことに到達しました。 サイクルはすべてのMXレコードで開始され、各反復で次のMXへの接続が試行されます。 しかし、このサイクルのアルゴリズムを頭の中で実行すると、最初のMXレコードについて次のように想像するとどうなりますか?($ this-> smtp-> Connect(($ ssl? 'Ssl://': '')。$ Host、$ port、$ this-> Timeout))falseを返しましたか? サイクルは、サイクルの後に既にキャッチされる例外をスローすることがわかります。 つまり 他のすべてのMXレコードの可用性はチェックされず、例外がキャッチされます。



しかし、これは最も不快ではありません。 PHPMailerは2つのモードで動作します-例外をスローするか、ErrorInfoフィールドにエラーメッセージを書き込むことで静かに死にます。 サイレントモードを使用する場合($ this-> exceptions == false、これがデフォルトモード)、SmtpConnect()はtrueを返します!



一般に、このバグには時間がかかり、開発者に通知されます。 バージョン5.2.1で気付きましたが、古いバージョンでも同じように動作します。



先に進む前に、クイックフィックスを紹介します。 開発者からの公式リリースまで、私は彼と一緒に住んでいます。 今月は飛行が正常です。

 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; }
      
      







PHPMailerを拡張して、複数のネットワークインターフェイスで動作するようにします



PHPMailerのSMTPプラグインは、fsockopen、fputs、fgetsを介してネットワークで動作します。 マシン上にインターネットに接続するネットワークインターフェイスが複数ある場合、fsockopenはいずれの場合も最初の接続でソケットを作成します。 何でも作成できる必要があります。



最初に思いついたのは、標準的なソケットsocket_create、socket_bind、socket_connectの標準的な束を使用することでした。これにより、socket_bindで、IPアドレスにソケットを関連付けるネットワークインターフェイスを指定できます。 結局のところ、このアイデアは完全に成功しているわけではありません。 その結果、fputsとfgetsはsocket_createによって作成されたリソースを使用できないため、PHPMailer SMTPプラグインのほぼ全体を書き換えて、fputsとfgetsをsocket_readとsocket_writeに置き換えなければなりませんでした。 獲得したが、魂は堆積物のままだった。



次の考えがより成功したことが判明しました。 fgetsが安全に読み取れるストリームソケットを作成するstream_socket_client関数があります! その結果、SMTPプラグインの1つのメソッドを置き換えるだけで、PHPMailerにネットワークインターフェースの明示的な指示でメールを送信するように教えることができ、同時に実際には開発者のコ​​ードに手を触れません。



プラグインは次のとおりです。

 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; } }
      
      







実際、Connect()メソッドの実装も最小限に変更されています。 ソケットを直接作成する文字列のみが置き換えられ、署名に別のパラメーター(ネットワークインターフェイスのIPアドレス)が追加されます。



このプラグインを使用するには、PHPMailerクラスを次のように拡張する必要があります。

 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; } }
      
      







MultipleInterfaceMailerクラスに新しいパブリックIpフィールドが追加されました。これは、メールを送信するネットワークインターフェイスのIPアドレスの文字列表現によって設定する必要があります。 また、IsSMTPX()メソッドが追加され、新しいプラグインを使用して文字を送信する必要があることを示しています。 PostSend()、SmtpSend()、およびSmtpConnect()メソッドも、SMTPXプラグインを使用するためにやり直されました。 この場合、使用手順もクラスインターフェイスも変更されていないため、MultipleInterfaceMailerクラスのオブジェクトを既存のクライアントコードで安全に使用できます。たとえば、sendmailまたは元のSMTPプラグインを介してメールを送信します。



以下は、新しいクラスを使用する小さな例です。

 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();
      
      







おわりに



要約すると:

  1. PHPMailerのバグを修正しました。これは、SMTPサーバーへの接続に失敗した場合でも、SmtpConnect()が常にtrueを返すためです。
  2. SmtpConnect()は、最初に成功する前に渡されたすべてのMXレコードを正直にチェックし始めました。
  3. 新しいプラグインが作成されました。このプラグインを使用すると、SMTP経由でメールを送信し、使用する送信サーバーのネットワークインターフェイスを明示的に指定できます。
  4. PHPMailerは、古いクライアントコードが新しいSMTPXプラグインを使用するために簡単に拡張されます。


お友達、頑張ってください!



All Articles