使用 Postfix 來測試 Email 寄送功能
How Email Works
Email 在我們生活中大概還扮演著相對重要的角色啦
至少我是很常用的
平常跟公家單位對接,亦或著是面試公司回覆郵件等等我都是信手捻來
CC, BCC 等等觀念也很熟悉
不過這是站在使用者的角度
對於實際上要怎麼收發信
雖然接觸過,但終究沒有仔細深究過
MTA, MDA and MRA
在電子郵件系統中會分成所謂的 MTA, MDA 以及 MRA
MTA
Mail Transfer Agent(MTA) 是 郵件傳輸代理
他是負責寄送以及轉送郵件
SMTP
傳輸這件事,當然要定義一個傳輸協定,所以 SMTP 這種技術標準就是為此而生
要注意到 SMTP 是郵件傳輸通訊協定
白話文說就是,寄信用的協定是一套,收信用的協定是另一套
SMTP 是基於 TCP 的協定,定義於 RFC 5321 與 RFC 3207
傳輸的過程滿簡單的,就是
- 建立 TCP 連線(port
25or587) - 傳送 HELO 或是 EHLO 命令開始電子郵件傳輸(可參考 SMTP Envelop)
- 然後等傳輸完成就關閉連線
SMTP Envelop
要傳去哪路要怎麼走,實際上是放在 SMTP Envelop 裡面
他跟信件內容本身是獨立的
Envelop 裡面,實際上是一系列的 “指令”
過程會是這樣子的
-
HELO/EHLO: 你好,我要開始建立 SMTP 連線了 -
MAIL FROM: 這封郵件是從 xxx 來的 -
RCPT TO: 這封郵件是要給 yyy/zzz 的 -
DATA: 宣告郵件內容是以下這些
1
2
3
4
5
6
7
8
9
10
DATA
Date: Mon, 4 April 2022
From: Alice alice@example.com
Subject: Eggs benedict casserole
To: Bob bob@example.com
Hi Bob,
I will bring the eggs benedict casserole recipe on Friday.
-Alice
。
-
RESTorQUIT: 關閉連線(或是清空所有東西然後再關閉連線)
SMTP 的運作方式是採一問一答
我逐條的宣告我要傳到哪,我是誰,我要傳的內容是啥
但不知道你看到這有沒有一些疑問
為什麼 FROM/TO 出現在 SMTP Envelop 與 Data 這兩個地方?
Bounced Email
這跟 “退信” 有很大的關係
被退信有多種可能,比方說
- 你寄到錯的地址,查無此人
- 對方的信箱已經滿了,被退信
- 被當垃圾信拒收
被退信之後會發生什麼事情
你的信件會原路返回到你的收件匣
那這跟 FROM/TO 在兩個地方出現有什麼關係
信件在發送過程中,看的是 SMTP Envelop 裡面的寄件資訊
但是你信裡面要寫 我是誰 那是你自己決定的
也就是說兩個 FROM 可以長不一樣
如果被退信,我看的當然是 SMTP Envelop 裡面的寄件資訊
他從哪來就該從哪回去
至於說你裡面宣稱你是誰,你開心就好
聰明的你是不是以為你可以冒充任何人呢?
當然不行,我們會在 Validate an Email 有更多討論

跑起來大概長這樣
我原本以為跑失敗,結果他花了滿久的時間才退信回來
等會的實驗我們會去看,不同 FROM 他會不會退回正確位置,可參考 Experiment
MDA
Mail Delivery Agent(MDA) 是 郵件投遞代理
當信件抵達電子郵件系統當中,他需要找到正確的位置放進去
就是要到對的郵箱當中,一個電子郵件系統上面可以有很多個收件匣
所以 MDA 的功能是將郵件放到正確的收件匣
MRA
Mail Retrieval Agent(MRA) 是 郵件檢索代理
想不到吧,你想看 Email 還要有一個代理負責幫你把信件拿出來給你看
為什麼? 我要看個 Email 為什麼要有一個代理幫我拿信
因為郵件伺服器 不直接與使用者互動
IMAP and POP3
如同 MTA 利用 SMTP 來寄信
MRA 也需要利用 IMAP 或 POP3 之類的協議來讀取信件
IMAP 協議 並不會下載 Email 到客戶端
他允許直接從客戶端 讀取 他們的 Email
相反 POP3 協議就是下載到本地了
但是這個下載之後,伺服器上面的資料就會消失
| IMAP | POP3 | |
|---|---|---|
| 存取 | 不同裝置 | 只能同裝置 |
| 收信模式 | 線上 | 本地 |
| 儲存地 | 伺服器 | 伺服器 or 本地(下載) |
| 網路 |
Validate an Email
如果你能夠存取 SMTP Server,就像我們在 Bounced Email 裡面提到的
你可以冒充任何人,顯然這是不合理的
所以開發者想出了幾種對策,Sender Policy Framework(SPF), DomainKeys Identified Mail(DKIM) 以及 DMARC
Sender Policy Framework(SPF)
其中一種冒充方式是,我在惡意 SMTP Server 上假冒我是 gmail.com 發 Email
最簡單的驗證是,SMTP Server 是不是隸屬於 gmail.com
domain name 與 ip 的對應關係可以依靠 DNS Server 上面的 DNS Record 反查
所以 SPF 的核心就是,看送信的伺服器有沒有在白名單裡面
有關 DNS 可以參考 重新認識網路 - 從基礎開始 | Shawn Hsu

