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クラスに置き換わったことでした。
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で管理しています。
<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を使用しています。
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は抽象クラスとなっており、各プロバイダーが提供しているサブクラスが使用されます。
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のサブクラスを取得しています。もう少し読んでみます。
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 になります。
public String getType() {
return "rfc822";
}
getTransport(transportProtocol);を読みます。
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に該当するクラスを取得しています。
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 の設定値を読み込んでいます。
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
# 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
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です。
設定で指定できる目途はありましたが、特定するまで結構大変でした。
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();
}
}
}