Jamesの使用 第2回: matcherとmailetを使った電子メール・ベースでのアプリケーション構築

2つの新たなAPIが電子メールサーバーの能力を高めます

この記事は、ApacheグループのJames 電子メール・サーバーでのアプリケーション開発に関するシリーズの第2回目です。今回は、前回の基本的なJamesインフラストラクチャーの域より更に踏み込んで、ユーザーに(メールの)受信可/不可のフラグを立てたり、(受信)不可に設定されたユーザーから送信者へ自動的にカスタム・メッセージを送信する、といった実践的なアプリケーションを構築します。

Claude Duguay (claude.duguay@verizon.net), Senior J2EE architect, Capital Stream Inc.

Claude DuguayClaude Duguayは氏は20年以上ソフトウェアを開発しています。彼はJavaプラットフォームに関して情熱をもっており可能な限り執筆をしています。彼の連絡先はclaude.duguay@verizon.netです。



2003年 6月 10日

James (Java Apache Mail Enterprise)を探るシリーズの第2回です。シリーズ1回目では、Jamesの基本的な構造や機能を考察し、テストシステム上にJamesをインストールするプロセスをご紹介しました。今回の記事ではそれらの概要を踏まえて、ユーザーのアカウントに受信不可のフラグを設定するアプリケーションの構築を行い、Jamesインフラストラクチャーの実践的な考え方を活用していくことにします。このアプリケーションは、特定した電子メール・サーバー・アドレスへ特定の型のメッセージを送信させます。この送信メッセージは、ユーザーが再び受信可であることを示すメッセージを送信するまでは、あらゆる着信メールに対する自動レスポンスとして使用されることになります。メール送信者にユーザーの不在(例えば休暇中)を通知する同様のクライアント側の機能はしばしば使用されていますが、ユーザーがクライアント・ソフトウェア(PCのメーラーソフト等)を起動しておかなければ、そのような機能はあまり便利とは言えません。サーバー側のソリューションを使うことで、実際に休暇中である間も、代わりのクライアント・ソフトウェアを使用してログインでき、いつでもメッセージを変更することができます。

アプリケーション設計

アプリケーション構築の準備段階として、設計要件を明確にしておきましょう。以下のポイントが今回の設計例の基本となります。

  1. あるユーザーが自分のアカウントを受信不可モードに設定するために、unavailable@emailserver に(メッセージを記述した)電子メールを送信します。unavailable@emailserver に送られたメッセージは(サーバー側で)今後の使用に備えて格納されます。受信不可メッセージが既に格納されている場合は新しいものが上書きされます。メール送信後、ユーザーは(受信不可設定の)完了通知を(サーバー側から)受信します。
  2. 受信不可メッセージを取り消す(受信可に設定し直す)には、available@emailserver に電子メールを送信します。available@emailserver に送信されたメッセージは廃棄され、格納されている受信不可メッセージも削除されます。メール送信後、ユーザーは、受信可の設定に変更されたという通知を受信します。
  3. 受信不可メッセージが設定されているユーザー(のアカウント)が電子メールを受信する場合はいつでも、ユーザーが受信できない状態であることを示す、格納メッセージのコピーを送信者に送ります。無論、このプロセスを引き起こすオリジナルの着信メールは、正常に処理されます。

James インフラストラクチャーの理解

このシリーズの第1回で James の基本構造および機能について紹介をしています。

この3つの要件は単なる思い付きではありません。それぞれ特定の条件下で起こる一連のアクションを直接示しています。またそれぞれの条件は matcher によって認識されます (matcherおよびmailetの詳細については、このシリーズの第1回を参照してください)。例えば、特定のアドレスへ送信された電子メールは、組み込みのRecipientIsmatcher によってマッチされますが、不運にもRecipientIsのソース・コードを解析した際、RecipientIsは何人の受信者にも使用でき、どんなマッチも適切であると判断されることに気付きました。これはおそらく、ほとんどの場合では差し支えないでしょうが、今回のアプリケーションでは、たった1人の特定の受信者が関係することを確かめる必要がありますので、そのような機能をもつ単純な matcher を開発することにします。ここで開発するMatchSingleRecipientクラスは、Matcher API を機能させる方法を示すイントロダクションとしても役立つことでしょう。

また、受信者が受信不可メッセージを格納しているかどうかを見分けるのは少々複雑ですので、MatchUnavailableUser matcher を開発することにします。これをより効果的にするには、Unavailableディレクトリ内のファイルを確認する前に、ユーザーがlocalであるかを確認します。そうでなければ、この処理はかなり単純なものになるに違いありません。まず2つの matcher クラスを開発し、それから mailet の開発へと進んでいきます。

Unavailable ディレクトリはかなり頻繁に利用されます。実際、いくつかのオペレーションは、メッセージの存在の検索、そのメッセージの保存、読込、削除等の処理を含めて要求されます。これらの各関数はメッセージが格納されるディレクトリおよびユーザーに関する情報を把握する必要があるので、MatchUnavailableUser matcher が使用可能な別のクラスを作成し、mailet 実装設計でそれを利用することにします。

