問い合わせフォームのメールが届かない原因と直し方

· · メール配信, SPF, DKIM, DMARC, 問い合わせ導線改善, B2B

エグゼクティブサマリー

問い合わせフォームの通知メールが「送信は成功しているのに届かない」最大の原因は、SMTP の接続自体ではなく、見えている差出人(From:)と、実際に認証される差出人(SPF の MAIL FROM / DKIM の d=)が噛み合っていないことです。DMARC は RFC5322.From を基準に、SPF か DKIM のどちらか一方でもアラインメント付きで通っているかを見ます。つまり、フォーム送信者の Gmail や会社アドレスをそのまま From: に入れる設計は、返信しやすそうに見えても、実運用では失敗しやすい設計です。

実務上の結論はシンプルです。問い合わせ通知メールの From: は自サイトのドメインに固定し、フォーム利用者のアドレスは Reply-To: に入れるのが基本です。Sender: は「作者と実際の送信主体が異なる」ときだけ使い、Return-Path はヘッダを手書きするのではなく、MTA やメールサービス側でエンベロープ送信者として設定します。

また、転送は SPF を壊しやすく、メーリングリストや中継サーバで本文やヘッダが書き換わると DKIM も壊れます。だからこそ、問い合わせフォームでは SPF だけに依存せず DKIM を必ず有効化し、DMARC はまず p=none で観測してから quarantine / reject に進めるのが安全です。

さらに、Google は Gmail 宛て送信について、送信ドメインに SPF または DKIM を設定すること、一定規模以上の送信では SPF・DKIM・DMARC を揃えること、そして From: ヘッダのドメインが SPF または DKIM の認証済みドメインと一致していることを求めています。配信問題の正体は、たいてい「メールコード」ではなく「差出人設計」にあります。

SPF・DKIM・DMARC と From ヘッダの役割

メール配送では、エンベロープヘッダを分けて考える必要があります。SPF が主に検証するのは SMTP セッション中の MAIL FROM で、これは配送上の差出人です。最終配送時には、その逆経路が Return-Path として 1 つだけ残されるべきで、送信側 SMTP システムは最初から Return-Path ヘッダを持ったメッセージを作るべきではありません。いっぽう From: は本文ヘッダ側の「誰からのメールに見えるか」を表し、Reply-To: は返信先、Sender: は実際に送出した主体を表します。

RFC 5322 は、From:メッセージの作者Sender:実送信主体として定義しています。作者と送信主体が同じなら Sender: は使うべきではなく、返信先を作者とは別にしたいときは Reply-To: を使うのが正しい筋です。さらに RFC 5322 は、From: に作者のものでないアドレスを入れるべきではない、と明記しています。問い合わせフォーム通知は通常、ユーザー本人が MUA から送っているのではなく、サイト側システムが作る通知なので、利用者アドレスを From: に置く設計は仕様の意味ともズレやすいです。

DKIM は、メッセージの一部ヘッダと本文に対して署名を付け、受信側が DNS にある公開鍵で検証します。署名ドメインは DKIM-Signatured= で表され、公開鍵は selector._domainkey.example.com のようにセレクタで引きます。DKIM は「転送でも比較的生き残りやすい」認証として使われますが、本文や署名対象ヘッダが途中で改変されると、bh= の本文ハッシュや署名検証が失敗します。

DMARC は、SPF や DKIM の成否だけではなく、それらの認証済みドメインが From: ドメインと整合しているかを見ます。ここがポイントです。SPF が通っても MAIL FROM=bounces.vendor.netFrom: contact@example.com なら、SPF のアラインメントは落ちます。DKIM が d=example.com で通っていれば DMARC はパスできますが、DKIM も無いと DMARC 失敗です。DMARC には adkim / aspf による strict / relaxed のアラインメントモード、p=none|quarantine|reject のポリシー、rua / ruf のレポート送信先が定義されています。

Gmail の最近のガイドラインは、この設計思想をそのまま運用要件に落とし込んでいます。Google は「From: ヘッダのなりすましをしないこと」「ダイレクトメールでは From: ヘッダ内ドメインが SPF ドメインまたは DKIM ドメインと一致していること」を明示しています。問い合わせフォーム通知はマーケティングメールではありませんが、受信側フィルタの基本ロジックは同じなので、この考え方を無視すると到達率は下がります。

