Sun JavaMail(mail.jar)とApache Geronimo(geronimo-j2ee_1.4_spec-1.0.jar)の競合

クラスパスにJavaMailの(Sunのリファレンス実装である)mail.jarとgeronimo-j2ee_1.4_spec-1.0.jarが両方含まれていると、SMTPPOP3などのプロトコル使用時にmail.jar側とGeronimo側の実装が混在して使用され、うまく動かないことがある。

今回POP3で接続するプログラムでこの現象が起こったときの症状をまとめておく。これらの症状が出た場合、クラスパスを見直すと良いかも。

環境

  • Java5
  • activation.jar(JAF 1.0.2)
  • mail.jar(JavaMail 1.3.1)
  • geronimo-j2ee_1.4_spec-1.0.jar(Seasar 2.4同梱)

connect()で接続されない

以下のようなサンプルで、connect()で接続がおこなわれず、その後の操作で"Not connected"と言われ例外が投げられる。
但し、connect(host, user, password)で接続すると接続できる。

サンプルコード1:

        String host;		// POP3サーバが設定されてるとしよう
        final String user;	// メールアカウントが設定されてるとしよう
        final String password;	// パスワードが設定されてるとしよう
...
        Properties props = new Properties();
        props.setProperty("mail.pop3.host", host);

        Session session = Session.getInstance(props, new Authenticator() {
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(user, password);
            }
        });

        Store store = session.getStore("pop3");
        store.connect();
        Folder folder = store.getFolder("INBOX"); // ここで例外
...

スタックトレースの結果

javax.mail.MessagingException: Not connected
	at com.sun.mail.pop3.POP3Store.checkConnected(POP3Store.java:223)
	at com.sun.mail.pop3.POP3Store.getFolder(POP3Store.java:205)
...

ヘッダが取得できない

以下のようなサンプルでヘッダを取得しようとするが、正しい値を取得できない。
また、Recipientを取得しようとするとNullPointerExceptionが投げられる

サンプルコード2(サンプルコード1の続き):

...
            folder.open(Folder.READ_ONLY);
            Message[] messages = folder.getMessages();
            if (messages != null) {
                for (Message message: messages) {
                    String contentType = message.getContentType();  // 常にtext/plain
                    String subject = message.getSubject();          // 常にnull
                    Address[] toAddresses =
                        message.getRecipients(RecipientType.TO);    // ぬるぽ
...

スタックトレースの結果

java.lang.NullPointerException
	at javax.mail.internet.MimeMessage.isStrictAddressing(MimeMessage.java:597)
	at javax.mail.internet.MimeMessage.getRecipients(MimeMessage.java:258)
...

また、session.setDebug(true)でトレースを表示すると、以下のようにSTATしか取ってなくTOPやRETRが送られていない。

DEBUG POP3: connecting to host "hogehoge", port 110
S: +OK <9999.1253072068@hogehoge>
C: USER 〔ユーザ名〕
S: +OK 
C: PASS 〔パスワード〕
S: +OK 
C: STAT
S: +OK 1 74477
C: QUIT
S: +OK 

コンテンツを取得できない

以下のようなサンプルでヘッダを取得しようとするが、例外が発生する。

サンプルコード3(サンプルコード2の続き):

...
                    Object content = message.getContent(); // ここで例外
...

スタックトレースの結果

java.io.IOException: No content
	at javax.mail.internet.MimePartDataSource.getInputStream(MimePartDataSource.java:52)
	at com.sun.mail.handlers.text_plain.getContent(text_plain.java:64)
	at javax.activation.DataHandler.getContent(DataHandler.java:145)
	at javax.mail.internet.MimeMessage.getContent(MimeMessage.java:472)
...

スタックトレースの結果(cause)

javax.mail.MessagingException: No content
	at javax.mail.internet.MimeMessage.getContentStream(MimeMessage.java:456)
	at com.sun.mail.pop3.POP3Message.getContentStream(POP3Message.java:182)
	at javax.mail.internet.MimePartDataSource.getInputStream(MimePartDataSource.java:43)
	at com.sun.mail.handlers.text_plain.getContent(text_plain.java:64)
	at javax.activation.DataHandler.getContent(DataHandler.java:145)
	at javax.mail.internet.MimeMessage.getContent(MimeMessage.java:472)
...

どうして混ざるのか

Geronimo側の実装もSunの実装も、spec部分のクラス構造などについては共通と思われる。Geronimoの実装には個々のプロトコルを使用するためのservice providerが含まれて居ない為、getStore("pop3")などでそれらを呼び出した時には、クラスパス上の優先度によりGeronimo側が使用されている場合でも、個々のプロトコルに対する実装はSun側のものが使用される。


例えば、上記のスタックトレースで言えば、

Geronimo =>	at javax.mail.internet.MimeMessage.getContentStream(MimeMessage.java:456)
Sun      =>	at com.sun.mail.pop3.POP3Message.getContentStream(POP3Message.java:182)
Geronimo =>	at javax.mail.internet.MimePartDataSource.getInputStream(MimePartDataSource.java:43)
Sun      =>	at com.sun.mail.handlers.text_plain.getContent(text_plain.java:64)
Geronimo =>	at javax.activation.DataHandler.getContent(DataHandler.java:145)
Geronimo =>	at javax.mail.internet.MimeMessage.getContent(MimeMessage.java:472)

のように、呼び出し元/呼び出し先や親子のクラス間で実装が混在することになる。


仕様が完全に同じであれば、由来するパッケージが違っても問題はない。実際にはGeronimoの実装とSunの実装で仕様が異なるため、おかしな動きをすることになる。


例えば、MimeMessageのコンストラクタMimeMessage(Folder,int)において、Geronimo側ではheadersフィールドを空のInternetHeadersインスタンスで初期化しているが、Sun側では初期化をおこなっていない(null値のままである。) POP3MessageはgetHeader(String,String)等でヘッダ情報にアクセスがあった際に、headersフィールドがnullであればTOPコマンドでPOPサーバから内容を取得する実装になっている(つまり、未取得時にはnullであることを期待している)が、POP3Messageの親クラスのMimeMessageにGeronimo側の実装が使われていると、既に空のInternetHeadersインスタンスがセットされておりheadersフィールドがnullではないので、取得処理がおこなわれず、上記の「ヘッダが取得できない」に見られるような現象が発生する。