AJAX+Comet+空メールでメールアドレスを入力

Webフォームでメールアドレス登録する際に、誤入力防止の為のメールアドレス確認欄が必要かどうかという議論があるけど、それなら手入力しなければ良いよね、と思う。

PCブラウザの場合はAJAXが使えるので、「空メールを送信したその場でフォームに反映」とかも出来そう。
ということで作ってみた。

デモ

http://terazzo.dyndns.org/comet_maddr/

  • サーバが落ちていたり電話代を滞納している時は見れません。
  • 家サーバな上、ISDN回線(64kbps)なため、あまりCometぽい応答速度ではないかも。
  • できれば捨てアカでやって下さい

方針

  • CometサーバとしてMeteorを使用
    • Meteor付属のJavaScriptコードだとコールバックでチャンネル名が取れないのでそこだけ修正する
  • 入力フォーム作成時に文字列(ID)をランダム生成し、メールアドレスとMeteorのチャンネル名に使用
    • メールアドレスは、qmailの拡張アドレス(hoge-XXXX@example.comなどのワイルドカード機能)を使用し、ハイフン以降の部分をチャンネル名にする
    • フォーム上では、mailto:でこのメールアドレスを表示し、同時にMeteorのjoinChannel()のチャンネル名および入力フィールドのIDにも指定
  • 言語はperl

環境

サーバの準備

Meteorをインストールして起動(参考)

mkdir /usr/local/meteor
cd /usr/local/meteor
wget http://meteorserver.org/download/latest.tgz
tar zxvf latest.tgz
# 設定ファイルをコピー
cp ./meteord.conf.dist /etc/meteord.conf
# PingIntervalを変更
vi /etc/meteord.conf
-----------------------------------------------------
PingInterval 60
-----------------------------------------------------
# 起動
chmod a+x meteord
./meteord

apache->meteorの転送設定
(以下をconf/httpd.confに追記)

ProxyPass /push http://localhost:4670/push

meteor/public_html以下のファイルをApacheのhtdocs下にコピー

