2017-10-07

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
1
$handle = fsockopen('ssl://smtp.gmail.com', 465);

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

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

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

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

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

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

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

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

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

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

使用 DATA 後的內容資料可以當作遵從 HTTP Request 的寫法,例如
1
2
3
4
5
6
7
8
9
$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);

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

例如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$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);
兩者都要留意, 最後有一點及一次換行
傳回回應
1
250 2.0.0 OK <b>timestamp</b> <b>ID</b> - gsmtp
若沒有設定 MAIL FROM 傳送時會傳回回應
1
503 5.5.1 MAIL first. <b>ID</b> - gsmtp
若沒有設定 RCPT TO 傳送時會傳回回應
1
503 5.5.1 RCPT first. <b>ID</b> - gsmtp

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

以下為測試程式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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);
}

得到如下的指令及回應
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
220 smtp.gmail.com ESMTP <b>***** Hidden ID *****</b> - gsmtp
EHLO smtp.gmail.com
250-smtp.gmail.com at your service, [<b>***** Hidden IP *****</b>]
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
<b>***** Hidden base64 encoded Username *****</b>
334 UGFzc3dvcmQ6
<b>***** Hidden base64 encoded Password *****</b>
235 2.7.0 Accepted
MAIL FROM: <<b>***** Hidden Sender E-Mail *****</b>>
250 2.1.0 OK <b>***** Hidden ID *****</b> - gsmtp
RCPT TO: <<b>***** Hidden Recipient E-Mail *****</b>>
250 2.1.5 OK <b>***** Hidden ID *****</b> - gsmtp
DATA
354  Go ahead <b>***** Hidden ID *****</b> - gsmtp
Subject: This is subject
From: "Mr.Z" <<b>***** Hidden Sender E-Mail *****</b>>
To: "Mr.A" <<b>***** Hidden Recipient E-Mail *****</b>>
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 <b>***** Hidden ID *****</b> - gsmtp
QUIT
221 2.0.0 closing connection <b>***** Hidden ID *****</b> - gsmtp

參考結果
見下文
見下文
見下文

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

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

沒有留言 :

張貼留言