今回実装する matcher 間には共通点がほとんどありません。しかし、今回の要件 (各要件に1つの mailet があります)を取り扱う mailetは、すべて同じ設定情報および Unavailable ディレクトリへのアクセスを必要とします。したがって、それぞれの mailet が継承できるベースクラスを使用します。ベースクラスは基本的な初期化コードに加えて、複数のサブクラスに使用されるユーティリティ・メソッドを置くのによい場所でもあります。このベースクラスをUnavailableUserBase と呼ぶことにします。

3つのアプリケーション関数は、mailetクラス (UnavailableMessageSaveUnavailableMessageDropUnavailableMessageSend)によって取り扱われます。最初の二つのクラスは、スーパークラス中で終了するコードを共有する、非常に似通った関数を持っており、単数の受信者 (受信不可か受信可能なアドレス)上で動作します。UnavailableMessageSend mailetは最も複雑で、多数の受信者を処理しなければならず、受信不可のメッセージをそれぞれの送信者に対して送らなければなりませんが、幸運にも、Matcher API と Mailet API は容易に作動します。


matcher の書き込み

James での matcher の書き込みは、ほとんどの処理系が継承するベースクラスGenericMatcherによって簡素化することが出来ますので、今回も双方の matcher で利用します。まず、init()メソッドで構成情報を検索し、match()メソッドで今回の処理を行います。厳密には、ベンダーやバージョンなどをレポートするgetMatcherInfo()メソッドを実装するべきですが、例をより簡潔にするためにそれらはすべて省略しました。

James の config.xml ファイル (このシリーズの第1回でこのファイルについて詳細に説明しました)で matcher を構成する際は、XML 属性として matcher を規定します。符号および補足テキストが matcher クラス名に続きますが、このテキストはgetCondition()メソッドで取得する事が出来ます。James において有効なMailAddress比較コードを利用するためには、getCondition()メソッドから取得した値を保持するインスタンスを作成し、インスタンス変数に格納しなければなりませんが、果然、リスト1で示されるMatchSingleRecipientは、唯一の引数に1つの有効なローカル電子メールアドレスを要求します。

match()メソッドでは、処理された電子メールを格納した受信者のリストを取得します。Mailオブジェクトはいくつかの興味深いメソッドを提供していますが、その中で最もよく使われるのはgetRecipients()であり、MailAddressインスタンスを含む JavaCollection オブジェクトを返します。マッチしているかどうか確かめるためには、まずCollection のサイズチェックで1人の受信者だけが含まれていることを確かめ、次に(レングス1の)Collection が設定ファイルで指定したアドレスを含んでいることを確かめます。

これらのクラスのコードをコンパイルするには、このシリーズの第1回で説明した構成要素を全てダウンロードしている必要があります。james.jar ファイルと同様に、classpath に JavaMail (mail.jar)および JavaBeans Activation Framework (activation.jar)の JARファイルが存在していなければなりません。james.jar が中々見付からない場合は、James-2.1.2/apps ディレクトリの James.SARファイルから抽出できます。

リスト1. MatchSingleRecipient: 単一の特定アドレスへ送信された電子メールの識別
package com.claudeduguay.mailets;
import java.util.*;
import javax.mail.*;
import org.apache.mailet.*;
public class MatchSingleRecipient
  extends GenericMatcher
{
  protected MailAddress addressToMatch;
  public void init(MatcherConfig config)
    throws MessagingException
  {
    super.init(config);
    addressToMatch = new MailAddress(getCondition());
  }
  public Collection match(Mail mail)
    throws MessagingException
  {
    Collection recipients = mail.getRecipients();
    if (recipients.size() == 1 &&
        recipients.contains(addressToMatch))
    {
      return recipients;
    }
    return null;
  }
}

リスト2に示されるMatchUnavailableUserクラスでは、UnavailableStoreクラス (後述を参照)を使用します。このクラスを機能させるにあたり、getCondition()メソッドにより受信不可メッセージを格納するディレクトリを検索します。

リスト2. MatchUnavailableUser: 受信不可ユーザーの識別
package com.claudeduguay.mailets;
import java.util.*;
import javax.mail.*;
import org.apache.mailet.*;
public class MatchUnavailableUser
  extends GenericMatcher
{
  protected UnavailableStore store;
  public void init(MatcherConfig config)
    throws MessagingException
  {
    super.init(config);
    String folder = getCondition();
    store = new UnavailableStore(folder);
  }
  protected boolean isLocalAddress(MailAddress address)
  {
    String host = address.getHost();
    MailetContext context = getMailetContext();
    return context.isLocalServer(host);
  }
  protected boolean isUserUnavailable(MailAddress address)
  {
    return store.userMessageExists(address);
  }
  public Collection match(Mail mail)
    throws MessagingException
  {
    Collection matches = new Vector();
    Collection recipients = mail.getRecipients();
    Iterator iterator = recipients.iterator();
    while (iterator.hasNext())
    {
      MailAddress address = (MailAddress)iterator.next();
      String user = address.getUser();
      if (isLocalAddress(address) &&
          isUserUnavailable(address))
      {
        matches.add(address);
      }
    }
    return matches;
  }
}

