執行摘要
聯絡表單通知郵件「明明送信成功卻收不到」的最大原因,通常不在 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 文案與公開前檢查點的具體寫法。讀者能立刻判斷頁面該為誰、做什麼用,縮短通往諮詢的距離。
什麼是數位發票(Digital Invoice)?──與「把請款單 PDF 寄送 email」有何不同
數位發票是指將請款資訊從賣方系統直接以資料形式傳遞給買方系統、無需人工介入的機制。本文將淺顯解說它與 PDF 請款單的差異、與日本發票制度(Invoice 制度)的關係、Peppol・JP PINT 的運作方式、與電子帳簿保存法的關聯,以及中小企業的入門方法。
Windows 應用程式的工作列通知區常駐與 Toast 通知 —— NotifyIcon 的陷阱與 AppNotification 的選型
本文整理將業務用 Windows 應用程式常駐於工作列通知區,並透過 Toast 通知告知使用者的實作要點。內容涵蓋 NotifyIcon 的正確用法與「關閉後仍留在通知區」的設計、檔案總管重新啟動時的重新註冊、三種 Toast API(Windows App SDK Ap...
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
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 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。