受信側MXDNS送信MTA / SMTPサービスWebアプリフォーム利用者受信側MXDNS送信MTA / SMTPサービスWebアプリフォーム利用者フォーム送信From / Reply-To / Sender を決定SMTP送信要求DKIM署名付与MAIL FROM / RCPT TO / DATASPF参照(MAIL FROM)DKIM公開鍵参照(selector._domainkey)DMARC参照(_dmarc + Fromドメイン)Alignment判定受信・迷惑メール・拒否・バウンス

このフローで重要なのは、DMARC の判定基準が最後まで From: ドメイン中心だという点です。アプリ側で From:、SMTP 側で MAIL FROM、DNS 側で SPF/DKIM/DMARC を別々に触るため、どれか 1 つだけ直しても不達は解決しません。

問い合わせフォームで起こりやすい失敗シナリオ

もっとも多いのは、フォーム利用者のアドレスを 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 を ON にすると 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 を使うべき場面で SenderFrom をいじってしまうケースがあります。返信先を利用者に向けたいだけなら Reply-To で十分です。Sender は「作者と実送信主体が異なる」ことを明示したいときに使うヘッダで、問い合わせフォームで常用するものではありません。設計の目的が「返信を利用者へ返したい」なのか、「通知の責任主体を示したい」なのかを分けて考えると、事故が減ります。

診断手順とコマンド

まずやるべきことは、コードを見る前に、生のメールヘッダを見ることです。Gmail では「メッセージのソースを表示」、Outlook では「メッセージの詳細」または「インターネット ヘッダー」から、Authentication-ResultsReturn-PathFromReply-ToDKIM-SignatureReceived を確認できます。ここを見れば、問題が「送れていない」のか「認証と整合性が崩れている」のかをかなりの精度で切り分けられます。

ヘッダで最初に見るポイント

最優先はこの 5 点です。

  1. From: のドメインは何か
  2. Return-Path: のドメインは何か
  3. Authentication-Results:spf=pass / dkim=pass / dmarc=pass が出ているか
  4. dkim=pass のとき header.i= または d= がどのドメインか
  5. 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 レコードが 1 つにまとまっているかDKIM の公開鍵が引けるかDMARC に p= があるかです。SPF は DNS 参照を発生させるメカニズムを合計 10 個までに制限すべきと RFC 7208 が定めており、超えると 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 / 同じ宛先で」1 通だけ送って再現するのが有効です。

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 や共用サーバで「何となく届かない」ケースの一次診断に向いています。

使い方は単純です。

  1. mail-tester で発行されたテストアドレスを取得する
  2. 問い合わせフォームと同じ経路で 1 通送る
  3. スコアと、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.com
Reply-To: visitor@gmail.com
Return-Path: bounce@example.com
ほぼすべての問い合わせフォーム DMARC を取りやすい/返信しやすい/実装が単純 Reply-To を忘れると返信先がサイト側になる
サブドメイン分離 From: contact@form.example.com
Reply-To: visitor@gmail.com
Return-Path: bounce.form.example.com
フォーム通知を本体メールと分離したい レピュテーション分離しやすい/管理しやすい SPF/DKIM/DMARC をサブドメイン側でも整える必要がある
Sender 明示型 From: contact@example.com
Sender: mailer@example.com
Reply-To: visitor@gmail.com
送信主体を明示したい特殊要件 運用責任主体を見せられる 通常は不要。作者と送信主体が同じなら冗長
非推奨パターン From: visitor@gmail.com
Reply-To: visitor@gmail.com
返信先を目立たせたいだけの実装 見た目だけは自然 DMARC 失敗の原因になりやすい。問い合わせ通知では避けるべき

この表の根拠は RFC 5322 の From / Sender / Reply-To の意味づけと、RFC 5321 の Return-Path の扱い、そして DMARC が From: 基準で整合性を判定するという仕様です。フォーム通知のデフォルトは 1 行目の「推奨パターン」 で十分です。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.com
  • Reply-To: はフォーム利用者アドレス
  • Return-Path / MAIL FROM は bounce.example.com など自分が管理するバウンス用サブドメイン
  • DKIM は example.com または送信用サブドメインで署名
  • DMARC は 見える From: ドメイン に置く

SendGrid の典型設定