match()メソッドの実装を容易にするため、2つのユーティリィティ・メソッドを実装しました。1つは、MailetContextisLocalServer()メソッドを利用するisLocalAddress()であり、ユーザーがローカルであるかを判断します。2つ目は、UnavailableStoreuserMessageExists()メソッドを使用するisUserUnavailable()であり、ユーザーが受信不可メッセージを指定したかどうかを把握します。

mailetに処理されるべき受信者を集めるために、match はCollection オブジェクト(Vector)を作成します。その後、それぞれの受信者に関して、ローカルアドレスを含み、受信不可メッセージがユーザーに指定されているかどうかを検査します。両方の条件が真の場合、その受信者を追加したリストを返します。

リスト3のUnavailableStoreコードを簡単に見てみましょう、matcher の処理系はこれで全部です。これは、mailet の各処理系だけでなく、MatchUnavailableUser にも使用されますが、その主旨は、単一のクラスに、受信不可メッセージ・ストアへのアクセスと関連するメソッドを全て集めることにあります。必要ならもっと精巧な処理系を開発できるでしょうが、今回は、意図的に単純化され、単一のディレクトリに全てのユーザーメッセージを格納しているにすぎません。もしサーバー上で数千規模のユーザーを管理していれば、これでは問題かもしれませんが、ほとんどのアプリケーションにはこれで十分でしょう。

リスト3. UnavailableStore: 受信不可メッセージ・ストアへのアクセス
package com.claudeduguay.mailets;
import java.io.*;
import java.util.*;
import javax.mail.*;
import javax.mail.internet.*;
import org.apache.mailet.*;
public class UnavailableStore
{
  protected File home;
  public UnavailableStore(String folder)
  {
    home = new File(folder);
    if (!home.exists())
    {
      home.mkdirs();
    }
  }
  public File getUserMessageFile(MailAddress address)
  {
    String user = address.getUser();
    File file = new File(home, user + ".msg");
    return file;
  }
  public boolean userMessageExists(MailAddress address)
  {
    return getUserMessageFile(address).exists();
  }
  public void deleteMessage(MailAddress address)
  {
    getUserMessageFile(address).delete();
  }
    public void storeMessage(MailAddress address, MimeMessage msg)
    throws MessagingException, IOException
  {
    File file = getUserMessageFile(address);
    FileOutputStream out = new FileOutputStream(file);
    msg.writeTo(out);
    out.close();
  }
  public MimeMessage getMessage(MailAddress address)
    throws MessagingException, IOException
  {
    File file = getUserMessageFile(address);
    Properties props = System.getProperties();
    Session session = Session.getDefaultInstance(props);
    FileInputStream in = new FileInputStream(file);
    MimeMessage msg = new MimeMessage(session, in);
    in.close();
    return msg;
  }
}

UnavailableStore の重要な構成引数は、ファイルを検索するディレクトリです。コンストラクタに値を渡し、ディレクトリが存在しない場合は作成されるようにします。このアプリケーションが機能するためには、(config.xmlの中の)設定において、全ての matcher と mailet は同じディレクトリーを指していることを確認しておかなければなりません。

メソッドの残りの部分は主にファイル・アクセス機能です。getUserMessageFile()メソッドは、特定のユーザーのファイル名を決定する共通の方法を提供します。matcher および mailet のコードではMailAddressオブジェクトを扱うことが容易なため、これらの各メソッドの引数として James の MailAddressオブジェクトを使用します。ファイル名は、[unavailabledirectory]/[username].msgという形式になります。

MimeMessageオブジェクトは JavaMail APIの一部です。James のインフラストラクチャーにこれらのオブジェクトが活用されているので、同じAPIを使用することができます。storeMessage()メソッドおよびgetMessage()メソッドは、ほとんどのメールサーバーに使用されている、標準テキスト・フォーマットでメッセージを読取り/書込みを行う、MimeMessage 機能を利用しています。連続したオブジェクト形式でメッセージを格納するのが容易な (かつ効率的な)一方で、メッセージがテキストとして判読可能な場合はデバッグが容易になっています。


mailetの書き込み

処理を要求する電子メールの構造(matcher)を理解したところで、処理要素自体を mailet処理系の形に組み立てる必要があります。受信不可メッセージを格納してアクセスするためにUnavailableStore を活用します。

mailetの実装へ入る前に、ある共通の振舞いをアクセス可能にしておくために使用するベースクラスを簡単に見ておきましょう。mailet の構成引数は、XMLタグの形式でconfig.xmlファイルの中で規定されています。これらのタグは、今回継承するGenericMailetクラスのgetInitParameter()メソッドを使用して取得することができます。UnavailableMessageBase は、直接 serviceメソッドを実装しないので、リスト4に示されるように抽象的に宣言されます。

