從零開始的 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

收拾行李搬家去:從 Medium 到 Ghost

收拾行李搬家去:從 Medium 到 Ghost

想搬家想很久,連身邊的朋友都搬完了,我還沒動工。 原因是我懶,我討厭麻煩,每次有人問我吃什麼,我都回答麥當勞。搬家是一件麻煩事,我已經有一份很讚的工作了,全副精神都放在工作上,偶爾才會想起來,反正家什麼時候都能搬,一點也不急,有什麼好急的呢對吧。這樣一拖,就拖到現在。 繼續用 Medium 不好嗎? 跟男女朋友分手一樣,通常被問到:「對方不好嗎?」得到回答是:「也沒有不好啦,只是……(以下開放填空)。」 從優點開始講吧!Medium 的編輯器很棒,它是 WYSIWYG(所見即所得)類型的編輯器,能讓創作者快速發佈內容,也因為它讓內容發佈更容易了,它開始吸引一批優秀的創作者,這批創作者持續創作內容,又吸引來更多讀者,更多讀者激勵創作者產出內容,內容又再吸引讀者……這形成一個增強迴圈。Medium 還能支援多人協作,拜它時尚簡約的風格所賜,科技公司會使用 Medium 來打造品牌形象,例如我前公司的 Tech Blog

By Ken Chen
OpenTelemetry 的可觀察性工程:以 Sentry 為例

OpenTelemetry 的可觀察性工程:以 Sentry 為例

點進 OpenTelemetry 的官方文件,它最先映入眼中的句子是「什麼是 OpenTelemetry」。例如,它是套可觀察性框架,用於檢測、蒐集與導出遙測數據;它是開源且供應商中立,能搭配其他的開源工具,像 Jaeger 或 Prometheus;它能將應用程式與系統儀表化,無關是用 Go 還是 .NET 開發,也無關部署在 AWS 還是 GCP 上。 但是身為一名開發者,當下我們想的是:「公司常開發一些沒人要用的功能,聽說 OpenTelemetry 可以提高可觀察性,也許我們應該放棄開發功能,轉頭建立更好的開發環境。」「AWS 常常要不到需要的數據,也許我們應該改用另一套工具,像是 OpenTelemetry,來解決這件事。」我們想像 OpenTelemetry 「應該」要能解決目前面臨到的一些問題,就像在技術的鏡像中尋找願望一樣。 如果已經有在用 Sentry,還需要導入 OpenTelemetry

By Ken Chen
標準化之路:Go 1.23 中的迭代器

標準化之路:Go 1.23 中的迭代器

Ian Lance Taylor 在 "Range Over Function Types" 這篇文章聊到 iterator 誕生的原因。如果我們有兩個容器,稱為集合(Set),想要取得這兩個集合中的不重複元素,加到新的集合中形成聯集,我們可以寫個 Union 函式來執行 // Set holds a set of elements. type Set[E comparable] struct { m map[E]struct{} } // Union returns the union of two sets. func Union[E comparable](s1, s2 *Set[

By Ken Chen
OAuth 2.0 的身份認證:OpenID Connect

OAuth 2.0 的身份認證:OpenID Connect

OAuth 2 讓網路服務可以存取第三方的受保護資源,因此,有些開發者會進一步利用 OAuth 2 來進行使用者認證。但這中間存在著一些語義落差,因為 OAuth 2 當初設計目的是「授權」而不是「認證」,兩者關注的焦點會有些不同。OpenID Connect 是基於 OAuth 2 的一套身份認證協定,讓開發者可以在 OAuth 2 授權的基礎上,再加入標準的認證流程。在這篇文章中,我會說明授權跟認證的場景有何差異,並講解 OpenID Connect 如何滿足認證需求。 因為 OpenID Connect 是建構在 OAuth 2 的基礎上,我會假設這篇文章的讀者已經知道 OAuth 2 的組件與流程,如果你不熟悉,可以先閱讀另外兩篇文章 * OAuth 2.0:

By Ken Chen