James君!〜独自 Mailet を書く

org.apache.mailet.generiMailet クラス

まあ、Mailet だって、「Matcher より少し難しい」だけだ。これの基底クラスは org.apache.mailet.GenericMailet である。

package org.apache.mailet;

import javax.mail.MessagingException;
import java.util.Iterator;

public abstract class GenericMailet implements Mailet, MailetConfig {
    private MailetConfig config = null;

    // 破棄時に呼ばれる
    public void destroy() {
        //Do nothing
    }

    // オプションを取得する。
    // とはいえ、これは子エレメントである...名前ベースで引き出せるように
    // なっている。
    // もし、同一の名前の子エレメントがあれば、それらはカンマ区切りで
    // 一つの文字列として返ってくる。ちょいと意外だ。
    public String getInitParameter(String name) {
        return config.getInitParameter(name);
    }

    // オプションを列挙する
    public Iterator getInitParameterNames() {
        return config.getInitParameterNames();
    }

    // MailetConfig を取得する
    public MailetConfig getMailetConfig() {
        return config;
    }

    // MailetContext を取得する
    public MailetContext getMailetContext() {
        return getMailetConfig().getMailetContext();
    }

    // 適当な情報を返す。author とか version とか copyright とかである。
    public String getMailetInfo() {
        return "";
    }

    // Mailet 名を返す
    public String getMailetName() {
        return config.getMailetName();
    }


    // James 起動時に呼ばれる。初期化をさせればよい。
    public void init(MailetConfig newConfig) throws MessagingException {
        config = newConfig;
        init();
    }

    public void init() throws MessagingException {
        //Do nothing... can be overriden
    }

    // ログを出力する
    public void log(String message) {
        StringBuffer logBuffer =
            new StringBuffer(256)
                    .append(getMailetName())
                    .append(": ")
                    .append(message);
        getMailetContext().log(logBuffer.toString());
    }

    public void log(String message, Throwable t) {
        StringBuffer logBuffer =
            new StringBuffer(256)
                    .append(config.getMailetName())
                    .append(": ")
                    .append(message);
        getMailetContext().log(logBuffer.toString(), t);
    }

    // で Mailet はこいつを実装する
    public abstract void service(Mail mail) throws javax.mail.MessagingException;
}

本質的には Matcher とそう変わらない。実装すべきメソッドは...

  1. public void service(Mail mail) throws MessagingException; ←当然
  2. public void init() throws MessagingException; ←初期化。実装しているもの多し。
  3. public String getMailetInfo(); ←実装メリットはある。

くらいなものである。あと当然、「お役立ちメソッド」として、

String getInitParameter(String name);
オプション名前指定で取得する
Iterator getInitParameterNames();
オプション名を列挙する Iterator を返す
void log( String message, [Throwable t] );
ログする

がある。Matcher では 「Matcher名=オプション」でオプションを指定したので、拾い出しは String getCondition() で一発だったが、今度は、

<mailet match="Matcher名=Matcherオプション" class="Mailetクラス名">
   <Mailetオプション>値</Mailetオプション>
   <Mailetオプション>値</Mailetオプション>
   .....
</mailet>

のかたちなので、少しだけ面倒になっている。

実例〜デバッグ用 Info Mailet

まあ、service() の引数で org.apache.mailet.Mail オブジェクトが渡されるのも Matcher と同様である。この仕様は Matcherの方 を参照したまえ。

とはいえ、実際に James で用意されている Mailet 例は、Matcher 例よりも複雑なものが多い....特に自動返信関連なぞ、Bounce の場合だと、

Bounce --> AbstractNotify --> AbstractRedirect --> GenericMailet

などと深い継承関係にあるが、これは「自動返信Mailetグループ」で James が凝った設計をしているからに過ぎない。とはいえ、org.apache.james.transport.mailets を見ても、あまりイイ例がないんだよな..... どうせ「読者のしたいこと」というのは、「受け取ったメールに対して、その送信元とかでDB登録して...」っていうようなタイプのことであろう。そういう役にたちそうなサンプルがあまり用意されていない。

いきなりは何だから、ごく簡単な(でも使える)デバッグ用途の Mailet でも作ろうか。ホントは LogHeaders Mailet があるにはあるんだが、いまいち使いにくい...こういう仕様の Mailet があれば、デバッグに役立つと思うぞ。

<mailet match="All" class="Info">
  <console>true|false。true なら標準出力に出力。デフォルトtrue</console>
  <log>true|false。true ならログ出力。デフォルトtrue</log>
  <passThrough>true|false。true なら処理終了。デフォルトfalse</passThrough>
  <message>任意のメッセージ。デフォルトは「Info Mailet」</message>
  <showRecipient>true|false。true なら受取人を表示。デフォルトtrue</showRecipient>
  <showSender>true|false。true なら送信者を表示。デフォルトtrue</showSender>
  <showSubject>true|false。true ならタイトル表示。デフォルトtrue</showSubject>
  <showBody>true|false|数値。本文表示文字数。デフォルト false、
     true or -1 だと全部! ただし、表示の見栄えを考えて改行は削除
     (文字数にもカウントしない)。</showBody>
  <showHeader>表示すべき追加ヘッダ。オプション</showHeader>
  <showAttribute>表示すべきアトリビュート。オプション</showAttribute>