init()メソッドは、UnavailableStoreオブジェクトへの参照を格納する点でMatchUnavailableUserクラスのinit()メソッドと似ていますが、UnavailableMessageBase のほとんどのコードは、ユーティリィティ・メソッドの形式をとります。ここでは、電子メール・メッセージを作成するためのコレクションと一組のメソッドとして単一のMailAddress に一括するメソッドとともに、MailAddress とアドレス・オブジェクトの配列との間で変換するメソッドと、アドレスのコレクションから最初のMailAddress を得るメソッドを開発しました。

メッセージが任意の Multipart content を含んでいるかもしれないので、後者のcreateMessage()メソッドは、コンテンツおよび MIME形式を指定するためにオブジェクトと文字列の引数をそれぞれ使用します。コンテンツは文字列を、MIME形式は「text/plain」を使用するような単純なケースは、簡素化したシグネチャを備えた別のメソッドによって抽象化されます。格納されたメッセージが不規則だったり複雑だったりする場合があるので、ここでは、受信不可メッセージを送るためのより一般的なケースを使用します。

リスト4. UnavailableMessageBase: 共通の振舞いを利用可能にするためのベースクラス
package com.claudeduguay.mailets;
import java.util.*;
import javax.mail.*;
import javax.mail.internet.*;
import org.apache.mailet.*;
public abstract class UnavailableMessageBase
  extends GenericMailet
{
  protected UnavailableStore store;
  public void init(MailetConfig config)
    throws MessagingException
  {
    super.init(config);
    MailetContext context = config.getMailetContext();
    String folder = getInitParameter("folder");
    store = new UnavailableStore(folder);
  }
  protected Address[] toAddressArray(MailAddress address)
    throws AddressException
  {
    InternetAddress[] array = new InternetAddress[1];
    array[0] = address.toInternetAddress();
    return array;
  }
  protected MailAddress getFirstAddress(Collection list)
  {
    Iterator iterator = list.iterator();
    return (MailAddress)iterator.next();
  }
  protected Collection addressAsCollection(MailAddress address)
  {
    Collection list = new Vector();
    list.add(address);
    return list;
  }
  protected MimeMessage createMessage(
    MailAddress from, MailAddress to,
    String subject, String text)
      throws MessagingException
  {
    return createMessage(from, to, subject, text, "text/plain");
  }
  protected MimeMessage createMessage(
    MailAddress from, MailAddress to,
    String subject, Object content, String type)
      throws MessagingException
  {
    Properties props = System.getProperties();
    Session session = Session.getDefaultInstance(props);
    MimeMessage msg = new MimeMessage(session);
    msg.addFrom(toAddressArray(from));
    msg.addRecipients(Message.RecipientType.TO, toAddressArray(to));
    msg.setSubject(subject);
    msg.setContent(content, type);
    return msg;
  }
}

これで mailet を直接処理することができます。まず最初に Unavailableアドレスへ送られたメッセージを保存します。リスト5に示されるように、受信不可の該当者にメッセージをディスパッチするために、UnavailableMessageSave mailet へMatchSingleRecipient matcher を設定することが可能です。構成を最大限利用するために、構成メッセージのサブジェクトとコンテンツ文字列を指定できるようにしました。値はinit()メソッドでローカル変数に格納されます。

すべての処理はserviceメソッドで行われます。ベースクラスのUnavailableMessageBase を継承したので、UnavailableStore のインスタンスにアクセスします。ここでの目的は、誰がメッセージを送ってきたかを把握し、格納すべきMimeMessage を取得し、それを正しいディレクトリーに配置し、送信者に確認メールを送ることです。ここでは、matcherがメッセージの通過を既に許可しているので、送信者は有効であると仮定します。

例外に対処する方法にも注意してください。log()メソッドは問題が確実に報告されるために使用されていますが、期待した振舞いが見られないときは、ログをチェックしなければなりません。さらにメールをGHOST状態にセットしていることにも注意してください。これはメールを格納したのちは、それ以降補足動作は要求されないため、送信された電子メールへの処理を止めることになります。

リスト5. UnavailableMessageSave: unavailable@emailserverに送られたメッセージの保存
package com.claudeduguay.mailets;
import java.io.*;
import java.util.*;
import javax.mail.*;
import javax.mail.internet.*;
import org.apache.mailet.*;
public class UnavailableMessageSave
  extends UnavailableMessageBase
{
  protected String subject;
  protected String content;
  public void init(MailetConfig config)
    throws MessagingException
  {
    super.init(config);
    subject = getInitParameter("subject");
    content = getInitParameter("content");
  }
  public void service(Mail mail)
    throws MessagingException
  {
    MailAddress sender = mail.getSender();
    Collection recipients = mail.getRecipients();
    MailAddress address = getFirstAddress(recipients);
    MailetContext context = getMailetContext();
    try
    {
      MimeMessage msg = (MimeMessage)mail.getMessage();
      store.storeMessage(sender, msg);
      mail.setState(Mail.GHOST);
    }
    catch (IOException e)
    {
      log("Unable to store user message", e);
    }
    try
    {
      MimeMessage msg = createMessage(address, sender, subject, content);
      Collection target = addressAsCollection(sender);
      context.sendMail(address, target, msg);
    }
    catch (MessagingException e)
    {
      log("Unable to send confirmation message", e);
    }
  }
}

