かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot + Spring Integration でいろいろ試してみる ( その25 )( Docker Compose でサーバを構築する、SMTP+POP3サーバ編 )

概要

記事一覧はこちらです。

Spring Integration のアプリケーションで使用するサーバを Docker Compose で構築します。

  • SMTPサーバ+POP3サーバを構築します。
  • Dockerイメージは tvial/docker-mailserver を使用します。選定理由は以下の通りです。
    • STARS と PULLS の数が多い。
    • SMTP, POP3, IMAP が使用可能で、各SSL版もサポートされている模様。
    • 中身が Postfix+Dovect でオーソドックスな構成である。
  • SMTPサーバは認証なし、SSLなし、ポート番号は25番を使用。
  • POP3サーバは認証あり、SSLなし、ポート番号は110番を使用。
  • 認証に使用するパスワードは、サーバ側で PLAIN TEXT で保存します。

参照したサイト・書籍

目次

  1. ksbysample-eipapp-dockerserver プロジェクトを作成する
  2. SMTP+POP3 サーバを構築する
    1. docker-mailserver をカスタマイズする設定ファイルを配置するディレクトリを作成する
    2. docker-compose.yml を作成する
    3. Postfix をカスタマイズするための postfix-main.cf を作成する
    4. Dovecot をカスタマイズするための dovecot.cf を作成する
    5. ユーザを登録するための postfix-accounts.cf を作成する
    6. サーバを起動する
    7. 動作確認
  3. SMTP でメールを送信するサンプルを作成する
  4. POP3 でメールを受信するサンプルを作成する

手順

ksbysample-eipapp-dockerserver プロジェクトを作成する

Spring Initializr で作成します。

f:id:ksby:20180826201831p:plain f:id:ksby:20180826202733p:plain f:id:ksby:20180826202823p:plain f:id:ksby:20180826202923p:plain f:id:ksby:20180826203301p:plain

生成された build.gradle は以下のものですが、

buildscript {
    ext {
        springBootVersion = '2.0.4.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'ksbysample'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}


dependencies {
    compile('org.springframework.boot:spring-boot-starter-integration')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

dependencies block を以下のように変更します。

dependencies {
    implementation('org.springframework.boot:spring-boot-starter-integration')
    implementation("org.springframework.boot:spring-boot-starter-mail")
    implementation('org.springframework.integration:spring-integration-mail')
    implementation("org.apache.commons:commons-lang3")
    testImplementation('org.springframework.boot:spring-boot-starter-test')
}
  • compileimplementationtestCompiletestImplementation に変更します。
  • Spring Integration の Mail Support の機能を使用するので、以下の行を追加します。
    • implementation('org.springframework.integration:spring-integration-mail')
  • メールを送信するのに JavaMailSender Bean を使用したいので、以下の行を追加します。
    • implementation("org.springframework.boot:spring-boot-starter-mail")
  • StringUtils を使用したいので、以下の行を追加します。
    • implementation("org.apache.commons:commons-lang3")

build.gradle を変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

また @SpringBootApplication アノテーションが記述されているファイル名が長いので、KsbysampleEipappDockerserverApplication.java → Application.java に変更します。

SMTPPOP3 サーバを構築する

docker-mailserver をカスタマイズする設定ファイルを配置するディレクトリを作成する

プロジェクトの直下に docker/mail-server/config ディレクトリを作成します。

f:id:ksby:20180826210331p:plain

docker-compose.yml を作成する

プロジェクトのルートディレクトリ直下に docker-compose.yml を新規作成し、以下の内容を記述します。

version: '3'

services:
  # docker-mailserver
  # https://hub.docker.com/r/tvial/docker-mailserver/
  # https://github.com/tomav/docker-mailserver
  #
  # 起動した docker-mailserver のコンテナ(mail-server) にアクセスする場合には以下のコマンドを実行する
  # docker exec -it mail-server /bin/sh
  #
  # アカウントのパスワードを SHA512 で作成する場合には、コンテナにアクセスして以下のようにコマンドを実行して生成する
  # doveadm pw -s SHA512-CRYPT -u [メールアドレス(例:tanaka@mail.example.com)] -p [パスワード]
  #
  mail-server:
    image: tvial/docker-mailserver:latest
    container_name: mail-server
    hostname: mail
    domainname: example.com
    ports:
      - "25:25"
      - "110:110"
    volumes:
      - ./docker/mail-server/config/:/tmp/docker-mailserver/
    environment:
      # debug したい場合には以下の行のコメントアウトを解除する
      # - DMS_DEBUG=1
      - ENABLE_SPAMASSASSIN=0
      - ENABLE_CLAMAV=0
      - ENABLE_FETCHMAIL=0
      - ENABLE_FAIL2BAN=0
      - ENABLE_POSTGREY=0
      - ENABLE_POP3=1
    cap_add:
      - NET_ADMIN
      - SYS_PTRACE
    restart: always
  • コンテナ名は mail-server
  • メールのドメイン名は mail.example.com
  • https://github.com/tomav/docker-mailserver の docker-compose.yml のサンプルから主に以下の点を変更しています。
    • PORT は 25番(SMTP)、110番POP3)のみ記述しています。Note: Port 25 is only for receiving email from other mailservers and not for submitting email. You need to use port 465 or 587 for this. と記述されていますが、465番、587番は SSL, TLS 用のポートなので、今回は SSL なしのサンプルのため 25番を使用します。
    • サンプルでは volumes に maildata:/var/mail, mailstate:/var/mail-state が記載されていますが、Windows PC 上のディレクトリにマウントしないことにしたので記述していません。マウントする場合には docker volume create コマンドで maildata, mailstate という名前のボリュームを作成して、サンプルのように記述すればよいはず。
    • 開発環境用としてシンプルに SMTP, POP3 を使用したいだけなので、spamassasin, clamav, fetchmail, fail2ban, postgrey は OFF にします。
    • POP3 を使用するので ENABLE_POP3=1 を追加します。

Postfix をカスタマイズするための postfix-main.cf を作成する

docker/mail-server/config の下に postfix-main.cf を新規作成し、以下の内容を記述します。

mydestination =
smtpd_recipient_restrictions =
  • mydestination と virtual_mailbox_domains に同じドメインが設定されていると warning: do not list domain mail.example.com in BOTH mydestination and virtual_mailbox_domains というログが出るので、mydestination を空にします。
  • 既存の smtpd_recipient_restrictions の設定のままではメール送信時に Helo command rejected: need fully-qualified hostname というエラーが出るため、空にします。

Dovecot をカスタマイズするための dovecot.cf を作成する

docker/mail-server/config の下に dovecot.cf を新規作成し、以下の内容を記述します。

disable_plaintext_auth = no
ssl = no
# debug したい場合には以下の行のコメントアウトを解除する
# auth_verbose = yes
# auth_debug = yes
  • SSL をオフにして、かつ PLAIN TEXT のパスワード送信を受け付けるようにするために disable_plaintext_auth = nossl = no の設定を入れます。この2つの設定がないと POP3 の認証時に Plaintext authentication disallowed on non-secure (SSL/TLS) connections. というエラーが発生します。

ユーザを登録するための postfix-accounts.cf を作成する

tanaka@mail.example.com、suzuki@mail.example.com の2ユーザを作成します。docker/mail-server/config の下に postfix-accounts.cf を新規作成し、以下の内容を記述します。

tanaka@mail.example.com|{PLAIN}xxxxxxxx
suzuki@mail.example.com|{PLAIN}yyyyyyyy

ここまでの作業で docker ディレクトリは以下の構成になります。

f:id:ksby:20180828020508p:plain

サーバを起動する

コマンドプロンプトを起動し docker-compose.yml のあるディレクトリへ移動して docker-compose up -d コマンドを実行して起動します。

f:id:ksby:20180827010033p:plain

IntelliJ IDEA の docker plugin を見ると mail-server コンテナが起動していることが確認できます。

f:id:ksby:20180827010307p:plain

動作確認

telnet で接続して SMTP, POP3 コマンドを実行すれば動作確認できるのですが、Windows には標準では telnet コマンドがありません。「Windows の機能の有効化または無効化」で「Telnet クライアント」がインストール可能ですが、インストールしてもなぜかうまく接続できませんでした。Git for Windows の中を見ても telnet コマンドはありませんでした。

Windows 10 では WSL(Windows Subsystem for Linux)という機能で Ubuntu 18.04 がインストールできますので、Ubuntu から telnet で接続して動作確認します(おそらく Windowstelnet を使うのはこれが一番使いやすいです)。WSL や Ubuntu 18.04 のインストール方法はここでは書きません。

まずは telnet localhost 25 で接続してメールを送信します。

f:id:ksby:20180827012105p:plain

コマンドプロンプトdocker exec -it mail-server /bin/sh コマンドを実行して mail-server コンテナに接続し、/var/mail の下の tanaka@mail.example.com にメールが届いていることを確認します。

f:id:ksby:20180827012323p:plain

最後に telnet localhost 110 で接続して POP3 でメールを受信できることを確認します。

f:id:ksby:20180827012707p:plain

SMTP でメールを送信するサンプルを作成する

Spring Framework のスケジューリング機能を利用して一定時間毎にメールを送信したいので、src/main/java/ksbysample/eipapp/dockerserver/Application.java@EnableScheduling アノテーションを付与します。

package ksbysample.eipapp.dockerserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

src/main/resources/application.properties に以下の内容を記述します。

spring.mail.host=localhost
spring.mail.port=25
# メール送信処理を debug したい場合には以下の行のコメントアウトを解除する
# spring.mail.properties.mail.debug=true

src/main/java/ksbysample/eipapp/dockerserver の下に flow パッケージを新規作成し、その下に MailFlowConfig.java を新規作成して、以下の内容を記述します。

package ksbysample.eipapp.dockerserver.flow;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.handler.LoggingHandler;
import org.springframework.integration.mail.MailHeaders;
import org.springframework.integration.mail.MailSendingMessageHandler;
import org.springframework.integration.support.StringObjectMapBuilder;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.scheduling.annotation.Scheduled;

import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

@Configuration
public class MailFlowConfig {

    /****************************************
     * メール送信処理のサンプル                 *
     ****************************************/

    private final JavaMailSender mailSender;

    private final AtomicInteger count;

    public MailFlowConfig(JavaMailSender mailSender) {
        this.mailSender = mailSender;
        this.count = new AtomicInteger(0);
    }

    @Bean
    public MailSendingMessageHandler mailSendingMessageHandler() {
        return new MailSendingMessageHandler(this.mailSender);
    }

    @Autowired
    private MailSendingMessageHandler mailSendingMessageHandler;

    /**
     * 受信したメッセージを元にメールを送信する
     *
     * @return {@IntegrationFlow} オブジェクト
     */
    @Bean
    public IntegrationFlow sendMailFlow() {
        return f -> f
                // メッセージ送信に時間がかかるので(1件あたり約10秒)、スレッドを生成してメール送信は別スレッドに処理させる
                .channel(c -> c.executor(Executors.newCachedThreadPool()))
                .filter(Message.class, m ->
                        m.getHeaders().containsKey(MailHeaders.FROM)
                                && m.getHeaders().containsKey(MailHeaders.TO)
                                && m.getHeaders().containsKey(MailHeaders.SUBJECT))
                .log(LoggingHandler.Level.WARN, m -> String.format("★★★ メール送信: %s", m.getPayload()))
                .wireTap(sf -> sf.handle(mailSendingMessageHandler))
                .log(LoggingHandler.Level.WARN, m -> String.format("◇◇◇ メール送信: %s", m.getPayload()));
    }

    /**
     * sendMailFlow へ5秒おきに MailHeaders.* のヘッダーをセットした Message を送信する
     */
    @Scheduled(initialDelay = 5000, fixedDelay = 5000)
    public void sendMessageTask() {
        Map<String, Object> headers = new StringObjectMapBuilder()
                .put(MailHeaders.FROM, "sample@test.co.jp")
                .put(MailHeaders.TO, "tanaka@mail.example.com,suzuki@mail.example.com")
                .put(MailHeaders.SUBJECT, "これはテストです")
                .get();
        MessageHeaders messageHeaders = new MessageHeaders(headers);
        Message<String> message
                = MessageBuilder.createMessage(String.format("count = %d", this.count.incrementAndGet())
                , messageHeaders);
        sendMailFlow().getInputChannel().send(message);
    }

}

動作確認してみます。最初に docker-compose up -d でメールサーバを起動し、メールが何も届いていないことを確認します。

f:id:ksby:20180828010712p:plain

次に作成したアプリケーションを実行してメールを送信します。

f:id:ksby:20180828010940p:plain

メールサーバを見るとメールが届いていることが確認できます。

f:id:ksby:20180828011054p:plain

POP3 でメールを受信するサンプルを作成する

src/main/java/ksbysample/eipapp/dockerserver/flow/MailFlowConfig.javaPOP3 のメール受信の処理を追加します。

    /****************************************
     * メール受信処理のサンプル                 *
     ****************************************/

    /**
     * Pop3MailReceiver {@link MailFlowConfig#pop3MessageSource()} に記述せず Bean で定義する必要がある
     * Bean にしないと受信したメッセージをサーバから削除してくれない
     *
     * @return {@Pop3MailReceiver} オブジェクト
     */
    @Bean
    public Pop3MailReceiver pop3MailReceiver() {
        Pop3MailReceiver pop3MailReceiver
                = new Pop3MailReceiver("localhost", "tanaka@mail.example.com", "xxxxxxxx");
        pop3MailReceiver.setShouldDeleteMessages(true);
        Properties javaMailProperties = new Properties();
        // debug したい場合には以下のコメントアウトを解除する
        // javaMailProperties.put("mail.debug", "true");
        pop3MailReceiver.setJavaMailProperties(javaMailProperties);
        return pop3MailReceiver;
    }

    @Bean
    public MailReceivingMessageSource pop3MessageSource() {
        return new MailReceivingMessageSource(pop3MailReceiver());
    }

    /**
     * 5秒おきにメールを受信してみる
     *
     * @return
     */
    @Bean
    public IntegrationFlow mailRecvByPop3Flow() {
        return IntegrationFlows.from(pop3MessageSource()
                // 5秒おきに最大100件受信する
                , c -> c.poller(Pollers.fixedDelay(5000).maxMessagesPerPoll(100)))
                .<MimeMessage>log(LoggingHandler.Level.ERROR, m -> {
                    try {
                        return String.format("◎◎◎ メール受信: %s"
                                , StringUtils.chomp((String) m.getPayload().getContent()));
                    } catch (IOException | MessagingException e) {
                        throw new RuntimeException(e);
                    }
                })
                .get();
    }

メールサーバを再起動してから(送信されたメールがクリアされます)、アプリケーションを実行するとメールの送信と受信が行われます。

f:id:ksby:20180828015033p:plain

履歴

2018/08/28
初版発行。