「http」タグアーカイブ

【JavaScript】Ajaxを使ったメールフォーム

クライアントサイドとサーバーサイドを連携するうえでの基本的な流れを知っておきたかったので、Ajaxを使ってメールフォームを作ってみた。使った言語はJavaScriptとPHP。以下はコードの解説と気をつけたい点について。完成したものはこちら【1】

クライアントサイド

まず、クライアントサイドの流れとしては以下のような感じ。

  1. ユーザーが送信ボタンを押した時にXMLHttpRequestでフォームデータをサーバーに送信。
  2. メールが送信されたかされていないかサーバーが返してきた結果を元にユーザーに通知。
  3. 戻るボタンで再びフォーム画面を表示。

実際にコードを見ながら細かな流れを書いていく。

(function() {

	var server = ''; //送信先サーバーのURLを設定する;

	var xhr = new XMLHttpRequest();
	var form = document.getElementById('mail-form');
	var formHTML = form.innerHTML;
	var tempForm = {};

	form.onsubmit =  function(e){
		e.preventDefault();
		//多重送信を防止;
		var submit = form.querySelector('input[type="submit"]');
		submit.disabled = false;

		//formの内容を保存;
		(function() {
			var elements = form.elements;
			for(var i = 0; i < elements.length; i++) {
				tempForm[elements[i].name] = elements[i].value;
			}
		})();

		//サーバーにFormDataを送信;
		(function() {
			//FormDataを取得し、送信元サーバーを追加;
			var formData = new FormData(form);
			formData.append('from', location.host);

			xhr.open('POST', server);
			//カスタムヘッダーをつける;
			xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
			//結果はjsonだが、IEが対応していないのでtextで受け取る;
			xhr.responseType = 'text';
			xhr.timeout = '10000';
			xhr.send(formData);
		})();
	};

	xhr.addEventListener('loadend', function() {	
		//formのchildNodesを全て消す;
		(function removeAllChild(element) {
			var child;
			while(child = element.firstChild) {
				element.removeChild(child);
			}
		})(form);

		//送信後に表示する要素;
		var resultElements = (function createResultElements() {
			var elements = {};
			elements.df = document.createDocumentFragment();
			elements.p = document.createElement('p');
			elements.back = document.createElement('input');
			elements.back.type = 'button';
			elements.back.value = '戻る';
			elements.df.appendChild(elements.p);
			elements.df.appendChild(elements.back);

			return elements;
		})();
		form.appendChild(resultElements.df);

		if(xhr.status === 200) {
			var response = JSON.parse(xhr.response);
			if(response.result) {
				//メールが送信された時の処理;
				resultElements.p.textContent = '送信に成功しました。';
				resultElements.back.onclick = function() {
					form.innerHTML = formHTML;
				};
				return;
			}
		}
		//メールが送信されなかった時の処理;
		resultElements.p.textContent = '送信に失敗しました。再度送信してください。';
		resultElements.back.onclick = function() {
			form.innerHTML = formHTML;
			var elements = form.elements;
			for(var i = 0; i < elements.length; i++) {
				elements[i].value = tempForm[elements[i].name];
			}
		};
	}, false);
})();

まず、ユーザーがフォームを送信したらpreventDefaultで通常の動作を止めて、多重送信を防止するために送信ボタンのdisabled属性をfalseに設定する。次にユーザーが書いたフォームの内容を保存する。これは、メール送信に失敗した後、再びフォーム画面に戻った時に復元するためである。そして次に、フォームデータをXMLHttpRequestで送る。フォームの内容はHTML5からFormDataオブジェクトでそのまま渡せるようになった。FormDataコンストラクタの引数にform要素を入れてnewすればいい。ここでは一応、どのサイトからフォーム送信されたのかわかるように自分のサーバーのホスト名もフォームデータに追加して送る。

最後にXMLHttpRequestオブジェクトの設定をする。まず、リクエストはPOSTで、送り先はsend_mail.phpファイルがあるサーバーである。最初のserver変数にurlをいれる。そして、ヘッダーをセットする。

xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest')

というのは、サーバーにXMLHttpRequestを使って通信をしているということを示すカスタムヘッダーである。カスタムなので自由に設定できるが、他のライブラリはこのカスタムヘッダーを使用しているので同じようにする。そして、送る情報はフォームデータだが、FormDataオブジェクトを使えば、Content-type: multipart/form-dataというヘッダーを付加しなくていい。FormDataオブジェクト万歳!!。次に、responseTypeで返ってきたデータをどの形式で受け取るか指定する。今回は勉強がてらjsonで結果を返してもらうことにした。IEはresponseTypeにjsonを指定できないようなので、結果はtextで受け取ってJSON.parseでjsonに変換することにした。準備ができたのでsendメソッドの引数にフォームデータをいれて送信する。

そして、loadendイベントで結果を処理する。XMLHttpRequestのloadendイベントは結果がエラーであっても、タイムアウトであっても発火する。ここでは、xhr.statusが200で返ってきたresponseのresultプロパティの結果がtrueであれば、メールが送信されていて、それ以外はメール送信失敗したということがわかる。このresultプロパティはサーバー側で設定する。メールが送信されたかどうかの旨を表示して、戻るボタンで元のフォーム画面を再び表示する。もしメールの送信が失敗したのであれば戻るボタンでフォーム画面を再び表示するとともに、先ほど保存したフォームの内容を復元する。ここらへんの処理は実際に使ってみるとわかりやすい。

サーバーサイド

次にサーバー側の処理を見てみる。短いコードだが、あまりサーバーサイド言語を書いたことがなかったので色々と学べることが多かった。再びコードを見ながら細かく流れを書いていく。

<?php

$mail_address = ''; //送信先メールアドレスを設定する;

//通信を許可する設定;
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: X-Requested-With');
//結果はjsonで返す;
header('Content-Type: application/json; charset=UTF-8');
header('X-Content-Type-Options: nosniff');

mb_language('japanese');
mb_internal_encoding('UTF-8');

//XMLHttpRequest以外からのアクセスの処理;
if(!isset($_SERVER['HTTP_X_REQUESTED_WITH']) || $_SERVER['HTTP_X_REQUESTED_WITH'] !== 'XMLHttpRequest') {
	die(json_encode(array('result' => false), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP));
}

$name = trim(rm_indention($_POST['name']));
$email = trim(rm_indention($_POST['email']));
$from = trim(rm_indention($_POST['from']));
$message = trim($_POST['message']);

//nameとmessageがあれば。メール送信;
if($name !== '' && $message !== '') {
	$to = $mail_address;
	$subject = rm_indention(trim($_POST['subject']));
	$body = 'Referer: ' . $_SERVER['HTTP_REFERER'] . "\n";
	$body .= 'Name: ' . $name . "\n";
	$body .= 'Email: ' . $email . "\n";
	$body .= "\n" . $message;
	$header = 'From: ' . 'mail@' . $from . "\n";
	if($email !== '') {
		$header .= 'Reply-To: ' . $email . "\n";
	}
	$header .= 'MIME-Version: 1.0' . "\n";
	$parameters = "-f" . $mail_address;

	//メール送信;
	$success = mb_send_mail($to, $subject, $body, $header, $parameters);
}else {
	$success = false;
}
$response = array('result' => $success);
echo json_encode($response, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);

//改行を消す(メールヘッダ・インジェクション対策);
function rm_indention($str) {
	if(isset($str)) {
		str_replace(array("\r\n", "\r", "\n"), '', $str);
	}
	return $str;
}

まず始めにヘッダーについて。Ajaxを使ってクロスオリジン通信する時はHTTPヘッダーについて多少知らなければならない。Ajaxを使う上で知っておかなければならないHTTPヘッダーについてはMDNのHTTP access control (CORS)にわかりやすく書いてある。ここでは簡単に、使っているヘッダーについて説明する。

