在 Zeabur 上為 Ghost CMS 設定 Email 發信功能(Brevo SMTP)

Ghost 預設的 Direct 發信在雲端環境幾乎不可靠。記錄我把 Ghost 改成 Brevo SMTP 的完整過程,以及如何找出容器真實的出站 IP 解決白名單問題。

分享
在 Zeabur 上為 Ghost CMS 設定 Email 發信功能(Brevo SMTP)
Photo by Mariia Shalabaieva on Unsplash

在把 Ghost CMS 部署到 Zeabur 之後,我一直沒有特別在意 Email 發信的問題——直到有一天想測試會員訂閱流程,才發現根本收不到任何確認信。翻了一下 Ghost 的設定,才知道預設的發信方式問題很大。這篇文章記錄我把 Ghost 的 Email 改成用 Brevo SMTP 發信的完整過程,以及中間踩到的一個讓我卡很久的坑。

為什麼 Ghost 預設的 Direct 發信不可靠

Ghost 預設的 mail__transportDirect,意思是 Ghost 會直接從伺服器連線到收件方的 Mail Server 送出信件,中間不經過任何 SMTP relay。這在本地開發環境偶爾能用,但在正式環境幾乎可以確定會失敗,原因有幾個:

  • 大多數雲端服務商(包含 Zeabur)的出站 IP 不在主流郵件服務商的信任名單內
  • 沒有 DKIM 簽章,信件會被 Gmail、Outlook 等判定為可疑
  • 容易被直接丟進垃圾信匣,甚至被拒收

結論是:只要是正式環境,一定要換成 SMTP relay

為什麼選擇 Brevo

市面上的 SMTP 服務很多,我選 Brevo(前身是 Sendinblue)有幾個理由:

  • 免費方案提供每天 300 封,對個人部落格完全夠用
  • 支援自訂寄件網域,搭配 DKIM/SPF/DMARC 設定後信件不容易進垃圾信
  • SMTP 設定簡單,不需要特殊設定就能用

設定寄件網域:透過 Cloudflare 自動驗證 DNS

在取得 SMTP 憑證之前,有一個步驟我覺得很值得特別說:設定寄件網域(Sender Domain)

這步驟不是必要的,跳過也能發信——但如果沒有正確設定 DKIM 和 SPF,信件仍然容易被判定為可疑。既然都花時間設定了,順手做完才值得。

