當你發郵件時,你的電腦都幹了什麼?藉助Python探索SMTP協議

當你發郵件時,你的電腦都幹了什麼?藉助Python探索SMTP協議

你是否需要每天使用電子郵件服務?(私信小編007即可自動獲取大量python視頻教程以及各類PDF!)

電子郵件(email)是互聯網上歷史悠久又常用的消息收發形式。對於大多數辦公室一族,每天到班上的第一件事恐怕就是要查一下新的郵件。雖然即時通信工具在飛速佔領著通信市場,但是在商業或者學術圈裡,email依然佔據著主流地位。

一般收發email,要麼使用電子郵件管理工具(如Outlook, Mac的Mail等),要麼登錄網頁版的email服務網頁(gmail, 126等等)。作為一個好奇心很強的程序員,我一直很想知道當自己編輯了一封郵件,點擊發送的時候,自己的電腦/手機都在我背後幹了什麼?收發email的服務器是如何工作的?藉助Python的TCP接口和smtplib,我們可以很容易就可以寫一個“email服務器“。這裡email服務器加了引號,是因為它只是假裝自己是一個email服務器(見文尾的討論)。但是,這個假服務器其實已經具備了真服務器的所有邏輯功能。如果你希望,你完全可以把它做成一個可以正常工作的email服務器!(文尾也會大體講解如何去做,但是需要你購買一個域名並向DNS服務器進行相應的註冊。)

當你發郵件時,你的電腦都幹了什麼?藉助Python探索SMTP協議

當你點擊“發送”之後,你的郵箱做了那些操作?今天的實驗帶你親自看一看。

引言:互聯網協議和SMTP協議

在之前的一篇文章(點擊這裡查看)中,筆者討論了互聯網協議的幾個層,並且構建實驗探討了提供網頁服務的HTTP服務器如何工作。如果你也喜歡探索原理,並且還沒有做過這個實驗,那麼強烈建議你點開連接,跟著文章中設計的實驗探索一下HTTP協議。這裡,我們會做一個類似的實驗來窺探email收發使用的SMTP協議。

簡單概括原理:我們的互聯網分為四個層,每一層的正常工作建立在下面層的基礎上。工作在最上層的“應用層”,有提供網頁服務的HTTP協議,提供郵件收發的SMTP協議,提供文件傳輸的FTP協議等等。這些協議想要正常工作,都要基於下面“傳輸層”的支持。傳輸層比較常用的是TCP協議。今天的實驗裡,我們將在SMTP層和TCP層兩個層面上觀察SMTP協議,並且在TCP層上構造一個簡單的,需要手動控制的“SMTP服務器”。

當你發郵件時,你的電腦都幹了什麼?藉助Python探索SMTP協議

這張圖詮釋了SMTP連接。當我們說SMTP通信時,它其實是一種虛擬的,抽象的說法。真正建立連接的,是在下面的層上

實驗0(準備工作):查看email服務器地址

在真正開始實驗之前,我們先看一下如何從一個email地址出發,查詢它對應的SMTP服務器域名。比如 [email protected] ,我們知道它的email域名是 126.com 。但是我們需要知道, 126.com 背後的SMTP服務器地址是多少。為了便於區分,通常管 126.com 叫做 email域名 ,而其背後的SMTP服務器地址,叫做 mx域名 。(mx是mail exchange的縮寫。)

這裡我們使用工具 nslookup 查詢mx域名。無論你使用的是Windows系統,還是Mac OS,還是Linux, nslookup 都已經存在於你的電腦裡了。使用它的步驟如下:

  1. 打開命令行。Windows系統:打開“開始”菜單,輸入"cmd",搜索到“命令提示行”工具。打開後界面如下。
當你發郵件時,你的電腦都幹了什麼?藉助Python探索SMTP協議

windows中的命令提示行

