JavaMailでライブラリの競合(というか設定ミス)

Google App Engineのライブラリ「appengine-api-1.0-sdk」を追加したところ、
今まで動いていたJavaMailが異常終了するようになりました。

原因は、javax.mail-1.5.0.jarのcom.sun.mail.smtp.SMTPTransportクラスが、appengine-api-1.0-sdkのcom.google.appengine.api.mail.stdimpl.GMTransportクラスに置き換わったことでした。

```java:スタックトレース
com.google.apphosting.api.ApiProxy$CallNotFoundException: The API package 'mail' or call 'Send()' was not found.
	at com.google.apphosting.api.ApiProxy.makeSyncCall(ApiProxy.java:100)
	at com.google.apphosting.api.ApiProxy.makeSyncCall(ApiProxy.java:55)
	at com.google.appengine.api.mail.MailServiceImpl.doSend(MailServiceImpl.java:96)
	at com.google.appengine.api.mail.MailServiceImpl.send(MailServiceImpl.java:32)
	at com.google.appengine.api.mail.stdimpl.GMTransport.sendMessage(GMTransport.java:231)
	at javax.mail.Transport.send0(Transport.java:254)
	at javax.mail.Transport.send(Transport.java:124)
	at com.example.main.SendMail.main(SendMail.java:39)
```

ライブラリはmavenで管理しています。

```:pom.xml
<dependency>
    <groupId>com.sun.mail</groupId>
    <artifactId>javax.mail</artifactId>
    <version>1.5.0</version>
</dependency>
<dependency>
	<groupId>com.google.appengine</groupId>
	<artifactId>appengine-api-1.0-sdk</artifactId>
	<version>1.7.3</version>
</dependency>
```

jarは先に読み込まれたほうが優先されるみたいですが、読み込み順に頼るのは不安です。
明示的にクラスを指定する方法を探るべく、ソースを読んでみました。

まず、サンプルで私が実装したメール送信クラスです。
SMTPサーバーはgmailを使用しています。
```java:com.example.main.SendMail
public class SendMail {

	public static void main(String[] args) {
		try {
			Properties property = new Properties();
			property.put("mail.smtp.host", "smtp.gmail.com");
			property.put("mail.smtp.auth", "true");
			property.put("mail.smtp.port", "587");
			property.put("mail.smtp.starttls.enable", "true");
			property.put("mail.smtp.ssl.protocols", "TLSv1.2");

			Session session = Session.getInstance(property, new javax.mail.Authenticator() {
				protected PasswordAuthentication getPasswordAuthentication() {
					return new PasswordAuthentication("アカウント", "パスワード");
				}
			});
			
			MimeMessage mimeMessage = new MimeMessage(session);
			InternetAddress toAddress = new InternetAddress("宛先アドレス", "宛先アドレスの表示名称");
			mimeMessage.setRecipient(Message.RecipientType.TO, toAddress);
			InternetAddress fromAddress = new InternetAddress("差出人アドレス", "差出人アドレスの表示名称");
			mimeMessage.setFrom(fromAddress);
			mimeMessage.setSubject("テスト送信", "ISO-2022-JP");
			mimeMessage.setText("あいうえお", "ISO-2022-JP");
			Transport.send(mimeMessage);
		} catch (SendFailedException se) {
			Address[] invalidAddresses = se.getInvalidAddresses();
			for (Address adress : invalidAddresses) {
				if (adress != null) {
					System.out.println("フォーマットエラー : " + adress);
				}
			}
			Address[] unsentAddresses = se.getValidUnsentAddresses();
			for (Address adress : unsentAddresses) {
				if (adress != null) {
					System.out.println("送信失敗 : " + adress);
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}
```
プロパティに設定内容をセット、gmailの認証のため、Authenticatorクラスにgmailのアカウント、アプリパスワードをセットしています。
宛先、差出人、件名、本文をMimeMessageクラスのインスタンスにセットした後、↓のメソッドでメール送信を実行しています。
__Transport.send(mimeMessage);__

このTransportは抽象クラスとなっており、各プロバイダーが提供しているサブクラスが使用されます。
```java:javax.mail.Transport
public static void send(Message msg) throws MessagingException {
	msg.saveChanges(); // do this first
	send0(msg, msg.getAllRecipients(), null, null);
}
（略）

private static void send0(Message msg, Address[] addresses,
    String user, String password) throws MessagingException {
    （略）
	Session s = (msg.session != null) ? msg.session :
		     Session.getDefaultInstance(System.getProperties(), null);
	Transport transport;

	/*
	 * Optimize the case of a single protocol.
	 */
	if (dsize == 1) {
	    transport = s.getTransport(addresses[0]);
	    try {
		if (user != null)
		    transport.connect(user, password);
		else
		    transport.connect();
		transport.sendMessage(msg, addresses);
	    } finally {
		transport.close();
	    }
	    return;
	}
}
```

