この章では、JavaMailのインストール、ファイル構成の説明とともに、添付されているdemoの動作確認を行い、それに沿った基本的なメイル送受信プログラムの作り方の説明を行います。
普通にJavaMailを使用する上ではこの章の内容(と4章の「日本語を扱う場合の注意点」)を理解すれば充分です。
JavaMailのページは、http://java.sun.com/products/から辿れる、http://java.sun.com/products/javamail/index.htmlにあります。
ここでは、JavaMail本体と、JavaMail API仕様、JavaMail
Service Provider Guide(Providerの作り方)、POP3
Providerとサードパーティ製Providerのダウンロードが可能です。
そして、このページにも記述されていますが、JavaMailを利用するためにはJAF(JavaBeansTM Activation Framework)が必要になります。こちらは、http://java.sun.com/beans/glasgow/jaf.htmlからダウンロードが可能です。
では、それぞれのインストールを行いましょう。
まず、http://java.sun.com/beans/glasgow/jaf.htmlから、JAF1.0.1(2000年12月時点での最新版)をダウンロードします。
zipまたはtar.gz形式のアーカイブを展開すると、リリースノートやAPIドキュメント、デモに加えてactivation.jarというファイルが見つかります。
このactivation.jarをJDK1.2以降の場合はjre/lib/extディレクトリにコピーすることでインストールは完了ということになります。
JDK1.1以前の場合は適宜CLASSPATHにこのjarファイルを指定することになります。簡単ですね。
さて、次はJavaMailのインストールですが、これもJAFと同様、ライブラリのjarファイルにクラスパスを通すだけなのでごく簡単です。
まず、http://java.sun.com/products/javamail/index.htmlからJavaMail1.2(2001年1月時点での最新版)をダウンロードします。
zipまたはtar.gz形式のアーカイブを展開すると、リリースノートやAPIドキュメント、デモに加えてmail.jarというファイルが見つかります。
JDK1.2以降の場合はこのmail.jarを、jre/lib/extディレクトリにコピーすることでインストールは完了ということになります。
JavaMail1.2とそれ以前のアーカイブ構成の変更について
JavaMail1.2より以前のリリースではmail.jarのみが提供されていて、JavaMail本体とSMTPProvider/IMAPProviderがこのアーカイブに含まれていました。POP3Providerは別途入手する必要があったわけです。1.2ではmail.jarにPOP3Providerも含まれるようになり、さらに、本体とそれぞれのプロバイダをそれぞれ個別にアーカイブしたmailapi.jar/smtp.jar/imap.jar/pop3.jarが含まれるようになりました。これによって、例えば送信しか行わない場合はmail.jarではなく、mailapi.jarとsmtp.jarのみクラスパスに指定するようにすることができるようになりました。Appletから利用する場合などにライブラリサイズを抑えることができます。
JavaMail1.1.3以前でPOP3Providerを利用する場合は、別途ダウンロードする必要がありました。JavaMailのようなライブラリに関しては古いバージョンを使わなければならない理由は薄いので今はあまり意味がなくなりましたが(ただし若干の非互換あり)、JavaMailのトップページからPOP3Providerがダウンロードできます。
http://java.sun.com/products/javamail/index.html
JavaMail1.1.3以前を利用している方への注意点として、POP3Provider1.1.1はJavaMail1.1.3以降を必須としています。これらのパッケージはちょうどこのバージョンに変わるタイミングでプロバイダとのインターフェイスについて過去のバージョンとの互換性がなくなる変更がされています。例えばJavaMail1.1.3に以前のPOP3Provider1.0を組み合わせても実行時にNoSuchMethodErrorが発生してしまいます。このケースに限らず、この例外を見た場合はバージョンの問題であることがほとんどですので覚えておきましょう。
ところでJavaMail1.2の若干の非互換とは何かといいますと、筆者がちょっと使った限りでは以下の二ヶ所でコンパイルエラーが出るようになっています。
これらはJavaMail1.1.3までは問題ありませんでしたが、JavaMail1.2ではambiguous
method(曖昧なメソッド呼び出し)としてコンパイルエラーとなります。
これは、JavaMail1.2でMimeMessage(MimeMessage
source)というコピーコンストラクタとMimeMessage#setRecipients(Message.RecipientType
type, String addresses)というコンビニエンスメソッドが追加されたため、nullを渡そうとした場合にそれぞれMimeMessage(Session
session)、MimeMessage#setRecipients(Message.RecipientType
type, Address[] addresses)と何れの呼び出しに解決すればよいかがコンパイラに判断できなくなったためです。
2の問題については、そもそもMimeMessage#setRecipients(Message.RecipientType.TO,
null)ではなくMessage#setRecipient(Message.RecipientType
type, Address addresses)、またはPart#setHeader()の方を使っていれば問題はありませんでしたが、筆者はたまたまsetRecipientsの方を使っていたのでエラーになってしまいました。
1の問題は仕方がないのでnew MimeMessage((Session)null);として静的な方をコンパイラに教えてあげなければなりません。
ちなみに、メソッドにnullを渡した時に同じパラメタ数で複数の参照型(プリミティブ型以外)を受け取れるようにオーバーロードされたメソッドがある場合、コンパイラはより具体的な型を受け取るメソッドの呼び出しとして解釈しようとします。上記の場合はオーバーロードされたメソッドのパラメタ型が参照関係になかったため、どちらがより具体的な型かを判断できなくなったということです。
では、インストールが終わったところで、動作確認を行いましょう。
1章でご紹介したSMTPTransportSample.javaでもかまいませんが、あれは使いにくいので、やはりdemoディレクトリにあるサンプルを実行するのがよいですね。
IMAP環境があれば話は早いのですが、そうでない人は多いことでしょう。というわけで、取り敢えずメッセージ送信のデモであるmsgsendsample.java(*)から試してみましょう。
*: なぜ小文字ばかりのクラス名なのかと思ってしまいましたが、これはどうやらコマンドラインから起動するクラスであるためそのようにしているようです。確かにunixでもMS-DOSコマンドでも小文字のみが基本ではありますが…。
msgsendsampleは1章で紹介したSMTPTransportSampleよりもさらに単純で、プログラム内に記述されている固定のメッセージを指定した宛て先に送信します。
まず、ターミナルやMS-DOSプロンプトからJavaMailを展開したディレクトリの下にあるdemoディレクトリに移動します。
次にmsgsendsample.javaをコンパイルします。
>javac msgsendsample.java
何も表示されなければコンパイルは成功です。
ここでエラーが表示された場合は、mail.jarとactivation.jarがクラスパスに含まれていないか、JDKが正しくインストールされていないかのいずれかでしょう。
では生成されたmsgsendsample.classを実行します。
このアプリケーションは、4つのコマンドラインパラメタを渡す必要があります。
>java msgsendsample 送信先メイルアドレス 送信元メイルアドレス SMTPサーバホスト名 true/false(DEBUG出力あり/なし)
SMTPサーバが稼働していればおそらくあっさり動作すると思います。
何の例外もなく実行が完了したら、送信先メイルアドレスからメイラ等で受信して送信されていることを確認してみましょう。
このとき、メッセージのヘッダ情報も確認してみてください。
受信結果は以下のようになっているはずです(From: / To: 等はもちろんあなたの指定したものになります)。
Received: from [192.168.0.11] (helo=shin-mebius) by linux.localdomain with esmtp (Exim 3.12 #1 (Debian)) id 13NUC5-00004L-00 for <shin@linux.localdomain>; Sat, 12 Aug 2000 14:50:53 +0900 Message-ID: <529641.966059492920.JavaMail.shin@shin-mebius> Date: Sat, 12 Aug 2000 14:51:32 +0900 (JST) From: shin@sk-jp.com To: shin@linux.localdomain Subject: JavaMail APIs Test Mime-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Transfer-Encoding: 7bit X-UIDL: 58fdb84e12ed1208db9968483cdc0035 This is a message body. Here's the second line.
注:これはメッセージの全文なのですが、このような結果を見る方法はメイラによって異なります。例えばOutLookExpressであればメッセージをデスクトップなどにドラッグ&ドロップして出来たファイルをエディッタで開くと言うようないわゆる「エクスポート機能」を用います。また、メイルサーバによってヘッダの内容が若干変わります。
この受信結果を見ながら、プログラムの方を覗いてみましょう。
まず、メッセージ送信のためのSessionオブジェクトを取得しています。
Properties props = new Properties();
props.put("mail.smtp.host", host);
:
Session session = Session.getDefaultInstance(props, null);
java.util.PropertiesをパラメタにしてSessionクラス(javax.mail.Session)のstaticメソッドによりSessionオブジェクトを取得しています。
Sessoinオブジェクトはメイルの送受信のためのサーバとのやりとり(=セッション)の一単位を表しています。解りにくいですねえ。
つまり、送信/受信のためのプロトコルや接続先、さらにはその送受信におけるプロトコル独自のパラメタや接続に関るユーザアカウント情報等が管理されるものと考えてください。
ご存知の方も多いでしょうが、これはfactory
methodと呼ばれるもので、new クラス名()とすることによるそのクラスのインスタンスの直接生成を避けて、呼び出されたメソッド側で適切なオブジェクトを生成するためによく使われる方法です。
また、複数の関連性のないクラスから同じオブジェクトの参照を得たい場合にも利用されます。
Sessionクラスは後者の使われ方となります。
Sessionクラス自身はfinalクラスであるため、上記のgetDefaultInstance()やもう一つのfactory
methodであるgetInstance()でサブクラスを返そうにもサブクラスを作ることができません。さらに言えば、現状ではgetInstance()というメソッドでは単にnew
Session()を呼び出しているだけです。getDefaultInstance()を使った場合のみ、同一VM(Java仮想マシン)上で唯一のSessionオブジェクトが得られます。
このDefaultSession(*1)は実はJavaMailで内部的に随所で使用されています。ほとんどのSessionオブジェクトを必要とする処理は、明示的にSessionオブジェクトが指定されていない場合に必要な情報をDefaultSessionから得ます。
例えばMimeMessageクラスはSessionをパラメタに取るコンストラクタがありますが、nullを渡して生成することができます。このような場合に、必要な時にDefaultSessionが用いられるのです。
アプリケーション内で接続先ホストやメイル送信を行う場合の送信者(From:)の内容などが固定の場合は、DefaultSessionを一度作成しておけば(最初にSession#getDefaultInstance()を呼び出した時に生成されます)、以降はSessionオブジェクトの意識無しにメイルの送受信が可能になります。
逆に、Session#getInstance()で得られるSessionオブジェクトを用いたい場合ですが、例えばメイラを作成する場合、複数のアカウント(複数の接続先)に接続したいことは良くあります。このような場合にDefaultSessionを用いていると、コネクションの度にDefaultSessionのProperties情報を書き換える必要があるのですが、並列接続を行おうとすると途端に破綻するのは想像に難くないですよね。
つまり、並列接続を考慮したクラスの場合はSession#getInstance()を用いる必要があります。
getInstance()メソッドはnew Session()と同じ(finalクラスなのでなおさら)なのに、コンストラクタをprivate宣言してnewできなくしているのは不可解ですが、将来final宣言が取れてサブクラスを返すことができるようになるのかもしれません:-P(*)。
なお、パラメタに渡すPropertiesに対して"mail.smtp.host"というキーを設定していますが、これは、後に示すTransport#send()がこの情報を必要とするためです。
*1:Session#getDefaultInstance()で取得できるSessionオブジェクトをこう呼ぶ事にします。
常に同じオブジェクトが返されます。Singletonと呼ばれる手法で、Javaでグローバル変数に相当するものを提供してしまう考えようによっては危険な手法です:-P。
*:普通の考え方であればそういうことを想像しますが、実際のところは単にインターフェイスを合わせようとしただけなのでしょう。
次に以下の行を見てみましょう。
Message msg = new MimeMessage(session);
msg.setFrom(new InternetAddress(from));
InternetAddress[] address = {new InternetAddress(args[0])};
msg.setRecipients(Message.RecipientType.TO, address);
msg.setSubject("JavaMail APIs Test");
msg.setSentDate(new Date());
msg.setText(msgText);
Messageオブジェクトとして、MimeMessageオブジェクトを生成しています。MimeMessageクラスはインターネットメイルを表すクラスであり、他のサービスプロバイダを用いない限りは常にこのクラスを用いることになります。変数msgの型をMessageとしていますが、これは、MimeMessage特有の機能を用いていないことを明示するためであり、日本語メッセージを生成する場合はMimeMessage msg = new MimeMessage(session)と書くことになります(→「型とオブジェクト」)。
さて、以降の行は生成されたmsgに対して送信元/送信先アドレス、題名、送信日時、本文を順番にセットしています。「見れば解る」と言われそうですね。そうです、JavaMailはすごく単純明快なAPIですので、Javaに詳しい方なら、APIドキュメントを見ただけでもすぐに使うことができます。
ここで設定された情報が、最初に示した結果に結びついているわけですね。なお、メッセージヘッダの詳細は2章を参照して下さい。
From(送信元)/Recipients(宛て先=受信者)としてInternetAddressクラスのオブジェクトを生成して渡しています。
これらはいずれも複数のアドレスを配列にしたものを設定することができますが(*)、このサンプルではFromは単一のInternetAddressオブジェクトを設定しています。
*:ご存知ない方もいらっしゃると思いますが送信元も複数にすることができます。これはRFC822の定義に従って実装されています。
どのようなときに使うかは2章を参照してください。
Transport.send(msg);
生成されたMessageオブジェクト(MimeMessageオブジェクト)を送信します。
このメソッドはコンビニエンスメソッドで、Messageオブジェクトに含まれる全ての送信先アドレスの種類から適切なTransportオブジェクトを取得して、そのTransportオブジェクトによって送信を行います。
JavaMailパッケージだけの場合、アドレスタイプは"rfc822"しか存在せず、従って、送信に使われるプロトコルも"smtp"しかあり得ませんので、SMTPTransportというオブジェクトによって送信が行われます。
このSMTPTransportというオブジェクトはSessionのプロパティの"mail.smtp.host"に関連付けられたSMTPサーバに対して送信を行うというわけです。
SMTPを使って送信するかぎりは特に意識せずにこのメソッドで送信を行ってもよいでしょう。ただ、実際にはMessage-ID:で使用されるドメインパートを正しくするために"mail.smtp.host"ではなく"mail.host"に接続先を設定するようにした方がよいです(それぞれの方法で送信されたメイルのヘッダを見比べれば解ります)。
"mail.smtp.host"のようなJavaMailが利用するキー名とその設定値/意味については、付録5に一覧があります。また、Message-ID:の問題については4章の「JavaMailにおけるMessage-ID:ヘッダの扱いについて」を参照してください。
Javaは強く型付けされた言語です。変数には全て型を指定しなければなりません。また、オブジェクトは全て型を持っています。
こんなことは今さらいわれなくても解っているといわれそうですが、皆さんはオブジェクトの静的な型と動的な型について正確に理解されておられるでしょうか?
Message msg = new MimeMessage(session);
この行における変数msgの静的な型はMessage型で、動的な型はMimeMessage型になるわけですね。
変数の型(=静的な型)は一つしかありませんが、動的な型(そこに格納されるオブジェクトの持つ型)は複数あります。
MimeMessageというクラスのオブジェクトは、以下の5つの型を持ちます。
つまり、自分自身の型と、それが継承している全スーパークラスの型、さらにそれが実装している全interfaceおよびそのスーパーinterfaceの型をすべて持つということです。なお、「型」や「インターフェイス」(界面と訳されたりします)の話をするときに、それがclassであるかinterfaceであるかは全く関係がありません。(*)
*:コラム「interfaceとインターフェイス」も参照してください。
Javaのプログラミングでは、オブジェクトを適切な(静的な)型で扱うことがとても重要です(これは同じく強く型付けされたオブジェクト指向言語であるC++でも言えますが、Javaの方が言語仕様上、より徹底されています)。
例えば、上記を以下のように記述することがよくあります。
Message msg = createMessage();
本文でもすこし触れたfactory methodですね。
変数msgに対しては、以降でMessage型の持つインターフェイスしか利用していません。
この場合、createMessage()メソッド内でnew
MimeMessage()を行うわけですが、このような作りにしておくと、createMessage内でMimeMessage以外のMessageクラスのサブクラスを生成して返すことができます。
架空の話ですが、適切な条件判断によってreturn
new GroupWareMessage();等としてインターネットメイルとは異なるグループウェアのメイルシステムで使われるメッセージを送信することも可能になります。また、何よりその変数の静的な型は、それに格納されるオブジェクトの持つ性質のうち、どの性質を利用するかを明快に表すことができます。
基本的に変数は使用するインターフェイスを含んだものの中で最も親階層の型(なるべくinterface型にするのが理想です)で定義するようにすることで、どのような使い方をするかを明示するようにし、メソッドの復帰値の型は、そのメソッドが返し得るものの中で最も具体的な型(将来の変更を見据えるならこれもinterfae型になっている方が柔軟です)を返すように宣言することで、これだけの役割を持つオブジェクトを返すことができるのだということを明示するようにします。
では次に受信のデモであるmsgshow.javaを実行してみましょう。ここではIMAPサーバ環境が必要です。IMAP環境ではない方は実際の動作はこの後ご紹介するPOP3での受信のページにて行ってください。
同じプログラムでIMAP4もPOP3も実行できるので、プログラムの説明は読んでおくことをお薦めします。
まずmsgshow.javaをコンパイルしましょう。
>javac msgshow.java
何も表示されなければコンパイルは成功です。
では生成されたmsgsendsample.classを実行します。
このアプリケーションは、たくさんの(^^)コマンドラインパラメタを指定できます。省略したオプションについてはシステムプロパティの情報から取得するか単に無視されます。
java msgshow [-T プロトコル名] -H サーバホスト名 -U ユーザ名 -P パスワード [-v] [-D] [-f メイルボックス名] [-L URL] [-p ポート] [-s] [取得するメッセージ番号]
いくつかのオプションの説明を以下に記します。
| -v | フォルダ内のメッセージ数を表示します。 |
| -D | デバッグモード。サーバとのやりとりなどのデバッグメッセージを表示します。 |
| -s | メッセージ構造を表示するモードです。本文を出力せず、各パートのツリー構造をインデントで表現します。 |
| メッセージ番号 | 指定しないとメッセージの一覧を表示します。指定するとそのメッセージを表示します。 |
| -L URL | JavaMail独自のURL形式で接続先等を一括指定します。この形式は、msgshow.javaだけで使われるものではなく、URLNameというAPIで定義されています。以下のようなURLを与えます。 プロトコル名://ユーザ名:パスワード@ホスト名:ポート名/メイルボックス名#ref
(refの部分は現状使われていません)従って、-Lの指定を行えば-T、-H、-U、-P、-p、-fの指定は不要(無視)になります。 これはRFC1738で定義されているURL一般形式なのですが、現在RFCにはpop://(RFC2384)、imap://(RFC2192)といったURLスキームが定義されています。古い記法ということになりますね。 |
では、以下のように指定して起動してみましょう。
>java msgshow -T imap -H サーバホスト名 -U ユーザ名 -P パスワード -v
以下のようにメッセージが表示されれば成功です。
もちろんあなたのアカウントのINBOXフォルダの内容によって内容は変化します。
Total messages = 2 New messages = 1 ------------------------------- -------------------------- MESSAGE #1: This is the message envelope --------------------------- FROM: =?ISO-2022-JP?B?GyRCJC0kTiQ3JD8bKEI=?= <shin@linux.localdomain> TO: shin@linux.localdomain SUBJECT: てすと SendDate: Sat Aug 12 14:24:59 JST 2000 FLAGS: \Seen X-Mailer: Datula version 1.50.45 for Windows -------------------------- MESSAGE #2: This is the message envelope --------------------------- FROM: shin <shin@linux.localdomain> TO: shin@linux.localdomain SUBJECT: てすとそのにです SendDate: Sat Aug 12 14:37:24 JST 2000 FLAGS: \Recent X-Mailer: Datula version 1.50.45 for Windows
次に、以下のように指定して起動してみましょう。今度はメッセージ番号を指定しています。
>java msgshow -T imap -H サーバホスト名 -U ユーザ名 -P パスワード -v 2
以下のようにメッセージが表示されると思います。
Total messages = 2 New messages = 0 ------------------------------- Getting message number: 2 This is the message envelope --------------------------- FROM: shin <shin@shin-linux.localdomain> TO: shin@shin-linux.localdomain SUBJECT: てすとそのにです SendDate: Sat Aug 12 14:37:24 JST 2000 FLAGS: X-Mailer: Datula version 1.50.45 for Windows CONTENT-TYPE: TEXT/PLAIN; charset=ISO-2022-JP This is plain text --------------------------- 新着メッセージです。 -- 木下 信@ひらつか
では、やはりこの受信結果を見ながら、プログラムの方を覗いてみましょう。
最初のコマンドラインオプション解析部分は飛ばして、以下の行を見ましょう。
// Get a Properties object Properties props = System.getProperties(); // Get a Session object Session session = Session.getDefaultInstance(props, null);
msgsendsample.javaと同じようにSessionオブジェクトの取得を行っています。ただし、今回はシステムプロパティをそのまま渡すだけとなっています。
この理由はmsgsendsample.javaの説明に書いたとおり、あちらはTransport#send()メソッドが"mail.smtp.host"を参照して接続先を決めていたのに対し、受信の場合はメソッドのパラメタで接続先などの情報を渡せるようになっているためです(*)。
なお、受信の場合もPropertiesに対して接続先情報などを指定することもできます。
利用者の状況に応じてどちらの方法も使えるようになっているということです。
*:送信の場合もstaticなTransport#send()メソッドではなくTransportオブジェクトを生成した場合は、connect()メソッドのパラメタにより接続先を指定することもできます。
その後に書かれているのは以下のような処理です。
(88)
// Get a Store object
Store store = null;
if (url != null) {
URLName urln = new URLName(url); (1)
store = session.getStore(urln);
store.connect();
} else {
if (protocol != null) (2)
store = session.getStore(protocol);
else
store = session.getStore();
// Connect
if (host != null || user != null || password != null)
store.connect(host, port, user, password);
else
store.connect();
}
プロトコルに応じたメッセージの受信は、メッセージの置き場所を抽象化したStoreオブジェクトと、その中のFolderオブジェクトによって行われます。
ここではSessionオブジェクトから指定されたプロトコルに対応するStoreオブジェクトの取得を行っています。
(1)は-LオプションによるURL形式で指定された場合の処理です。Session#getStore(URLName)を呼び出すことで、URL中のプロトコル部分に応じたStoreオブジェクトが返されます。ここではIMAPStoreクラスのオブジェクトが返されています(それは見えませんが)。
その後Store#connect()によって、サーバとの接続が行われます。ここではURLNameにより接続先情報やアカウント情報が既に渡されているため、パラメタ無しのconnect()を呼び出しています。
(2)はその他の場合ということになり、プロトコル指定があれば、それを指定してSession#getStore(String)を呼び出します。そうでない場合はSession#getStore()によって、Sessionのプロパティ(コンストラクタに渡されるもの)中の"mail.store.protocol"キーに対応するプロトコル名に従ったStoreオブジェクトが返されます。
接続に関しても、コマンドラインからホスト名/ユーザ名/パスワードが指定されていればStore#connect(String,
int, String, String)によって指定された接続先に接続を行います。指定されていない場合は、Store#connect()により、Sessionのプロパティ中の"mail.プロトコル名.host"、"mail.プロトコル名.user"等から接続先を取得して接続を試みます。このあたりの詳しい内部手順は、4章で説明しています。
Storeオブジェクトが取得できたら、次はそこから見たいフォルダを取り出しています。
(110)
Folder folder = store.getDefaultFolder();
if (folder == null) {
System.out.println("No default folder");
System.exit(1);
}
folder = folder.getFolder(mbox);
if (folder == null) {
System.out.println("Invalid folder");
System.exit(1);
}
// try to open read/write and if that fails try read-only
try {
folder.open(Folder.READ_WRITE);
} catch (MessagingException ex) {
folder.open(Folder.READ_ONLY);
}
Folderオブジェクトはユーザ毎に自由に作成できるフォルダを表します。IMAP4の場合はフォルダがサーバ上に存在します。POP3の場合は今ならほとんどのメイラ上でフォルダが作成できますね。これはクライアント側に作られています。
JavaMailでのFolderクラスの扱いはProviderに任されています。SunのIMAPProviderの場合はIMAPサーバ上に作成するフォルダと一対一に対応します(Store/FolderのAPI自体がIMAP4の規約を元に作られていますから当然なのですが)。
Storeにはルートフォルダを返すgetDefaultFolder()メソッドと、ルートフォルダ階層上のフォルダを返すgetFolder()メソッドがあります。
上記ではデフォルトフォルダを取り出して、そこからmboxで示されるフォルダを取り出していますが、Storeに対して直接getFolder()を呼び出しても結果は同じです。
デフォルトフォルダの概念はフォルダツリーのトップレベルであるということになるのですが、API上ではStore自体がデフォルトフォルダを表していると見なすことも可能です。
フォルダのルート階層から見たmbox変数で表されるフォルダを取得した後、そのフォルダをオープンします。
IMAPProviderにおけるフォルダのオープンとは、カレントフォルダを指定したフォルダに移動することになります。
(128)
int totalMessages = folder.getMessageCount();
if (totalMessages == 0) {
System.out.println("Empty folder");
folder.close(false);
store.close();
System.exit(1);
}
if (verbose) {
int newMessages = folder.getNewMessageCount();
System.out.println("Total messages = " + totalMessages);
System.out.println("New messages = " + newMessages);
System.out.println("-------------------------------");
}
目的のフォルダからメッセージを取り出します。
Folder#getMessageCount()によりそのフォルダ内のメッセージ数をチェックします。また、コマンドラインで-vが指定されている場合はFolder#getNewMessageCount()により、未読メッセージ数を取得して画面表示しています。
(144)
if (msgnum == -1) {
// Attributes & Flags for all messages ..
Message[] msgs = folder.getMessages();
// Use a suitable FetchProfile
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.ENVELOPE);
fp.add(FetchProfile.Item.FLAGS);
fp.add("X-Mailer");
folder.fetch(msgs, fp);
for (int i = 0; i < msgs.length; i++) {
System.out.println("--------------------------");
System.out.println("MESSAGE #" + (i + 1) + ":");
dumpEnvelope(msgs[i]);
// dumpPart(msgs[i]);
}
ここはコマンドラインでメッセージ番号が指定されなかった場合の処理です。全メッセージのサマリを出力しています。
まず、フォルダ内の全メッセージオブジェクトをFolder#getMessages()で取り出し、それらに対してfetch()を行っています。
Folder#fetch()は、メッセージ一覧を表示するためにヘッダの一部だけを取り出す場合などに使います。
ここではヘッダ中のディスティネーション/ソース/デイツフィールド(2章を参照)の主なものに加え、メッセージの状態(未読/既読等)、X-Mailerヘッダを取得します。その他のヘッダやボディは取り出しません。
その後、全メッセージのヘッダ情報をdumpEnvelope()により標準出力に表示します。dumpEnvelope()では、Addressクラスを用いてFrom:To:を表示し、Subject:Date:X-Mailer:および状態情報の表示を行っています。こちらの詳細説明はここでは省略します。
(161)
} else {
System.out.println("Getting message number: " + msgnum);
Message m = null;
try {
m = folder.getMessage(msgnum);
dumpPart(m);
} catch (IndexOutOfBoundsException iex) {
System.out.println("Message number out of range");
}
}
メッセージ番号が指定されていた場合は、そのメッセージを表示します。
Folder#getMessage(int)により、メッセージ番号を指定して一通のメッセージを取得し、それをdumpPart()で標準出力に表示しています。
dumpPart()はメッセージ中のマルチパートを再帰的に処理して、全てのパートを表示します。ここで行われているようなマルチパートの処理は後程自分で作るときに説明します。
(173)
folder.close(false);
store.close();
最後にオープンしたフォルダのクローズおよびStoreもクローズしてセッションを終了します。
個々の手順は難しくはないと思いますが送信と比べると少し複雑かもしれませんね。
デモプログラムの動作確認が終わったところで、JavaMailを使ったプログラムを作ってみようと思うのですが、その前に、プロトコル毎に異なるStoreとTransportのサブクラスをどのようにバインディングしているかをもう少し詳しく見てみます。
JavaMailではクラスパス上または<java.home>/libにあるjavamail.providersファイルとJavaMailのmail.jarに含まれるjavamail.default.providersファイルからプロトコル名とProviderのマッピングを得ます。
例えば、mail.jarに含まれるjavamail.default.providersファイルには以下のような行が含まれます。
protocol=smtp; type=transport; class=com.sun.mail.smtp.SMTPTransport; vendor=Sun Microsystems, Inc;
この一行によって、「プロトコル名"smtp"はTransportのサブクラスを提供するプロバイダであり、その実装はcom.sun.mail.smtp.SMTPTransportクラスである」という情報をJavaMailが取得しているわけです。
プロバイダを検索する様子を見るために、最初に試したデモプログラムであるmsgsendsample.javaをデバッグオプション付きで実行したときの出力を見てみます。この出力は説明のために、mail.jarではなく、それぞれのプロバイダのjarファイル(mailapi.jar/smtp.jar/imap.jar/pop3.jar)を個別にインストールした状態で得たものです(見やすいように改行を挿入しています)。
DEBUG: not loading system providers in <java.home>/lib
DEBUG: successfully loaded optional custom providers from URL:
jar:file:/C:/java/jdk13/jre/lib/ext/pop3.jar!/META-INF/javamail.providers
DEBUG: successfully loaded optional custom providers from URL:
jar:file:/C:/java/jdk13/jre/lib/ext/smtp.jar!/META-INF/javamail.providers
DEBUG: successfully loaded optional custom providers from URL:
jar:file:/C:/java/jdk13/jre/lib/ext/imap.jar!/META-INF/javamail.providers
DEBUG: can't load default providers file/META-INF/javamail.default.providers
DEBUG: Tables of loaded providers
DEBUG: Providers Listed By Class Name:
{com.sun.mail.smtp.SMTPTransport=javax.mail.Provider[TRANSPORT,smtp,com.sun.mail.smtp.SMTPTransport,Sun Microsystems, Inc],
com.sun.mail.imap.IMAPStore=javax.mail.Provider[STORE,imap,com.sun.mail.imap.IMAPStore,Sun Microsystems, Inc],
com.sun.mail.pop3.POP3Store=javax.mail.Provider[STORE,pop3,com.sun.mail.pop3.POP3Store,Sun Microsy stems, Inc]}
DEBUG: Providers Listed By Protocol:
{imap=javax.mail.Provider[STORE,imap,com.sun.mail.imap.IMAPStore,Sun Microsystems, Inc],
pop3=javax.mail.Provider[STORE,pop3,com.sun.mail.pop3.POP3Store,Sun Microsy stems, Inc],
smtp=javax.mail.Provider[TRANSPORT,smtp,com.sun.mail.smtp.SMTPTransport,Sun Microsystems, Inc]}
DEBUG: successfully loaded optional address map from URL:
jar:file:/C:/java/jdk13/jre/lib/ext/smtp.jar!/META-INF/javamail.address.map
DEBUG: getProvider() returning javax.mail.Provider[TRANSPORT,smtp,com.sun.mail.smtp.SMTPTransport,Sun Microsystems, Inc]
1行目の出力時、JavaMailはJDKインストールディレクトリのlibディレクトリから「システムプロバイダ」(javamail.providers)をロードしようとしますが、見つかりませんでした。
次にJavaMailはオプショナルプロバイダのロードを試みています。これはCLASSPATH上の/META-INF/javamail.providersというファイルを探していて、上記の出力では、pop3.jar/smtp.jar/imap.jarに含まれる/META-INF/javamail.providersを見つけています。
なお、JavaMail1.1.2以前のバージョンでは、この時見つけられるjavamail.providersは最初に見つけた一つしか対象になりませんので、他のサードパーティ製プロバイダがCLASSPATH上にあっても検出されないということがありました。現在はCLASSPATHに含まれる全ての/META-INF/javamail.providersを検出するようになっています。
次にJavaMailはデフォルトプロバイダのロードを行います。これはCLASSPATH上の/META-INF/javamail.default.providersを探しており、通常はmail.jarに含まれるものがロードされます。
こちらはシステム上に一つしかないものと考えられているため、CLASSPATH上で最初に見つけた/META-INF/javamail.default.providersが使われることになります。
上の例では、mail.jarをインストールしていませんので、/META-INF/javamail.default.providersは見つかりませんでした。
その後の行[Tables of loaded providers]ではロードに成功したProviderがダンプされています。
JavaMail内部ではクラス名からProvider情報(javax.mail.Providerクラスが表す情報です)を得るMapとプロトコル名からProvider情報を得るMapを有しているので、その両方がダンプされています。
その後の行はTransport#send()呼び出し時に行われており、send()メソッド内部で、MessageのReccipientsアドレスから"smtp"というプロトコル名を得て、Session#getProvider()メソッドが呼び出され、そこではプロトコル"smtp"に対応するProviderが返されています。
このようにjarファイル中または<java.home>/libの.providersファイルを実行時に参照して、クラスパス上に含まれるプロバイダを動的に検出します。
つまり、.providersファイルは特定のプロトコル名とそれを処理するための未知のクラス名を結びつける役割を持っているわけですね。
テキストメイルの送信を行うプログラムを作ってみましょう。demoのmsgsendsample.javaを見れば送信部分は非常に単純に記述できることが分かるので、ほとんどの人はこの本を見ずとも作れてしまうことでしょう。
msgsendsample.javaの焼き直しでは意味がないので、ここではもちろんメイル送信処理をカプセル化して単純なインターフェイスで扱えるものにします。
msgsendsample.javaではTransport#send()を用いてメッセージの送信を行っていました。Transport#send()はSessionオブジェクトの内容に依存するメソッドです。MimeMessageクラスのコンストラクタがSessionを受け取ることからも解りますが、Sessionオブジェクトから接続先情報を獲得して送信を行っています。
まず、Session#getDefaultInstance()を用いてデフォルトセッションを作成しておけば、実際の送信処理ではTransport#send()を用いることでSessionオブジェクトを意識せずにメイル送信を行うプログラムが作れるという例をご紹介します。
プログラムの開始時などに以下の処理を行っておきます。
Properties p = new Properties();
p.put("mail.from", "shin@example.com"); // Message-ID:に使用されるメイルアドレス
p.put("mail.smtp.host", "localhost"); // 接続先SMTPサーバ
Session.getdefaultInstance(p, null); // Sessionクラス内にDefaultSessionを生成。
実際の送信処理は以下のようになります。
MimeMessage message = new MimeMessage(Session.getDefaultInstance(null, null));
message.setFrom();
message.setRecipients(Message.RecipientType.TO, "shin@localhost");
message.setSubject("タイトル", "ISO-2022-JP");
message.setText("本文です", "ISO-2022-JP");
Transport.send(message);
これでメッセージの送信が行われます。MimeMessageオブジェクトを作って中身の設定を行った後Transport#send()に渡しているだけです。msgsendsample.javaと何ら変わらないようにみえますが、メッセージ送信の時までSessionオブジェクト覚えておく必要がない点と、msgsendsample.javaではパソコンから送信する場合などにMessage-ID:が不正になる場合がある問題が解消されている点が異なります(実際、先のmsgsendsample.javaの出力したメッセージはMessage-ID:が不正でした)。
実は、MimeMessage生成時にSessionを渡さず、nullを渡しても送信は正常に行われます。しかし、Message-ID:は正しいものがつかなくなります(*)。この点はJavaMailを少し修正するだけで改善されるのですが、まあ、上記のようにDefaultSessionをMimeMessageに渡すところをnullにできるかどうかの違いでしかないので気にしないでおきましょう(*1)。
*:不正なMessage-ID:については4章の「JavaMailにおけるMessage-ID:ヘッダの扱いについて」を参照して下さい。
*1:一応new MimeMessage((Session)null)が仕様として許されるのかについて(実装上は許されることになっているが完全でないことも含めて)、javamail@sun.comに指摘してみたのですが、反応はありませんでした。仕様策定者側は想定していなかった(禁止していた)ようで、JavaMail1.2ではMimeMessage自身をパラメタに取るコピーコンストラクタが追加されていますが、これによってnew
MimeMessage(null)とした場合にコンパイルが通らなくなっています(ambiguous
methodコンパイルエラー)。実装者は多少意識していたようでsessionがnullの場合というのも考慮した作りになっているのですが、それが完全ではないという状況ですね。
なお、一度生成したDefaultSessionは上記例のようにSession.getDefaultInstance(null,null)によってプログラム上のどこからでも取得することができます。
この方法はDefaultSessionのPropertiesがプログラム実行中に書き換わるようなことがない場合に使います。
接続先("mail.smtp.host")やenvelope-from("mail.smtp.from")、From:("mail.from")等を実行中に書き換えるような場合で、且つマルチスレッドの場合にはDefaultSessionを用いないようにしましょう。DefaultSessionはプログラム(プロセス)中で唯一のものですので、同時に操作するスレッドが一つであれば積極的に利用して下さい。
例えば、以下のような簡単なクラスを用意しておくことで、一通のメイル送信を手軽に利用できるようになります。
// [VerySimpleSender.java]
import java.util.Date;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.InternetAddress;
public class VerySimpleSender {
static {
java.util.Properties p = new java.util.Properties();
p.put("mail.from", "shin@example.com"); // Message-ID:に使用されるメイルアドレス
p.put("mail.smtp.host", "localhost"); // 接続先SMTPサーバ
Session.getdefaultInstance(p, null); // Sessionクラス内にDefaultSessionを生成。
}
public static void send(String subject, String body,
String to)
throws MessagingException {
MimeMessage msg = new MimeMessage(Session.getDefaultInstance(p, null));
msg.setFrom();
msg.addRecipients(MimeMessage.RecipientType.TO, to);
msg.setSubject(subject, "ISO-2022-JP");
msg.setText(body, "ISO-2022-JP");
send(msg);
}
public static void send(String subject, String body,
String to, String from)
throws MessagingException {
MimeMessage msg = new MimeMessage(Session.getDefaultInstance(p, null));
msg.addFrom(InternetAddress.parse(from, true));
msg.addRecipients(MimeMessage.RecipientType.TO, to);
msg.setSubject(subject, "ISO-2022-JP");
msg.setText(body, "ISO-2022-JP");
send(msg);
}
public static void send(MimeMessage msg) throws MessagingException {
msg.setSentDate(new Date());
msg.setHeader("X-Mailer", "JavaMail Sender"); // 適当な名前にしてください
Transport.send(msg);
}
}
取り敢えず送れればいいやという時は、即席でこのようなコードを書けばまっとうなメッセージを送信することができます。
さて、送信に関してはTransport#send()が接続->送信->切断まで行ってくれるのでこちらとしてはほとんど何も考えることがありません。メッセージ内容を作成することに専念できます。メッセージ内容の加工については以降で説明していきますが、その前にここでは接続->送信->切断インターフェイスをもったライブラリを作成します。
Transport#send()があるのになぜそんなものを作る必要があるの?VerySimpleSenderで十分やんっと言う意見があると思いますが、同じSMTPサーバに対して連続的にメッセージを送信する場合、一通毎にconnect->send->disconnectを繰り返すTransport#send()では使い勝手が悪く、Transport#send()を用いない方法を使用したい場面は多くあります。
それを実現するためにはTransportオブジェクトを取得してsendMessageメソッドを用いて送信を行うことになります。このあたりはdemoのtransport.javaがそちらの手法でメイル送信を行うサンプルとなっています。
では、一回のSMTPセッションで複数のメッセージを送信するためにconnect/send/disconnectを別々に定義しましょう。
public class SimpleSender {
public void connect(String host, int port) {
}
public void disconnect() {
}
}
SMTPでの送信の場合はユーザ認証を必要としないため、送信先(ReceiverMTA)ホストのみを指定します。SMTP
AUTHを使用する場合はユーザ名/パスワードも使用しますので、それをパラメタに取るconnect()も定義してもよいでしょう。
connect()はTracsport#connect()(実際にはStore/Transport共通のスーパークラスであるServiceクラスのメソッド)によって実現できます。APIドキュメントとにらめっこしながらこの部分を実装してみましょう。
SMTPしか考えないなら不要なものではありますが、JavaMailは送信プロトコルにも依存しないわけですから、プロトコルをコンストラクタで与えるようにします。しかし、ほとんどがSMTPなので、パラメタ無しのコンストラクタでSMTPプロトコルを利用するようにしておきます。
また、TransportクラスのAPIドキュメントから辿って、そのスーパークラスであるServiceクラスのAPIを確認すると、Service#connect()メソッドには3種類あります。まず、host/port/user/passの4つのパラメタを受けるものがあり、それを呼び出すようになっている、パラメタのないものとhost/user/passの3つを受けつけるものです。それぞれのメソッドは省略された情報をSessionに渡したPropertiesオブジェクトや、プロバイダのデフォルト値から補完するようになっています。
ここでは、以下のようにconnect()を定義してみました。
public class SimpleSender {
private Session session;
private Transport transport;
public SimpleSender() throws NoSuchProviderException {
this("smtp");
}
public SimpleSender(String protocol) throws NoSuchProviderException {
session = Session.getInstance(System.getProperties(), null);
transport = session.getTransport(protocol);
}
public synchronized void connect(String host) throws MessagingException {
connect(host, -1, null, null);
}
public void connect(String host, int port) throws MessagingException {
connect(host, port, null, null);
}
public synchronized void connect(String host,
int port,
String user,
String pass) throws MessagingException {
transport.connect(host, port, user, pass);
}
public synchronized void disconnect() {
try {
transport.close();
} catch (MessagingException e) {}
}
}
connect/disconnectを行うためのTransportオブジェクトを保持するようにしています。またそれを得るために必要なSessionオブジェクトも保持しています。送信にあたってはSessionオブジェクトを覚えておく必要はないのですが、このようにした理由は後程説明します。
Sessionオブジェクトを得る時のPropertiesには空のPropertiesオブジェクトを渡しています。これは、実際のところconnect()等においてパラメタを渡さない場合のデフォルト情報を参照するために用いられます。常に明示的にパラメタを与えているかぎりは変にシステムプロパティ等を渡さないほうが良いでしょう。
だからといってnullを渡すわけにもいかないので(内部で"mail.debug"等を参照しようとするため)空のPropertiesオブジェクトを渡しておきます。第二パラメタのAuthenticatorに関してはnullでも構いません(JavaMail1.2では第二パラメタを持たないSession#getInstance()メソッドが提供されています)。
このようにすると、このクラス以外からSessionオブジェクト内のPropertiesの操作ができなくなるわけですので、柔軟性が落ちますがカプセル化は強固なものになります(System.getProperty()としてしまうと外部からいつでもその内容の変更が可能になってしまいます)。
なお、Transport#close()を呼び出すメソッドをdisconnect()という名称にしたのは、後に記す受信の章では、Folderに対するopen/closeとStoreに対するconnect/closeでcloseという同じ単語が用いられていることから、connect/disconnectという名称にしたのですが、こちらもそれにあわせたというところです。
Sessionオブジェクトを隠蔽したことにより、Messageオブジェクトの生成を行うメソッドを提供する必要が出てきます。
public MimeMessage createMessage() {
return new MimeMessage(session);
}
このようにしておいて、使用する側は常にこのメソッドを用いてメッセージオブジェクトを生成するようにしておけば、MimeMessageのサブクラスを返すようにする事が容易になりますね。
では続きを作りましょう。といっても後はsendメソッドを定義するだけです。sendメソッドにはさまざまな状況で使いやすいように便利そうなインターフェイスをいくつか用意することにします。
public void send(MimeMessage message) {
}
public void send(MimeMessage message, Address[] envelopeTo) {
}
public void send(String subject, String body,
String to, String from) {
}
public void send(MimeMessage msg,
String envelopeTo,
String envelopeFrom) {
}
最初の2つはTansportクラスに定義されているものと同じようなインターフェイスです。
3番目は単純なテキストメイルの送信に利用するコンビニエンスメソッドです。
4番目は指定されたメッセージについて、メッセージヘッダのTo:From:を無視して、指定されたenvelope-to/envelope-fromを用いて送信を行うものです。
まず、最初の二つを記述してみます。Tansportクラスに対応するメソッドsendMessage()があり、それを呼び出すコードです。
public void send(MimeMessage msg) throws MessagingException {
send(msg, msg.getAllRecipients());
}
public void send(MimeMessage msg, Address[] recipients)
throws MessagingException {
msg.setSentDate(new Date());
// Message-ID:にFromアドレスを使用します。
session.getProperties().put("mail.from",
((InternetAddress)msg.getFrom()[0]).getAddress());
msg.saveChanges();
transport.sendMessage(msg, envelopeTo);
}
渡されたメッセージに対して、送信日時を設定して、Transport#sendMessage()に処理を委譲するという処理なのですが、ここでちょっとトリッキーな処理が必要になってしまいます。SessionのPropertiesの"mail.from"に対して送信者のメイルアドレスを設定してMessage#saveChanges()を呼び出す処理です。後になってこのようにしなければならないことがわかったのですが、この処理を含めないと送信するメッセージのMessage-ID:が正しくならないケースが多くなってしまいます。詳細は4章の「JavaMailにおけるMessage-ID:ヘッダの扱いについて」を参照して下さい。
次にsend(String subject, String body, String to, String from)ですが、これは先のVerySimpleSenderの中でほぼ同じものを示しました。
public void send(String subject, String body,
String to, String from)
throws MessagingException {
MimeMessage msg = createMessage();
msg.addFrom(InternetAddress.parse(from, true));
msg.setRecipients(MimeMessage.RecipientType.TO,
InternetAddress.parse(to, true));
msg.setHeader("X-Mailer", "JavaMail Sender"); // 適当な名前にしてください
msg.setSubject(subject, "ISO-2022-JP");
msg.setText(body, "ISO-2022-JP");
send(msg);
}
Messageオブジェクトを生成する人はみんな上記のようなコードを書くのでしょうか?若干面倒な気はしますので、ここは決まり切ったヘッダを設定するためのツールメソッドを用意することにして、sendメソッドも修正します。
public static void setHeaders(MimeMessage msg, String to, String from)
throws MessagingException, AddressException {
msg.setFrom(null);
msg.addFrom(InternetAddress.parse(from, true));
msg.setRecipients(Message.RecipientType.TO,
InternetAddress.parse(to, true));
msg.setHeader("X-Mailer", "JavaMail Sender"); // 適当な名前にしてください
}
public void send(String subject, String body,
String to, String from)
throws MessagingException {
MimeMessage msg = createMessage();
setHeaders(msg, to, from);
msg.setSubject(subject, "ISO-2022-JP");
msg.setText(body, "ISO-2022-JP");
send(msg);
}
setHeaders()メソッドはSimpleSenderオブジェクトの属性に全く依存しないため、staticとしています。
このようなメソッドをstaticとするか否かは意見が分かれるところであり、慎重に判断すべきところです。
本来ならこのようなメソッドはMimeMessageのサブクラスで定義すべきものですね。この修正は後程行いますが、ここではSenderクラスにもVerySimpleSenderと同等の簡単送信を行うstaticメソッドを作成する予定であること、VerySimpleSenderのようなものを使って送信する人でも利用できることを理由にstaticとしておきます。
では最後の一つを作成します。これはメイルをちょっと突っ込んだ使い方をする人にとっては非常に重要なものです。
envelope-fromは配送エラーの報告先となります。要するにFrom:やReply-To:に記述されたアドレスにエラーメイルが届いてしまってはこまるという場合にここにエラーメイルの宛て先を指定するのです。メイリングリストなどでは必須の機能になります。
envelope-toを指定することについてはメイルヘッダのbcc:に対してアドレスを指定するのと同様に、同時に配信された他のアドレスを受信者側の宛て先に見えなくする効果があります。envelope-toを明示的に指定する場合はさらに、To:やcc:に指定したアドレスも無視されますので、bcc:を用いる場合のようにTo:にダミーのアドレスを記述する必要もありません(*)。
*:実際には記述すべきです。ここで言っているのはTo:に記述されたアドレスにそのメイルを送らないようにもできるということです。
では、その実装を記述します。
public void send(MimeMessage msg,
String envelopeTo,
String envelopeFrom)
throws MessagingException, AddressException {
session.getProperties().put("mail.smtp.from", envelopeFrom);
send(msg, InternetAddress.parse(envelopeTo, true));
session.getProperties().remove("mail.smtp.from");
}
ここで、最初にSessionオブジェクトをインスタンス変数に保持していた理由がやっと解ります。
JavaMailではTransport#send()やTransport#sendMessage()を呼び出した時に、通常は渡されたメッセージオブジェクトのFrom:ヘッダに書かれた先頭のアドレスをenvelope-fromとして扱います。詳しくは4章のAPI解説を見ていただけば解りますが、Sessionオブジェクト中のPropertiesのエントリ"mail.smtp.from"が存在する場合は、メッセージオブジェクトのFrom:ヘッダではなくそちらがenvelope-fromとして使用されるのです。ここでは今送信しようとしているメイルのenvelope-fromに対してのみ変更を行いたいのでTransport#sendmessage()呼び出し前にProperties#put()を行い、呼び出し後にProperties#remove()によりPropertiesのエントリから削除しています。
JavaMailは1.1.2から1.1.3に変わる時に、このenvelope-fromを指定するためのPropertiesのキー名が変更されています。1.1.2以前では"mail.smtp.user"に対してenvelope-fromを指定する事になっていましたが、1.1.3で"mail.smtp.from"に変更され、さらにJavaMail1.2になると"mail.smtp.user"は異なる意味(SMTP
AUTHのユーザ名)で扱われるようになってしまっていますので注意が必要です。
なお、Session#getProperties()を呼び出したいためにSessionオブジェクトをインスタンス変数として保持すると記述していますが、Session#getInstance()に渡すPropertiesオブジェクトを保持しておくようにしても一向に構いません。
では、テストを行います。こういったライブラリクラスをテストする最も単純で扱いやすい方法は、public static void main()メソッドを用意して、このクラス自身を起動可能なアプリケーションとすることです。こんなものを用意してみました。
public static void main(String[] args) throws Exception {
String host = args[0];
String to = args[1];
String from = args[2];
String subject = "てすと";
String body = "日本語メッセージのてすとです。";
SimpleSender s = new SimpleSender();
MimeMessage msg = s.createMessage();
s.setHeaders(msg, to, from);
msg.setSubject(subject, "ISO-2022-JP");
msg.setText(body, "ISO-2022-JP");
s.connect(host);
s.send(msg);
s.disconnect();
}
コンパイル/実行は以下のように行います。JavaMailとJAFのjarファイルはjre/lib/ext/に置いてありますね?置きたくない場合は-cpオプションでjarファイルとカレントディレクトリをCLASSPATHに指定する必要があります。
>javac SimpleSender.java java SimpleSender SMTPサーバホスト名 送信先メイルアドレス 送信元メイルアドレス
VerySimpleSenderと同程度にライブラリ呼び出し部は単純になったと思います。MimeMessageオブジェクトにTo:/From:/Subject:/本文を設定し、コマンドラインで指定されたSMTPサーバに対してそのメッセージを送信するというものです。皆さんが試される場合はもちろんTo:/From:および接続ホスト名は皆さんの環境に会わせる必要があります。
他のsend()メソッドもテストするなら、main()メソッドを適当に書き換えて実行すればよいです。
それはそうと、このmainメソッドのインターフェイスは単純で、外部のクラスからもこのような呼び出し方ができてもよいと思います。
少し改良してstaticなsendメソッドも用意して、mainメソッドも修正してみます。
public static void send(String host,
String to,
String from,
String subject,
String body)
throws MessagingException, AddressException {
SimpleSender s = new SimpleSender();
MimeMessage msg = s.createMessage();
s.setHeaders(msg, to, from);
msg.setSubject(subject, "ISO-2022-JP");
msg.setText(body, "ISO-2022-JP");
s.connect(host);
s.send(msg);
s.disconnect();
}
/** テスト用起動メソッドです。 */
public static void main(String[] args) throws Exception {
SimpleSender.send(
"localhost",
"postmaster@localhost",
"shin@localhost",
"てすと",
"日本語メッセージのてすとです。");
}
新しいmain()メソッドを見ての通り、テキストのメッセージであればSimpleSender.send()で送信できるようになりました。
これで、一通のメッセージを送信する手軽な手段と、コネクションを確立して複数のメッセージを送信する手段の両方が利用できるようになったわけです。
では、ここで完成したSimpleSender.java全体を載せておきます。マルチスレッドでの実行に耐えられるようにいくつかのメソッドにsynchronizedモディファイアを付加し、パッケージ指定も追加しています(*)。また、プロバイダ固有の機能を利用できるようにする例として、SMTPProviderの場合のSMTP AUTHの使用の有無を設定するuseSMTPAuth(boolean)というメソッドを追加しています。このように、プロバイダ独自の機能についても対応するメソッドを追加するようにすることで、Propertiesのキー文字列を使用する箇所が散在して混乱することを防ぐことができます。
*:筆者自身の利用しているコードなのでpackageを指定していますが、これはこのコードの使用をこのパッケージに制限するものではありません。
// [SimpleSender.java]
package com.sk_jp.mail;
import java.util.Properties;
import java.util.Date;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.Address;
import javax.mail.NoSuchProviderException;
import javax.mail.MessagingException;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
public class SimpleSender {
private Session session;
private Transport transport;
public SimpleSender() throws NoSuchProviderException {
this("smtp");
}
public SimpleSender(String protocol) throws NoSuchProviderException {
session = Session.getInstance(new Properties(), null);
// "mail.debug"のみシステムプロパティの情報を利用する事にします。
session.setDebug(Boolean.getBoolean("mail.debug"));
transport = session.getTransport(protocol);
}
public synchronized void connect(String host) throws MessagingException {
connect(host, -1, null, null);
}
public void connect(String host, int port) throws MessagingException {
connect(host, port, null, null);
}
public synchronized void connect(String host,
int port,
String user,
String pass) throws MessagingException {
transport.connect(host, port, user, pass);
}
public synchronized void disconnect() {
try {
transport.close();
} catch (MessagingException e) {}
}
public void useSMTPAuth(boolean auth) {
session.getProperties().put("mail.smtp.auth", String.valueOf(auth));
}
public MimeMessage createMessage() {
return new MimeMessage(session);
}
public static void setHeaders(MimeMessage msg, String to, String from)
throws MessagingException, AddressException {
msg.setFrom(null);
msg.addFrom(InternetAddress.parse(from, true));
msg.setRecipients(MimeMessage.RecipientType.TO,
InternetAddress.parse(to, true));
msg.setHeader("X-Mailer", "JavaMail Sender");
}
public void send(MimeMessage msg)
throws MessagingException {
send(msg, msg.getAllRecipients());
}
public synchronized void send(MimeMessage msg, Address[] envelopeTo)
throws MessagingException {
msg.setSentDate(new Date());
// Message-ID:にFromアドレスを使用します。
session.getProperties().put("mail.from",
((InternetAddress)msg.getFrom()[0]).getAddress());
msg.saveChanges();
transport.sendMessage(msg, envelopeTo);
}
public synchronized void send(MimeMessage msg,
String envelopeTo,
String envelopeFrom)
throws MessagingException, AddressException {
session.getProperties().put("mail.smtp.from", envelopeFrom);
send(msg, InternetAddress.parse(envelopeTo, true));
session.getProperties().remove("mail.smtp.from");
}
public void send(String to, String from,
String subject, String body)
throws MessagingException, AddressException {
MimeMessage msg = createMessage();
setHeaders(msg, to, from);
msg.setSubject(subject, "ISO-2022-JP");
msg.setText(body, "ISO-2022-JP");
send(msg);
}
protected void finalize() throws Throwable {
disconnect();
}
public static void send(String host,
String to,
String from,
String subject,
String body)
throws MessagingException, AddressException {
SimpleSender s = new SimpleSender();
MimeMessage msg = s.createMessage();
s.setHeaders(msg, to, from);
msg.setSubject(subject, "ISO-2022-JP");
msg.setText(body, "ISO-2022-JP");
s.connect(host);
s.send(msg);
s.disconnect();
}
public static void main(String[] args) throws Exception {
SimpleSender.send(
"localhost",
"postmaster@localhost",
"shin@localhost",
"てすと",
"日本語メッセージのてすとです。");
}
}
パッケージ指定が追加されていますので、実行する時はFQCN(Fully Qualified Class Name:パッケージ名を含んだ完全なクラス名)で指定します。
>java com.sk_jp.mail.SimpleSender SMTPサーバホスト名 送信先メイルアドレス 送信元メイルアドレス
では次にマルチパートメッセージを送信するプログラムを作ってみましょう。とはいっても送信部は前項のSimpleSenderがそのまま利用できるため、マルチパートメッセージを作成する部分のみ新たに作成すればよいことになります。
マルチパートメッセージとはプレインテキスト以外のコンテンツが含まれたメッセージと解釈してよいでしょう。字面そのままですが複数のパート(テキストと添付ファイル1、添付ファイル2なら3つのパート)が含まれたメッセージというわけです(*)。マルチパート構成のメッセージオブジェクトを作成して送信することで、音声や動画をメッセージに埋めこんで(添付ファイルとしてではなく)送信することも可能になります。
マルチパートメッセージそのものの詳しい説明は2章で行っています。パート/ボディといった用語の意味がわからない場合は2章を見てみてください。
ヘッダの設定部分についてはプレインテキストの送信で行ったものとほぼ同様ですので詳しい説明は省略して、ここでは、マルチパートメッセージがJavaMailでどのような構造としてモデル化されているかを図示して、その処理方法をご紹介していきます。
まずは以下の図をご覧ください。

MimeMessage(およびMessage)オブジェクトはjavax.mail.Part interfaceをimplementsしているので、図中のPartはMimeMessageを表すと考えて下さい。
まず、MimeMessageにはsetContent()等によってボディを設定することができます。
ボディにはStringやImageなどのJAFが解釈可能なオブジェクトが設定できるほか、MultipartオブジェクトとMimeMessageオブジェクトを設定することができます。
後者はそれぞれMIMEタイプのmultipart/*とmessage/*に対応します。
2章で説明した通り、multipartメディアタイプの場合、内部に任意個のパートを保持することができます。この構造を表すのがMimeMultipartとMimeBodyPartオブジェクトです(*2)。MimeMultipartオブジェクトは任意個のMimeBodyPartを保持するコンテナとしての役割のみを持ちます。MimeMultipartオブジェクト自身は出力されるメッセージからその存在を読み取ることはできません。
そして、MimeBodyPartはMimeMessageと同様にjavax.mail.Part
interfaceをimplementsしています。これが重要です。
即ち、図中の塗りつぶされたオブジェクトは全てPartであり、「ボディを保持することができる役割」を持っています。
これらは当然のごとく皆setContent()メソッドを持っており、それを使って設定できるのは「ボディ」つまり、図中の「パートのボディ」で示されるオブジェクトです。
この関係を実際のメッセージの構造にあてはめると、java.awt.Container/java.awt.ComponentによるCompositeと呼ばれる構造に似たものになります(*3)。以降で具体的な例を挙げますが、マルチパートメッセージは再帰的なツリー構造で表されます。
*:実際にはプレインテキストのみのマルチパートメッセージもあります。また、プレインテキスト以外しか存在しないシングルパートメッセージもあり得ます。ただ、プレインテキストパートを全く含まないメッセージをデフォルトで送信するようなMUAはタコです。そのMIMEタイプを解釈できないMUAのことも考え、multipart/alternativeを用いてテキストパートも含める必要があります。詳しくは2章を参照して下さい。
*2:MultipartはMultiPartではないことに注意しましょう。
*3:Composite パターンそのものとは若干、そして大きな違いがあります。Composite パターンではCompositeオブジェクトはComponentのサブクラスですが、Compositeに相当するMultipartはComponentに相当するPartを継承していません。JavaMailのクラス構成でのMultipartオブジェクトは、それが設定されるPartにCompositeとしての機能を付加するものに過ぎないと理解すればよいでしょう。なお、Composite パターンについては「オブジェクト指向における再利用のためのデザインパターン」(Erich Gamma他著)を参照して下さい。
添付ファイルとは、2章で説明したようにContent-Type: multipart/mixedなメッセージに対して、ファイルを表すパート(Content-Disposition: attachment; filename="foo.bar"を含むパート)が付加されたものになります。
添付ファイル付きメッセージの構造を見てみましょう。

マルチパートメッセージは、メッセージ本体のContent-Typeでmultipartメディアタイプを指定し、boundaryパラメタに指定された文字列で各パートの区切りを示します。boundaryで区切られた各パートはそれぞれが、空行によってヘッダとボディに分けられます。このような構造をJavaMailでは図中の右側に示したようなオブジェクト構造で表します。
ツリー構造のルートにあたるのはMimeMessageオブジェクトです。これはPartでもあることは先に示していましたね。PartであるMimeMessageオブジェクトのボディには、MimeMultipartオブジェクトが設定されます。MimeMultipartオブジェクトは複数のパートを束ねるためのコンテナです。
MimeMultipartオブジェクトには二つのMimeBodyPartオブジェクトが設定されます。それぞれのMimeBodyPartはtext/plain、application/octed-streamのContent-Typeヘッダを持ちます。また、添付ファイルの場合はそのファイル名が設定されている必要があります。これはContent-Disposition:ヘッダのfilenameパラメタに相当します。
そして、それぞれのMimeBodyPartオブジェクトのボディとして、StringオブジェクトやFile内容を表すInputStreamオブジェクトが設定されます(厳密にはこれらはDataHandlerというクラスにラッピングされて保持されるています。これについてはJAFの説明を参照下さい)。
JavaMailではContent-Disposition:ヘッダという意識はあまりしなくても添付ファイル付きメッセージを生成できます。
以下のコードを見て下さい。
// [SendAttachmentSample.java]
// 添付ファイル付きメッセージの送信を行うサンプルプログラム
import java.io.IOException;
import javax.mail.MessagingException;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimeMessage;
import javax.activation.DataHandler;
import javax.activation.FileDataSource;
import com.sk_jp.mail.SimpleSender;
public class SendAttachmentSample {
public static void main(String[] args) throws Exception {
String host = args[0];
String to = args[1];
String from = args[2];
String subject = "テストメッセージです";
SimpleSender sender = new SimpleSender();
MimeMessage msg = sender.createMessage();
///////////////////////////////////////////////////// メッセージの編集(1)
SimpleSender.setHeaders(msg, to, from);
msg.setSubject(subject, "ISO-2022-JP");
msg.setContent(createAttachmentPart());
msg.saveChanges();
/////////////////////////////////////////////////////
sender.connect(host);
sender.send(msg);
sender.disconnect();
}
public static MimeMultipart createAttachmentPart()
throws MessagingException, IOException {
String text = "テキストファイルを添付しています。";
String filename = "test.txt";
MimeBodyPart textPart = new MimeBodyPart();
textPart.setText(text, "ISO-2022-JP");
MimeBodyPart filePart = new MimeBodyPart();
filePart.setDataHandler(new DataHandler(new FileDataSource(filename)));
// デフォルトではContent-Disposition:はattachmentになります。
filePart.setFileName(filename);
// デフォルトはmultipart/mixedになります。
MimeMultipart mp = new MimeMultipart();
mp.addBodyPart(textPart);
mp.addBodyPart(filePart);
return mp;
}
}
main()メソッドでは先程作成したSimpleSenderクラスを用いていますので、送信処理自体は特に問題ないでしょう。(1)の範囲がMessageオブジェクトの内容を編集している部分になります。To:/From:/Subject:の設定を行った後、メッセージのボディとしてcreateAttachmentPart()で生成したMimeMultipartオブジェクトを設定しています。
createAttachmentPart()は先に示した図の中のパートのツリー構造のうち、MimeMultipartオブジェクトより下の層を構築することになります。これはGUIコンポーネントのツリー構造を作っていくのと同様の感覚で作成できます。
まずマルチパートを構成する各パートを表すMimeBodyPartオブジェクトを生成します。そして、textPartにはMimeMessageに行うのと同じようにsetText()で本文を設定しています。
次に添付ファイルのパートを表すfilePartのボディの設定です。
添付ファイルのボディはsetDataHandler()を用いて設定しています。
これは本来なら、setContent()にFileオブジェクトあたりを渡すことで設定できそうなものなのですが、JavaMail自身がデフォルトではこの章の最後に記した4種類のMIMEタイプに対してしかDataContentHandler(Javaオブジェクトとストリームの変換を行うものです)を用意していないため、その他のMIMEタイプのオブジェクトはストリームに変換するための適当なDataSourceオブジェクトを作成して、そのオブジェクトを元にDataHandlerオブジェクトを作成、そして、そのDataHandlerをsetDataHandler()でPartに設定するという手順を踏まざるを得ないのです(*)。
とはいえ、この手法が公開インターフェイスで行える事から、現状でもあらゆるオブジェクトをメッセージに付加する事が可能になっています。
さて、ここではFileDataSourceというJAFのクラスのインスタンスを生成しています。このクラスは、与えられたファイル名の拡張子からMIMEタイプを自動的に判断して、データの読み込み手段を提供します(上記サンプルでは"test.txt"ですので"text/plain"MIMEタイプが選択されます)。そしてそれを与えてJAFのDataHandlerオブジェクトを生成しています。DataHandlerは後程説明しますが、データの書き出し手段を提供しています。
*:この理由は、JAFのDataContentHandlerの仕組みは特定のMIMEタイプに対して使用するDataContentHandlerを決定するというものですので、Fileオブジェクトに対応するDataContentHandlerを作っても、そのファイルが表すMIMEタイプに対して登録するというわけに行かないためと思われます。つまり、setContent()でFileオブジェクトを渡せるようにするというよりは、"xxx/yyyy"というMIMEタイプに対してFileオブジェクトを扱うDataContentHandlerを登録するという感覚です。こうなると、そのファイルが表すMIMEタイプでメッセージを表現できるようにするのが厄介になるわけです。例えばapplication/octed-streamというMIMEタイプに対してFileオブジェクトを解釈するDataContentHandlerを登録するということは考えられますが、デフォルト状態ではそれは行われていません。
次に、filePartに対して添付ファイルのファイル名を設定しています。これはメッセージヘッダ中のContent-Disposition:のfilenameパラメタを設定する事になります。コメントにある通り、いきなりsetFileName()を呼び出した場合、Content-Disposition:そのものの値はattachment(添付)となります。
これでtextPart/filePartという二つのMimeBodyPartオブジェクトが作成できました。後はこれらをmultipart/mixedなMimeMultipartオブジェクトに設定すれば完成です。残りの行がそれを行っているのですが、あまりにも見たままですので特に説明するところもありませんね^^。MimeMultipartはコンストラクタでMIMEタイプのサブタイプを与えない場合は"multipart/mixed"が選ばれる事になっています。
このプログラムを以下のように起動してメッセージを送信させ、それをメイラで受信してみました。
>java SendAttachmentSample localhost shin@localhost postmaster@localhost

どうやら正しく添付ファイルとして受信できているようですね。
送信されたメッセージのソースを見てみましょう。
Message-ID: <4366642.974740138010.JavaMail.postmaster@localhost>
Date: Tue, 21 Nov 2000 02:08:58 +0900 (JST)
From: postmaster@localhost
To: shin@localhost
Subject: =?ISO-2022-JP?Q?=1B=24B=25F=259=25H=25a=25C=25=3B!=3C=258=24G=249=1B=28B?=
Mime-Version: 1.0
Content-Type: multipart/mixed; boundary="----=_Part_0_6830354.974740137510"
X-Mailer: JavaMail Sender
Received: from localhost ([127.0.0.1])
by shin (JAMES SMTP Server 1.2) with SMTP ID 75
for <shin@localhost>;
火, 21 11 2000 02:08:58 +0900
------=_Part_0_6830354.974740137510
Content-Type: text/plain; charset=ISO-2022-JP
Content-Transfer-Encoding: 7bit
テキストファイルを添付しています。
------=_Part_0_6830354.974740137510
Content-Type: text/plain; name=test.txt
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=test.txt
g2WDWINng3SDQINDg4uCxYK3DQo=
------=_Part_0_6830354.974740137510--
SubjectがQエンコーディングになっている点については、4章の「JavaMailにおけるSubjectのエンコーディングスキームについて」を参照して下さい。
Received:の位置や内容(曜日)がおかしいのはJAMESというサーバの問題ですので大目にみるとして、ちゃんと2章で紹介した添付ファイルの例と同じ形式で送信が行われているようですね。
このプログラムはファイル名に日本語は用いることはできません。これはJavaMail自体が対応していないためで、これに対応する方法は4章の「日本語添付ファイル名の対応」に記述しています。
HTMLメッセージは、Content-Type: text/htmlであるメッセージですが、普通はそのようにはせず、Content-Type: multipart/alternativeなメッセージに対して、HTMLを表すパート(Content-Type: text/html)と、同じ内容を表すプレインテキストパート(Content-Type: text/plain)を付加したものにします。このようにすることで、HTMLメッセージの表示に対応していないMUAでもプレインテキストパートの表示が可能になるためです。
では、HTMLメッセージの構造を見てみましょう。

2章でも説明しましたが、まずメッセージそのもののContent-Type:はmultipart/alternativeとなっています。boundaryパラメタはmultipart/mixedの場合と同様に付加されます。
それぞれのパートがboundary文字列で区切られている点など全体の構造はmultipart/mixedと何ら変わりません。multipart/mixedと異なるのは、それぞれのパートが同一の内容を表していて、従属関係にない事からContent-Disposition:ヘッダが不要である点ですね。
さて、図の右側のオブジェクトツリー構造についてですが、これも添付ファイルの場合とほとんど同じになります。メッセージのボディはMimeMultipartオブジェクトであり、それは(この場合)二つのMimeBodyPartオブジェクトを保持します。それぞれのMimeBodyPartのボディにはテキストパート用のStringオブジェクトとHTMLパート用のStringオブジェクトが設定されています。
Content-Disposition:関連の設定が全く不要であり、両方のパートがtextメディアタイプであることもあってこちらの方が単純ですね。
JavaMailでは添付ファイル付きメッセージの構築とほとんど同じ手順でHTMLメッセージのパートツリーを構築できます。
以下のコードを見て下さい。
// [SendHTMLSample.java]
// HTMLメッセージの送信を行うサンプルプログラム
import java.io.*;
import javax.mail.MessagingException;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimeMessage;
import javax.activation.DataHandler;
import com.sk_jp.mail.SimpleSender;
public class SendHTMLSample {
public static void main(String[] args) throws Exception {
String host = args[0];
String to = args[1];
String from = args[2];
String subject = "テストメッセージです";
SimpleSender sender = new SimpleSender();
MimeMessage msg = sender.createMessage();
SimpleSender.setHeaders(msg, to, from);
msg.setSubject(subject, "ISO-2022-JP");
msg.setContent(createHTMLPart());
msg.saveChanges();
sender.connect(host);
sender.send(msg);
sender.disconnect();
}
public static MimeMultipart createHTMLPart() throws MessagingException {
String text = "これはテキストのパートです。";
String html =
"<html><body>" +
"これは<em>HTML</em>のパートです。" +
"</body></html>";
// 一つ目のボディパート(text/plain)の構築
MimeBodyPart textPart = new MimeBodyPart();
textPart.setText(text, "ISO-2022-JP");
// 二つ目のボディパート(text/html)の構築
MimeBodyPart htmlPart = new MimeBodyPart();
htmlPart.setContent(html, "text/html; charset=ISO-2022-JP");
MimeMultipart mp = new MimeMultipart("alternative");
mp.addBodyPart(textPart);
mp.addBodyPart(htmlPart);
return mp;
}
}
createHTMLPartメソッド以外はSendAttachmentSampleと全く同じです。
createHTMLPart()メソッドではtext/plainとtext/htmlの二つをmultipart/alternativeであるコンテナに格納して返しています。
各MimeBodyPart(BodyPart)を生成する箇所の構造も、SendAttachmentSample.javaと同様です。HTMLテキストは(1)のようにsetContent()に対して"text/html"MIMEタイプを指定すればStringオブジェクトを設定することができます。注目するのは(2)のMimeMultipartオブジェクトをサブタイプ"alternative"で生成していることです。multipart/alternativeについては2章で説明していますが、要するに「MUAが表示可能なパートを表示する」というものです。
このプログラムを以下のように起動して送信したメッセージをメイラで受信してみました。
>java SendHTMLSample localhost shin@localhost postmaster@localhost

HTMLレンダリングが可能なメイラであれば、このようにHTMLパートの表示が正しく行われていることを確認することができます。HTMLに対応していないメイラや、HTML表示をOffにしている場合は、もう一方のプレインテキストパートの方が表示されることになります。
送信されたメッセージのソースを記しておきます。添付ファイルの場合と同様に、2章でご紹介した例と同じ構造になっていることを御確認いただけますでしょうか。
Message-ID: <6987074.974717964200.JavaMail.postmaster@localhost>
Date: Mon, 20 Nov 2000 19:59:24 +0900 (JST)
From: postmaster@localhost
To: shin@localhost
Subject: =?ISO-2022-JP?Q?=1B=24B=25F=259=25H=25a=25C=25=3B!=3C=258=24G=249=1B=28B?=
Mime-Version: 1.0
Content-Type: multipart/alternative;
boundary="----=_Part_0_529641.974717963710"
X-Mailer: JavaMail Sender
Received: from shinm ([127.0.0.1])
by Shin-mebius (JAMES SMTP Server 1.2) with SMTP ID 781
for <shin@localhost>;
月, 20 11 2000 19:59:24 +0900
------=_Part_0_529641.974717963710
Content-Type: text/plain; charset=ISO-2022-JP
Content-Transfer-Encoding: 7bit
これはテキストのパートです。
------=_Part_0_529641.974717963710
Content-Type: text/html; charset=ISO-2022-JP
Content-Transfer-Encoding: 7bit
<html><body>これは<em>HTML</em>のパートです。</body></html>
------=_Part_0_529641.974717963710--
なお、demoにはsendhtml.javaとして、HTMLメッセージを送信するサンプルがありますが、このデモはContent-Type: text/htmlのメッセージを生成するようになっているため、この処理をそのまま使うのは好ましくありません。また、そもそも生成しようとするメッセージがHTMLである必要があるものかどうかを常に考えるようにしましょう。
JavaMail1.2は実はtextメディアタイプについては、plain、html、xmlの三種類のサブタイプにしか対応していません。HTMLメッセージの送信では、これらのMIMEタイプしか用いなかったため、直接Stringオブジェクトを設定する事で適切にフォーマットされましたが、そうはいかない例としてtext/enrichedメッセージを送信するサンプルを作ってみます(*)。
*:textメディアタイプには他にもたくさんのサブタイプがあります。実は本稿執筆時点ではJavaMailはtext/xmlにも対応していなかったため、この章は「XMLメッセージの送信」でしたが、JavaMail1.2 FCSでtext/xmlの対応がなされたため、例を改めたという経緯があります。
まず、先程のSendHTMLSampleのHTMLを設定する部分をむりやりtext/enrichedメディアタイプで規定されるリッチテキストに変更して実行してみます。
// html文をむりやりリッチテキストに変更
String html =
"これは<BOLD>text/enriched</BOLD>の<UNDERLINE>パート</UNDERLINE>です。";
:
// MIMEタイプをtext/enrichedに変更
htmlPart.setContent(html, "text/enriched; charset=ISO-2022-JP");
さて、実行すると以下のように見事に例外が発生します。
>java SendHTMLSample localhost shin@localhost postmaster@localhost
javax.mail.MessagingException: IOException while sending message;
nested exception is:
javax.activation.UnsupportedDataTypeException
at com.sun.mail.smtp.SMTPTransport.sendMessage(SMTPTransport.java:347)
at com.sk_jp.mail.SimpleSender.send(SimpleSender.java:112)
at SendHTMLSample.main(SendHTMLSample.java:27)
Exception in thread "main"
javax.activation.UnsupportedDataTypeExceptionはJAFがDataContentHandlerというMIMEタイプに応じた処理オブジェクトを生成できなかった場合に発生します。即ち、text/enrichedを処理するDataContentHandlerが登録されていないので、setContent()メソッドが失敗したのです(実際に例外が報告されるのは送信しようとした時ですが)。
JavaMailはsetContent()にJavaオブジェクトとMIMEタイプを与えるだけで、そのオブジェクトを伝送形式に変換できると書いていましたが、既に何度か書いてきた通り、それはあくまでそのMIMEタイプに応じたDataContentHandlerが登録されていればの話であって、JavaMail1.2の時点ではデフォルトでは章末の「JAFについて」の項で記すいくつかのMIMEタイプにしか対応していません。
それら以外のMIMEタイプを処理したい場合は、先の添付ファイルの時に説明した通り、part.setDataHandler(new
DataHandler(dataSource));の形式でそのMIMEタイプの伝送形式のストリームを得ることができるDataSource実装クラスのオブジェクトが必要です。
つまり、そのMIMEタイプを処理できるDataContentHandlerかDataSourceの何れか(*)が存在しなければなりません。送信対象がファイルであり、拡張子によってMIMEタイプが決められてもよいのであれば、先に上げたFileDataSourceを使うことができますが、今回は単なるStringオブジェクトですので別の手段が必要になります。
このような状況を想定して、JavaMailのdemoディレクトリにあるByteArrayDataSource.javaというファイルで独自のデータ形式を送信可能にする方法の例が示されています(なぜコアAPIに含めないのか疑問ですが)。
ByteArrayDataSourceクラスはInputStream/String/byte[]の何れかのオブジェクトから転送用のストリームを生成する機能を持っています。つまり、Javaのオブジェクトのうち、これらの何れかの型で表現可能なオブジェクトであれば、ByteArrayDataSourceを用いることで任意のMIMEタイプのデータとして送信することができます。
他の型のJavaオブジェクトを送信可能にする場合もそれに対応したDataSource実装クラスを用意すればよい事になります。
では、ByteArrayDataSourceを用いてtext/enrichedのボディを設定するようにします。先程のcreateHTMLPart()を改造したものです。ただ、demoディレクトリのByteArrayDataSourceは日本語に対応していませんので(サンプルだからでしょうね)、日本語に対応させたByteArrayDataSourceを用います。そのソースコードも以下に記します。
*:章末の「JAFについて」も参照下さい。
// [SendEnrichedSample.java(一部)]
// text/enrichedのボディを設定する処理
public static MimeMultipart createEnrichedPart()
throws MessagingException {
String text = "これはテキストのパートです。";
String enriched =
"これは<BOLD>text/enriched</BOLD>の<UNDERLINE>パート</UNDERLINE>です。";
// 一つ目のボディパート(text/plain)の構築
MimeBodyPart textPart = new MimeBodyPart();
textPart.setText(text, "ISO-2022-JP");
// 二つ目のボディパート(text/enriched)の構築
MimeBodyPart enrichedPart = new MimeBodyPart();
// Bad
// enrichedPart.setContent(enriched, "text/enriched; charset=ISO-2022-JP");
// OK
enrichedPart.setDataHandler(new DataHandler(
new com.sk_jp.mail.ByteArrayDataSource(
enriched, "text/enriched; charset=ISO-2022-JP")));
enrichedPart.setHeader("Content-Transfer-Encoding", "7bit"); // (1)
MimeMultipart mp = new MimeMultipart("alternative");
mp.addBodyPart(textPart);
mp.addBodyPart(enrichedPart);
return mp;
}
注:(1)の行がないとJavaMail1.2ではContent-Transfer-Encoding:に"quoted-printable"が設定されます。1.1.3以前では不要です。JavaMail1.2でContent-Transfer-Encoding決定アルゴリズムが変更されており、エスケープシーケンス等が含まれる場合もquoted-printableになるようになっています。しかし、iso-2022を本文に記述する場合はエンコード無し("7bit")で良いですので、明示的に"7bit"を指定します。quoted-printableになっても普通のMUAなら正しく受信できますが…。
実はヘッダについても同様の変更がなされているため、"Q"エンコーディングになってしまっているのですがこの点については後ほど。
さて、では日本語にも対応したByteArrayDataSourceを以下にご紹介します。
// [ByteArraydataSource.java]
// demoのByteArrayDataSourceを改良して日本語文字列に対応したものです。
package com.sk_jp.mail;
import java.io.*;
import javax.mail.internet.ContentType;
import javax.mail.internet.ParseException;
import javax.activation.DataSource;
/**
* 任意のバイナリデータおよびテキストデータを表現するDataSourceです。
* charsetパラメタに対応しています。
*/
public class ByteArrayDataSource implements DataSource {
private byte[] data;
private String contentType;
/**
* バイナリデータのデータソースを生成します。
*/
public ByteArrayDataSource(byte[] data, String contentType) {
this.data = data;
this.contentType = contentType;
}
/**
* バイナリデータのデータソースを生成します。
*/
public ByteArrayDataSource(InputStream in, String contentType)
throws IOException {
this.contentType = contentType;
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[2048];
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
data = out.toByteArray();
}
/**
* 文字ストリームからtext/*用のデータソースを生成します。
* contentTypeのcharsetパラメタに応じてエンコードされたデータを生成します。
* charsetパラメタは、Readerに施されたエンコーディングと
* 同じものが指定されなければなりません。
*/
public ByteArrayDataSource(Reader in, String contentType)
throws IOException {
this.contentType = contentType;
String charset = null;
try {
ContentType ct = new ContentType(contentType);
charset = ct.getParameter("charset");
} catch (ParseException e) {}
if (charset == null) {
charset = "us-ascii";
}
BufferedReader reader = new BufferedReader(in);
ByteArrayOutputStream out = new ByteArrayOutputStream();
String line;
while ((line = reader.readLine()) != null) {
out.write(line.getBytes(charset));
out.write('\r');
out.write('\n');
}
data = out.toByteArray();
}
/**
* 文字列からtext/*用のデータソースを生成します。
* contentTypeのcharsetパラメタに応じてエンコードされたデータを生成します。
*/
public ByteArrayDataSource(String text, String contentType) {
this.contentType = contentType;
String charset = null;
try {
ContentType ct = new ContentType(contentType);
charset = ct.getParameter("charset");
} catch (ParseException e) {}
if (charset == null) {
charset = "us-ascii";
}
try {
data = text.getBytes(charset);
} catch (UnsupportedEncodingException e) {
try {
data = text.getBytes("us-ascii");
} catch (UnsupportedEncodingException e2) {}
}
}
//////////////////////////////////////////////////////////////////////////
// 以降はDataSource interfaceのメソッド群です。
// 重要なのはgetInputStream()でそのデータのストリーム表現を返すことです。
/**
* Return an InputStream for the data.
* Note - a new stream must be returned each time.
*/
public InputStream getInputStream() throws IOException {
if (data == null) {
throw new IOException("no data");
}
return new ByteArrayInputStream(data);
}
public OutputStream getOutputStream() throws IOException {
throw new IOException("cannot do this");
}
public String getContentType() {
return contentType;
}
public String getName() {
return "dummy";
}
}
「JAFについて」でも記していますが、JavaMailが特定オブジェクトを伝送形式に変換できない場合は、このようにDataSource実装クラスを用意して任意のMIMEタイプのデータを表現する方法を使用する事になります。ただ、特殊なデータは普通はファイルが元になるため、大概はFileDataSourceで間に合います。
このSendEnrichedSample.javaで生成したメッセージは、以下のようなものになります。Outlook Express等のtext/enrchedをサポートしたMUAであれば、text/enrichedの方のパートが修飾が付加された状態で表示されます。
Message-ID: <6987074.974741755230.JavaMail.postmaster@localhost>
Date: Mon, 15 Jan 2001 02:08:04 +0900 (JST)
From: postmaster@localhost
To: shin@localhost
Subject: =?ISO-2022-JP?Q?=1B=24B=25F=259=25H=25a=25C=25=3B!=3C=258=24G=249=1B=28B?=
Mime-Version: 1.0
Content-Type: multipart/alternative;
boundary="----=_Part_0_6805824.974741754740"
X-Mailer: JavaMail Sender
Received: from localhost ([127.0.0.1])
by shin (JAMES SMTP Server 1.2) with SMTP ID 175
for <shin@localhost>;
月, 5 1 2001 02:08:04 +0900
------=_Part_0_6805824.974741754740
Content-Type: text/plain; charset=ISO-2022-JP
Content-Transfer-Encoding: 7bit
これはテキストのパートです。
------=_Part_0_6805824.974741754740
Content-Type: text/enriched; charset=ISO-2022-JP
Content-Transfer-Encoding: 7bit
これは<BOLD>text/enriched</BOLD>の<UNDERLINE>パート</UNDERLINE>です。
------=_Part_0_6805824.974741754740--
さて、送信のサンプルにページを割きましたがそろそろJavaMailを使ったメッセージ受信プログラムを作ってみましょう。msgshow.javaという完成度の高いデモプログラムがありますので、自分が作成する場合にもこれを参考に作れば簡単にできます。といっても受信したデータをどう扱うかはアプリケーション次第ですのでその後の処理が簡単かどうかは解りませんが。
ここでは、送信のときと同様に今後も使っていけるようにメッセージ受信をカプセル化して単純なインターフェイスにしたものを作ってみましょう。
まず、msgshow.javaを参考に、IMAPサーバへの接続/切断を行うメソッドを定義します。
public class SimpleRetriever {
public void connect(String host, String user, String pass) {
}
public void connect(String host, int port, String user, String pass) {
}
public void disconnect() {
}
}
connect()はStore#connect()と同じインターフェイスなので、msgshow.javaのStoreを取得する部分までを見ながらこの部分を実装してみましょう。
public class SimpleRetriever {
private Store store;
public SimpleRetriever(String protocol) {
Session session = Session.getInstance(new Properties(), null);
store = session.getStore(protocol);
}
public void connect(String host, String user, String pass)
throws MessagingException {
connect(host, -1, user, pass);
}
public void connect(String host, int port, String user, String pass)
throws MessagingException {
store.connect(host, port, user, pass);
}
public void disconnect() {
try {
store.close();
} catch (MessagingException e) {
e.printStackTrace();
}
}
}
Session#getInstance()に渡すPropertiesオブジェクトはmsgshow.javaではSystem#getProperties()によって取得していましたが、SimpleSenderの時と同様の理由で空のPropertiesオブジェクトを渡すようにしています。
また、こちらのプログラムではSessionをStoreオブジェクトを取得するために一時的に使用しているだけです。これは通常の受信手順ではProviderの独自機能を用いることがないと考えたからです。つまりはSessionのPropertiesオブジェクトは常に空になります。
このような設計でも、もし後にProviderの独自機能を使用したいという要求が出た場合は、PropertiesオブジェクトかまたはSessionオブジェクトを保持するようにして、独自機能を扱うメソッド(Propertiesに値を設定したりStore等の実装クラスで定義されたメソッドを呼び出すもの)を提供するようにすればよいでしょう。単にPropertiesをパラメタに取るコンストラクタを追加する方法もあります(*)。
*:誰もが自由に触れる情報はなるべく触れないように設計しておくことで堅牢性が増します。Propertiesオブジェクトを外部からもらうように修正した場合、そのオブジェクトに外部からputする事で自由に触れてしまうという危険があります。これはコンストラクタで受け取るという性質から初期値である事を明文化して仕様とするのが妥当ですが、完全に外部からの直接操作をシャットアウトするなら、コンストラクタで受け取ったPropertiesオブジェクトのコピーをSession#getInstance()に渡すようにします。
では続きを作りましょう。msgshow.javaを見ると次はStoreオブジェクトからFolderオブジェクトを生成しています。ここではStore#getDefaultFolder()を呼び出し、次にそのfolderからgetFolder()によって目的のフォルダを取得しています。
実際のところ、ここは単にStore#getFolder()でも同じ結果になります。
ちょっとここで、フォルダの階層について記した以下の図をご覧ください。

FolderオブジェクトはIMAPサーバ上のフォルダに対応するオブジェクトで、ツリー構造になっています。
また、最上位層には"INBOX"フォルダを含む複数のフォルダが存在します。
で、ツリーの最上位層が複数だと扱い辛いので、その上にさらにDefaultFolderというオブジェクトが*JavaMailでは*用意されています。
ただ、StoreにもgetFolder()メソッドが用意されているため、特にDefaultFolderを経由しなくてもいきなり第一層のフォルダが取得できます。より正確には、getFolder()はフォルダのセパレータとなる文字(通常'/')さえ解っていれば、いきなり二段以上下の階層にあるフォルダを取得することもできます。
FolderオブジェクトにおけるgetFolder()メソッドの役割は、現在のフォルダの位置から見た相対パス指定のようなものですね。
さて、フォルダを取得する部分はどうしましょう?メイラを作成するわけではないため、参照するフォルダの頻繁な移動やフォルダ間でのメッセージ編集等は取り敢えず考えなくてもよいと思いますので、できればフォルダを一発指定できるようにしたいものです。また、せっかく扱いやすい汎用ライブラリにしようというのですから、Folderを取り出してそこからMessageを取り出して・・・という部分は隠蔽してしまいたい所です。
というわけで、SimpleRetrieverオブジェクト自身がカレントフォルダ(現在着目しているフォルダ)を管理し、利用者からはFolderオブジェクトを意識せずにアクセスできるようにインターフェイスを考えることにします。
private Folder currentFolder = null;
public void setCurentFolder(String name) {
currentFolder = store.getFolder(name);
}
前述のとおり、DefaultFolderを取得しなくても目的のFolderの取得は可能ですのでこのようなコードとしてみました(*)。
ところで、msgshow.javaを見ながら作成しているわけですから次はFolder#open()を行う必要がありますね。これはどのタイミングでやるのがいいのでしょう?もちろんSimpleRetriever#openFolder()メソッドを用意すれば済む話ですが、せっかくJavaMailのAPI呼出し手順を簡易化したライブラリを作ろうとしているのですから、単純な読み込みには単純な利用方法で済むようにしたいところです。
一つはこの後作成するメッセージ取得メソッド内で自動的に行うという方法が考えられます。しかし、これも後で説明しますが、メッセージオブジェクトの取得はそのまま、メッセージのダウンロードを意味するわけではありません。つまり、メッセージオブジェクトを利用者に渡した後にも(サーバ上の)フォルダにアクセスされる可能性があります。従って、そのフォルダにいるあいだはフォルダをオープンしておくほうが問題が発生しないことになります。
というわけで、ここではsetCurrentFolder()によってフォルダの移動が行われると同時にオープンするものとしてみます。
*:なお、今回のサンプルはこのメソッドに'/'で区切った目的フォルダへのフルパスを指定して直接対照フォルダを取得するという約束にしています。
また、msgshow.javaのフォルダオープン処理は以下のようになっています。
// try to open read/write and if that fails try read-only
try {
folder.open(Folder.READ_WRITE);
} catch (MessagingException ex) {
folder.open(Folder.READ_ONLY);
}
つまり、READ_WRITEモードでオープンしようとして失敗したらREAD_ONLYモードでオープンしています。
このようにするのが正しいかについては微妙な問題があります。
SimpleRetrieverクラスの利用者はメッセージを受信するために利用するのか編集するために利用するのか解りません。READ_WRITEでオープンしていると考えて実装していると、いざ書き込もうとした時点で例外が発生するということになります。このような設計では対処がし辛くなるため、通常はREAD_ONLYとして、READ_WRITEでオープンしたい場合は利用者が指示するようにすることにします(msgshow.javaはサンプルなのでそのようになっているというだけであり、実際にはWRITE操作は行われていないので、上記コードの箇所は単にREAD_ONLYでオープンしても問題ないのです)。
以下が、オープン処理まで組み込んだものです。
public void disconnect() {
if (currentFolder != null && currentFolder.isOpen()) {
try {
currentFolder.close(true);
} catch (MessagingException e) {
e.printStackTrace();
}
currentFolder = null;
}
try {
store.close();
} catch (MessagingException e) {
e.printStackTrace();
}
}
private boolean writable = false;
public void setWritable(boolean writable) {
this.writable = writable;
}
public void setCurrentFolder(String name) throws MessagingException {
if (currentFolder != null) {
currentFolder.close(true);
}
currentFolder = store.getFolder(name);
if (currentFolder == null) {
throw new FolderNotFoundException();
}
if (writable) {
currentFolder.open(Folder.READ_WRITE);
} else {
currentFolder.open(Folder.READ_ONLY);
}
}
まず、disconnect()メソッド中でFolderがオープンしている場合はフォルダのcloseを行ってからStoreのcloseを行うように修正しています。また、setCurrentFolder()メソッドでcurrentFolderを書き換える前に以前のcurrentFolderに対してcloseを行っています。
そして、writableフィールドに応じてREAD_WRITE/READ_ONLYの何れかでオープンしています。
さて、どこから取り出すかがはっきりしたところで、やっとメッセージの取得と行きましょうか。
メッセージの取得方法はmsgshow.javaを見ると、
Folder#getMessageCount()
Folder#getMessages()
Folder#fetch()
Folder#getMessage()
といったメソッドによって行われています。
これらについてAPIドキュメントで確認すると(ほとんど確認するまでもないですが)、getMessageCount()によりフォルダ上の全メッセージ数を確認し、getMessages()でフォルダ内の全メッセージオブジェクトの取得(メッセージ本体はまだダウンロードしていません)、fetch()でメッセージ中の指定された部分をMessageオブジェクト上にダウンロード、getMessage()は番号を指定して一通のメッセージの取得、といったことをしています。
今作成しているSimpleRetrieverクラスの仕様としてはフォルダ上の全メッセージの取得と指定したメッセージの取得を取り敢えずサポートすることにします。
public MimeMessage[] get() {
}
public MimeMessage get(int num) {
}
中身の記述と行きましょう。
先に一通のメッセージを取得するget(int)を記述するほうが簡単そうです。メソッドの仕様を単純化しておけば、APIドキュメントとmsgshow.java(等のサンプル)を見ながら書けば簡単に書けると思います。
public MimeMessage get(int num) throws MessagingException {
if (currentFolder == null) {
setCurrentFolder("INBOX");
}
try {
return (MimeMessage)currentFolder.getMessage(num);
} catch (IndexOutOfBoundsException e) {
throw new MessagingException("Message number out of bounds", e);
}
}
最初にcurrentFolderのチェックを行っているのは、setCurrentFolder()を呼び出さずにget()を呼び出した人に向けての救済です。
そのような場合に例外を投げるのか、このようにメソッド内で補正を行うかはやはり慎重に検討する必要があります。ここではデフォルトのフォルダとして"INBOX"という名称のフォルダが使われることになっていることから「setCurrentFolder()を行わなかった場合は"INBOX"から読み込む」という仕様にしたということです(*)。
メッセージの取得ですが、単にFolder#getMessage(int)を呼び出しているだけです。
Folder#getMessage(int)はMessage型を返すためMimeMessage型にキャストして返しています。get(int)メソッド自体をMimeMessage型を返すようにしてここでキャストしている理由は、どうせMimeMessageとしてしか扱わないため、ライブラリ中で(静的な)型の変換を行ってしまっておこうということです。「コラム:型とオブジェクト」も参照して下さい。もちろんこのようにする事で、このクラスはMessageクラスを直接継承したクラスを用いるプロバイダに対しては使えなくなってしまうわけですが、問題になる可能性より利便が上まわっていると判断しました。
また、与えられたメッセージ番号がフォルダ内の番号範囲外の場合、ArrayIndexOutOfBoundsException等のRuntimeExceptionがthrowされます。このままではよくないのでMessagingExceptionでthrowしなおしています。MessagingExceptionは他の例外を内包して数珠繋ぎに表示する機能を持ちますので、意味的に正しければこのようにMessagingExceptionとしてthrowしなおす方法を利用できます(ただし、スタックトレースはMessagingExceptionをthrowした場所からになってしまいます。これは設計上の欠陥で、うまく作ればちゃんと元の例外発生箇所からのスタックトレースを表示することも可能です)。
*:後で紹介するPOP3では"INBOX"しか使えないことになっていますので実はこうすることが好都合なのです。
では、次にget()の方を実装します。
public MimeMessage[] get() throws MessagingException {
if (currentFolder == null) {
setCurrentFolder("INBOX");
}
Message[] messages = currentFolder.getMessages();
MimeMessage[] mimeMessages = new MimeMessage[messages.length];
for (int i = 0; i < messages.length; i++) {
mimeMessages[i] = (MimeMessage)messages[i];
}
return mimeMessages;
}
内容は簡単で、まずは先のget(int)と同様にcurrentFolderのチェックを行います。次にmsgshow.javaと同様に、Folder#getMessages()でフォルダ内のメッセージの配列を得て、それをMimeMessageの配列に入れ直して返します。Folder#getMessages()はnullを返すことはなく、メッセージ数が0の場合は要素数0の配列を返しますので、上記の処理で問題なく動作します。わざわざMimeMessageの配列に詰め直すのは無駄なのでMessage[]を返すだけでもよいところですが、ここは使用者側の利便を優先しています。
これで単純な受信処理は完成です。IMAPの他の機能(メッセージのフォルダ間の移動など)に関しては本書では詳しくは説明しませんが、4章のAPI仕様を参考に作ってみてください。
では、テストを行います。こんなものを用意してみました。
public static void main(String[] args) throws Exception {
String protocol = args[0];
String host = args[1];
String user = args[2];
String pass = args[3];
SimpleRetriever r = new SimpleRetriever(protocol);
r.connect(host, user, pass);
Message[] m = r.get();
System.out.println("******************* Recieved: " + m.length);
for (int i = 0; i < m.length; i++) {
m[i].writeTo(System.out);
System.out.println("*******************");
}
r.disconnect();
}
protocol/host/user/passwordの4つのコマンドラインパラメタをとって、"INBOX"フォルダ内の全てのメッセージを取得して標準出力に表示するというものです。以下のように実行します。
>java com.sk_jp.mail.SimpleRetriever imap IMAPサーバホスト名 ユーザ名 パスワード
このクラスの仕様として、先にも少し書きましたが、パラメタ無しのget()メソッドで取得したMessageオブジェクトは取得した時点では内容がダウンロードされているとは限りません。Messageオブジェクト内のgetXXX()系のメソッドを呼び出したり、Folder#fetch()で明示的に取得しようとした時に実際のダウンロードが行われます。
従って、そういった操作を行うまではclose()してはいけないことになります。いつ内容のダウンロードを行うか、また、いつclose()するかはライブラリ利用者の判断に任されるということです。
その点を踏まえて上記処理を読んでいくと、まずSimpleRetrieverオブジェクトを生成し、connectしてget、取得したメッセージ群をMimeMessage#writeTo()により標準出力に書き出し(このときにサーバからダウンロードが行われる)た後disconnectする、となります。
これで、JavaMailを直接使うよりかなり楽になるのではないでしょうか。
このプログラムの全容は以下のようになります。
なお、文中には記しませんでしたが、マルチスレッドで使いまわされる可能性を考え、一通りのメソッドにsynchronizedを付加しています。
ただ、このクラスの場合、スレッド毎に異なるオブジェクトを使うような約束にしておけばsynchronizedは不要になります。
しかし、スレッド毎に異なるオブジェクトにしたとしても接続先のユーザが同じ場合は同時接続が許されないため接続に失敗してしまいます。従って、同じアカウントに対する接続を複数のスレッドから行うために一つのSimpleRetrieverオブジェクトを使用することを考えるとsynchronizedが必要になると言うことです。ただ、この対処だけでは、あるスレッドがまだ受信したいメッセージがあるのに他のスレッドがdisconnect()を呼び出してしまう可能性があります。そのような場合はセッションごと排他にするか、接続/切断をアプリケーションの開始/終了時に行うようにするといった対処が必要になります。
// [SimpleRetriever.java]
package com.sk_jp.mail;
import java.util.Properties;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.NoSuchProviderException;
import javax.mail.FolderNotFoundException;
import javax.mail.internet.MimeMessage;
public class SimpleRetriever {
private Store store;
private Folder currentFolder = null;
public SimpleRetriever(String protocol) throws NoSuchProviderException {
Session session = Session.getInstance(new Properties(), null);
// "mail.debug"のみシステムプロパティの情報を利用する事にします。
session.setDebug(Boolean.getBoolean("mail.debug"));
store = session.getStore(protocol);
}
public void connect(String host,
String user,
String password) throws MessagingException {
connect(host, -1, user, password);
}
public synchronized void connect(String host,
int port,
String user,
String password)
throws MessagingException {
store.connect(host, port, user, password);
}
public synchronized void disconnect() {
if (currentFolder != null && currentFolder.isOpen()) {
try {
currentFolder.close(true);
} catch (MessagingException e) {
e.printStackTrace();
}
currentFolder = null;
}
try {
store.close();
} catch (MessagingException e) {
e.printStackTrace();
}
}
private boolean writable = false;
public synchronized void setWritable(boolean writable) {
this.writable = writable;
}
public synchronized void setCurrentFolder(String name)
throws MessagingException {
if (currentFolder != null && currentFolder.isOpen()) {
currentFolder.close(true);
}
currentFolder = store.getFolder(name);
if (currentFolder == null) {
throw new FolderNotFoundException();
}
if (writable) {
currentFolder.open(Folder.READ_WRITE);
} else {
currentFolder.open(Folder.READ_ONLY);
}
}
public synchronized MimeMessage[] get() throws MessagingException {
if (currentFolder == null) {
setCurrentFolder("INBOX");
}
Message[] messages = currentFolder.getMessages();
MimeMessage[] mimeMessages = new MimeMessage[messages.length];
for (int i = 0; i < messages.length; i++) {
mimeMessages[i] = (MimeMessage)messages[i];
}
return mimeMessages;
}
public synchronized MimeMessage get(int num) throws MessagingException {
if (currentFolder == null) {
setCurrentFolder("INBOX");
}
try {
return (MimeMessage)currentFolder.getMessage(num);
} catch (IndexOutOfBoundsException e) {
throw new MessagingException("Message number out of bounds", e);
}
}
protected void finalize() throws Throwable {
disconnect();
}
public static void main(String[] args) throws Exception {
String protocol = args[0];
String host = args[1];
String user = args[2];
String pass = args[3];
SimpleRetriever r = new SimpleRetriever(protocol);
r.connect(host, user, pass);
Message[] m = r.get();
System.out.println("******************* Recieved: " + m.length);
for (int i = 0; i < m.length; i++) {
m[i].writeTo(System.out);
System.out.println("*******************");
}
r.disconnect();
}
}
では受信部から取り出したメッセージはどのように扱えばよいのでしょう。このあたりは、作成しようとしたプログラムに依存しますので汎用的な解はないのですが、msgshow.javaの場合はメッセージの各パート毎に内容を表示できるものは表示し、表示できないものはどのようなパートであったかを表示するようになっています。
ここではマルチパートメッセージが送られてくる事を想定して処理を行う方法の一例として筆者が用いた方法をご紹介します。
マルチパートメッセージの送信の項で説明した通り、メッセージはマルチパートの各パートを表すPartオブジェクトのツリーで構成されます。
MimeMessage#getContent()メソッドでメッセージのボディを取得することになります。getContent()は内容に応じて様々な型のオブジェクトを返すため、Object型を復帰値としています。従って、呼び出した側で内容の種類に応じてキャストして利用する必要があります。
msgshow.javaを見れば分かる通り、JavaMailがPartのツリー構造を構築してくれたとしても、その中から目的のパートを取り出して処理するのはまだ複雑です。そして、ツリーをトラバースする処理は誰もがmsgshow.javaのような再帰処理を記述する事になります。
これでは煩雑ですので、トラバースする部分と処理する部分をさらに分離してみました。以下のコードをご覧ください。
// [PartHandler.java]
package com.sk_jp.mail;
import java.io.IOException;
import javax.mail.Part;
import javax.mail.MessagingException;
import javax.mail.internet.ContentType;
public interface PartHandler {
boolean processPart(Part part, ContentType context)
throws MessagingException, IOException;
}
// [MultipartUtility.java]
package com.sk_jp.mail;
import java.io.IOException;
import javax.mail.Part;
import javax.mail.Multipart;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.internet.ContentType;
public class MultipartUtility {
public static void process(Part part, PartHandler handler)
throws MessagingException {
process(part, handler, null);
}
private static boolean process(Part part, PartHandler handler,
ContentType context)
throws MessagingException {
try {
if (part.isMimeType("multipart/*")) {
Multipart mp = (Multipart)part.getContent();
ContentType cType = new ContentType(part.getContentType());
for (int i = 0; i < mp.getCount(); i++) {
if (!process(mp.getBodyPart(i), handler, cType)) {
return false;
}
}
return true;
}
return handler.processPart(part, context);
} catch (IOException e) {
throw new MessagingException(
"Got exception \nin " + part + "\n", e);
}
}
}
このinterfaceとstaticメソッドによって各Partオブジェクトをツリーを辿っていく(共通)部分を切り離しました。MultipartUtility#process()を呼び出すと、そのPart(MessageもPartの一つです)配下の全てのPartについてPartHandler#processPart()メソッドが呼び出されます。
processPart()がfalseを返す事で、その階層におけるトラバース処理を中断します。
後は要求に応じたPartHandlerの実装を与えればよいのです。PartHandlerの実装は容易に再利用可能になります。
PartHandlerを利用して、メッセージ内の最初に見つかったtext/plainパートを抽出するFirstPlainPartExtractorを作ってみると以下のようになります。
// [FirstPlainPartExtractor.java]
package com.sk_jp.mail;
import java.io.IOException;
import javax.mail.Part;
import javax.mail.MessagingException;
import javax.mail.internet.ContentType;
public class FirstPlainPartExtractor implements PartHandler {
private String text = null;
public boolean processPart(Part part, ContentType context)
throws MessagingException, IOException {
if (!part.isMimeType("text/plain")) {
return true;
}
text = (String)part.getContent();
return false;
}
public String getText() {
return text;
}
}
Part毎に呼び出されるprocessPart()では、それがtext/plainパートでなければトラバースを継続させ、text/plainであれば、その内容を保持してトラバースを中断しています。
実際に抽出を行いたい箇所では以下のように記述するだけとなります。
FirstPlainPartExtractor h = new FirstPlainPartExtractor();
MultipartUtility.process(message, h);
String detectedText = h.getText();
同じような感覚で、全ての添付ファイルを抽出するParHandlerもご紹介しておきます。プログラムの説明はJavaDoc用コメントを参照して下さい。コメントを含めていることと、message/rfc822のパートの扱いを選択できるようにしている事から若干長くなっていますが、処理はごく単純である事が分かっていただけるのではないでしょうか。
// [AttachmentsExtractor.java]
package com.sk_jp.mail;
import java.io.*;
import java.util.*;
import javax.mail.Part;
import javax.mail.MessagingException;
import javax.mail.internet.ContentType;
/**
* 添付ファイルを抽出するPartHandlerです。
* <p>
* MultipartUtility#process()呼び出し後にgetFileNames()によって、
* 添付ファイル名の配列を得ることができます。
* </p><p>
* ファイル名配列のindexを指定してその添付ファイルに対する
* InputStreamを得たり、渡されたOutputStreamに対して書き出すことができます。
* </p>
* @version 1.00
* @author Shin
*/
public class AttachmentsExtractor implements PartHandler {
/** message/*のパートを無視します。 */
public static final int MODE_IGNORE_MESSAGE = 1;
/** Content-Disposition: inline; パートはfilenameがあっても無視します。 */
public static final int MODE_IGNORE_INLINE = 2;
private final int mode;
private final List attachmentParts = new ArrayList();
/**
* 添付ファイル一覧を得るためのPartHandlerを作成します。
* message/*のパートやinline且つファイル名指定ありのパートも
* 添付ファイルとして扱います。
*/
public AttachmentsExtractor() {
this(0);
}
/**
* 添付ファイル一覧を得るためのPartHandlerを作成します。
* @param mode 動作モード。MODE_で始まる識別子をor指定します。
*/
public AttachmentsExtractor(int mode) {
this.mode = mode;
}
/** MultipartUtility#process()から呼びだされるメソッドです。 */
public boolean processPart(Part part, ContentType context)
throws MessagingException, IOException {
if (part.isMimeType("message/*")) {
if ((mode & MODE_IGNORE_MESSAGE) != 0) {
return true;
}
attachmentParts.add(part);
return true;
}
if (MailUtility.getFileName(part) == null) {
return true;
}
if ((mode & MODE_IGNORE_INLINE) != 0 &&
Part.INLINE.equalsIgnoreCase(part.getDisposition())) {
return true;
}
attachmentParts.add(part);
return true;
}
/**
* 添付ファイル個数を返します。
*/
public int getCount() {
return attachmentParts.size();
}
/**
* 添付ファイル名の配列を返します。
* <P>
* 添付ファイルが存在しない場合は空の配列を返します。<BR>
* ファイル名は同一のものが複数存在する事もありえます。
* </P>
*/
public String[] getFileNames() throws MessagingException {
String[] names = new String[getCount()];
for (int i = 0; i < names.length; i++) {
names[i] = getFileName(i);
}
return names;
}
/**
* 指定添付ファイルのファイル名を返します。
*/
public String getFileName(int index) throws MessagingException {
Part part = (Part)attachmentParts.get(index);
// part.getFileName()の日本語対応版です。4章でご紹介します。
String name = MailUtility.getFileName(part);
if (name == null) {
// 添付ファイル名が取得できない場合は、指定されていなかった場合か、
// あるいはmessage/*のパートの場合です。
// この場合は仮のファイル名を付けることとします。
if (part.isMimeType("message/*")) {
// If part is Message, create temporary filename.
name = "message" + index + ".eml";
} else {
name = "file" + index + ".tmp";
}
}
return name;
}
/**
* 指定添付ファイルのContent-Typeを返します。
*/
public String getContentType(int index)
throws MessagingException, IOException {
return MailUtility.unfold(
((Part)attachmentParts.get(index)).getContentType());
}
/**
* 指定添付ファイルのサイズを返します。
*/
public int getSize(int index) throws MessagingException, IOException {
return ((Part)attachmentParts.get(index)).getSize();
}
/**
* 指定添付ファイルを読み込むストリームを返します。
*/
public InputStream getInputStream(int index)
throws MessagingException, IOException {
return ((Part)attachmentParts.get(index)).getInputStream();
}
/**
* 指定添付ファイルを指定ストリームに書き出します。
*/
public void writeTo(int index, OutputStream out)
throws MessagingException, IOException {
InputStream in = getInputStream(index);
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
}
public static void main(String[] args) throws Exception {
SKMessage msg = new SKMessage(System.in);
AttachmentsExtractor h = new AttachmentsExtractor();
MultipartUtility.process(msg, h);
for (int i = 0; i < h.getCount(); i++) {
System.out.println("Attachment no : " + i);
System.out.println("Filename = " + h.getFileName(i));
System.out.println("******************");
h.writeTo(i, System.out);
}
}
}
以下はAttachmentsExtractorを使用して、全ての添付ファイルのファイル名/内容を標準出力に表示するものです。
AttachmentsExtractor h = new AttachmentsExtractor();
MultipartUtility.process(msg, h);
for (int i = 0; i < h.getCount(); i++) {
System.out.println("Attachment no : " + i);
System.out.println("Filename = " + h.getFileName(i));
System.out.println("******************");
h.writeTo(i, System.out);
}
JavaMailでPOP3サーバからメッセージを取り出す方法は、IMAP4の場合と全く変わりません。
動作確認は、IMAPの項でご紹介したデモプログラムであるmsgshow.javaが利用できます。
JavaMailのdemoディレクトリに移動して以下のようにデモを起動してみてください(コンパイルは済んでいますよね)。
>java msgshow -T pop3 -H POP3サーバホスト名 -U ユーザ名 -P パスワード -v
以下のようにメッセージが表示されれば成功です(もちろんパラメタはちゃんとPOP3サーバとそのアカウント情報を指定して下さいね)。
Total messages = 1 New messages = 0 ------------------------------- -------------------------- MESSAGE #1: This is the message envelope --------------------------- FROM: shin@sk-jp.com TO: shin@linux.localdomain SUBJECT: JavaMail APIs Test SendDate: Sat Aug 12 14:51:32 JST 2000 FLAGS: X-Mailer NOT available
(表示内容はメイルボックス上のメッセージに応じて変化します)
同じプログラムでIMAP4もPOP3も受信できるわけです。「送信」「受信」を抽象化したということの意義が解るのではないでしょうか?
当然ですが受信の手順もJavaMailを使う限りはIMAP4の場合と同じでよいということになります。ただし、POP3は単純なプロトコルですのでStore/Folder等のメソッドのうち、IMAPでしかサポートされないようなメソッドの場合は呼び出したときにMethodNotSupportedExceptionがthrowされます。例えばFolder#appendMessage()というメソッドがありますが、POP3というプロトコル自体はメッセージを取り出すことしかできないのでこのようなメソッドは機能しません(*26)。
*26:POP3Providerの実装によってはローカルにダウンロード済みのメッセージとサーバ上のメッセージを区別なく扱えるようにして、新しいフォルダを作製したりそこにメッセージを書き込んだりという操作をサポートするものも可能です。
代表的なものとして日本の宮館さんが作成されたPOP3Providerがあります。
http://www2s.biglobe.ne.jp/~dat/java/project/poppers/index.html
SunのPOP3Providerはそのような実装を選択せず、純粋なPOP3プロトコルの処理のみをサポートしています。
では、demoによる受信確認ができたところで、自分で受信処理を作ってみましょう。まずはIMAP4での受信時に作成したSimpleRetriever.javaを使って、どこを修正しようか・・・修正する箇所がありませんねえ。
msgshow.javaと同じく、全く修正することなくpop3でも受信ができてしまいます。以下のように実行してみてください。
>java com.sk_jp.mail.SimpleRetriever pop3 POP3サーバホスト名 ユーザ名 パスワード
この特徴を使えば単純なメイラ等は両方のプロトコルに対応したものがごく簡単に作れてしまうことでしょう。
少し脱線になりますが、ここでJAF(JavaBeansTM Activation Framework)についての説明とJAFそのものの使い方について説明しておきます。
1章で少し紹介していましたが、JAFとはさまざまな形式のコンテンツをアプリケーション側が形式を意識せずに利用できるようにするための仕組みを実装したパッケージです。
ここまでの説明で解るとおり、JavaMailを利用するアプリケーションは基本的にこのパッケージをほとんど意識しなくても良いようになっているのですが、「リッチテキストメッセージの送信」でも書いたようにプログラマがJAFを意識しなければならないケースもあります。ここで、JAF単体が提供する機能について説明し、JAFの機能を明示的に使う場合についても概要のみ記します。
JAFの機能には、MIMEタイプに応じたオブジェクトのストリームからの構築/ストリームへの書き出しの他に、それぞれのオブジェクトの種類に応じた可能な操作の列挙とその操作を実行する機能も含まれています。
JAFの中核をなすinterface/classの概要を以下に記します(interfaceは斜体で表しています)。
| DataSource | 各種オブジェクトの読み込み元/書き出し先を表すinterfaceです。 |
| FileDataSource | オブジェクトの読み込み元/書き出し先ファイルを表します。 MIMEタイプはファイルの拡張子を元にFileTypeMapクラスによって決定されます。 デフォルトの拡張子とMIMEタイプの対応表はactivation.jarのMETA-INF/mimetypes.defaultに書かれているものです。 |
| URLDataSource | オブジェクトの読み込み元/書き出し先URLを表します。 対象がURLである事以外はFileDataSourceと同じです。 |
| DataHandler | 特定のDataSource、またはオブジェクト+そのMIMEタイプ生成したMIMEタイプに応じたDataContentHandlerを用いて、ストリームからのオブジェクトの構築、またはストリームへの書き出しを行うためのクラスです。 内部で、MIMEタイプに応じた適切なDataContentHandlerを選択して利用します。 |
| DataContentHandler | 特定のMIMEタイプに対してストリームからオブジェクト(Content)の構築、あるいはストリームへの書き出し(writeTo)を行う役割を表すinterfaceです。 実装クラスはMIMEタイプごとに存在し、外部から追加することもできます。 |
| DataContentHandlerFactory | MIMEタイプから、適切なDataContentHandlerを選択して生成を行います。 DataHandlerはこのinterfaceの実装クラスに問い合わせることでDataContentHandlerを取得します。 DataContentHandler提供者は、このinterfaceの実装クラスも同時に提供することで、新たなMIMEタイプに対応したDataContentHandlerを利用できるようになります。JavaMailではこのinterfaceは用いず、mailcapファイルによりDataContentHandlerの登録を行っています。 |
| CommandMap | JAFが管理する全MIMEタイプにおける、それぞれに対して実行可能なコマンドを管理するクラスです。 MIMEタイプを指定して特定のコマンドを表すCommandInfoを返すことができます。 また、DataContentHandlerFactoryが存在しない場合にDataContentHandlerの生成も受け持ちます。 |
| MailcapCommandMap | mailcapファイルなどに記述された、MIMEタイプとMIMEタイプごとに可能な操作の一覧表を利用してCommandMapを構築します。 mailcapファイルの書式はRFC1524で規定されたものとなっています。 MIMEタイプ名; ; パラメタリストの形式で、パラメタリスト部分にx-java-コマンド識別子名=クラス名と言う記述があるものを解釈します。 このクラスはデフォルトのCommandMapとなっています。従って、通常はmailcapファイルとMIMEタイプに対応するDataContentHandlerクラスを用意する事で、DataContentHandlerの存在をJAFに教えることができます。 |
| CommandInfo | 特定のMIMEタイプのオブジェクトに対する特定のコマンドを現します。 getCommandObject()によって、そのオブジェクトに対してコマンドを実行するためのJavaBeansオブジェクトを得ることができます。 |
| CommandObject | CommandInfoオブジェクトから得られるJavaBeansに対して操作対象であるオブジェクトを通知するために、JavaBeansが実装しておくべきinterfaceです。 |
これらのオブジェクトの大まかな関係を以下に記します。
注:text_xml もJavaMailが提供
JAFはフレームワークと呼ばれていますが、要するに入り口にDataHandlerというクラスがいて、そのクラスから利用されるDataContentHandlerやCommandInfo/CommandObject、さらに必要であればCommandMapやDataSource等についても外部から独自の実装に差し替えることができる、少し言い替えればデフォルトでは大した実装をしていませんのであなたがこれらのクラスの実装を書いて下さいという作りになっているのが特徴です。JavaMailはJAF(フレームワーク)をある程度実装したクラスライブラリということになります。
JavaMailでは、メイル受信時は内部的にDataHandlerが使われて、MimeMessage#getContent()でJavaのオブジェクトを受け取ることができますが、MimeMessage#getDataHandler()により、ContentではなくDataHandlerを取得して、その中のgetAllCommands()/getCommand()などを利用して、受信したメイルに対する操作をメニューに表示することもできます。このような操作に利用されるのがCommandMap等の上記の表では下側に記したクラス群です(これらの具体的な使用方法は本書の範疇を超えますので割愛します。同様にJAFが提供するTransferable関連機能についてもJavaMailを使用する上では関係してこないので省略しています)。
DataHandlerとDataSourceの関係はJavaMailでもちょっと込み入ったことをしようとすると意識する必要が出てくるものです。
JavaMailは、JAFを実装していると書きましたが、JavaMailが実装したDataContentHandlerは、mail.jarやmailapi.jarのMETA-INF/mailcapファイルに記述されている以下の5種類しかありません。
[mailcapファイルの内容]
# # @(#)mailcap 1.5 00/09/26 # # Default mailcap file for the JavaMail System. # # JavaMail content-handlers: # text/plain;; x-java-content-handler=com.sun.mail.handlers.text_plain text/html;; x-java-content-handler=com.sun.mail.handlers.text_html text/xml;; x-java-content-handler=com.sun.mail.handlers.text_xml multipart/*;; x-java-content-handler=com.sun.mail.handlers.multipart_mixed message/rfc822;; x-java-content-handler=com.sun.mail.handlers.message_rfc822
つまり、これら以外のMIMEタイプのオブジェクトに対してはPart#setContent()やPart#getContent()でJavaのオブジェクトとの相互変換を行うことができないのです。(また、このファイルには操作についても定義されていませんのでこのままでは先程少し書いたgetCommand()等での操作も行えません)
「リッチテキストメッセージの送信」でお話しした通り、上記のMIMEタイプ以外のものについては、Part#setContent()ではなくPart#setDataHandler()を用いなければなりません。Part#setContent()でオブジェクトを取得できるようにするためには、独自のDataContentHandler実装クラスを提供して、上記に代わるmailcapファイルを用意するか、DataContentHandlerFactoryの実装クラスを用意して、あるMIMEタイプに対して独自のDataContentHandlerを返すことができるようにしなければなりません。この方法は手順が若干面倒ですし、できればJavaMailの方でDataContentHandlerの種類を増やしていってもらった方が良いですので、当面はPart#setDataHandler()を用いるのが得策です。
Part#setDataHandler()に渡すDataHandlerオブジェクトはDataSourceオブジェクトを使って構築する必要があります。DataHandlerを構築する手段はDataSourceオブジェクトを渡すか、任意のオブジェクトとMIMEタイプを渡すかのいずれかです(*)。後者はPart#setContent()のケースで使用されるもので、MIMEタイプに応じてDataContentHandlerの実装クラスを得ようとするので、上記のmailcapファイルに書かれているものしか扱えないことになります。
*:DataHandlerにはURLを渡すコンストラクタもありますが、これは内部でURLDataSourceが使われますのでDataSourceを使って構築するパターンです。