Mac OS在應用程序中找到"Terminal"。Linux我就不說命令行在哪裡了。

  1. 在命令行中輸入 nslookup 按回車,進入 nslookup 工具中。輸入 set q=mx ,指定查詢mx域名。輸入 126.com ,按下回車,你就會得到查詢結果。
當你發郵件時,你的電腦都幹了什麼?藉助Python探索SMTP協議

nslookup查詢126.com的mx域名

圖中可以看到, 126.com email域名背後有4個mx服務器。後面的討論中,使用任何一個mx服務器(比如“126mx01.mxmail.netease.com”)都可以。友情提示:複製mx域名時,注意不要把最後的句號複製上了。

實驗1(應用層):使用Python發送email

Python提供了一個很強大的包進行email的相關操作: smtplib 。在這一節,我們通過使用這個包,熟悉一下smtp協議工作的基本步驟,下一節,我們再深入到smtp協議的底層。

如果你想知道如何 姿勢正確 地發送郵件,你可以參見這一篇文章。這裡說的“姿勢正確”,意思是你將會使用 smtplib 登錄到你使用的郵箱服務器(比如你註冊了新浪的郵箱,你就可以使用 smtplib 的 login 函數登錄到新浪郵箱服務器上),然後再對你的目標收件人(比如126的email郵箱)發郵件。只要正確設置,這樣發郵件一定不會有問題,因為有你的郵件服務提供商(這個例子裡是新浪)給你撐腰,對方的郵箱(這裡是126郵箱)不敢拒絕你的郵件。

但是,這裡筆者只想討論smtp協議的結構。嚴格來說 login 並算不上smtp協議的要求(至少並不是基本要求)。很多接收者的郵箱,並不需要發件人有一個具體的email地址,只需要收件人的email地址明確,郵件內容格式正確就可以了。是的,

你不需要自己有一個email地址才可以給別人發郵件!

當然,很多時候這樣的郵件會被對方SMTP服務器拒收。即時接收了,也有可能因為來源不明而被放到垃圾郵件裡。所以,並不建議讀者用這種收發日常郵件。但是為了弄懂SMTP的協議,這樣做一兩次還是值得的。

話不多說,先上一個完整的郵件發送的截圖。注意,我作為發件人,並沒有登錄任何自己的郵箱。另外,注意變量 s_body 的格式。大部分郵件服務器對這個格式很看重。不符合這個格式的郵件經常會被拒絕。最後,注意在 server.connect 那一行運行之後,後面手速一定要快。隔一會再執行下一行的話,對方服務器通常會斷開。

當你發郵件時,你的電腦都幹了什麼?藉助Python探索SMTP協議

Python中使用smtplib發送郵件。要先把收件人地址、發件人地址和郵件內容都實現編輯好,存在變量中。避免連接到服務器之後,由於超時沒有響應而導致服務器斷開連接

大概來看一下代碼裡面都幹了什麼。

  1. 創建了一個叫 server 的SMTP對象。在調用 connect 方法之前,事先準備好了收件人、發件人和郵件內容的字符串 s_from , s_to 和 s_body 。因為服務器允許的連接時間有限。特別注意 s_body 字符串的格式。
  2. server.connect(...) 連接126的mx服務器。這裡用到的是上一節中 nslookup 查詢到的信息。
  3. server.sendmail(...) 發送郵件。返回 {} ,就說明發送成功了。

So far so good。 但是,作為一個好奇寶寶,你肯定要問: server.sendmail 到底幹了什麼?使用 server.sendmail 發郵件,跟打開outlook寫好了郵件點發送,似乎並沒有太大提高。為了探尋後面發生了什麼,我們就要祭出TCP socket了。

實驗2 (傳輸層):使用TCP socket假裝自己是個SMTP服務器

怎麼才能搞清楚 server.sendmail 揹著你跟服務器幹了什麼事情呢?好吧,換個問題:假設你懷疑你對象在網上見了漂亮妹子/帥氣男生就勾搭,怎麼才能抓住他/她的把柄呢?一個方法就是,自己註冊個上網賬號,把自己偽裝成漂亮妹子/帥氣男生,跟他/她聊。

