2017年10月7日星期六

PHP fsockopen 發送電子郵件

上次提及過使用 fsockopen 發送 HTTP Request
在下再嘗試使用 fsockopen 發送電子郵件

使用 PHP 下,一般都會使用內建的 mail() 功能
需要修改 php.ini 的 SMTP 資料,修改後還要重新啟動伺服器才能應用
若需要測試不同的 SMTP 伺服器,經常這樣修改比較麻煩,利用 ini_set 可以暫時修改 php.ini
但有些情況是閣下並不是伺服器擁有者,不能隨意修改 php.ini 、重新啟動伺服器、ini_set 的修改受限制
若使用 fsockopen 便可以直接使用像 Telnet 連接 SMTP 伺服器的指令來發送電子郵件

以 Gmail 作例子
Gmail 的 SMTP 位置為 smtp.gmail.com
由於 Gmail 需要使用安全通道加密資料,因此使用 fsockopen 時需要以 ssl:// 連接至 465 port 或以 tls:// 連接至 587 port
$handle = fsockopen('ssl://smtp.gmail.com', 465);

$handle = fsockopen('tls://smtp.gmail.com', 587);
連接後使用
$response = fgets($handle);
echo $response;
傳回成功回應的內容
220 smtp.gmail.com ESMTP ID - gsmtp
或傳回失敗回應,其回應碼為 421

留意:
回應內容,前端必定有一組 三位數 的 SMTP 回應碼
另在由於 SMTP 完成整個發送程序前,都會以互動形容操作
若當前操作為使用者給予指令,但使用如 fgets 等獲取伺服器的回應,便會變成不斷等待伺服器
而判斷是否是使用者給予指令的狀態,可以以 第四字元 是否為 空白 作判斷
若伺服器的回應不只一行, SMTP 回應碼會以「-」減號連接
例如
連接到 Gmail 的 SMTP 後編寫
fwrite($handle, "EHLO gmail.smtp.com\r\n");
需要使用數次 fgets 來獲取完整的回應

250-smtp.gmail.com at your service, [IP]
250-SIZE 35882577
250-8BITMIME
250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH
250-ENHANCEDSTATUSCODES
250-PIPELINING
250-CHUNKING
250 SMTPUTF8
但每個伺服器傳回傳的內容行數都不一定固定,若未獲取完整回應便不能向伺服器給予指令,若過度獲取資料便會不斷等待伺服器
function check_response($handle) {
    $response = fgets($handle);
    echo $response;
    if (substr($response, 3, 1) != ' ') {
        check_response($handle);
    }
}
使用 迴遞 (recursion) 的方式檢查回應,直至 第四個字元 是 空白 便會離開 功能

完成 EHLO 後,編寫
fwrite($handle, "AUTH LOGIN\r\n");
check_response($handle);
傳回回應
334 VXNlcm5hbWU6
VXNlcm5hbWU6 其實是經 base64 加密後的「Username:」

編寫
fwrite($handle, base64_encode($username) . "\r\n");
check_response($handle);
傳回回應
334 UGFzc3dvcmQ6
UGFzc3dvcmQ6 其實是經 base64 加密後的「Password:」

編寫
fwrite($handle, base64_encode($password) . "\r\n");
check_response($handle);
若登入成功傳回回應
235 2.7.0 Accepted
若登入失敗傳回回應
535 5.7.8  https://support.google.com/mail/?p=BadCredentials ID - gsmtp
另外若使用兩步認證,此方法則不能通過登入過程

登入 SMTP 後,可以正式編寫郵件資料及內容

編寫
fwrite($handle, "MAIL FROM: <sender@gmail.com>\r\n");
check_response($handle);
若成功傳回回應
250 2.1.0 OK ID - gsmtp
MAIL FROM 的內容是可以留空,會自動以登入的帳戶資料顯示

編寫
fwrite($handle, "RCPT TO: <recipient@gmail.com>\r\n");
check_response($handle);
若成功傳回回應
250 2.1.0 OK ID - gsmtp
RCPT TO 可以增加更多,但每加入 RCPT TO 都需要使用 fgets

編寫
fwrite($handle, "DATA\r\n");
check_response($handle);
傳回回應
354  Go ahead ID - gsmtp
之後可以編寫郵件的內容資料

使用 DATA 後的內容資料可以當作遵從 HTTP Request 的寫法,例如
$request = '';
$request .= "header-1: data-1\r\n";
$request .= "header-2: data-2\r\n";
$request .= "header-3: data-3\r\n";
$request .= "\r\n";
$request .= "message body\r\n";
$request .= ".\r\n";
fwrite($handle, $request);
check_response($handle);

若需要加入附件,便需要在加入
Content-Type: multipart/mixed; boundary="END_OF_PART"
表示多重結構
內容需要使用
Content-Type: text/plain; charset=UTF-8; format=flowed
附件需要使用
Content-Type: MIME Type; charset=UTF-8
Content-transfer-encoding: Encryption Method
Content-Disposition: attachment; filename="filename.ext"
若果附件的內容不是純文字,則需要將內容以 7bit 、 8bit 、 base64 等加密後才能加入

