かんがるーさんの日記

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

Spring Boot + Spring Integration でいろいろ試してみる ( その2 )( POP3 でメールを受信するバッチを作成する )

概要

記事一覧はこちらです。

参照したサイト・書籍

  1. Spring Integration Reference Manual - 21. Mail Support
    http://docs.spring.io/spring-integration/reference/html/mail.html#mail-inbound

  2. GreenMail
    http://www.icegreen.com/greenmail/

  3. How to read text inside body of mail using javax.mail
    http://stackoverflow.com/questions/11240368/how-to-read-text-inside-body-of-mail-using-javax-mail

    • MultiPart のメールからメール本文を取得する方法を参照しました。
  4. How to really read text file from classpath in Java
    http://stackoverflow.com/questions/1464291/how-to-really-read-text-file-from-classpath-in-java

    • classpath に存在するファイルから File クラスのインスタンスを生成する方法を調査した時に参照しました。
    • Spring Framework が提供する ClassPathResource クラスと Resource インターフェースを利用します。
  5. how to use spring send email with attachment use InputStream?
    http://stackoverflow.com/questions/5677490/how-to-use-spring-send-email-with-attachment-use-inputstream

    • MimeMessageHelper::addInline(String contentId, InputStreamSource inputStreamSource, String contentType) で第2引数の InputStreamSource インターフェースのインスタンスを生成する方法を参照しました。
  6. Pop3MailReciever is not deleting messages
    http://stackoverflow.com/questions/32327646/pop3mailreciever-is-not-deleting-messages

    • Pop3MailReciever クラスで POP3 でメール受信後に削除する方法を調査していた時に参照しました。
  7. How do I manually autowire a bean with Spring?
    http://stackoverflow.com/questions/11965600/how-do-i-manually-autowire-a-bean-with-spring

    • クラスを自分で Bean にする方法を参照しました。
  8. Add Bean Programatically to Spring Web App Context
    http://stackoverflow.com/questions/4540713/add-bean-programatically-to-spring-web-app-context

    • 自分で生成した Bean を ApplicationContext に登録する方法を参照しました。
  9. Spring Framework Reference Documentation - 10. Spring Expression Language (SpEL)
    http://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html

    • SpEL で正規表現を使う方法を調べた時に参照しました。

目次

  1. 作成するバッチの仕様
  2. RecvMailUsingPop3BatchRunner クラスを作成する
  3. テストクラスを作成して動作確認する
  4. 特定の From や Subject のメールのみ受信するには?

手順

作成するバッチの仕様

以下の仕様のバッチを作成します。

  • Spring Boot の ApplicationRunner インターフェースを利用したバッチとして実装します。
  • POP3 の処理は Spring Integration の Mail-Receiving Channel Adapter に書かれている Pop3MailReceiver クラスを利用します。
  • メールは multipart でないメールも multipart のメールも処理できるようにします。
  • 以下の処理で実装します。
    1. POP3 サーバからメール一覧を受信します。
    2. メールを1件ずつ処理し、Subject とメール本文を標準出力に出力します。
    3. 受信したメールを削除します。

RecvMailUsingPOP3BatchRunner クラスを作成する

  1. メール処理に必要なライブラリをダウンロードするために、build.gradle を リンク先の内容 に変更します。

  2. Gradle projects View の左上にある「Refresh all Gradle projects」ボタンをクリックして build.gradle を反映します。

  3. src/main/java/ksbysample/batch/integration の下に recvmailusingpop3batch パッケージを作成します。

  4. src/main/resources の下に ksbysample/batch/integration/recvmailusingpop3batch ディレクトリを作成します。

  5. src/main/resources/ksbysample/batch/integration/recvmailusingpop3batch の下に recvmailusingpop3batch.properties を作成し、リンク先の内容 に変更します。

  6. src/main/java/ksbysample/batch/integration/recvmailusingpop3batch の下に RecvMailUsingPOP3BatchRunner.java を作成し、リンク先の内容 に変更します。

