CodeZine(コードジン)

特集ページ一覧

放置されている未読メールを携帯に自動転送する

JavaMailを利用した簡易メール転送プログラムの作成

  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加
2005/08/01 12:00

「未読状態のまましばらく経過したメールを携帯に転送したい」という筆者のちょっとした悩みを解決するために書いたのが今回のプログラムです。このJavaMailを利用したプログラムは、IMAP形式のメールやLinux環境でも利用できます。

はじめに

 「未読状態のまましばらく経過したメールを携帯に転送したい」――という筆者のちょっとした悩みを解決するために書いたのが今回のプログラムです。POP対応のメール転送ソフトならば選択肢も豊富なため、こうしたニーズに応える製品を見つけるのは難しくないかもしれません。しかし、「IMAPで使いたい」「Linuxサーバに常駐させたい」という筆者の要件を満たすツールはすぐに見つかりそうになかったので、手っとり早くJavaMailで実装してみることにしました。

対象読者

 Javaプログラミングの経験者を対象とします。

必要な環境

 Windows XPおよびLinux上のJ2SE 1.4で動作を検証しました。コンパイル済みのクラスファイルがサンプルコードに付属しますので、そのまま動作可能です。ソースコードからコンパイルする場合は、AntEclipseをご利用ください。

UnreadMailの使い方

 今回のプログラム「UnreadMail」の使い方は簡単です。J2SEをインストールしたのち、サンプルコードのzipファイルを展開します。メール転送設定ファイル「mail.properties」をテキストエディタで開き、実行環境に合わせて各プロパティを設定します。なお、UnreadMailはPOP3とIMAP両方のメールサーバをサポートしています。

mail.propertiesの設定例
プロパティ設定例 内容
mail.smtp.host=smtp.example.co.jp 送信メールサーバ
mail.smtp.from=your-address@example.co.jp 「Enverope From」アドレス(配信エラー時の戻り先。転送先以外のアドレスを指定します)
mail.smtp.auth=false 送信メールサーバの認証(必要ならtrue、不要ならfalse)
unread.smtp.user= 送信メールサーバの認証用アカウント名とパスワード(不要なら空欄)
unread.smtp.password=
mail.store.protocol=pop3 受信メールサーバのプロトコル(pop3またはimap):pop3の場合、メールサーバに残っているすべてのメールを未読として転送しますのでご注意ください
unread.store.host=pop.example.co.jp 受信メールサーバ、受信アカウント名、パスワード
unread.store.user=your-account
unread.store.password=your-password
unread.forwardto=your-address@example.co.jp 転送先のメールアドレス
unread.forwardafter=15 転送保留時間(分):メール送信日時からこの時間を経過した未読メールを転送します。なお、ここで指定した時間の1/4の間隔でメールサーバをチェックします
unread.maxquotelines=5 最大引用行数(この行数を超える引用部分をカットします。カット不要時は0に設定)

 修正した「mail.properties」ファイルを保存したら、コマンドプロンプトで「UnreadMail」ディレクトリに移動し、起動スクリプト「run.bat」を実行します(Linuxの場合は「run.sh」を適宜修正して使用してください)。

> run.bat

 以下のようなメッセージが表示され、特にエラーが発生していないことを確認してください。

Sun Jul 24 19:11:53 JST 2005 Unread Mail started.

 メールが到着し、未読状態のまま設定した時間を経過すると、以下のようなメッセージが表示されてメールが転送されます。

Sun Jul 24 09:51:58 JST 2005 forwarded: <メールのタイトル>

 UnreadMailを停止するには、コマンドプロンプトを閉じてください(Linuxの場合はプロセスをkillしてください)。

 以下に、UnreadMailのディレクトリ構成を示します。

UnreadMailのディレクトリ構成
ディレクトリ名/ファイル名 内容
src Javaソースコード
bin Javaクラスファイル
lib 使用クラスライブラリ(JavaMail)
mail.properties メール転送設定ファイル
run.bat Windows用起動スクリプト
run.sh Linux用起動スクリプト
build.xml Ant用ビルドスクリプト