</mailet>

オプションはすべて任意なので、

<mailet match="All" class="Info"/>

でも十分期待どおりの働きをする。

package jp.or.nurs.sug.james.mailets;

import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.InternetAddress;

import org.apache.mailet.GenericMailet;
import org.apache.mailet.Mail;
import org.apache.mailet.MailAddress;

import java.util.Collection;
import java.util.Iterator;
import java.util.StringTokenizer;

import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimePart;

import java.util.Collection;
import java.io.IOException;

public class Info extends GenericMailet {
    // オプションたち
    private boolean console = true;
    private boolean log = true;
    private boolean passThrough = true;
    private String message = "Info Mailet";
    private boolean showRecipient = true;
    private boolean showSender = true;
    private boolean showSubject = true;
    private int showBody = 0;
    private String showHeader = "";
    private String showAttribute = "";

    /* 初期化する。本質的にはオプションを拾い出すだけ */
    public void init() throws MessagingException {
        console = getBoolean( "console", console );
        log = getBoolean( "log", log );
        passThrough = getBoolean( "passThrough", passThrough );
        showRecipient = getBoolean( "showRecipient", showRecipient );
        showSender = getBoolean( "showSender", showSender );
        showSubject = getBoolean( "showSubject", showSubject );
        String tmp;
        if( (tmp = getInitParameter( "showBody" )) != null ) {
            if( tmp.trim().equalsIgnoreCase("true") ) {
                showBody = -1;
            } else if( tmp.trim().equalsIgnoreCase("false") ) {
                showBody = 0;
            } else {  // 文字数制限
                try {
                    showBody = Integer.parseInt( tmp );
                } catch( NumberFormatException e ) {
                    /* NOP */
                }
            }
        }
        if( (tmp = getInitParameter( "message" )) != null ) {
            message = tmp;
        }
        if( (tmp = getInitParameter( "showHeader" ) ) != null ) {
            showHeader = tmp.trim();
        }
        if( (tmp = getInitParameter( "showAttribute" ) ) != null ) {
            showAttribute = tmp.trim();
        }
    }

    /* 下請け。Bool値オプションを処理する */
    private boolean getBoolean( String param, boolean def ) {
        String tmp;
        if( (tmp = getInitParameter( param ) ) != null ) {
            if( tmp.trim().equalsIgnoreCase( "true" ) ) {
                return true;
            } else if( tmp.trim().equalsIgnoreCase( "false" ) ) {
                return false;
            }
        }
        return def;
    }

    /* サービス本体 */
    public void service( Mail mail ) throws MessagingException {
        // StringBuffer にすべて貯める
        StringBuffer sb = new StringBuffer();
        sb.append( message + "\n" );
        try {
            // 例外処理があるので、別メソッドに割る
            sb = doInfo( mail, sb );
        } catch( Exception e ) {
            // スタックトレースを取得する
            sb.append( "caught Exception: " + e.toString() );
            StackTraceElement [] elem = e.getStackTrace();
            for( int i = 0; i < elem.length; i++ ) {
                sb.append( "\t\t" + elem[i] + "\n" );
            }
            // 一応例外なら常に passThrough == false にしておこうか。
            passThrough = false;
        }

        String out = sb.toString();
        // 終末の統一
        if( out.endsWith( "\n" ) ) {
            out = out.substring( 0, out.length() - 1 );
        }

        // 出力
        if( console ) {
            System.out.println( out );
        }
        if( log ) {
            log( out );
        }

        // passThrough に応じて、処理を継続するか否か。
        if( ! passThrough ) {
            mail.setState( Mail.GHOST );
        }
    }

    /* 下請け: メール情報から出力内容を構築 */
    private StringBuffer doInfo( Mail mail, StringBuffer sb ) throws Exception {
        MimeMessage message = mail.getMessage();
        if( showSender ) {  // Sender について
            MailAddress addr = mail.getSender();
            sb.append( "\tSender: " + addr.toString() + "\n" );
        }
        if( showRecipient ) { // Recipient について
            Collection rec = mail.getRecipients();
            String tos = "";
            String delim = "";
            Iterator i = rec.iterator();
            while( i.hasNext() ) {
                MailAddress at = (MailAddress)i.next();
                tos += delim + at.toString();
                delim = ", ";
            }
            sb.append( "\tRecipient: " + tos + "\n" );
        }
        if( showSubject ) {  // Subject について
            sb.append( "\tSubject: " + message.getSubject() + "\n" );
        }
        if( !showHeader.equals("") ) {  // その他 Header について
            // 複数のパラメータ・タグがあっても、すべて「,」でつながって
            // 取得される。
            StringTokenizer st = new StringTokenizer( showHeader, "," );
            while( st.hasMoreTokens() ) {
                String nam = st.nextToken().trim();
                sb.append( "\t" + nam + ": " + message.getHeader(nam,null) +
                           "\n" );
            }
        }
        if( !showAttribute.equals("") ) { // Attribute について
            StringTokenizer st = new StringTokenizer( showAttribute, "," );
            while( st.hasMoreTokens() ) {
                String nam = st.nextToken().trim();
                sb.append( "\t" + nam + "= " + mail.getAttribute( nam ) +
                           "\n" );
            }
        }

        if( showBody != 0 ) {  // メッセージ本体について
            String text = null;
            try {
                text = getText( message );
            } catch( Exception e ) {
                text = "(caught exception:" + e.toString() + ")";
            }
            if( text == null ) {
                text = "(null)";
            }
            sb.append( "\tText: " + editBody(text) + "\n" );
        }
        return sb;
    }