テストクラスを作成して動作確認する

  1. GreenMail で SMTP サーバ、POP3 サーバを起動・終了するためのクラスを作成します。src/test/java/ksbysample の下に common.test.rule.mail パッケージを作成します。

  2. ksbysample-webapp-lending プロジェクトで作成していた MailServerResource クラス ( https://github.com/ksby/ksbysample-webapp-lending/blob/1.0.x/src/test/java/ksbysample/common/test/rule/mail/MailServerResource.java ) を src/test/java/ksbysample/common/test/rule/mail の下にコピーし、リンク先の内容 に変更します。

  3. @Component アノテーションを付加した MailServerResource クラスが Bean として自動生成されるよう ComponentScan のルートパッケージを変更します。src/main/java/ksbysample/batch/integration の下の Application.javaリンク先の内容 に変更します。

  4. テストクラスを作成します。RecvMailUsingPOP3BatchRunner.java のソース上で Ctrl+Shift+T を押下してコンテキストメニューを表示した後、「Create New Test…」メニューをクリックします。

    f:id:ksby:20160814170237p:plain

  5. 「Create Test」ダイアログが表示されます。画面下半分に表示されているメソッド一覧から run メソッドのチェックボックスをチェックした後、「OK」ボタンをクリックします。

    f:id:ksby:20160813222320p:plain

  6. 「Choose Destination Directory」ダイアログが表示されます。src\test\javaディレクトリを選択した後、「OK」ボタンをクリックします。

    f:id:ksby:20160813222430p:plain

  7. src/test/java/ksbysample/batch/integration/recvmailusingpop3batch/ の下に RecvMailUsingPOP3BatchRunnerTest.java が作成されますので、リンク先の内容 に変更します。

  8. RecvMailUsingPOP3BatchRunnerTest.java の中で @Autowired private JavaMailSender mailSender; を記述したので、src/main/resources の下に application.properties を作成し、リンク先の内容 に変更します。

  9. テストを実行します。run メソッドの左側に表示されているアイコンをクリックしてメニューを表示した後、「Run ‘run()'」メニューをクリックします。

    f:id:ksby:20160814170621p:plain

  10. テストは正常に終了し、1回目の run メソッド実行ではメールの内容が出力され ( この時受信したメールが削除されます )、2回目の run メソッド実行ではメールがないので何も出力されませんでした。

    f:id:ksby:20160814170935p:plain

特定の From や Subject のメールのみ受信するには?

※ここから先は commit しません。

Pop3MailReceiver.setSelectorExpression メソッドを呼び出して、条件を SpEL で指定します。

例えば件名が “件名” に一致するものだけを取得してみます。src/main/java/ksbysample/batch/integration/recvmailusingpop3batch の下の RecvMailUsingPOP3BatchRunner.java を以下のように変更します。

    @Autowired
    private Pop3MailReceiver receiver;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        ExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression("subject == '件名'");
        receiver.setSelectorExpression(expression);

        Message[] recvMessages = receiver.receive();
        ..........
  • private MailReceiver receiver;private Pop3MailReceiver receiver; へ変更します。MailReceiver では setSelectorExpression メソッドが呼び出せないためです。
  • run メソッドの直後の3行を追加します。parser.parseExpression("subject == '件名'"); で「件名が “件名” に一致するもの」という条件になります。

テストを実行して動作を確認します。以下のように「件名が “件名” に一致するもの」だけが表示されました。

f:id:ksby:20160814182030p:plain

次に From が “@multipart.co.jp” で終わるものだけを取得してみます。src/main/java/ksbysample/batch/integration/recvmailusingpop3batch の下の RecvMailUsingPOP3BatchRunner.java を以下のように変更します。

    @Autowired
    private Pop3MailReceiver receiver;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        ExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression("from matches '.*@multipart.co.jp$'");
        receiver.setSelectorExpression(expression);

        Message[] recvMessages = receiver.receive();
        ..........
  • parser.parseExpression("subject == '件名'");parser.parseExpression("from matches '.*@multipart.co.jp$'"); に変更します。

テストを実行して動作を確認します。以下のように「From が @multipart.co.jp で終わるもの」だけが表示されました。

f:id:ksby:20160814182632p:plain

ソースコード

build.gradle

group 'ksbysample'
version '1.1.0-RELEASE'

..........

dependencies {
    // dependency-management-plugin によりバージョン番号が自動で設定されるもの
    // Appendix A. Dependency versions ( http://docs.spring.io/platform/docs/current/reference/htmlsingle/#appendix-dependency-versions ) 参照
    compile("org.springframework.boot:spring-boot-starter-mail")
    compile('org.springframework.boot:spring-boot-starter-integration')
    compile('org.springframework.integration:spring-integration-mail')
    compile('org.springframework.integration:spring-integration-sftp')
    testCompile("org.springframework.boot:spring-boot-starter-test")
    testCompile("org.spockframework:spock-core") { exclude module: "groovy-all" }
    testCompile("org.spockframework:spock-spring") { exclude module: "groovy-all" }

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    compile("org.projectlombok:lombok:1.16.10")
    compile("org.apache.commons:commons-lang3:3.4")
    testCompile('com.icegreen:greenmail:1.5.1')
}
  • version を 1.0.0-RELEASE → 1.1.0-RELEASE に変更します。
  • dependencies に以下の3行を追加します。
    • compile("org.springframework.boot:spring-boot-starter-mail")
    • compile('org.springframework.integration:spring-integration-mail')
    • testCompile('com.icegreen:greenmail:1.5.1')

recvmailusingpop3batch.properties

pop3.url=pop3://tanaka:12345678@localhost:110/INBOX

RecvMailUsingPOP3BatchRunner.java

package ksbysample.batch.integration.recvmailusingpop3batch;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.integration.mail.MailReceiver;
import org.springframework.integration.mail.Pop3MailReceiver;

import javax.mail.BodyPart;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMultipart;
import java.io.IOException;

public class RecvMailUsingPOP3BatchRunner implements ApplicationRunner {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    public static final String BATCH_NAME = "RecvMailUsingPop3Batch";

    @Autowired
    private MailReceiver receiver;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        Message[] recvMessages = receiver.receive();
        for (Message message : recvMessages) {
            String body = null;
            // 受信しているメールの内容は org.apache.commons.io.IOUtils の toString メソッドを使用して確認できる
            // System.out.println("★" + IOUtils.toString(message.getInputStream(), "UTF-8"));
            if (message.getContent() instanceof MimeMultipart) {
                body = processMimeMultipart((MimeMultipart) message.getContent());
            } else {
                body = (String) message.getContent();
            }
            System.out.println(message.getSubject() + ", " + body);
        }
    }

    private String processMimeMultipart(MimeMultipart mimeMultipart) throws MessagingException, IOException {
        StringBuilder sb = new StringBuilder();
        String body = null;
        for (int i = 0; i < mimeMultipart.getCount(); i++) {
            BodyPart bodyPart = mimeMultipart.getBodyPart(i);
            if (bodyPart.getContent() instanceof MimeMultipart) {
                body = processMimeMultipart((MimeMultipart) bodyPart.getContent());
                sb.append(body);
            } else if (bodyPart.isMimeType("text/plain")) {
                body = (String) bodyPart.getContent();
                sb.append(body);
            }
        }

        return sb.toString();
    }

    @Configuration
    @PropertySource("classpath:ksbysample/batch/integration/recvmailusingpop3batch/recvmailusingpop3batch.properties")
    public static class RecvMailUsingPOP3BatchConfig {

        @Value("${pop3.url}")
        private String POP3_URL;

        @Bean
        @ConditionalOnProperty(value = {"batch.execute"}, havingValue = RecvMailUsingPOP3BatchRunner.BATCH_NAME)
        public MailReceiver pop3MailReceiver() {
            return createPop3MailReceiverInstance();
        }

        @Bean
        @ConditionalOnProperty(value = {"batch.execute"}, havingValue = RecvMailUsingPOP3BatchRunner.BATCH_NAME)
        public ApplicationRunner recvMailUsingPOP3BatchRunner() {
            return new RecvMailUsingPOP3BatchRunner();
        }

        public Pop3MailReceiver createPop3MailReceiverInstance() {
            Pop3MailReceiver pop3MailReceiver = new Pop3MailReceiver(POP3_URL);
            // 受信したメールは削除する
            pop3MailReceiver.setShouldDeleteMessages(true);
            return pop3MailReceiver;
        }

    }

}
  • 以下に実装時のポイントを書きます。
    • Pop3MailReceiver クラスは必ず Bean にして利用します。Bean にせずに単に new でインスタンスを生成してもメールの一覧を取得できますが、メールの削除が行われなくなります。
    • 接続先の POP3 サーバの設定は pop3://tanaka:12345678@localhost:110/INBOX の形式の文字列でセットします。今回は外部の properties ファイルで設定しています。
    • メールの受信は Message[] recvMessages = receiver.receive(); で行います。
    • あとは受け取った javax.mail.Message オブジェクトを処理するだけです。メールが Multipart の場合が以外に面倒なので、上のソースを見てください。

