從零開始的 SMTP:以 Python 為例

從零開始的 SMTP:以 Python 為例

這陣子在重溫 SMTP,想說拿 Gmail 來測試,看看能不能從底層刻出一個簡單的 SMTP Client。沒有其他目的,本文會使用 Python 當範例,一步步實現 SMTP 協議。

Set Gmail

既然是用 Gmail,就要先把 Gmail 設定好。因為安全性的因素,Gmail 會管控部分應用程式,不讓它們登入,很不幸的,我們自己寫的 Python Script 就是所謂的低安全性應用程式。因此在測試前請先到 Google 設定頁面中開啟「允許低安全性應用程式」,測試完後再改回去。

Create a SSL Socket

在寫 Code 前,要先知道 Server 的位置跟 Port,參考 Gmail 頁面的說明

知道 SMTP 的 Server 是 smtp.gmail.com,SSL port 是 465。

創建一個 Socket,用來發送 SMTP

from socket import *
import ssl
import smtplib
import base64

mailserver = "smtp.gmail.com"
mailport = 465

context = ssl.create_default_context()

clientSocket = socket(AF_INET, SOCK_STREAM)
clientSocket.connect((mailserver, mailport))
clientSocket = context.wrap_socket(clientSocket, server_hostname=mailserver)

ssl 是用來加密,如果沒有加密,等於將自己的機密資訊暴露在網路上,現在比較正式的應用都會要求加密。如果沒有用 ssl 就要使用 tls,否則無法連線。

執行 connect 後,host 會跟 server 握手,雙方的連線就完成了。

Hello and Login

SMTP 的 Command 可以參照 RFC 821,連線相關的指令是 HELO 跟 AUTH LOGIN,前者用來通知身分,後者用來登入,記得要用 \r\n 結尾

recv = clientSocket.recv(1024).decode()
print(recv)
if recv[:3] != '220':
    print("220 reply not received from server.")

heloCommand = 'HELO Ken\r\n'.encode()
clientSocket.send(heloCommand)
recv1 = clientSocket.recv(1024).decode()
print(recv1)
if recv1[:3] != '250':
    print('250 reply not received from server.')

heloCommand = 'AUTH LOGIN\r\n'.encode()
clientSocket.send(heloCommand)
recv1 = clientSocket.recv(1024).decode()
print(recv1)

如果前面的操作都順利,應該會收到 server 的回覆

220 smtp.gmail.com ESMTP d6sm6677367pju.8 - gsmtp
250 smtp.gmail.com at your service
334 VXNlcm5hbWU6

後面 334 是等待客戶端輸入,VXNlcm5hbWU6 是經過 base64 編碼後的 username:,簡單講,Gmail 在等登入資訊。

同樣將登入的帳號密碼編碼後傳送給 Gmail

clientSocket.send(base64.b64encode("account".encode()))
clientSocket.send("\r\n".encode())
recv1 = clientSocket.recv(1024).decode()
print(recv1)
clientSocket.send(base64.b64encode("password".encode()))
clientSocket.send("\r\n".encode())
recv1 = clientSocket.recv(1024).decode()
print(recv1)

得到回應

334 UGFzc3dvcmQ6
235 2.7.0 Accepted

看到 Accepted 代表登入成功。

Send Mail

到這裡就可以開始寫信了,使用 MAIL FROM: 標明寄件人,使用 RCPT TO: 標明收件人

mailCommand = "MAIL FROM: <sender[@gmail.com](mailto:[email protected])>\r\n".encode()
clientSocket.send(mailCommand)
recv1 = clientSocket.recv(1024).decode()
print(recv1)
if recv1[:3] != '250':
    print('250 reply not received from server.')

mailCommand = "RCPT TO: <[[email protected]](mailto:[email protected])>\r\n".encode()
clientSocket.send(mailCommand)
recv1 = clientSocket.recv(1024).decode()
print(recv1)
if recv1[:3] != '250':
    print('250 reply not received from server.')

寄件人跟收件人填自己的帳號。

接著用 DATA 表示信件內容

dataCommand = 'DATA\r\n'.encode()
print(dataCommand)
clientSocket.send(dataCommand)
recv1 = clientSocket.recv(1024).decode()
print(recv1)
if recv1[:3] != '354':
    print('data 354 reply not received from server.')

收到

354  Go ahead d6sm6677367pju.8 - gsmtp

表示 Server 等著接收信件內容,這時可以填入正文。正文結尾要用 .\r\n

message = 'Hello, world\r\n.\r\n'
clientSocket.send(message.encode())
recv1 = clientSocket.recv(1024).decode()
print(recv1)
if recv1[:3] != '250':
    print('end msg 250 reply not received from server.')

Gmail Server 就會幫忙寄出這封 SMTP 報文。

Quit

完成後,不要忘記結束跟 Server 的連線,使用 QUIT

quitCommand = 'QUIT\r\n'.encode()
clientSocket.send(quitCommand)
recv1 = clientSocket.recv(1024).decode()
print(recv1)
if recv1[:3] != '221':
    print('quit 221 reply not received from server.')

