深入理解SMTP協議之郵件客戶端


本文將使用Python從零實現一個簡易的郵件客戶端,通過本文你將對SMTP協議有更深入的了解,同時掌握使用Python實現標准協議的經驗。

我們將開發一個簡單的郵件客戶端,將郵件發送給任意收件人。我們的客戶端將需要連接到郵件服務器(QQ郵件服務器),使用SMTP協議與郵件服務器進行對話,並向郵件服務器發送電子郵件。

Python提供了一個名為smtplib的模塊,它內置了使用SMTP協議發送郵件的方法。但是我們不會使用此模塊,因為它隱藏了SMTP和套接字編程的細節,我們將完全從零開始實現自己的郵件客戶端。

1.基本郵件客戶端

我們先來了解SMTP客戶和SMTP服務器之間交換報文時在客戶端需要發送哪些命令,服務器又是如何對每個命令作出回答,其中每個回答含有一個響應碼和英文解釋。

命令 含義 響應碼及其英文解釋
HELO <domain><CRLF> HELLO的縮寫,客戶端為標識自己的身份而發送的命令,通常帶域名 220 <domain> Service ready
MAIL FROM: <reverse-path><CRLF> 標識郵件的發件人,<reverse-path>為發送者的地址,此命令告訴接收方一個新郵件發送的開始,並對所有的狀態和緩沖區進行初始化 250 Requested mail action okay, completed
RCPT TO: <forward-path><CRLF> 標識郵件的收件人,<forward-path>為收件人的地址 250 Requested mail action okay, completed
DATA<CRLF> 標識郵件數據傳輸的開始,<CRLF>.<CRLF>標識數據的結尾,客戶端發送的、用於啟動郵件內容傳輸的命令 354 Start mail input; end with <CRLF>.<CRLF>
QUIT<CRLF> 表示會話的終止 221 <domain> Service closing transmission channel

注意:<CRLF>中的CR和LF分別表示回車和換行。SMTP響應碼的每一個數字都是有特定含義的,如第一位數字為2時表示命令成功,為5時表示失敗,為3時表示沒有完成。

以上命令是正常完成一次郵件傳輸的必不可少的命令,我們將在后面的代碼中用到它們,更多的命令這里不做更多的介紹。下面我們來看代碼實現:

from socket import *
from base64 import b64encode


global clientSocket


def init():
    global clientSocket
    mail_server = 'smtp.qq.com'
    clientSocket = socket(AF_INET, SOCK_STREAM)
    while True:
        clientSocket.connect((mail_server, 25))
        recv = clientSocket.recv(1024).decode()
        print(recv)
        if recv[:3] == '220':
            print('成功與郵件服務器建立TCP連接!')
            break


def command_send(command, success_code, data_type='str'):
    if data_type == 'str':
        command = command.encode()
    while True:
        print(command)
        clientSocket.send(command)
        recv = clientSocket.recv(1024).decode()
        print(recv)
        if recv[:3] == success_code:
            break
        else:
            print('Failed')


if __name__ == "__main__":
    init()

    heloCommand = 'HELO Alice\r\n'
    command_send(heloCommand, '250')

    authLoginCommand = 'AUTH LOGIN\r\n'
    command_send(authLoginCommand, '334')

    user = b64encode('你的QQ郵箱賬戶'.encode()) + b'\r\n'
    command_send(user, '334', 'bytes')

    pwd = b64encode('你的QQ郵箱授權碼'.encode()) + b'\r\n'
    command_send(pwd, '235', 'bytes')

    mailFromCommand = 'MAIL FROM: <發送方郵箱地址>\r\n'
    command_send(mailFromCommand, '250')

    reptToCommand = 'RCPT TO: <接收方郵件地址>\r\n'
    command_send(reptToCommand, '250')

    dataCommand = 'DATA\r\n'
    command_send(dataCommand, '354')

    msg = "FROM: 發送方郵箱地址\r\nTO: 接收方郵件地址\r\nSubject: Say Hello\r\n\r\nHello World!"
    clientSocket.send(msg.encode())

    endmsg = '\r\n.\r\n'
    command_send(endmsg, '250')

    quitCommand = 'QUIT\r\n'
    command_send(quitCommand, '221')

發送命令和接收響應是每一步都需要做的事情,因此我們其封裝到command_send函數中以降低代碼冗余,事實上每一個步驟都有出錯的可能,而我們處理差錯的方法也很簡單:一直循環直到成功為止。

通過觀察代碼不難發現AUTH LOGIN命令是我們沒介紹的,這是因為我們所采用的QQ郵件服務器要求進行安全認證。最初的SMTP協議並不包含安全認證,而ESMTP通過增加命令EHLO和AUTH在安全性方面擴展了SMTP。如今的SMTP服務器,無論是公網的還是內網的,大多都要求安全認證。

通過向服務器發送EHLO命令(格式為EHLO <domain><CRLF>),客戶端可以了解到服務器是否支持擴展簡單郵件傳輸協議(ESMTP)。我們向QQ郵件服務器發送該命令收到的響應如下:

250-newxmesmtplogicsvrsza5.qq.com
250-PIPELINING
250-SIZE 73400320
250-STARTTLS
250-AUTH LOGIN PLAIN XOAUTH XOAUTH2
250-AUTH=LOGIN
250-MAILCOMPRESS
250 8BITMIME

