コラム
問い合わせフォームをHTMLで作る手順|送信処理とスパム対策まで
「問い合わせフォーム html」で調べて見つけたコードを貼ってみたものの、送信ボタンを押しても何も届かない。あるいは、なんとか動いたと思ったら、数日後から英文スパムと営業メールが毎日届くようになった——。HTMLで問い合わせフォームを自作するときに、多くの方がこのどちらかでつまずきます。
原因ははっきりしていて、HTMLが担当しているのは「入力欄を表示する」ところまでだからです。送信された内容を受け取ってメールにする処理、不正な送信を弾く処理、そして届いた中身を仕分ける処理は、HTMLの外側で別に用意する必要があります。
本記事では、問い合わせフォームをHTMLで作る手順を、コピペで使えるHTMLコード → 送信処理(PHP)→ セキュリティ対策 → スパム対策の順に解説します。最後に、HTMLやボット対策では止まらない人間が送る営業メールをどう扱うかも整理します。
この記事の結論(先に要点)
HTMLで問い合わせフォームを自作する場合、必要な部品と、それぞれを担当する場所は次のとおりです。
| 部品 | 担当する場所 | HTMLだけで足りるか | 本記事の該当箇所 |
|---|---|---|---|
| 入力欄・ラベル・送信ボタン | HTML | ◎ | 2章 |
| ブラウザ上の入力チェック | HTML(required 等) | ○(補助にとどまる) | 2章 |
| 送信内容の受け取り・メール送信 | サーバー側(PHP等) | ✕ | 1章・3章 |
| サーバー側の入力チェック | サーバー側 | ✕ | 3章 |
| ヘッダーインジェクション・XSS・CSRF対策 | サーバー側 | ✕ | 5章 |
| ボットによる自動送信の対策 | HTML+サーバー側 | △(検証はサーバー側) | 6章 |
| 人間が送る営業メールの仕分け | 内容を読む判定 | ✕ | 7章・8章 |
→ HTMLは「入力の受け皿」にすぎません。送信処理とセキュリティはサーバー側、ボット対策はHTMLとサーバー側の両方、営業メールの仕分けは内容を読む別の層が担当します。自作するなら、この分担を最初に決めておくと作り直しを避けられます。
1. HTMLだけでは問い合わせフォームは動かない
まず前提を押さえておきます。<form> タグは、入力された内容を action 属性で指定した送信先にHTTPで送るところまでしか担当しません。送り先で内容を受け取り、メールに整形して送信する処理がなければ、データはどこにも届きません。
mailto: で送る方法をおすすめしない理由
送信先を用意せずに済む方法として、action="mailto:info@example.co.jp" と書く方法が紹介されることがあります。これはサーバー側の処理が不要な代わりに、次の問題があります。
- 送信者のパソコンにメールソフトが設定されていないと、何も起きないか、意図しないアプリが起動する
- 送信者のメールソフトに下書きが開くだけなので、そこで離脱されやすい
- 送信先のメールアドレスがHTMLにそのまま載るため、アドレス収集ボットに拾われやすい
- 入力チェックや送信記録ができない
問い合わせ窓口として使うには不確実な点が多いため、送信先(サーバー側の処理)を用意する前提で進めるのが現実的です。
送信先の選択肢は3つ
| 送信先 | 仕組み | 向いているケース | 注意点 |
|---|---|---|---|
| PHPなどのサーバー側スクリプト | 自分のサーバーで受け取り、メールを送信する | PHPが動くレンタルサーバーを使っている | セキュリティ対策をすべて自分で担う |
| フォーム送信を受け付ける外部サービス | action に外部サービスのURLを指定する | 静的サイト・サーバー側の処理を書きたくない | 無料枠・保存期間などはサービスごとに異なる |
| フォーム作成サービスの埋め込み | サービスで作ったフォームをタグで設置する | 最短で公開したい、運用も任せたい | デザインの自由度はサービスによる |
本記事では、HTMLで自作する方がもっとも多く選ぶ「PHPで送信処理を書く」方式を中心に解説します。外部サービスの例としては、静的サイト向けのフォームバックエンドを比較したDefform vs Formspreeも参考になります。作り方そのものを3通りから選ぶ段階であれば、問い合わせフォームの作り方|項目設計から営業メール対策までの7手順を先に読んでおくと判断しやすくなります。
2. 問い合わせフォームのHTMLコード(コピペ用テンプレート)
まずはフォーム本体です。項目は「お名前・会社名・メールアドレス・電話番号・お問い合わせ内容・プライバシーポリシーへの同意」の基本構成にしています。CSRF対策のトークンを埋め込むため、ファイルは contact.php として保存する前提です(理由は5章で説明します)。
<?php
// contact.php(UTF-8で保存)
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>
<form action="/send.php" method="post">
<input type="hidden" name="csrf_token"
value="<?= htmlspecialchars($_SESSION['csrf_token'], ENT_QUOTES, 'UTF-8') ?>">
<label for="name">お名前(必須)</label>
<input type="text" id="name" name="name" autocomplete="name" maxlength="50" required>
<label for="company">会社名</label>
<input type="text" id="company" name="company" autocomplete="organization" maxlength="100">
<label for="email">メールアドレス(必須)</label>
<input type="email" id="email" name="email" autocomplete="email" maxlength="254" required>
<label for="tel">電話番号</label>
<input type="tel" id="tel" name="tel" autocomplete="tel" maxlength="20">
<label for="message">お問い合わせ内容(必須)</label>
<textarea id="message" name="message" rows="8" maxlength="2000" required></textarea>
<label>
<input type="checkbox" name="agree" value="1" required>
<a href="/privacy-policy/" target="_blank" rel="noopener">プライバシーポリシー</a>に同意する
</label>
<button type="submit">送信する</button>
</form>
各属性の役割
コードの中で、意味を理解しておきたい属性は次のとおりです。
| 属性 | 役割 | 省略するとどうなるか |
|---|---|---|
method="post" | 入力内容をリクエスト本文で送る | get だと内容がURLに載り、履歴やログに残る |
<label for> と id | ラベルと入力欄を結びつける | ラベルをタップしても入力欄が選択されず、読み上げソフトでも項目名が伝わりにくい |
type="email" / type="tel" | 入力形式を指定する | スマートフォンで適したキーボードが出ない |
autocomplete | ブラウザの自動入力を効かせる | 入力の手間が増え、離脱につながる |
required | 未入力のまま送信させない | 空の送信が届く |
maxlength | 文字数の上限を設ける | 極端に長い送信を受け付けてしまう |
電話番号を任意にしているのは、必須にすると個人からの問い合わせが減りやすいためです。項目を増やしてもスパムが減るわけではないので、返信に必要な情報だけを必須にするのが基本です(項目設計の考え方は問い合わせフォームの作り方で詳しく扱っています)。
ブラウザの入力チェックは「補助」にとどまる
required や type="email" による入力チェックは、正規のお客様の入力ミスを防ぐには有効です。ただし、これらはブラウザ側の仕組みなので、HTMLを経由せずに直接データを送ってくるボットや、開発者ツールで属性を外した送信には効きません。同じチェックを必ずサーバー側でも行う必要があります。これが次章の内容です。
3. 問い合わせフォームの送信処理をPHPで書く
続いて、action で指定した send.php の中身です。処理の流れは次の5段階です。
- POST以外のアクセスを断る
- CSRFトークンを確認する
- 入力内容をサーバー側でチェックする
- メールを送信する
- 完了ページへリダイレクトする
<?php
// send.php(UTF-8で保存)
session_start();
mb_language('Japanese');
mb_internal_encoding('UTF-8');
// 1. POST以外のアクセスを断る
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit;
}
// 文字列以外(配列など)が送られてきても扱えるようにする
function field(string $key): string {
$value = $_POST[$key] ?? '';
return is_string($value) ? trim($value) : '';
}
// 2. CSRFトークンの確認
if (!hash_equals($_SESSION['csrf_token'] ?? '', field('csrf_token'))) {
http_response_code(400);
exit('送信できませんでした。お手数ですが、フォームのページから再度お送りください。');
}
// 3. サーバー側の入力チェック
$name = field('name');
$company = field('company');
$email = field('email');
$tel = field('tel');
$message = field('message');
$agree = field('agree') === '1';
$errors = [];
if ($name === '' || mb_strlen($name) > 50) $errors[] = 'お名前';
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = 'メールアドレス';
if ($message === '' || mb_strlen($message) > 2000) $errors[] = 'お問い合わせ内容';
if (!$agree) $errors[] = 'プライバシーポリシーへの同意';
// 1行で入力する項目に改行が含まれていたら受け付けない
if (preg_match('/[\r\n]/', $name . $company . $email . $tel)) $errors[] = '入力内容';
if ($errors) {
http_response_code(400);
echo '次の項目をご確認ください:';
echo htmlspecialchars(implode('、', array_unique($errors)), ENT_QUOTES, 'UTF-8');
exit;
}
// 4. メール送信
$to = 'info@example.co.jp'; // 通知先(共有アドレス推奨)
$subject = '【Webサイトお問い合わせ】' . $name . ' 様';
$body = "お名前:{$name}\n会社名:{$company}\nメール:{$email}\n電話番号:{$tel}\n\n"
. "お問い合わせ内容:\n{$message}\n";
$headers = "From: noreply@example.co.jp\r\nReply-To: {$email}";
if (!mb_send_mail($to, $subject, $body, $headers)) {
error_log('[contact] mail send failed');
http_response_code(500);
exit('送信に失敗しました。お手数ですが、時間をおいて再度お試しください。');
}
// 5. トークンを破棄して完了ページへ(再読み込みでの二重送信を防ぐ)
unset($_SESSION['csrf_token']);
header('Location: /thanks/');
exit;
エラー表示は最小限にしています。実際のサイトでは、エラー時に入力内容を保持したままフォームに戻す画面を作ると親切です。その場合も、画面に出力する値は必ず htmlspecialchars() を通してください。
メールが届かないときに確認すること
PHPのフォームで「送信完了と表示されるのに、メールが届かない」という相談は非常に多くあります。原因の多くは次のどれかです。
| 症状 | よくある原因 | 対処 |
|---|---|---|
| 1通も届かない | From のアドレスがサーバーのドメインと一致せず、受信側で破棄されている | From を自社ドメインのアドレスにし、SPFなど送信ドメイン認証の設定を確認する |
| 迷惑メールフォルダに入る | 送信元の信頼性が低い | 同上。Gmailなどで受け取る場合は送信者のガイドラインも確認する |
| 件名・本文が文字化けする | 文字コードの指定がない | mb_language('Japanese') と mb_internal_encoding('UTF-8') を設定し、ファイルもUTF-8で保存する |
| サーバーによって送れない | レンタルサーバー側でPHPからの送信に制限がある | サーバー会社のマニュアルで mail() / mb_send_mail() の扱いを確認する |
特に From に送信者が入力したメールアドレスを入れるのは避けてください。自社のサーバーから他人のドメインを名乗って送ることになり、受信側で弾かれる原因になります。上のコードのように、From は自社ドメインの固定アドレスにして、送信者のアドレスは Reply-To に入れておけば、通知メールにそのまま返信できます。Gmail宛ての到達性については、Googleのメール送信者のガイドラインにまとまっています。
自動返信メールを送る場合
送信者に「受け付けました」と返す自動返信を付けることもできます。ただし、自動返信は入力されたメールアドレス宛てに、こちらのサーバーから送信することになるため、他人のアドレスを入力されると第三者に自社名義のメールが届きます。本文に問い合わせ内容をそのまま載せると、悪用される余地も生まれます。
自動返信を付けるなら、本文は定型文にとどめ、入力内容の引用は最小限にするのが安全です。文面の例は問い合わせ返信メールのテンプレート集にまとめています。
4. 完了ページ(サンクスページ)を用意する
送信処理の最後で /thanks/ へリダイレクトしているのは、POSTの結果画面をそのまま表示すると、ブラウザの再読み込みで同じ内容が再送信されるためです。送信処理のあとに別ページへ移動させる(いわゆるPRGパターン)ことで、二重送信を防げます。
完了ページには、次の2点を書いておくと問い合わせ後の不安が減ります。
- 送信が完了したこと
- 返信の目安(例:「2営業日以内にご連絡します」)
専用のURLを持つ完了ページにしておくと、GA4などでコンバージョンを計測しやすくなる利点もあります。ただし、営業メールやスパムの送信もコンバージョンとして数えられる点には注意が必要です。この問題と除外方法はGA4のコンバージョンが営業メール・スパムで水増しされる問題で解説しています。
5. 自作フォームで必ず入れるセキュリティ対策
フォームを自作する場合、プラグインやサービスが裏で処理してくれていた部分を、すべて自分で担うことになります。最低限、次の4つは押さえてください。
| 脅威 | 何が起きるか | 対策(上のコードでの該当箇所) |
|---|---|---|
| メールヘッダーインジェクション | 入力欄に改行を混ぜられ、宛先を追加されて迷惑メールの踏み台にされる | 1行項目の改行を拒否する、FILTER_VALIDATE_EMAIL でアドレスを検証する |
| XSS(クロスサイトスクリプティング) | 画面に出した入力値にスクリプトを仕込まれる | 出力時に htmlspecialchars() でエスケープする |
| CSRF(クロスサイトリクエストフォージェリ) | 外部サイトから勝手に送信させられる | セッションに保存したトークンと照合する |
| 二重送信 | 再読み込みや連打で同じ内容が複数届く | 送信後にリダイレクトする、トークンを使い捨てにする |
ヘッダーインジェクションは「改行」が入口
メールのヘッダーは改行で区切られています。そのため、Reply-To や件名に入れる値に改行が含まれていると、そこから Bcc: などのヘッダーを追加され、自社サーバーから大量の迷惑メールを送らされるおそれがあります。
上のコードでは、お名前・会社名・メールアドレス・電話番号の1行項目に改行が含まれていたら受け付けないようにしています。本文(お問い合わせ内容)は改行を含むのが普通なので、ヘッダーには使わず本文にだけ入れる、という分け方がポイントです。
CSRFトークンでフォームを書き換えた理由
2章でフォームを contact.php にしたのは、ページ表示のたびにセッションへトークンを保存し、送信時に照合するためです。これにより、外部サイトから送信を仕掛けられることを防げます。
副次的な効果として、フォームのページを一度も読み込まずに、送信先へ直接データを投げつけてくる単純なボットも、トークンが一致しないため弾けます。ただし、ページを読み込んでから送ってくるボットには効かないため、ボット対策としては次章の手段と組み合わせて考えてください。
6. HTMLフォームのスパム対策(ボット対策)
ここまでで「正しく届くフォーム」はできあがりました。公開すると、次に起きるのがスパムです。自作フォームでも使えるボット対策は、主に次の3つです。
| 手段 | 仕組み | ユーザーの手間 | 実装の手間 |
|---|---|---|---|
| ハニーポット | 人間には見えない入力欄に記入があればボットとみなす | なし | 小さい(HTML・CSS・PHP数行) |
| 時間差トラップ | 表示から送信までが極端に短い送信をボットとみなす | なし | 小さい |
| reCAPTCHA v3 / Cloudflare Turnstile | 送信時の挙動から自動送信かどうかを推定する | ほぼなし | 中(キーの発行とサーバー側の検証が必要) |
ハニーポットと時間差トラップ
もっとも手軽なのがハニーポットです。HTMLに隠し入力欄を1つ足し、サーバー側で「値が入っていたら、エラーを返さずに完了画面へ送る」処理を加えます。隠し方を誤ると、正規のお客様が送信できなくなったり、支援技術を使う方に影響が出たりするため、実装の細部は問い合わせフォームのハニーポット実装|効く範囲と3つの限界のコードを参照してください。同記事では時間差トラップの実装例も紹介しています。
reCAPTCHA v3・Turnstileを自作フォームに入れる場合
reCAPTCHA v3やTurnstileは、HTMLにスクリプトを読み込むだけでは機能しません。送信時に発行されるトークンを、サーバー側で検証APIに問い合わせて確認するところまで実装して、はじめて効果が出ます。流れは共通です。
- 管理画面でサイトキーとシークレットキーを発行する
- HTMLにスクリプトとウィジェット(またはトークン取得処理)を追加する
send.phpで、受け取ったトークンをシークレットキーとともに検証APIへ送り、結果を確認する- 検証に失敗した送信は受け付けない
手順の詳細は、reCAPTCHAの公式ドキュメントとCloudflare Turnstileの公式ドキュメントにまとまっています。
ひとつ注意したいのが、reCAPTCHA v3のスコアのしきい値を厳しくしすぎないことです。営業メールは人間が送るため高いスコアで通過する一方、プライバシー保護設定の強いブラウザやVPN経由の正規のお客様が低いスコアになり、先に弾かれてしまいます(→reCAPTCHAが効かない理由)。
ひらがなチェックや海外IPブロックは補助として
「本文にひらがなを含まない送信を拒否する」「海外IPからのアクセスを拒否する」といった方法も、自作フォームなら数行で実装できます。英文のボットスパムには効果がありますが、海外のお客様からの問い合わせも弾いてしまいます。使う場合は、自社の顧客に海外からの問い合わせがないかを確認してからにしてください(→問い合わせフォームの海外IPブロック)。
7. HTMLで作ったフォームに残る「営業メール」の問題
ボット対策まで入れると、英文スパムや機械的な自動送信は目に見えて減ります。そして多くのサイトで、日本語の営業メールだけが残ります。
これは実装の不備ではありません。ハニーポットもreCAPTCHAもCSRFトークンも、判定しているのは「ボットが送ったか、人間が送ったか」です。フォーム営業の多くは、人間または人間と同じようにブラウザを操作するツールで、ページを開き、必須項目を埋め、同意にチェックを入れて送信されます。判定の軸から見れば正常な送信なので、仕様どおりに通過します。
[送信]
↓
【第1層】ボット判定 ← ハニーポット / reCAPTCHA / Turnstile / CSRFトークン
↓ 機械的な自動送信を除去
【第2層】内容の判定 ← ★ 自作フォームではここが空白になりがち
↓ 人間が送った営業メールを仕分ける
[本当に対応すべき問い合わせ]
自作フォームでこの第2層を埋めようとすると、選択肢は限られます。
| 手段 | 自作での実装 | 限界 |
|---|---|---|
| 「営業お断り」の文言を置く | HTMLに1行足すだけ | 読まない・無視する送信者には効かない |
| NGワードで拒否する | PHPで preg_match を足す | 表現を変えられると通る。正規の問い合わせを弾くおそれもある |
| 問い合わせ種別に「営業・ご提案」を設ける | <select> を足して振り分ける | 正しく選んでくれる送信者にしか効かない |
| 目視で仕分ける | 実装不要 | 毎日の確認時間がそのままコストになる |
お断り文言をフォームに置く場合の書き方と、同意チェックボックスとしてHTMLに組み込む例は問い合わせフォームの「営業お断り」文言で紹介しています。いずれもやる価値はありますが、営業メールの仕分けを完結させる手段にはなりにくい、というのが実態です。対策の全体像は問い合わせフォームの営業メール・スパム対策 完全ガイドにまとめています。
8. 内容を読む判定をAIに任せる(自作のボット対策と併用前提)
第2層を埋める手段として、DefformはAIが問い合わせの内容を読んで仕分けるアプローチを取っています。判定に使うのは送信時の挙動ではなく、本文・件名・送信者情報・文章のパターンです。
- 誰にでも当てはまる定型的な提案文か、自社に固有の用件か
- 差出人の情報と問い合わせ内容に整合性があるか
- 営業メールに頻出するパターンと一致するか
これらを総合して、営業メール / スパム / 本物の問い合わせに振り分けます。判定の軸がハニーポットやreCAPTCHAと重ならないため、ボット対策は残したまま併用するのが前提です。
AIの判定が常に自社の感覚と一致するとは限りません。そのため、判定結果はすべてダッシュボードから確認できるようになっており、営業メールに振り分けられた回答も内容を確認できます。導入直後はダッシュボードで振り分けを見ながら運用するのが現実的です。
HTMLで作ったサイトへの導入は、次のような形になります。
- Defformに無料サインアップする
- ダッシュボードでテンプレートからフォームを作成する
- ビルドしたファイルをサーバーにアップロードし、HTMLにフォーム表示用のタグを設置する
- テスト送信をして、ダッシュボードと通知メールで届き方を確認する
フォームの送信処理・通知メールはDefform側が担うため、3章〜5章のPHPの送信処理を自分で保守する必要がなくなります。設置の具体的な手順はDefformで正当なお問い合わせのみコンバージョンを取得する方法で、埋め込みタグの例とあわせて紹介しています。WordPressで作ったサイトなら、Contact Form 7などの既存フォームを残したままWordPressプラグインで後付けすることもできます。
料金は、FREEがAI判定30通/月(うちフリーメール送信者10通)まで永年無料、PROが月額3,000円でAI判定300通/月、BUSINESSが月額15,000円です(→料金プラン)。FREEプランでも3フォームまで作成できます。月間の判定枠を超えた回答も保存はされますが、AI判定と通知は行われないため、問い合わせ件数に合わせてプランを選んでください。まずはFREEで1〜2ヶ月運用し、届く問い合わせの内訳を見てから判断するのが現実的です。
AIによる仕分けの考え方は問い合わせの自動仕分けをAIで実現する方法で詳しく解説しています。
よくある質問(FAQ)
Q. HTMLだけで問い合わせフォームを作ることはできますか?
A. 入力画面はHTMLだけで作れますが、送信された内容を受け取ってメールにする処理は作れません。PHPなどのサーバー側スクリプトを用意するか、フォーム送信を受け付ける外部サービスやフォーム作成サービスを使う必要があります。mailto: で送る方法は、送信者のメールソフトに依存するため問い合わせ窓口には不向きです。
Q. PHPのフォームから送ったメールが届きません。何を確認すればよいですか?
A. まず From のアドレスが自社ドメインのものになっているかを確認してください。送信者が入力したアドレスを From に入れると、受信側で破棄されたり迷惑メールに振り分けられたりしやすくなります。あわせて、迷惑メールフォルダ、SPFなどの送信ドメイン認証の設定、レンタルサーバー側の送信制限も確認すると原因を絞り込めます。
Q. 自作フォームのスパム対策は何から入れればよいですか? A. ユーザーの手間が増えないハニーポットと時間差トラップから始め、それでも自動送信が多い場合にreCAPTCHA v3やCloudflare Turnstileを追加する順番が現実的です。どの手段も、サーバー側で値を検証する処理まで実装しないと効果が出ない点に注意してください。
Q. reCAPTCHAを入れても営業メールが減らないのはなぜですか? A. reCAPTCHAが判定しているのは「人間による送信かどうか」で、内容が営業かどうかは判定の対象外だからです。フォーム営業は人間または人間と同等の操作で送られるため、通過します。営業メールを仕分けるには、内容を読む別の層を併用する必要があります。
Q. HTMLで作ったサイトでもDefformを使えますか? A. 使えます。Defformで作成したフォームを、タグを設置してHTMLのページに埋め込む方法があります。判定結果はダッシュボードで確認でき、AI判定は30通/月まで永年無料です。
まとめ
- HTMLが担当するのは入力画面まで。送信内容を受け取ってメールにする処理は、PHPなどのサーバー側か外部サービスが必要
mailto:で送る方法は、送信者のメールソフトに依存するため問い合わせ窓口には不向きrequiredやtype="email"は補助。同じ入力チェックをサーバー側でも必ず行うFromは自社ドメインの固定アドレスにし、送信者のアドレスはReply-Toに入れる- 自作フォームでは、ヘッダーインジェクション・XSS・CSRF・二重送信の対策を自分で担う
- ボット対策はハニーポット・時間差トラップから始め、必要に応じてreCAPTCHA v3やTurnstileを足す
- ボット対策では人間が送る営業メールは止まらない。内容を読む判定を併用し、結果はダッシュボードで確認しながら運用する