header('Access-Control-Allow-Origin: *');

このヘッダーは、どのOriginからのアクセスを許可するかというものだ。HTTPのOriginというのは、「プロトコル + ドメイン + ポート番号」のことで、例えばこのサイトだと単に「http://webkatu.com」というのがOriginに当たる。ここで使用している「*」はどのOriginからのアクセスも許可するという設定で、どこからでもAjaxを使ってアクセスできるようになる。セキュリティなどを考慮するならメールフォームを使うサイトのOriginを指定しておけばいい。スペースで区切ることによって複数のOriginを指定できる。

header('Access-Control-Allow-Methods: POST, OPTIONS');

このヘッダーは、どのリクエストメソッドからのアクセスを許可するかというものだ。ここでは、POSTとOPTIONSのアクセスメソッドを許可している。OPTIONSというのはプリフライトリクエストといって、実際にリクエストを送ってもいいかを確かめる時に使われるメソッドで、今回はあってもなくてもどちらでもいいが、基本的にPOSTと一緒に指定する。Access-Control-Allow-Methodsはカンマで区切ることによって複数指定できる。

header('Access-Control-Allow-Headers: X-Requested-With');

このヘッダーはクライアントサイドのカスタムヘッダーを許可するもので、ここではクライアントサイドで書いたxhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest')のヘッダーを許可している。このカスタムヘッダーにより、XMLHttpRequstを使ったアクセスか否かの処理を分けることができる。

header('Content-Type: application/json; charset=UTF-8');
header('X-Content-Type-Options: nosniff');

このヘッダーは結果をjsonで返すということを表している。Ajax通信に限らず結果をjsonで出力する時はこのヘッダーを使う。

if(!isset($_SERVER['HTTP_X_REQUESTED_WITH']) || $_SERVER['HTTP_X_REQUESTED_WITH'] !== 'XMLHttpRequest') {
	die(json_encode(array('result' => false), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP));
}

カスタムヘッダーを見てXMLHttpRequestを使ってアクセスされたのかどうかを確かめる。例えば、send_mail.phpに直接アクセスした場合やカスタムヘッダーを付けないでアクセスした場合はここで処理を終了させる。

前準備はこんな感じで次は実際にメールを送る処理について。メールはメールヘッダーを適切に設定して送信しなければいけない。メールヘッダーについてはメールヘッダ情報 - メールヘッダの意味・見方・調べ方にわかりやすく書いてある。

ここではメール送信するのにPHPのmb_send_mail関数を使う。mb_send_mailは第一引数に宛先、第二引数に件名、第三引数にメール本文、第四引数にその他のヘッダー、第五引数はメールデーモンにパラメーターを与えることができる。実際にコードはこんな感じ。

$name = trim(rm_indention($_POST['name']));
$email = trim(rm_indention($_POST['email']));
$from = trim(rm_indention($_POST['from']));
$message = trim($_POST['message']);

//nameとmessageがあれば。メール送信;
if($name !== '' && $message !== '') {
	$to = $mail_address;
	$subject = rm_indention(trim($_POST['subject']));
	$body = 'Referer: ' . $_SERVER['HTTP_REFERER'] . "\n";
	$body .= 'Name: ' . $name . "\n";
	$body .= 'Email: ' . $email . "\n";
	$body .= "\n" . $message;
	$header = 'From: ' . 'mail@' . $from . "\n";
	if($email !== '') {
		$header .= 'Reply-To: ' . $email . "\n";
	}
	$header .= 'MIME-Version: 1.0' . "\n";
	$parameters = "-f" . $mail_address;

	//メール送信;
	$success = mb_send_mail($to, $subject, $body, $header, $parameters);
}else {
	$success = false;
}
//改行を消す(メールヘッダ・インジェクション対策);
function rm_indention($str) {
	if(isset($str)) {
		str_replace(array("\r\n", "\r", "\n"), '', $str);
	}
	return $str;
}

