java-recipes

ホーム ネットワーク › 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 では最初から暗号化接続を確立します。 ソケットで手作りする場合は SSLSocketSSLSocketFactory の処理も必要になり、 非常に複雑になります。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 でソースコードを見る →