Contact Form 7で運用しているお問い合わせフォームから、こんなメールが届くようになりました。
お名前:[your-name]
メールアドレス:[your-email]
ご相談内容:[detail]
入力値が入るはずの場所に、メールタグがそのまま出力された状態です。しかも毎回ではなく、忘れた頃に1件届く。テスト送信すると普通に成功してしまうので、再現条件が掴めない。
結論から書くと、原因は確認画面プラグイン「Contact Form 7 Multi-Step Forms」がステップ間のデータ受け渡しに使っているCookieの容量オーバーでした。この記事では、症状の読み解き方から、実際に投入した対策コードまでをまとめます。
目次
まずここを混同しないことが出発点です。よくある「空メールが届く」問題とは、発生メカニズムが違います。
Contact Form 7がメールタグを置換するとき、内部では送信データから該当するキーを探しにいきます。このときの挙動が2通りに分かれます。
[your-name] という文字列をそのまま出力するつまり、タグが生のまま届いているメールは「お客様が未入力のまま送信した」のではありません。その項目がサーバー側に丸ごと届いていない状態です。
この区別が付くと、疑う場所が一気に絞られます。メールを作る前の段階で壊れているので、メールサーバーもSMTPプラグインも無関係です。SMTPは正常に仕事をした結果、壊れた本文をそのまま配送しただけ、ということになります。
今回のフォームは、確認画面を出すために「Contact Form 7 Multi-Step Forms」を使っていました。このプラグインは、入力画面から確認画面へ値を持ち回るためにCookieを使います。
Cookieには1つあたり約4KBという容量上限があります。入力データがこれを超えると、Cookieが生成されません。その結果こうなります。
ポイントは、長文を入れた項目だけでなく、全項目が道連れになることです。Cookieそのものが作られていないので、名前もメールアドレスも一緒に消えます。
日本語は1文字あたり3バイト前後です。単純計算で1300文字強で4KBに達します。実際にはURLエンコードや他の項目のデータも乗るので、体感ではもっと早く上限に届きます。
通常のお問い合わせは数十文字から200文字程度で収まるため、まず発生しません。今回の発生源は営業メールでした。定型の営業文面を「お見積り・ご相談内容」欄に丸ごと貼り付けてくるため、簡単に上限を超えます。住所欄に長文が入っているケースもありました。
開発者が短い文字列でテストしている限り、絶対に再現しません。これが「テストすると成功するのに、たまに壊れたメールが届く」の正体です。
[your-email] も空になっているため、自動返信メール(メール2)の送信先が解決できません。管理者宛のメールだけが壊れた状態で届き、送信者には何も届かない。送信者側からは「送ったのに返事がない」ように見えます。
意図的に壊す手順です。対策の効果を確認するときにも使えます。
Cookieの上限に達しないよう、そもそも長文を入力させない方向で対処します。今回は住所を100文字、相談内容を400文字に制限しました。
先に注意点です。Contact Form 7のフォームタグには maxlength オプションがありますが、これに頼ってはいけません。
[textarea* detail maxlength:400]
この指定はHTMLの maxlength 属性として出力されるため、ブラウザ側で入力が打ち切られます。500文字を貼り付けても400文字までしか入らず、PHPには常に400文字以内のデータしか届きません。エラーも出ません。
ユーザーから見ると、貼り付けた文章の後半が理由も分からないまま消えている状態です。体験としてよくありませんし、Botや自動送信ツールにはそもそも効きません。
そこで、フォームタグから maxlength を外します。
[textarea your-address placeholder "愛知県名古屋市中区栄1-1-1"]
[textarea* detail placeholder "ご自由にどうぞ"]
そのうえで、PHP側でバリデーションしてエラーメッセージを出します。
/**
* CF7:住所欄の文字数制限
*/
add_filter('wpcf7_validate_textarea', 'validate_cf7_address_length', 20, 2);
add_filter('wpcf7_validate_textarea*', 'validate_cf7_address_length', 20, 2);
function validate_cf7_address_length($result, $tag)
{
if ($tag->name !== 'your-address') {
return $result;
}
$value = isset($_POST['your-address'])
? wp_unslash((string) $_POST['your-address'])
: '';
$value = preg_replace("/\r\n|\r/", "\n", $value);
$value = trim($value);
$length = mb_strlen($value, 'UTF-8');
if ($length > 100) {
$result->invalidate(
$tag,
sprintf(
'ご住所は100文字以内で入力してください。現在%s文字です。',
number_format($length)
)
);
}
return $result;
}
/**
* CF7:相談内容の文字数制限
*/
add_filter('wpcf7_validate_textarea', 'validate_cf7_detail_length', 20, 2);
add_filter('wpcf7_validate_textarea*', 'validate_cf7_detail_length', 20, 2);
function validate_cf7_detail_length($result, $tag)
{
if ($tag->name !== 'detail') {
return $result;
}
$value = isset($_POST['detail'])
? wp_unslash((string) $_POST['detail'])
: '';
// Windows・Mac系の改行を「1文字の改行」に統一
$value = preg_replace("/\r\n|\r/", "\n", $value);
$value = trim($value);
$length = mb_strlen($value, 'UTF-8');
if ($length > 400) {
$result->invalidate(
$tag,
sprintf(
'お見積り・ご相談内容は400文字以内で入力してください。現在%s文字です。',
number_format($length)
)
);
}
return $result;
}
これで450文字入力すると、「お見積り・ご相談内容は400文字以内で入力してください。現在450文字です。」と表示されます。何文字オーバーしているかが分かるので、ユーザーは削る量を判断できます。
ハマりやすいポイントです。バリデーション用のフィルターフックは、フォームタグの種類ごとに名前が違います。
| フォームタグ | 使うフック |
|---|---|
[textarea your-address] |
wpcf7_validate_textarea |
[textarea* your-address] |
wpcf7_validate_textarea* |
[text your-address] |
wpcf7_validate_text |
[text* your-address] |
wpcf7_validate_text* |
住所欄を textarea で作っているのに wpcf7_validate_text にフックしていると、関数は一度も呼ばれずエラーも出ません。「コードを追加したのに何も起きない」ときは、まずここを疑ってください。必須マーク付き(*)の有無でもフック名が変わるので、両方に登録しておくのが安全です。
文字数制限だけでは不十分です。Cookieの4KBは全項目の合計なので、今後フォームに項目が追加されれば、また上限に触れる可能性があります。
そこで、確認画面フォームから送信されたデータに必須項目が入っていなければ、メール送信そのものを中止する保険をかけます。
/**
* CF7確認画面:
* 引き継ぎデータが空の場合はメール送信を中止する
*/
add_action(
'wpcf7_before_send_mail',
function ($contact_form, &$abort) {
if (
$contact_form->title()
!== '無料見積り・お問い合わせフォーム(確認)'
) {
return;
}
$submission = WPCF7_Submission::get_instance();
if (!$submission) {
$abort = true;
return;
}
$data = $submission->get_posted_data();
$required_fields = [
'whatabout',
'your-name',
'your-email',
];
foreach ($required_fields as $field) {
$value = $data[$field] ?? '';
if (is_array($value)) {
$value = implode('', $value);
}
if (trim((string) $value) === '') {
$abort = true;
return;
}
}
},
10,
2
);
ポイントは3つです。
フォームタイトルで対象を限定している サイト内に複数のフォームがある場合、全部に適用されると事故のもとです。確認画面用フォームのタイトルと一致したときだけ処理します。ただしこれは文字列の完全一致なので、フォーム名を変更すると無効になります。フォーム名を触るときは、このコードもセットで見直してください。
チェックボックスなどの配列に対応している whatabout のような複数選択項目は配列で届くため、implode() で文字列化してから判定しています。
Flamingoには記録が残る $abort = true でメール送信は止まりますが、Flamingoへの保存は行われます。万が一、正規のお問い合わせが巻き込まれても、管理画面から内容を拾い上げられます。
原因特定までの過程で効いたものを2つだけ紹介します。同種のトラブルに当たったときに使えます。
メールタブの本文末尾に、これを追加しておきます。
------------------------------
IP:[_remote_ip]
UA:[_user_agent]
URL:[_url]
日時:[_date] [_time]
管理番号:[_serial_number]
これらはフォーム項目とは別系統の「特別なメールタグ」で、送信のコンテキストから値を取ります。壊れたメールが届いたとき、フォーム項目だけが生でこちらは置換されているなら、送信自体は正常なフローを通っていて、データだけが欠けていると判断できます。逆にこちらも生のままなら、送信オブジェクト自体が取得できていない、より深い問題です。
副次的に、IPとUAから「同一IPから連続している」「UAがブラウザらしくない」といったBotの兆候も掴めます。
Flamingoを入れていないなら、この手の調査では真っ先に入れてください。壊れたメールに対応するレコードが存在するか、項目が空か、値が入っているかで、疑う場所が変わります。[_serial_number] を仕込んでおけば、メールとレコードを1対1で確実に突き合わせられます。
念のため書いておきます。この症状で以下を疑うのは時間の無駄です。
メールは届いています。届いた内容が壊れているだけです。壊れたのはメールを作る前の段階なので、配送経路をいくら調べても何も出てきません。
今回は文字数制限と送信中止の二段構えで運用に乗せましたが、これはCookieの4KBに収まるように入力を絞り込んでいるだけで、構造的な弱点そのものは残っています。
Multi-Step Formsには、他にも報告されている挙動があります。同じフォームを別タブで2つ開いて確認画面に進み、それぞれで「戻る」を押すとCookieが競合して値が混ざる、といったケースです。ページ遷移とCookieに依存する設計である以上、この手の再現しづらい不具合は付いて回ります。
より確実なのは、1ページ完結のフォームにして、確認画面をJavaScriptによる表示切り替えで実現する方法です。ページ遷移が発生しないためデータの持ち回り自体が不要になり、容量制限からも解放されます。ファイル添付がある場合も、FileListを保持したまま扱えます。
既存案件で急に作り替えるのは現実的でないことも多いので、まずは今回の対策で止血し、リニューアルのタイミングで構成を見直す、という進め方が落としどころだと思います。
maxlength はブラウザ側で切られるため、対策として機能しない「たまに変なメールが届くだけ」と放置されがちな症状ですが、同時に自動返信も止まっているので、お客様側では「問い合わせたのに無視された」ことになっています。心当たりのある方は、一度確認してみてください。