0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-04-30

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で管理しています。

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を使用しています。

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は抽象クラスとなっており、各プロバイダーが提供しているサブクラスが使用されます。

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のサブクラスを取得しています。もう少し読んでみます。

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 になります。

javax.mail.internet.InternetAddress
public String getType() {
    return "rfc822";
}

getTransport(transportProtocol);を読みます。

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に該当するクラスを取得しています。

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.providersMETA-INF/javamail.default.providers の設定値を読み込んでいます。

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.providersMETA-INF/javamail.default.providers
protocol、type、class、vendor、versionをProviderクラスに格納しています。
javax.mail-1.5.0.jarappengine-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です。

設定で指定できる目途はありましたが、特定するまで結構大変でした。

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();
		}
	}
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?