リスト6に示される次のクラスはUnavailableMessageDrop mailet です。この mailet はUnavailableMessageSave と非常に似ていますが、確認メールを送る前に、メッセージを保存せずに削除します。したがって、このクラスの方がわずかにコード数は少なくなります。いずれの場合にも、受信不可メッセージ・ストアにアクセスするために、UnavailableStore クラスを活用しています。

リスト6. UnavailableMessageDrop: 受信不可メッセージの削除
package com.claudeduguay.mailets;
import java.io.*;
import java.util.*;
import javax.mail.*;
import javax.mail.internet.*;
import org.apache.mailet.*;
public class UnavailableMessageDrop
  extends UnavailableMessageBase
{
  protected String subject;
  protected String content;
  public void init(MailetConfig config)
    throws MessagingException
  {
    super.init(config);
    subject = getInitParameter("subject");
    content = getInitParameter("content");
  }
  public void service(Mail mail)
    throws MessagingException
  {
    MailAddress sender = mail.getSender();
    Collection recipients = mail.getRecipients();
    MailAddress address = getFirstAddress(recipients);
    MailetContext context = getMailetContext();
    store.deleteMessage(sender);
    mail.setState(Mail.GHOST);
    try
    {
      MimeMessage msg = createMessage(address, sender, subject, content);
      Collection target = addressAsCollection(sender);
      context.sendMail(address, target, msg);
    }
    catch (MessagingException e)
    {
      log("Unable to send confirmation message", e);
    }
  }
}

リスト7に示されたUnavailableMessageSend mailet は最も複雑ですが、それでも、Mailet API によりこの種の処理が比較的単純化されていることが分かるでしょう。init()メソッドは、特定のユーザーへの着信メッセージに対処する際に無視されるべきアドレスのリストを維持します。これは確認メッセージをユーザーのもとへ送る際に生じる可能性があるループを回避するために必要です。今回の設定では、2つのアドレス(受信可状態の1つと受信不可状態の1つ)がignoreの対象となります。管理者が異なるアドレスを使用することにした場合に、このような設定を可能にしました。アドレスのリストはセミコロンで区切りられた文字列でトークン化されます。

そこで、メッセージを取得する際にどんな問題でも記録するUnavailableStoregetMessage()メソッドをラップし、MailAddress が ignoreリストに含まれるかどうかをチェックするisIgnorable()メソッドを追加しました。処理の大部分はserviceメソッドで行われています。

リスト7. UnavailableMessageSend: 送信者へ受信不可メッセージを送信

リスティングを見るにはここをクリック

リスト7. UnavailableMessageSend: 送信者へ受信不可メッセージを送信

package com.claudeduguay.mailets;
import java.io.*;
import java.util.*;
import javax.mail.*;
import javax.mail.internet.*;
import org.apache.mailet.*;
public class UnavailableMessageSend
  extends UnavailableMessageBase
{
  protected MailAddress[] ignore;
    protected MimeMessage getMessage(MailAddress address)
  {
    try
    {
      return store.getMessage(address);
    }
    catch (Exception e)
    {
      log("Unable to read stored e-mail message: ", e);
    }
    return null;
  }
  public void init(MailetConfig config)
    throws MessagingException
  {
    super.init(config);
    String skip = getInitParameter("skip");
    StringTokenizer tokenizer = new StringTokenizer(skip, ";", false);
    int count = tokenizer.countTokens();
    ignore = new MailAddress[count];
    for (int i = 0; i < count; i++)
    {
      String address = tokenizer.nextToken().trim();
      ignore[i] = new MailAddress(address);
    }
  }
  protected boolean isIgnorable(MailAddress address)
  {
    for (int i = 0; i < ignore.length; i++)
    {
      if (address.equals(ignore[i]))
      {
        return true;
      }
    }
    return false;
  }
  public void service(Mail mail)
    throws MessagingException
  {
    MailAddress sender = mail.getSender();
    MailetContext context = getMailetContext();
    Collection recipients = mail.getRecipients();
    Iterator iterator = recipients.iterator();
    while (iterator.hasNext())
    {
      MailAddress address = (MailAddress)iterator.next();
      // If the recipient is unavailable, send the message.
      if (!isIgnorable(sender) && store.userMessageExists(address))
      {
        Collection target = addressAsCollection(sender);
        MimeMessage msg = getMessage(address);
        if (msg != null)
        {
          try
          {
            msg = createMessage(address, sender, msg.getSubject(), msg.getContent(), msg.getContentType());
            // Send e-mail from unavailable recipient, to sender
            context.sendMail(address, target, msg);
          }
          catch (IOException e)
          {
            log("Unable to construct new message: ", e);
          }
        }
      }
    }
  }
}

UnavailableMessageSend serviceメソッドでは、受信不可メッセージを格納しているローカルユーザーに該当する受信者をすべてチェックする必要がありますが、それぞれの受信者を確認するために2つのメソッドを使用します。isIgnorable()メソッドはアドレスがignoreではないことを確かめるために使用されます。また、UnavailableStoreクラスのuserMessageExists()メソッドは、受信不可メッセージを格納しているユーザーかどうか確認するために使用されます。これらの条件が満たされる場合、その受信者の受信不可メッセージを取得し、格納されているサブジェクトおよびコンテンツを使用して新しいメッセージを作成して、最初のメッセージを送ってきたユーザーへ新しいメッセージを送信します。


構築と配置

このプロジェクトでのクラスの構築はかなり単純ですが、それらを配置することは複雑です。実際、JavaMail(mail.jar)、JavaBeans Activation Framework (JavaMail APIに必要なactivation.jar)、Jamesパッケージ(james.jar)が CLASSPATH上にある限り、これらのクラスは簡単に構築することができます。JamesのJARファイルを使用するには、james/appsディレクトリにあるjames.sarファイルからJARファイルを取り出さなければなりません (これらのパッケージに関する全てのダウンロードリンクについては、参考文献を参照してください。このシリーズの第1回でインストールに関して詳細に説明をしています)。

SARファイル (Server Application Resource)は Phoenix の概念です。Avalonプロジェクトの一部である Phoenix サーバー・インフラストラクチャーで James は起動しているということを、みなさんは思い出すでしょう。(Avalonについての詳細は、参考文献を参照してください)。Phoenix は Javaプラットフォーム・プロジェクトに共通のサーバー・フレームワークを提供しています。SARファイルは指定された構造をもつ JARファイル(言い換えるとzipファイル)です。必要となる james.jar は、SARアーカイブ・ファイルの SAR-INF/libサブディレクトリに存在しています。ファイルを入手するためには、WinZIPや UNIX のunzipなどのユーティリィティを使用してください。

残念ながら、Jamesパッケージに mailets を追加する唯一の実行可能な方法は、SARファイルを再生成することです。これは不便で非常に複雑な作業です。しかし、James開発チームはこの不便さに気づいており、この方法をより便利にする予定です。とはいえ現状では、james.sarファイル全体をそれが機能するどこかに解凍して、SAR-INF/libディレクトリに (JAR形式で)クラスを配置することでクラスを追加し、コードが適切に認識され使用されるよう James構成ファイルを変更するようにしなければならず、その後 james.sarファイルを再パッケージし直す必要があります。競合を避けるために、安全な場所にオリジナルのjames.jarファイルを移動させて、新しいものをjames-plus.jarと呼ぶことにします。

今回の mailets を追加するためには、SAR-INFディレクトリにある config.xml設定ファイルを編集する必要があります。構成ファイルに2つのタイプのタグを追加します。最初のタグのセットでは、matcher および mailetクラスのパッケージ名をJamesに伝えます。matcher も mailet も org.apacheクラスを参照するために既に存在するタグを継承します。リスト8に追加箇所を示します。

リスト8. matcher と mailet のパッケージ宣言
<mailetpackages>
  <mailetpackage>org.apache.james.transport.mailets</mailetpackage>
  <mailetpackage>com.claudeduguay.mailets</mailetpackage>
</mailetpackages>
<matcherpackages>
  <matcherpackage>org.apache.james.transport.matchers</matcherpackage>
  <matcherpackage>com.claudeduguay.mailets</matcherpackage>
</matcherpackages>

定義されたクラスパッケージにより、リスト9に示すような matcher および mailet が検出でき、アプリケーションを形成することができるようになります。

リスト9. matcher と mailet のプロセス宣言
<mailet
  match="MatchSingleRecipient=unavailable@localhost"
  class="UnavailableMessageSave">
  <folder>d:/james/james-2.1.2/unavailable</folder>
  <subject>You have been marked as UNAVAILABLE</subject>
  <content>Send a message to available@localhost to reset.</content>
</mailet>
<mailet
  match="MatchSingleRecipient=available@localhost"
  class="UnavailableMessageDrop">
  <folder>d:/james/james-2.1.2/unavailable</folder>
  <subject>You have been marked as AVAILABLE</subject>
  <content>Send a message to unavailable@localhost to send the
    message to users when you are unavailable.</content>
</mailet>
<mailet
  match="MatchUnavailableUser=d:/james/james-2.1.2/unavailable"
  class="UnavailableMessageSend">
  <folder>d:/james/james-2.1.2/unavailable</folder>
  <skip>available@localhost;unavailable@localhost</skip>
