I will be waiting for your knock ever after from today.


Visards, Inc.

Java Sticky Note

java
  ・JSTL
  ・Commons Net
  ・Apache
  ・Tomcat
  ・James
  ・Jetspeed
  ・POI
  ・Jexl
  ・Jelly
  ・Blojsom
  ・eclipse

link
  ・James

author
  ・profile

James  
<< prev | Index | next >>
Apache Jamesによるメールフレームワークの導入

mixiチェック

5. Jamesによるメールアプリケーションの開発
さて,Jamesでのメール受信の確認ができたところ で,メールを使用したアプリケーションを作成するこ とにします.


5.1 サンプルアプリケーションの説明
メールアプリケーションの例として,イベントの 申し込みをメールで受け付けるアプリケーションを考 えてみましょう.

clubjamesという同好会があるイベントを開催しま す.clubjames同好会の事務局は,会員からそのイベ ントの参加申し込みをメールで受け付けることにします.会員が,

apply-<member-id-hash>@clubjames.jp

あてにメールを出したら,そのメールを出した会員の イベント申し込みを受け付け,データベースにイベン ト参加の情報を設定します.
<member-id-hash>は,会員IDのハッシュ値としま す.たとえば,会員ID が186 番のメンバーの場合, 186 のハッシュ値が627zvqq956 だとすると,apply- 627zvqq956@clubjames.jpあてにメールが届いた場合, 会員ID 186番の会員のイベント申し込みを受け付ける ものとします.

ちなみに,イベント申し込みのために上記の長いメ ールアドレスを,会員自らメールソフトに入力する必 要はありません.同好会の事務局は,会員に対して図 5のようなメールを出しているからです.

同好会会員xxxx様


来たる4月25日、同好会のイベントが開催されます。
参加をご希望される場合は、
apply-<member-id-hash>@clubjames.jp
宛てに本文が空のメールを送ってください。

これを受け取った会員の方は,apply...@clubjames.jp で記述されたメールアドレスをクリックすることで, 簡単に上記メールアドレスあてに空のメールを送るこ とができます.携帯電話を含む最近のメールソフトの 多くは,このようにメール本文内に記述されたメール アドレスをクリックするだけで,簡単にそのメールア ドレスあてのメールを作成できるようになっているか らです.この例では,イベントの開催通知もその受付 もメールのみで行っているため,PCをはじめほとんど の携帯電話からも申し込みが可能で,面倒なマルチキ ャリア対応のWebサイトを構築する必要がありませ ん.さらに会員は,ごく簡単な操作でミスが少なくイ ベントへの申し込みが可能です.これは,メールベー スのアプリケーションが持つ利点の1つです.

ところで,このアプリケーションが,受信したメー ルのFromヘッダではなく,Toヘッダの中に会員ID のハッシュ値を埋め込むことで申し込んだ会員を特定 するようにしているのはなぜでしょうか.これは,事 務局から出したメールを,会員の方が他のメールアド レスに転送しているような場合,事務局からのメール のあて先と会員からの申し込みメールのFromが必ず しも一致しない可能性があり,またFromヘッダは容 易に偽ることができてしまうためです.

この例では,インターネットを介してclubjames.jp あてのメールを受け取るためにDNSの設定が必要とな ってしまいますので,以下に解説するサンプルプログ ラムを試すときには,clubjames.jpの部分をlocalhost としてローカルホスト上で実行してください.本サン プルアプリケーションでは,受信するメールをMatcher クラスを使って制御する方法と,受け付けたメールの 処理をMailetクラスで処理する方法を見ていきます.


5.2 Mailet インタフェースの概要
Mailetパッケージの中で,重要な働きをするのが, MailetとMatcherです.

図2 Jamesのフロー


届いたメールのうち,そのメールを処理対象とする かどうかを決定するためのインタフェースを定義して いるのがMatcherです.Mailetは,Matcherを通過 し,処理することを決定したメールに対して,実際の 処理を行うためのインタフェースを定義します.Mailet コンテナ上では,複数のMailetおよびMatcherが動作 し,受信したメールの処理を行ないます.
Mailetパッケージが提供するMailet/Matcherクラ スの関係を,下図に示します.

図3 Mailet/Matcherのクラス関係


受信したメールを表現するインタフェースは,Mail インタフェースです.一方,Mailetでは,Servletと 異なり明確な“ 応答” という概念がなく, ServletResponseに相当するクラスは用意されていま せん.ただ,Mailetコンテナでは,メールを送信する 機能を提供しており,返信メールを送信することは可 能です.


5.3 Matcher の実装
ここで,受け取ったメールを処理 するかどうかを決定します.今回のサ ンプルアプリケーションでは,apply- で始まるメールアドレスあてのメール を処理することにします(リスト1). member-id-hash値まで含めて詳細に メールのあて先の正当性をチェックす ることも可能ですが,リスト1ではそ こまでは行なわないことにします.