例如
$request = '';
$request .= "header-1: data-1\r\n";
$request .= "header-2: data-2\r\n";
$request .= "header-3: data-3\r\n";
$request .= 'Content-Type: multipart/mixed; boundary="END_OF_PART"' . "\r\n";
$request .= "MIME-Version: 1.0\r\n";
$request .= "\r\n";
$request .= "--END_OF_PART\r\n";
$request .= "Content-Type: text/plain; charset=UTF-8; format=flowed\r\n";
$request .= "\r\n";
$request .= "message body\r\n";
$request .= "--END_OF_PART\r\n";
$request .= "Content-Type: image/jpeg\r\n";
$request .= "Content-Transfer-Encoding: base64\r\n";
$request .= 'Content-Disposition: attachment; filename="filename.jpg"' . "\r\n";
$request .= "\r\n";
$request .= base64_encode($file_contents) . "\r\n";
$request .= "--END_OF_PART\r\n";
$request .= ".\r\n";
fwrite($handle, $request);
check_response($handle);
兩者都要留意,最後有一點及一次換行
傳回回應
250 2.0.0 OK timestamp ID - gsmtp
若沒有設定 MAIL FROM 傳送時會傳回回應
503 5.5.1 MAIL first. ID - gsmtp
若沒有設定 RCPT TO 傳送時會傳回回應
503 5.5.1 RCPT first. ID - gsmtp

最後完成後編寫
fwrite($handle, "QUIT\r\n");
check_response($handle);
終止 SMTP 伺服器的連接,傳回回應
221 2.0.0 closing connection ID - gsmtp

以下為測試程式
function check_response($handle, $code) {
    $response = fgets($handle);
    echo $response;
    if (substr($response, 3, 1) != ' ') {
        check_response($handle, $code);
    } else {
        if (substr($response, 0, 3) != $code) {
            die();
        }
    }
}

function send_request($handle, $request, $code) {
    echo $request;
    fwrite($handle, $request);
    check_response($handle, $code);
}

$host = 'smtp.gmail.com';
$errno = 0;
$error = '';
if ($handle = @fsockopen('ssl://' . $host, 465, $errno, $error)) {
    check_response($handle, 220);
    send_request($handle, 'EHLO ' . $host . "\r\n", 250);
    send_request($handle, "AUTH LOGIN\r\n", 334);
    echo "***** Hidden Request *****\r\n";
    fwrite($handle, base64_encode($username) . "\r\n");
    check_response($handle, 334);
    echo "***** Hidden Request *****\r\n";
    fwrite($handle, base64_encode($password) . "\r\n");
    check_response($handle, 235);
    send_request($handle, 'MAIL FROM: <' . $sender_email . ">\r\n", 250);
    send_request($handle, 'RCPT TO: <' . $recipient_email . ">\r\n", 250);
    send_request($handle, "DATA\n", 354);
    $request = '';
    $request .= "Subject: This is subject\r\n";
    $request .= 'From: "' . $sender_name . '" <' . $sender_email . ">\r\n";
    $request .= 'To: "' . $recipient_name . '" <' . $recipient_email . ">\r\n";
    $request .= 'Content-Type: multipart/mixed; boundary="END_OF_PART"' . "\r\n";
    $request .= "MIME-Version: 1.0\r\n";
    $request .= "\r\n";
    $request .= "--END_OF_PART\r\n";
    $request .= "Content-Type: text/plain; charset=UTF-8; format=flowed\r\n";
    $request .= "\r\n";
    $request .= "This is body\r\n";
    $request .= "--END_OF_PART\r\n";
    $request .= "Content-Type: text/plain; charset=UTF-8\r\n";
    $request .= "Content-Transfer-Encoding: base64\r\n";
    $request .= 'Content-Disposition: attachment; filename="filename.txt"' . "\r\n";
    $request .= "\r\n";
    $request .= base64_encode('This is attachment') . "\r\n";
    $request .= "--END_OF_PART--\r\n";
    $request .= ".\r\n";
    send_request($handle, $request, 250);
    send_request($handle, "QUIT\r\n", 221);
} else {
    printf('[%d] %s', $errno, $error);
}

得到如下的指令及回應
220 smtp.gmail.com ESMTP ***** Hidden ID ***** - gsmtp
EHLO smtp.gmail.com
250-smtp.gmail.com at your service, [***** Hidden IP *****]
250-SIZE 35882577
250-8BITMIME
250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH
250-ENHANCEDSTATUSCODES
250-PIPELINING
250-CHUNKING
250 SMTPUTF8
AUTH LOGIN
334 VXNlcm5hbWU6
***** Hidden base64 encoded Username *****
334 UGFzc3dvcmQ6
***** Hidden base64 encoded Password *****
235 2.7.0 Accepted
MAIL FROM: <***** Hidden Sender E-Mail *****>
250 2.1.0 OK ***** Hidden ID ***** - gsmtp
RCPT TO: <***** Hidden Recipient E-Mail *****>
250 2.1.5 OK ***** Hidden ID ***** - gsmtp
DATA
354  Go ahead ***** Hidden ID ***** - gsmtp
Subject: This is subject
From: "Mr.Z" <***** Hidden Sender E-Mail *****>
To: "Mr.A" <***** Hidden Recipient E-Mail *****>
Content-Type: multipart/mixed; boundary="END_OF_PART"
MIME-Version: 1.0

--END_OF_PART
Content-Type: text/plain; charset=UTF-8; format=flowed

This is body
--END_OF_PART
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="filename.txt"

VGhpcyBpcyBhdHRhY2htZW50
--END_OF_PART--
.
250 2.0.0 OK 1505711793 ***** Hidden ID ***** - gsmtp
QUIT
221 2.0.0 closing connection ***** Hidden ID ***** - gsmtp

參考結果
展示 Gmail 郵件內容
展示 Gmail 郵件收件資料
展示 Gmail 郵件附件資料

在送出 DATA 指令後的標頭資料,例如 From 及 To 不一定要與 MAIL FROM 及 RCPT TO 相對
基本上這些標頭資料都不一定要加上,或胡亂編寫都可以
這些資料是為了方便收件人了解郵件資料,例如 To (收件), Cc (副本), Bcc (密件) 等
關於更多 SMTP 標頭資料可以到 IETF 的 RFC 4021 文件 查看

實際上 PHPMailer 都是使用 fsockopen 的方式發送電子郵件

沒有留言 :

發佈留言