__transport = s.getTransport(addresses[0]);__ で、Transportのサブクラスを取得しています。もう少し読んでみます。
```java:javax.mail.Session
public Transport getTransport(Address address) throws NoSuchProviderException {
    String transportProtocol;
   	transportProtocol = getProperty("mail.transport.protocol." + address.getType());
	if (transportProtocol != null)
        return getTransport(transportProtocol);
    transportProtocol = (String)addressMap.get(address.getType());
	if (transportProtocol != null)
	　  return getTransport(transportProtocol);
	throw new NoSuchProviderException("No provider for Address type: "+
	    address.getType());
}
```
プロパティから"mail.transport.protocol." + address.getType()の値を取り出していますね。
address.getType()は文字列"rfc822"のため、プロパティキーは  __mail.transport.protocol.rfc822__ になります。
```java:javax.mail.internet.InternetAddress
public String getType() {
    return "rfc822";
}
```

getTransport(transportProtocol);を読みます。
```java:javax.mail.Session
public Transport getTransport(String protocol) throws NoSuchProviderException {
	return getTransport(new URLName(protocol, null, -1, null, null, null));
}

public Transport getTransport(URLName url) throws NoSuchProviderException {
	String protocol = url.getProtocol();
	Provider p = getProvider(protocol);
	return getTransport(p, url);
}
```
__Provider p = getProvider(protocol);__ では、ハッシュテーブルprovidersByProtocolから、プロパティキーprotocolに該当するクラスを取得しています。
```java:javax.mail.Session
public synchronized Provider getProvider(String protocol) throws NoSuchProviderException {
    （略）
	Provider _provider = null;
    （略）
	if (_provider != null) {
	    return _provider;
	} else {
	    _provider = (Provider)providersByProtocol.get(protocol);
	}
	（略）
}
```

ハッシュテーブル"providersByProtocol"はSessionクラスのコンストラクタで __META-INF/javamail.providers__ 、__META-INF/javamail.default.providers__ の設定値を読み込んでいます。
```java:javax.mail.Session
private Session(Properties props, Authenticator authenticator) {
	this.props = props;
	this.authenticator = authenticator;
	（略）
	Class cl;
	if (authenticator != null)
	    cl = authenticator.getClass();
	else
	    cl = this.getClass();
	// load the resources
	loadProviders(cl);
	loadAddressMap(cl);
}

private void loadProviders(Class cl) {
	StreamLoader loader = new StreamLoader() {
	    public void load(InputStream is) throws IOException {
    	    loadProvidersFromStream(is);
	    }
	};
	try {
	    String res = System.getProperty("java.home") + 
			File.separator + "lib" + 
			File.separator + "javamail.providers";
	        loadFile(res, loader);
	} catch (SecurityException sex) {
	        logger.log(Level.CONFIG, "can't get java.home", sex);
	}
	loadAllResources("META-INF/javamail.providers", cl, loader);
	loadResource("/META-INF/javamail.default.providers", cl, loader);
}

private void loadAllResources(String name, Class cl, StreamLoader loader) {
    boolean anyLoaded = false;
	try {
	    URL[] urls;
	    ClassLoader cld = null;
	    cld = getContextClassLoader();
	    if (cld == null)
	        cld = cl.getClassLoader();
	    if (cld != null)
		    urls = getResources(cld, name);
	    else
		    urls = getSystemResources(name);
	    if (urls != null) {
		    for (int i = 0; i < urls.length; i++) {
		        URL url = urls[i];
		        InputStream clis = null;
		   	    try {
			        clis = openStream(url);
			        if (clis != null) {
			            loader.load(clis);
			            anyLoaded = true;
			    	（略）
    }
}

private void loadResource(String name, Class cl, StreamLoader loader) {
	InputStream clis = null;
	try {
   	    clis = getResourceAsStream(cl, name);
    	if (clis != null) {
			loader.load(clis);
			（略）
        }
   	}
}

private void loadProvidersFromStream(InputStream is) throws IOException {
	if (is != null) {
    	LineInputStream lis = new LineInputStream(is);
    	String currLine;

    	while ((currLine = lis.readLine()) != null) {
			if (currLine.startsWith("#"))
			    continue;
			Provider.Type type = null;
			String protocol = null, className = null;
			String vendor = null, version = null;
		    
			StringTokenizer tuples = new StringTokenizer(currLine,";");
			while (tuples.hasMoreTokens()) {
	   		    String currTuple = tuples.nextToken().trim();
	    		int sep = currTuple.indexOf("=");
	    		if (currTuple.startsWith("protocol=")) {
					protocol = currTuple.substring(sep+1);
	    		} else if (currTuple.startsWith("type=")) {
					String strType = currTuple.substring(sep+1);
				if (strType.equalsIgnoreCase("store")) {
		    		type = Provider.Type.STORE;
				} else if (strType.equalsIgnoreCase("transport")) {
		    		type = Provider.Type.TRANSPORT;
	    		} else if (currTuple.startsWith("class=")) {
					className = currTuple.substring(sep+1);
	    		} else if (currTuple.startsWith("vendor=")) {
					vendor = currTuple.substring(sep+1);
	    		} else if (currTuple.startsWith("version=")) {
					version = currTuple.substring(sep+1);
	    		}
			}
    		（略）
    		Provider provider = new Provider(type, protocol, className, vendor, version);
    		addProvider(provider);
	    }
	}
}
```
__META-INF/javamail.providers__、__META-INF/javamail.default.providers__ の
protocol、type、class、vendor、versionをProviderクラスに格納しています。
__javax.mail-1.5.0.jar__ と __appengine-api-1.0-sdk-1.7.3.jar__ の該当ファイルを確認します。