    /* 行の単一化および字数制限 */
    private String editBody( String s ) {
        StringTokenizer st = new StringTokenizer( s, "\n\r" );
        String ret = "";
        while( st.hasMoreTokens() ) {
            ret += st.nextToken();
        }
        if( showBody > 0 ) {
            if( ret.length() > showBody ) {
                ret = ret.substring( 0, showBody );
            }
        }
        return ret;
    }

    /* MimeMessage から正しく text を抽出 */
    private String getText( MimePart part ) throws MessagingException, IOException {
        // Mime Type によって text 部分を特定する
        if( part.isMimeType( "text/plain" ) ) {  // プレーンメール
            return part.getContent().toString();
        } else if( part.isMimeType( "text/html" ) ) {  // HTMLメール
            return part.getContent().toString();
        } else if( part.isMimeType( "multipart/mixed" ) ) { // マルチパート
            MimeMultipart mp = (MimeMultipart)part.getContent();
            return mp.getBodyPart(0).getContent().toString();
        } else if( part.isMimeType( "multipart/alternative" ) ){ // マルチパート
            MimeMultipart mp = (MimeMultipart)part.getContent();
            int num = mp.getCount();
            for( int i = 0; i < num; i++ ) {
                MimeBodyPart body = (MimeBodyPart)mp.getBodyPart(i);
                String ret = getText( body );
                if( ret != null ) {
                    return ret;
                }
            }
            return null;
        } else {
            // 多分再帰で呼ばれるマルチパートメールのテキスト以外
            return null;
        }
    }
}

次のページではもう少し実戦的な例を示す。

例外処理

前節でも「Matcher が MessagingException を投げた場合」についての議論をしたが、Mailet だって投げるかもしれない。で、パイプライン処理中の例外をどうハンドリングするか?というのは大きな問題だ。まあ、シグナチュアにもあるように、Matcher も Mailet も「javax.mail.MessagingException を投げてもよし」というかたちで定義されている。

MessagingException を投げるんだったら、他の例外を投げるより要領がいい。もう少し柔軟な処理が可能なんである。実際、デフォルトの仕様である、

  1. Matcher or Mailet が例外を投げたら、その <mailet> の実行は中止。
  2. メールはエラー扱いとなって、error Processor による処理に分岐。
  3. 結果として、james-2.2.0/apps/james/var/mail/error リポジトリに保存。

という動作も、実は今から紹介するやり方の特殊ケースに過ぎない。そのやり方とは、

Matcherの場合
  1. その Matcher と結び付いた Mailet の MailetConfig を見る。
  2. その MailetConfig から "onMatchException" という InitAttribute を取得する。
  3. もし、"onMatchException" が未定義ならば、値は "error" だ。
  4. もし、"onMatchException" が "nomatch" ならば、例外を投げた Matcher は「マッチしなかった」と解釈される。
  5. もし、"onMatchException" が "matchall" ならば、例外を投げた Matcher は「マッチした(Recipients をそのまま返した)」と解釈する。
  6. "onMatchException" が "nomatch" でも "matchall" でもなければ、ログを出力し、 "onMatchException" の値を「遷移すべきプロセッサ名」だと解釈して遷移する。だから、"onMatchException" が未定義のケースだと、error プロセッサに遷移するんである。
Mailetの場合
  1. 自分自身の MailetConfig を見る。
  2. その MailetConfig から "onMailetException" という InitAttribute を取得する。
  3. もし、"onMailetException" が未定義ならば、値は "error" だ。
  4. もし、"onMailetException" が "ignore" ならば、例外を投げた Mailet は例外を投げたことを無視して処理を継続する。
  5. "onMailetException" がそれ以外の場合には、ログを出力し、 "onMailetException" の値を「遷移すべきプロセッサ名」だと解釈して遷移する。だから、"onMailetException" が未定義のケースだと、error プロセッサに遷移するんである。

という仕組みだ。じゃあ、この InitAttribute って何だ...というと、実は意外なものなんである。要するに、

<mailet match="Thrower" class="Info" onMatchException="matchall" 
                                     onMailetException="ignore" />

と書ける、ということなんである。これで例外処理はバッチリだな。



copyright by K.Sugiura, 1996-2006