まず、受信したフォームデータを処理する。rm_indentionという関数は自分で定義した関数で、フォームデータの不必要な改行コードを消す。メールヘッダーは改行を使って色々と設定できるため、改行コードを消すことによってフォーム送信するユーザーがCcやBccを使ってSPAMの踏み台にしてくる余地を潰す。そして、必須な情報が受信したフォームデータに入っていればメールを送る。$toにはこのプログラムを使う人が自分で送り先を決める。一番上の$mail_address変数にメールアドレスを入れればいい。$subjectにはユーザーが入力した件名を入れる。$bodyにはメール本文を入れる。ここでは、メールの冒頭にフォーム送信元がわかるリファラーと名前とメールアドレスを表示している。$headerには他に追加したいヘッダーを入れる。Fromヘッダーは送信者のメールアドレスを入れる。このヘッダーは必須で、適切にいれなければいけない。ここでは、クライアントサイドのドメインでメールを送っている。他にも色々とヘッダーを追加して送っているが、適切に書けるヘッダーはできるだけ書いておいた方がいいかもしれない。

$parameters = "-f" . $mail_address;

この$parametersに-fというパラメーターを使ってReturn-Pathというヘッダーを設定している。Return-Pathというのはメール送信に失敗した時、どのメールにエラーを通知するかというもので、ここでは$toと同じにしている。このヘッダーをつけることによって迷惑メールとして扱われる可能性が下がるようだ。本当は-fというパラメーターはFromヘッダーを設定するものだそうだ。通常であれば送信元にエラーメールを返すので送信元と同じである方が迷惑メールとして扱われる可能性は下がると思うが、ここでは宛先にエラーメールを返すように設定して、もし何かエラーが発生して宛先に送れなくてもその旨が宛先に伝わるようにしている。もし迷惑メールとして扱われるならFromと一緒のメールアドレスを指定した方がいいかもしれない。そして、この第五引数はPHPのメールデーモンの設定をするため、レンタルサーバーによっては動かないところもあるそうだ。とりあえず頭の片隅にいれておきたい。この第五引数のパラメーターについてはLinuxコマンドのsendmailを参照。

最後にメールが送られたかどうかの結果をjson形式で返す。mb_send_mail関数の戻り値は真偽値なので、そのまま変数に入れて結果を返せばいい。

$response = array('result' => $success);
echo json_encode($response, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);

あまり詳しく調べていないが、とりあえずjson形式で返す時はjson_encode関数の第二引数にこのように指定すれば安全にデータを送れるようだ。ajax json セキュリティとGoogleさんに聞くといいかもしれない。

作った感想

コード自体は短いが、HTTPヘッダーについてメールヘッダーについてjsonについてセキュリティについてなどなどネットワークが絡むので思った以上に学べることが多かった。ある程度サーバーサイドに慣れるときっと定石みたいなものが見えてくると思うのでこれを気にどんどん使っていきたい。そして、ネットワークが絡んだプログラムが動くと感動が大きい。特にAjaxを使ってサーバー側のプログラムを動かすのが凄い楽しい。そもそも最初からサーバー側でやれよという話だが、実はAjaxを使えば広告なし無料のレンタルサーバーであれこれできるのであった。Herokuやサーバーサイド言語やデーターベースが動く無料で広告なし独自ドメインが使えるレンタルーサーバーもあってそれだけでも充分ありがたいが、当然ながら色々と制限があって面倒くさい。しかし、静的サイトだけのサーバーであれば制限があまりないところが多い。そして、広告付きでサーバーサイドやデーターベースが動くサーバーも多い。この2つのサーバーとAjaxを使って広告なし無料独自ドメインの動的サイトを作れる!JavaScript万歳!節約万歳!・・・でも実際のところAjaxはブラウザ毎の対応などなど色々と面倒くさい。

さっそくGitHubに上げてみたので興味のある方は使ってみてください。