我們知道,像 requests 一樣, smtplib 要想進行SMTP通信,一定會使用下面傳輸層的TCP協議,跟對方的SMTP服務器建立TCP連接。所以,我們就像上一篇文章那樣,準備一個TCP連接,把對方發過來的數據都顯示到屏幕上,具體哪些消息,什麼格式,就一目瞭然了。

跟http請求不同的是, server.sendmail 不是一個 單次 的請求/響應,而是要求雙方使用協議規定的格式 反覆提問回答幾次 才可以完成郵件發送。因此,我們的服務器裡也要仔細設置響應的內容,確保返回的東西符合格式,使得對話能繼續進行。(也就是說,想假裝自己是SMTP服務器,比假裝自己是HTTP服務器要難一點,穿幫的可能性也更大一點。)為了增強體驗感,我們在每次收到信息時,讓我們手動填寫返回內容。

我們先來看代碼。

"""
這是一個虛擬服務器。當任何程序簡介到它時,它先發送一條歡迎信息WELCOME_MSG,
然後等待對方發送信息。對方每發送一條信息,它就會把信息顯示到屏幕上,然後提示
我們輸入應答內容。緊接著,它會把我們輸入的內容後面加上換行符\r\n,發送回去。

"""
SERVER_IP = "localhost"
SERVER_PORT = 25 #默認的SMTP之一
MAX_LENGTH = 1023 #規定每條信息長度上限。
WELCOME_MSG = "220 Virtual Server At Your Service!\r\n" #歡迎信息
socket_list = []
import socket
def close_sockets(): #再程序出現異常退出時關閉所有端口,避免端口占用
for sock in socket_list:
sock.close()
def main():

sock_listen = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket_list.append(sock_listen)
sock_listen.bind((SERVER_IP, SERVER_PORT))
sock_listen.listen(1)
conn, addr = sock_listen.accept()
socket_list.append(conn)
# Once connection is built: send welcome message and print connection
print("Connection established. From: " + str(addr))
if WELCOME_MSG is not None and WELCOME_MSG != '': # 如果WELCOME_MSG為''或者None,
# 則不發送歡迎信息,鏈接建立後
# 直接進入接收信息狀態
conn.send(WELCOME_MSG.encode())
print("歡迎信息發出!")

while True:
print("等待對方應答... 信息長度上限: " + str(MAX_LENGTH))
data = conn.recv(MAX_LENGTH)
print("收到信息:\n", data)
if len(data) == 0:
break
reply_msg = input("您的回覆: ")
reply_msg += '\r\n'
conn.send(reply_msg.encode())
print("回覆消息發出!")
if __name__ == "__main__":
try:
main()
except Exception as err:

print(str(err))
close_sockets()
exit()

這段代碼比較直接。代碼裡的註釋或者 print 的提示字都描述著每一部分代碼的功能。

有了這個人工服務器,我們就可以拿它接收 smtplib 發來的請求了。前面的演示用了Windows和Mac OS的電腦。為了不偏心,這裡就用Linux的電腦做演示了。(我才不會告訴你,其實是因為Windows電腦老婆在用,而Mac電腦落在辦公室裡了T_T)

  1. 服務器開啟。注意由於使用了25端口(SMTP協議的默認端口之一),為系統預留端口,因此程序需要管理員權限。第一幅圖中,第一次嘗試由於沒有用管理員權限 sudo 而被拒絕執行了。加了 sudo 程序得以啟動,並開啟端口,等待連接。
當你發郵件時,你的電腦都幹了什麼?藉助Python探索SMTP協議

開啟服務器。第一次由於沒有使用管理員權限而被拒絕。第二次成功開啟,進入等待連接狀態

  1. 使用Python3的 smtplib 連接SMTP服務器。這一步跟上面實驗是一樣的。注意 server.connect 連接的是 'localhost' 。此時,右邊圖中服務器也顯示收到了連接,併發送了歡迎消息'220 Virtual Server At Your Service!' 這時從 smtplib 接收到的信息來看,它識別了這種 狀態碼回覆信息 的格式,返回了一個二元素的數組。
當你發郵件時,你的電腦都幹了什麼?藉助Python探索SMTP協議

python的smtp連接,服務器發送來歡迎信息

接下來,我們就重複上面實驗中的做法,定義 s_from , s_to 和 s_msg ,然後交給 server.sendmail 函數來以郵件形式發送出去。見下圖。

當你發郵件時,你的電腦都幹了什麼?藉助Python探索SMTP協議

使用smtplib的sendmail發送郵件

  1. 接下來的圖 很重要! 它顯示了sendmail函數執行後,服務器上收到的
    一連串信息
當你發郵件時,你的電腦都幹了什麼?藉助Python探索SMTP協議

sendmail函數給服務器發送的一系列SMTP消息。從`helo`開始

注意,這裡為了把換行符也都顯示出來,特意沒有將Bytes型字符串解碼成普通的Python3字符串(即,沒有調用 decode 方法)。所以圖裡每條信息前面都有個 b 。

這段對話是這樣的:

sendmail: helo [自己地址]

我: 250 Nice to meet you # 注意格式是“狀態代碼響應信息”。

sendmail: mail FROM:

我: 250 Ok

sendmail: rcpt TO:

我: 250 Ok

sendmail: data # 這個 data 單詞是告訴對方,注意,我後面要開始發送正文了!

我: 354 Go ahead # 這裡狀態碼也不再是250,而是354,表示“我等你發信息”

sendmail: # 注意:這段文字最後的 \r\n.\r\n 是SMTP協議定義的data結束符號。

我: 250 Received

到這裡, sendmail 函數就完成了它的任務,發出一封郵件。其實, sendmail 正常工作,分析的是每一個請求對方發來的狀態碼(250, 354這些)。比如 sendmail 發送 data 字符的時候,如果你還是回覆250而不是354的話, sendmail 會認為你這個服務器有問題,就不再理你了。與之相比,後面的響應信息的具體內容,SMTP協議是沒有具體要求的。所以才會有五花八門的回覆。比如,gmail的服務器響應 helo 的內容是"at your service",而我這裡寫的是"Nice to meet you"。

一切發送完畢後, sendmail 返回了熟悉的 {} ,即空字典,表示信息發送成功了。後面我又調用了 server.quit() 結束對話。從下圖可以看到,這個函數在斷開連接之前,先給服務器發送了 quit 信息。我響應了 221 bye 之後,它才關閉了TCP連接。

當你發郵件時,你的電腦都幹了什麼?藉助Python探索SMTP協議

sendmail返回`{}`之後,調用quit函數發送“結束通信”的消息

總結

這一篇實驗有點長。在準備階段(實驗0),我們瞭解瞭如何使用 nslookup 查詢一個郵件域名對應的MX服務器地址。實驗1中,我們使用Python的 smtplib 包發送郵件,觀察了在應用層(SMTP層)上的情況,並且掌握了 smtplib 的使用方法。實驗2中,我們下潛到TCP層,開啟了一個簡單的人工SMTP服務器,接收 smtplib 發來的郵件發送請求。看到了 helo , mail FROM , rcpt TO , data , quit 這些標準的SMTP請求報文,也瞭解了服務器響應信息的"狀態代碼響應消息"格式。順便說一下,其實 smtplib 裡也是提供了 server.helo , server.mail , server.rcpt , server.data 這些函數的。有興趣的讀者可以自己嘗試一下。

通過親手進行這個實驗,相信讀者會對SMTP協議有一個更直觀的瞭解,以後再發郵件的時候,腦子裡會不會自動浮現出你的郵件管理程序在背後發送的這一連串請求呢?


分享到:


相關文章: