你發郵件出去的時候!你的電腦洩露了這些東西?你還不知道吧?
你是否需要每天使用電子郵件服務?
電子郵件(email)是網際網路上歷史悠久又常用的訊息收發形式。對於大多數辦公室一族,每天到班上的第一件事恐怕就是要查一下新的郵件。雖然即時通訊工具在飛速佔領著通訊市場,但是在商業或者學術圈裡,email依然佔據著主流地位。
當你點選“傳送”之後,你的郵箱做了那些操作?今天的實驗帶你親自看一看。
引言:網際網路協議和SMTP協議
在之前的一篇文章(點選這裡檢視)中,筆者討論了網際網路協議的幾個層,並且構建實驗探討了提供網頁服務的HTTP伺服器如何工作。如果你也喜歡探索原理,並且還沒有做過這個實驗,那麼強烈建議你點開連線,跟著文章中設計的實驗探索一下HTTP協議。這裡,我們會做一個類似的實驗來窺探email收發使用的SMTP協議。
簡單概括原理:我們的網際網路分為四個層,每一層的正常工作建立在下面層的基礎上。工作在最上層的“應用層”,有提供網頁服務的HTTP協議,提供郵件收發的SMTP協議,提供檔案傳輸的FTP協議等等。這些協議想要正常工作,都要基於下面“傳輸層”的支援。傳輸層比較常用的是TCP協議。今天的實驗裡,我們將在SMTP層和TCP層兩個層面上觀察SMTP協議,並且在TCP層上構造一個簡單的,需要手動控制的“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 都已經存在於你的電腦裡了。使用它的步驟如下:
- 開啟命令列。Windows系統:開啟“開始”選單,輸入"cmd",搜尋到“命令提示行”工具。開啟後介面如下。
windows中的命令提示行
Mac OS在應用程式中找到"Terminal"。Linux我就不說命令列在哪裡了。
- 在命令列中輸入 nslookup 按回車,進入 nslookup 工具中。輸入 set q=mx ,指定查詢mx域名。輸入 126.com ,按下回車,你就會得到查詢結果。
nslookup查詢126.com的mx域名
圖中可以看到, 126.com email域名背後有4個mx伺服器。後面的討論中,使用任何一個mx伺服器(比如“126mx01.mxmail.netease.com”)都可以。友情提示:複製mx域名時,注意不要把最後的句號複製上了。
實驗1(應用層):使用Python傳送email
當然,很多時候這樣的郵件會被對方SMTP伺服器拒收。即時接收了,也有可能因為來源不明而被放到垃圾郵件裡。所以,並不建議讀者用這種收發日常郵件。但是為了弄懂SMTP的協議,這樣做一兩次還是值得的。
話不多說,先上一個完整的郵件傳送的截圖。注意,我作為發件人,並沒有登入任何自己的郵箱。另外,注意變數 s_body 的格式。大部分郵件伺服器對這個格式很看重。不符合這個格式的郵件經常會被拒絕。最後,注意在 server.connect 那一行執行之後,後面手速一定要快。隔一會再執行下一行的話,對方伺服器通常會斷開。
Python中使用smtplib傳送郵件。要先把收件人地址、發件人地址和郵件內容都實現編輯好,存在變數中。避免連線到伺服器之後,由於超時沒有響應而導致伺服器斷開連線
實驗2 (傳輸層):使用TCP socket假裝自己是個SMTP伺服器
怎麼才能搞清楚 server.sendmail 揹著你跟伺服器幹了什麼事情呢?好吧,換個問題:假設你懷疑你物件在網上見了漂亮妹子/帥氣男生就勾搭,怎麼才能抓住他/她的把柄呢?一個方法就是,自己註冊個上網賬號,把自己偽裝成漂亮妹子/帥氣男生,跟他/她聊。
我們知道,像 requests 一樣, smtplib 要想進行SMTP通訊,一定會使用下面傳輸層的TCP協議,跟對方的SMTP伺服器建立TCP連線。所以,我們就像上一篇文章那樣,準備一個TCP連線,把對方發過來的資料都顯示到螢幕上,具體哪些訊息,什麼格式,就一目瞭然了。
跟http請求不同的是, server.sendmail 不是一個 單次 的請求/響應,而是要求雙方使用協議規定的格式 反覆提問回答幾次 才可以完成郵件傳送。因此,我們的伺服器裡也要仔細設定響應的內容,確保返回的東西符合格式,使得對話能繼續進行。(也就是說,想假裝自己是SMTP伺服器,比假裝自己是HTTP伺服器要難一點,穿幫的可能性也更大一點。)為了增強體驗感,我們在每次收到資訊時,讓我們手動填寫返回內容。
我們先來看程式碼。
""" 這是一個虛擬伺服器。當任何程式簡介到它時,它先發送一條歡迎資訊WELCOME_MSG, 然後等待對方傳送資訊。對方每傳送一條資訊,它就會把資訊顯示到螢幕上,然後提示 我們輸入應答內容。緊接著,它會把我們輸入的內容後面加上換行符 ,傳送回去。 """ SERVER_IP = "localhost" SERVER_PORT = 25 #預設的SMTP之一 MAX_LENGTH = 1023 #規定每條資訊長度上限。 WELCOME_MSG = "220 Virtual Server At Your Service! " #歡迎資訊 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("收到資訊: ", data) if len(data) == 0: break reply_msg = input("您的回覆: ") reply_msg += ' ' 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)
- 伺服器開啟。注意由於使用了25埠(SMTP協議的預設埠之一),為系統預留埠,因此程式需要管理員許可權。第一幅圖中,第一次嘗試由於沒有用管理員許可權 sudo 而被拒絕執行了。加了 sudo 程式得以啟動,並開啟埠,等待連線。
開啟伺服器。第一次由於沒有使用管理員許可權而被拒絕。第二次成功開啟,進入等待連線狀態
- 使用Python3的 smtplib 連線SMTP伺服器。這一步跟上面實驗是一樣的。注意 server.connect 連線的是 'localhost' 。此時,右邊圖中伺服器也顯示收到了連線,併發送了歡迎訊息'220 Virtual Server At Your Service!' 這時從 smtplib 接收到的資訊來看,它識別了這種 狀態碼<空格>回覆資訊 的格式,返回了一個二元素的陣列。
進群:548377875 即可獲取數十套PDF以及大量學習資料!
python的smtp連線,伺服器傳送來歡迎資訊
接下來,我們就重複上面實驗中的做法,定義 s_from , s_to 和 s_msg ,然後交給 server.sendmail 函式來以郵件形式傳送出去。見下圖。
使用smtplib的sendmail傳送郵件
- 接下來的圖 很重要! 它顯示了sendmail函式執行後,伺服器上收到的 一連串資訊 。
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: <郵件正文> # 注意:這段文字最後的 . 是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連線。
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協議有一個更直觀的瞭解,以後再發郵件的時候,腦子裡會不會自動浮現出你的郵件管理程式在背後傳送的這一連串請求呢?
感謝您的支援,如果您有任何疑問,或者建議,或者還想看什麼簡單的探索計算機的小實驗,歡迎給我留言!