GENEVIS
無料相談受付中
相談する
Webサイト技術メモ

問い合わせフォームの再送で、通知メールを二重送信しないための設計

更新日:

問い合わせを送った直後に通信が切れると、ブラウザーにはエラーが出ても、サーバーでは通知メールの処理が進んでいる可能性があります。この状態で最初から送信し直すと、同じ相談を担当者へ重複して届けてしまいます。GENEVISのサイト制作では、受付の識別子と処理状態を使い、再試行でどこまで実行するかを判断する形にしました。

この記録は、2026年9月6日時点のPHP実装とローカル検証が対象です。本番のメール配送、受信トレイへの到着、長期運用は未検証です。

ブラウザーの結果と、メール処理の結果を分ける

送信ボタンを押した後には、いくつか別の処理があります。

  1. ブラウザーから送信内容が届く。
  2. サーバーで入力を検証する。
  3. 担当者への通知をメール配送処理へ渡す。
  4. 相談者への自動返信をメール配送処理へ渡す。
  5. ブラウザーへ受付結果を返す。

例えば5番の応答だけが届かなかった場合、3番と4番は終わっているかもしれません。ブラウザーで通信エラーが出たという理由だけでは、メールをもう一度送ってよいか判断できません。

紙の申込書に例えるなら、受付へ渡した後、控えを受け取る前にその場を離れてしまった状態です。もう一枚提出する前に、同じ受付かを確認するための番号が必要になります。

同じ受付には、同じ識別子を使う

今回は、フォームで取得した受付用の識別子を、同じ内容の再試行でも使います。サーバー側には、その識別子に対応する処理状態を保持します。

このように、同じ操作を繰り返しても余分な処理を増やさない性質を、冪等性と呼びます。識別子を付けるだけで成立するわけではなく、保存した状態を見て、実行済みの処理を飛ばすところまで必要です。

同じ識別子のまま内容を差し替えることは許可しません。検証済み入力からHMACを作って保存し、再試行時の内容と照合します。内容が違えば別の処理として扱う前にエラーにします。編集可能な画面から入力を直す場合は、新しい識別子を取得します。

一方、通信が切れて結果不明になった受付を、安易に新しい識別子へ取り替えると、同じ相談を別件として送れてしまいます。画面側も同じ識別子と内容を保ち、元の受付結果を確認できるようにしています。

担当者通知と自動返信は、別々に状態を持つ

このフォームでは、担当者への通知と相談者への自動返信を順に処理します。状態は各メールについて、次の4つを持たせています。

new      未実行
sending  配送処理を呼び出す直前の状態を保存済み
sent     配送処理への引き渡しが成功し、結果を保存済み
failed   配送処理が失敗を返し、結果を保存済み

ここでの sent は、この実装内の状態名です。利用しているPHPの mail() が成功を返すのは、配送処理に受け付けられたときで、宛先への到着保証ではありません。PHP公式マニュアルの戻り値の説明

両方をひとつの「送信済み」で管理すると、担当者通知だけ成功した場面を表せません。状態を分けることで、次の動作を選べます。

  • 担当者通知が失敗したら、自動返信は実行しない。再試行時は通知からやり直す。
  • 担当者通知が成功し、自動返信だけ失敗したら、受付済みと案内する。再試行では自動返信だけを処理する。
  • 両方が成功済みなら、メール処理を呼ばず、保存済みの受付結果を返す。

自動返信だけの失敗で、担当者通知まで送り直さないことが、この分離の目的です。

配送途中で止まった場合は、結果不明として扱う

送信処理の前後では、次の順序で状態を保存します。

sending を保存

メール配送処理を呼ぶ

戻り値に応じて sent / failed を保存

問題になるのは、配送処理へ渡した後、結果を保存する前にプロセスが止まる場合です。記録には sending が残りますが、メールが引き渡されたかは、アプリの記録だけでは断定できません。呼び出す直前に止まった場合も、同じ状態が残ります。

今回の実装では、再試行時に sending があれば自動再送を止め、結果不明として受付状況の確認を案内します。

この選択には、未送信だった場合も人の確認が必要になるという負担があります。通知を重複させる可能性と、確認待ちになる負担を比較し、現在のフォームでは後者を受け入れる設計にしました。すべての業務に同じ判断を当てはめるものではありません。

メール配送とローカルの状態保存を、ひとつのトランザクションとして確定することは、この構成ではできません。「送信済みか不明」という状態を残し、確認する手順も用意する必要があります。

同時に届く再試行と、保存する情報の範囲

同じ識別子のリクエストが同時に来た場合に備え、状態を読むところから更新までをファイルロックの中で処理しています。先に実行した処理の結果を、後の処理が読み直してから判断する構成です。

これは、同じ状態ファイルを参照する現在の単一サーバー構成を前提にしています。複数サーバーへ広げる場合には、共有する状態ストアと排他制御をあらためて設計する必要があります。

再送防止のためのファイルには、問い合わせ本文や連絡先そのものを保存せず、内容照合用のHMAC、処理状態、受付番号、時刻・期限を保持します。ただし、担当者へ渡す通知メールには問い合わせ内容を含むため、メール側の保管や削除は別に考える必要があります。

また、状態には保持期限があります。期限を超えた再送や、別の識別子を使って新規に送られた相談まで、同じ受付として判定する仕組みではありません。本文が同じでも別の相談である可能性があるため、内容の一致だけで一律に送信を止める設計にはしていません。

ローカルで確認したことと、公開時に確認すること

テストでは実メールの代わりに結果を返す送信処理を使い、成功・失敗・例外を切り替えて確認しました。

  • 同じ識別子と内容を再送しても、成功済みのメール処理が増えない。
  • 同じ識別子で内容を変えた場合に拒否する。
  • 担当者通知の失敗後は通知から再試行し、自動返信だけの失敗後は自動返信だけを処理する。
  • 送信中に例外が起きて sending が残ると、次の試行で自動再送しない。
  • 再送防止の状態ファイルへ、入力した本文・氏名・連絡先をそのまま保存しない。

これらはアプリの分岐と状態管理の確認です。本番の配送設定や迷惑メール判定、日本語の表示、実際の受信は、公開時の別の確認として残っています。

配送設定の確認先を整理する際は、ホームページとメールの契約先・管理者の確認項目を使えます。アプリの再送制御とは別に、メール環境を誰が管理しているかを把握するための記事です。

フォームの処理を組むときは、「失敗したら再送する」の前に、どこまで成功したかを判断できる情報を決めておく。さらに、判断できないときに利用者へ何を案内するかまで考える。この2点が、今回の実装で残しておきたい設計上の要点です。

RELATED SERVICE

この記事に関連する支援

UNIT 01

サイト制作

企画、構成、文章、デザイン、実装まで。新規制作もリニューアルも、公開後の運用を見据えて進めます。

サイト制作の詳細

あわせて相談できること:効率化・自動化

RELATED ARTICLES

あわせて読みたい

TOP