MailServerResource.java

package ksbysample.common.test.rule.mail;

import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.ServerSetup;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.springframework.stereotype.Component;

import javax.mail.internet.MimeMessage;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Component
public class MailServerResource extends TestWatcher {

    private final GreenMail greenMail;

    public MailServerResource() {
        List<ServerSetup> serverSetupList = new ArrayList<>();
        serverSetupList.add(new ServerSetup(25, "localhost", ServerSetup.PROTOCOL_SMTP));
        serverSetupList.add(new ServerSetup(110, "localhost", ServerSetup.PROTOCOL_POP3));
        this.greenMail = new GreenMail((ServerSetup[]) serverSetupList.toArray(new ServerSetup[serverSetupList.size()]));
    }

    @Override
    protected void starting(Description description) {
        greenMail.start();
    }

    @Override
    protected void finished(Description description) {
        greenMail.stop();
    }

    public GreenMail getGreenMail() {
        return this.greenMail;
    }

    public int getMessagesCount() {
        return greenMail.getReceivedMessages().length;
    }

    public List<MimeMessage> getMessages() {
        return Arrays.asList(greenMail.getReceivedMessages());
    }

    public MimeMessage getFirstMessage() {
        MimeMessage message = null;
        MimeMessage[] receivedMessages = greenMail.getReceivedMessages();
        if (receivedMessages.length > 0) {
            message = receivedMessages[0];
        }
        return message;
    }

}
  • ksbysample-webapp-lending プロジェクトで作成していたものと比較して、以下の点を追加・変更します。
    • 継承元のクラスを ExternalResource → TestWatcher へ変更します。
    • Override するメソッドを before/after → starting/finished へ変更します。
    • フィールド private GreenMail greenMail に final を追加します。またフィールドでインスタンスを生成するのではなく、コンストラクタを追加してその中でインスタンスを生成するように変更し、インスタンス生成時に POP3 サーバの設定も追加します。
    • public GreenMail getGreenMail() {...} を追加します。