</mailet>

Ant (Web サービスをエンタープライズ アプリケーション アーカイブにアセンブルするために使用する Java ユーティリティ)を使用したことがないと、リスト10のビルド・スクリプトは見慣れないものかも知れません。Ant も Apacheグループで作られたビルドツールで、構造の自動化のために現在広く使用されています。オリジナルの SARファイルからの抽出を自動化し、かつアプリケーションをコンパイルし、再構築し、配置するために、このツールを使用します。Ant に不慣れな方は、これについて知識を深めた方が良いでしょう。このツールは信じられないほどに強力なだけでなく、完全にポータブルであり、自動ビルドに関して大きな前進を遂げています。このビルドファイルをカスタマイズするためには、プロパティー・タグで指定されたディレクトリーを変更します (参考文献には、Antについての参照リンクがあります)。

リスト10. Ant ビルド・スクリプト
<project name="unavailable" default="packageSAR">
  <property name="project.dir" value="d:/james" />
  <property name="javamail.dir" value="javamail-1.3" />
  <property name="james.dir" value="james-2.1.2" />
  <property name="jaf.dir" value="jaf-1.0.2" />
  <property name="temp.dir" value="temp" />
  <target name="init" >
    <echo>Project dir: ${project.dir}</echo>
    <echo>James dir: ${james.dir}</echo>
    <echo>JavaMail dir: ${javamail.dir}</echo>
    <echo>JAF dir: ${jaf.dir}</echo>
  </target>
  <target name="moveOriginalSAR" depends="init" >
    <move todir="${project.dir}">
      <fileset dir="${project.dir}/${james.dir}/apps" >
        <include name="james.sar" />
      </fileset>
    </move>
    <delete includeEmptyDirs="true" failonerror="false" >
      <fileset dir="${project.dir}/${james.dir}/apps/james" />
    </delete>
  </target>
  <target name="unzipSAR" depends="moveOriginalSAR" >
    <delete includeEmptyDirs="true" failonerror="false" >
      <fileset dir="${project.dir}/${temp.dir}" />
    </delete>
    <unjar
      src="${project.dir}/james.sar"
      dest="${project.dir}/${temp.dir}" />
  </target>
  <target name="compile" depends="unzipSAR" >
    <echo>Compiling</echo>
    <javac srcdir="." destdir=".">
      <classpath>
        <pathelement path="${project.dir}/${temp.dir}/SAR-INF/lib" />
      </classpath>
    </javac>
    <echo>Compiled</echo>
  </target>
  <target name="packageJAR" depends="compile" >
    <echo>Packaging</echo>
    <copy file="config.xml" todir="${project.dir}/${temp.dir}/SAR-INF" />
    <delete file="${project.dir}/${temp.dir}/SAR-INF/lib/unavailable.jar" />
    <jar jarfile="${project.dir}/${temp.dir}/SAR-INF/lib/unavailable.jar"
      basedir="." includes="com/claudeduguay/**/*.class" />
    <echo>Packaged</echo>
  </target>
  <target name="packageSAR" depends="packageJAR" >
    <delete includeEmptyDirs="true" failonerror="false" >
      <fileset dir="${project.dir}/${james.dir}/apps/james-plus" />
    </delete>
    <jar jarfile="${project.dir}/${james.dir}/apps/james-plus.sar"
      basedir="${project.dir}/${temp.dir}" includes="**/*" />
  </target>
  
</project>

一旦ビルドが完了すれば、james/binディレクトリの実行スクリプトを使用して Jamesサーバーを起動することができます。すべてがうまくいけば、Jamesが起動中であることを示すシリーズの第1回で見た時と同じ出力がなされるでしょう。

アプリケーションをテストする前に、red、green、blue といったテスト用のユーザーを作成する必要があります。ユーザーを既に作成していなければ、第1回の記事を参考にして作成してください。ユーザーが作成できればテスト処理の準備は完了です。


ユーザーのシミュレート

一旦サーバー上でコードを実行させたら、アプリケーションが適切に起動していることを確認する一連のテストを行う必要があります。リスト11に示されるJamesApplicationTestクラスは、このシリーズの第1回の記事で開発したJamesConfigTestクラスに似ています。JamesApplicationTestクラスは、前回開発したMailClientクラスを使用して電子メールメッセージを送受信します。

さて、matcher と mailet をどのようにテストするか? まず、red、green、blue の各ユーザーのメッセージをすべてクリアすることからはじめます。それから、red と green から blue へ何通かのメッセージを送信し、blue に受信不可メッセージを送信させます。次に blue がメールをチェックする際は、blue は red と green からのオリジナルメッセージを確認できると共に、受信不可ユーザーより(受信不可の)メッセージが適切に保存されており受信不可状態でいる旨の確認メッセージを受け取ります。つまり、blue は通常にオペレーションをしてメッセージを送受信し続けることができますが、blue が受信不可状態でいる間に blue にメッセージを送信するユーザーは、blue が格納しておいた受信不可メッセージを受け取ることになります。