リスト1 SampleMatcher
  1 package sample.matcher;
  2 
  3 import org.apache.mailet.GenericRecipientMatcher;
  4 import org.apache.mailet.MailAddress;
  5 
  6 public class SampleMatcher extends GenericRecipientMatcher {
  7 
  8   public boolean matchRecipient(MailAddress recipient) {
  9     if (recipient.getUser().startsWith("apply-")) {
 10       return true;
 11     }
 12     return false;
 13    }
 14 
 15 }

Matcherは,受信したメールを処理するかどうかを 決定するためのインタフェースを定義していますが, その標準的な実装を提供するクラスとして GenericMatcherが用意されています.GenericMatcher は,受信したメールからそのメールを処理するかどう かを決定するために作られた抽象クラスです.受信し たメール全体からではなく,メールのあて先のみから そのメールを処理するかどうかを決定するのであれば, GenericMatcherを継承して作られた GenericRecipientMatcherクラスを継承して作成する のが簡単な実装方法です(図3).

GenericRecipientMatcherクラスを継承する場合は, matchRecipientメソッドを実装します.matchRecipient メソッドは,引数としてあて先のメールアドレス MailAddressを受け取ります.MailAddressクラスの getUserメソッドで,メールアドレスのローカルパー ト(@の前の文字列)を文字列として取得できます (リスト1 - 9行目).受け取ったメールを処理すると判 断した場合には,matchRecipientメソッドの戻り値に trueを設定します(リスト1 - 10行目).

これだけで,apply-で始まるメールを処理すること を決定するクラスの完成です.なお,リスト1では, 独自にMatcherを実装していますが,ユーザの識別や 添付ファイルのチェックなど,Jamesには標準で多く の有用なMatcherクラスが用意されています.ぜひご 確認ください.


5.4 Mailet の実装
次に,リスト2において受け取ったメールを処理す るMailetクラスを実装します.

リスト2 SampleMailet
 1 package sample.mailet;
 2
 3 import java.util.Collection;
 4 import java.sql.*;
 5 import javax.mail.*;
 6 import javax.mail.internet.*;
 7 
 8 import org.apache.avalon.cornerstone.services.datasource.*;
 9 import org.apache.avalon.excalibur.datasource.DataSourceComponent;
10 import org.apache.avalon.framework.component.ComponentManager;
11 import org.apache.james.Constants;
12 import org.apache.james.util.JDBCUtil;
13 import org.apache.mailet.GenericMailet;
14 import org.apache.mailet.MailetContext;
15 import org.apache.mailet.Mail;
16 import org.apache.mailet.MailAddress;
17 
18 public class SampleMailet extends GenericMailet {
19 
20   protected DataSourceComponent datasource;
21   String sql;
22 
23   // JDBC Helperクラス
24   private final JDBCUtil jdbcUtil = new JDBCUtil() {
25     protected void delegatedLog(String logString) {
26       log("SampleMailet: " + logString);
27     }
28   };
29 
30 
31   // Mailetの初期化.
32   public void init() throws MessagingException {
33     try {
34       sql = getInitParameter("sql"); // 初期パラメータからSQL文の取得
35 
36       MailetContext mailetContext = getMailetContext();
37       // Avalonコンポーネントマネージャから、コネクションプール取得
38       ComponentManager componentManager=(ComponentManager)mailetContext
39         .getAttribute(Constants.AVALON_COMPONENT_MANAGER);
40       DataSourceSelector datasources = (DataSourceSelector)
41         componentManager.lookup(DataSourceSelector.ROLE);
42       datasource = (DataSourceComponent)datasources.select("sampledb");
43     } catch(Exception e) {
44       throw new MessagingException("Error getting datasource", e);
45     }
46   }
47 
48 
49     // イベント参加申し込みメールの処理.
50   public void service(Mail mail) throws MessagingException {
51 
52     // 最初の宛先のみ取得
53     Collection recipients = mail.getRecipients();
54     MailAddress recipient = (MailAddress)recipients.iterator().next();
55     String request = recipient.getUser();  // ローカルパートを取得
56 
57     // ユーザIDを取得
58     String hash = request.substring(request.indexOf("-")+1);
59     String id = hash;    // 本来は、hashからユーザIDを取得する処理...
60     // 送信元のメールアドレス取得
61     String email = mail.getSender().toString();
62 
63     // イベント参加者をデータベースに登録
64     Connection conn = null;
65     PreparedStatement stmt = null;
66     try {
67       conn = datasource.getConnection();
68       stmt = conn.prepareStatement(sql);
69       stmt.setString(1, id);
70       stmt.setString(2, email);
71       stmt.execute();
72     } catch(SQLException e) {
73       throw new MessagingException(e.toString());
74     } finally {
75       jdbcUtil.closeJDBCStatement(stmt);
76       jdbcUtil.closeJDBCConnection(conn);  // connectionリリース
77     }
78 
79     // Thanksメール送信
80     Session s=Session.getDefaultInstance(System.getProperties(), null);
81     MimeMessage thanx = new MimeMessage(s);
82     thanx.setFrom(new InternetAddress("admin@clubjames.jp"));
83     thanx.setRecipients(Message.RecipientType.TO, email);
84     thanx.setSubject("club:jamesイベント受け付け", "iso-2022-jp");
85     thanx.setText("イベント参加を受け付けました。","iso-2022-jp");
86     getMailetContext().sendMail(thanx);
87   }
88 }