SendGrid では Domain Authentication が前提で、Automated Security を ON にすると 3 つの CNAME が生成されます。OFF の場合は 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 では 3 つの CNAME を DNS に追加します。

; 例: 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 custom 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 の評判が悪い場合にはブロック原因になり得ると案内しています。

実務的には、この順で進めるのが安全です。

  1. ホスティング会社が SPF/DKIM/PTR を管理画面またはサポートで設定可能か確認する
  2. From: は必ず自ドメインに固定する
  3. SPF にホスティングの送信元 IP または許可された送信ドメインを含める
  4. DKIM をホスティング機能で有効化する。無ければ外部 SMTP へ切り替える
  5. 可能なら 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 や outbound relay を自分で制御できないなら、外部 SMTP に移す方が近道です。問い合わせフォーム通知のような少量送信でも、認証が弱いローカル MTA は Gmail や企業メールに不利です。

サイトが PHP mail() または SMTP ライブラリを使う場合

PHP mail() は便利ですが、認証と到達率の観点では「その先の MTA が何者か」に依存します。PHP マニュアルは、メールには From ヘッダが必要で、sendmail_path 経由の送信では追加パラメータでエンベロープ送信者を指定できる、と説明しています。逆にいえば、mail() を使っても SPF/DKIM/DMARC が自動で整うわけではありません

まずは最低限、こう設計します。

  • From:contact@example.com
  • Reply-To: はフォーム利用者
  • エンベロープ送信者は bounce@example.com
  • 本文には利用者のアドレスも明記する
  • 可能なら mail() ではなく、認証済み SMTP を使う

mail() の最小構成例

ヘッダに利用者入力をそのまま入れるのは危険です。$name$email に CR/LF が混ざると、攻撃者が Bcc: などの追加ヘッダを差し込めてしまい、フォームがスパム中継器になります。PHP マニュアルもヘッダで使う外部入力は 必ず検証/正規化 するよう案内しています。下記の例では、エンベロープ送信者引数 (additional_params) を含め、ヘッダに混入させる値は事前にサニタイズします。

<?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 もシェルに渡るため、固定値だけを使い動的入力は混ぜない。
mail($to, $subject, $body, $headers, '-fbounce@example.com');

この例のポイントは二つです。一つは、Return-Path: をヘッダとして書かず、5 番目の引数の -f でエンベロープ送信者を渡していること。もう一つは、Reply-To: 等のヘッダに混入させる利用者入力を、CR/LF を弾くサニタイズ関数を通していることです。サニタイズなしでヘッダを組み立てると、攻撃者が \r\nBcc: victim@example.com のような文字列を流し込んで追加ヘッダを差し込めるため、PHP マニュアルでも外部入力をヘッダに使うときの検証は必須とされています。additional_params も最終的にシェルに渡るため、利用者入力を混ぜず固定値で渡してください。

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..."

セレクタは鍵ローテーションのために使われ、複数の公開鍵を同じドメインに共存させられます。運用では 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 中の拒否推奨という意味です。adkimaspf は 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-Resultsspf=pass または dkim=pass があり、さらに dmarc=pass
  • dmarc=fail なら、認証失敗か alignment failure か
  • SPF が 1 レコードにまとまっているか
  • SPF の参照回数が多すぎないか
  • DKIM 公開鍵が引けるか
  • DMARC に p= があるか
  • 送信元 IP の PTR と逆引きが妥当か
  • 共有 IP を使っていないか、またはレピュテーションが悪化していないか

バウンスとログで見る場所

バウンスメールが来ているなら、message/delivery-status 形式の DSN の中にある Final-RecipientStatusActionDiagnostic-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 条件を同時に満たしていれば、問い合わせフォーム通知の設計は堅いと言えます。

  1. From:example.com 配下
  2. Reply-To: がフォーム利用者アドレス
  3. Authentication-Resultsdmarc=pass が出る

この 3 つが揃っていれば、SendGrid・SES・Mailgun・共有ホスティング・SMTP ライブラリのどれを使っていても、設計としては筋が通っています。逆に、どれか 1 つでも欠けるなら、まず From: 設計から疑うのが最短です。

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

HP制作

問い合わせフォームの設計、通知メールの差出人運用、Reply-To 設計を含めた問い合わせ導線の整理は、ホームページ制作と一緒に進めやすいテーマだからです。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る