從零開始的 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 第 7 期:從 GitHub Spark 看 Prompt 工程

近期開始有人建議用 Context Engineering 來取代 Prompt Engineering,的確相較於 Prompt,Context 是更精確的用詞。前一期也提到,當 Duolingo 的 CEO 被問到 AI 是否只是模型套皮時,他也說模型一定有影響,但更多是關乎你的 Context。 那麼,業界現在是如何看待 Prompt 的呢?Github Spark 跟 V0 的例子或許能提供一些參考。 🗞️ 熱門新聞 Using GitHub Spark to reverse engineer GitHub Spark GitHub Spark 最近推出公開預覽,讓你可以用 prompt 直接開發應用。 作者用逆向工程,找出 Spark 的 system

By Ken Chen

Weekly Issue 第 6 期:Duolingo CEO 看 AI 與遊戲化

現在是 AI 時代,大家都在想怎麼讓自己的產品跟 AI 掛勾,但具體要怎麼做呢?背後的思考有哪些?Duolingo 給出他們自己的觀點。 例如,現在的產品是否只是 AI 套皮,你接收使用者的問題,套上自己的提詞後,拿去給 OpenAI,要它回答你?在現在百家爭鳴的情況下,選擇哪個模型會有差嗎?AI 能帶來新用戶與新營收嗎?等等。 另外本週也選了一篇少數派的文章,談 AI 對 RSS 的影響,對 RSS 未來方向有興趣的人不妨看看。 🗞️ 熱門新聞 Duolingo CEO Luis von Ahn wants you addicted to learning Duolingo CEO 專訪,相當紮實,推薦閱讀。 「對我們來說,

By Ken Chen

Weekly Issue 第 5 期:OpenAI 的企業文化

我一直都喜歡看科技公司的願景與文化,原因是,我想知道別人是如何看待自己的使命,又是用什麼方式打造它。願景通常在官網都會有,但想要知道文化,只能聽內部人講講了。 Palantir 前陣子因為它不同於矽谷的文化,而引起很多討論。受此影響,前 OpenAI 的員工在離職創辦公司後,也發文談論他所見到的 OpenAI。最讓我震撼的是,他們幾乎沒有資金困擾,想的都是如何打造出色的 AI 模型。 🗞️ 熱門新聞 Reflections on OpenAI 前員工談 OpenAI 的內部文化。 讀起來最大的感觸是,有些價值觀、觀點、實踐,只有在世界級的公司跟資源下,才有可能建立起來。讓每個團隊各自為政,看誰能端出最好的成果,這對新創(特別是沒拿創投)實在太奢侈了。 我相信這種經歷會變成是「可以帶著走的饗宴」,那種衝擊也是最寶貴的。 AI Open Source Productivity METR 前陣子發了一篇研究,說使用 AI

By Ken Chen

Weekly Issue 第 4 期:Canonical 的面試經驗

這星期看了比較多職涯相關的內容,最讓我驚訝的是 Canonical 的面試流程,當我分享這則新聞後,有更多朋友紛紛補充他們的面試經驗:需要經歷三個 Tier,每個 Tier 都有三關,而內容甚至還包括問人選「高中成績」與「大學生活」。 我很難想像一家做 Linux 發行版的公司,會如此草率對待人選,這讓我對他們家的產品有了很大的問號。 🗞️ 熱門新聞 My experience with Canonical's interview process 這是一篇 Canonical 的面試經歷(如果你不知道什麼是 Canonical,就是開發 Ubuntu 的公司)。 整個過程讓人非常驚訝,甚至還需要人選回答「高中成績」,而在面試中做筆記居然是扣分項。我看完後有股移除 Ubuntu 的衝動。真的太扯啦。 What happens when engineers work

By Ken Chen