Mailetを作成するには, 一般的に GenericMailet を継承して実装します. GenericMailetは,Mailetを作成するための 標準的な実装を提供しているので, GenericMailetを継承してMailetを作成する ことで,Mailetのプログラミングを最小限 にすることができます.GenericMailetは抽 象クラスで,serviceメソッドの実装が必須 です.サンプルアプリケーションでは,デー タベースのコネクションプールを取得するた めserviceメソッドの他にinitメソッドも実装すること にします(リスト2 - 32〜46行目).

- データベースのコネクションプールの作成
サンプルアプリケーションでは,あて先のメールア ドレスを元に,イベントに参加する会員の情報をデー タベースに設定する処理を実装します.まず,データ ベースを使うために,データベースのコネクションプ ールを作成します.コネクションプールの作成は, Servlet同様initメソッドに記述します.initメソッド は,Mailetを初期化するために起動時に1度だけ呼び 出されるメソッドです.initメソッドにこの処理を記 述し,それ以降コネクションプールを使用することで, コストの高いデータベース接続処理をメール到着ごと に実行することがなくなり,無用なオーバーヘッドを 減らすことができます.

データベースのコネクションプールは,Avalonが提 供している機能を使用して実現することができます (リスト2 - 36〜42行目).getMailetContextメソッド で,MailetContextオブジェクトを取得することがで きます. MailetContextは, Servletにおける ServletContext同様,コンテナの提供する機能にアク セスする手段を提供するものです.MaletConextは, この後説明するserviceメソッドでもメールを送信する ために使用しています.

DataSourceComponentはAvalon Excaliburが提供 するデータソースの定義インタフェースです. DataSourceSelectorはAvalon Cornerstoneが提供す るクラスで,Factory パターンにより抽象化された DataSourceComponentの実装を取得する手段を提供 します.詳細な説明は割愛しますが,このプログラム では,Jamesの提供するMailetコンテナからAvalon フレームワークを通して,Excaliburが提供するデー タソースを取得しています.データソースの実態は JdbcDataSourceで,JDBCを介してアクセスされるデ ータベースのコネクションプールです.

設定ファイル内に記述したMailetの初期パラメータ は,Servlet同様getInitParameterメソッドで取得で きます(リスト2 - 34行目).サンプルプログラムでは, データベースのINSERT文を初期パラメータから読み 込むようにしています.なお,本サンプルでは実装し ていませんが,Servlet同様Mailetが停止されるとき に呼び出されるメソッドとしてdestroyが用意されて います.

-serviceメソッドの実装
次はメール処理本体であるserviceメソッ ドの実装です.serviceメソッドは,メール 処理が発生するごとに呼び出されるメソッド です.serviceメソッドには,Mailオブジェ クトが渡されます.Mailオブジェクトには, メールの内容とあて先情報の他に送信ホスト の情報や送信者のメールアドレスなどが含ま れています.表2に,Mailクラスの主なメソ ッドを記述します.メールのSubject や本文は, getMessageメソッドで取得できるMimeMessageオ ブジェクトから取得することができます.

表2 Mailクラスの主なメソッド
メソッド名処理概要
getMessageメールのメッセージを取得
getRecipientsメールの送信先を取得
getRemoteAddrメール送信のために接続したホストのIPアドレスを取得
getRemoteHostメール送信のために接続したホストのホスト名を取得
getSender送信者のメールアドレスを取得

このサンプルアプリケーションでは,メールのあて 先からイベント申込者を識別してデータベースに設定 し,申込者にthanksメールを返信します.サンプル プログラムでは,以下のような処理を実行していま す.
  • リスト2 - 52 〜55 行目で,メールのあて先を取得 しています.(このサンプルでは,メールの最初のあ て先しか処理していません)
  • リスト2 - 67〜71行目では取得したイベント申込者 をデータベースに保存しています
  • リスト2 - 79〜86行目ではthanksメールを送信し ています