Application.java

package ksbysample.batch.integration;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan("ksbysample")
public class Application {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Application.class, args);
        Runtime.getRuntime().exit(SpringApplication.exit(context));
    }

}
  • @ComponentScan("ksbysample") を追加します。

RecvMailUsingPOP3BatchRunnerTest.java

package ksbysample.batch.integration.recvmailusingpop3batch;

import com.icegreen.greenmail.user.GreenMailUser;
import com.icegreen.greenmail.util.GreenMail;
import ksbysample.batch.integration.Application;
import ksbysample.common.test.rule.mail.MailServerResource;
import org.apache.commons.io.IOUtils;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.integration.mail.Pop3MailReceiver;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.mail.internet.MimeMessage;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class RecvMailUsingPOP3BatchRunnerTest {

    private final String POP3_USER_MAILADDR = "test@sample.co.jp";

    @Rule
    @Autowired
    public MailServerResource smtpPop3Server;

    @Autowired
    private JavaMailSender mailSender;

    @Autowired
    private RecvMailUsingPOP3BatchRunner.RecvMailUsingPOP3BatchConfig recvMailUsingPOP3BatchConfig;

    @Autowired
    private ApplicationContext applicationContext;

    @Test
    public void run() throws Exception {
        GreenMail greenMail = smtpPop3Server.getGreenMail();
        GreenMailUser user = greenMail.setUser(POP3_USER_MAILADDR, "tanaka", "12345678");

        // メールを送信する ( 1通目 )( 非マルチパート )
        MimeMessage mimeMessage = this.mailSender.createMimeMessage();
        MimeMessageHelper message = new MimeMessageHelper(mimeMessage, false, "UTF-8");
        message.setFrom("from@test.com");
        message.setTo(POP3_USER_MAILADDR);
        message.setSubject("件名");
        message.setText("本文", false);
        user.deliver(message.getMimeMessage());

        // メールを送信する ( 2通目 )( マルチパート、本文のみ )
        mimeMessage = this.mailSender.createMimeMessage();
        message = new MimeMessageHelper(mimeMessage, true, "UTF-8");
        message.setFrom("from2@multipart.co.jp");
        message.setTo(POP3_USER_MAILADDR);
        message.setSubject("件名(Multipart、添付ファイルなし)");
        message.setText("本文(Multipart、添付ファイルなし)", false);
        user.deliver(message.getMimeMessage());

        // メールを送信する ( 3通目 )( マルチパート、本文+添付ファイル )
        mimeMessage = this.mailSender.createMimeMessage();
        message = new MimeMessageHelper(mimeMessage, true, "UTF-8");
        message.setFrom("from3@multipart.co.jp");
        message.setTo(POP3_USER_MAILADDR);
        message.setSubject("件名(Multipart、本文+添付ファイル)");
        message.setText("本文(Multipart、本文+添付ファイル)", false);
        Resource resource = new ClassPathResource("logback-spring.xml");
        message.addAttachment("設定ファイル", resource.getFile());
        user.deliver(message.getMimeMessage());

        // メールを送信する ( 4通目 )( マルチパート、本文+本文INLINE )
        mimeMessage = this.mailSender.createMimeMessage();
        message = new MimeMessageHelper(mimeMessage, true, "UTF-8");
        message.setFrom("from4@multipart.co.jp");
        message.setTo(POP3_USER_MAILADDR);
        message.setSubject("件名(Multipart、本文+本文INLINE)");
        message.setText("本文(Multipart、本文+本文INLINE)", false);
        resource = new ClassPathResource("application.properties");
        message.addInline("設定ファイル", new ByteArrayResource(IOUtils.toByteArray(resource.getInputStream())), "text/plain");
        user.deliver(message.getMimeMessage());

        // Pop3MailReceiver クラスの Bean を作成して context に登録する
        // Bean を生成する
        Pop3MailReceiver pop3MailReceiverBean = recvMailUsingPOP3BatchConfig.createPop3MailReceiverInstance();
        AutowireCapableBeanFactory factory = applicationContext.getAutowireCapableBeanFactory();
        factory.autowireBean(pop3MailReceiverBean);
        factory.initializeBean(pop3MailReceiverBean, pop3MailReceiverBean.getClass().getSimpleName());
        // context に登録する
        ConfigurableListableBeanFactory beanFactory = ((ConfigurableApplicationContext) applicationContext).getBeanFactory();
        beanFactory.registerSingleton(pop3MailReceiverBean.getClass().getCanonicalName(), pop3MailReceiverBean);

        // RecvMailUsingPOP3BatchRunner クラスの Bean を作成して run メソッドを実行する
        RecvMailUsingPOP3BatchRunner recvMailUsingPOP3BatchRunnerBean = new RecvMailUsingPOP3BatchRunner();
        factory.autowireBean(recvMailUsingPOP3BatchRunnerBean);
        factory.initializeBean(recvMailUsingPOP3BatchRunnerBean, recvMailUsingPOP3BatchRunnerBean.getClass().getSimpleName());
        recvMailUsingPOP3BatchRunnerBean.run(null);

        // 再度 run メソッドを実行すると、メールが POP3 サーバに残っていないことを確認する
        recvMailUsingPOP3BatchRunnerBean.run(null);

        // 今回はバッチを実行しているだけで、assert 入れてテストしている訳ではありません
    }

}

application.properties

spring.mail.host=localhost
spring.mail.port=25

履歴

2016/08/14
初版発行。