---
# System prepended metadata

title: 自然人眼中的 Email 通訊協定技術
tags: [SW, 自然組眼中系列]

---

# 自然人眼中的 Email 通訊協定技術
因為跨部門的合作中，需要程式自動化從公司信箱寄出，因此展開了這個篇幅。最後是用 Python 實做。
會牽扯到的協定分別是**LDAP、SMTP、IMAP、POP3**，每個擔任的角色不同，我實做的部份會多著墨在寄信 SMTP 以及認證方式 NTLM 上。

---

## 四大通訊協定
先簡單了解背景知識如下：
| 協定 | 全名 | 功能角色 | 常用 Port | 傳輸對象 | 協定類型 |
|-----|------|--------|----------|---------|---------|
| **LDAP**  | Lightweight Directory Access Protocol | 驗證 & 目錄查詢  | 389（LDAP）、636（LDAPS） | LDAP Server | Directory Access |
| **SMTP**  | Simple Mail Transfer Protocol     | 郵件傳送（寄信）  | 25、587（STARTTLS）、465（SMTPS） |  Mail Server（MTA） → MTA | Push Protocol |
| **IMAP**  | Internet Message Access Protocol  | 郵件存取（收信），多裝置同步信件 | 143（IMAP）、993（IMAPS） |  Mail Server | Pull Protocol |
| **POP3**  | Post Office Protocol v3           | 郵件下載（收信），單一裝置下載後刪除server副本 | 110（POP3）、995（POP3S） |  Mail Server | Pull Protocol |


### LDAP
參考 [What is LDAP?](https://teleforum.ethiotelecom.et/blogs/1446/What-is-LDAP) 以及 [鳥哥: 使用 LDAP 統一管理帳號](https://linux.vbird.org/linux_server/rocky9/0240ldap.php)
![image](https://hackmd.io/_uploads/HJ3CPoZGex.png)
使用者登入
- Mail Client 傳帳號密碼給系統
- 系統對接 LDAP，使用 Bind 操作驗證身份

### SMTP
參考 [Email Protocols Explained: SMTP vs POP3 vs IMAP](https://chamaileon.io/resources/email-protocols-explained-smtp-vs-pop3-vs-imap/)
![image](https://hackmd.io/_uploads/S1aywjZflg.png)

- 使用者透過 Mail Client 編寫並發送郵件
- SMTP Server 接收，查詢對方網域的 MX 記錄並發信
- 常用 MTA：Postfix、Sendmail、Exim
### IMAP/POP3
![image](https://hackmd.io/_uploads/SkHNDiZzxl.png)

- Mail Client 透過 IMAP 或 POP3 與 Mail Server 溝通
- IMAP：支援多裝置同步信件與資料夾
- POP3：單一下載信件，預設刪除伺服器副本


## 動手做 SMTP Email Sender
網路上 SMTP with Python 資源非常多，主要參考 [Sending emails with attachments in Python](https://mailtrap.io/blog/python-send-email-gmail/#Send-email-with-attachments) 以及 [Python Email](https://docs.python.org/3/library/email.html)，這邊不多贅述，多說明其中的認證方式尤其是 ntlm。


## 常見的 SMTP 認證方式
接下來進到重頭戲ㄌ，我們要用 smtp 寄信出去取決於該伺服器使用哪一種認證方式，像是公共網域常見的 gamil 是用 PLAIN / LOGIN，而公司內部網域使用較安全的 ntlm，伺服器不需要知道密碼本身，只需要確認用戶是否「能夠證明知道正確的密碼」。

| 認證方式   | 說明  |
| --------- | ---- |
| **PLAIN** | 最常見的方式，帳號與密碼以 Base64 編碼傳送，**需搭配 TLS 加密**以防洩漏|
| **LOGIN** | 與 PLAIN 類似，但分兩步傳送帳號與密碼，**一樣需要 TLS**|
| **CRAM-MD5**| 使用 HMAC-MD5 演算法，回傳加密的驗證資料，也是 Challenge-Response Authentication |
| **NTLM**    | 微軟自己的 Challenge-Response Authentication，常用於公司 Exchange 環境，**安全但複雜** |


### Challenge-Response Authentication
一種不直接傳密碼，而是用密碼去算一個答案來證明你是本人的登入方式，伺服器會丟 challenge，用戶端會用拿到的密碼算一個答案並 response，伺服器自己也算一次如果答案一樣就登入成功。

| NTLM 封包    | 對應角色                  | 說明     |
| ---------- | ------------------------- | -------- |
| Type 1 | Client 發送的 **negotiate**    | 用戶主動說：「我要登入，而且支援 NTLM，這是我能接受的安全參數」|
| Type 2 | Server 發送的 **challenge**    | 伺服器說：「這是你要解的挑戰（含隨機亂數、domain name 等）」  |
| Type 3 | Client 發送的 **authenticate** | 用戶說：「這是我的回應（用密碼 + challenge 算出來的 hash 值）」 |


### 實做 ntlm 認證
不過我們客戶端在 Python 中 smtplib package 並沒有直接提供 ntlm 接口，所以需要另外安裝有大神寫好的第三方 [pyspnego](https://github.com/jborean93/pyspnego) package，而 spnego 是微軟提供的一種 GSS-API 認證機制（先認知到這裡，難QQ)。

不囉唆上程式碼，完整程式碼請至 [github](https://github.com/keepgoing-228/smtpx/blob/main/smtp_email.py)

```python!
# create NTLM context
context = spnego.client(
    username=f"{domain}\\{username}" if domain else username,
    password=password,
    protocol="ntlm",
)

# send AUTH NTLM command by smtplib
smtp.docmd("AUTH", "NTLM")

# first step: send negotiate message
negotiate_token = context.step()
negotiate_b64 = base64.b64encode(negotiate_token).decode("ascii")

code, challenge_b64 = smtp.docmd("", negotiate_b64)
if code != 334:
    raise Exception(f"NTLM negotiate failed: {code}")
```
先準備好 ntlm 認證的 context，呼叫 context.step() 產生 NTLM Type 1 token，並把它用 base64 編碼後才能送給 SMTP server，server 回傳 code 跟 NTLM Type 2 base64，此時還不是 token。
code 334 代表已經傳 challenge 並等待你回應。


```python!
# second step: process challenge and authenticate
challenge_token = base64.b64decode(challenge_b64)
auth_token = context.step(challenge_token)
auth_b64 = base64.b64encode(auth_token).decode("ascii")

code, response = smtp.docmd("", auth_b64)
if code != 235:
    raise Exception(f"NTLM authentication failed: {code}")
```
把剛剛拿到的 base64 轉/解碼成 token，再次使用 context.step(challenge_token) 產生 NTLM Type 3 認證回應，並以 base64 再次送出。


## 結論
經過這次經驗有簡單的 email 輪廓，成功由公司信箱寄信出去，不過因為使用 python 在 OA 機上沒有運行的環境，所以我用 [Nuitka](https://nuitka.net/user-documentation/tutorial-setup-and-build.html) 多做一步成 binary executable program（而且還必須在 Windows 環境下，頭痛），未來要學習用 GO 來避免以後有類似的情節QQ