はじめに
「未読状態のまましばらく経過したメールを携帯に転送したい」――という筆者のちょっとした悩みを解決するために書いたのが今回のプログラムです。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を利用した簡単なメール転送プログラムの例を紹介しました。