はじめに
「未読状態のまましばらく経過したメールを携帯に転送したい」――という筆者のちょっとした悩みを解決するために書いたのが今回のプログラムです。POP対応のメール転送ソフトならば選択肢も豊富なため、こうしたニーズに応える製品を見つけるのは難しくないかもしれません。しかし、「IMAPで使いたい」「Linuxサーバに常駐させたい」という筆者の要件を満たすツールはすぐに見つかりそうになかったので、手っとり早くJavaMailで実装してみることにしました。
対象読者
Javaプログラミングの経験者を対象とします。
必要な環境
Windows XPおよびLinux上のJ2SE 1.4で動作を検証しました。コンパイル済みのクラスファイルがサンプルコードに付属しますので、そのまま動作可能です。ソースコードからコンパイルする場合は、AntやEclipseをご利用ください。
UnreadMailの使い方
今回のプログラム「UnreadMail」の使い方は簡単です。J2SEをインストールしたのち、サンプルコードのzipファイルを展開します。メール転送設定ファイル「mail.properties」をテキストエディタで開き、実行環境に合わせて各プロパティを設定します。なお、UnreadMailはPOP3とIMAP両方のメールサーバをサポートしています。
| プロパティ設定例 | 内容 |
| 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のディレクトリ構成を示します。
| ディレクトリ名/ファイル名 | 内容 |
| 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つのクラスの概要を示した図です。
これら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を見てみましょう。
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の後半部分に注目します。
……(続き)……
} catch (Throwable th) {
UnreadMail.log(th);
} finally {
// オープンしたリソースをクローズ
try {
folder.close(false);
} catch (Throwable th) {
}
try {
store.close();
} catch (Throwable th) {
}
}
}
このように、catch節にてThrowableを指定してあらゆる例外やエラーをキャッチし、メソッドlogを通じてログに記録します。こうしておけば、一時的なネットワーク障害などが発生した場合でもメソッドstartのwhileループが止まることはなく、サービスの可用性が向上します。
またfinally節では、「例外が発生してもしなくても実行するコード」を記述します。上記のコード例では、JavaMailのFolderやStoreといったオープン済みリソースをクローズし、「例外が発生したためにリソースがオープンしたまま」という状況を回避します。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); }
このコードで、以下のようなアルゴリズムが実装されています。
- メッセージの
Message-IDを取得する。 UnreadSessionのフィールドforwardedMsgsが保持するMapを参照し、転送済みのメッセージならreturnする。- メッセージに
SEENフラグ(既読)が立っていればreturnする。 - メッセージの送信日時を取得し、転送保留時間が経過していなければ
returnする。 - メソッド
forwardMessageを呼び出し、メッセージを転送する。 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を利用した簡単なメール転送プログラムの例を紹介しました。