__・javax.mail-1.5.0.jar__
```:META-INF/javamail.default.providers
# JavaMail IMAP provider Oracle
protocol=imap; type=store; class=com.sun.mail.imap.IMAPStore; vendor=Oracle;
protocol=imaps; type=store; class=com.sun.mail.imap.IMAPSSLStore; vendor=Oracle;
# JavaMail SMTP provider Oracle
protocol=smtp; type=transport; class=com.sun.mail.smtp.SMTPTransport; vendor=Oracle;
protocol=smtps; type=transport; class=com.sun.mail.smtp.SMTPSSLTransport; vendor=Oracle;
# JavaMail POP3 provider Oracle
protocol=pop3; type=store; class=com.sun.mail.pop3.POP3Store; vendor=Oracle;
protocol=pop3s; type=store; class=com.sun.mail.pop3.POP3SSLStore; vendor=Oracle;
```

__・appengine-api-1.0-sdk-1.7.3.jar__
```:META-INF/javamail.providers
protocol=gm; type=transport; class=com.google.appengine.api.mail.stdimpl.GMTransport;
```
protocolごとにクラスが設定されています。
javax.mail-1.5.0.jarは複数設定がありますが、appengine-api-1.0-sdk-1.7.3.jarは1つしかありません。
appengine-api-1.0-sdk-1.7.3.jarが先に読み込まれ、私が作成したソースはprotocolを指定していなかったため、 __com.google.appengine.api.mail.stdimpl.GMTransport__ がTransportクラスのサブクラスとして使用されていました。

protocolを指定すれば、com.sun.mail.smtp.SMTPTransportを固定して使用できそうです。
protocolはプロパティ __mail.transport.protocol.rfc822__ の値でした。

com.sun.mail.smtp.SMTPTransportクラスのprotocolの設定値はsmtpですので、
サンプルソースに __property.put("mail.transport.protocol.rfc822", "smtp");__ を追加すればOKです。

設定で指定できる目途はありましたが、特定するまで結構大変でした。
```java:com.example.main.SendMail
public class SendMail {

	public static void main(String[] args) {
		try {
			Properties property = new Properties();
			property.put("mail.smtp.host", "smtp.gmail.com");
			property.put("mail.smtp.auth", "true");
			property.put("mail.smtp.port", "587");
			property.put("mail.smtp.starttls.enable", "true");
			property.put("mail.smtp.ssl.protocols", "TLSv1.2");	
            property.put("mail.transport.protocol.rfc822", "smtp");
			
			Session session = Session.getInstance(property, new javax.mail.Authenticator() {
				protected PasswordAuthentication getPasswordAuthentication() {
					return new PasswordAuthentication("アカウント", "パスワード");
				}
			});
			
			MimeMessage mimeMessage = new MimeMessage(session);
			InternetAddress toAddress = new InternetAddress("宛先アドレス", "宛先アドレスの表示名称");
			mimeMessage.setRecipient(Message.RecipientType.TO, toAddress);
			InternetAddress fromAddress = new InternetAddress("差出人アドレス", "差出人アドレスの表示名称");
			mimeMessage.setFrom(fromAddress);
			mimeMessage.setSubject("テスト送信", "ISO-2022-JP");
			mimeMessage.setText("あいうえお", "ISO-2022-JP");
			Transport.send(mimeMessage);
		} catch (SendFailedException se) {
			Address[] invalidAddresses = se.getInvalidAddresses();
			for (Address adress : invalidAddresses) {
				if (adress != null) {
					System.out.println("フォーマットエラー : " + adress);
				}
			}
			Address[] unsentAddresses = se.getValidUnsentAddresses();
			for (Address adress : unsentAddresses) {
				if (adress != null) {
					System.out.println("送信失敗 : " + adress);
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}
```