メールの送信機能は,Mailetコンテナが提供してい ます.getMailetContextメソッドでMailetコンテキス トを取得し,sendMailメソッドでメールを送信しま す.メールのメッセージはJavaMailが提供している MimeMessageを使用して作成します.このように serviceメソッドの典型的な実装は,メールをMailオ ブジェクトとして受け取り,そのメールに関して何ら かの処理を行い,MailetContextが提供するsendMail メソッドで返信を送る形となります(もちろん,必要 がなければ返信を行う必要はありません).

JDBCUtilは、JDBCのための簡単なユーティリティクラスで、closeJDBCStatement()メソッド、closeJDBCConnection()メソッドは、それぞれステートメントとコネクションがnullではないかどうかをチェックしてからclose()処理を行ない、例外が発生した場合にログを記録する処理を実行しています。なお、datasource.getConnection()(67行目)で取得されるコネクションは、Avalonによりラッピングされたコネクションで、close()処理を呼び出した場合、実際にはデータベースがクローズされるのではなく、コネクションプールにコネクションが返される処理が実行されます。


なお,リスト2では<member-id-hash>値からユー ザIDを取得する処理や一部のエラー処理などを省略 しています.


5.5 コンパイルと設定

コンパイルには, Avalonフレームワークと $PHOENIX_HOME/apps/james.sarの中に含まれて いるjarファイルが必要です.james.sarファイルを展 開し,作成されるSAR-INF/lib下のjar ファイルと, $PHOENIX_HOME/lib/avalon-framework-4.1.3.jar をクラスパスに設定してコンパイルしてください.コ ンパイルしたSampleMatcherとSampleMailetのクラ スファイルは,jarでまとめてsample.jarとし,james.sar ファイル内のSAR-INF/lib/に配置します.

● 設定ファイルの修正
次にサンプルプログラムが実行できるように,設定 ファイルを修正します. 以下の設定はすべて $PHOENIX_HOME/apps/james/SAR-INF/ config.xmlに行ないます.

- パッケージの登録
今回のサンプルアプリケーションは,SampleMailet はsample.mailet パッケージ,SampleMatcher は sample.matcherパッケージで作成しました.作成し たパッケージ名を図4のように設定します.

図4 パッケージの登録(config.xml 103行目付近)
<mailetpackages>
  <mailetpackage>org.apache.james.transport.mailets</mailetpackage>
  <mailetpackage>sample.mailet</mailetpackage>
</mailetpackages>
<matcherpackages>
  <matcherpackage>org.apache.james.transport.matchers</matcherpackage>
  <matcherpackage>sample.matcher</matcherpackage>
</matcherpackages>

- データベースの設定
次にデータソースを設定します(図5).これは, SampleMailetのinitメソッドで取り出すデータベース のコネクションプールを定義するものです.データソ ース名,ドライバ,データベースのユーザ/パスワー ド,コネクションの最大数などを設定します.

図5 データベースの設定(config.xml 608行目付近)
<data-sources>
  <data-source name="sampledb"
      class="org.apache.james.util.mordred.JdbcDataSource">
    <driver>org.gjt.mm.mysql.Driver</driver>
    <dburl>jdbc:mysql://localhost/sampledb</dburl>
    <user>username</user>
    <password>password</password>
    <max>20</max>
  </data-source>


- Mailet/Matcherの登録
そして,SampleMailetとSampleMatcherの登録で す.図6のように,SampleMatcherに適合したメー ルに対し,SampleMailetが実行されるように設定し ます.初期パラメータは<mailet>〜</mailet>の中に, <初期パラメータ名>パラメータ値</初期パラメータ名> のように記述します.

図6 Matcher及びMailetの登録(config.xml 190行目付近)
<processor name="transport">
  <mailet match="SampleMatcher" class="SampleMailet">
    <sql>INSERT INTO event (id, email) VALUES (?,?)</sql>
  </mailet>


以上で設定は終了です.james.sar ファイル内の config.xmlも上記設定を行なったファイルに更新して おいてください.
データベースの設定は、本稿では割愛します。

5.6 サンプルアプリケーションの実行
サンプルアプリケーションを実行してみます.James を再起動し,apply-@localhost あ てにメールを出すと,データベースにイベ ント参加者が設定されるとともに,thanks メールが返信されます.

サンプルでは,エラー処理を一部省略し ており,またメールの最初のあて先しか処 理していないなど,プログラムを簡略化し ています.しかし,Mailetの持つ可能性は 感じていただけるのではないかと思います. 今回は,比較的シンプルなメール処理の 例でしたが,メールの翻訳処理,スパムな どのメールに対するフィルタリング処理, メーリングリストの管理などさまざまなメ ール処理アプリケーションの作成が可能で あり,またそれらのメールアプリケーショ ンの作成を手助けする多くのMatcherやい くつかのMailetも提供されています.


Resources:
 << prev  ↑index  next >>

このドキュメントに関するご意見、ご要望などはまで。




Copyright (C) 2003-2005 Visards, Inc. All Rights Reserved.