打開 Gmail,看看成果

小結

簡單用 Python 跑一次 SMTP 的流程,其實就是不斷寫進各種指令,看看會吐什麼出來,好像有點造輪子的感覺?這類基礎打磨好,對熟悉網路通訊協議很有幫助。如果只是要寫應用,Python 有提供 smtplib,可以 call method 直接搞定。或者不一定要用 Python,直接用 telnet 跟 Server 連線也是個方式。

Reference

Read more

Weekly Issue 第 26 期:AI 批評指南

最近在讀《高效槓桿力》,書中提出一套變革管理框架:「尋找關鍵支點,重新配置資源。」當然,書裡給出很多案例,說明如何找到支點,只是我同時在想,如何將他們帶到我面對的情境呢? ✨ 科技觀點 Pluralistic: The Reverse-Centaur’s Guide to Criticizing AI 看到有人非常認真討論事情,即使是批評 AI,都會讓我有興趣。 附上一些我的觀點: 1) 成長型公司聽起來很美好,每個人都會想待在那,但當它變成前提時就是另一回事了。很多決策都會以成長為基礎,最後就是投資人跟企業都沒辦法接受不成長的代價。 2) 常常在爭論 AI 是否會取代工作,看的是 AI 的兩個面向,賦能與自動化,哪個會更符合當前情境。贊同賦能的人會認為 AI 帶來生產力的解放,並創造價值,可是實際上呢? 3) 很多人提過 AI 的解壓縮 / 壓縮特性,特別是在履歷或信件應用。

By Ken Chen

Weekly Issue 第 25 期:Slack 基礎設施爭議

因為地緣政治議題,我們會關心資料存放的地點是否足夠安全,即使當使用者被盯上,他仍然可以放心資料足夠隱密。這也是為什麼當網路上傳出 Slack 台灣的資料轉移到阿里雲時,會引起爭議的原因。 Slack 已經出面澄清並無此事,這也讓我們反思,當軟體業面臨這類公關危機時,應該要揭露到什麼程度。 🗞️ 熱門新聞 Slack 在臺服務將移轉至中國? Salesforce:臺灣用戶使用全球基礎設施,與阿里巴巴無關 前幾天 Salesforce 傳出要將 Slack 台灣資料轉移到阿里雲,立刻引起一陣討論,有 Salesforce 的人出來澄清,說沒有這回事。 「台灣市場一直以來都是採用 Global Infrastructure 全球基礎設施。簡單說,台灣用戶的資料是儲存在美洲或亞太區(如日本),跟中國的阿里雲在物理和邏輯上都是完全切開的。 」 讓我有興趣的是,Salesforce 沒有說他們是用哪個雲平台。我們以前有次遇到類似情況,也討論到是否揭露使用平台。當時我持反對意見,認為只需要揭露「使用全球基礎設施」已經夠了,頂多說非中國廠商的服務就好,不需要也不應該說明具體是哪個。

By Ken Chen

Weekly Issue 第 24 期:網路的精神高地

前陣子去了雪梨一趟,跟布里斯本或台北都形成有趣的對比,旅行中也不斷在想,一座城市如何發展出自己的文化?這有點像是網路平台如何形成聚落,而又如何消亡。 很喜歡本期談知乎的一篇文章,理想主義的光輝是最吸引人的,我常在想,有沒有辦法將那座「看不見的城市」帶到真實世界中。 🗞️ 熱門新聞 A ChatGPT prompt equals about 5.1 seconds of Netflix 看到 Simon Willison 提到,如果 Sam Altman 的資訊是對的,每個 LLM 提問相當於 5.1s 的 Netflix 影片耗能。 計算的需求讓輝達跟台積電挖到金礦,那電力需求又會讓誰挖到金礦呢? ✨ 科技觀點 我们失去的不只是知乎,而是中文互联网的精神高地 「那时的知乎,更像“思想沙龙”,而非“内容平台”。」 昨天跟朋友聊天,

By Ken Chen

Weekly Issue 第 23 期:Mastodon CEO 離職感言

電子報本質是種自媒體,儘管我發文前都會確認,還因為能力所限,偶爾還是有沒做好的地方。每次遇到時我都會想,不知道其他自媒體是如何查證的呢? 現代的訊息越來越快,不只是自媒體,很多專業媒體也不見得有完備的查證能力,我猜當內容氾濫,「真實」會變得越來越有價值,最終變成一門生意。 🗞️ 熱門新聞 Explore the independent web Ghost 最新一期的電子報談到他們如何處理「內容發現」的問題。 簡單來說,他們有個內容發現工具 Ghost Explore,如果創作者願意提交自己的網站數據,他們能依照這些網站數據來推薦。再來,他們還會參考 ahrefs 的資料,判斷該網域是否具有高品質。 這比 Substack 發展社群工具,更貼近我對產品的想像。現代內容網站基本都需要演算法,這已經不是要不要,是怎麼設計的問題。 My next chapter with Mastodon Mastodon 的 CEO 即將卸任,他發了篇談談這段時間的心路歷程。

By Ken Chen