流程是這樣的:

  1. 進入 Brevo 後台,點選左側 Settings → Senders & IPs → Domains
  2. Add a domain,輸入你的網域(例如 yourdomain.com
  3. Brevo 會列出幾筆需要新增的 DNS 記錄(DKIM、SPF、DMARC)

這裡有個讓我很驚喜的功能:因為我的網域是在 Cloudflare 購買和管理的,Brevo 提供了 直接登入 Cloudflare 自動設定 的選項。

點下 Connect to Cloudflare 後,頁面會跳轉到 Cloudflare 的 OAuth 授權頁面。登入 Cloudflare 帳號並授權之後,Brevo 會自動把所需的 DNS 記錄(包含 DKIM CNAME、SPF TXT、DMARC TXT)全部寫進 Cloudflare DNS,完全不用手動貼記錄。

授權完成後回到 Brevo,等幾分鐘讓 DNS 生效,再點 Verify 確認即可。驗證通過後,這個網域就可以作為合法的寄件來源。

注意:Cloudflare 的 DNS API 授權是 per-account 的,確認你用的 Cloudflare 帳號有該網域的管理權限。

申請帳號並取得 SMTP 憑證

brevo.com 註冊免費帳號,完成後進入後台:

  1. 點選左側選單的 Settings
  2. 找到 SMTP & API 頁面
  3. 在 SMTP 區塊可以看到 Server、Port、Login 等資訊
  4. Generate a new SMTP key 產生密碼(只會顯示一次,要立刻儲存)
Brevo SMTP settings page showing server, port, login and SMTP key generation

Brevo 後台的 SMTP 設定頁,記下 Server、Port、Login 和新產生的 SMTP Key

取得的資訊大致如下:

  • SMTP Serversmtp-relay.brevo.com
  • Port587
  • Login:你在 Brevo 申請的帳號 Email
  • SMTP Key:剛才產生的密碼

在 Zeabur 設定 Ghost 的環境變數

有了 SMTP 憑證之後,到 Zeabur Dashboard 的 Ghost 服務,切換到 Variables 標籤,新增或更新以下幾個變數:

變數名稱
mail__transport SMTP
mail__options__host smtp-relay.brevo.com
mail__options__port 587
mail__options__auth__user Brevo 帳號 Email
mail__options__auth__pass Brevo SMTP Key
mail__from 你希望寄件人顯示的 Email(需在 Brevo 驗證過的網域)

設定完記得重啟 Ghost 服務讓變數生效。

關鍵:IP 白名單要填對 IP

Brevo 的免費方案需要設定 IP 白名單,只有被允許的 IP 才能透過你的帳號發信。這裡我卡了一段時間,原因是我一開始填的 IP 填錯了

直覺上,我用 dig 查了 Ghost 網域的 A 記錄,拿到一個 IP 填進去,結果測試寄信時仍然收到 525 5.7.1 Unauthorized IP address 的錯誤。

問題在於:DNS 查到的 IP 是入站流量的 IP(通常是 Load Balancer),但 Ghost 發 SMTP 請求時走的是容器自己的出站 IP,兩個不一定相同。

正確的做法是直接在容器內部查詢出站 IP:

npx zeabur@latest service exec --id <ghost-service-id> -- sh -c "wget -qO- https://api.ipify.org"

這樣得到的才是 Ghost 連線到 Brevo SMTP 時實際使用的 IP。把這個 IP 填到 Brevo 的 Settings → Authorized IPs

Brevo authorized IPs whitelist settings page

Brevo 的 IP 白名單設定,要填容器的出站 IP 而不是 DNS 查到的入站 IP

測試成功

IP 白名單設定完成後,到 Ghost Admin 的 Posts,開啟任意一篇文章進入發布流程,在 Newsletter 選項下方會看到 Send a test email to yourself 按鈕,點下去應該就能收到測試信了。

Brevo dashboard showing successful email delivery stats

Brevo 後台確認信件成功發出

FAQ

設定之後重啟,IP 會不會變?不一定,Zeabur 有時候重啟後 IP 不變,但不保證永遠如此。如果之後又出現 Unauthorized IP,用同樣的方式重新查一次容器出站 IP,更新白名單即可。免費方案每天 300 封夠用嗎?對個人部落格來說完全夠用。Ghost 的 Email 主要用在會員訂閱確認、登入 magic link、Newsletter 發送等,一般流量下每天 300 封綽綽有餘。Zeabur Email(ZSend)和 Brevo 有什麼差別?Zeabur Email 是 HTTP API 形式的發信服務(類似 Resend),適合透過程式碼呼叫 API 發信。Ghost 使用的是 SMTP 協定,兩者無法直接相容,所以需要另外搭配支援 SMTP 的服務。

結語

整個設定流程不算複雜,但「入站 IP 和出站 IP 不同」這個細節確實容易踩坑。如果你也在 Zeabur 上跑 Ghost,遇到 Unauthorized IP 的錯誤,記得用 service exec 查容器內部的出站 IP 再填白名單。

Read more

Gaze Guard 短影片旁白的 TTS 成本與隱私防護示意圖

短影片旁白的成本,其實比我想像低

我最近在做一支 Gaze Guard 的 20 秒產品介紹影片。 原本想說簡單做個動畫就好。 後來想說如果有配音,會感覺比較生動也比較能夠描述使用的情境和達成的效果。 這次我測了什麼 我測的是產品介紹影片用的英文旁白。 工具底層使用的是 Edge TTS,也就是 Microsoft Edge 的線上 Text-to-Speech 服務。常見的底層工具是 edge-tts。 這不是正式 Azure Speech API 的計費路徑。 在我這次的環境裡,它沒有要求 API key,也沒有回傳可見的 per-use billing。 所以這次測試本身,比較像是快速 prototype。 如果未來要正式商用、穩定大量產出,就要回頭看 Azure Speech、OpenAI TTS、ElevenLabs 這類正式付費方案。

By SteepingLogic
用 Cloudflare Tunnel,從外面連回本機 Hermes Dashboard

用 Cloudflare Tunnel,從外面連回本機 Hermes Dashboard

# 用 Cloudflare Tunnel,從外面連回本機 Hermes Dashboard 我有一台 local Hermes。 平常它就在自己的機器上跑。 可能是家裡的桌機。 可能是 NAS。 可能是一台一直開著的 Linux box。 也可能只是我自己的筆電。 問題是,人一離開那個網路,Hermes dashboard 就變成一個只存在於 localhost 的東西。 我想在外面打開它。 但我沒有 static IP。 沒有買網域。 也不想進路由器開 port forwarding。 因為那條路通常長這樣: * 找 public IP * 設定 NAT * 開 port * 處理防火牆 * 補 HTTPS * 設定憑證 * 再擔心 origin IP 被掃 非常友善。 所以這篇先走另一條路:

By SteepingLogic
Invoshot 開發日記|正向回饋

Invoshot 開發日記|正向回饋

想信很多人在開發的時候都常常會做一件事,那就是: 不斷地反覆思考,在腦袋內把流程跑過很多次,然後思考哪裡可以做得更好 不限坐在電腦前的時後,可能是在捷運上,在人行道上, 在馬桶上或是在洗澡的時候 🤪 我不斷地在想:AI 辨識完一張發票後,使用者接下來最需要的是什麼? 每個欄位都要核對嗎? 這麼多欄位真的不會漏看嗎? 所以我在 Invoshot 裡加了提醒功能。 辨識結果出來後,系統會把比較需要注意的地方標出來。 不是把所有不確定都丟給使用者,而是把他真正需要介入的那幾秒鐘留出來。 今天收到一則訊息: 今天用了有新增提醒功能很棒! 真的有變輕鬆 😎 看到這句的時候,我第一個想到的是: 原來那些只是我腦內風暴的優化項目,使用者是真的可以感受到差異的! 感覺又有動力了!

By SteepingLogic