ユーザー登録完了のウェルカムメールやパスワードリセットリンクの送信は、実務でほぼ必ず登場する機能ですよね。Spring Bootには spring-boot-starter-mail という便利なスターターがあり、JavaMailSender を使えば手軽にメール送信を実装できます。

この記事では、依存の追加からGmail SMTPの設定、テキストメール・HTMLメールの送信、@Async を使った非同期化まで、実務で使えるコードを交えて順に解説します。

spring-boot-starter-mailを追加する

まずは依存を追加しましょう。

Maven:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>

Gradle:

implementation 'org.springframework.boot:spring-boot-starter-mail'

これだけで JavaMailSenderImpl のBeanが自動登録されます。あとは接続先を設定するだけです。

Gmail SMTPをapplication.propertiesで設定する

spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username[email protected]
spring.mail.password=${MAIL_PASSWORD}
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true

ポートは 587(STARTTLS)を使うのが一般的です。465番(SSL)を使う場合は starttls.enable の代わりに mail.smtp.ssl.enable=true を指定してください。

パスワードは直書きせず、環境変数 ${MAIL_PASSWORD} で参照するようにしましょう。設定ファイルの管理についてはapplication.propertiesの設定ガイドプロファイルを使った環境別設定の切り替えも参考にしてください。

Gmailアプリパスワードを取得する

Gmailで2段階認証を有効にしている場合(最近のアカウントはほぼ必須です)、通常のパスワードではSMTP認証に失敗します。 アプリパスワード を発行する必要があります。

手順はこちらです。

  1. Googleアカウントの「セキュリティ」ページを開く
  2. 「2段階認証プロセス」を選択
  3. 下にスクロールして「アプリパスワード」を選択
  4. アプリ名を入力して「作成」をクリック
  5. 表示された16桁のパスワードを環境変数 MAIL_PASSWORD にセットする

このパスワードをapplication.propertiesに直接書くのは避けましょう。Gitにコミットしてしまうリスクがあります。

テキストメールを送信する(最小動作例)

SimpleMailMessage を使えば数行で送れます。

@Service
public class EmailService {

    private final JavaMailSender mailSender;

    public EmailService(JavaMailSender mailSender) {
        this.mailSender = mailSender;
    }

    public void sendWelcomeEmail(String to, String username) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom("[email protected]");
        message.setTo(to);
        message.setSubject("ご登録ありがとうございます");
        message.setText("こんにちは、" + username + "さん!\nご登録いただきありがとうございます。");
        mailSender.send(message);
    }
}

コンストラクタインジェクションで JavaMailSender を受け取り、send() を呼ぶだけですね。シンプルです。

HTMLメールを送信する(MimeMessage)

HTMLメールを送る場合は MimeMessageMimeMessageHelper を使います。

public void sendHtmlEmail(String to, String subject, String htmlContent)
        throws MessagingException {
    MimeMessage message = mailSender.createMimeMessage();
    MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
    helper.setFrom("[email protected]");
    helper.setTo(to);
    helper.setSubject(subject);
    helper.setText(htmlContent, true); // trueでHTMLとして扱われる
    mailSender.send(message);
}

helper.setText() の第2引数を true にするのがポイントです。文字コードはUTF-8を明示しておくと文字化けを防げます。

HTMLのスタイルはインラインCSSで書くことを推奨します。メーラーによってはCSSファイルの参照が無視されることがあるためです。ThymeleafでHTMLテンプレートを管理する方法もありますが、詳細はまた別の機会に。

添付ファイルを送る

MimeMessageHelper.addAttachment() で添付できます。

helper.addAttachment("invoice.pdf", new FileSystemResource("/path/to/invoice.pdf"));

バイト配列から添付したい場合は ByteArrayResource を渡せます。添付ファイルが大きい場合はタイムアウトやメールサーバー側の制限に注意してください。

@Asyncと組み合わせて非同期送信にする

SMTPサーバーとのやりとりには時間がかかるため、リクエストスレッドで同期実行するとレスポンスが遅くなります。@Async を使って非同期化しましょう。

まず設定クラスに @EnableAsync を付けます。

@Configuration
@EnableAsync
public class AsyncConfig {
}

送信メソッドに @Async を付けるだけで非同期化されます。

@Async
public void sendWelcomeEmailAsync(String to, String username) {
    // sendWelcomeEmailと同じ処理
}

注意点として、@Async メソッド内でスローされた例外は呼び出し元には伝播しません。例外をキャッチしてログに記録するか、AsyncUncaughtExceptionHandler を実装して対処しましょう。@Async の詳しい設定はSpring Bootの非同期処理ガイドを参照してください。

テストでJavaMailSenderをモック化する

テスト時に実際にメールを送りたくない場合は @MockBean でモック化します。

@SpringBootTest
class EmailServiceTest {

    @MockBean
    private JavaMailSender mailSender;

    @Autowired
    private EmailService emailService;

    @Test
    void sendWelcomeEmail_shouldCallSend() {
        emailService.sendWelcomeEmail("[email protected]", "太郎");
        verify(mailSender, times(1)).send(any(SimpleMailMessage.class));
    }
}

verify()send() が呼ばれたことを確認できます。より本格的な結合テストをしたい場合は GreenMail というテスト用SMTPサーバーライブラリも便利です。

認証エラーが出たときは

Gmailまわりのエラーはパターンが決まっているので、ポイントだけ押さえておきましょう。

535 Authentication Failed → アプリパスワードを使っていない可能性があります。2段階認証が有効なアカウントでは通常のパスワードは使えません。

Connection timed out(ポート587) → 社内ネットワークやプロバイダがポート587をブロックしているケースがあります。465番ポートを試すか、ネットワーク管理者に確認を。

SSLHandshakeException → 465番ポートを使う場合は mail.smtp.ssl.enable=true が必要です。587番(STARTTLS)とは設定が異なります。

AuthenticationFailedExceptionspring.mail.username がGmailのメールアドレスになっているか確認してください。

まとめ

spring-boot-starter-mail を追加してapplication.propertiesを設定するだけで、JavaMailSender がすぐ使えるようになります。

  • テキストメールは SimpleMailMessage、HTMLメールは MimeMessageHelper で対応
  • GmailはアプリパスワードをSMTP認証に使う(通常パスワードは不可)
  • パスワードは環境変数で管理して直書きを避ける
  • @Async を付けるだけで非同期化できる

メール送信は一度設定してしまえばそれほど難しくないので、ぜひ実装してみてください。