從零構建SMTP郵件發送類
SMTP(Simple Mail Transfer Protocol,簡單郵件傳輸協議)是由原地址到目的地址傳送郵件的一組規則,用來控制信件的中轉方式。SMTP協議屬於TCP/IP協議簇,其使每臺計算機在發送或中轉信件時能到找下一個目的地。通過使用指定的服務器,把E-mail寄到收件人的服務器上。
SMTP連接和發送過程如下:
- 建立TCP連接。
- 客戶端發送
HELO
命令標識發件人自己的身份,客戶端發送MAIL命令。服務器以OK作爲響應,表明準備接收。 - 使用
AUTH
命令登錄SMTP服務器,輸入用戶名和密碼(注意,用戶名和密碼都需要使用base64加密)。 - 客戶端發送
RCPT
命令,標識該電子郵件的計劃接收人,可以有多個RCPT行。服務器以OK作爲想要,表示願意爲收件人發送郵件。 - 協商結束後,使用DATA命令發送。
- 以
.
號表示結束,輸入內容一起發送出去,結束此次發送,用QUIT
命令退出。
例如:使用Telnet創建一個SMTP會話,其中S代表服務器,C代表客戶端,如下圖所示
注意: 上述命令並不一定會一次性成功,服務器可能會返回錯誤響應,客戶端應該安裝協議規定的順序輸入後續命令(或重複執行失敗的命令、或重置會話、或退出會話等)。
SMPT協議常用命令
下面開始我們的整體,構建一個簡單的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 ) );
測試結果如圖: