2017年10月7日星期六

PHP fsockopen 發送電子郵件

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

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

以 Gmail 作例子
GmailSMTP 位置為 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"
若果附件的內容不是 純文字 ,則需要將內容以 7bit8bitbase64 等加密後才能加入

例如
$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 指令後的標頭資料,例如 FromTo 不一定要與 MAIL FROMRCPT TO 相對
基本上這些標頭資料都不一定要加上,或胡亂編寫都可以
這些資料是為了方便收件人了解郵件資料,例如 To (收件), Cc (副本), Bcc (密件)
關於更多 SMTP 標頭資料可以到 IETF 的 RFC 4021 文件 查看

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

沒有留言 :

發佈留言