この章ではJavaMailのAPI群について、API Referenceだけでは得られない情報を交えてご紹介していきます。API Referenceを見れば解るようなものや、使用頻度が低いもの、例外クラスについては記述を省略しています。プロバイダが呼び出す/オーバーライドするために用意されたprotectedメソッドに関する説明は簡略化しています。プロバイダ作成者はそれらのメソッドも意識する必要があります。publicメソッドにもProviderから呼び出すことを想定されたメソッドがいくつかあります。これらには通常使用する必要はないことを明記しています。特にjavax.mail.internetパッケージのクラス群における日本語を扱う上での問題点に関して注意してください。日本語に関する問題は章末にまとめておきます。
また、APIを紹介する順番については、なるべく機能単位でグループ化しています。従って、javax.mailパッケージのクラスの説明がjavax.mail.internetパッケージの項で行われていたりします。インデックス的に参照するには索引を利用していただく方がよいですね。
javax.mailパッケージにはメッセージ送受信のセッションを管理するためのクラス群と、最低限のメッセージ/アドレス関連情報を抽象化したクラス群があります。ただし、利用者の立場で言えば、ほとんどのメッセージ送受信はjavax.mail.internetパッケージのクラスに依存することになります。本書では思い切ってPartやMessageクラス等のメソッド群の説明は、それを継承/実装したMimeMessage等のクラスとまとめて後の「メッセージ関連クラス」の項で説明することにします。従って、ここでは主にセッション管理関連クラスや細かな部品クラスについて説明することにします。
ユーザ名/パスワードの組を獲得する方法を定義するクラスです。
Sessionオブジェクトの生成(獲得)時にこのクラスのサブクラスを渡すことで、任意のユーザ名/パスワード獲得方法を使用することができます。abstractクラスであり、サブクラスを作成して使用することを前提としています。
JavaMailのdemo/client/SimpleAuthenticator.javaには、必要なときにダイアログボックスでユーザ名/パスワードを問い合わせるタイプのAuthenticatorのサンプルがあります。
何らかの方法でPasswordAuthenticationオブジェクトを生成して返します。サブクラス作成者はこのメソッドだけをオーバーライドする必要があります。ユーザ名/パスワードの獲得方法はどのような方法でも使えます。例えば設定情報から獲得するように実装したり、ダイアログボックスを表示してユーザからの入力を促したりするなどが考えられます。サブクラス作成者はこのメソッドから以下の5つのメソッドを呼び出すことで接続に関する情報を取得できる可能性があります(下記のメソッドからはnullが返される場合もあるということです)。
このメソッドは接続開始時など認証が必要になったタイミングでプロバイダから(Session#requestPasswordAuthentication()を経由して)呼び出されます。ただし、Service#connect()メソッドでは、このメソッドを呼び出す前に一度接続を試みます。このときに用いられるのはStore/Transport生成時にURLNameを指定した場合はそのURLに含まれるユーザ/パスワードであり、これで接続に成功した場合はこのメソッドは呼び出されません。また、一度接続に成功するとSessionオブジェクトに認証情報が保存されて、次回の接続時にはやはり呼び出されなくなることにも注意して下さい。
これらはprotected finalメソッドなのでオーバーライドもできず、サブクラスから呼び出されるためだけにあります。プロバイダがSession#requestPasswordAuthentication()を呼び出す時にその時点でプロバイダ自身が知っている情報を渡し、その情報がこれらのメソッドにより取り出せるようになっています。従ってプロバイダによっては5つの情報全てが解っているとは限らないため、nullが返される可能性があるわけです。
認証が必要になったタイミングで、解っているならばそれぞれ以下の情報が返されます。
| getDefaultUserName() | (例えばダイアログボックスにデフォルトで表示される)ユーザ名 |
| getRequestingPort() | 接続先ポート番号 |
| getRequestingPrompt() | 接続先サーバからのメッセージ(付属プロバイダではnullしか返されません) |
| getRequestingProtocol() | 接続プロトコル名 |
| getRequestingSite() | 接続先ホストのInetAddress |
ユーザ名/パスワードの組を表すクラスです。使われ方については次のjavax.mail.Authenticatorを参照して下さい。このクラス自体はコンストラクタで、userName/passwordを渡してそれを参照できるだけのものです。
javax.mail.Authenticatorサブクラスを作成しようとしない限りは意識する必要はありません。
Providerクラスは登録されたプロバイダの情報を表します。3章の「StoreとTransport」で説明したように、JavaMailではクラスパス上のMETA-INF/javamail.default.providersおよびMETA-INF/javamail.providersのそれぞれからプロバイダ情報を獲得して、Providerオブジェクトを生成します。Providerオブジェクトは上記リソースファイルに記述された情報へのアクセサのみを提供します。
それぞれ、クラス名、プロトコル名、プロバイダのタイプ(STORE/TRANSPORT)、ベンダ名、バージョンを返します。
SessionオブジェクトはgetClassName()で得られるクラス名からプロバイダの提供するStore/Transportインスタンスを生成して返すことになります。
プロトコル名からプロバイダの決定
"mail.protocol.class"プロパティにクラス名が記述されている場合、そのクラス名からプロバイダのインスタンスを得ようとします。
キャッシュに存在しなかった場合、プロトコル名からプロバイダインスタンスを引きます。
このようになっている理由は、一つのプロトコルについて複数のプロバイダが登録されている場合があり、プロトコル名からProviderインスタンスを得ようとすると、"mail.protocol.class"が未設定の場合は、指定プロトコル名のプロバイダのうち、JavaMailが最初に見つけたプロバイダが返されるためです。
"mail.protocol.class"にクラス名が指定されればそちらのプロバイダが使用されます。
つまり、特定のプロトコルにおけるデフォルトプロバイダを変更したい場合は、"mail.protocol.class"プロパティに使用したいStore/Transportのクラス名を入れておくことによって可能です。
SessionオブジェクトはStore/Transportを取得する機能を持ち、その際に接続/認証に関するデフォルト情報のセットを保持します。
あくまでデフォルト情報であり、Sessionオブジェクトが保持する情報に関係なく、全てを明示的に指定してメイルの送受信を行うことも可能です。その方針を取る場合はSessionオブジェクトは最初にStore/Transportを取り出すためだけにしか使わないようにします。後はAPI中でパラメタの省略されたメソッドを用いないようにするのです。低レベルな処理を行いたかったり、Sessionにおけるさまざまなデフォルト値に関する挙動を理解するのが難しい場合は、この手法の方がよいでしょう。ただし、プロバイダに特化したオプションはSessionに渡すPropertiesオブジェクトでしか指定できないものがありますので、そういったものはPropertiesオブジェクトに設定するしかありません。また後述するMessage-ID:の問題を解決するためにもPropertiesオブジェクトの設定が必要になってしまいます。
元々JavaMailはクライアント向けのAPIであることから、単にメイルを送受信するというよりは、メイラを作成するための補助としての機能を多く持っています。従って、JavaMailにはたくさんのコンビニエンスメソッドがあります(1.2になってさらに追加されました)が、これらを積極的に使用するかどうかは内部で行っていることを理解する労力との天秤といえるでしょう。本書では内部で行われることの説明は行いますが、サンプルプログラムは3章で紹介したSimpleSender/SimpleRetrieverのようなアプローチを取っています。つまりは低レベルな使い方に徹底し、Sessionに渡すPropertiesオブジェクトに設定することでしか解決できない問題に関しても、文字列による指定を伴うPropertiesの操作は自分のクラスライブラリ中に隠蔽することで静的な検査(コンパイル時のエラーチェック)を援助しています。
デバッグモードか否かを設定/参照します。Sessionに渡すPropertiesオブジェクトにおけるの"mail.debug"に対する設定と等価です。
JavaMail内部でこのメソッドを呼び出して各所でデバッグ出力を行っていますし、必要とあらばアプリケーション自身もgetDebug()の結果に応じてデバッグ出力を行ってもよいでしょう。
Session生成時にシステムプロパティ(System.getProperties())を渡していれば、アプリケーション起動時のコマンドラインパラメタに-Dmail.debug=trueを指定する事によりデバッグモードに切り替えることができます。
本書のサンプルでは、Sessionにシステムプロパティを与えることを嫌っていますので、Sessionオブジェクト獲得後に、
session.setDebug(Boolean.getBoolean("mail.debug"));
とすることで同等の効果を得ています。
なお、デバッグ出力は標準出力(System.out)に対して行われます。Servlet等では標準出力を見ることができない場合がありますので、この出力先を切り替えるために、
System.setOut(new java.io.PrintStream(
new java.io.FileOutputStream(
"/tmp/stdout.txt", true)));
といったように標準出力を差し替えるという手法を使うことがあります。
VM内で共有されるSessionオブジェクト、または新しいSessionオブジェクトを取得します。getDefaultInstance()で得られるSessionオブジェクトは本書ではDefaultSessionと呼んでいる、VM内で共有されるSessionオブジェクトです。getInstance()は毎回Sessionオブジェクトがnewされます。
3章で説明した通り、Sessionをパラメタにとるメソッドに対してnullを渡した場合、大体はDefaultSessionが用いられるようになっていますが、完全には対応されていません。しかし、DefaultSessionのグローバル性を利用して、メッセージ送受信の接続先情報が一つしかない場合はアプリケーション起動時に一度getDefaultInstance()を実行しておいて、後はSessionオブジェクトが必要になった時にSession.getDefaultInstance(null,
null)を用いるということは可能です。これについては3章のVerySimpleSender.javaで例を挙げています。
なお、Authenticatorオブジェクトを渡さないタイプのメソッドは、JavaMail1.2で追加されたコンビニエンスメソッドで、authenticatorにnullを渡すものと等価です。
Storeオブジェクトを取得するメソッドです。それぞれ、以下のようにして取得するStoreオブジェクトを決定します。
| getStore() | "mail.store.protocol"に指定されたプロトコル名を持つStoreオブジェクトを取得します。 "mail.store.protocol"が設定されていないとNoSuchProviderExceptionがthrowされます。 |
| getStore(Provider provider) | Providorオブジェクトを指定してそれによって定義されるStoreオブジェクトを取得します。 |
| getStore(String protocol) | プロトコル名を指定して登録済みプロバイダからStoreオブジェクトを検索/生成して返します。 |
| getStore(URLName url) | URLNameオブジェクトを指定して登録済みプロバイダからStoreオブジェクトを検索/生成して返します。 urlのプロトコル部のみを見て判断します。 |
Transportオブジェクトを取得するメソッドです。
ほぼStoreと同様ですが、getTransport(Address
address)というAddressをパラメタにとるメソッドだけはgetTransport()の方にしかありません。
また、パラメタ無しのgetTransport()は、"mail.transport.protocol"に指定されたプロトコル名を持つTransportオブジェクトを取得します。
| getTransport() | "mail.transport.protocol"に指定されたプロトコル名を持つTransportオブジェクトを取得します。 "mail.transport.protocol"が設定されていないとNoSuchProviderExceptionがthrowされます。 |
| getTransport(Provider provider) | Providorオブジェクトを指定してそれによって定義されるTransportオブジェクトを取得します。 |
| getTransport(String protocol) | プロトコル名を指定して登録済みプロバイダからTransportオブジェクトを検索/生成して返します。 |
| getTransport(URLName url) | URLNameオブジェクトを指定して登録済みプロバイダからTransportオブジェクトを検索/生成して返します。 urlのプロトコル部のみを見て判断します。 |
| getTransport(Address address) | Addressオブジェクトのアドレスタイプを元にプロトコル名を決定して登録済みプロバイダからTransportオブジェクトを検索/生成して返します。 |
Addressオブジェクトを引き数に取るgetTransport(Address
address)メソッドは、指定されたAddressオブジェクトのtype属性(javax.mail.Address参照)からプロトコルを判断してTransportオブジェクトを検索/生成します。このtype属性名(アドレスタイプと記します)とそれに対応するプロトコル名の変換規則は、/META-INF/javamail.default.address.mapファイルおよび、各プロバイダの/META-INF/javamail.address.mapに記述されます。
JavaMailに付属するjavamail.default.address.mapのエントリは一つしかありません。
と言うものです。
"rfc822"がアドレスタイプで、"smtp"が対応するプロトコル名です。"rfc822"というアドレスタイプはInternetAddressクラスのtype属性値です。つまり、InternetAddressクラスのインスタンスを渡した場合は"smtp"プロトコルのTransportが使用されることになります。
異なるAddressサブクラスのインスタンスが渡されれば異なるプロトコルとなる場合があります。
例えば、javax.mail.internet.NewsAddressというクラスのアドレスタイプは"news"となりますが、このインスタンスをgetTransport()に渡した場合、NNTPProvider(NetNews用プロバイダ)がインストールされていればNNTP用のTransportオブジェクトが得られるようにすることができます。そのためにはNNTPProviderのjarファイル中の/META-INF/javamail.address.mapに、
のようなエントリがあればよいことになります。直接このメソッドを利用することはまずないでしょうけれど、実はこのgetTransport(Address)メソッドはTransport#send()メソッド内から利用されています。つまり、正しくNNTPProviderのアドレスタイプとプロトコル名のマッピングが定義されていれば、Transport#send()メソッドに渡すメッセージの宛て先にNewsAddressが設定されていても正しく送信が行われるわけです。InternetAddress以外のAddressに対するTransportオブジェクトを提供するプロバイダ作成者は/META-INF/javamail.address.mapを用意する必要があるわけですね。
Sessionオブジェクト生成時に渡したPropertiesオブジェクトに対するアクセスメソッドです。
session.getProperties().put(key, value);のようにしてSessionオブジェクト中のPropertiesオブジェクトの内容を変更することもできます。
Storeオブジェクトを取り出すのを省略して、いきなりFolderオブジェクトを取得するメソッドです。
やっていることはgetStore() → connect() →
getFolder()でしかありません。
URLName情報に欠けている情報がある場合は、Sessionに渡すPropertiesオブジェクトからデフォルト情報を使用します。取り出したFolderオブジェクトからメッセージを取り出すためにはオープンする必要があります。
詳しくはStore#getFolder(URLName)の項で説明しますが、このメソッドは存在しないフォルダを指定してもnullを返すことはありません。Folderオブジェクトは実際のフォルダ(*)と同期しているわけではないということです。
*:IMAP4の場合はサーバ上のフォルダを指します。他のプロトコルの場合は「実際のフォルダ」の定義はプロバイダ次第です。
Authenticatorオブジェクトによるパスワードの入力等を行わせたいときに、プロバイダが呼び出します。
プロバイダ作成者以外が呼び出すことはほぼないといってよいでしょう。
URLNameオブジェクトをキーにしてPasswordAuthenticationオブジェクトをSession中に保持/取り出しを行います。
一度認証に成功したら、次は保持した認証情報を使って、ユーザへの再入力を省略するような用途で使われます。
ほぼ内部的に(JavaMail内で)しか呼び出されないと思ってよいでしょう。
java.mail.Providerオブジェクトの獲得/登録を行います。これらも通常は他のコンビニエンスメソッド(内部的にこれらを呼び出してくれるいわゆるラッパメソッド)の方を使用するため使用することはないのですが、それぞれについて簡単に説明します。
public Provider getProvider(String protocol)
プロトコル名から登録済みプロバイダオブジェクトを返します。
同じプロトコル名で複数のプロバイダが登録されている場合があります。この場合は起動時のプロバイダの検索時に最初に見つかったものが、そのプロトコルのデフォルトプロバイダとして返されます。
通常はgetStore()/getTransport()を用いるため、このメソッドを直接呼び出すことはないでしょう。
public Provider[] getProviders()
起動時にロードされた全てのプロバイダを返します。
setProvider()によりデフォルトプロバイダを変更したいときに、現在インストールされているプロバイダから選択させたいような場合に用います。
setProviderで後から追加されたプロバイダは含まれません。
public void setProvider(Provider provider)
そのSessionオブジェクト内での特定プロトコルにおけるデフォルトプロバイダを設定します。
次にそのプロトコルのプロバイダを取得しようとした時に、ここでセットしたプロバイダが用いられるようになります。
このメソッドはSessionに渡すPropertiesオブジェクトの"mail.protocol.class"の内容を上書きします。
ServiceはStore/Transportのスーパークラスであり、それらの共通部分を定義しています。
Store/Transportを用いる時には、ここのメソッド群も使用可能ということになりますので、重要なものは把握しておきましょう。
このクラスの説明の中で頻繁に「URLNameオブジェクト」という用語が出てきます。これは、Serviceクラスのメンバであり、このサブクラスであるStore/Transportオブジェクト生成時には内部的にURLNameオブジェクトが渡されるようになっています。URLNameオブジェクトには最低でもプロトコル名が設定されていますが、SessionオブジェクトからStore/Transportオブジェクトを取得する時に、ホスト名なども指定したURLNameオブジェクトを明示的に渡すこともでき、「URLオブジェクトの〜が使われる」といえばその情報が含まれていればそれが用いられるという意味です。
サーバへの接続を行います。それぞれ以下のように処理を行います。
public void connect()
connect(null, -1, null, null)と同じです。
public void connect(String host, String user,
String password)
connect(host, -1, user, password)と同じです。
public void connect(String host, int port,
String user, String password)
接続先情報を明示的に指定して接続を試みます。
nullまたは-1をパラメタに与えると、そのプロトコルにおけるデフォルト値、あるいはSessionに渡すPropertiesオブジェクトやシステムプロパティなどからパラメタを参照しようとします。
明示的に指定されなかった場合の各パラメタの代替値は以下の順で検索されます。
サーバから切断します。プロバイダはこれをオーバーライドして実際の切断を行い、super.close()を呼び出す必要があります。
接続中であるか否かを返します。各プロバイダはconnect()/close()メソッドの呼び出し以外の要因で接続/切断が行われた場合に、責任をもってprotected
void setConnected(boolean connected)
メソッドを呼び出し、isConnected()が正しい状態を返すように実装する必要があります。
Providerがオーバーライドする必要がある、実際の接続処理部です。
ConnectionEventの取得を可能にするメソッドのセットです。
notifyConnectionListeners()はプロバイダが呼び出すものです。
ConnectionListenerについては「イベント関連クラス(javax.mail.event)」の項をご覧ください。
JavaMailのイベントのポストを行います。JavaMail本体内、またはプロバイダが呼び出すことでMailEventを渡されたイベントリスナ群に配送します。
このService(Store/Transportのことです)に設定されたURLNameオブジェクトを返します。
URLNameオブジェクトはコンストラクタで設定されますが、そこでは明示的にURLNameオブジェクトを渡さなければプロトコル名以外は空のものになっています。
一度でも接続に成功すれば、その接続先情報をURLNameオブジェクト化して保持するようになっています。
Storeクラスはメッセージを保管するものを抽象化したクラスです。
保管庫はFolderにより階層化された保管庫の集合として扱うことになっています。
デフォルトフォルダを返します。デフォルトフォルダはフォルダツリーのルート階層にあるフォルダの一覧を取得するためにのみ存在するフォルダです。
デフォルトフォルダは、言わばファイルシステムにおけるルートディレクトリのようなものです。IMAP4の場合、ルートディレクトリのような概念はなくフォルダには必ず名前が付けられているのですが、そのトップレベルにあたるフォルダ群を他のサブフォルダと同じように扱えるようにするために、JavaMailでは仮想的にデフォルトフォルダという概念を持っています。そのようなものですので、デフォルトフォルダにはメッセージを格納することはできません。
デフォルトフォルダを意識しなくても、Storeオブジェクトから直接getFolder()でルート階層以下のFolderオブジェクトの取得は可能です。
指定した名称、またはURLNameオブジェクトのfile属性で表されるフォルダを返します。
getDefaultFolder().getFolder()と等価と考えてよいでしょう。
また、フォルダ名はセパレータを用いて孫階層以下のフォルダを直接指定することも可能です。
ただし、セパレータはFolder#getSeparator()で取得するのですが、そのためのFolderオブジェクトが必要になります。要するに、孫階層のFolderオブジェクトを取得するには一度何れかのFolderを取得してから、getSeparator()でフォルダパス区切り文字を取得して、その文字を使ってフォルダのパスを指定することになります(*)。
*:実際にはほとんどの場合'/'で区切ることで取得できてしまいますが、これは常に正しいとは限りません。JavaMail添付のIMAPFolderであれば、'\uffff'で区切ることで実際のセパレータに置き換えてくれるのですが、これも実装依存の方法ですのでやってはいけないことでしょう:-P。
FolderEventの取得を可能にするメソッドのセットです。
notifyFolderListeners()はプロバイダが呼び出すものです。
FolderListenerについてはjavax.mail.eventの項をご覧ください。
StoreEventの取得を可能にするメソッドのセットです。
notifyStoreListeners()はプロバイダが呼び出すものです。
StoreListenerについてはjavax.mail.eventの項をご覧ください。
Transportクラスはメッセージ送信処理を抽象化したクラスです。
基本はTransportオブジェクトに対してsendMessage()を使ってメッセージ送信を行うのですが、メッセージの送信を手軽に行えるように、staticなsendメソッドも提供されています。
指定されたメッセージを送信します。詳しくは3章で説明しています。APIドキュメントの内容以上に説明する部分は特にないようです。
TransportEventの取得を可能にするメソッドのセットです。
notifyTransportListeners()はプロバイダが呼び出すものです。
TransportListenerについてはjavax.mail.eventの項をご覧ください。
FolderクラスはStore上のメッセージや他のフォルダを束ねておく入れ物を抽象化しています。
「フォルダ」の定義はプロバイダによって異なりますが、Folderオブジェクトは、それが関連付けられた実際のフォルダの有無とは無関係に存在し得ます(*)。
例えば、新しいフォルダを作成する場合は、getFolder()で作ろうとする(まだ存在しないフォルダの)Folderオブジェクトを獲得してから、そのFolderのcreate()メソッドでフォルダの生成を行います。子フォルダを生成するメソッドがあるわけではありません。
*:"Folder"と"フォルダ"という用語の使い分けにご注意。本書では"interface"と"インターフェイス"のようにカタカナ表記の用語はJavaの言語機能/識別子に特化しないより一般的な意味で使用しています。
インスタンス群はディレクトリツリーと同様の階層構造を構成することになっています(3章の「メッセージの受信」を参照)。いわゆるサブフォルダの概念を持たないプロバイダであってもStore#getDefaultFolder()で獲得できるデフォルトフォルダをルートとして、その子フォルダとして"INBOX"という名前のフォルダが取得できるようになっているはずです(*)。
フォルダには名前がありますが、この「名前」が実際には何を表すかについてもプロバイダに依存します。フォルダが階層構造を持つプロバイダであれば、フォルダ名として(ディレクトリやURLを表現する文字列のように)セパレータで区切られた名前を指定できるかもしれません。
また、Folderはそのフォルダ内のメッセージや別のフォルダに対する操作インターフェイスも持ちます。
フォルダ内のメッセージにアクセスするには、フォルダをオープンする必要があります。逆にそのフォルダ自身を操作(rename等)する場合は、フォルダがクローズ状態である必要があります。
このあたりはAPIドキュメントやJavaMail仕様書に記されている内容なわけですが、要するにFolderの仕様はIMAP4の仕様を満たせるように作られているということです。
例えばPOP3の場合はフォルダの概念がありませんので、仮想的に"INBOX"という唯一のフォルダが存在するかのようにプロバイダが作られていて、フォルダに対する操作メソッドの大半は単に例外を投げるようになっています。
メソッド数が多いので分割して説明します。
*:"INBOX"という名称のフォルダが必ず存在するわけではありませんが、"INBOX"という名称のFolderオブジェクトはほぼあると思ってよいでしょう。ただし"INBOX"は単なる予約名であり、Folderオブジェクトとして必ず存在しなければならないと決められているわけではありません。
フォルダのオープンを行います。フォルダ内のメッセージの取り出しや操作を行う場合はフォルダがオープンされていなければなりません。
modeにはFolder.READ_ONLY/Folder.READ_WRITEの何れかを指定します。
フォルダのクローズを行います。expungeにtrueを指定すると、オープン中に削除マークを付けたメッセージがサーバから削除されます。
メッセージに削除マークを付けるとは、Message#setFlag(Flags.Flag.DELETED,
true)を呼び出すことを指します。
expungeにfalseを指定した場合は、削除マークを付加していても削除されません。削除の一括取り消し動作と言ってよいでしょう。
そのFolderオブジェクトが表現する実際のフォルダ(バックエンド)を生成します。typeにはFolder.HOLDS_FOLDERS(内部にフォルダを配置可能)およびFolder.HOLDS_MESSAGES(内部にメッセージを配置可能)をorで指定できます。
そのFolderオブジェクトが表現する実際のフォルダを削除します。
recurse = trueでフォルダ内のメッセージおよび各サブフォルダとその内部のメッセージまで削除が行われます。recurse
= falseの場合は、このメソッドの仕様としては実装依存ということになっていますが、最低限フォルダ内のメッセージは削除されることになっています。IMAPProviderにおけるrecurse
= false時の実装は、単にIMAP4のDELETEコマンドを発行するため、このコマンドの仕様に従います(RFC2060)。この場合、あくまでそのFolderオブジェクトが表すフォルダと内部のメッセージのみが削除され、その配下のフォルダはそのまま残ります。そして、そのフォルダは\Noselect属性(付録参照)、即ちHOLDS_FOLDERSタイプとなります。
そのFolderオブジェクトが表現する実際のフォルダを別のFolderオブジェクトが表現する名前に変更します。フォルダの名称変更に失敗した場合はfalseが返されます。
そのFolderオブジェクトが表現する実際のフォルダが存在するか否かを調べます。存在しない場合はcreate()メソッドでそれを作成することができます。
フォルダのオープン状態を調べるメソッドです。isOpen()メソッドで現在オープン中か否かを取得できます。
また、getMode()メソッドでオープン時に指定したモードが取得できます。
このフォルダの名称を返します。getFullNameではルート階層からのいわゆるフルパス表記が返されます。
このフォルダを表すURLNameオブジェクトを返します。
このフォルダのタイプを返します。Folder.HOLDS_FOLDERS(内部にフォルダを配置可能)およびFolder.HOLDS_MESSAGES(内部にメッセージを配置可能)、またはその両方の論理和が返されます。
このフォルダがサポートする永続的フラグ(*)の集合を返します。返されたFlagsオブジェクトには、フォルダ内のメッセージで有効となるFlags.Flagオブジェクトが含まれます。IMAP4サーバはSELECT/EXAMINEコマンドの結果として使用可能な永続的フラグの一覧を返しますので、IMAPProviderであれば、この応答に応じてFlagsオブジェクトが決定されます。
*:メッセージが取り得る状態として、IMAP4では永続的フラグとセッション中のみ有効なフラグが定義されています。
フォルダ名の区切り文字を返します。IMAPではフォルダ名の区切り文字が固定されていないため、このメソッドで取得する事になっています。つまり、本来はFolderオブジェクトを取得してからでなければStoreオブジェクトから直接子階層のFolderオブジェクトを得ることはできないことになります。ただ、ほとんどの場合は'/'が返されるといってよいでしょう。
このフォルダが購読済みか否かを参照/設定します。「購読」の概念はIMAP4やNetNewsに存在するものですが、JavaMailやIMAP4/Newsサーバは「購読」されているからといって何かをしてくれるわけではありません。購読しているフォルダに対して何を行うかはアプリケーション次第といえるでしょう。
なお、IMAP4ではサーバからの通知機構が存在し、新規メッセージの到着を通知してもらう事が可能(ただし何らかのコマンドの発行が必要)ですが、これは「購読」しているフォルダに対してではなく、オープンしているフォルダに対して行われるものとされています。
フォルダ内の指定メッセージに対して、指定フラグ群の状態を設定します。対象メッセージの指定方法として、メッセージ番号を格納した配列、開始/終了メッセージ番号による指定と、Messageオブジェクトの配列による指定があります。MessageクラスにもsetFlags()が存在しますが、このメソッドはそれを複数メッセージに対して一括で行う目的で存在するということになります。
フォルダ内のメッセージ数を返します。それぞれ、フォルダ内の全メッセージ数、フォルダ内のRECENTフラグがセットされたメッセージ数、フォルダ内のSEENフラグがセットされていないメッセージ数、フォルダ内にRECENTフラグがセットされたメッセージが存在するか否かを返します。
フォルダからメッセージを取り出します。各メソッドはもう説明不要でしょう。setFlags()と同様に、メッセージ番号を指定して複数のメッセージを取得するメソッドが用意されています。Folderオブジェクトを取得したら、open()を呼び出した後、これらのメソッドでMessageオブジェクトを獲得します。
取り出されたメッセージオブジェクトにヘッダやボディといった内容が含まれているとは限りませんが、取り出したMessageオブジェクトのメソッドを呼び出す時にダウンロードが行われます。
メッセージの絞り込みを行います。先に後者の方を説明しますと、渡されたメッセージ群の中から、termで渡されたSearchTermオブジェクトで表される検索条件にマッチするメッセージのみで構成された新たなMessageオブジェクトの配列を返します。
SearchTermのみをパラメタに持つ方はメッセージを取り出すメソッドの亜種と言えます。フォルダ内の全メッセージに対して指定された条件でメッセージの抽出を行います。具体的にはsearch(term, getMessages())と同じです。ただIMAP4の場合などは、サーバ上で検索機能を提供しているため、IMAPProviderでは何れのメソッドでもサーバに対して検索処理の依頼を行います。
検索条件オブジェクトについてはjavax.mail.searchの項を参照して下さい。
Flags.Flag.DELETEDフラグが設定されたメッセージを実際にサーバ上から削除し、削除されたメッセージを表すMessageオブジェクトを返します。ただし、SunのPOP3Providerではこのメソッドではなく、close(true)によってサーバからのメッセージ削除を行う事になっています。このメソッドはサーバとの接続中に削除を実行することを想定しているため、POP3の用に切断時に削除を行うプロトコルでは単純には実装できないためです。
このメソッド呼び出し後はメッセージ番号の再配置が行われることになります。従って、アプリケーションはメッセージを取り扱う場合にメッセージ番号は使用しないで、Messageオブジェクトを用いるべきとされています。
そのフォルダの子を表すFolderオブジェクトを取得します。存在しないフォルダに対してもFolderオブジェクトは取得できます。新しいフォルダを生成する場合はこのメソッドでFolderオブジェクトを取得してから、そのFolderのcreate()を呼び出す事になります。
DefaultFolderから順次getFolderを呼び出していくことでツリー上のFolderオブジェクトの階層を辿っていくことができます。
Folder f = store.getDefaultFolder().getFolder("会議").getFolder("飲み会日程");
この場合、フォルダ名にtypoがあってもFolderの取得自体は成功してしまう事に注意して下さい。
それぞれ、フォルダ内に存在するサブフォルダを表すFolderオブジェクトの一覧、または、フォルダ内に存在するsubscribeしているサブフォルダを表すFolderオブジェクトの一覧を返します。
patternによってリストアップするフォルダ名を指定できます。patternに"*"を指定すると、サブフォルダ中のさらなるサブフォルダも再帰的に辿られてリストアップされます。
また"%"を指定すると、このフォルダの直接のサブフォルダのみがリストアップの対象となります。これはパラメタを指定しない場合と等価です。また、"*"を含んだ文字列でpatternを指定すると、シェルでファイル名を指定する時のようにワイルドカードとして機能します。
フォルダ内のメッセージそのものに対する操作を行います。それぞれ「そのフォルダへのメッセージの追加」「そのフォルダから指定フォルダへメッセージのコピー」を行うものです。
Folderが発生させる各種イベントの取得を可能にするメソッド群です。
各イベントの意味についてはjavax.mail.eventの項を参照して下さい。
このフォルダから取得したメッセージ中のFetchProfileオブジェクトで指定された情報をダウンロードします。
このメソッドを使用しなくても、MessageオブジェクトのgetXxxx()メソッドでヘッダ情報を取得しようとしたときに自動的に該当情報のダウンロードは行われるのですが、コネクションを張りっぱなしにしたくない場合等で複数メッセージのヘッダのみを一括でダウンロードする場合などに使用します。
ダウンロードする情報の指定方法は、javax.mail.FetchProfileの項を参照して下さい。
このフォルダの親フォルダを取得します。デフォルトフォルダに対して呼び出した場合はnullを返します。
このフォルダを取得するために用いたStoreオブジェクトを返します。
POP3Folder#getUID(Message) (プロバイダが提供するメソッド)
POP3Providerが提供するcom.sun.mail.pop3.POP3FolderクラスにはgetUID()メソッドが定義されており、特定のメッセージのUIDLの結果を得ることができます。
また、UIDFolder.FetchProfileItem.UIDを使用してfetchを行う事で一度に複数のメッセージのUIDを得ることができます。
これを利用して2章のPOP3の項で説明したような、未ダウンロードメッセージの検出機能を実装することができます。
IMAP4プロトコルにもUIDというメッセージのユニークな番号を得るコマンドがあり、IMAPProviderの提供するcom.sun.mail.imap.IMAPFolderにはやはり、getUID()が定義されています。しかし、こちらはlongを返し、POP3Folder#getUID()はStringを返します。何れもプロトコルに固有の機能なのでjavax.mail.Folderクラスには取り込めなかった機能というわけです。利用するにはそれぞれの型にキャストしなければならず、即ちどのプロバイダを用いているのか把握していなければなりません。
ちなみに、IMAPFolderにはgetUID()以外にもプロトコル固有のメソッドがたくさん定義されています。これらの詳細はJavaMailのdocsディレクトリのsundocsにあるプロバイダのAPIリファレンス中に詳しく説明されています(本書では残念ながらプロバイダ固有の機能については深くは追求できませんでした)。
FetchProfileオブジェクトはFolderから取り出したメッセージオブジェクト中の、サーバからダウンロードしたいヘッダ/状態の種類を保持します。
メッセージオブジェクトを獲得したからと言って、そのヘッダやボディがメッセージオブジェクト中に格納されるわけではないのです。実際にはメッセージを表すクラスのgetXxxx()メソッドを呼び出した時に、返す情報がまだダウンロードされていなければ自動的にダウンロードが行われるので特に意識しなくてもよいのですが、さまざまなフォルダ上の様々なメッセージに対してばらばらにアクセスするその時々にサーバへのアクセスが走るのも困るので、予め想定される必要なヘッダ情報をダウンロードしておくことで、後から不要なアクセスが発生するのを防ぎたい場合に用います。
このオブジェクトをFolder#fetch()に渡すことで、メッセージオブジェクト中に指定されたヘッダ情報がダウンロードされます。
実際の使用方法はdemoのmsgshow.javaを見るのがよいでしょう。
このFetchProfileオブジェクトに、ダウンロードしたい種別情報やヘッダ名を追加します。
FetchProfile.Itemに指定できるのは、以下のようなものがあります。
| FetchProfile.Item.CONTENT_INFO | Content-Xxxx:ヘッダ群。ボディがどの様なものか調べることができます |
| FetchProfile.Item.ENVELOPE | メッセージの送信者/宛て先/Subject等。メッセージ一覧表示に利用できます |
| FetchProfile.Item.FLAGS | メッセージの状態フラグ。既読であるか否か等 |
| UIDFolder.FetchProfileItem.Item.UID | IMAPにおけるUIDやPOP3におけるUIDL |
上記以外のヘッダ/または上記に含まれていても特定ヘッダを個別に指定したい場合はadd(String headerName)を用います。
このFetchProfileオブジェクトに、指定したFetchProfile.Itemやヘッダ名が追加されているか調べます。
このFetchProfileオブジェクトに追加されている全てのFetchProfile.Item、ヘッダ名を配列として返します。
IMAPのdisconnected mode(切断中のメッセージに対する操作を後でサーバに反映可能にするような利用形態)で、メッセージのUID(Unique
identifier)を利用してメッセージの操作を行うためのメソッドが宣言されています。
UIDはメッセージ番号とは異なり、同じサーバ上では常にユニークな値で、UIDを利用すればサーバ上のメッセージの特定が可能です。メッセージ番号はexpunge等の操作により1から順番に再配置が行われるため、長期間同じメッセージを表す事はありません。
demoのuidmsgshow.javaで利用例が示されていますが、取得したFolderインスタンスがUIDFolderをimplementsしているか否かをinstanceofで調べて、implementsされていればUIDFolderの機能が利用できるようになるというものです。
UIDを指定して対応するメッセージオブジェクトを得ます。それぞれ、単一、任意の複数ID、IDの範囲指定となります。
範囲指定であるgetMessagesByUID(long start,
long end)のendにはUIDFolder.LASTUIDが指定できます。IMAPのUIDはより後のメッセージのIDがより大きい値となることが保証されているので、前回の最終メッセージのIDを覚えておいてstartに指定し、endにUIDFolder.LASTUIDを指定することで、新規メッセージのみを取得するといったことが可能になります。
また、getMessagesByUID(1, UIDFolder.LASTUID)で全メッセージ取得と等価になります。
メッセージオブジェクトからUIDを得ます。
IMAPのフォルダにおけるUIDValidity値を得ます。これはフォルダ内のメッセージのUIDが前回のセッションから変更されていないかチェックするために用います。
この値そのものは通常フォルダ生成日時の32ビット値とされます。そうすることでフォルダを削除して、同じ名前のフォルダを再生成した場合などにUIDValidity値が増加します。
クライアントはフォルダのUIDValidity値を覚えておいて、このメソッドで得た現在のUIDValidity値と比較することで、前回のセッション時に記憶しておいた各メッセージのUIDが以前有効かどうかを調べる必要があります。
javax.mail.FetchProfileItemを継承して、Folder#fetch()により、プロトコル依存のUID(ユニークID)を獲得するための定数UIDが定義されています。UIDFolderはIMAP専用といって良いものでしたが、UIDFolder.FetchProfileItemはJavaMail1.2で特例としてPOP3ProviderでのUIDLの取得のために使用できるようになりました。以下のような感じでFolder#fetch()を呼び出します。
FetchProfile fp = new FetchProfile();
fp.add(UIDFolder.FetchProfileItem.UID);
folder.fetch(folder.getMessages(), fp);
なお、実際に特定メッセージのUIDLを獲得するには、「POP3Folder#getUID(Message) (プロバイダが提供するメソッド)」に記したようにFolderオブジェクトをPOP3Folder型にキャストしてgetUID()を呼び出します。上記はあくまで一括でJavaMail内部に取り込むための処理です。
メイルの送受信に関する接続先を表すURLを表します。
RFC1738の「Common Internet Scheme Syntax」の記法で接続先を表します。
java.net.URLオブジェクトとの相互変換メソッドを持ちますが、現在公開されているRFC2192(IMAP
URL Scheme)、RFC2384(POP URL Scheme)等との互換性はありません。
ただ、FTP URL Schemeなどと同じ記法なので馴染みやすいかもしれません。
以下のような文字列をURLとして認識し、パース/出力する機能を持ちます。
imap://user:password@host:port/path/to/folder
※実は末尾に#refというhttpのnameアンカーを参照するものと同様の書式もサポートしていますが、アプリケーションの解釈の仕方が未定義なので現状は意味がありません。
URLNameクラスは文字列/URLオブジェクト/各パートの値のそれぞれをパラメタに取るコンストラクタを持ちます。三番目の各パートをパラメタに取るコンストラクタは、任意のパートをnullにしてもよいことになっています。
URLNameオブジェクトからURLオブジェクトへの変換結果を返します。ただ、これで得た結果を何に使うのか解りませんが…。
それぞれ、URLNameオブジェクト中の各パートの値を返します。
getFile()はフォルダのパスを表し、ポート番号の後の部分で'#'までの部分を返します。getRef()は'#'以降を返します。しかし、JavaMailでは'#'を含むURLを与えても無視されるだけですので意味はありません。
インターネットメッセージ/MIMEに特化したクラス群です。javax.mailパッケージのクラス群から継承しているものが多いですが、この章の最初で述べたとおり、ほとんどのケースでインターネットメッセージを扱うことを考え、ここで継承元のクラスのメソッドと合わせて説明することにします。
メッセージ送受信に必要なアドレスを表すクラスです。JavaMailに同梱されているサブクラスはInternetAddressとNewsAddressです。Addressクラスを直接使うことはまずありませんので、各サブクラスの説明を参照してください。
このAddressオブジェクトの種別を返します。サブクラスによって適切な主別名を返すようにオーバーライドされます。
このメソッドはJavaMail内部から呼び出されて、アドレスの種別に応じたプロトコルを決定するために使用されます。
プログラマが意識することはないのですが、参考までに、アドレスタイプについては、Session#getTransport(Address
address)の項で説明していますので参照してください。
インターネットメイルのアドレスを表すクラスです。
このInternetAddressオブジェクトが表現するアドレスの「メイルアドレス」の設定/参照を行います。ここで言う「メイルアドレス」は個人名やコメントを除いた純粋なaddr-spec部(local-part@domain-partで表されるもの)です。
InternetAddressではpersonalという属性があり、こちらに"木下"といった人名等を記述することができます。ちなみにコメントは解釈されず、取り出すこともできません。getPersonal()の説明を参照して下さい。
メイルアドレスの「名前」欄を操作します。インターネットメイルにおけるメイルアドレスは<>で囲まれたメイルアドレスと()で囲まれたコメント部、そしてその他に分けることができます。
JavaMailでは"<>"の手前の文字列をpersonalとして設定します。コメント"()"内は無視され、個別に取得することはできません。また"<>"の後ろに記述された文字列もparse時に捨てられるため取得できません。これは問題に思う人がいるかもしれませんが、ライブラリの挙動としては妥当なものでしょう。もし、これらの部分を取得したい場合はPart#getHeader()によりアドレスフィールドの文字列全体を取得して、独自に文字列解釈を行うことになります。
getPersonal()には一つ問題があり、JavaMail1.2の時点でpersonal部はquoteされた(""で囲まれた)内側もデコードされてしまいます。quoteされた部分文字列は何の改変もしてはいけないことになっていますので、これはRFCに違反した挙動といえます。しかし、日本で流れるメッセージの中にはエンコードした文字列がquoteされてしまっているような間違ったものも流れているため、この挙動によってたまたまquoteされた日本語もデコードできているという現実もありますので、実は実情に合った挙動であるという見方もできます。
setPersonal()はcharsetを指定するものと指定しないものの二種類ありますが、日本では間違いなくcharsetを指定する必要があります。他のPart#setSubject()などでも同様ですが、charset(文字エンコーディング)を指定しないタイプのset〜()メソッドで非ASCII文字を含む文字列が渡された場合、プラットフォームデフォルトエンコーディングを使用してエンコードが行われます。これではWindowsではShift_JISとなってしまい、Unix系であってもEUC-JPやISO-8859-1になってしまいます(*)。
*:ちなみにJavaMail1.1.3まではMIMEエンコードのcharset部分に"MS932"というJava言語内でのみ通用するエンコーディング名が現れたりしていました。これはJavaMailのMETA-INF/javamail.charset.mapが修正され、正しく"Shift_JIS"という名称が使われるようになったのですが、結局日本の慣習は全てISO-2022-JPを使う事になっているため、デフォルトエンコーディングが使用される状況はあってはならないということになります。
それぞれAddressオブジェクトから文字列への変換を行います。
toString()はObjectクラスのメソッドであり、デバッグ用途の使用に限るとされているにもかかわらず、Addressを文字列に変換する手段がtoString()しかないということになっています。また、このメソッドでは非ASCII文字がエンコードされた状態の文字列を返します。
JavaMail1.2ではデコードされた状態の文字列を返すtoUnicodeString()メソッドが追加されました。
staticメソッドの方は任意のAddressオブジェクトの配列を','区切りの文字列に変換します。
usedは「ヘッダの先頭行として既に消費されたバイト数」を表します。この二つのメソッドはRFC822に従って76バイトを超えるヘッダに対して自動的に改行を挿入(folding)(*)しますので、先頭行に対しては、ヘッダ名そのものが使用するバイト数を与える必要があるわけです。例えばTo:ヘッダに対するアドレスの出力を行いたい場合は"To:
"が先頭に含まれますのでusedに4を与えることになります。通常はJavaMail内部でこの計算が行われますので、プログラマが意識することはないのですが、もし自分でヘッダの出力を行うことがあるならこのメソッドを利用しましょう。
ただし、逆に複数のアドレスヘッダを一行の文字列として表現したい場合にはこのstaticなtoString()メソッドは使えません。こちらの用途には以下のようなメソッドを自分で用意する必要があります。
*:一つのヘッダフィールドが長くなる場合は改行(CRLF)+空白(SPACE/HTAB)を挿入してもよいことになっています。ヘッダ部で行頭が空白の場合は、前の行からの継続とみなされます。この折り返しのことをfoldingと呼び、折り返されたフィールドを元どおりにする事をunfoldingと呼びます。後の「folding/unfoldingについて」も参照してください。
public static String toSingleLineString(Address[] addresses) {
StringBuffer sb = new StringBuffer();
for (int i = 0; addresses.length; i++) {
if (i != 0) sb.append(", ");
sb.append(addresses[i].toString());
}
return new String(sb);
}
例えばGUIのTextFieldに表示したい場合等、改行されてほしくないので上記のように一行の文字列にしたい場合もあるでしょうね。
文字列からInternetAddressオブジェクトを得ます、','で区切られて複数のアドレスが記述されたものをまとめて解釈してInternetAddressの配列として返します。strictにtrueが指定された場合は、アドレスとして切り出されたトークンに対してRFC822水準のチェックが行われます。falseの場合はメイルアドレス文字列のチェックは行わずに単に各フィールドにアドレスとおぼしきトークンを設定します。
パラメタが一つのparse()メソッド(strictパラメタを持たない)は、strict
= trueとして解釈します。
渡されたSessionオブジェクトに設定されたPropertiesオブジェクトの情報を元に、自身のメイルアドレスを生成します。
アドレスのドメインパート(右端の'@'より右側)についてはSessionに渡すPropertiesオブジェクトの"mail.host"の値が使用されます。このキーの値が存在しない場合、またはSession自体が存在しない場合(このメソッドにnullが渡された場合)は、java.net.InetAddress.getLocalHost().getHostName()で得られる文字列が用いられます。
アドレスのローカルパート(右端の'@'より左側)のほうは以下の順番で検索を行い、最初に見つかったものが使用されます。
しかし、JavaMail1.2の時点で、これによって生成されるメイルアドレスには問題が含まれる可能性があります。
まず、ドメインパートについてですが、Sessionに渡すPropertiesオブジェクトの"mail.host"が設定されていない場合は、java.net.InetAddress.getLocalHost().getHostName()で得られる文字列が用いられると書きましたが、これによって得られるホスト名は正しいFQDNにならない場合が多くあります。
そもそもそのユーザのメイルアドレスのドメインパートがローカルホストのドメイン名であることすら現在のパソコン全盛の状況では少ないといえますので、このメソッドが正しく動作するためには、Sessionに渡すPropertiesオブジェクトの"mail.host"を正しいFQDNにしましょうということになります。
おっと、そもそもgetLocalAddress()を使わなければそんなことを考える必要はないんですね。ではgetLocalhost()を使用している箇所はというと、以下のような場合に使用されます。
問題は4です。現在の実装ではMessage-ID:ヘッダにこのメソッドで得られるメイルアドレスを必ず用いるようになってしまっています。このため、このメソッドで生成されるアドレスが全世界でユニークなものでなければ、Message-ID:が正しいとはいえなくなってしまいます。詳しくは「JavaMailにおけるMessage-ID:ヘッダの扱いについて」に記していますが、結論としてはこのメソッドはMessage-ID:生成専用のものと割り切って、少なくとも上記の1-3のケースでは使われないようにする方が良いでしょう。
1のような状況には当然しないはずですのでこれは問題ないですね。2:についてもこのメソッドを使用することはない(する必要はない)ですのでやはり問題になりません。
3は便利なメソッドですがやはりこの問題を考えるとあまり使用したくはないところです。返信メッセージの生成は後のMessage#reply()の項で述べる通り、自分で実装してもたいした手間ではないでしょう。
JavaMailにおけるMessage-ID:ヘッダの扱いについて
JavaMailではMimeMessage#saveChanges()メソッドが呼び出された時に、強制的にMessage-ID:ヘッダの付加が行われます。ここで設定されるMessage-ID:は"<hashcode(ここでは乱数のようなもの)>.<currentTime>.JavaMail."
+ InternetAddress.getLocalAddress(session).getAddress();という形式なのですが、既に述べた通り、InternetAddress#getLocalAddress()の結果は、Sessionに渡すPropertiesオブジェクトに適切な設定がされていないとプラットフォームによっては怪しげなドメイン部が生成されてしまいます。
Sessionに渡すPropertiesオブジェクトの"mail.from"にインターネットで有効なメイルアドレスが、または"mail.host"に適切なFQDNが設定されていればそれほど問題ではないのですが、本書ではなるべくSessionに頼らないで、指定すべき時に指定すべき情報を指定できるような設計を目指していましたので、この問題は目の上のタンコブです。
この問題のために仕方なくSessionに渡すPropertiesオブジェクトの内容を設定するように本書のサンプルを書き直しましたT_T。
さて、何も指定しない場合にローカルホストのホスト名(場合によってはいわゆる「コンピュータ名」!!)が設定されてしまうのはだめだめなのですが、では何が設定されればよいのでしょう?Message-ID:が世界中でユニークである事を保証するための最低条件として、最後の'@'より右側にFQDNが設定されていて、且つ'@'の左側がそのドメイン内で一意である、ということがありますので、接続するメイルサーバのドメイン名にするのがよさそうですが、接続するサーバすらプライベートなホストである可能性もありますので、最も適切なのはインターネットで通用するメイルアドレスを用いる事といえます。
解決方法は、ユーザまたはプログラムは、必ず送信者メイルアドレスを指定するものとし、それをSessionに渡すPropertiesオブジェクトの"mail.from"に設定するという方法を取りました。これなら、'@'の左側も少なくとも他人と重複することはほぼ完全に無くなります。
ところで、先に述べたように"mail.host"にFQDNを設定するという方法もあるのですが、"mail.host"はTransport/Storeのデフォルトの接続先としても用いられます。ただ、これらはそれぞれ、"mail.smtp.host""mail.imap.host""mail.pop3.host"のようにそれぞれより優先されるキーがありますので、接続先としてはそれらを用いて、"mail.host"はMessage-ID:に使われるドメイン名を設定するためのものと決め打ってしまう事で一応の回避とする方法です。
しかし、"mail.smtp.host""mail.imap.host""mail.pop3.host"への設定をうっかり忘れてしまうと"mail.host"に設定されたホストに接続しようとするわけです。このような設計はバグの発見を遅らせる原因になってしまいますね…。
本書ではSessionに渡すPropertiesオブジェクトへの設定を行う部分を隠蔽し、常にメソッドのパラメタで指定させるようなラッパクラスを用いるようにしました。アプリケーションが文字列をキーにして処理や設定を指示するのは危険であり、筆者としてはJavaMailの最もよくない点と考えています。しかし任意の拡張機能や隠し機能の追加が容易であり、Providerという仕組みの性質上致し方ない設計なんですね(それでも筆者はメソッドを追加した方がよいと考えていますが)。
話を戻しますが、実は全く別の方法としてMimeMessage#updateHeaders()をオーバーライドして、saveChanges()時に付加されたMessage-ID:のドメインパートを書き直すという方法もあります。そして実はこの方法ならMessage-ID:の内容を完全にプログラマの思い通りにすることができてしまいます。しかし、この方法はMessage#reply()が返すMimeMessageオブジェクトのようにライブラリ側で生成されるものについては効果がありませんし、Message-ID:を自力で生成するのは大変ですので本書では採用しませんでした。MimeMessage#updateHeaders()が呼び出された時に既に設定されているMessage-ID:の後半部分のみ書き換えるようにすれば自分でnewするMimeMessageサブクラスのオブジェクトに対しては安全になりますが、いずれにせよ設定すべき値は外部から渡してもらっておく必要がありますね。
なお、Date:ヘッダと同様にMessage-ID:を付加しなかった場合に自動的に付加するMTAも存在します。しかし、それに期待することは本来は誤りで、Message-ID:は(Date:も)RFC822メッセージの一部なので、MUAが生成することが前提です。
しかし、MTAに付けさせればユニーク性がほぼ完全に保証できますので、積極的にMTAに付けさせましょうという意見もあります。この問題についてはMSA(Message
Submission Agent:RFC2476)なるもので補完をしようという動きもあります。
USENET(NetNews)のNewsGroupを表すクラスです。サードパーティのNNTPプロバイダがないと意味を成しません。
NewsAddressはRFC1036で定義されたnewsgroupを表します。hostも指定可能になっていますが、現状特に意味をなしていません。NNTPプロバイダの実装次第であり、本書の範疇からはずれてしまいますので個々の説明は省略します。
メッセージの宛て先種別を表すクラスです。
TO/CC/BCCという三つの定数を持ち、Message#setRecipient()等では、これらの何れかを指定します。
インターネットメッセージにおける宛て先種別としてNEWSGROUPSフィールドが追加されています。
NNTPプロバイダをインストールしていれば、msg.setRecipients(MimeMessage.RecipientType.NEWSGROUPS,
new NewsAddress("fj.comp.lang.java"));のように宛て先としてNetNewsのグループを指定して送信することができます。
Partはメッセージを構成するパートを抽象化したinterfaceです。これにはメッセージそのものも含まれています。「ヘッダ部とボディ部を持つもの」を表すと考えればよいでしょう。
マルチパートメッセージの各パートはメッセージそのものと同様にヘッダ部とボディ部を持つため、これらをまとめて抽象化したinterfaceというわけですね。
これを実装したクラスとして、メッセージ全体を表すMessage(MimeMessage)クラスや、マルチパートメッセージにおけるそれぞれのパートを表すBodyPart(MimeBodyPart)クラスがあります。
Part interfaceにおけるヘッダ操作インターフェイスは、全てのヘッダに統一的にアクセスするsetHeader()/addHeader()/getHeader()/removeHeader()に加え、全てのパートに記述可能ないくつかのヘッダにアクセスするためのコンビニエンスメソッドから成ります。
このPartオブジェクトの指定されたヘッダの全ての内容を返します。同じヘッダが複数存在する場合もあり、その場合、その一つ一つがString配列の要素になります。
注意しなければならないのは、構造化フィールドなどで、一行に複数の要素が記述できるものであっても、それが分割されることはないという点です。
:
To: shin@sk-jp.com, kumi@sk-jp.com
To: rio@sk-jp.com
:
このようにTo: が複数行存在した場合、getHeaderの返す配列の内容は以下のようになります。
message.getHeader("To")[0] ->
"shin@sk-jp.com, kumi@sk-jp.com"
message.getHeader("To")[1] ->
"rio@sk-jp.com"
一つの要素に一つのメイルアドレスが格納されるわけではないというわけですね。To:の場合はgetRecipients()メソッドを用いればちゃんとメイルアドレス単位で分割してくれますが、そのような特化メソッドが用意されていないヘッダの場合は注意が必要です。
MimePart#getHeader(String header_name, String
delimiter)の方を用いたほうがこのような意識をしなくてすむでしょう。
このPartオブジェクトに対して指定したヘッダを付加します。
既に該当ヘッダが存在する場合は、「上書き」になります。すなわち、既存のものを削除して、今回指定したものを新たに付加することになります。
指定ヘッダを追加します。既存のものには手を加えません。
setHeader()/addHeader()に渡すheader_valueに日本語を用いる場合は、javax.mail.internet.MimeUtility#encodeText()を用いてRFC2047形式のエンコードを施す必要があります。
構造化フィールド(メイルアドレスを設定するヘッダなど)に関してはそれを設定するためのメソッドが別に用意されていますので、そういったものが用意されている場合は基本的にそちらを使う方がよいでしょう。
2章の各ヘッダフィールドの説明に、それを設定するためのメソッドが存在するものについては記載しています。
指定ヘッダを削除します。固有の設定/参照メソッドが用意されているヘッダフィールドについてはそちらのメソッドを用いて削除する方が良いでしょう。例えばsetFrom(null)とする事でFrom:を削除することができます(*)。
*:ただ、この点については議論の余地はあるかもしれません。removeHeader()というメソッドはヘッダを削除する事を明快に表しているのに対し、setRecipient(type, null)やsetFrom(null)からはヘッダを削除するという意図が読み取り辛い可能性があるためです(3章のSimpleSender.javaではこのような使い方をしています)。ただ、removeHeader()はヘッダ名を文字列で与えるものですので、細かいこととはいえ、筆者は意味のあるキーワードを文字列リテラルで渡すようなプログラミングは避けるべきと思います。
このPartオブジェクト内の全てのヘッダを列挙します。
返されたEnumerationの各要素はjavax.mail.Headerオブジェクトです。
以下のようにしてヘッダを順番に処理していくことができます。
java.util.Enumeration enum = part.getAllHeaders();
javax.mail.Header header;
while (enum.hasMoreElements()) {
header = (Header)enum.nextElement();
if ("Received".equalsIgnoreCase(header.getName())) {
:
}
if ("X-Received".equalsIgnoreCase(header.getName())) {
:
}
}
指定されたヘッダ/または指定されたヘッダを除く全てのヘッダを列挙します。
返されたEnumerationの各要素はjavax.mail.Headerオブジェクトです。
このメソッドはヘッダの一覧を表示/格納したい場合など、列挙されるヘッダ群の意味を区別せずに同じ処理を施したい場合に使うと便利です。逆にヘッダの種類に応じて処理を切り替えるような場合はgetAllHeaders()で十分でしょう。理由はヘッダ毎にheader_namesの要素数分の無意味な比較が入るためです。
Content-Type:ヘッダの内容を得ます。MIMEタイプ;パラメタの形式です。
返される文字列はヘッダの内容そのものなので、MIMEタイプを調べたい場合は、ここで返された文字列を調べるより、isMimeType()メソッドを用いた方がよいでしょう。
また、パラメタのチェックを行いたい場合は、ContentTypeオブジェクトが利用できます。
ContentType cType = new ContentType(part.getContentType());
String charset = cType.getParameter("charset");
のように、charsetパラメタを取り出すことができます。
このPartオブジェクトのMIMEタイプが指定されたものであるかを調べます。パラメタ部分のチェックは行われず、純粋に、メディアタイプ/サブタイプのみがチェックされます。
メディアタイプのみを比較したい場合には、"multipart/*"というように、サブタイプに'*'を指定することができます。
このメソッドはContentTypeクラスのmatch()メソッドを利用してテストを行うコンビニエンスメソッドです。
このPartオブジェクトのContent-Description:ヘッダの設定/参照を行います。Content-Description:ヘッダについては2章を参照して下さい。
このPartオブジェクトのContent-Disposition:ヘッダの設定/参照を行います。
普通は"attachment"か"inline"になり、これらはPart.ATTACHMENT/Part.INLINEという定数が用意されていますので、これらを指定する方が間違いがないでしょう。また、setDisposition()で渡す文字列中にファイル名等のパラメタを記述しても構いませんが、Content-Disposition:ヘッダの"filename"パラメタについてはset/getFileName()という専用のメソッドが用意されています。ただ、何れの方法をとっても日本語を含むファイル名を直接設定/取得することはできません。getFileName()/setFileName()の説明を参照してください。
また、取得したContent-Disposition:ヘッダのパラメタのチェックを行ったり、添付ファイルの更新日時等を取得したい場合は、ContentDispositionオブジェクトが利用できます。
ContentDisposition disposition = new ContentDisposition(part.getDisposition());
String creationDate = dispositioin.getParameter("creation-date");
このようにすれば、creation-dateパラメタを(存在するなら)取り出すことができます。
ただし、このクラス(に限らずJavaMail全般に言えることですが)は、RFC2231で規定されたヘッダのパラメタ部のfolding(複数パラメタに分割して折り返す)および非ASCII文字のエンコード/デコードに対応していませんので、RFC2231形式で記述されたfilenameパラメタを取得できません。getFileName()メソッドも上記の方法でfilenameを取得しようとするので同様の問題があります。
もしそのパート自身に関連付けられたファイル名があればそれを返します。
Content-Disposition:のfilenameパラメタ、それが存在しない場合は、Content-Type:のnameパラメタが該当します。
ファイル名に日本語が含まれていた場合の処理は、JavaMail1.2では全く行われません。従って、自分で解釈する必要があるのですが、現在この添付ファイル名に対する非ASCII文字の扱いは混沌としています。
RFCではRFC2231にContent-Dispositionを含むヘッダのパラメタ部に非ASCII文字を記述する方法が規定されているのですが、現状これに準拠しているMUAがまだ少なく、RFC2047のヘッダにおけるエンコード方法をそのままパラメタ部に適用していたり、非ASCII文字をそのままパラメタ部に記述したメッセージがインターネット上を流れています。
つまりは、これら全てを解釈できるように自分で実装する必要があります。
JavaMailがRFC2231で規定されている非ASCII文字をパラメタに記述する記法に対応していない件は、RFEとしてBugParadeにも記載があるのですが、まだ当分サポートする気がないようです。まあ、実際解釈できるMUAが少ないからというのは確かではありますが、だからといってJavaMailもいつまでたってもサポートしなければ、非ASCIIファイル名対応が遅れるばかりですね(受信できる人がいないエンコード方法では誰も送信したがりませんから、まずは誰もが受信できるようになるのが先決ですね)。
皆さんVOTEしましょう:)。
http://developer.java.sun.com/developer/bugParade/bugs/4107342.html
さて、送信に関してはともかく受信はできなければまずいので、自分で実装するわけですが、現在インターネットで流れている日本語添付ファイル名の形式は何通りもあり、RFC2231に準拠した解釈処理だけ組み込んでもやはり日本語ファイル名を表示することはできない場合が多いでしょう。そういうわけで、ある程度幅を持たせて解釈できるようにしてあげなければなりません(JavaMailそのものは今後どう転んでもRFC2231形式以外の形式を解釈できるようにはなりません。扱いづらいRFC2231に変わる仕様が出てきた場合は別ですが)。
章末に記載したdecodeParameter()メソッドを通すことでだいたいの日本語ファイル名について正しいUnicode文字列に変換できると思います。ご利用下さい。
そのパートを表すファイル名を設定します。具体的にはContent-Disposition:ヘッダのfilenameパラメタと、Content-Type:のnameパラメタを設定します。
setFileName()に限らずエンコーディングを指定しないヘッダ設定メソッドの全てに言えることですが、日本語を含むファイル名を単に設定した場合、JavaMailはその文字列をASCIIと見なして上位バイトを切り落としてしまいます(具体的には(byte)headerValue.charAt(i)のように単にキャストされてしまいます)。
このため、アドレスヘッダのためのInternetAddressやsetSubject()等は文字エンコーディングスキームを指定できるようになっているのですが、JavaMail1.2ではまだsetFileName()についてその対応がされていないということです。
従って、日本語のファイル名を設定するためには、自分でエンコーディングした文字列を設定しなければなりません。
しかし、getFileName()の項に書いた通り、現状はこのfilenameパラメタに非ASCII文字を設定しようとした場合、どのような方式であっても全てのメイラで閲覧可能にはなりません。
現状もっとも妥当な方法は、Content-Disposition:にはRFC2231に準拠した方式で日本語を含むfilenameパラメタを記述し、Content-Type:のnameパラメタに対してヘッダ(のパラメタ以外)に適用されるMIME B エンコーディングを施したものを設定するというものです。
こうすることによって、RFC2231に準拠したメイラではContent-Disposition:から正しいファイル名を取得でき、RFC2231に対応していないメイラではfilenameパラメタを見つけられない(RFC2231の形式では"filename*"といったパラメタ名になるため)ので、多くのメイラがContent-Type:のnameパラメタを参照してファイル名を復元しようとします。
RFC2231形式もMIME B エンコーディング形式も解釈しないメイラでは、ファイル名として"=?ISO-2022-JP?B?・・"といったContent-Type:のnameパラメタに記述されたMIME B エンコーディングされた文字列をそのまま表示してしまうでしょうし、Content-Type:のnameパラメタを参照しないようなメイラがもしあれば、ファイル名無しとみなされてテンポラリファイル名が表示されるかもしれませんが、仕方ありません。現在、前述の方法で送ったメイルの添付ファイル名をデコードできないメイラは少数派です。
さて、ややこしい説明が長くなってしまいましたが、setFileName()のラッパとして前述のエンコードを行うメソッドも章末にご紹介しますのでご利用くださいませ。
このPartオブジェクトのボディの行数を返すことになっています。このメソッドは行数を判断できない場合は-1を返すことになっており、ほとんどの場合は-1が返されます。API仕様としてはContent-Transfer-Encoding:のデコードを行うかもしれないし行わないかもしれないというように書かれており、基本的に信頼できるものではないということになります。
同梱のプロバイダの場合、IMAPProviderのときだけFetch応答の値を返すようです。
このPartオブジェクトのボディのバイト数を返します。プロバイダから返されるPartオブジェクト(Message/BodyPartのサブクラス)は常に正しいボディサイズを返すと考えてよいでしょう。パート全体のサイズではない(ヘッダのサイズは含まない)事に注意して下さい。
JavaMail1.2の実装においては、アプリケーションがnew
MimeMessage()として生成したものの場合は、コンストラクタでストリームやバイト配列を渡してボディも同時に生成した場合はそのサイズを返しますが、setContent()やsetDataHandler()で設定された任意の型のボディに対しては、サイズの計算が行われないため-1を返す事に注意が必要です。
RFC822にはヘッダの1フィールドが長い場合にそれを<CRLF>を挿入する事で折り返して2行以上で表現してもよいことになっています。折り返されて継続している行は行頭がSPACE/HTABの何れかになります。これをfoldingと呼びます。この<CRLF>の挿入は人間の可読性を上げるために推奨されているものですが(SMTP自体は最近のものなら1行は1000bytesまで許容します)、コンピュータが解読する時には邪魔になる場合がありますので、元の一行に戻す操作が必要になります。これをunfoldingと呼びます。
注意:RFC822ではfoldingを行う基準として「65または72bytesを超える場合」という風に記されていますが、RFC2047のencoded-wordに関する記述ではencoded-wordが75bytesを超えてはならないといった記述があります。現在は慣習的に<CRLF>の2bytesを含めて76bytesを超えないようにするというのが基本となっているようです。さて、JavaMailはこれらの処理をどの程度までサポートしてくれているのでしょう?
まず、foldingについてですが、Subject:に日本語を用いてmessage.setSubject("長い長い・・・題名です",
"ISO-2022-JP");のように設定したSubject:は正しく(RFC2047に従って)foldingが行われます。
しかし、MIMEエンコードを伴わないmessage.setSubject("long
long ・・・ subject.");では全くfolding処理が行われません。非ASCII文字を含まないSubject:をsetSubjectメソッドで設定する際は、それがあまりに長い場合はアプリケーション側でfoldingを行わないとRFC的に望ましくないということになります。
他にも、メイルアドレスを設定するヘッダに対しては、そのヘッダ設定メソッドを用いる限りは、内部的にInternetAddressクラスの機能によってfolding処理がなされるのですが、固有の設定メソッドを持たないヘッダを設定するときに用いるPart#setHeader()などは全くfoldingを行ってくれません。
foldingする/しないについては、最近のメイラでは通常表示されないヘッダの中での問題であり、そこまで神経質になる必要がないといえばそれまでなのですが、万が一ヘッダが1000bytes(MTAによってはもっと少ないこともある)を越えるようなことがあると、MTAに配送を拒否されてしまうかもしれません。References:などは長くなりがちなヘッダですので設定する場合はちゃんとfoldingすべきでしょう。
次にunfoldingについてです。
これもRFC2047の定義に従い、隣接するenoded-word(MIMEエンコードがされた文字列)間の<CRLF>+空白はデコード時に正しく取り除かれます。また、メイルアドレスを記述するヘッダの内容取得メソッド等は文字列ではなくJavaのオブジェクトとして復帰値を返すため、foldingされていることを意識する必要はなくなっています。
しかし、SubjectにおいてのASCII部分とencoded-wordの境界やASCIIのみの部分でfoldingされたものや、foldingの場合と同じようにヘッダの種類に特化した取得メソッドを持たないものに対して使用するPart#getHeader()などで取得したヘッダの内容はunfoldingされていません。
これはJavaMailというライブラリ内でunfoldingを行ってしまうと余計なお世話になってしまう可能性も否定できないため、MIMEデコード以外については元のヘッダの状態を保証しようとしているという考え方もできますが、この仕様により、アプリケーション側でunfoldingを行わなければGUIのTextField等に表示しようとした時に不都合が発生してしまいます。
このようなわけで、JavaMailを普通に使っていると、設定したヘッダが折り返されない/取得したヘッダに改行が入っているといった問題が出るケースがあるというわけです。ここで、folding/unfoldingを行うツールメソッドをご紹介しておきます。なお、fold()メソッドの方はソースのコメントにもある通り、そこまで厳密な処理にはしていません。
/**
* header valueの folding を行います。
* <P>
* white spaceをfolding対象にします。<BR>
* 76bytesを超えないwhite space位置に<CRLF>を挿入します。
* </P><P>
* 注:quoteを無視しますので、structured fieldでは不都合が
* 発生する可能性があります。
* </P>
* @param used ヘッダの':'までの文字数。76 - usedが最初のfolding候補桁
* @return foldingされた(<CRLF>SPACEが挿入された)文字列
*/
public static String fold(String source, int used) {
if (source == null) return null;
StringBuffer buf = new StringBuffer();
String work = source;
int lineBreakIndex;
while (work.length() > 76) {
lineBreakIndex = work.lastIndexOf(' ', 76);
if (lineBreakIndex == -1) break;
buf.append(work.substring(0, lineBreakIndex));
buf.append("\r\n");
work = work.substring(lineBreakIndex);
}
buf.append(work);
return new String(buf);
}
/**
* header valueの unfolding を行います。
*/
public static String unfold(String source) {
if (source == null) return null;
StringBuffer buf = new StringBuffer();
boolean skip = false;
char c;
// <CRLF>シーケンスを前提とするならindexOf()で十分ですが、
// 念のためCR、LFいずれも許容します。
for (int i = 0; i < source.length(); i++) {
c = source.charAt(i);
if (skip) {
if (isLWSP(c)) {
continue;
}
skip = false;
}
if (c != '\r' && c != '\n') {
buf.append(c);
} else {
buf.append(' ');
skip = true;
}
}
return new String(buf);
}
public static boolean isLWSP(char c) {
return c == '\r' || c == '\n' || c == ' ' || c == '\t';
}
これらのメソッドは筆者のライブラリのMailUtility.javaに含まれています。5章のソースコードではMailUtility.unfold(source)のように使用している箇所が出てきますが、これは上記のメソッドの事です。MailUtility.javaは筆者のWebサイトから取得できます(http://www.sk-jp.com/java/library/)。
Part interfaceにおけるボディ部操作インターフェイスは、そのパートの本文となるテキストや画像ファイルなどのボディを設定/参照するメソッド群です。
このパートのボディ部を取得します。
getDataHandler().getContent();と等価なコンビニエンスメソッドです。返されるオブジェクトの種類は、JavaMailがJAFに登録したDataContentHandlerの種類だけあります。JavaMail1.2においては、そのボディのMIMEタイプに応じて、後のsetContent()の項で示したオブジェクトが返されますので、適切にキャストして使用する事になります。setContent()の項に示していないMIMEタイプだった場合は、次のgetInputStream()の復帰値と等価なInputStreamが返されます。
このパートのボディ部をストリームとして返します。これによって返されるのはContent-Transfer-Encoding:に基づいてデコードされた結果のストリームです。Content-Transfer-Encoding:が間違って付けられていた場合などは、このメソッドで返されるストリームの中身はぐちゃぐちゃになってしまいます。このような場合は、getRawInputStream()を用いて、デコード前のストリームを取得してから、MIMEUtility#decode()を用いて実際のエンコーディングを推測して変換するなどの手を講じる必要があります。
setDataHandler(new DataHandler(obj, type));と等価なコンビニエンスメソッドです。typeにはContent-Type:の内容となるMIMEタイプを示す文字列を指定します。Content-Type:の内容ですので、charsetパラメタを付加することもできます。
JavaMailがデフォルトでサポートするMIMEタイプと渡すべきobjの内容の対応は以下のようになっています。
| text/plain | Stringオブジェクト |
| text/html | Stringオブジェクト |
| text/xml | Stringオブジェクト(JavaMail1.2でサポート) |
| multipart/* | MimeMultipartオブジェクト |
| message/rfc822 | MimeMessageオブジェクト |
textメディアタイプについてですが、JavaMailでは、text/plainとtext/htmlおよびtext/xmlのMIMEタイプについてDataContentHandlerを提供しています。これら以外のtextメディアタイプ(例えばtext/enriched等)のボディを普通にsetContent()で設定した場合、UnsupportedDataTypeExceptionが発生してしまいます。上記5種類以外のMIMEタイプのデータを使用する場合は、F3.4.3.に記したByteArrayDataSourceのようなカスタムデータソースとDataHandlerを使用して、setDataHandler()メソッドでボディを設定する必要があります。
このパートのボディ部に相当するオブジェクトを設定します。ボディ部がマルチパートコンテナとなる場合に使用します。
Content-Type: text/plainのパートの本文を設定するためのコンビニエンスメソッドです。しかし、日本語を設定する場合は(ISO-2022-JPで送信すべきにもかかわらず)このメソッドはプラットフォームデフォルトエンコーディングを元にContent-Type:のcharsetパラメタを決定してしまいます。日本語を送信する場合はこのメソッドは用いず、MimePart#setText(text,
encoding)を用いて"ISO-2022-JP"を指定するようにして下さい。
なお、これは以下のようにsetContent()メソッドでContent-Type:全体を明示的に指定した場合と等価です。
part.setContent(messageString, "text/plain; charset=ISO-2022-JP");
この方法であればMimePart(MimeMessage等)にキャストしなくても、静的なMessage型/Part型に対して直接呼び出す事ができますので、他のMimePartのメソッドを利用しないならこの方法を用いるのもよいでしょう(*)。
*:文字列で指定される箇所を極力減らすべき(静的なチェックの強化)という立場からは推奨できませんが、利便との天秤であり趣味の範疇とも言えるでしょう。
このパートのボディを表すDataHandlerを取得します。
DataHandlerオブジェクトはバイト列とMIMEタイプから、MIMEタイプに応じたJavaオブジェクトを生成したり、逆にJavaオブジェクトを適切なバイト列に変換することを可能にするものです。
DataHandler#getContent()を呼び出すことで、そのDataHandlerに設定されたデータをJavaオブジェクトとして返します。
通常の使い方をしているかぎりはgetContent()で事足りるはずです。
このパートのボディを表すDataHandlerを設定します。
DataHandlerについては3章のJAFについての説明を参照してください。
通常はsetContent()を呼び出す事で適切なDataHandlerが設定されるようになっています。
このPartオブジェクトをRFC822形式で出力します。
RFC822形式については2章でご紹介したような、SMTPプロトコルでやりとりされるメッセージ形式です。
MimePartはメッセージを構成するパートを抽象化したinterfaceです。これにはメッセージそのものも含まれています。
Part interfaceを継承してインターネットメイルに特化した機能が追加された形になっており、Partの持つ機能はすべて持ちます。ここではMimePartで宣言されているメソッドについて説明します。
なお、MimePartで宣言されているメソッドはコンビニエンスメソッドばかりです。つまり、Partのインターフェイスを使って同様の効果を得ることができるようなものです。
たとえば日本語のメッセージを構築するために便利なメソッドであるsetText(text,
charset)メソッドはMimePart interfaceにしかありませんので、このメソッドを使用するときは、変数の型(静的な型)をMimePartかその実装クラスとして扱わなければなりませんが、先に記したようにPart#setContent()で同様の効果が得られます。どちらの方法を使うかといえばその時のオブジェクトを扱う静的な型に応じて楽な方法を用いればよいでしょう。
RFC822形式のヘッダを追加します。"ヘッダ名:
内容"の形式で指定します。
addHeaderでは(name, value)を渡すことで":
"の補完をしてくれましたが、このメソッドでは渡された行(単一行とは限らない)がパートのヘッダ領域にそのまま追加されます。
addHeaderではなくこちらのメソッドを使用しなければならないケースというのはほとんど考えられませんので、このメソッドを使用する機会はほとんどないでしょう。
指定ヘッダの値(複数あり得る)を、指定されたデリミタで区切って連結された一つの文字列として返します。
例えば、
To: shin@sk-jp.com, kumi@sk-jp.com To: rio@sk-jp.com
のようなヘッダがあった場合、getHeader("To", ", ");は"shin@sk-jp.com, kumi@sk-jp.com"と"rio@sk-jp.com"を連結して、"shin@sk-jp.com, kumi@sk-jp.com,rio@sk-jp.com"を返します。
また、このメソッドはdelimiterにnullを与えると最初に見つかった一つだけを返すという挙動になり、返されるものはgetHeader(headerName)[0]に等しくなります(こちらはNullPointerExceptionになる可能性があるのであまりよくないです)。JavaMailのコアAPI中でも多用されていますが、本来ヘッダの項目中の最初の一つだけに作用するという挙動はあまり存在しないものです(要するに手抜きの場合が多い)。厳密にRFCに従うならば、大概の場面でヘッダが複数存在した場合の処理は必要となりますので、製品となるようなプログラムでこれらの使用法をする場合は、本当にそれでよいのかを再確認するようにしましょう。
例えばRFCに規定されている/いないに関らず、複数のヘッダ項目が現れる可能性を考えなくても良いようなプロジェクトや運用環境の条件が合えば、記述が楽で見やすいこれらの手法を用いてもよいでしょう。
Part interface のgetAllHeaders()/getMatchingHeaders()/getNonMatchingHeaders()と同様のものです。ただし、こちらは返されるEnumerationがヘッダ行全体を表すStringオブジェクトの列挙になります。
addHeaderの説明と同様にこの形式で必要だという状況はあまりありませんので使用する機会は少ないです。ただし、ヘッダがヘッダの形式をなしていない(壊れている)ような場合に何らかの処理を行いたい場合はこれらのメソッドを利用できます。
Content-Transfer-Encoding:ヘッダの内容を取得します。つまり文字エンコーディングではなく転送エンコーディングを返します。
ヘッダが存在しない場合は、nullが返されます。
Content-Id:ヘッダの内容を取得します。Content-Id:は特定のパートを識別するユニークな文字列です。詳細は2章をご覧ください。
JavaMail1.2では、なぜかContent-Id:ヘッダを設定するメソッドがMimeMessageにしかありません。これは要求を出したところ今後のリリースでMimePartの方に定義するように修正するとのことですので、次のリリースではMimePart#setContentID()が追加されることでしょう。
Content-Language:ヘッダの内容を取得/設定します。
Content-Language:については2章をご覧ください。
Content-MD5:ヘッダの内容を取得/設定します。
Content-MD5:については2章をご覧ください。
なお、Content-MD5:を設定したり、受信したボディとこのヘッダの内容との照合を行う場合は、コアAPIのjava.security.MessageDigestクラスを用いて自分でダイジェストを生成する必要があります。ただ、このクラスはbase64エンコードを行ってくれませんのでこれを自分で行う必要があります。あるいは逆にContent-MD5:ヘッダの値の方をデコードして128Bitのダイジェストに変換したものとMessageDigest#isEqual()によって比較する方法もあります。
Content-Type: text/plainのパートの本文を設定するためのコンビニエンスメソッドです。charsetパラメタ部も指定可能になっています。日本語のメイルの場合setText("日本語",
"ISO-2022-JP")としましょう。
なお、これはあくまで、"text/plain"に対するものです。他のtext/*(text/htmlやtext/xmlなど)は、普通にsetContent(text,
mimeType)を用います。もちろん"text/plain"に対してsetContent()を使っても全く問題はありません。
BodyPartは、マルチパートメッセージのパートツリーの子パートを表すクラスです。
このクラスだけでなく、BodyPart/MimeBodyPartとMessage/MimeMessageはいずれもPart/MimePart
interfaceを実装しています。すなわち、いずれも
is a Part ですので、すべて同じようにPartとして扱うことができます。
パートのツリー構造のルートはMessage(MimeMessage)オブジェクトとなります。
BodyPartオブジェクトはMultipartオブジェクトにaddするためのオブジェクトであり、マルチパートではないメッセージには存在しません。
このBodyPartオブジェクトがaddされている親Multipartオブジェクトを返します。パートのツリー構造の親を辿る場合に利用することができます。
以下のようなコードでそのパートが存在するMessageオブジェクトを取得することができます。
Message getRootMessage(Part p) {
Part current = p;
Multipart mp;
while (!(current instanceof Message)) {
mp = ((BodyPart)current).getParent();
current = mp.getParent();
}
return (Message)current;
}
Multipartは後に記していますが、親であるPartのボディとして設定されるもので、BodyPartと同様に親を得るgetParent()メソッドを持っています。
パートのツリー構造がJavaMailのオブジェクト構造としては、Message
[- Multipart - BodyPart]* - ContentのようにMultipartとBodyPartが交互に現れる構造であることは3章で説明しました。このような構造であるため、親を辿ると言う処理も、単にgetParentを繰り返し呼ぶという(awtでComponentオブジェクトからFrameオブジェクトを得るときのような)単純な処理にはなりません。上記処理ではオブジェクト階層を一度に二段ずつ上昇していくようなイメージになっていますね。
MimeBodyPartは、BodyPartのinternetメッセージ版ということになります。
MimeBodyPartに対して新たに付加される機能は、JavaMail1.2で追加されたgetRawInputStream()のみです。後はMimePartに定義されたメソッドを実装しただけということになります。従って、MimeBodyPartクラスに関してはMimeBodyPartとして扱わなければならないケースはほとんどありませんので、常にBodyPartやMimePart、或いは単にPart型変数を使ってアクセスするべきです。
なお、本来はgetRawInputStream()もMimePartに定義したかった所でしょうけれど、MimePartにメソッドを追加することは、既にJavaMailを使用しているアプリケーションやプロバイダがMimePartの実装クラスを提供していた場合に上位互換性を損なってしまいますので、このメソッドは各実装クラスであるMimeBodyPartおよびMimeMessageのみに定義されました。
MimeBodyPartには、MimePart interfaceの実装とgetRawInputStream()の他に、MimeBodyPartをサブクラス化した場合にサブクラスから呼び出すための二つのprotectedメソッドを持ちます。
自身が表すパートのボディの生データに対するInputStreamを得ます。getInputStream()やgetContent()ではContent-Transfer-Encoding:に従ってボディをデコードした結果を返しますが、これらのメソッドはそのパートのContent-Transfer-Encoding:の内容が正しくなかった場合にデコードに失敗し、MessagingExceptionがthrowされてしまいます。このような場合に、自力でデコードを試みたり、デコードを諦めたとしてもそのデータをそのまま保存する手段として、このメソッドがJavaMail1.2で追加されました。
このメソッドはただgetContentStream()メソッドを呼び出すだけのものですが、getContentStream()はprotectedであり、これを単にpublicに変更した場合、既にこのメソッドをprotectedとしてオーバーライドしているアプリケーションのコンパイルが通らなくなってしまうため、このように別メソッドとして提供されました。先に述べたinterfaceに宣言を追加しなかったことといい、一度配布されたライブラリの互換性を損なわないようなインターフェイス変更は面倒ですね。
InputStreamから構築されたMimeBodyPartのcontentフィールドの内容をストリームとして返します。これはこのボディパートが表すパートのボディのオリジナルデータ(送受信されるデータ)です。
パートにボディ対する変更に合わせてヘッダの内容を補正します。
現状はContent-Type: Content-Transfer-Encoding:
Content-Disposition: の内容の補正を行います。今後のMIMEの仕様の変更に追随して実装も変更されることでしょう。
MimeBodyPartにおけるこのメソッドは、MimeMessageのupdateHeaders()からMimeMultipartのupdateHeaders()を経由して再帰的に呼び出されるためにあります。MimeMessage#updateHeaders()の説明も参照して下さい。
Messageオブジェクトは、メッセージのパートツリーのルートを表します。
メッセージ作成者の操作を行います。少なくともインターネットメイルにおけるFrom:ヘッダには複数のメイルアドレスを記述できますので、Addressの配列を受渡します。
とはいっても、From:に複数のアドレスを記述することはまれですので、引き数無し、または単一のAddressを引き数に取るsetFrom()メソッドも用意されています。引き数無しのsetFrom()メソッドはInternetAddress#getLocalAddress()で取得したメイルアドレスを設定します。こちらはInternetAddressクラスの説明を参照して下さい。
setFrom()に限らず、メイルアドレスを設定するメソッドについては、日本語の名前を含んだInternetAddressオブジェクトを設定できますが、若干気を使うことがあります。こちらについてもInternetAddressクラスの説明を参照して下さい。
addとsetの違いは名称から類推できると思いますが、addFrom()は既にFrom:ヘッダが設定されていた場合にそれに追加を行い、setFrom()は既存のFrom:ヘッダを削除してから指定されたヘッダを追加します。
メッセージ宛て先(受信者)の操作を行います。これまたインターネットメイルに依存する話ですが、宛て先にはTO/CC/BCCといった種別があります。また、宛て先がNetNewsのニュースグループだった場合は宛て先はニュースグループ名となるでしょう。Messageオブジェクトに宛て先を設定する際には、この宛て先の種別であるMessage.RecipientTypeを渡します。
取り敢えず、Message.RecipientType.TO/Message.RecipientType.CC/Message.RecipientType.BCCの何れかを指定すると考えて下さい(詳しくはMessage.RecipientTypeクラスの説明を参照下さい)。
getAllRecipients()では、それらの種別に関係なく、設定された宛て先アドレスの全てを配列として返すわけです。これは実際の宛て先を決定するのに使用されています。
他のメソッドは、Message.RecipientTypeを指定して、その宛て先種別に対して働きます。
addとsetの違いはFrom:に対するメソッド群と同じです。これは他のヘッダ操作メソッドにも当てはまります。
また、日本語を含むアドレスに対する注意も同様です。
返信アドレスに対する操作を行います。インターネットメイルではReply-To:ヘッダに相当します。
getReplyTo()メソッドはReply-To:ヘッダが存在しない場合はFrom:ヘッダの内容を返すように実装されています。これは、JavaMailがクライアントサイド向けAPIである事から、このメソッドを呼び出す時はメイラで「返信」を行おうとした時と考えられているためです。従って、純粋にReply-To:ヘッダの内容が取得したい場合は、このメソッドではだめで、getHeader("Reply-To");によって取得しなければなりませんので注意して下さい。
メッセージ送信日時操作を行います。MimeMessageではDate:ヘッダを操作することになっています(厳密にはDate:ヘッダは送信日時ではなくメッセージ作成日時を意味するのですが)。Messageクラスにおいては、次のgetReceivedDate()と対応させて、送信日時/受信日時というAPIに抽象化しておいて、実装クラスとしてはget/setSentDate()をDate:ヘッダに対応付けたというところですね。
とにかく普通はMessage作成時にsetSentDate(new
Date())を呼び出して、現在日時をセットすることになります。
setSentDate()を呼び出さずに送信したメッセージに対しては、MTAがDate:ヘッダを付加してくれる場合がありますが、それを期待するのは正しい実装ではありません。新しいMessageを生成/送信する場合には必ずsetSentDate()を呼び出すようにしましょう(JavaMailが内部的に呼び出してくれてもよかったかもしれませんね)。
ちなみにRFC822およびRFC1123ではDate:ヘッダのフォーマットが厳密に決められているのですが、JavaMailではDateオブジェクトを渡せば適切にフォーマットしてくれますし、getSentDate()ではDateオブジェクトに変換して返してくれますので、Date:ヘッダのフォーマットを意識することはありません(*)。
*:っと思いきやgetSentDate()では正しく日付を解釈できないケースがありました。詳細は「日本語を扱う場合の注意点」を参照してください。
メッセージ受信日時を取得します。RFC822形式のメッセージに対応するヘッダはありませんので、MimeMessageにおいてはこのメソッドはnullを返すようになっています。
IMAP4では受信日時に相当するINTERNALDATEという属性(Fetch
Response)がありますので、IMAPProviderから取得できるIMAPMessageの場合は、内部日付を得ることができます。POP3Providerから取得したメッセージではこのメソッドはnullを返します。MUA
によっては最後(先頭)の Received: ヘッダを解釈して
"受信日時" を出してくれるものもありますね。このような処理を行いたい場合は自分でReceived:を解析する必要がありますが常に正しいとは限りません。
メッセージの表題を操作します。MimeMessageにはcharsetを指定できるsetSubject(subject,
charset)メソッドがあり、日本語を含める場合はそちらを利用することになります。また、日本語の表題は自動的にデコードされてStringオブジェクトが生成されます。
…という仕様ではありますが…、JavaMailはRFCに準拠した実装になっているのですが、日本で流れているメッセージのいくつかに問題があってデコードができないようなケースが度々発生します。これについては章末の「日本語を扱う場合の注意点」を参照して下さい。
メッセージの現在の状態を示すFlagの操作を行うメソッド群です。JavaMailがIMAPを前提に設計されていることは何度も書いていますが、これらのメソッドが扱うFlagの内容もIMAP4プロトコルで規定されているものとなっています。
Flagsクラスが一つのメッセージに対する状態フラグの集合を表し、内部クラスであるFlags.Flagクラスは特定の状態フラグを表します。設定/参照できるFlags.Flagクラスの値は以下のようになっています。
| 状態名 | 意味 |
|---|---|
| Flags.Flag.SEEN | メッセージは既に読まれている。 |
| Flags.Flag.ANSWERED | メッセージに対して返信済みである。 |
| Flags.Flag.FLAGGED | メッセージが緊急または特別な注意であることを表す。 |
| Flags.Flag.DELETED | 削除予約済みである。 |
| Flags.Flag.DRAFT | メッセージは作成中である。 |
| Flags.Flag.RECENT | フォルダを選択していない間に到着したメッセージである。 |
| Flags.Flag.USER | ユーザ定義フラグ。 |
メッセージ番号を獲得します。setMessageNumber()の方はサブクラスが呼び出すものです。メッセージ番号の定義はプロトコルに固有ですが、IMAP/POP3ではそのフォルダ上での(通常メッセージ到着順の)シーケンシャルな値になります。
このメソッドをアプリケーションが利用することはほぼないと思います。メッセージ番号はFolderオブジェクトから特定メッセージを取り出すときに使用しますが、一度取り出したメッセージに対してメッセージ番号が欲しいケースは通常はありません。また、メッセージ番号はセッションのたびに変わるものであり、IMAP4においてはセッション中であっても変わる場合があります。プログラマはメッセージ番号に依存しないようにアプリケーションを作成すべきです。
このメッセージが削除されたか否かを参照/設定するメソッドです。設定はサブクラス、即ちProviderから呼び出すことが想定されています。具体的にはIMAPProviderではサーバからメッセージの削除が通知された時にsetExpunged(true)が呼び出されます。Messageオブジェクトのexpunged属性はFlags.Flag.DELETEDとは異なり既に削除された事を意味しますので、isExpunged()がtrueのメッセージについてはgetMessageNumber()以外のメソッドは無効であるとされています。
このメッセージが属するFolderオブジェクトを返します。
このメッセージが渡された検索条件にマッチするか否かをチェックします。Folder#search()から呼びだされるためにあるメソッドと言えるのですが、アプリケーションがその時点で保有するMessageオブジェクト群に対してこのメソッド(あるいはSearchTerm#match()を用いて検索を行う事も可能です。
reply()はこのメッセージに対する「返信」メッセージの雛形となるMessageオブジェクトを返します。具体的には以下のようなことが行われます。
また、replyToAllがtrueの場合は、さらに元のメッセージのTo: cc: Newsgroup:の内容を返信メッセージにも追加します。ただし、InternetAddress.getLocalAddress()で得られることになっている自分のメイルアドレスと、Sessionに渡すPropertiesオブジェクトの"mail.alternates"に記述したアドレスは追加されないことになっています(*)。
*:"mail.alternates"はアンドキュメントですので将来も利用できるとは限りません。
このメソッドで生成されるMessageに対して行われることはこれだけです。従って、呼び出し側で以下のような処理または注意が必要になります。
2については、引用してくれればいいのに、という要望が出ていましたが、Messageに一度セットされたボディは基本的に編集するものではないですし(再設定のみ可能)、引用の仕方も固定ではない場合が多いですのでこの要望は採用されていません。ボディに関しては元のメッセージから自分で引用/編集("> "の付加など)を行って、ユーザによって編集されたものを返されたメッセージにセットしなければなりません。
メッセージ内容が変更されたことをメッセージオブジェクトに伝えます。
setContent()等で内容を設定した場合に、Content-Transfer-Encoding:等のヘッダを正しく反映させるために呼び出す必要があります。
このメソッドはMessage-ID:ヘッダも付加しますが、ここで生成されるMessage-ID:については注意すべきことがあります。これについては「JavaMailにおけるMessage-ID:ヘッダの扱いについて」を参照して下さい。
MimeMessageクラスはインターネットメッセージを抽象化しています。abstractクラスであるMessageのインターネットメイル向けの実装ということになります。ほとんどはMessageクラスのメソッドをオーバーライドしたものですが、いくつかはMimeMessageで追加されたメソッドがあります。
また、Store/FolderのインターフェイスがIMAPに合わされたように、MessageクラスのインターフェイスはMimeMessageの実装に合わされたといえる部分があります。
その一つはMessage#saveChanges()メソッドです。MimeMessageクラスはそのボディを保持する方法として、protected
byte[] contentというフィールドにボディのバイトイメージを持つか、protected
DataHandler dhにボディを表すDataHandlerオブジェクト(*)を持つという2通りがあります。
InputStreamからMimeMessageクラスが構築された場合は、contentフィールドにボディが格納され、setContent等のメソッドでボディを表すオブジェクトを設定した場合はdhフィールドが設定されます。
saveChanges()メソッドはMimeMessageにおいては、メッセージに加えられた変更をヘッダに反映し、次にwriteTo()が呼び出されたときにdhフィールドの内容を用いて出力を行うことを示すフラグをセットします(modifiedフィールド)。
ストリームから生成されたMimeMessageで、saveChanges()が呼び出されていない場合は、MimeMessageは自身が保持するcontentを用いて出力を行います。
つまり、ストリームから生成されたMimeMessageのボディに対して変更を加えたり、異なるボディを設定したりした場合は、saveChanges()を呼び出さないと、ボディ部分が読み込んだときのままになってしまいます。(*)
*:JavaMail1.2ではMimeMessageに対してsaveChanges()の呼び出しが必要になるような操作に対してフラグをセットし、writeTo()時に必要に応じてsaveChanges()を呼び出すように拡張されています。
例えばMimeMessageがmodified状態を持たず、contentフィールドを一時的にしか使わず、常にdhフィールドを信用するようにすればsaveChanges()というメソッドは不要でしたが、読み込んだボディが不正だった場合などで、contentからDataHandlerが生成できない場合などもありますし、本文をDataHandlerに変換することなく転送するようなことができなくなることもありますので、プログラマが明示的にsaveChanges()を呼び出すという設計になっています。
*:javax.activation.DataHandlerはJAFのクラスです。このクラスについては3章で概要を説明しています。
Message-ID:ヘッダの内容を取得します。Message-ID:については2章で説明しています。また、JavaMailにおけるMessage-ID:に関する注意点は「JavaMailにおけるMessage-ID:ヘッダの扱いについて」で記述しています。
getMessageID()メソッドは、単にMessage-ID:ヘッダの内容を返すだけですので特に気にすべきことはありません。
Message-ID:ヘッダは、他のメッセージのIn-Reply-To:やReference:ヘッダから参照されます。
Content-ID:ヘッダを設定します。
JavaMailではMessage-ID:と異なり、Content-ID:を自動生成はしてくれないため、プログラマが何らかの方法で生成したContent-ID:を設定する必要があります。
しかし、メッセージ本体のヘッダにContent-ID:が追加できてもあまり嬉しいことがありません。本当は内部の各パートに対してこそ設定したいのです(2章を参照してください)。
パートにContent-ID:を設定するためのMimePart#setContentID()メソッドは今後のリリースで追加するということですので、現状はパートに対してContent-ID:を設定する場合はsetHeader()メソッドを用いることになります。
このPart interfaceに定義されている同名のメソッドについて、RFC2047に従った指定されたcharsetによるエンコードを伴うヘッダ設定メソッドです。日本語の文字列を設定する場合はこれらのメソッドを用いてcharsetに"ISO-2022-JP"を指定するようにしましょう。ただし、これらのメソッドでは渡した文字列全体がエンコードされますので、ASCII文字の部分をエンコードしたくないという場合(後に記しますがそういう場合もあります)はこれらのメソッドではなく、自分でエンコード処理を行う必要が出てきます。全体がエンコードされても通常は問題ありませんので、通常はこれらのメソッドに"ISO-2022-JP"を指定すれば充分です。
JavaMailにおけるSubjectのエンコーディングスキームについて
JavaMail1.1.3以前は、setSubject()でSubject:に"ISO-2022-JP"の日本語を設定すると"=?ISO-2022-JP?B?・・・?="のように Bエンコーディングが選ばれていました。しかし、JavaMail1.2では、同じプログラムでQエンコーディング"=?ISO-2022-JP?Q?・・・?="が選ばれるようになっています。ISO-2022-JPの扱いについてはRFC1468に記述されているのですが、ここには「ヘッダのエンコーディングにはBが使われるべき」という記述があり、JavaMail1.2以降の挙動はRFC1468に反するようになりました。なぜでしょう?
まず、B/Qという二つのエンコーディングの意味について考えてみます。QエンコーディングとはQuoted-Printableエンコーディングをヘッダに適用するもののことで、可視のASCII文字以外の文字についてエンコード(文字コード表記に変換)する手法で、アルファベットの比率が多い場合エンコードされた文字列でもなんとか読むことができるのが特徴です。BエンコーディングはBase64エンコーディングを指し、全ての文字列をbit単位で再配置して可視のASCII文字の範囲のコードの羅列に変換する方法です。このエンコードを施せば、元の文字列は面影もなくなります。MIMEの規定(RFC2045など)では、Quoted-Printableは符号化前の文字列にASCII文字が多く含まれている場合に使用すべきとされています。そのような背景からRFC1468では、ISO-2022-JPの文字列は各バイトはASCIIの範囲内ですがアルファベットとしての可視性はないことと、それでもエスケースシーケンス(0x1B)が含まれているのでそのままヘッダに記述してはいけないという二点から、Bエンコーディングを行うことを推奨してきました。
さて、JavaMailに戻りまして、JavaMail1.1.3以前まではヘッダのエンコーディングとしてB/Qの何れを使うかは以下のようなアルゴリズムで決定されていました。
(**)で示したように符号化したバイト列が全てASCIIである場合になぜかBエンコーディングとされていた(バグ)のですが、このようになっていたおかげで、たまたまISO-2022-JPのsubjectはBエンコーディングが行われていたのです。
筆者がこのバグを指摘したところ、上記の処理が間違いであることはあっさり認められ、JavaMail1.2で(**)の場合にQエンコーディングになるように修正されたのですが、この修正によって、ISO-2022のような文字エンコード後のバイト列が7bitになるような文字エンコーディングの場合は常にQエンコーディングが選ばれるようになりました。
これはRFC1468で推奨される方法ではないわけですが、MIMEには適合していることと、RFC1468自体がInformationalであること、および特定のコード系に特化した処理をJavaMailのコアAPIに含めるわけにはいかないことから、現在の実装は妥当であると判断しています。しかし、日本で流通しているメイラの中にはヘッダに'Q'エンコードが施されていてもデコードできないものもあるようです(PostPetなど)。もしBエンコーディングにしたいという場合は、setSubject(MimeUtility.encodeText(subject,
"ISO-2022-JP", "B"))のようにMimeUtilityを用いて明示的にエンコーディングスキームを指定して自分でエンコードしたものを設定すれば可能です。
なお、このような経緯があり、本書で記載しているメッセージのSubject:はBだったりQだったりしているのですが、これは出力に用いたJavaMailのバージョンに依存しているためであることをここでお断りしておきます。
メッセージボディの生データに対するInputStreamを得ます。詳細はMimeBodyPart#getRawInputStream()の項を参照してください。
InputStreamから構築されたMimeMessageのcontentフィールドの内容をストリームとして返します。
JavaMail1.2では、このメソッドと等価なpublicメソッドとして、getRawInputStrem()が追加されています。
メッセージのボディ対する変更に合わせてヘッダの内容を補正します。
現状は各パートのContent-Type: Content-Transfer-Encoding:
Content-Disposition: の内容の補正を行います。今後のMIMEの仕様の変更に追随して実装も変更されることでしょう。
saveChanges()内から呼び出されます。
ところで、このメソッドがprotectedである理由が、筆者にはちょっとわかりませんでした。
このメソッドの実処理部分はMimeBodyPartで定義されている(パッケージスコープの)staticメソッドに委譲されているため、サブクラスでオーバーライドして利用するにはちょっと扱い辛いところがあります。しかし、調べていると、
http://java.sun.com/products/javamail/FAQ.html#msgid
に、独自のMessage-ID:を付加したい場合は、updateHeaders()をオーバーライドしなさいと記述されています(MimeMessageの、ですが)。つまり、やはりこのメソッドはオーバーライドして使用するためにprotectedとされているようです。
筆者としてはこの方式は、オーバーライドした場合にsuper.updateHeaders()を呼び出すようにしないと重要な処理が飛ばされてしまいますのであまりいい方法とは思えないのですが…。
メソッドをprotectedにするというのは以下の二つの側面があります。
このどちらの意図でprotectedにしたかは細かいことではありますが重要です。誤った使用方法は、クラス設計意図と反するものとなり、可読性が低下するだけでなく、ライブラリ側の設計変更時の非互換が発生しやすくなったり、使い方を誤ったクラスを再利用しようとしたときに見つけにくいバグを内在させるという問題があります。
例えば、デザインパターンのTemplate Method(*)について考えると、template
methodから呼び出されるメソッドはオーバーライドされるためにあるのであって、サブクラスから呼び出してほしいという意図はないはずです。デフォルトで空の実装とされることもよくありますが、abstractメソッドとして宣言することもあります。
*: Template Methodパターンそのものの詳細は「オブジェクト指向における再利用のためのデザインパターン」(Erich
Gamma他著)を参照して下さい。
class Foo {
public final void templateMethod() {
// 必ず必要な処理
:
hook();
//必ず必要な処理
:
}
// abstractメソッドとすることもあります。
protected void hook() {}
}
class Bar extends Foo {
protected void hook() {
System.out.println("Hook!!");
}
}
上記のように、template methodそのものはfinalとしてしまうことでオーバーライドを禁止して、呼び出すためのものであることを明示することができます(*b)。しかし、hookメソッドのオーバーライドだけを許して上記箇所以外からの呼び出しを禁止することはJavaではできません。そして、hook()をサブクラスの変な箇所から呼び出すコードを書いていると、そのクラスはちゃんと動いても、そのクラスからさらにサブクラスを作成した時にバグが発生してしまったりします。
*b:これはpublicメソッドですが、実はpublicメソッドでもここの話題に挙げている問題は同じです。Barの作成者がFooの作成者の意図を理解せず、以下のようなコードを書いていたとしましょう。
class Bar extends Foo {
protected void hook() {
System.out.println("Hook!!");
}
public void someMethod() {
:
hook();
:
}
}
Fooの作成者は、hook()はtemplateMethod()からのみ呼び出されることを想定しているのですが、そのことを知らないサブクラス作成者が誤って上記のようなコードを書いていた場合、hook()はスーパークラス作成者にとって予期せぬときに呼び出されることになります。さらに別の開発者がBarを継承したBazを作成して、hook()メソッドを実装(オーバーライド)した場合のことを想像してみましょう。
…どうでしょう、Bazの作成者はFooがTemplate Methodパターンを用いていることを理解し、それに従ってhook()を実装したつもりなのですが、実はtemplateMethod()以外の箇所からも不意に呼び出されるわけです。これは気付きにくいバグとなるでしょうね。
先頭に挙げた二つの意図を明確にすることの重要性はご理解いただけますでしょうか。
サブクラスから呼び出すことのみを明示するためにはfinal修飾子が利用できますが、Javaの言語仕様では、サブクラスでのオーバーライドのみを許し、呼び出しを許さないという制限を課すことはできないため、この点についてはAPI仕様に明快に記述するしかないという現状です。
しかし、少なくともfinalが付加されているメソッドは、サブクラスや外部から呼び出すことのみが想定されていることを明快に表していますので、他人が利用する可能性のあるクラスであれば、この点だけでもできる限り守っていくようにしたいものです。言語仕様でサポートされているということは、コンパイラなどのツールが誤りを確実に検出してくれることを表しますので、利用しない手はないでしょう。
RFC822形式のストリームからMimeMessageオブジェクトの内容を再生成します。
元々コンストラクタで渡されたInputStreamから内容を構築するために使用されるprivateメソッドでしたが、JavaMail1.2では、サブクラスからも呼び出すことができるようにprotectedに変更されました。
parse()メソッドにより、ストリームからMimeMessageの内部のInternetHeadersオブジェクトを構築する際に用いられるfactory
methodです。
これはFactory Methodパターンであり、元々parse()メソッド内でnew
InternetHeaders()によって生成されていたヘッダ集合を表現するオブジェクトを、このクラスをサブクラス化することによって切り替えることを可能にするためにJavaMail1.2で追加されました。
このメソッドをオーバーライドして、適切なInternetHeadersのサブクラスのオブジェクトを返すようにすることで、例えばサーバから読み込んだメッセージであっても、独自のInternetHeadersオブジェクトを使用することが可能です。
マルチパートメッセージのコンテナとなるパートです。
このクラスはメッセージそのものには現れません。マルチパートメッセージの構造自体は以下のようにパートの階層構造となっています。
では、Multipartクラスはなんなのかというと、複数のパートを一つのパートの*内容*と見なすためのオブジェクトです。
MultipartはPartオブジェクトの内容としてsetContent()に渡すことができます。
このコンテナに設定されているindex番目のBodyPartオブジェクトを返します。
このオブジェクトが表現している(multipartメディアタイプの何れかである)Content-Type:の内容を返します。
このメソッドが返すものは、その親にあたるPartのgetContentType()が返すものと同一です。
このあたりの関係は3章の「添付ファイル付きメッセージの送信」等で示したJavaMailのオブジェクト構成図を見ていただければ解りやすいかと思います。
自分が保持しているパートの個数を返します。
このMultipartオブジェクトにBodyPartオブジェクトを追加します。indexを指定すれば、指定した位置にBodyPartオブジェクトが挿入されます。
このMultipartオブジェクトが保持するBodyPartオブジェクトの一つを削除します。これは特に説明不要ですね。
このMultipartオブジェクトが設定されたPartオブジェクト(親)を返します。
このメソッドは内部的にしか使用されません。
親オブジェクトを設定するメソッドですが、このメソッドは外部から呼び出してはなりません。JavaMail
API内で自動的に呼ばれるものです。これがなぜ、publicメソッドになってしまっているかというと、このメソッドの定義されているのがjavax.mail.Multipartであるのに対して、呼び出しを行っているのがjavax.mail.internet.MimeBodyPartやjavax.mail.internet.MimeMessageなどであるためです。
外部から勝手に呼び出すと、パートツリーの構造に矛盾が生じてしまいますので呼び出してはならないわけです。
これはださいところですが、JavaにはC++のfriendのように特定のクラスからのみの呼び出しを許すという指定がありませんので、パッケージが異なりサブクラスでもないクラスから呼び出されるメソッドは呼び出し元が固定的であってもpublicとせざるを得ないのですね。コラム「protectedの意義?」でも説明したように、Javaのアクセス修飾子だけでもある程度厳密なアクセス制限を行うことはできるのですが、完全ではないためにどうしても開発者間の約束に頼らなければならない部分も出てきてしまいます。JavaMailの他の部分を見ても、プロバイダ作成者が使うべきものなのかアプリケーション作成者が使うべきものなのかはメソッドシグネチャからは判別しようがないという現状です。API仕様を確認する時はこのような点についても注意するようにしましょう。
このMultipartオブジェクトのデータ構造を表すDataSourceであるMultipartDataSourceを設定します。
Providerのためにあるメソッドで、サブクラスから呼び出します。実際にはMimeMultipartオブジェクトをDataSourceを指定して生成した場合にこのメソッドが呼び出されるので、JavaMail外部から呼び出すことはありません。このメソッドも単にライブラリ内部で使用するものだがjavax.mail/javax.mail.internetパッケージをまたがって呼び出すためにprotectedにされているという性質のものです。
MultipartDataSourceはMultipartと同じようにgetBodyPart(int
index)メソッドを持っており、これを実装したクラスがBodyPartの任意のサブクラスを返すことができるようにするためのものです。
BodyPart/MessageのwriteTo()と同じ役割です。具体的にはMultipartクラスそのものの役割と同じで、親パートと子パートを繋ぐ、つまり、親パートに対してwriteTo()が呼び出された場合に、このMultipartの配下の各MimeBodyPartのwriteTo()を次々に呼び出すという実装になっています。
マルチパートメッセージのコンテナとなるパートです。このクラスの持つ機能を直接使用することはあまりないでしょう。従って、このオブジェクトを保持する変数の型は大抵javax.mail.Multipart型になるはずです。このクラスにはmultipart/related等のMIMEタイプを処理しようと思ったときに利用可能なツールメソッドが含まれています。
Content-ID:ヘッダを指定して、そのContent-ID:ヘッダを持つPartオブジェクトを検索して返します。
multipart/relatedなパートを解釈(レイアウト)する機能を実装する場合に使用します。
multipart/relatedとContent-ID:については2章および3.7章に少し記述しています。
multipart/xxxxのxxxxの部分を設定します。デフォルトでは"mixed"となっています。普通はコンストラクタで指定しますのでこのメソッドを使用することはあまりないでしょう。
MimeBodyPart/MimeMessageのupdateHeaders()と同じ役割です。具体的にはMultipartクラスそのものの役割と同じで、親パートと子パートを繋ぐ、つまり、親パートに対してupdateHeaders()が呼び出された場合に、このMultipartの配下の各MimeBodyPartのupdateHeaders()を次々に呼び出すという実装になっています。writeTo()でも同じような事を書きましたね。
parse()メソッドにより、ストリームからMimeMultipartの内部の各パートオブジェクトを構築する際に用いられるfactory
methodです。
これはFactory Methodパターンであり、元々parse()メソッド内でnew
MimeBodyPart()等によって生成されていたメッセージツリーを構成するオブジェクトを、このクラスをサブクラス化することによって切り替えることを可能にするためにJavaMail1.2で追加されました。
これらのメソッドをオーバーライドして、適切なMimeBodyPartやInternetHeadersのサブクラスのオブジェクトを返すようにすることで、例えばサーバから読み込んだメッセージであっても、独自の(サブ)クラスのオブジェクトによって構築することが可能です。
MIMEに準拠したメッセージを生成/解釈するためのツールメソッドが集められたクラスです。Part#getHeader()やMimeMessage#getRawInputStream()等はエンコードされたままの情報を返しますのでこれを自分でデコードしたい場合などに用います。また、Part#setHeaderを使ってヘッダを設定する時は逆に明示的にエンコードを行わなければなりません(Message#getSubject()のようなメソッドは内部的にこのクラスを用いてデコードした結果を返すようになっているわけです)。
渡されたストリームをラップした、指定転送エンコーディングスキームに従ってデコードを行いながら入力を行うストリームを返します。転送エンコーディングスキーム(TES)には、
"base64"、 "quoted-printable"、
"7bit"、 "8bit" 、"binary"、"uuencode"、"x-uuencode"
の何れかが指定可能です。実質Base64、Quoted-Printable、uuencodeの3種類のデコーダを持っているということになります。"7bit"や"8bit"
、"binary"の場合は特別なデコードの必要がないので渡されたストリームをそのまま返します。
渡されたストリームをラップした、指定転送エンコーディングスキームに従ってエンコードを行いながら出力を行うストリームを返します。転送エンコーディングスキーム(TES)にはdecode()と同じく、
"base64"、 "quoted-printable"、
"7bit"、 "8bit" 、"binary"、"uuencode"、"x-uuencode"
がの何れかが指定可能です。filename指定が可能なものは、uuencode専用で、uuencode時に付加されるファイル名を指定可能にするためにJavaMail1.2で追加されました。
指定DataSourceの内容を元に、Content-Transfer-Encoding:を決定します。
メッセージ送信前にPartに設定されたデータをエンコードするために使用されます。
DataSource内のストリームの内容が7bitの範囲内であれば"7bit"となり、非ASCIIバイトが含まれる場合は、その数が全体の50%を越えるか否かによって"base64"または"quoted-printable"が返されます。
なお、Content-Type:がtext/*でない場合は、quoted-printableは選択されません。1バイトでも非ASCIIバイトが含まれた場合は"base64"が返されます。
ヘッダにおけるMIMEエンコーディングが施されたテキストをデコードします。
decodeText()はencoded-word混じりの文字列をデコードし、decodeWord()は一つのencoded-wordをデコードします。
decodeText()はRFCに厳密に従ったエンコード文字列でなければデコードに失敗してしまいますので、日本では「日本語を扱う場合の注意点」で示すような独自のdecodeText()を使用する必要があるでしょう。
渡された文字列に対し、ヘッダにおけるMIMEエンコードが行われたテキストを返します。
現状はencodeText()とencodeWord()はほとんど同じ挙動であり、渡された文字列内に非ASCII文字が含まれていた場合は文字列全体を一つのencoded-wordとしてエンコードを行います。全ての文字がASCIIである場合は元の文字列がそのまま返されます。
異なる点は転送エンコーディングスキームとして"Q"エンコーディングが指定(または内部的に選択)された場合の挙動です。encodeWord()の方は、構造化フィールド(メイルアドレスを設定するヘッダフィールドなど)に対して用いられることが想定されており、エンコード対象の文字種が多いのに対し、encodeText()はSubject:やContent-Description:等の非構造化フィールド(自由入力欄)に対して使用することを想定されています。encodeWord()の方では、構造化フィールド上で意味のある記号として扱われる'('や'@'等の記号も文字コード表記に変換されるようになっています。
パラメタについてですが、パラメタが一つのメソッドは、それぞれパラメタが三つのものに対して、第二/第三パラメタをnullとして呼び出した場合と同じ結果となります。では、charsetやencodingにnullが渡された場合はというと、まず、charsetがnullの場合はプラットフォームデフォルトエンコーディングが使用されます。これは日本では"Shift_JIS"や"EUC-JP"になってしまうことが多いですのでnull指定ではなく、確実に"ISO-2022-JP"を指定する必要があります。
encodingの方がnullの場合、渡された元の文字列を文字エンコーディングに従ってバイト列に変換した結果中に含まれる非ASCIIバイトの比率に応じて"Q"/"B"の何れかが選ばれます。非ASCIIバイトが50%を越える場合は"B"エンコーディングになります。
ちなみに、"ISO-2022-JP"でエンコードしようとした場合、バイト列は7bit、すなわち全てASCII文字の範囲で表現されるため、JavaMail1.2では明示的にしていしない場合は"Q"エンコーディングになります(*)。
*:JavaMail1.1.3以前はバグによりISO-2022-JPでのエンコード時に"B"エンコーディングが使用されるようになっていました。しかし、ヘッダが"Q"エンコーディングされていた場合に正しくデコードできないMUAもまだのこっているようですので悩ましいところです。「JavaMailにおけるSubjectのエンコーディングスキームについて」も参照してください。
プラットフォームデフォルトエンコーディングに対応するコンバータ名を返します。System.getProperty("file.encoding")と等価ですが、こちらのメソッドでは、セキュリティ上System.getProperty("file.encoding")が使用できない状況でもデフォルトエンコーディングを取得できるようになっています。
それぞれ、IANAに登録されたエンコーディング名からJavaのUnicodeとの文字コードコンバータ名へ、或いはJavaのコンバータ名からIANAに登録されたエンコーディング名への変換を行います。
JDK 1.2以降ではJavaのコンバータ名のエイリアス(別名)として、ある程度のIANA登録済みエンコーディング名も登録されているため、Javaのコンバータ名を使用することは少ないのですが、JavaMailはJDK1.1でも動作するように作られていることと、システムプロパティの"file.encoding"等にはJavaのコンバータ名がそのまま入っていたりするため、それをIANA登録名に変換しないでメッセージ中に記述するわけにはいかないことから、これらのメソッドの存在意義があります。
この相互変換には、META-INF/javamail.charset.mapに記述された変換表が用いられます。この記述を変更することで、これらのメソッドの変換結果に影響を与えることが可能です。
渡されたwordがquote(引用符付け)する必要がある場合はquoteを行います。ヘッダに記述する際はエスケープしなければならない記号がword内に存在する場合は、その記号をエスケープ('\\'を付加)し、全体を引用符(")で囲んだ文字列を返します。
specialsには、文字列をquoteしなければならない条件となる記号群を指定します。word内にspecialsに指定された記号が含まれる場合はやはりquoteが行われます。
ここには、javax.mail.internet.HeaderTokenizer.MIME/javax.mail.internet.HeaderTokenizer.RFC822の何れかを利用することができ、これらはMIME準拠のヘッダで意味のある記号群/非MIMEのヘッダで意味のある記号群(RFC822で意味が規定される記号群)を表します。といってもどういうときにどちらを使えばよいかわかりませんね。
通常ヘッダに使用する文字列は、quoteされていなければMIMEに従って解釈されるものですので、HeaderTokenizer.MIMEを指定してquote()を呼び出すと、MIMEエンコードで使用される記号が含まれてもquote対象になってしまいます。これではまずいのでヘッダに対してquote()メソッドを使用する場合はHeaderTokenizer.RFC822を指定することになります。
それとは打って変わって、ヘッダ内のパラメタ(Content-Type:におけるcharsetパラメタなど)の値に関しては、MIMEエンコードの対象外であり、それでいて、MIMEで規定された記号群を誤って解釈してしまわないように、MIMEで規定された記号が含まれる場合はquoteしなければなりません。つまり、ヘッダのパラメタ部の文字列に対してはHeaderTokenizer.MIMEを指定する必要があります。
ややこしい説明を書きはしましたが、通常はヘッダのパラメタを扱うParameterListクラスでこのメソッドを自動的に呼び出すため、意識する必要はありません。
JavaMail1.2で追加されたinterfaceです。
InputStreamに対して、同じ内容にアクセスするInputStreamのコピーを生成する機能を持たせます。
このinterfaceを実装したInputStreamに対して、newStream()を呼び出す事により、同じデータにアクセスする別のInputStreamを得ることができます。
JavaMail API内部で使用するものと思って差し支えはありません。
RFC822の仕様から現状に即するように変更されたdraft-ietf-drums-msg-fmt-08に従った形式(しかし単にRFC822形式といってもこちらを表すと思ってよいです)で日付をフォーマット/解釈するjava.text.DateFormatのサブクラスです。JavaMail1.1.3まではcom.sun.mail.utilパッケージにあったクラスですが、プログラマが日付を利用する全てのヘッダに利用できるようにjavax.mail.internetパッケージに移動/公開されました。
使い方はごく単純で、パラメタ無しのコンストラクタでMailDateFormatオブジェクトを生成しておいて、必要な時にformat(Date)メソッドでRFC822形式日時文字列に変換し、逆にヘッダの日付文字列をparse(String)に与える事でDateオブジェクトを得ることができます。このあたりはコアAPIのjava.text.DateFormatクラスのインターフェイスですのでそちらを参照して下さい。
注意:このクラスを用いて日本で流れるメイルをparseしようとした場合に正しく動作しないケースがあります。「日本語を扱う場合の注意点」でこの問題に触れています。このクラスを用いるMimeMessage#getSentDate()等も同じ問題があります。
JavaMailが発生させるイベントに関連するクラスはjavax.mail.eventパッケージにまとめられています。
イベントリスナについて紹介してゆくことでJavaMailがどのようなときにイベントを発生させるかを説明していきます。
イベントの発生するタイミングさえプログラマの要求にあっていれば、その時に通知されるべき情報はパラメタであるXxxxEventオブジェクトに含まれます(取り出せます)ので、各XxxxEventクラスの説明は省略します。
サーバとの接続の確立/切断時に呼び出されるイベントがまとめられています。
ConnectionListenerはTransport/Store/Folderオブジェクトにaddすることができます。
Transport/Storeがサーバに接続した、またはFolderがオープンされたときに呼び出されます。
Transport/Storeが切断された、またはFolderがクローズされたときに呼び出されます。
Storeが切断されたときに呼び出されるとドキュメントにありますが、実際にはこのメソッドが呼び出されることはないようです(JavaMail1.1.3)。切断されたときはclosed()が呼び出されています。
Folderに対する操作による状態変化が通知されます。
FolderListenerはStore/Folderオブジェクトにaddすることができます。
FolderオブジェクトはFolder/Store#getFolder()等で取得しようとする度に新たなインスタンスが生成されることが仕様で規定されています。これはすなわち、同じフォルダを指すFolderオブジェクトでも異なるFolderオブジェクトが複数存在し得ることを意味します。
このようなときに、Folderオブジェクトにイベントリスナを登録した場合、登録したFolderオブジェクトそのものに対して発生したイベントしか通知されませんので注意が必要です。
つまり別の場所で同じフォルダを表す別のFolderオブジェクトの操作を行ったとしても、それはイベントとして通知されることはないということです。
この仕様はIMAPの性質に合わせることと実装を容易にすることのためと思われますが、このような挙動のため、FolderEventを利用する場合は、同じフォルダに対するFolderオブジェクトの獲得は一ヶ所で行ってそのFolderオブジェクトを常に使用するようにするのが懸命なプログラミングということになります。
あるいは、このような意識をしなくてもよいように、Storeオブジェクトにaddして、配下の何れかのFolderに変化があったことを通知してもらうようにするのがよいでしょう。
名前からすぐ解るように、フォルダが作成/削除/改名されたときに呼び出されます。
これらのイベントはFolderオブジェクトのcreate()/delete()/renameTo()が呼び出されたときに発生すると考えてよいです("フォルダ"と"Folderオブジェクト"の違いは説明しましたよね)。
Folderオブジェクトに対してaddすることで、そのフォルダ上のメッセージの変更が通知されます。
そのフォルダ上の特定のメッセージ内容が変更されたことをFolderオブジェクトが検出したときに呼び出されます。
JavaMail付属のIMAPプロバイダでは、サーバからのFETCH応答によってイベントの発生としています。
ちなみにPOP3等ではサーバ上のメッセージ内容の改変ができないのでこのイベントが発生することはなさそうです。
しかし、例えばPOP3であっても、フォルダをクライアント上に仮想的に用意するようなプロバイダであれば、メッセージの改変が可能かもしれません。
Folderオブジェクトに対してaddすることで、そのフォルダ上のメッセージ数の変化が通知されます。
FolderListenerの項で記したのと同じ問題があり、たとえ同じフォルダを指しているものであっても別のFolderオブジェクトに対してメッセージ数の変化するメソッドが呼び出されてもイベントは通知されないことに注意が必要です。
ただし、JavaMail付属のIMAPプロバイダにおいては、これらのイベントはサーバからの応答コードによってメッセージ数の変化を検出するようになっていますので、何らかのサーバへのコマンドを送信するメソッドを呼び出したタイミングでは通知が行われます。
要するにメッセージ数が変更された瞬間に呼び出されるとは限らないということです。あくまで「そのFolderオブジェクトが自身の内部のメッセージ数の変化を検出したタイミング」になります。
そのフォルダ上のメッセージ数が増加したことをFolderオブジェクトが検出したときに呼び出されます。
JavaMail付属のIMAPプロバイダでは、サーバからのEXISTS応答によるメッセージ個数と、現在自身が保持しているメッセージ個数を比較することで検出します。
そのフォルダ上のメッセージ数が減少したことをFolderオブジェクトが検出したときに呼び出されます。
JavaMail付属のIMAPプロバイダでは、サーバからのEXPUNGE応答を受信した場合にイベントを発生させます。
Storeオブジェクトに対してaddすることで、サーバからのレスポンスに応じたイベントが通知されます。
通知されるStoreEventには、IMAPのレスポンスであるALERT/NOTICEが定義されています。
Storeが何らかのイベントを送付したいと思ったときに呼び出されます。おかしな物言いですがこのとおりなのです。
同梱のIMAPStoreの場合はIMAPサーバのレスポンスによってイベントを発生しますが、他のプロバイダでどのようなときにイベントが発生するかは未知です。プロバイダ毎のドキュメントを見る必要があるでしょう。
Transportオブジェクトに対してaddすることで、メッセージ送信における所定のタイミングでイベントが通知されます。
メッセージ送信が成功したタイミングで呼び出されます。
メッセージ送信に失敗したタイミングで呼び出されます。
サーバとの送信ネゴシエーション中の障害で、SendFailedException/MessagingExceptionが発生するような場合です。
メッセージの一部は送信に成功したという場合に呼び出されます。
JavaMail同梱のSMTPTransportではこのイベントは発生しません。つまり呼び出されることはありません。
筆者は最初、複数指定されたうちの一部のrecipients(宛て先)のみに送信が成功した場合を指すと思いましたが、そのケースでは少なくともSMTPTransportにおいてはこのイベントは発生しません(messageDelivered()が呼び出されます)。
このイベントを発生させるプロバイダがあるかどうかも不明ですが、仕様として用意されているというところでしょうか。
基本的にIMAPでサポートする機能をAPI化したもので、メッセージを検索するときの検索条件をオブジェクト化したものとなっています。各種の条件オブジェクトを組み合わせて複雑な条件を生成して、Folder#searchに対して渡すことで、条件にマッチするメッセージの集合を得ることができます。
この設計はよく利用されるものですので仕組みを覚えておいて損はありません。
まず、条件オブジェクトの基盤となるクラスであるSearchTermというクラスがあります。
このクラスには以下のメソッドのみが宣言されています。
public abstract boolean match(Message msg)
つまり、SearchTermクラスは必ずサブクラス化してmatchメソッドをオーバーライドせよ、という意味です。
サブクラスはそのサブクラス毎に特有のマッチ条件を定義することができます。特定のヘッダがあればtrueを返すように実装したり、ボディ中に特定のキーワードが含まれればtrueを返すように実装したりできます。
では、検索を実行する側はどうするのでしょう?
検索処理である、Folder#search()の内部では、フォルダ内の全メッセージに対して、パラメタで渡されたSearchTermオブジェクトのmatch()を呼び出します。matchがtrueを返したメッセージの集合がFolder#search()の返す値となります。
つまり、利用者は「検索条件に適合するか否か」という「処理」をオブジェクト化したものをパラメタで渡すのです。
JavaMailで用意されている検索条件クラスは以下のようになっています。
abstract SearchTerm
・・abstract AddressTerm Addressオブジェクトの比較によって検索条件とできるもの
・・・・FromTerm From:ヘッダとAddressオブジェクトが一致すればマッチとなる。
・・・・RecipientTerm To:cc:bcc:ヘッダと指定Addressオブジェクトが一致すればマッチとなる。
・・abstract ComparisonTerm 大小比較によって検索条件とできるもの
・・・・abstract DateTerm Dateオブジェクトの大小比較によって検索条件とできるもの
・・・・・・ReceivedDateTerm Message#getReceivedDate()で得られる日時とDateオブジェクトの比較結果に応じてマッチとなる。
・・・・・・SentDateTerm Message#getSentDate()で得られる日時(通常はDate:の内容)とDateオブジェクトの比較結果に応じてマッチとなる。
・・・・abstract IntegerComparisonTerm 整数値の大小比較によって検索条件とできるもの
・・・・・・MessageNumberTerm メッセージ番号が指定値に等しければマッチとなる。
・・・・・・SizeTerm メッセージサイズと指定値の比較結果に応じてマッチとなる。
・・abstract StringTerm 文字列比較によって検索条件とできるもの
・・・・abstract AddressStringTerm アドレス文字列の比較によって検索条件とできるもの
・・・・・・FromStringTerm From:ヘッダに指定文字列が含まれればマッチとなる。
・・・・・・RecipientStringTerm To:cc:bcc:ヘッダに指定文字列が含まれればマッチとなる。
・・・・BodyTerm ボディ(最初のtext/*パート)に指定文字列が含まれればマッチとなる。
・・・・HeaderTerm 任意の指定ヘッダに指定文字列が含まれればマッチとなる。
・・・・MessageIDTerm Message-ID:ヘッダに指定文字列が含まれればマッチとなる。
・・・・SubjectTerm Subject:ヘッダに指定文字列が含まれればマッチとなる。
・・FlagTerm 指定されたフラグが指定された状態(On/Off)であればマッチとなる。
・・AndTerm 渡された複数の条件オブジェクトの全てにマッチすればマッチとなる。
・・OrTerm 渡された複数の条件オブジェクトのいずれかにマッチすればマッチとなる。
・・NotTerm 渡された条件オブジェクトにマッチしなければマッチとなる。
クラスツリーは整然としています。役割の分担がうまく表されていますね。
利用者としては太字で示したクラス群をインスタンス化して条件オブジェクトを構築することになります。もちろんこれらの何れのクラスからでも、継承して独自の条件オブジェクトを作成することができます。
重要なのはAndTerm/OrTermの存在です。これらは複数のSearchTermサブクラスを渡すことで一つの条件を表すことができます。これによって、単純な条件オブジェクトを複数組み合わせて、複雑な条件を表現可能となります。
SearchTermはなぜabstract class?
SearchTermはabstract match()メソッドしか持っておらず、実装がないのでinterfaceで良いのですが、なぜabstract
classになっているのでしょう?
この理由として考えられるのは、以下のようなものです。
interfaceとすると条件オブジェクトではないクラスに対して条件オブジェクトとしての役割を付けられるようになります。これがinterfaceのもっとも大きな特徴なのですが、SearchTermの実装クラスは任意のクラスではなくあくまで「検索条件オブジェクト」としての役割だけを持っているべきで、他の処理と混ざってほしくない、という考え方ができます。
SearchTerm実装クラスはそのオブジェクト群が(AndTerm/OrTermを駆使して)複雑に組み合わされることが想定されるため、他の用途で設計されたクラスにimplementsするようなオブジェクトが含まれるとオブジェクトの構成として不自然なものになってしまいます。
このような場合は敢えてinterfaceとせずにabstract
classとして提供することで、他のクラスを継承したものと役割が混ざることを言語の仕組みで排除しようという設計があります。
もちろん、このようにしていても、他のinterfaceをimplementsされれば異なる役割を付け足すことはできてしまうのですが、元々がinterfaceの場合と比べれば柔軟過ぎる部分を少し制限したといえるでしょう。
このパッケージの他のXxxxTermクラスは全てSearchTermのサブクラスです。つまり、全てis-a
SearchTermです。
このクラス自身の説明は前の項に書き尽くしましたね。検索条件そのものを表し、match()を呼び出すことでその検索条件に適合するか否かを調べることができるという役割を持ちます。
メッセージがSearchTerm自身で定義される検索条件に適合すればtrueを返します。
サブクラスはさまざまな方法でmsgの内容をチェックしてtrueまたはfalseを返します。