blue へ新たにメッセージを送信することで、ユーザーが受信不可状態の時に何が起きるかをテストできます。green を利用してメッセージを送信し、その後 green の受信トレイを確認します。green ユーザーは、blue がUnavailableアドレスへ送信し格納したメッセージコンテンツを反映している blue からの受信不可メッセージを受信します。最後に、blue は受信可メッセージを送信し、Availableアドレスからの確認メッセージが送られたことを確かめに電子メールをチェックします。

リスト11. JamesApplicationTest: matcher と mailet のテスト
public class JamesApplicationTest
{
  public static void main(String[] args)
    throws Exception
  {
    // CREATE CLIENT INSTANCES
    MailClient redClient = new MailClient("red", "localhost");
    MailClient greenClient = new MailClient("green", "localhost");
    MailClient blueClient = new MailClient("blue", "localhost");
    // CLEAR EVERYBODY'S INBOX
    redClient.checkInbox(MailClient.CLEAR_MESSAGES);
    greenClient.checkInbox(MailClient.CLEAR_MESSAGES);
    blueClient.checkInbox(MailClient.CLEAR_MESSAGES);
    Thread.sleep(500); // Let the server catch up
    // SEND A COUPLE OF MESSAGES TO BLUE (FROM RED AND GREEN)
    redClient.sendMessage(
      "blue@localhost",
      "Testing blue from red",
      "This is a test message");
    greenClient.sendMessage(
      "blue@localhost",
      "Testing blue from green",
      "This is a test message");
    // BLUE SENDS UNAVAILABLE MESSAGE
    blueClient.sendMessage(
      "unavailable@localhost",
      "On Vacation",
      "I am on vacation at the moment. " +
      "I will answer your mail as soon as I get back.");
    Thread.sleep(500); // Let the server catch up
    // LIST BLUE MESSAGES (RED, GREEN & UNAVAILABLE CONFIRMATION)
    blueClient.checkInbox(MailClient.SHOW_AND_CLEAR);
    // GREEN SENDS A NORMAL MESSAGE TO BLUE
    greenClient.sendMessage(
      "blue@localhost",
      "Testing blue from green",
      "This is a test message");
    Thread.sleep(500); // Let the server catch up
    // GREEN CHECKS MESSAGES (SHOULD SEE BLUE UNAVAILABLE MESSAGES)
    greenClient.checkInbox(MailClient.SHOW_AND_CLEAR);
    // BLUE SENDS MESSAGES TO BECOME AVAILABLE
    blueClient.sendMessage(
    "available@localhost",
    "Ignored subject",
    "Ignored content");
    Thread.sleep(500); // Let the server catch up
    // BLUE CHECKS MAIL, SHOULD SEE MESSAGE FROM GREEN AND AVAILABLE MSG
    blueClient.checkInbox(MailClient.SHOW_AND_CLEAR);
  }
}

JamesApplicationTes の出力は次のようになります。

リスト 12. JamesApplicationTest output
Clear INBOX for red@localhost
Clear INBOX for green@localhost
Clear INBOX for blue@localhost
SENDING message from red@localhost to blue@localhost
SENDING message from green@localhost to blue@localhost
SENDING message from blue@localhost to unavailable@localhost
Show and Clear INBOX for blue@localhost
    From: green@localhost
 Subject: Testing blue from green
 Content: This is a test message
    From: red@localhost
 Subject: Testing blue from red
 Content: This is a test message

SENDING message from green@localhost to blue@localhost
Show and Clear INBOX for green@localhost
    From: blue@localhost
 Subject: On Vacation
 Content: I am on vacation at the moment. I will answer your mail as soon as I get back.

SENDING message from blue@localhost to available@localhost
Show and Clear INBOX for blue@localhost
    From: available@localhost
 Subject: You have been marked as AVAILABLE
 Content: Send a message to unavailable@localhost to send the message to users when you are unavailable.
    From: green@localhost
 Subject: Testing blue from green
 Content: This is a test message
    From: unavailable@localhost
 Subject: You have been marked as UNAVAILABLE
 Content: Send a message to available@localhost to reset.

要約

この記事で示されたコードから分かるように、Jamesを扱うことはかなり容易と言えます。現在唯一複雑なのは、Jamesチームが取り掛かっている配置プロセスです。電子メールはインターネットにおける事実上最も使用されているアプリケーションであるので (明らかにWeb サービスより普及しています)、James インフラストラクチャーによって便利になり得る可能性を熟考することは興味深いことです。今回の手法は、思い込みから来る制限や重要な問題を解決するはずです。

参考文献

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=218913
ArticleTitle=Jamesの使用 第2回: matcherとmailetを使った電子メール・ベースでのアプリケーション構築
publish-date=06102003