このページでは書籍中で曖昧な箇所や正しくない部分の補足を
行っていきたいと思います。
誤字関係は正誤表を参照して下さい。
BBS上でたくさんのご指摘をいただいているにもかかわらず、 補足としてまとめる時間が取れず、大変申し訳ありません。 せめてもの償いとして、ひとまず、BBSへのリンクという形で 補足が必要な点を明記したいと思います_o_。
以下は補足の目次です。
SMTPやIMAPに特化した機能をJavaMailから利用したいが、 JavaMailのAPIを見ていても出来なさそうに見える、 といった疑問をもたれるかたがわりといらっしゃるようです。 各プロトコルに特化した機能はJavaMailサービスプロバイダの範疇になり、 実際には拡張機能を含めてそこそこの機能が実装されていたりします。 例えば、POP3においてUIDLを獲得する com.sun.mail.pop3.POP3Folder#getUID(Message) といったメソッドがあります。これはJavaMailのAPIの範疇では うまく扱えないものですので、POP3Providerの実装クラスで 提供される機能です。
このような機能はプロバイダのドキュメントを見るとみつけることができます。 プロバイダのドキュメントは、JavaMailのディレクトリ上のdocs/sundocs から参照できます。IMAPのこの機能は使えるのかな?といった疑問があった 場合は、このドキュメントのimapパッケージの概要をみるといろいろ解ります。
書籍ではJavaMailが対応していない「日本語添付ファイル名」を扱うユーティリティメソッドを紹介しています。これについて、特定メイラから送信した日本語ファイル名の添付ファイル付きメイルを受信した時に、正しく日本語ファイル名が取得できないことがあるというご指摘をいただきました。例えばEudoraなどがそうです。このメイラは日本語ファイル名のファイルを添付して送信した場合、Content-Disposition:のfilenameパラメタ、Content-Type:のnameパラメタ双方に、ISO-2022-JPでファイル名を記述します。これはそもそもRFC違反なのですが、RFC2231公開以前は、各ベンダが独自の方式で日本語添付ファイル名を送信していたので仕方のない面もあります。
さて、書籍で紹介しているMailUtility#getFilename()なのですが、このメソッドは一応ISO-2022-JP直書きの添付ファイル名もデコードできるように作ったつもりでした。これは、この辺のコードが意図するものです。
public static String getFileName(Part part) throws MessagingException {
String[] disposition = part.getHeader("Content-Disposition");
if (disposition == null || disposition.length < 1) {
return null;
}
// 本来そのまま返すところだが日本固有のデコードを入れる。
return decodeParameterSpciallyJapanese(
getParameter(disposition[0], "filename"));
}
decodeParameterSpciallyJapanese()というメソッドが、Content-Disposition:のfilenameパラメタにISO-2022-JPコードがそのまま記述されていた場合に対応するためのデコード処理です。この中では、new
String(s.getBytes("ISO-8859-1"), "JISAutoDetect")というようなコードでデコードを試みます。
書籍でほとんど説明していないこともあり、つらつらと書いてみます。上記の部分を順に見ていくと、まず先頭行でContent-Disposition:ヘッダの値を取得します。これでdisposition[0]には、そのパートの最初に現れたContent-Disposition:ヘッダの値全体が格納されます。次にその値をgetParameter()メソッドに渡します。getParameter()メソッドは指定ヘッダの値中の指定パラメタを取り出すものです(*)。RFC2231形式のエンコードがなされていた場合にそのデコードも行います。その後、getParameter()で得られたファイル名に対して、decodeParameterSpciallyJapanese()を呼び出すことで、getParameter()でデコードしきれなかった場合(生の漢字コードが入っていた場合や、RFC2047形式でエンコードされていた場合)に対応しています。
*:ヘッダの値/パラメタという表現はややこしいですが、以下の下線部分がヘッダの値で、太字部分がパラメタです。念のため。
この場合、filenameがパラメタ名で"test.txt"がパラメタ値という呼び方をします。「Content-Disposition:ヘッダのfilenameパラメタに"test.txt"が設定されている」という言い方で通じます。
Content-Disposition: attachment; filename="test.txt"
さて、上記の処理である程度は日本語のデコードに成功するはずなのですが、実際には欠陥がありました。Eudoraで"あ.txt"というファイルを添付して送信したメッセージについて、上記の処理を利用してファイル名を取り出そうとすると、正しいファイル名が得られません。なんて簡単に再現するんだT_T。これ、実はISO-2022-JPでの"あ.txt"が以下のようなバイトシーケンスになることが原因です。
1B 24 42 24 22 1B 28 42 2E 74 78 74
下線部が"あ"のISO-2022-JP表現なのですが、この中に0x22というコードが含まれます。これはダブルクォーテーション(")と同じであり、このようなコードがヘッダのパラメタ中に存在すると、getParameter()メソッド中でパラメタの値がそこで閉じられたと解釈されてしまったというわけです(より厳密にはJavaMailのjavax.mail.internet.HeaderTokenizerクラスがそのように解釈してしまいます)。このような文字コードはspecial characterに分類されるのですが、RFC2045を見る限り、単にダブルクォーテーションで囲むべしとしか書いていないようです。そうすると、getParameter()メソッドが、パラメタの値の終わりを行末または';'で検出しなければならないところを、ダブルクォーテーションが閉じたところでパラメタの区切りとみなしてしまうことに問題があるということになります。# なんだか釈然としないのですが^^
暫定処置として、以下のように修正することでこの問題は回避できます。
public static String getFileName(Part part) throws MessagingException {
:
// return decodeParameterSpciallyJapanese(
// getParameter(disposition[0], "filename"));
return getParameter(disposition[0], "filename");
}
public static String getParameter(String header, String name) {
header = decodeParameterSpciallyJapanese(header);
:
怪しい(RFCに沿っていない)エンコードと思われる部分は先にデコードをすませてしまうということです。これで、JISやShift_JISが混入されたヘッダのパラメタを解釈できるようになります。ただし、かなり特殊な状況では、逆にRFCに忠実なメッセージの適正な解釈に失敗する可能性があるあまり好ましくない実装であることを、念のためお断りしておきます(ほぼ問題はないのですが)。
また、ご参考までに、Windowsにおける各メイラが日本語添付ファイル名をどのような形式で送信するかがまとめられているページをご紹介します。
すごくためになりますので、E-mailに興味のある方は全てのページを読む価値ありです:)。
P31中ほどに以下のような記述があります。
「通常MUAはReceiverMTAに対して、メッセージヘッダ上のTo: cc: bcc: Resent-To: Resent-cc: Resent-bcc:に書かれている全てのメイルアドレスについてのrcpt to:コマンドを送信します(そしてbcc:ヘッダは削除されます)。」
これは実は正確ではありません。何故正確でないかは2章の各ヘッダの説明を見ると分かりますが、丁寧に書くとこうですね。
「通常MUAはReceiverMTAに対して、メッセージヘッダ上にResent-To: Resent-cc: Resent-bcc:が存在する場合はそれらの全て、存在しない場合はTo: cc: bcc: に書かれている全てのメイルアドレスについてのrcpt to:コマンドを送信します(そしてResent-bcc:及びbcc:ヘッダは削除されます)。」
しかし、さらに実を言うと、Resent-To: Resent-cc: Resent-bcc:
ヘッダに未対応で、これらのヘッダがあっても無視してTo: cc: bcc:
に書かれたアドレスに送信するMUAの方が多数派だったりします^^。
書籍ではRFC822にのっているという事で(且つマイナーである事から)、どういうものかの説明をけっこうしているんですが、やっぱり使われることはほとんどないんですね…。
で、上記記述についてはResent-*について細かく言いたくない文脈だった、且つ見直し時は読み流していたという感じなのですが、やはり正確でない記述であることは間違いないという事でこちらで補足とさせていただきます_o_。
書籍中では(よく質問になることにもかかわらず)、メッセージのサーバからの削除手順についてちゃんと書ききっていませんでした。また、3章のSimpleRetriever.java等を用いて受信したメッセージを削除しようとしても消えてくれない、というご質問もいただいております。ここではメッセージの削除について整理しておこうと思います。
まず、メッセージのサーバからの削除はプロトコル依存の部分ではありますが、JavaMail API上では削除マークという統一概念を利用して、基本的には以下の手順でどのプロトコルでもメッセージの削除が実行されることになっています。
これが基本です。よく間違えられる点は以下です。
前者については、Folderのopen時にREAD_ONLYでオープンしてしまっている可能性が高いです。削除を可能にするには READ_WRITEでopenしましょう。
後者について、POP3Providerの場合はexpunge()メソッドを呼んでも何も起こりません。
expunge()メソッドはサーバとの接続を保ったまま(open状態で)メッセージの削除指示を行うメソッドですが、そもそもPOP3はQUITコマンド時に削除マークの付いたメッセージの削除を行うというものですので、expunge()メソッドの実装がないのです。
(もちろん裏でquit->再接続を行うようにProviderを実装する事も不可能ではありませんが)
さて、書籍の方ですが、3-5章でご紹介したSimpleRetriever.javaは、デフォルトではREAD_ONLYでFolderのopenを行うようになっていますので、普通に使うとメッセージの削除が行えません。これについては、同クラス内に定義したsetWritable(true)を呼び出す事で、以降はREAD_WRITEでopenを行うようになります。この点書籍では説明不足でした_o_。
この項では、JavaMailが対応していない添付ファイル名に日本語を使用する方法を説明しており、サンプルコードを掲載しているのですが、使い方の説明がなく、いささか不親切でした_o_。ここで簡単に使用方法を記述します。
一つ前のencodeText()/decodeText()等もそうなのですが、書籍にはメソッドのみを掲載しています。これらはサンプルコードにあるMailUtilityクラスの中で実装しているのですが、執筆時点の意図としては、掲載したstaticメソッドを自分のクラスに組み込んで使って下さって結構です、という立場です。
使用方法はメソッドシグネチャを見れば解るかとたかをくくっていましたが、よく読み返すとやはりこれだけでは解りにくいですね。すみません。
まず、添付ファイル名を設定するsetFileName()メソッドについてですが、これは以下のようなシグネチャとなっています。
public static void setFileName(Part part, String filename,
String charset, String lang)
throws MessagingException {
:
JavaMailのAPIで添付ファイル名を設定する時は、添付ファイルを表すMimeBodyPartオブジェクトのsetFilename()メソッドを使いますよね。
MimeBodyPart part = new MimeBodyPart();
part.setDataHandler(new DataHandler(new FileDataSource("日本語.txt")));
part.setFileName("日本語.txt");
赤字の部分を以下のように置き換える事で、日本語添付ファイル名を正しく設定できるようになります。
MailUtility.setFileName(part, "日本語.txt", "ISO-2022-JP", null);
langパラメタにはnullを渡しても構いません。"ja"を渡しておくのが安全かもしれませんが、現状はlangに相当する部分は解釈するMUAが見当たらず意味のないものとなっているようです。
次に添付ファイル名の取得です。これも全く同様にMimePart#getFileName()を使用する場面を以下のように置き換える事で、添付ファイル名に日本語が用いられていた場合も正しく取得できるようになります。
String filename = MailUtility.getFileName(part);
この項では、Content-Transfer-Encoding: が"7-bit"のように誤った指定がされていた場合にPart#getContent()が失敗するケースについての回避方法について述べています。この場合の一般的な解は本文に記した通り、以下のようなコードになります。
try {
object = msg.getContent();
} catch (IOException e) {
// 特定の例があれば、ここに対処コードを記述
if (何らかの条件) {
// ヘッダの補正
} else {
// 原因不明な場合は取り敢えず"7bit"とする。
msg.setHeader("Content-Transfer-Encoding", "7bit");
}
object = msg.getContent();
}
ただ、この方法は「4-5-6.MIME未対応メイラの送信するメッセージへの対応」の項で記した通り、プロバイダが生成した読み取り専用のMessageオブジェクトに対しては利用できないため、現実的にはそのままではうまくいかないケースの方が多くなってしまいます。
(読み取り専用だった場合、setHeader()で例外が発生してしまいます)
ここはJavaMail1.2のMimeMessageのコピーコンストラクタを用いて変更可能なコピーを生成してから、Content-Transfer-Encoding:の補正&getContent()を行うか、getRawInputStream()で生のストリームを得て、そのストリームを用いてなんとかする例を示すべきでした。前者の場合は以下のようになります。ただし、メッセージが巨大な場合もコピーが生成される事に注意が必要です。
try {
object = msg.getContent();
} catch (IOException e) {
// 特定の例があれば、ここに対処コードを記述
if (何らかの条件) {
// ヘッダの補正
} else {
msg = new MimeMessage(msg);
// 原因不明な場合は取り敢えず"7bit"とする。
msg.setHeader("Content-Transfer-Encoding", "7bit");
}
object = msg.getContent();
}
後者の場合は、JavaMailが提供するストリームからオブジェクトを取得する機能を利用できません。それがあまり嬉しくなかった為詳しく紹介しなかったのですが、気になって確認したところ、ちょっと手順は複雑になりますが、CorrectedContentTypeDataSourceと同様の手順でも可能らしい事が解りました。つまり、 Content-Type:がおかしい場合の対応と同様の手法を適用して、MIMEタイプに応じたオブジェクトをJavaMailに生成させることが可能でした。
CorrectedContentTypeDataSourceクラスはDataSourceオブジェクトをラップして、getContentType()メソッドで補正したContent-Typeを返すようにするものでしたが、このクラスのgetInputStream()メソッドで、誤った転送エンコーディングを補正したInputStreamを返すことができればよいわけでして、そのためにはPartのgetRawInputStream()を呼び出せる必要があります。
ラップするDataSourceがMimePartDataSourceである場合(JavaMailで受信したメッセージに対してはこのオブジェクトになるはずです)、このクラスはMessageAware interfaceをimplementsしている為、getMessageContext()でMessageContextを得て、そこからPartオブジェクトを獲得する事が可能でした。
Part part = mimePartDataSource.getMessageContext().getPart();
これによって得たPartオブジェクトはMimeMessage/MimeBodyPartの何れかにキャストする事で、getRawInputStream()を呼び出すことができます(*)。この手法により、ラッパDataSourceのgetInputStream()メソッド内からPartに含まれるデコード前のストリームを得る事が可能になります。
後はContent-TransferEncoding:がどのようなものだったかに応じて、MimeUtility#decode(inputStream, encoding)を用いて独自のデコードを生成すれば良いことになります。
*:このメソッドがMimePart interfaceで宣言されていない理由は下位互換性の為です。おかげでinstanceofでサブクラスの型を調べてキャストしなければならないのですが…。
サンプルとして、本文中に示したCorrectedContentTypeDataSourceUTF7Supportを改造したものを以下に記します。
package com.sk_jp.mail;
import java.io.InputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import javax.mail.Part;
import javax.mail.MessagingException;
import javax.mail.MessageAware;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.ContentType;
import javax.mail.internet.ParseException;
import javax.activation.DataSource;
import com.sk_jp.io.ByteToCharUTF7;
/**
* Content-Type:の不適合をISO-2022-JPに補正します。
* さらにcharset=UTF-7の場合にUTF-16のストリームに変換してgetContent()を
* 無理やり成功させます。<BR>
* また、未知のTES(Content-Transfer-Encoding:)だった場合に、"7bit"
* と見なしてボディを取得します。
* 使用方法は<PRE>
* Object o = new DataHandler(
* new CorrectedContentTypeDataSourceUTF7Support(part, charset)
* ).getContent();
* </PRE><P>のようになります。</P><P>
* スレッドセーフではありませんので利用者側で排他制御を行ってください。
* </P>
* @author Shin
* @version $Revision: 1.2 $ $Date: 2001/03/04 13:32:56 $
*/
class CorrectedContentTypeDataSourceUTF7Support
extends CorrectedContentTypeDataSource {
private boolean utf7 = false;
public CorrectedContentTypeDataSourceUTF7Support() {}
public CorrectedContentTypeDataSourceUTF7Support(
DataSource dataSource,
String defaultCharset) {
super(dataSource, defaultCharset);
}
public CorrectedContentTypeDataSourceUTF7Support(
Part part,
String defaultCharset)
throws MessagingException {
super(part, defaultCharset);
}
public void setDataSource(DataSource newSource) {
super.setDataSource(newSource);
utf7 = false;
}
public void setDefaultCharset(String defaultCharset) {
super.setDefaultCharset(defaultCharset);
utf7 = false;
}
public String getContentType() {
try {
ContentType contentType = new ContentType(super.getContentType());
String specifiedCharset = contentType.getParameter("charset");
if ("UTF-7".equalsIgnoreCase(specifiedCharset)) {
// UTF-7コンバータが存在しない為、
// 独自フィルタストリームを用いる。
contentType.setParameter("charset", "UTF-16");
utf7 = true;
}
return contentType.toString();
} catch (ParseException e) {
throw new InternalError();
}
}
public InputStream getInputStream() throws IOException {
InputStream in;
try {
in = super.getInputStream();
} catch (IOException e) {
// ここでのIOExceptionはエンコーディング不良の可能性が高い。
// 生InputStreamを得てリトライ
if (!(source instanceof MessageAware)) {
throw e;
}
Part part = ((MessageAware)source).getMessageContext().getPart();
try {
if (part instanceof MimeMessage) {
in = ((MimeMessage)part).getRawInputStream();
} else if (part instanceof MimeBodyPart) {
in = ((MimeBodyPart)part).getRawInputStream();
} else {
throw e;
}
} catch (MessagingException mex) {
throw new IOException(mex.getMessage());
}
}
if (!utf7) {
return in;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
int c;
while ((c = in.read()) != -1) {
out.write(c);
}
ByteToCharUTF7 btc = new ByteToCharUTF7();
byte[] bytes = out.toByteArray();
char[] chars = new char[bytes.length / 2 + 1];
btc.convert(bytes, 0, bytes.length,
chars, 0, chars.length);
String string = new String(chars);
return new ByteArrayInputStream(string.getBytes("UTF-16"));
}
}
他の部分は書籍で説明しているので、太字の部分のみ触れます。
まず、オリジナルのgetInputStream()を呼び出すのですが、このとき、Content-Transfer-Encoding:が不正だとIOExceptionが発生します。このとき、(CorrectedContentTypeDataSourceUTF7SupportはDataSourceのラッパである事から)sourceは通常MimePartDataSourceです。MimePartDataSourceはMessageAwareをimplementsしている為、このインターフェイスを元にPartを取得する事が可能です。
そのようにして得たオリジナルメッセージのPartオブジェクトがMimeMessage/MimeBodyPartの何れであるかをinstanceofで調べて、それぞれのオブジェクトにあるgetRawInputStream()を呼び出す事で、デコード前のストリームを得ています。
ここまでの処理に失敗すると、catchしていたIOExceptionをそのまま上位に投げ直しています。
ようするに、復旧できる可能性があるならやるだけやってみようという立場です^^。
このクラスのオブジェクトは、コメントにある通り、DataHandlerに与えてDataHandler#getContent()を呼び出す事で内部的に利用されます。
Content-Type: text
Content-Type: text/plain; charset=utf-7
Content-Transfer-Encoding: 7-bit
といったJavaMailが通常処理できないメッセージを処理できるようにしているわけです。
さて、いきなり長い補足になりましたが、書籍中に記した方法でも、基本的な方針は間違っていません。ただほとんどの場合にMessageオブジェクトの改変ができない為、適用しようとすると詰まる可能性が高い悪い例であったと思います。申し訳ありません。
とはいえ、ここに示した方法なんかややこしくてさっぱり理解できないかもしれません…。でも使う分にはそんなに解りにくくないですよね、ね。今度気力があれば図を書きたいかなと思います_o_。