工具應用

Cloudflare Email Routing + (Haraka) + SendGrid 組成的低成本 Email 服務

前幾週收到 Gandi 的信,說接下來要把免費 Email 收回去了,看了一下之後一年要 1,500 多,考慮到使用量不多,就來研究看看怎麼自己搞一個。自己搞的主要目標就是達成和原本效果類似,同時成本不會增加太多。目前想嘗試的就是 Cloudflare Email Routing,但這個只能收信,所以寄信就還需要另外找方法了。

補:後來發現 Haraka 對於私人使用不是必要的存在,文末說明,如果你是自己用而無須使用者管理的話,可以跳過 Haraka 部分。

需求

  1. 能夠收信,但不需要有太大的儲存空間(因為有串到 Gmail,抓回去後會刪掉原本伺服器的信)
  2. 能夠寄信

第一點上 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 玩起來了,就放著吧。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *