ホーム › ネットワーク › N-06: SMTP プロトコル手作り
N-06: TCP ソケットによるメール送信(SMTP プロトコル手作り)
TCP ソケットで SMTP コマンドを手作りし、メール送信の仕組みを理解するサンプルです。 実際のネットワーク通信を通じて、EHLO・AUTH LOGIN・MAIL FROM・RCPT TO・DATA などの SMTP コマンドがどのようにやり取りされるかを学べます。
⚠️ このページは「仕組みを理解する」ための低レベル実装です
実務でメールを送信するときは N-05(Jakarta Mail) を使ってください。 SMTP プロトコルを手作りすると TLS 対応・エンコーディング処理など多くの考慮事項があり、 バグが混入しやすくなります。このサンプルはプロトコルの流れを学ぶ目的で作成しています。
SMTP プロトコルとは
SMTP(Simple Mail Transfer Protocol)はメール送信に使うテキストベースのプロトコルです。 クライアントがコマンドを送り、サーバーが3桁の数字(レスポンスコード)と説明文で返答するという シンプルな対話形式で通信します。
| コマンド | 意味 | 例 |
|---|
| EHLO | クライアントの自己紹介(拡張 SMTP) | EHLO client.example.com |
| AUTH LOGIN | ユーザー認証の開始 | AUTH LOGIN → Base64 のやり取り |
| MAIL FROM | 送信者メールアドレスの指定 | MAIL FROM:<from@example.com> |
| RCPT TO | 受信者メールアドレスの指定 | RCPT TO:<to@example.com> |
| DATA | メール本文の送信開始 | DATA → ヘッダー・本文 → . で終了 |
| QUIT | 接続の終了 | QUIT |
| レスポンスコード | 意味 |
|---|
| 220 | サービス準備完了(接続成功) |
| 250 | リクエスト成功 |
| 334 | サーバーのチャレンジ(Base64 データ送信を促す) |
| 235 | 認証成功 |
| 354 | メール本文の入力を待っている |
| 221 | 接続終了 |
| 550 | エラー(メールボックスが存在しないなど) |
サンプルコード
SMTP プロトコルのコマンドとレスポンスの流れをシミュレーションするサンプルです。 Java 8 版では Socket を使った基本的な SMTP クライアントクラスと プロトコルフローの表示、Java 17 版では record と List を活用した整理された表示、 Java 21 版では sealed interface と pattern matching switch で レスポンスコードを型安全に処理します。
Sample.java
import java.io.*;
import java.net.Socket;
import java.util.Base64;
public class SmtpSocketSample {
// SMTP プロトコルの基本コマンド送受信クラス
static class SmtpClient implements Closeable {
private final Socket socket;
private final BufferedReader reader;
private final PrintWriter writer;
SmtpClient(String host, int port) throws IOException {
this.socket = new Socket(host, port);
this.reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
this.writer = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream()), true);
}
// コマンドを送信してレスポンスを受信(null の場合は受信のみ)
String sendCommand(String command) throws IOException {
if (command != null) {
writer.println(command);
System.out.println("C: " + command);
}
String response = reader.readLine();
System.out.println("S: " + response);
return response;
}
// レスポンスコードが期待値か確認し、違えば例外を投げる
void expect(String command, int expectedCode) throws IOException {
String response = sendCommand(command);
if (!response.startsWith(String.valueOf(expectedCode))) {
throw new IOException("予期しないレスポンス(期待: " + expectedCode + "): " + response);
}
}
@Override
public void close() throws IOException {
socket.close();
}
}
// SMTP プロトコルの流れをコンソールに表示(仕組みの理解用)
static void showSmtpFlow() {
System.out.println("=== SMTP プロトコルの流れ(仕組みの説明) ===");
System.out.println();
System.out.println("1. TCP 接続");
System.out.println(" Socket("smtp.example.com", 587) で接続");
System.out.println(" S: 220 smtp.example.com ESMTP ready");
System.out.println();
System.out.println("2. EHLO(拡張 SMTP のあいさつ)");
System.out.println(" C: EHLO client.example.com");
System.out.println(" S: 250-smtp.example.com");
System.out.println(" S: 250-SIZE 35882577");
System.out.println(" S: 250 AUTH LOGIN PLAIN");
System.out.println();
System.out.println("3. 認証(AUTH LOGIN)");
System.out.println(" C: AUTH LOGIN");
System.out.println(" S: 334 Username:");
System.out.println(" C: " + Base64.getEncoder().encodeToString("user@example.com".getBytes()));
System.out.println(" S: 334 Password:");
System.out.println(" C: " + Base64.getEncoder().encodeToString("password".getBytes()));
System.out.println(" S: 235 Authentication successful");
System.out.println();
System.out.println("4. 送信者の指定");
System.out.println(" C: MAIL FROM:<from@example.com>");
System.out.println(" S: 250 OK");
System.out.println();
System.out.println("5. 受信者の指定");
System.out.println(" C: RCPT TO:<to@example.com>");
System.out.println(" S: 250 OK");
System.out.println();
System.out.println("6. メール本文の送信");
System.out.println(" C: DATA");
System.out.println(" S: 354 Start mail input; end with <CRLF>.<CRLF>");
System.out.println(" C: From: from@example.com");
System.out.println(" C: To: to@example.com");
System.out.println(" C: Subject: テストメール");
System.out.println(" C: ");
System.out.println(" C: メール本文");
System.out.println(" C: . ← 単独のドット(.)で本文終了を通知");
System.out.println(" S: 250 Message accepted for delivery");
System.out.println();
System.out.println("7. 切断");
System.out.println(" C: QUIT");
System.out.println(" S: 221 Bye");
}
// Base64 エンコード(AUTH LOGIN で認証情報を送る際に使用)
static String encodeBase64(String text) {
return Base64.getEncoder().encodeToString(text.getBytes());
}
public static void main(String[] args) {
showSmtpFlow();
System.out.println();
System.out.println("=== Base64 エンコード例(AUTH LOGIN 用) ===");
System.out.println("ユーザー名: " + encodeBase64("user@example.com"));
System.out.println("パスワード: " + encodeBase64("mypassword"));
System.out.println();
System.out.println("注意: 実務では N-05(Jakarta Mail)を使用してください");
System.out.println("このサンプルは SMTP プロトコルの仕組みを学ぶためのものです");
}
}
よくあるミス・注意点
⚠️ TLS(STARTTLS / SSL)の扱いが複雑
現在のメールサーバーは平文(暗号化なし)の通信を受け付けないケースがほとんどです。 STARTTLS では平文で接続した後に TLS にアップグレードし、 SSL/TLS では最初から暗号化接続を確立します。 ソケットで手作りする場合は SSLSocket やSSLSocketFactory の処理も必要になり、 非常に複雑になります。Jakarta Mail はこれらを自動で処理してくれます。
📌 AUTH LOGIN では認証情報を Base64 でエンコードして送る
SMTP の AUTH LOGIN 認証では、ユーザー名とパスワードをBase64 エンコードしてサーバーに送ります。 Base64 は暗号化ではなく単なるエンコードなので、TLS 暗号化なしに送ると 第三者に盗聴される危険があります。必ず TLS 接続上で認証してください。
// Base64 エンコード(Java 8 以降、java.util.Base64 が標準搭載)
String encoded = Base64.getEncoder().encodeToString("user@example.com".getBytes());
// → "dXNlckBleGFtcGxlLmNvbQ=="
// デコード(確認用)
String decoded = new String(Base64.getDecoder().decode(encoded));
// → "user@example.com"📌 メール本文の終端は単独の「.」(ドット)
SMTP では DATA コマンド後のメール本文の終わりを、 行の先頭に単独のドット(.)だけの行で通知します。 本文中に「.」で始まる行がある場合は先頭に「.」を追加して.. にするエスケープ処理が必要です。 Jakarta Mail はこの処理を自動で行います。
📌 行末は CRLF(\r\n)が仕様
SMTP の仕様では行末を CRLF(\r\n)にする必要があります。 Unix の改行コード(LF のみ)を使うと 一部のサーバーで正しく解釈されない場合があります。PrintWriter.println() は OS の改行コードを使うため、 厳密には writer.print("EHLO hostname\r\n") のように明示的に CRLF を使う方が安全です。
テストする観点
- Base64 エンコード結果が正しいか(Java 標準の
java.util.Base64 でエンコード・デコードの往復確認) - SMTP レスポンスコードの解析が正しいか(220・250・334・235・354・221・550 などの境界値テスト)
- コマンド文字列が正しく生成されるか(
MAIL FROM:<address> の形式確認) - ローカルの SMTP テストサーバー(Mailtrap など)への実接続でコマンドシーケンスが成功するか
- サーバーが存在しない場合に
ConnectException が発生するか - 期待と異なるレスポンスコードを受け取った場合に適切な例外がスローされるか
GitHub でソースコードを見る →