1. 程式人生 > >從零構建SMTP郵件傳送類

從零構建SMTP郵件傳送類

從零構建SMTP郵件傳送類

SMTP(Simple Mail Transfer Protocol,簡單郵件傳輸協議)是由原地址到目的地址傳送郵件的一組規則,用來控制信件的中轉方式。SMTP協議屬於TCP/IP協議簇,其使每臺計算機在傳送或中轉信件時能到找下一個目的地。通過使用指定的伺服器,把E-mail寄到收件人的伺服器上。

SMTP連線和傳送過程如下:

  1. 建立TCP連線。
  2. 客戶端傳送HELO命令標識發件人自己的身份,客戶端傳送MAIL命令。伺服器以OK作為響應,表明準備接收。
  3. 使用AUTH命令登入SMTP伺服器,輸入使用者名稱和密碼(注意,使用者名稱和密碼都需要使用base64加密)。
  4. 客戶端傳送RCPT命令,標識該電子郵件的計劃接收人,可以有多個RCPT行。伺服器以OK作為想要,表示願意為收件人傳送郵件。
  5. 協商結束後,使用DATA命令傳送。
  6. .號表示結束,輸入內容一起傳送出去,結束此次傳送,用QUIT命令退出。

例如:使用Telnet建立一個SMTP會話,其中S代表伺服器,C代表客戶端,如下圖所示

SMTP連線和傳送過程

注意: 上述命令並不一定會一次性成功,伺服器可能會返回錯誤響應,客戶端應該安裝協議規定的順序輸入後續命令(或重複執行失敗的命令、或重置會話、或退出會話等)。

SMPT協議常用命令

SMTP協議常用命令

下面開始我們的整體,構建一個簡單的SMTP郵件傳送類

簡單介紹一下我們使用的socket函式,我們使用fsockopen()來連線SMTP伺服器,使用fsockopen的好處是把Socket連線繫結到一個流觴,然後使用各種操作流的函式操作這個Socket連線。fsockopen()函式的用法:

resource fscokopen(string $hostname
, int $port, int [$errno], string [$errstr], int [$timeout]);

引數說明如下:

  • $hostname 要連線的伺服器路徑
  • $port 要繫結的埠
  • $errno 儲存連線發生錯誤時的錯誤號
  • $errstr 儲存錯誤資訊
  • $timeout 設定連線的超時時間,單位秒

下面附上原始碼、註釋。

<?php
namespace smtp; // 設定這個命令空間是保證不和其他衝突,可以刪除
class Smtp
{
    /**
     * @var string 儲存要連線的SMTP伺服器
     */
    private $host;
    /**
     * @var int 要繫結的埠號
     */
    private $port = 25;
    /**
     * @var string 儲存要連線的SMTP伺服器使用者名稱
     */
    private $user;
    /**
     * @var string 儲存要連線的SMTP伺服器密碼
     */
    private $pass;
    /**
     * @var bool 標識是否開啟除錯模式,預設關閉
     */
    private $debug = false;
    /**
     * @var resource 儲存與SMTP伺服器連線的控制代碼
     */
    private $sock;
    /**
     * @var bool 標識使用什麼格式傳送郵件,false 普通文字, true HTML
     */
    private $isHTML = false;
    /**
     * @var int 連線超時時間
     */
    private $timeout = 10;

    /**
     * Smtp constructor.
     * @param string $host
     * @param int    $port
     * @param string $user
     * @param string $pass
     * @param bool   $isHTML
     * @param bool   $debug
     * @param int    $timeout
     */
    public function __construct( $host, $port, $user, $pass, $isHTML = false, $debug = false, $timeout = 10 )
    {
        $this->host   = $host;
        $this->port   = $port;
        $this->user   = base64_encode( $user );
        $this->pass   = base64_encode( $pass );
        $this->isHTML = $isHTML;
        $this->debug  = $debug;
        // 連線到伺服器
        $this->sock = @fsockopen( $this->host, $this->port, $errno, $errstr, $this->timeout );
        // 判斷連線是否成功
        if ( ! $this->sock ) {
            die( "Error number: $errno, Error message: $errstr" );
        }
        // 判斷連線成功返回的狀態碼是否正確
        $response = fgets( $this->sock );
        if ( strpos( $response, "220" ) === false ) {
            die( "Server error: $response" );
        }
    }

    /**
     * 向伺服器傳送命令
     * @param string $cmd        需要執行的命令
     * @param int    $returnCode 命令的返回碼
     * @return bool
     */
    private function doCommand( $cmd, $returnCode )
    {
        fwrite( $this->sock, $cmd );
        $response = fgets( $this->sock );
        if ( strpos( $response, "{$returnCode}" ) === false ) {
            $this->showDebug( $response );

            return false;
        }

        return true;
    }

    /**
     * 顯示除錯資訊
     * @param $message string
     */
    private function showDebug( $message )
    {
        if ( $this->debug ) {
            echo "<p>Debug: {$message}</p>";
        }
    }

    /**
     * 判斷郵件是否符合要求
     * @param $email string
     * @return bool
     */
    private function isEmail( $email )
    {
        $pattern = "/^[^_][\w]*@[\w.]+[\w]*[^_]$/";
        if ( preg_match( $pattern, $email ) ) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * 執行郵件傳送
     * @param string $from    傳送郵件者
     * @param string $to      接收郵件者
     * @param string $subject 郵件的主題
     * @param string $body    郵件的內容
     * @return bool
     */
    public function send( $from, $to, $subject, $body )
    {
        if ( ! $this->isEmail( $from ) OR ! $this->isEmail( $to ) ) {
            $this->showDebug( "Please enter valid from/to email." );

            return false;
        }
        if ( empty( $subject ) OR empty( $body ) ) {
            $this->showDebug( "Please enter subject/content." );

            return false;
        }
        $detail = "FROM:{$from}\r\n";
        $detail .= "TO:{$to}\r\n";
        $detail .= "Subject:{$subject}\r\n";
        if ( $this->isHTML ) {
            $detail .= "Content-Type: text/html;\r\n";
        } else {
            $detail .= "Content-Type: text/plain\r\n";
        }
        $detail .= "charset=utf-8\r\n\r\n";
        $detail .= $body;
        // 執行命令
        $this->doCommand( "HELO {$this->host}\r\n", 250 ); // 客戶端向服務端傳送HELO表明身份
        $this->doCommand( "AUTH LOGIN\r\n", 334 ); // 客戶端傳送命令登入SMTP伺服器
        $this->doCommand( "{$this->user}\r\n", 334 ); // 輸入使用者的名
        $this->doCommand( "{$this->pass}\r\n", 235 ); // 輸入密碼
        $this->doCommand( "MAIL FROM: <{$from}>\r\n", 250 ); // 設定發件人
        $this->doCommand( "RCPT TO: <{$to}>\r\n", 250 ); // 設定收件人
        $this->doCommand( "DATA\r\n", 354 ); // 設定郵件內容
        $this->doCommand( "$detail\r\n.\r\n", 250 ); // 傳送郵件
        $this->doCommand( "QUIT\r\n", 221 ); // 退出SMTP伺服器

        return true;
    }
}

// 下面是測試用例
$smtp    = new \smtp\Smtp( 'smtp.163.com', 25, '[email protected]', 'xfj529js515', true, true, 3 );
$subject = 'this is email subject';
$body    = 'this is test smtp email content';
var_dump( $smtp->send( '[email protected]', '[email protected]', $subject, $body ) );

測試結果如圖:

測試結果