聯絡表單郵件收不到的原因與解決方法
· 小村 豪 · 郵件遞送, SPF, DKIM, DMARC, 諮詢流程優化, B2B
執行摘要
聯絡表單通知郵件「明明送信成功卻收不到」的最大原因,通常不在 SMTP 連線本身,而在於看得見的寄件人(From:)與實際被認證的寄件人(SPF 的 MAIL FROM / DKIM 的 d=)沒有對齊。DMARC 以 RFC5322.From 為基準,檢查 SPF 或 DKIM 是否至少有一項帶著對齊(alignment)通過。也就是說,把表單送出者的 Gmail 或公司地址直接放進 From: 這種設計,看起來方便回覆,實際運作中卻很容易失敗。
實務上的結論很單純。聯絡表單通知郵件的 From: 應固定使用自家網站的網域,表單使用者的地址則放進 Reply-To:。Sender: 只在「作者與實際送信主體不同」時才使用,Return-Path 不該手動寫進標頭,而是應在 MTA 或郵件服務端,以信封寄件人(envelope sender)的形式設定。
此外,轉寄容易破壞 SPF,當郵件清單或中繼伺服器改寫了內文或標頭,DKIM 也會壞掉。正因如此,聯絡表單不能只依賴 SPF、必須同時啟用 DKIM,DMARC 也應先以 p=none 觀測,再逐步升到 quarantine / reject,這樣才安全。
再者,Google 針對寄往 Gmail 的郵件要求:送信網域要設定 SPF 或 DKIM;達到一定寄送規模的送信者,SPF、DKIM、DMARC 三項都要齊備;並且From: 標頭的網域必須與 SPF 或 DKIM 已認證的網域一致。配送問題的真正原因,通常不是「郵件程式碼」,而是「寄件人設計」。
SPF、DKIM、DMARC 與 From 標頭的角色
郵件遞送時,必須把信封(envelope)與標頭(header)分開來看。SPF 主要驗證的是 SMTP 交談過程中的 MAIL FROM,這是遞送層面的寄件人。最終遞送時,這條回程路徑應該只留下一個 Return-Path,發送端的 SMTP 系統一開始就不該建立帶有 Return-Path 標頭的訊息。另一方面,From: 表示訊息標頭端「看起來是誰寄出的」,Reply-To: 表示回覆對象,Sender: 則表示實際送出訊息的主體。
RFC 5322 將 From: 定義為訊息的作者,Sender: 定義為實際送信主體。如果作者與送信主體相同,就不該使用 Sender:;如果想讓回覆對象與作者不同,正確做法是使用 Reply-To:。RFC 5322 更明確指出,From: 不應該放入非作者本人的地址。聯絡表單的通知通常不是使用者本人透過 MUA 寄出的,而是由網站端系統產生的通知,所以把使用者地址放進 From: 的設計,也容易偏離規範原本的意義。
DKIM 會對訊息的部分標頭與內文附加簽章,收件端再用 DNS 上的公開金鑰驗證。簽章網域以 DKIM-Signature 的 d= 表示,公開金鑰則透過選擇器(selector)查詢,例如 selector._domainkey.example.com。DKIM 被視為「即使經過轉寄也比較容易存活」的認證方式,但如果內文或被簽章的標頭在傳送過程中遭到改動,bh= 的內文雜湊或簽章驗證就會失敗。
DMARC 不只看 SPF 或 DKIM 是否通過,還會看這些已認證的網域是否與 From: 網域一致。這正是重點所在。即使 SPF 通過,如果 MAIL FROM=bounces.vendor.net 而 From: contact@example.com,SPF 的對齊(alignment)依然會失敗。若 DKIM 以 d=example.com 通過,DMARC 就能通過,但如果連 DKIM 也沒有,DMARC 就會失敗。DMARC 定義了以 adkim / aspf 決定 strict / relaxed 的對齊模式、p=none|quarantine|reject 的政策,以及 rua / ruf 的報告收件位址。
Gmail 近期的指南,直接把這種設計理念落實成了運用要求。Google 明確要求「不要偽造 From: 標頭」「直接寄送的郵件,From: 標頭內的網域必須與 SPF 網域或 DKIM 網域一致」。聯絡表單通知並不是行銷郵件,但收件端過濾器的基本邏輯是一樣的,忽視這個原則就會拉低到達率。
sequenceDiagram
participant User as 表單使用者
participant App as Web 應用程式
participant SMTP as 送信 MTA / SMTP 服務
participant DNS as DNS
participant MX as 收件端 MX
User->>App: 送出表單
App->>App: 決定 From / Reply-To / Sender
App->>SMTP: 送出 SMTP 送信請求
SMTP->>SMTP: 附加 DKIM 簽章
SMTP->>MX: MAIL FROM / RCPT TO / DATA
MX->>DNS: 查詢 SPF(MAIL FROM)
MX->>DNS: 查詢 DKIM 公開金鑰(selector._domainkey)
MX->>DNS: 查詢 DMARC(_dmarc + From 網域)
MX->>MX: 進行 Alignment 判定
MX-->>App: 收件・進垃圾信・拒收・退信
這個流程中最重要的一點是,DMARC 的判定基準從頭到尾都以 From: 網域為中心。應用程式端負責 From:、SMTP 端負責 MAIL FROM、DNS 端則分別處理 SPF/DKIM/DMARC,這些是各自獨立設定的,只修正其中一項並不能解決不達問題。
聯絡表單常見的失敗情境
最常見的是把表單使用者的地址放進 From: 的情況。例如透過網站的 SMTP 送信卻寫成 From: taro@gmail.com,SPF 或 DKIM 通常通過的是網站端網域,而不是 Gmail 端網域。結果就是 From: 顯示 Gmail、已認證網域卻是 example.com,這樣的落差會導致 DMARC 對齊失敗。Google 自己也要求避免偽造 From:,並要求 From: 與 SPF/DKIM 網域一致。
其次常見的是轉寄導致 SPF 失效的情況。郵件轉寄時,最終收件人看到的送信來源 IP,很容易變成「不在原始送信網域的 SPF 記錄裡的中繼伺服器」,因此即使是正當郵件,SPF 也會失敗。Google 也建議「轉寄過的郵件容易讓 SPF 失敗,因此務必同時使用 DKIM」。此外,如果中繼端對內文、主旨加上前綴或附加頁尾,連 DKIM 也會失效。
使用外部 SMTP 服務時初期設定不足也是典型狀況。SendGrid 若未設定 Domain Authentication,有些情況下就無法送信;把 Automated Security 開啟後會產生以 CNAME 為基礎的認證記錄,需要時也可以設定 Custom Return Path 或 Custom DKIM Selector。SES 預設使用 amazonses.com 的 MAIL FROM,SPF 會隱含成立,但如果想讓 SPF 與網站網域對齊,就需要設定自訂 MAIL FROM。Mailgun 也一樣,若送信網域的 SPF/DKIM 與必要的 MX 沒有設定完整,就無法建立正確的送信簽章。
共享主機的本機 MTA 是容易被忽略的地雷。就算自家網站已完成認證,如果實際送出去的 IP 是評價不佳的共享 IP、沒有 PTR,或是主機端沒有掛上 DKIM,到達率一樣會下降。Google 已將送信 IP 的 PTR 列為要求,並說明共享 IP 評價不佳時也可能造成 5.7.1 系列的錯誤。
PHP 的 mail() 或送信函式庫的用法也很容易出錯。PHP 手冊說明 mail() 需要 From 標頭,並且可以透過額外參數以 sendmail -f 指定信封寄件人。也就是說,正確做法不是自己組出 Return-Path: 標頭,而是把信封寄件人交給 MTA 處理。一旦誤解這一點,SPF 判定所使用的寄件人,和應用程式預期的寄件人就會不一致。
最後,還有一種情況是明明該用 Reply-To,卻去動了 Sender 或 From。如果只是想把回覆對象指向使用者,用 Reply-To 就足夠了。Sender 是用來明確表示「作者與實際送信主體不同」的標頭,並不是聯絡表單常用的欄位。把設計目的分成「想把回覆送回使用者」與「想標示通知的責任主體」兩件事分開考慮,就能減少出錯。
診斷步驟與指令
首先該做的是在看程式碼之前,先看原始郵件標頭。Gmail 可以透過「顯示原始郵件」、Outlook 可以透過「訊息詳細資料」或「網際網路標頭」,確認 Authentication-Results、Return-Path、From、Reply-To、DKIM-Signature、Received。只要看這裡,就能相當精準地分辨問題是「根本沒送出去」,還是「認證與一致性出了問題」。
標頭中最先要看的重點
最優先要看的是這 5 點。
From:的網域是什麼Return-Path:的網域是什麼Authentication-Results:是否顯示spf=pass/dkim=pass/dmarc=passdkim=pass時,header.i=或d=是哪個網域dmarc=fail時,原因是認證本身失敗,還是對齊失敗(alignment failure)
Google 的 DMARC 疑難排解文件也明確指出,就算訊息通過了其他認證,只要標頭沒有對齊,DMARC 依然會失敗。
確認 DNS 的指令
dig 是 DNS 疑難排解的經典工具,BIND 的手冊也將它形容為輸出靈活明確的 DNS 查詢工具。nslookup 則是可在非互動模式下使用、更輕量的確認指令。聯絡表單的認證確認,至少要查詢 SPF、DKIM、DMARC 這 3 項。
# SPF
dig +short TXT example.com
# DKIM
dig +short TXT form2026._domainkey.example.com
# DMARC
dig +short TXT _dmarc.example.com
# 若是 Windows
nslookup -type=txt example.com
nslookup -type=txt _dmarc.example.com
nslookup -type=txt form2026._domainkey.example.com
預期的輸出範例大致如下。
"v=spf1 include:sendgrid.net include:mailgun.org ip4:203.0.113.10 -all"
"v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ..."
"v=DMARC1; p=none; rua=mailto:dmarc-agg@example.com; ruf=mailto:dmarc-afrf@example.com; adkim=r; aspf=r; fo=1"
這裡該檢查的是:SPF 記錄是否整合成一筆、DKIM 公開金鑰能否查得到、DMARC 是否有 p=。RFC 7208 規定,會觸發 DNS 查詢的 SPF 機制總數應限制在 10 個以內,超過就會造成 permerror。
SMTP 連線與 TLS 的確認
openssl s_client 是 OpenSSL 提供的通用 SSL/TLS 客戶端,用來確認 SMTP 伺服器的 STARTTLS 或憑證鏈相當方便。用 -starttls smtp 可以開始 SMTP 的 STARTTLS,-showcerts 則可以確認伺服器回傳的憑證清單。
printf 'QUIT\r\n' | openssl s_client \
-connect smtp.example.com:587 \
-starttls smtp \
-servername smtp.example.com \
-showcerts \
-brief
輸出範例如下。
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Verification: OK
250 CHUNKING
這樣至少可以確認:是否能連上 SMTP 伺服器、STARTTLS 是否有效、憑證驗證是否有明顯異常。Google 要求達到一定規模的寄件者必須使用 TLS,未使用 TLS 可能是 5.7.29 錯誤的原因。
實際送信的重現測試
swaks 是專門用於 SMTP 測試的實用工具,能靈活重現包含 TLS、驗證、SMTP 擴充功能的送信測試。調查聯絡表單不達問題時,「跳過應用程式,用相同的 SMTP / 相同的 From / 相同的 Reply-To / 相同的收件人」送出一封信來重現,是很有效的做法。
swaks \
--server smtp.example.com \
--port 587 \
--tls \
--auth LOGIN \
--auth-user contact@example.com \
--auth-password '********' \
--from bounce@example.com \
--to yourtest@gmail.com \
--h-From "網站通知 <contact@example.com>" \
--h-Reply-To "王小明 <visitor@gmail.com>" \
--header "Subject: swaks test" \
--body "This is a test"
送信成功時的典型情況如下。
=== Trying smtp.example.com:587...
=== Connected to smtp.example.com.
<- 250-STARTTLS
<- 250-AUTH LOGIN PLAIN
-> STARTTLS
<- 220 Ready to start TLS
...
<- 250 2.0.0 Ok: queued as ABC123DEF
如果透過應用程式送信會失敗,但用 swaks 卻能成功,原因很可能偏向函式庫的標頭組裝方式或信封寄件人設定。反過來,如果用 swaks 一樣會失敗,就可以把範圍縮小到 DNS、SMTP、收件端政策的問題。
用 mail-tester 做外部診斷
mail-tester 是一個服務:讓你送一封信到隨機產生的測試地址,再分析該訊息、送信伺服器與送信 IP,回傳詳細報告。適合用來對「本機 MTA 或共用伺服器莫名收不到信」的情況做初步診斷。
用法很單純。
- 取得 mail-tester 產生的測試地址
- 用和聯絡表單相同的路徑送出一封信
- 查看分數,以及 SPF / DKIM / DMARC / 反向解析 / 黑名單 / 內文結構方面的指出項目
不能只靠 mail-tester 的分數來判斷正式環境的送信結果,但至少可以及早發現「根本看不到 SPF」「DKIM 公開金鑰查不到」「內文或寄件人結構不自然」之類的問題。
標頭解析範例
這是失敗範例。
Return-Path: <bounce-123@vendor.example.net>
Authentication-Results: mx.google.com;
spf=pass (google.com: domain of bounce-123@vendor.example.net designates 198.51.100.10 as permitted sender) smtp.mailfrom=vendor.example.net;
dkim=none;
dmarc=fail (p=quarantine sp=quarantine dis=none) header.from=gmail.com
From: 王小明 <visitor@gmail.com>
Reply-To: 王小明 <visitor@gmail.com>
Subject: 洽詢
這封信雖然 SPF 本身通過,但因為 From: 是 gmail.com,所以 DMARC 失敗。這是聯絡表單把使用者地址放進 From: 時,典型的失敗模式。
成功範例則是這樣。
Return-Path: <bounce@example.com>
Authentication-Results: mx.google.com;
spf=pass smtp.mailfrom=example.com;
dkim=pass header.i=@example.com header.s=form2026;
dmarc=pass header.from=example.com
From: Example Site <contact@example.com>
Reply-To: 王小明 <visitor@gmail.com>
Subject: 聯絡表單通知
用這種寫法,可以透過 Reply-To: 保留對使用者回覆的便利性,同時讓看得見的寄件人與已認證的寄件人都統一在 example.com,到達率會大幅穩定下來。
建議的 From 設計模式
聯絡表單不動搖的原則是:讓「用於認證的網域」與「呈現給收件人的 From 網域」保持一致。在此基礎上,只把回覆對象移到 Reply-To:。把設計拆成「Sender: 只在必要時使用」「Return-Path 透過信封設定」這種三層結構,設計就會穩定下來。
| 模式 | 標頭範例 | 適合的情境 | 優點 | 注意事項 |
|---|---|---|---|---|
| 建議模式 | From: contact@example.comReply-To: visitor@gmail.comReturn-Path: bounce@example.com |
幾乎所有聯絡表單 | 容易通過 DMARC/方便回覆/實作單純 | 忘記加上 Reply-To 的話,回覆對象就會變成網站端 |
| 子網域分離 | From: contact@form.example.comReply-To: visitor@gmail.comReturn-Path: bounce.form.example.com |
想把表單通知與主要郵件分開管理 | 容易分離信譽/方便管理 | 也要在子網域端整理好 SPF/DKIM/DMARC |
Sender 明示型 |
From: contact@example.comSender: mailer@example.comReply-To: visitor@gmail.com |
想明示送信主體的特殊需求 | 可以呈現運用責任主體 | 通常不需要。若作者與送信主體相同就顯得多餘 |
| 不建議模式 | From: visitor@gmail.comReply-To: visitor@gmail.com |
只是想凸顯回覆對象的實作 | 表面上看起來自然 | 容易造成 DMARC 失敗,聯絡表單通知應避免 |
這張表的依據是 RFC 5322 對 From / Sender / Reply-To 的定義,以及 RFC 5321 對 Return-Path 的處理方式,再加上 DMARC 以 From: 為基準判定一致性的規範。表單通知的預設值,用第一行的「建議模式」就已經足夠。即使很想在 From: 放使用者地址,只要把回覆對象放進 Reply-To:,同樣能達成目的。
特別要記住的是,Return-Path 不是「拿來編輯的標頭」,而是「配送時使用的信封寄件人所產生的結果」。PHP mail() 的話用 -f,SMTP 服務的話則透過 Custom MAIL FROM / Return Path / bounce domain 之類的設定項目來處理,才是正確的實作方式。
依架構分類的設定指南
接下來,依照現場常見的 3 種模式,整理設定的思路。前提是:要放進 DNS 的正確數值,請以各服務管理後台產生的值為準。以下的記錄範例,只是用來理解結構的代表性範例。
網站使用外部 SMTP 時
使用外部 SMTP 時最重要的一點,是先完成自家網域的認證。Google 也建議:使用郵件服務供應商時,要確認該服務是否已認證自家網域的 SPF 與 DKIM。
建議架構
From:使用contact@example.com或contact@form.example.comReply-To:使用表單使用者的地址Return-Path/ MAIL FROM 使用自己管理的退信用子網域,例如bounce.example.com- DKIM 用
example.com或送信用子網域來簽署 - DMARC 放在看得見的
From:網域上
SendGrid 的典型設定
SendGrid 以 Domain Authentication 為前提,開啟 Automated Security 後會產生 3 筆 CNAME。若關閉,則會產生 1 筆 MX 與 2 筆 TXT,也可以設定 Custom Return Path 或 Custom DKIM Selector。
; 範例:SendGrid(實際數值請使用管理後台產生的內容)
em123.example.com. CNAME u123456.wl.sendgrid.net.
s1._domainkey.example.com. CNAME s1.domainkey.u123456.wl.sendgrid.net.
s2._domainkey.example.com. CNAME s2.domainkey.u123456.wl.sendgrid.net.
_dmarc.example.com. TXT "v=DMARC1; p=none; rua=mailto:dmarc-agg@example.com"
SendGrid 容易踩到的坑,是「只完成 SMTP 認證,網域認證卻沒設定」的狀態。這種狀態下雖然可以送信,但從收件端來看,From: 與已認證網域之間的關聯會變弱。基本做法是先通過 Domain Authentication,再依需要設定 Custom Return Path。
SES 的典型設定
SES 預設使用 amazonses.com 子網域的 MAIL FROM,因此 SPF 本身會隱含成立。不過,如果想讓網站網域與 SPF 對齊,就要使用自訂 MAIL FROM。此時 SES 會要求自訂 MAIL FROM 網域必須有 SPF TXT 與 MX,而且 MX 必須恰好只有 1 筆。另外,Easy DKIM 會在 DNS 加入 3 筆 CNAME。
; 範例:SES Easy DKIM
abcde12345._domainkey.example.com. CNAME abcde12345.dkim.amazonses.com.
fghij67890._domainkey.example.com. CNAME fghij67890.dkim.amazonses.com.
klmno54321._domainkey.example.com. CNAME klmno54321.dkim.amazonses.com.
; 範例:SES 自訂 MAIL FROM
bounce.example.com. MX 10 feedback-smtp.ap-northeast-1.amazonses.com.
bounce.example.com. TXT "v=spf1 include:amazonses.com -all"
_dmarc.example.com. TXT "v=DMARC1; p=none; rua=mailto:dmarc-agg@example.com"
SES 設計上重要的一點是,MAIL FROM 用的網域不應該直接使用送信用的 From: 網域本身,而是要用退信專用的子網域。AWS 也建議 MAIL FROM 使用的子網域,不要與實際送信的網域完全相同。
Mailgun 的典型設定
Mailgun 在驗證送信網域時,需要 SPF 用 TXT 與 DKIM 用 TXT,還需要額外新增 2 筆 MX。如果已經有 SPF 記錄,不要另外新增一筆 SPF 記錄,而是在現有記錄中加入 include:mailgun.org。DKIM 金鑰有時會看到多個,但只要目前使用的金鑰正確登記在 DNS 上,就能送信。
; 範例:以子網域方式使用 Mailgun
mg.example.com. TXT "v=spf1 include:mailgun.org ~all"
mx._domainkey.mg.example.com. TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ..."
mg.example.com. MX 10 mxa.mailgun.org.
mg.example.com. MX 10 mxb.mailgun.org.
email.mg.example.com. CNAME mailgun.org.
_dmarc.example.com. TXT "v=DMARC1; p=none; rua=mailto:dmarc-agg@example.com"
Mailgun 和子網域的運用方式相容性很好,如果切出像 mg.example.com 這樣的專用送信子網域,就能更方便管理表單通知或交易通知。
網站透過共享主機的本機 MTA 送信時
使用共享主機時,首先該看的不是自己的應用程式,而是主機端送信基礎設施的品質。如果送信 IP 的 PTR、DKIM 支援、共享 IP 的評價、送信日誌的可視性偏弱,這種架構本身就很不利。Google 也很重視送信 IP 的 PTR,並說明共享 IP 評價不佳時,可能成為被封鎖的原因。
實務上,依這個順序進行比較安全。
- 確認主機商是否能透過管理後台或客服支援設定 SPF/DKIM/PTR
From:一律固定使用自家網域- 把主機的送信來源 IP 或已授權的送信網域加進 SPF
- 用主機提供的功能啟用 DKIM。如果沒有,就改用外部 SMTP
- 若可行,把 MAIL FROM 用的退信地址獨立出來,例如
bounce.example.com
在共享主機自行建立 DKIM 的典型範例中,OpenDKIM 系列可以用 opendkim-genkey 產生私鑰與 DNS 用 TXT 記錄。DKIM 公開金鑰放在帶有選擇器的 selector._domainkey.example.com 這種結構,符合 RFC 6376 的規範。
mkdir -p /etc/opendkim/keys/example.com
opendkim-genkey -D /etc/opendkim/keys/example.com -d example.com -s form2026
chown opendkim:opendkim /etc/opendkim/keys/example.com/form2026.private
chmod 600 /etc/opendkim/keys/example.com/form2026.private
產生後大致會是這樣。
form2026._domainkey.example.com. TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ..."
# /etc/opendkim/KeyTable
form2026._domainkey.example.com example.com:form2026:/etc/opendkim/keys/example.com/form2026.private
# /etc/opendkim/SigningTable
*@example.com form2026._domainkey.example.com
不過,如果在共享主機上無法自行控制 PTR 或外寄 relay,改用外部 SMTP 會是更快的捷徑。即使像聯絡表單通知這種少量送信的場景,認證薄弱的本機 MTA 對 Gmail 或企業郵件系統來說仍然不利。
網站使用 PHP mail() 或 SMTP 函式庫時
PHP 的 mail() 很方便,但就認證與到達率而言,取決於「背後接的 MTA 到底是誰」。PHP 手冊說明,郵件需要 From 標頭,透過 sendmail_path 送信時,可以用額外參數指定信封寄件人。反過來說,使用 mail() 並不會自動幫你把 SPF/DKIM/DMARC 整理好。
至少先照這樣設計。
From:使用contact@example.comReply-To:使用表單使用者- 信封寄件人使用
bounce@example.com - 內文中也明確寫出使用者的地址
- 若可行,盡量不用
mail(),改用已認證的 SMTP
mail() 的最小組態範例
把使用者輸入直接放進標頭是很危險的。如果 $name 或 $email 混入 CR/LF,攻擊者就能插入像 Bcc: 這樣的額外標頭,讓表單變成垃圾郵件的中繼站。PHP 手冊也建議,用在標頭中的外部輸入必須先驗證/正規化。下面的範例中,包括信封寄件人參數(additional_params)在內,會混入標頭的數值都會事先做過清理(sanitize)。
<?php
// 只回傳可以用在標頭的值。若含有 CR/LF/NUL 就拒絕。
function sanitize_header_value(string $value): string {
if (preg_match('/[\r\n\0]/', $value)) {
throw new InvalidArgumentException('Invalid characters in header value');
}
return trim($value);
}
// 依 RFC 規範驗證電子郵件地址。
function sanitize_email(string $email): string {
$clean = sanitize_header_value($email);
if (!filter_var($clean, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email address');
}
return $clean;
}
$to = 'ops@example.com';
$subject = sanitize_header_value('聯絡表單通知');
// $name / $email / $message 是表單輸入。$message 用於內文,允許 CR/LF;
// 但用在標頭的 $name / $email 一定要擋掉 CR/LF。
$safeName = sanitize_header_value($name);
$safeEmail = sanitize_email($email);
$body = <<<TEXT
Name: {$safeName}
Email: {$safeEmail}
{$message}
TEXT;
$headers = [
'From' => 'Example Site <contact@example.com>',
'Reply-To' => sprintf('%s <%s>', $safeName, $safeEmail),
'Content-Type' => 'text/plain; charset=UTF-8',
];
// additional_params 最終也會傳給 shell,因此只使用固定值,不混入動態輸入。
mail($to, $subject, $body, $headers, '-fbounce@example.com');
這個範例的重點有兩個。第一,不是把 Return-Path: 寫成標頭,而是透過第 5 個參數的 -f 傳入信封寄件人。第二,混入 Reply-To: 等標頭的使用者輸入,都經過會擋掉 CR/LF 的清理函式。如果不做清理直接組裝標頭,攻擊者就能灌入像 \r\nBcc: victim@example.com 這樣的字串,插入額外標頭,因此 PHP 手冊也將「外部輸入用於標頭時務必驗證」列為必要事項。additional_params 最終也會傳給 shell,請只使用固定值,不要混入使用者輸入。
使用 SMTP 函式庫的範例
<?php
$mail->isSMTP();
$mail->Host = 'smtp.example.com';
$mail->Port = 587;
$mail->SMTPAuth = true;
$mail->SMTPSecure = 'tls';
$mail->Username = getenv('SMTP_USER');
$mail->Password = getenv('SMTP_PASS');
$mail->setFrom('contact@example.com', 'Example Site');
$mail->addAddress('ops@example.com');
$mail->addReplyTo($email, $name);
// 依函式庫不同,也可以另外設定 Sender / return-path
$mail->Sender = 'bounce@example.com';
$mail->Subject = '聯絡表單通知';
$mail->Body = $body;
$mail->send();
SMTP 函式庫的優點是,容易把標頭寄件人與信封寄件人分開控制。在聯絡表單的用途上,最適合的設計就是把 From: 固定為網站網域,只把回覆對象放進 Reply-To:。
SPF、DKIM、DMARC 的具體範例
SPF 的基本範例
example.com. TXT "v=spf1 ip4:203.0.113.10 include:sendgrid.net -all"
SPF 必須把實際會用到的所有送信來源都涵蓋進去。使用第三方送信服務時,Google 也要求確認該服務是否已透過 SPF 與 DKIM 完成認證。另外,由於 SPF 的 DNS 查詢次數有限制,也要注意不要疊加太多 include。
DKIM 的基本範例
form2026._domainkey.example.com. TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ..."
選擇器(selector)是用來做金鑰輪替的,可以讓多個公開金鑰共存在同一個網域上。實務上,比起用 default 這種名稱,用途或年月能區分的名稱,日後查看會更清楚。
DMARC 的導入範例
先從觀測模式開始。
_dmarc.example.com. TXT "v=DMARC1; p=none; rua=mailto:dmarc-agg@example.com; ruf=mailto:dmarc-afrf@example.com; adkim=r; aspf=r; fo=1"
接著,是想隔離一部分的時候。
_dmarc.example.com. TXT "v=DMARC1; p=quarantine; pct=25; rua=mailto:dmarc-agg@example.com; adkim=r; aspf=r"
最後,是嚴格運用的情況。
_dmarc.example.com. TXT "v=DMARC1; p=reject; sp=reject; rua=mailto:dmarc-agg@example.com; adkim=s; aspf=s"
p=none 表示只做監控,quarantine 表示建議隔離,reject 表示建議在 SMTP 階段拒收。adkim 與 aspf 用來切換 strict/relaxed 模式。比起一開始就跳到 reject,先用 none 觀測流量與正規送信來源,再逐步升級會更安全。
另外,如果要把 rua / ruf 送到公司外部的彙整服務,根據 RFC 7489,第三方那一側需要額外的 DNS 記錄。例如要把 example.com 的報告送到 thirdparty.example.net,接收方就必須公開 example.com._report._dmarc.thirdparty.example.net TXT "v=DMARC1"。
疑難排解檢查清單
最後整理現場可以直接套用的確認順序。聯絡表單的郵件不達問題,從上往下依序排除是最快的做法。
首先要確認的事項
From:是不是自家網域Reply-To:有沒有放使用者地址Authentication-Results中是否有spf=pass或dkim=pass,而且同時有dmarc=pass- 若
dmarc=fail,是認證失敗還是對齊失敗(alignment failure) - SPF 是否整合成一筆記錄
- SPF 的查詢次數是否過多
- DKIM 公開金鑰能否查得到
- DMARC 是否有
p= - 送信 IP 的 PTR 與反向解析是否正常
- 是否使用共享 IP,或該 IP 的信譽是否已經惡化
從退信與日誌中查看的地方
如果收到退信,message/delivery-status 格式的 DSN 裡的 Final-Recipient、Status、Action、Diagnostic-Code 很重要。RFC 3464 定義了這類機器可讀的配送失敗資訊。例如若出現 Diagnostic-Code: smtp; 550 relay not permitted 這樣的內容,就代表是 SMTP 端的拒絕,而不是應用程式層的問題。
在伺服器端,至少要查看MTA 的配送日誌。以 Postfix 來說,會看到送達成功/失敗、佇列積壓、拒絕轉送、DNS 解析失敗、DKIM milter 的警告訊息。Exim 也是一樣。這裡列出代表性的確認指令。
# 範例:Postfix
journalctl -u postfix -n 200 --no-pager
postqueue -p
# 範例:Exim
exim -bp
不同主機商的日誌路徑與指令權限可能不同,請先確認能不能看到的是「郵件配送的日誌」,而不是「應用程式的日誌」。如果環境無法看到這部分,改用外部 SMTP 會更容易分析問題。
Gmail 與 Outlook 的檢視方式
Gmail 可以透過「顯示原始郵件」,Microsoft 的 Outlook 則可以透過「訊息詳細資料」或「網際網路標頭」確認原始標頭。調查聯絡表單不達問題時,比起截圖,保存標頭全文的文字內容再進行比對會更有效。
判斷收件端錯誤的方法
Gmail 系列的錯誤,通常可以從代碼推斷出原因。
| 錯誤範例 | 意義 | 主要對策 |
|---|---|---|
5.7.27 |
SPF 未通過 | 在 SPF 記錄中加入送信來源 |
5.7.30 |
DKIM 未通過 | 修正 DKIM 金鑰/簽章設定 |
4.7.32 |
From: 與 SPF/DKIM 的組織網域不一致 |
重新檢視 From: 的設計 |
5.7.25 |
PTR/反向解析不完善 | 整理送信 IP 的反向解析 |
Google 的 FAQ 也明確列出這些錯誤與對應方針。聯絡表單中特別常見的是 4.7.32 這種對齊不一致的問題。
最終的判斷標準
如果同時滿足以下 3 個條件,就可以說聯絡表單通知的設計相當穩固。
From:屬於example.com底下Reply-To:是表單使用者的地址Authentication-Results中出現dmarc=pass
只要這 3 點都到位,無論使用 SendGrid、SES、Mailgun、共享主機還是 SMTP 函式庫,設計上都是站得住腳的。反過來說,只要缺了其中一項,最快的方式就是先從 From: 的設計開始懷疑。
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
為什麼郵件安全裡 PPAP 行不通?正確的做法是什麼?
本文整理為什麼把附件壓成有密碼的 ZIP 後再用同一條郵件路徑寄出密碼的 PPAP 在當今環境下並不安全,並從竊聽防護、寄錯防護、惡意程式檢查與真實性等角度說明問題,再以 TLS、S/MIME、附帶驗證的下載等實務替代方案,協助讀者重新設計符合目的的郵件與檔案交付控制。
不被特定服務綁住的中小企業群發郵件設計方式
本文以中小企業為對象,整理在不導入專用 EDM 服務的前提下,如何用既有網域與官網建立小型派送平台:個別寄送、訂閱與抑制名單、自動退訂動線,以及 SPF/DKIM/DMARC 與 PTR、TLS 等寄件者認證的最低要求,幫助讀者在數十至數百封規模長期穩定不出事地持續發信。
服務頁面該怎麼做 - 技術型、B2B 的整理步驟
本文為技術型、B2B 服務頁面整理出諮詢入口、比較材料、送出前確認等三個角色,並提供標題排序、CTA 文案與公開前檢查點的具體寫法。讀者能立刻判斷頁面該為誰、做什麼用,縮短通往諮詢的距離。
用 PerfView 與 dotnet-trace 找出「變慢」的原因 ── .NET 效能調查實務入門
當商用應用程式「變慢」「CPU 卡住不動」「偶爾當掉」時,該用哪個工具看什麼?本文整理 PerfView 與 dotnet-trace 的角色分工、CPU 取樣的讀法(inclusive/exclusive)、用 ThreadTime 調查阻塞時間,以及與 EventSou...
BtoB 企業的 Google 廣告小額投放 ── 用每月數萬日圓做出成果的設計與每週操作程序
本文是給想用每月數萬日圓預算開始投放 Google 廣告的 BtoB 企業的實務指南。內容涵蓋平均每日預算與請款上限的運作機制、轉換追蹤的整備、聚焦高意圖關鍵字、否定關鍵字與搜尋字詞報表的每週操作,以及小額預算下的出價策略選擇,皆以 Google 官方資訊為依據整理。
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
常見問題
整理諮詢這個主題時常見的問題。
- 聯絡表單郵件收不到最常見的原因是什麼?
- 問題通常不在 SMTP 連線本身,而在於看得見的寄件人(From:)與實際被認證的寄件人(SPF 的 MAIL FROM / DKIM 的 d=)沒有對齊。最常見的情況是把表單使用者的 Gmail 地址直接放進 From:。例如透過網站的 SMTP 送信卻寫 From: taro@gmail.com,實際被認證的是網站端網域,但 From: 卻顯示 Gmail,造成落差,導致 DMARC 對齊失敗。
- 聯絡表單通知郵件的 From 應該怎麼設定?
- 基本原則是 From: 固定使用自家網站的網域,並把表單使用者的地址放進 Reply-To:。這樣既能確保方便回覆,也能讓看得見的寄件人與已認證的寄件人在網域上一致,到達率會更穩定。Sender: 只在「作者與實際送信主體不同」時才使用,Return-Path 不該手動寫進標頭,而是應在 MTA 或郵件服務端設定為信封寄件人(envelope sender)。
- 調查郵件不達,第一步該做什麼?
- 在檢查程式碼之前,先看收到郵件的原始標頭。以 Gmail 來說,可以從「顯示原始郵件」確認 Authentication-Results、Return-Path、From、DKIM-Signature。最優先要確認的是 From: 的網域、Return-Path: 的網域,以及 spf / dkim / dmarc 是否都是 pass。若 dmarc=fail,要分辨是認證本身失敗,還是對齊失敗(alignment failure)。DNS 端則用 dig 或 nslookup 查詢 SPF、DKIM、DMARC 這 3 筆記錄來確認。
- DMARC 可以一開始就設成 reject 嗎?
- 比起一開始就設成 reject,先用 p=none 的觀測模式確認流量與正規送信來源,再逐步升到 quarantine、reject 會更安全。p=none 只是監控,quarantine 建議隔離,reject 建議在 SMTP 階段拒收。此外,轉寄容易破壞 SPF,中繼過程中若本文或標頭被改寫,DKIM 也會失效,因此不要只依賴 SPF,務必同時啟用 DKIM 也很重要。
作者檔案
本文作者的個人檔案頁面。
Go Komura
小村軟體有限公司 代表
以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。