上述響應中的AUTH LOGIN PLAIN XOAUTH XOAUTH2說明了SMTP服務器支持的驗證方式,這里我們采用的是LOGIN方式,具體步驟如下:

  1. 客戶端發送AUTH LOGIN命令,指示服務器進行身份認證;
  2. 客戶端收到334 VXNlcm5hbWU6響應后發送BASE64編碼的賬戶名;
  3. 客戶端收到334 UGFzc3dvcmQ6響應后發送BASE64編碼的密碼;
  4. 客戶端收到235 Authentication successful響應后表明身份驗證成功;

注意:對於QQ郵件服務器而言,步驟3中輸入的不是賬戶的登錄密碼而是授權碼,在QQ郵箱中開通POP3/SMTP服務需要生成授權碼,具體方法此處不介紹。

2.添加安全套接字層

SSL(Secure Sockets Layer)即安全套接層,是由Netscape公司於1990年開發,用於保障World Wide Web(WWW)通訊的安全。其主要任務是提供私密性,信息完整性和身份認證

SSL是一個不依賴於平台和運用程序的協議,位於TCP/IP協議與各種應用層協議之間,為數據通信提高安全支持。HTTP是第一個使用SSL保障安全的應用層協議,我們常見的HTTPS的全稱就是HTTP over SSL

注意:HTTPS默認工作在443端口,而HTTP默認工作在80端口

與HTTPS類似,諸如SMTP、POP3、IMAP等郵件協議也能支持SSL,基於SSL安全協議的SMTP協議被稱為SMTPS(SMTP over SSL)。在本例中,為了添加安全套接層,我們需要對init()函數進行修改,具體代碼如下:

import ssl


def init():
    global clientSocket
    mail_server = 'smtp.qq.com'
    clientSocket = socket(AF_INET, SOCK_STREAM)
    context = ssl.create_default_context()
    while True:
        clientSocket.connect((mail_server, 465))
        clientSocket = context.wrap_socket(sock=clientSocket, server_hostname=mail_server)
        recv = clientSocketSSL.recv(1024).decode()
        print(recv)
        if recv[:3] == '220':
            print('成功與郵件服務器建立TCP連接!')
            break

context是SSL創建的默認上下文對象,對象中保存了我們對證書的認證與加密算法選擇的偏好設置。通過調用上下文對象的wrap_socket()方法,表示由OpenSSL庫負責控制我們的TCP鏈接,然后與通信對方交換必要的握手信息,並建立加密鏈接,最終返回一個SSLSocket對象,該對象負責進行所有的后續通信。

在與郵件服務器建立連接的過程中,我們連接的端口號是465,而不是25。這是因為465號端口是為SMTPS協議服務開放的,而25號端口是為SMTP協議服務開放的。

3.發送圖像信息

到目前為止,盡管我們的SMTP郵件客戶端可以正常工作,但是只能在電子郵件的正文中發送文本消息。本節我們將修改客戶端代碼,使其可以發送包含文本和圖像的電子郵件。

其實要實現我們的功能很簡單,只需要對郵件的格式動動手腳就可以了。下面簡要說明一下郵件的格式:

郵件是由郵件頭和郵件體構成的,郵件體又可能由文本、超文本和附件等多個部分構成,當在同一郵件體內有多個不同的數據集合時,我們必須在郵件頭中通過multipart參數值顯式地指出這一點。郵件體的不同子部分之間是通過邊界boundary封裝的,每一部分都會由邊界開始,然后包含着郵件子體的頭信息(header),空行,然后是郵件正文。需要指出的是,最后一個子部分的后面必須跟一個結尾邊界。

關於boundary的使用方法,我們可以在Content-type字段的后面是把boundary的值包含在引號之中。也可以沒有引號,但有引號是最保險的。當有一些非法字符出現在boundary值中時,如果不加引號可能會引起錯誤。在使用邊界封裝郵件時,其使用方法是在值的前面加兩個-。其中,對於最后一個部分的結尾邊界,還需要在值的后面再加兩個-

對於客戶端代碼,其余部分不變,我們只需要修改郵件部分。接下來我們看下修改后的郵件部分:


if __name__ == "__main__":
    ...省略...
    
    html_data = b64encode(b'<img src="cid:image1">')
    with open("test.jpg", "rb") as f:
        image_data = b64encode(f.read())

    msg = "FROM: 發送方郵箱地址\r\nTO: 接收方郵箱地址\r\nSubject: Transmit Image by E-mail\r\nMIME-Version: " \
          "1.0\r\nContent-Type:multipart/related; boundary='12345678'\r\n\r\n--12345678\r\nContent-Type: text/html; " \
          "charset=UTF-8\r\nContent-Transfer-Encoding: base64\r\n\r\n".encode()
    msg += html_data
    msg += "\r\n\r\n--12345678\r\nContent-Type: image/jpeg; name='test.jpg'\r\nContent-Transfer-Encoding: " \
           "base64\r\nContent-ID: image1\r\n\r\n".encode()
    msg += image_data
    msg += "\r\n\r\n--12345678--\r\n".encode()
    clientSocketSSL.send(msg)

    ...省略...

可以看到,修改后的郵件由兩部分組成,一部分是HTML數據,另一部分則是圖片數據。此處我們通過HTML將圖片嵌入到了郵件正文中,具體方法是在HTML中通過引用src="cid:0"就可以把附件作為圖片嵌入了。如果有多個圖片,則依次給它們編號,然后引用不同的cid:x即可,例如這里我們將test.jpg編號為image1

郵件的最終效果如下圖所示:

mail.PNG


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM