前幾週收到 Gandi 的信,說接下來要把免費 Email 收回去了,看了一下之後一年要 1,500 多,考慮到使用量不多,就來研究看看怎麼自己搞一個。自己搞的主要目標就是達成和原本效果類似,同時成本不會增加太多。目前想嘗試的就是 Cloudflare Email Routing,但這個只能收信,所以寄信就還需要另外找方法了。
補:後來發現 Haraka 對於私人使用不是必要的存在,文末說明,如果你是自己用而無須使用者管理的話,可以跳過 Haraka 部分。
需求
- 能夠收信,但不需要有太大的儲存空間(因為有串到 Gmail,抓回去後會刪掉原本伺服器的信)
- 能夠寄信
第一點上 Cloudflare Email Routing 就能夠做到,和 Gandi 一樣,可以新增(應該是無限多個)名稱並指定轉寄對象,此外也可以選擇 Drop 或者送到 Cloudflare Worker 處理。
在開始處理第二點前,首先先說明 Email 寄信到底是怎麼回事。
Email 的網路協議們
平常我們使用 Gmail 等服務,都是把對方的 Email 填上,然後寫標題內文、夾個附件送出就好,但就像其他服務一樣,看不見的部分處理了相當多的事情。直觀而言,我們可以分為寄件者、伺服器、收件者三個角色,並有寄信與收信兩個流程。這次我們關注在寄信流程。
寄信
當你按下送信之後,到底發生了什麼?首先,使用者端會用某些方式告訴送信伺服器他要送信,接著送信伺服器根據 Email 地址中的域名查詢收信伺服器,通過 SMTP(Simple Mail Transfer Protocol)把信件送到收信伺服器。
此時,收信伺服器並不一定會知道這個送信來源是不是假冒的,因此會有兩個額外的協議: SPF 與 DKIM。SPF Sender Policy Framework 是在 DNS 裡面加入一條 TXT 紀錄,說明允許送信的地址。例如 v=spf1 ip4:192.0.2.1 -all
代表只有來自 192.0.2.1 的是可信的,-all
則代表排除所有其他 IP。
DKIM DomainKeys Identified Mail 這一層是對 Email 內容加上密碼學簽章,確保寄件方的身分。作法與 SPF 類似,在 DNS 加上 TXT 紀錄,如 v=DKIM1; k=rsa; p=...
,告訴收信伺服器信件應該被哪個公鑰簽名。
在這之外,DMARC 則負責上面驗證失敗時的回報,幫助管理者找到問題,但相較於 SPF 與 DKIM 沒設定就容易使信件被判定為垃圾信,DMARC 似乎不會有這個問題,因此在此省略。
驗證通過後,收信伺服器便處理信件,例如轉寄服務可能直接發起另一次寄信流程,或一般服務將信件儲存於伺服器。
啟用 Cloudflare Email Routing
如果沒有設定過其他服務,啟用會是相當簡單,只需要側邊欄找到電子郵件路由,跟著步驟一步步進行就可以完成。
開始搭建 Haraka
從上面的流程,不難看出我們需要一台送信用的伺服器,我選擇 Haraka,主要是評估後認為容易在這之上延伸開發(通過 Plugin),架設看起來也不難,而解決的問題幾乎完全契合需求。
由於是私人用的,我想並不會有太大的硬體需求(畢竟一個月可能就寄個一兩封),我選擇在既有的 VPS 上面用。
安裝 Haraka
如果沒有 Node.js 的話需要先安裝,安裝完後可以用任意的 Package Manager 安裝。
yarn global add Haraka
接著建立設定檔案。(這邊是建立在使用者目錄下的 haraka
資料夾,隨後出現的指令都假設工作目錄是 ~/haraka
)
haraka -i ~/haraka
設定 TLS
由於 SMTP 沒有加密的規範,我們需要提供 TLS。我們可以使用 acme 來取得 Let’s Encrypt 的簽證。首先安裝 acme
curl https://get.acme.sh | sh -s [email protected]
由於這台主機上沒有網頁伺服器,因此我只能選擇用 DNS 手動驗證(似乎 DNS 也有自動化的方式,但我沒有嘗試),可以參考用網頁伺服器認證的方式。注意這邊的網域 A 紀錄要對應到寄信服務,不然 Google 會出現域名不相符的錯誤。
acme.sh --issue --dns -d mail.limaois.me --yes-I-know-dns-manual-mode-enough-go-ahead-please
# 新增 TXT 紀錄後
acme.sh --issue --dns -d mail.limaois.me --yes-I-know-dns-manual-mode-enough-go-ahead-please
> ...
> Your cert is in: /home/flyinglimao/.acme.sh/mail.limaois.me_ecc/mail.limaois.me.cer
> ...
這邊的檔案位置可以先複製起來。
接著修改 config/plugins
,將 tls
取消註解,再建立 config/tls.ini
,放入
key=/home/flyinglimao/.acme.sh/mail.limaois.me_ecc/mail.limaois.me.key
cert=/home/flyinglimao/.acme.sh/mail.limaois.me_ecc/fullchain.cer
這邊的值即是剛才輸出的結果,注意 cert 要使用 fullchain。
設定 SendGrid 作為 SMTP Relay
在實際操作過程中,發現多數的 Email 服務方(Gmail、Hotmail 等)都只收白名單內的信,而我們自己的伺服器很可能就不在之中,因此需要經過一個 SMTP Relay。
SMTP Relay 的作用是在寄信時,不直接寄給目標伺服器,而是請中間的 Relay 轉寄,由中間的 Relay 代替目標伺服器去驗證來源。我選擇 SendGrid 作為 Relay 服務。
設定 SendGrid 的 SPF 與 DKIM
前面提到為了避免信件被阻擋,我們需要設定 DKIM,這裡是通過 Domain Authentication 完成。由於我註冊時是帶領我做 Single Singer Authentication,不確定是否可以在一開始就只做 Domain Authentication,或者從左邊的 Settings > Sender Authentication 來完成。
按照步驟的話,應該會建立三個 CNAME,其中兩個有包含 _domainkey
。
而 SPF 需要手動調整,找到 Cloudflare 代為建立的 SPF 紀錄後,插入 include:sendgrid.net
變成
v=spf1 include:_spf.mx.cloudflare.net include:sendgrid.net ~all
後儲存即可。
取得 Relay Credentials
接著進入 Email API 中的 Integration Guide,就會提示要用 API 或者 SMTP Relay,我們選擇 SMTP Relay。
接著建立 API KEY。
然後回到 Haraka,編輯 config/smtp_forward.ini
:
host=smtp.sendgrid.net
port=587
enable_tls=true
auth_type=plain
auth_user=apikey
auth_pass=<上面的 Password>
enable_outbound=true
儲存退出後,會到 SendGrid 的頁面,先按下 Next Verify Integration,但目前伺服器還沒辦法送信,因此先停留在該畫面。而這樣一來,我們的對外信件就會先經過 SendGrid,Gmail 等就不會阻擋了。但似乎有機會被判斷成廣告文件,算是一個小小缺點。
設定 SMTP
在設定 SMTP 上,我們要建立登入機制。首先先開啟 config/plugins
,將 auth/flat_file
取消註解。
接著建立 config/auth_flat_file.ini
,內容如下
[core]
methods=PLAIN
[users]
username=password
這邊雖然支援 MD5,但實際測試在之後 Google 登入會出現問題,因此使用 PLAIN。
啟動伺服器
執行這個指令先將伺服器啟動看看:
haraka -c ~/haraka
接著到 Gmail 中嘗試登入,Port 是 25 並使用 TLS,登入後試著寄一封信,再回到 SendGrid 驗證。
若是沒有問題,最後一步就是 Daemonize,先關閉伺服器後編輯 config/smtp.ini
,找到 daemonize
的三行取消註解:
...
; Daemonize
daemonize=true
daemon_log_file=/var/log/haraka.log
daemon_pid_file=/var/run/haraka.pid
...
再次啟動服務,就會自動轉為 Daemon,我們就可以安心切斷 SSH 了。
後記
我不太確定這篇文章的步驟是否能夠確實完成,因為在這些步驟以外,我其實繞了許多圈。首先第一個是 Gmail 登入時會查詢域名的 A 紀錄來檢查 TLS,而由於我原本是直接掛在頂層域名,所以 Gmail 查到的會和 Haraka 回給的不同,導致 Domain Dismatch 問題。第二個是 Gmail 登入時如果收到不認識的訊息,似乎會直接當作登入失敗,像是 MD5 在 Haraka 端顯示的是登入成功,但 Gmail 就隨即中斷連線,而 Gmail 端則是顯示登入失敗。再來是不知道 Gmail 有白名單機制,一開始直接送信會被拒絕,才知道要用 SMTP Relay。而用上了 SMTP Relay 後,也忘記 SPF 和 DKIM 要一起修改才能運作,又稍微花了點時間。
在這次把玩中,我應該能說了解了這幾個東西(DKIM、SPF、DMARC、SMTP)的運作方式,也是不錯的娛樂(?)。從前看 Gmail 的原始碼都覺得看不出所以然,但這次過程中就不斷反覆看其他信件,嘗試從中了解缺少的是哪一環,最終也成功給我搞出來了,可喜可賀。
補:發現 Gmail 可以直接接 SendGrid
這幾天又重新想了一下,才意識到有個流程被我忽視了。當我們在 Gmail 登入時,他會要求填寫 Email 地址,然而這地址與使用者名稱並沒有相干,因此在後面的步驟只需要修改成 SendGrid 的 Relay 就可以使用了。
但畢竟都把 Haraka 玩起來了,就放著吧。