mkdir /usr/local/apache2/htdocs/comet_maddr/
cp /usr/local/meteor/public_html/* /usr/local/apache2/htdocs/comet_maddr/
cd /usr/local/apache2/htdocs/comet_maddr/

meteor.jsをカスタマイズ(callbackでチャンネル名を取得出来るように変更。およびURLの変更)

$ diff /usr/local/meteor/public_html/meteor.js  /usr/local/apache2/htdocs/comet_maddr/meteor.js 
78c78
<                               Meteor.loadFrame("http://"+Meteor.host+((Meteor.port==80)?"":":"+Meteor.port)+"/stream.html");
---
>                               Meteor.loadFrame("http://"+Meteor.host+((Meteor.port==80)?"":":"+Meteor.port)+"/comet_maddr/stream.html");
84c84
<                       Meteor.loadFrame("http://"+Meteor.host+((Meteor.port==80)?"":":"+Meteor.port)+"/poll.html");
---
>                       Meteor.loadFrame("http://"+Meteor.host+((Meteor.port==80)?"":":"+Meteor.port)+"/comet_maddr/poll.html");
180c180
<                       Meteor.callbacks["process"](data);
---
>                       Meteor.callbacks["process"](data, channel);
244,245c244,245
<                       return function(args) {
<                               f(a);g(args);
---
>                       return function() {
>                               f(a);g.apply(g, arguments);

String::Randomの配置

mkdir -p /usr/local/apache2/htdocs/comet_maddr/lib/String/
wget -O /usr/local/apache2/htdocs/comet_maddr/lib/String/Random.pm http://search.cpan.org/src/STEVE/String-Random-0.22/lib/String/Random.pm

メールアカウントの作成、転送設定、

# アカウント作成およびID確認
groupadd maddr
useradd -g maddr -d /home/maddr maddr
id maddr
# users/assign設定(618:614の部分はuidとgidを指定)
vi /var/qmail/users/assign
---------------------------------------------------
=maddr:maddr:618:614:/home/maddr:::
+maddr-:maddr:618:614:/home/maddr:-::
.
---------------------------------------------------
/var/qmail/bin/qmail-newu
# .qmail-defaultの作成
echo '|./push_maddr.pl' > ~maddr/.qmail-default
chown maddr ~maddr/.qmail-default
chmod og-wx ~maddr/.qmail-default


メールを受けた際にmeteordに通知するスクリプト作成
/home/maddr/push_maddr.pl: (モードを実行可にすること)

#!/usr/bin/perl
use Socket;

my $port = 4671;
my $host = "127.0.0.1";

my $sender = $ENV{SENDER};
my $channelname = $ENV{EXT};


my $ipaddr = inet_aton($host);
my $sockaddr = pack_sockaddr_in($port,$ipaddr);

socket(SOCKET, PF_INET, SOCK_STREAM, 0) || die "socket error.\n";
connect(SOCKET,$sockaddr) || die "connect $host $port error.\n";
select((select(SOCKET), $|=1)[0]);

printf SOCKET "ADDMESSAGE %s %s\n", $channelname, $sender;
print SOCKET "QUIT\n";
shutdown(SOCKET, 1);
while(my $line = <SOCKET>) {
    # print $line;
}
close(SOCKET);

サンプルフォーム生成CGIの作成

/usr/local/apache2/htdocs/comet_maddr/index.cgi:

#!/usr/bin/perl

use lib lib;
use String::Random;
my $rnd = new String::Random;
my $hostname = "terazzo.dyndns.org";
my $post_madd_prefix = "maddr-";
my $post_madd_suffix = "\@$hostname";

my $clientId = $rnd->randregex("[0-9]{10}");
my $channelname1 = $rnd->randregex("[A-Za-z0-9]{12}");
my $channelname2 = $rnd->randregex("[A-Za-z0-9]{12}");
my $post_madd1 = $post_madd_prefix.$channelname1.$post_madd_suffix;
my $post_madd2 = $post_madd_prefix.$channelname2.$post_madd_suffix;

print <<EOF
Content-type:text/html; charset=Shift_JIS

<html>
<head>
<title>Mail Address Test</title>

<script type="text/javascript" src="http://$hostname/comet_maddr/meteor.js"></script>
<script type="text/javascript">
function start_connection() {
  Meteor.hostid = "$clientId";
  Meteor.host = "$hostname";
  Meteor.registerEventCallback("process", 
     function (address, channelname) {
          document.getElementById(channelname).value=address;
     }
  );
  Meteor.joinChannel("$channelname1", 5);
  Meteor.joinChannel("$channelname2", 5);
  Meteor.mode = 'longpoll';
  Meteor.connect();
}
</script>
</head>
<body onload="start_connection()">
   <form>
       Mail Address 1: <input type="text" size="32" id="$channelname1"> <a href="mailto:$post_madd1">Set...</a><br>
       Mail Address 2: <input type="text" size="32" id="$channelname2"> <a href="mailto:$post_madd2">Set...</a><br>
   </form>
</body>
</html>
EOF

出力されたHTML(例:)

<html>
<head>
<title>Mail Address Test</title>

<script type="text/javascript" src="http://terazzo.dyndns.org/comet_maddr/meteor.js"></script>
<script type="text/javascript">
function start_connection() {
Meteor.hostid = "7396487096";
Meteor.host = "terazzo.dyndns.org";
Meteor.registerEventCallback("process", 
   function (address, channelname) {
        document.getElementById(channelname).value=address;
   }
);
Meteor.joinChannel("RlfXx2vlWun3", 5);
Meteor.joinChannel("8YA1QKTETbiq", 5);
Meteor.mode = 'longpoll';
Meteor.connect();
}
</script>
</head>
<body onload="start_connection()">
   <form>
       Mail Address 1: <input type="text" size="32" id="RlfXx2vlWun3"> <a href="mailto:maddr-RlfXx2vlWun3@terazzo.dyndns.org">Set...</a><br>
       Mail Address 2: <input type="text" size="32" id="8YA1QKTETbiq"> <a href="mailto:maddr-8YA1QKTETbiq@terazzo.dyndns.org">Set...</a><br>
   </form>


</body>
</html>

考察その他

  • 通常の空メールの場合、入力が容易という事の他に到達が保証出来るというメリットもあるけど、今回の方法ではヘッダ偽装が可能なので、到達保証の為には再度確認メール等が必要
  • 今回は入力フィールドへの自動入力だけど、静的文字列+hiddenの方が良いかも
  • mailtoを使っているので、普通にサブメールアドレスを入れることが出来ない(デモは間抜け)
    • 携帯メールアドレス用にはQRコードとか出せばいいかも(追記: デモに入れてみた)

さらに追記:

  • 厳密に言えばMeteorの通信部分はAJAXじゃなくてscriptタグ追記方式(JSONPなどのような)かな?
  • String::Randomの部分セキュリティ的に少し不安
    • 文字列が予測可能ならjoinChannel()することで、他の人が送ったメールアドレスを受け取れる。