一開始我只是想省點事,真的。想說自己架台 SMTP/POP3 伺服器,要測 Outlook 接收內嵌圖檔信件的效果會方便許多。

伺服器架在內網自己玩不對外,通訊協定用 SMTP/POP3 明碼通訊即可,不必搞煩人的帳號密碼登入、OAuth 認證、API Key,也不需要考慮垃圾信檢核,聚焦郵件內容呈現才是重點。郵件伺服器有 Docker Mailserver 可用,花兩分鐘下載啟動容器,接著就有台標準 SMTP/POP3 能用 C# SmtpClient 發信、Outlook POP3 收信測個痛快,豈不美哉?

如意算盤打得好,但代誌沒憨人想的哈尼甘單。由於缺少郵件服務專業,原以為幾分鐘的事,一路踩坑、爬文、踩坑、爬文,我足足花了兩個多小時才搞定,但欣慰的是過程有學到不少新知識。

踩坑心得最後再講,以下是我測試成功的做法。

Docker Mailserver 安裝方式我是參考這篇 - Docker MailServer 架設與踩坑紀錄 by DJ MYG,但有些小更改:

  1. 從 Github 下載 docker-composer.yml,由於我打算走 POP3 110 協定,ports: 要多加 "110:110",hostname 沿用預設的 mail.example.com 即可:
  2. 從 Github 下載 mailserver.env,這是這次學到的新技巧,docker-composer.yml 可用 env_file 將 environments 設定移到另外的設定檔集中管理。以下幾處跟預設值不同要調
    OVERRIDE_HOSTNAME=mail.example.com
    ENABLE_POLICYD_SPF=0 # 停用 SPF
    ENABLE_OPENDMARC=0 # 停用 DMARC
    ENABLE_POP3=1 # 啟用 POP3
    
  3. 使用指令 docker-compose up -d 安裝並執行容器
    註:也可以選擇 docker-compose up 即時監查伺服器 Log,但後續建立帳號操作要用 tmux 或開 Terminal 執行。
  4. 建立 admin@example.com 帳號,密碼隨便給,另為 admin 帳號新增 postmaster 別名

完整指令碼整理如下:

S_GITHUB_URL="https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master"
curl -LO "${DMS_GITHUB_URL}/compose.yaml" && mv compose.yaml docker-compose.yaml
# nano docker-compose.yml 增加 POP3 Port
curl -LO "${DMS_GITHUB_URL}/mailserver.env"

# nano mailserver.env,依上述說明修改

# 把 Docker 跑起來
docker-compose up

# 用 tmux 或另外一個 Terminal 執行以下指令建立帳號
# 建立 admin
docker exec -it mailserver setup email add admin@example.com "password"
# 將 postmaster 建立為 admin 的別名
docker exec -it mailserver setup alias add postmaster@example.com admin@example.com

以上程序做完,在客戶端設定 C:\Widows\System32\drivers\etc\hostsmail.example.con 指向該主機 IP,測試 telent mail.example.com 25telent mail.example.com 110 有通(或者用更好用的 PowerShell Test-NetConnection 指令),下一步是 Outlook 新增帳號用 POP3 收信。


(註:我實測用 25 Port 會出現伺服器不支援 Outlook 使用的安全認證方式,改設 465 Port,加密方式選自動才 OK。問題似乎跟 amavisd-new 垃圾信防護有關,但由於不影響 C# SmtpClient 測試,我沒再深入研究。)

若一切順利,到這裡應該能用 Outlook 收信。(寄信會不會被退被擋是另一回事)

接著說說我踩到的坑。

Docker Mailserver 是個能用於正式環境,Production 等級的伺服器,因此常用的垃圾信防護及認證機制一樣沒少,我一口氣學會:DMARC、DKIM、SPF 三個名詞。(延伸閱讀:什麼是 DMARC、DKIM 和 SPF? by Cloudflare)

  • DKIM (DomainKeys Identified Mail) 是一種公鑰簽名驗證機制,以用證明郵件系出名門
  • SPF (Sender Policy Framework) 是在 DNS 列舉授權該網域可發送郵件的有效伺服器 IP,方便收信方查證來源
  • DMARC (Domain-based Message Authentication, Reporting & Conformance) 是一種電子郵件驗證、報告和一致性協定,它基於 DKIM 及 SPF,用來決定未通過驗證郵件的處理方式(例:隔離或拒絕)

Docker Mailserver 預設啟用 SPF 及 DMARC 都是啟用的,故你就算 SMTP 從 admin@example.com 寄給 admin@example.com 都會可能被擋掉,出現以下訊息。

550 5.7.23 <admin@example.com>: Recipient address rejected: Message rejected due to: SPF fail - not authorized.
550 5.7.1 rejected by DMARC policy for example.com

由於測試伺服器並不會用來收外部信,故省事做法是如前面所說的,修改 mailserver.env 將 SPF、DMARC 停用。

另外,我用 .NET SmtpClient 走 25 Port 送信時收到另一個錯誤:The server response was: 5.5.2 <MY-PC-HOSTNAME>: Helo command rejected: need fully-qualified hostname,看起來是 POSIX SMTP 的要求,之前用 Exchange SMTP HELO/EHLO 只給機器名稱沒問題的。查了一下,.NET Framework 可以透過 .config <network clientDomain="mail.example.com" />clientDomain 決定 HELO/EHLO 傳送的名稱,但這是 .NET 4.X 時代的事,我是用 .NET 8 啊啊啊啊~

最後,參考網友的解法:Resolving Syntactically invalid EHLO argument SmtpException in C#,我動用 Reflection 偷改私有 Field _clientDomain 結束這回合。

using (var client = new SmtpClient("mail.example.com", 25))
{

    var clientDomainField = typeof(SmtpClient).GetField("_clientDomain", BindingFlags.Instance | BindingFlags.NonPublic);
    if (clientDomainField != null)
    {
        clientDomainField.SetValue(client, "client.example.com"); // 隨便給即可
    }
    client.UseDefaultCredentials = false;
    client.EnableSsl = false;
    client.Send(message);
}

終於。

This post details the author’s experience setting up a local SMTP/POP3 server with Docker Mailserver for testing embedded images in Outlook. Key issues included disabling DMARC and SPF checks and handling .NET SmtpClient’s fully-qualified hostname requirement with Reflection.


Comments

# by 喵小掌

email蠻多詭異的事情。微軟的回答也是無法完美解決這些問題。內嵌圖檔的圖片出不來、設定UTF-8寄出來顯示big5...太多了

Post a comment