JavaMailについて

 UnreadMailでは、メールを送受信する手段として、J2EE仕様を構成するAPIのひとつである「JavaMail」を使用しています。JavaMailを使えば、SMTPサーバによるメール送信やPOP3/IMAPサーバによるメール受信を簡単に実装できます。同APIの仕様やサンプルコードは、サンのWebページ(http://java.sun.com/products/javamail/)からダウンロード可能です。

 この記事ではJavaMailについてあまり詳しくは紹介できませんが、木下氏の書籍『JavaMail完全解説(秀和システム)』で非常にわかりやすく解説されていますので、ぜひ参考にしてください。

UnreadMailのクラス構成

 では、UnreadMailがどのようなコードによって実装されているか説明しましょう。図1は、UnreadMailを構成する4つのクラスの概要を示した図です。

図1:UnreadMailを構成する4つのクラス
図1:UnreadMailを構成する4つのクラス

 これら4つのクラスについて、その役割を説明していきます。

 クラスUnreadSessionは、設定用プロパティファイル「mail.properties」の内容を読み込み、1つのメールアカウントに対応する設定情報をフィールドに保持するオブジェクトです。メールサーバのホスト名をはじめ、メールアカウントのユーザ名やパスワード、転送先のアドレス、転送保持時間などを管理します。これらの情報は、一度読み込まれたあとは値が変化しないので、すべてfinal修飾子の付いたpublicフィールドにセットされます。またこれらのアカウント情報をもとに、メールサーバとの接続を表すJavaMailのSessionを作成します。

 なお、現状のUnreadMailは1アカウントのみサポートするため、UnreadSessionのインスタンスもJavaプログラム全体で1つしか生成されません。よって、複数のUnreadSessionを扱えるようにコードを拡張すれば、複数アカウントへの対応も可能になるはずです。

クラスUnreadMail

 一方、クラスUnreadMailは、このプログラムのmainメソッドを持つクラスです。Javaプログラムが起動すると、設定ファイル「mail.properties」を読み込み、UnreadSessionのインスタンスを1つ生成してフィールドusにセットします。つづいてメソッドstartを呼び出し、以下のようなループを実行します。

public void start() {
    while (true) {
        poll();
        try {
            Thread.sleep(us.pollInterval * 1000);
        } catch (InterruptedException e) {
        }
    }
}

 ご覧のとおり、このループではメソッドpollを実行したのち、スレッドを一定の時間スリープさせています。つまり、受信メールサーバに接続し、転送すべきメールがあるかチェックするというポーリングを繰り返します。

 では、1回のポーリングではどのような処理が実行されるのか、メソッドpollを見てみましょう。

メソッドpoll(前半部分)
private void poll() {
    Store store = null;
    Folder folder = null;
    try {
        // メールサーバに接続
        store = us.session.getStore();
        store.connect(us.storeHost, us.storeUser, us.storePassword);

        // フォルダをオープン
        folder = store.getDefaultFolder();
        folder = folder.getFolder("INBOX");
        folder.open(Folder.READ_ONLY);
        // フォルダ内をチェック
        checkFolder(folder);

        ……(続く)……

 メソッドpollの前半では、UnreadSessionに格納されたSessionを通じてメールサーバに接続し、受信フォルダをオープンしたのち、フォルダ内のメッセージをチェックするメソッドcheckFolderに同フォルダを渡します。このようにJavaMailでは、POP3とIMAPというプロトコルの違いにかかわらず同じAPIを利用できるのが特徴です。とはいえ、各メソッドの振る舞いや利用可能な機能はプロトコルによって異なるので注意が必要です。

例外発生しても踏ん張るコードを書く

 ここで、メソッドpollの後半部分に注目します。

メソッドpoll(後半部分)
    ……(続き)……

    } catch (Throwable th) {
        UnreadMail.log(th);

    } finally {
        // オープンしたリソースをクローズ
        try {
            folder.close(false);
        } catch (Throwable th) {
        }
        try {
            store.close();
        } catch (Throwable th) {
        }
    }
}

 このように、catch節にてThrowableを指定してあらゆる例外やエラーをキャッチし、メソッドlogを通じてログに記録します。こうしておけば、一時的なネットワーク障害などが発生した場合でもメソッドstartwhileループが止まることはなく、サービスの可用性が向上します。

 またfinally節では、「例外が発生してもしなくても実行するコード」を記述します。上記のコード例では、JavaMailのFolderStoreといったオープン済みリソースをクローズし、「例外が発生したためにリソースがオープンしたまま」という状況を回避します。finally節のこうした使い方は、JavaMailに限らず、JDBCや「java.io」パッケージなど「リソースをオープンして使用するAPI」でよく用いられます。

 さて、メソッドpollから呼び出されるメソッドcheckFolderでは、受信フォルダ内のすべてのメッセージを取得したのち、以下のforループ内で各メッセージをメソッドcheckMessageに渡しています。

for (int i = 0; i < msgs.length; i++) {
    try {
        checkMessage(msgs[i]);
    } catch (Throwable th) {
        UnreadMail.log(th);
    }
}

 ここで、ループの内部でThrowableをキャッチしていることに注意してください。このように記述することで、もしいずれか1つのメッセージの処理中に例外が発生したとしても、残りのメッセージの処理を継続できます。

条件チェックは「ガード節」で見やすくする

 つづいて、受信フォルダ内の個々のメッセージを処理するメソッドcheckMessageを見てみましょう。

private void checkMessage(final Message m) throws IOException,
    MessagingException {

    // Message-IDの取得
    final String[] msgIds = m.getHeader("Message-ID");
    final String msgId;
    if (msgIds != null) {
        msgId = msgIds[0];
    } else {
        msgId = m.getSentDate() + m.getFrom()[0].toString();
    }

    // 転送済みメッセージならreturn
    final boolean isForwarded = us.forwardedMsgs.contains(msgId);
    if (isForwarded) {
        return;
    }

    // SEENフラグ付きならreturn
    final Flags.Flag[] sf = m.getFlags().getSystemFlags();
    for (int i = 0; i < sf.length; i++) {
        if (sf[i] == Flags.Flag.SEEN) {
            return;
        }
    }

    // 転送保留時間に満たないならreturn
    final long received = m.getSentDate().getTime();
    final long now = (new Date()).getTime();
    final boolean isReceivedRecently =
        (now - received) < us.forwardAfter * 1000;
    if (isReceivedRecently) {
        return;
    }

    // メッセージを転送
    forwardMessage(m);
    us.forwardedMsgs.add(msgId);
}

 このコードで、以下のようなアルゴリズムが実装されています。

  1. メッセージのMessage-IDを取得する。
  2. UnreadSessionのフィールドforwardedMsgsが保持するMapを参照し、転送済みのメッセージならreturnする。
  3. メッセージにSEENフラグ(既読)が立っていればreturnする。
  4. メッセージの送信日時を取得し、転送保留時間が経過していなければreturnする。
  5. メソッドforwardMessageを呼び出し、メッセージを転送する。
  6. forwardedMsgsのMapにMessage-IDを追加する。

 構造化プログラミング的には、こうしたロジックは何段ものif-then-elseを入れ子にして記述し、コードの入り口と出口を1つにすべきでしょう。とはいえ、あまりに入れ子が重なるとコードが読みにくくなります。そうしたときは、ガード節(guarded clause)と呼ばれる以下のような書き方が便利です。

if (条件チェック1) { return; }
if (条件チェック2) { return; }
if (条件チェック3) { return; }

……(処理を実行)……

 このように記述すれば、どのような条件を満たしたときに処理が実行されるのかわかりやすくなります。

 以上のような条件チェックを経て、条件を満たしたメッセージをメソッドforwardMessageで転送します。

private void forwardMessage(final Message m) throws IOException,
    MessagingException {

    // メッセージ本文を取得
    String body = messageReader.readMessage(m);

    // 引用部分をカット
    body = us.quoteSnipper.snipQuote(body);

    // メッセージを転送
    final MimeMessage msg = new MimeMessage(us.session);
    msg.setRecipients(Message.RecipientType.TO, us.forwardTo);
    msg.setFrom(m.getFrom()[0]);
    msg.setSubject(m.getSubject(), "ISO-2022-JP");
    msg.setText(body, "ISO-2022-JP");
    Transport.send(msg);
    UnreadMail.log("forwarded: " + msg.getSubject());
}

 ここでは、まずフィールドmessageReaderにセットされたMimeMessageReaderのメソッドreadMessageにメッセージを渡し、メッセージ本文を抽出しています。例えばHTML形式のメールも、この段階でテキスト形式に変換されます。つづいてQuoteSnipperのメソッドsnipQuoteを呼び出し、本文中の引用部分をカットします。こうして加工されたメッセージが最後に転送される流れです。なお、メッセージ本文全体の長さはとくに制限していないので、例えばドコモ端末のメールアドレス向けには200文字以下にカットするといった機能拡張が考えられます。

クラスMimeMessageReaderとクラスQuoteSnipper

 クラスMimeMessageReaderのメソッドreadMessageでは、メッセージのMIMEタイプに基づいて以下のようなロジックを実行します。

  • 「text/plain」の場合、メッセージ本文をStringとして取得する。
  • 「text/html」の場合、HTMLタグを取り除いたStringを取得する。
  • 「multipart/mixed」の場合、各PartについてメソッドreadMessageを再帰呼び出し。
  • 「multipart/alternative」の場合、「text/plain」もしくは「text/html」のPartについてメソッドreadMessageを再帰呼び出し(ただし「text/plain」の内容を優先)。

 またクラスQuoteSnipperのメソッドsnipQuoteでは、引用記号「>」で始まる引用文について、設定された最大行数を超える部分をカットします。この引用パターンの抽出には「java.util.regex」パッケージの正規表現機能を利用しています。

まとめ

 以上、この記事ではJavaMailを利用した簡単なメール転送プログラムの例を紹介しました。

  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • 吉川和巳(ヨシカワカズミ)

    エンタープライズ分野やJava分野を中心に、ITアーキテクトおよびテクニカルライターとして活動する。大学在学中の4年間はオブジェクト指向言語Smalltalk-80やMVCモデルを研究。日本オラクルを経て、1998年に有限会社スティルハウスを設立。ITアーキテクトとして大手外資系IT企業でのiアプリ...

All contents copyright © 2005-2020 Shoeisha Co., Ltd. All rights reserved. ver.1.5