📩

JavaでSMTPサーバー経由でメールを送信する

2025/02/20に公開

はじめに

前回、SMTPについて理解を深めてみました。

https://zenn.dev/takudooon/articles/4ce5e2070b2064

そもそも元々の動機を説明します。
AndroidアプリをからSMTPサーバー経由でメール送る時、ライブラリ「Jakarta Mail API
」を使用していました。

https://jakartaee.github.io/mail-api/

https://mvnrepository.com/artifact/com.sun.mail/jakarta.mail

気になったのは、このライブラリがGPLライセンスであることです。
このライセンスではプログラムを頒布した場合、ソースコードの開示が必要になります。そのため商用利用を視野に入れた場合、避けた方が良いと考えました。

それなら前回の記事で書いたようにSMTPのプロトコルを理解して、直接コマンドを打ち込んでメール送信したら良いのでは?、と考えにいたりました。

本記事では先ほど挙げたライブラリを使わず、JavaでSMTPサーバー経由でメールを送る方法について説明します。

ソースコード全体

コード全体としてはSSL/TLSでソケット通信を行い、各コマンド実行時のステータスコードを確認して、順次処理を進めていくという処理の流れになっています。

ソースコード
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Base64;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

public static void sendEmailViaSmtpServer() throws IOException {

		String fromMailAddress = "{$FROM_MAIL}";
		String toMailAddress = "{$TO_MAIL}";
		String smtpHost = "{$HOST_NAME}";
		Integer smtpPort = {$PORT_NUMBER};
		String smtpUser = "{$USER_NAME}";
		String smtpPassword = "{$PASSWORD}";

		try{

				// SMTPサーバーに対してソケット通信を開始
				Socket socket = new Socket(smtpHost, smtpPort);
				PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
				
				BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
				System.out.println("Response : " + reader);
				
				// EHLO
				writer.println("EHLO " + smtpHost);
				handleResponse(reader, 250);
				
				// STARTTLS
				writer.println("STARTTLS");
				System.out.println("Client : STARTTLS");
				
				handleResponse(reader, 220);
				
				// SSL/TLSにアップグレード
				SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
				sslContext.init(null, null, new SecureRandom());
				SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
				
				// SSL/TLSによるsocket通信を適用
				SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(
				socket, smtpHost, Integer.parseInt(smtpPort), true
				);
				
				// クライアントモードでSSL/TLS接続開始
				sslSocket.setUseClientMode(true);
				sslSocket.startHandshake(); // SSLハンドシェークを開始
				
				// SSL/TLS接続後の入出力ストリームの更新
				writer = new PrintWriter(sslSocket.getOutputStream(), true);
				reader = new BufferedReader(new InputStreamReader(sslSocket.getInputStream()));
				
				// ユーザー名・パスワードをBase64形式に変換
				Base64.Encoder encoder = Base64.getEncoder();
				String username = encoder.encodeToString(smtpUser.getBytes(StandardCharsets.UTF_8));
				String password = encoder.encodeToString(smtpPassword.getBytes(StandardCharsets.UTF_8));
				
				// 認証開始
				writer.println("AUTH LOGIN");
				handleResponse(reader, 334);
				
				// ユーザー名入力
				handleResponse(reader, 334);
				
				// パスワード入力
				handleResponse(reader, 235);
				
				System.out.println("Authenticated successfully!");
				
				// ヘッダー情報
				writer.println("MAIL FROM:<" + fromMailAddress +">");
				writer.println("RCPT TO:<" + toMailAddress +">");
				writer.println("DATA");
				writer.println("From: test <" + fromMailAddress +">");
				writer.println("To: test <"+toMailAddress+">");
				writer.println("Subject: hogehoge");
				writer.println("Content-Type: text/plain; charset=UTF-8");
			  // 1行あける
				writer.println("");
				// メール本文
				writer.println("This is a sample mail.");
				// 入力終了
				writer.println(".");
				System.out.println(reader.readLine());
				
				// 終了操作
				writer.println("QUIT");
				// ソケット通信終了
				sslSocket.close();
				
				System.out.println("SMTP session closed.");
		
		}catch(Exception e){
				System.out.println(String.valueOf(e));
		}

}

