Google App Engine for Javaでのメール受信コード

Google App Engineで受信メールの処理ができるようになった。
具体的な手順はこちら。
http://code.google.com/intl/en/appengine/docs/java/mail/receiving.html


手順はこう。
まず、appengine-web.xmlに次の設定を追加

<inbound-services>
  <service>mail</service>
</inbound-services>


そうすると、string@appid.appspotmail.comにメールが来たら /_ah/mail/<address> というURLが呼び出されるようになる。
なので、次のようなサーブレットマッピングをweb.xmlに追加してサーブレットで処理をする。

<servlet>
  <servlet-name>mailhandler</servlet-name>
  <servlet-class>MailHandlerServlet</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>mailhandler</servlet-name>
  <url-pattern>/_ah/mail/*</url-pattern>
</servlet-mapping>


特定のメールだけを受け取るときのurl-patternには、ドメイン名まで含める必要があるので注意。

<servlet-mapping>
  <servlet-name>mailhandler</servlet-name>
  <url-pattern>/_ah/mail/hoge@example.com</url-pattern>
</servlet-mapping>


このとき、そのままだと/_ah/mail/hogeに外からアクセスされてしまうので、管理者権限じゃないとアクセスできないようにする。このあたりはqueueとかと一緒。

<security-constraint>
  <web-resource-collection>
    <url-pattern>/_ah/mail/*</url-pattern>
  </web-resource-collection>
  <auth-constraint>
    <role-name>admin</role-name>
  </auth-constraint>
</security-constraint>


あとは、サーブレットを記述する。
問題は、この一文が正しくないこと。(※2009/12/3 挙動が変わって正しくなったようだ)

The getContent() method returns an object that implements the Multipart interface. You can then call getCount() to determine the number of parts and getBodyPart(int index) to return a particular body part.


getContentはMultipartをimplementsしたオブジェクトじゃなくByteArrayInputStreamを返す。
メールがplain/textの場合は、それをそのまま読み込むと本文になっている。
multipart/alternative(HTMLメール)とかmultipart/mixed(添付メール)の場合は、これをByteArrayDataSourceに渡してMimeMultipartオブジェクトを生成する。


ということで、メールを処理する普通のサーブレットを書けばいいということになる。
ここでは、次のようなエンティティを保存するコードを書いてみる。

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class ReceivedMail {
    @PrimaryKey
    @Persistent(valueStrategy=IdGeneratorStrategy.IDENTITY)
    Key id;

    @Persistent
    String from;

    @Persistent
    String subject;
    
    @Persistent
    String text;
}


PMFは、JDOのドキュメントのサンプルで定義されてるやつね。あとは適当に補完してそれっぽいクラスをimportする。

public class MailHandlerServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
        PersistenceManager pm = PMF.get().getPersistenceManager();

        Properties props = new Properties();
        Session session = Session.getDefaultInstance(props, null);

        try {
            MimeMessage message = new MimeMessage(session, request.getInputStream());

            ReceivedMail mail = new ReceivedMail();
            mail.setSubject(message.getSubject());
            //ローカルサーバーでは文字化けするので次のようなコードが必要。本番サーバーでは不要
            //new String(message.getSubject().getBytes("8859_1"), "UTF-8"));
            mail.setFrom(message.getFrom()[0].toString());

            String contentType = message.getContentType();
            InputStream is = null;
            //2009/12/11 挙動がかわったことに対応
            String mess = "";

            if(message.isMimeType("text/plain")){
                //ふつうのメールの処理
                /* 2009/12/11 ここも挙動が変わってたので、ClassCastExceptionになります。
                is = (InputStream) message.getContent();
                */
                mess = (String)message.getContent();
            }else{
                //HTMLメールや添付メールの処理
                /* 2009/12/3 挙動が変わったのでこれではClassCastExceptionが発生する
                Multipart content = new MimeMultipart(
                        new ByteArrayDataSource(
                            (InputStream)message.getContent(), 
                            message.getContentType()));
                */
                Multipart content = (Multipart)message.getContent();

                for(int i = 0; i < content.getCount(); ++i){
                    BodyPart bp = content.getBodyPart(i);
                    if(!bp.isMimeType("text/plain")) continue;
                    is = bp.getInputStream();

                    contentType = bp.getContentType();
                    break;
                }
            }
            if(is != null){
                //contentTypeからエンコーディングを取得
                String encoding = null;
                String[] elms = contentType.split(";");
                for(String elm : elms){
                    if(elm.trim().startsWith("charset=")){
                        encoding = elm.trim().substring("charset=".length());
                    }
                }

                Reader r = null;
                if(encoding != null){
                    //エンコーディングが入っている
                    if(encoding.startsWith("\"")) encoding = encoding.substring(1);
                    if(encoding.endsWith("\"")) encoding = 
                        encoding.substring(0, encoding.length() - 1);
                    r = new InputStreamReader(is, encoding);
                }else{
                    //エンコーディングが入っていない
                    r = new InputStreamReader(is);
                }

                //2009/12/11 挙動がかわったことに対応
                //String mess = "";
                BufferedReader buf = new BufferedReader(r);
                for(String line; (line = buf.readLine()) != null;){
                    mess += line + "\n";
                }
                //2009/12/11 挙動がかわったことに対応
                //mail.setText(mess);
            }
            //2009/12/11 挙動がかわったことに対応
            mail.setText(mess);
            pm.makePersistent(mail);
        } catch (MessagingException ex) {
            throw new ServletException(ex);
        }finally{
            pm.close();
        }
    }
}