Google App Engine でメイルの受信

Google App Engine 最大の死角

ここ数ヶ月で急速に整備の進んだGoogle App Engineだが,できて当然なのにまだできていないことがある.メイルの受信だ.メイルの送信はできるし,XMPPの受信だってできるのに,なぜか普通のメイルの受信ができない.GoogleにはGmailという巨大メイルハンドラがあるわけだし,そのうちできるようになるんだろうけど,この話は,それまでのつなぎということで.

smtp2web

smtp2webという無料サービスがある.要するにメイルを受け取ると,それを指定したwebpageへのPOSTアクセスに変換してくれる,というもの.smtp2webのページにも書かれているとおり,App Engine には好適.ソースコードGoogle codeで公開されているので,その気になれば自分で同じサービスを立ち上げることも可能,とのこと.

さて,使い方をまとめてみよう.基本的には,XXXX@smtp2web.com というアドレスを作らせてくれて,そのアドレスを指定したURLに転送する.どうやら,独自ドメインの転送もできるようだが,それには,そのドメインDNSのMXをいじる,とかやるんだろう.ここでは単純に,XXXX@smtp2web.com を転送するケースだけ試している.

まず,トップページに行く.シンプル.

サインインすると,Googleの認証ページに飛ばされる.OAuthを使ってるのだろう.Gmailのアカウントでログインする.

転送アドレスのドメイン名を聞かれる.ここはデフォルトのsmtp2web.comにしておく.

すると,メイル転送を設定するユーザ名,と転送先URLの指定をうながされる.転送先URLを,appengine上の適当なサーブレットにする.ここでは,http://XXXX.appspot.com/mailHandlerというURLにしている.

ここでもう一段認証が入る.そのURLを使う権利がユーザにあるか?を証明しなければならない.認証は,ユーザが指定したURLの付近に,smtp2webが指定した名前でファイルを作ることで行う.App Engine Java版では,warディレクトリにファイルを置けば,そのまま見えるので簡単.Python版だとスタティックファイルは明示的に指定しなければならないので,ちょっと面倒.

指定されたURLにうまくアクセスできると設定完了.アクセスに失敗するとやり直し.

ハンドラサーブレット

これであとは,ハンドルするサーブレットを書いてやればよい.メッセージは,MIME形式でサーブレットのストリームとして与えられるので,これをパーズしてやればよい.やればよいのだが,MIMEが複雑なのと,JavaMailがわけわからないのと,日本語のハンドルが元々面倒という3つ面倒の合わせ技でかなり面倒.

とりあえず,ISO 2022 でplain textのメイルならこんな感じでパーズしてやればよい,はず.JavaMail難しい.

import java.io.*;
import java.util.Properties;
import javax.servlet.http.*;
import javax.mail.*;
import javax.mail.internet.MimeMessage;

@SuppressWarnings("serial")
public class MailHandlerServlet extends HttpServlet {
  public void doPost(HttpServletRequest req, 
      HttpServletResponse res)
     throws IOException {
    System.err.println("from:" + req.getParameter("from"));
    System.err.println("to:  " + req.getParameter("to"));

    Properties prop = System.getProperties();
    Session session = Session.getInstance(prop);
    MimeMessage msg;
    try {
      msg = new MimeMessage(session, req.getInputStream());
      System.err.println("subject: " + msg.getSubject());
      System.err.println("body: ---->" ); 
      System.err.print  (getText(msg)); 
      System.err.println("body: <----" ); 
    } catch (MessagingException e) {
      e.printStackTrace();
      throw new IOException("Message Excetion: " + e.getMessage());      
    }
  }
  private String getText(MimeMessage msg) 
  throws IOException, MessagingException{
    InputStream is = (InputStream)msg.getContent();
    StringBuffer sb = new StringBuffer();
    LineNumberReader lnr = 
      new LineNumberReader(new InputStreamReader(is, "ISO2022JP"));
    String s = null;
    while ((s = lnr.readLine()) != null)
      sb.append(s + "\n");
    return sb.toString();
  }
}

最後に,web.xmlマッピングを書いてやる.こんなかんじ.

  <servlet>
    <servlet-name>mailhandler</servlet-name>
    <servlet-class>MailHandlerServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>mailhandler</servlet-name>
    <url-pattern>/mailHandler</url-pattern>
  </servlet-mapping>
テスト

適当なメイラからXXXX@smtp2webにメイルを送ってみる.しばらくするとApp Engineのログにメッセージの情報が記録されるはず.多分日本語は化けるがこれはメイルのパーズに失敗しているからではなく,ログを出すときにコード変換していないから,だと思う.

また,smtp2webの方にもログが出る.これはデバッグに重要.また,smtp2webはサーブレットがエラーを返すとちゃんとメイルの不達として扱ってくれるので,便利.

所感

smtp2webは非常に完成度が高い.プログラミングパターンとしても,App Engineがまさに実装しそうな方法で実装できる.App Engineがメイル受信を実現した暁には,smtp2web向けののハンドラをほとんど修正せずに流用できるのではないだろうか.

smtp2webでは多分できないのは,動的なアドレスに対するマッピングの追加か?たとえばブログサービスにメイルで投稿できるようにするには,ユーザごとにアドレスが必要なので,この枠組みだときついかも.でも,実はsmtp2webにAPIが用意されていて,外からマッピングの追加ができたりして...