Google App Engine 上の JRuby でメールを受信する方法

» Posted by on 1月 28, 2010 in Blog | 0 comments

以下の組み合わせで動作を確認しています

  • jruby-1.4.0
  • rails 2.3.2
  • appengine-java-sdk-1.3.0.zip
  • appengine-apis-0.0.11.jar (0.0.12 でもメール受信部分の機能は入っていないっぽい)

GAE/J でのメールの受信の流れは以下のようになっています。

  1. (string)@(appid).appspotmail.com 宛へメールを送る
  2. 次のURLが呼び出される http://(appid).appspot.com/_ah/mail/(string)@(appid).appspotmail.com
  3. ↑ の URL に関連付けられた method が呼び出される
  4. TomCat(?) の HttpServletRequest req の req.getInputStream() から受信したメールを読み込む

※ (string)には好きな文字列、(appid)には GAE の appid が入ります。

Java でメールを読み込む部分のサンプルは以下のような感じになります。
Receiving Email – Google App Engine – Google Code

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

public class MailHandlerServlet extends HttpServlet {
    public void doPost(HttpServletRequest req,
                       HttpServletResponse resp)
            throws IOException {
        Properties props = new Properties();
        Session session = Session.getDefaultInstance(props, null);
        MimeMessage message = new MimeMessage(session, req.getInputStream());

特に GAE 用の API を呼び出しているわけでもなく、HttpServletRequest にアクセスしているだけです。
ただ jruby の場合、HttpServletRequest のインスタンスが見あたりません・・・。
なので、少しトリッキーなことをして HttpServletRequest のインスタンスを取ってくる必要があります。
rails 2.3.2 で動作確認をしていますので、sinatora などの場合、HttpServletRequest を取ってくる辺りの処理を変更する必要があるかも知れません。(未確認)
それが↓のコード appengine_mail_ext.rb になります。

以下のエントリーの Java のコードを参考にさせていただきました。
Google App Engine for Javaでのメール受信コード – きしだのはてな

appengine_mail_ext.rb

# -*- coding: utf-8 -*-

begin
  module AppEngine
    module Mail
      import com.google.appengine.api.mail.MailServiceFactory
      import com.google.appengine.api.mail.MailService

      import java.io.IOException
      import java.io.InputStreamReader
      import java.io.BufferedReader
      import java.util.Properties
      import javax.mail.Message
      import javax.mail.Session
      import javax.mail.internet.MimeMessage
      import javax.servlet.http.HttpServletRequest
      import javax.servlet.http.HttpServletResponse

      module_function

      def java_servlet_request(controller)
        begin
          url = controller.instance_variable_get(:@url)
          req = url.instance_variable_get(:@request)
          env = req.instance_variable_get(:@env)
          java_servlet_request = env['java.servlet_request']
        rescue
          raise "java.servlet_request not found."
        end
        return java_servlet_request
      end

      def receive(java_servlet_request)
        props = Properties.new
        session = Session.getDefaultInstance(props, nil)
        message = MimeMessage.new(session, java_servlet_request.getInputStream())

        return nil if message.nil?

        mail = {
          :subject      => message.get_subject,
          :content_type => message.get_content_type,
        }
        mail[:from]     = message.get_from[0].to_string rescue nil
        mail[:to]       = message.get_recipients(Message::RecipientType::TO).map {|address| address.to_string } rescue nil
        mail[:cc]       = message.get_recipients(Message::RecipientType::CC).map {|address| address.to_string } rescue nil
        mail[:bcc]      = message.get_recipients(Message::RecipientType::BCC).map {|address| address.to_string } rescue nil

        if message.is_mime_type('text/plain')
          mail[:content] = message.get_content
        elsif message.mime_type?('multipart/alternative')
          content = message.get_content
          (0..(content.count-1)).each do |i|
            bp = content.get_body_part(i)
            if bp.mime_type?("text/plain")
              r = InputStreamReader.new(bp.get_input_stream)
              buf = BufferedReader.new(r)
              msg = ''
              while line = buf.read_line
                msg << line + "\n"
              end
              mail[:content] = msg
              break
            end
          end
        else
          raise "unknown content type."
        end
        return mail
      end
    end
  end
rescue NameError #LoadError
  # appengine api wasn't available so neither can the store be
  # This will occur when run script/*
end

この appengine_mail_ext.rb を config/environment.rb 内で require するか、
config/initializers へコピーすることで、
AppEngine::Mail が拡張され、メール受信の準備が整います。
(AppEngine::Mail へ受信の method をつっこんでよかったのかなぁ・・・)

次に、メールを受信する部分のコードを action に書いてメール受信処理が完了します。

http://(appid).appspot.com/_ah/mail/(username)@(appid).appspotmail.com へのアクセスを action につなげる部分は、
Java では web.xml に書きますが、GAE/J + jruby + rails の場合、いつも通り config/routes.rb を編集すればいいようです。

以下は MailHandleController#receive へ割り当てた例:

ActionController::Routing::Routes.draw do |map|
  # ...
  map.mail      '_ah/mail/:email', :controller => 'mail_handle', :action => 'receive', :email => /.*/

最後に、メールを受信するコントローラーとアクションを書いて完了です。
↓はメールを受信して、それをログに表示するサンプル。

class MailHandleController < ApplicationController
  def receive
    email = params[:email] # guess_gmail(params[:email])
    mail = AppEngine::Mail.receive(AppEngine::Mail.java_servlet_request(self))

    Rails.logger.debug("email          : #{email}")
    Rails.logger.debug("mail[:content] : #{mail[:content]}")
    Rails.logger.debug("mail[:subject] : #{mail[:subject]}")
    Rails.logger.debug("mail[:from]    : #{mail[:from]}")
    Rails.logger.debug("mail[:to]      : #{mail[:to]}")
    Rails.logger.debug("mail[:cc]      : #{mail[:cc]}")
    Rails.logger.debug("mail[:bcc]     : #{mail[:bcc]}")

    render :text => "mail received.\n"
  end
end

おしまい。