JavaでSMTPサーバー経由でメールを送信する
はじめに
前回、SMTPについて理解を深めてみました。
そもそも元々の動機を説明します。
AndroidアプリをからSMTPサーバー経由でメール送る時、ライブラリ「Jakarta Mail API
」を使用していました。
気になったのは、このライブラリが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引数のautoFlush
をtrue
にすることで、println
、printf
メソッド、または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通信に必要なオブジェクト (SSLSocketFactory
やSSLEngine
など) を作成する役割をもちます)。
今回の例ではサーバー側が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によるソケット通信を行う流れを説明しました。
これ以降の処理は、前回説明したコマンドからの実行と同じなので省略します。
おわりに
SMTPサーバー経由の単純なメールの送信ならライブラリを使わなくとも、上述したようなコードで送信できることがわかりました。また、ソケット通信やSSL/TLSについても理解を深めることができました。
Discussion