public static void handleResponse(BufferedReader reader, Integer status) throws IOException {
		String line;
		while((line = reader.readLine()) != null){
				System.out.println("Response : " + line);
				if(line.startsWith(String.valueOf(status))) break;
		}
}

詳細

まずソケット通信でSMTPサーバーのホスト、ポートに対して接続を行います。

Socket socket = new Socket(smtpHost, smtpPort);

PrintWriterを使い,
socket.getOutputStream()で取得したsocketに書き込むストリームへオブジェクトの書式付き表現をテキスト出力ストリームに出力するようにします。第2引数のautoFlushtrueにすることで、printlnprintfメソッド、またはformatメソッドをつかったとき出力バッファをフラッシュ(書き込む)します。これでソケット通信に対してコマンドを送信することができます。

PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);

下記はソケット通信からの応答を出力するための設定です。
InputStreamReaderでバイトストリームから文字ストリームへ変換し、BufferedReaderで文字ストリームを1行ずつ読み込みます。

BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

先ほどのwriterを使い、ソケット通信に対してEHLOを送信しています。そして、handleResponse関数で、ステータスコード250を得たときに次の処理に進むように記述しています。

writer.println("EHLO " + smtpHost);
handleResponse(reader, 250);
public static void handleResponse(BufferedReader reader, Integer status) throws IOException {
	String line;
	while((line = reader.readLine()) != null){
		System.out.println("Response : " + line);
		if(line.startsWith(String.valueOf(status))) break;
	}
}

サーバー側でSSL/TLSに対応している場合を想定しているので、STARTTLSを実行します。ステータスコード220を得たときに次の処理に進みます。

writer.println("STARTTLS");
System.out.println("Client : STARTTLS");
handleResponse(reader, 220);

次に、SSL/TLSでセキュアにソケット通信を行うよう設定を行います。
SSLContextクラスはセキュアなソケット・プロトコルの実装エンジンクラスです。このクラスのインスタンスはSSLソケット・ファクトリおよびSSLエンジンのファクトリとして動作します( = SSL/TLS通信に必要なオブジェクト (SSLSocketFactorySSLEngineなど) を作成する役割をもちます)。

今回の例ではサーバー側がTLSv1.2に対応しているとして、SSLContextクラスのインスタンスを作成しています。こちらはサーバー側の設定に合わせます。
そして、initメソッド(public void init(KeyManager[] km, TrustManager[] tm, SecureRandom random);)でSSLContextを初期化します。第1、2引数をnullにすると、デフォルト設定になります。第3引数のSecureRandomで乱数を生成して渡しています。

// SSL/TLSにアップグレード
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
// 初期化
sslContext.init(null, null, new SecureRandom());
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

そして、SSL/TLSによるソケットへアップグレードします。

SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, smtpHost, Integer.parseInt(smtpPort), true);

さらにSSLクライアントモードになるよう設定します。
これで、クライアントからサーバーへ接続を開始することができます。

// クライアントモードでSSL/TLS開始
sslSocket.setUseClientMode(true);
// SSLハンドシェークを開始
sslSocket.startHandshake(); 

SSL/TLS接続後、入出力ストリームを更新します。
これで、コマンドの送信やそのレスポンスを得ることができます。

writer = new PrintWriter(sslSocket.getOutputStream(), true);
reader = new BufferedReader(new InputStreamReader(sslSocket.getInputStream()));

ここまでSSL/TLSによるソケット通信を行う流れを説明しました。
これ以降の処理は、前回説明したコマンドからの実行と同じなので省略します。

https://zenn.dev/takudooon/articles/4ce5e2070b2064

おわりに

SMTPサーバー経由の単純なメールの送信ならライブラリを使わなくとも、上述したようなコードで送信できることがわかりました。また、ソケット通信やSSL/TLSについても理解を深めることができました。

参考

Discussion