SPF 的核心防護是很薄弱的,比如說我用個簡單的 DNS Hijacking 就可以輕鬆處理了
DNS Hijacking
DNS 本質上就是個 Server
那攻擊者劫持 DNS 之後,把你導向到錯誤的地方就可以了
有關 DNS 可以參考 重新認識網路 - 從基礎開始 | Shawn Hsu
那具體要怎麼劫持勒? 你可以
- 直接透過釣魚信之類的方式把 Domain 偷過來,然後把它指到惡意 ip 地址
- 打 DNS,可能透過尚未修復的漏洞之類的
DomainKeys Identified Mail(DKIM)
所以你知道了,Sender Policy Framework(SPF) 其實沒到很安全
除了上述提到,DNS Hijacking 就能夠攻破
另外他還有一個問題是,如果 SMTP 是扮演著 Relay 的角色,那不就對不上?
另外一種我們能想到的就是用密碼學的方式去驗證你是你
我第一個想到的是公鑰私鑰的機制
所以就是
- Domain 有私鑰
- 公鑰就放在網路上
- 發 Email 的時候用私鑰簽
- 收到的人拿公鑰去驗證是不是該 Domain 簽發的
那我就好奇了
還不是 DNS,如果又被劫持放假公鑰,那還不是會錯
所以 DomainKeys Identified Mail(DKIM) 解決的是資料沒有被篡改的問題
DMARC
DMARC 在做的事就很簡單了,他決定如果 Sender Policy Framework(SPF) 或 DomainKeys Identified Mail(DKIM) 失敗了該怎麼辦
你要讓他通行? 讓他丟到垃圾郵件?
這些機制就是由 DMARC 所決定的
Summary
| Sender Policy Framework(SPF) | DomainKeys Identified Mail(DKIM) | DMARC | |
|---|---|---|---|
| 目的 | 驗證 Email 是否來自該網域 | 驗證 Email 是否來自該網域 | 決定發生錯誤的時候怎麼做 |
| 方法 | 純 DNS 驗證 | 密碼學驗證 | - |
| 儲存位置 | DNS | DNS | DNS |
有關 DNS 可以參考 重新認識網路 - 從基礎開始 | Shawn Hsu
What is a Relay
所謂的 Relay 就是中繼站的概念,郵件從出生到收到信中間可能會跑過非常多不同的地方
當然實務上你可以在你的 Golang app 直接寄 email 到比如說 gmail.com 這種
不過通常人們不會這樣做
出於滿多種原因的,比如說
- 集中控管防護,一家公司每個部門都要獨立送信,你會沒辦法管,透過 relay 當作是企業對外發信出口
- 資安控管,透過 relay,你可以把不同職責分開。app 負責信件內容,relay 處理憑證那些
- 架構解耦,relay 有 Queue,你不需要管對方 SMTP 有沒開著,就丟到 relay 上面,他會自己重試
一封 Email 可能會經過多層 relay 最後才會到你手上
Open Relay
轉發這件事情你如果沒設定好,就會變成所謂的 Open Relay
意思是,任何寄到你這裡的 Email 你都無條件幫他進行轉送
這有很大的問題,你就會變成 垃圾郵件幫凶
只要惡意人士發現,他就會利用你的伺服器發送各種釣魚信件、詐騙信件以及病毒信件
然後你的伺服器會被瞬間黑名單,封鎖你的 ip
好加在,現在的 SMTP 預設都會是關閉的,別人無法利用此等漏洞了
Postfix
Postfix 是一款 MTA 軟體
注意到他不包含其他兩種,也就是說,他只能夠發信,轉信等等
你沒辦法在 Postfix 收信(i.e. MDA),也不能在 Postfix 讀信(i.e. MRA)
講了那麼多還是得跑起來看看才有感覺對吧
Prerequisite
1
2
3
4
5
$ docker --version
Docker version 29.4.0, build 9d7ad9f
$ uname -a
Darwin workstation 25.6.0 Darwin Kernel Version 25.6.0: Fri Jul 31 19:16:36 PDT 2026; root:xnu-12377.161.14~5/RELEASE_ARM64_T6030 arm64
Experiment
把 container 跑起來
1
2
3
4
5
6
7
8
9
10
11
12
$ docker run -it -d --rm \
-p 2525:25 \
-e POSTFIX_myhostname=ambersuncreates.com \
-e POSTFIX_mydomain=ambersuncreates.com \
-e POSTFIX_always_add_missing_headers=yes \
-e POSTFIX_relayhost=host.docker.internal:1025 \
-e ALLOWED_SENDER_DOMAINS=ambersuncreates.com \
boky/postfix:latest
$ docker run -it -d --rm \
-p 1025:1025 -p 8025:8025 \
axllent/mailpit
Send an Email
根據 SMTP Envelop 我們可以很輕易的手打 Email 如下
1
2
3
4
5
6
7
8
9
10
11
EHLO localhost
MAIL FROM:<noreply@ambersuncreates.com>
RCPT TO:<ambersun1019.shawn+123@gmail.com>
DATA
Subject: Test email
From: hello@ambersuncreates.com
To: ambersun1019.shawn+789@gmail.com
This is a test email
.
QUIT
結果就會長這樣

但是他卻顯示

其實也滿合理的,我自己的網域完全沒設定過 Sender Policy Framework(SPF) 與 DomainKeys Identified Mail(DKIM)
那也難怪,所以 Google 就把我的 Email 給擋下了
那要怎麼測試他能夠真正寄出?
本地額外再起一個 mailpit
再送一次

就能夠正確的收到了
Bounced Email

這邊你就可以看到,From 以及 Return Path 是長得不一樣
對應到我們為什麼在發 email 的時候是寫兩個 From
SMTP Envelop 這邊寫的 From,如果退信就是回到這個位置
也就是 noreply@ambersuncreates.com
BCC and CC

我在這看到為什麼會有 BCC?
+123 那個我是寫在 SMTP Envelop 裡面的,因為他是不可見的所以是 BCC?
那我就好奇了,是不是
- 有多個
RCPT TO就等於 BCC - 有多個
To就等於 CC


顯然不是,不過可以確定 BCC 就是用 RCPT TO 去做的
CC 這個其實滿簡單的,就是 Cc 這樣
所以做起來是


Leave a comment