Spring Boot + Spring Integration でいろいろ試してみる ( その2 )( POP3 でメールを受信するバッチを作成する )
概要
記事一覧はこちらです。
- Java で POP3 サーバからメールを受信するバッチを、Spring Integration の Mail-Receiving Channel Adapter ( http://docs.spring.io/spring-integration/reference/html/mail.html#mail-inbound ) の Pop3MailReceiver クラスを利用して作成してみます。
- Spring Boot の ApplicationRunner インターフェースを利用したバッチとして作成します。
- プロジェクトは Spring Boot + Spring Integration でいろいろ試してみる ( その1 )( SFTP でファイルアップロードするバッチを作成する ) で作成した ksbysample-batch-integration プロジェクトを使います。
- SMTP サーバ及び POP3 サーバは Windows 上にソフトをインストールせず、GreenMail ( http://www.icegreen.com/greenmail/ ) を使います。ユニットテストで動作確認する想定で、実行可能 jar を作成しての動作確認は行いません。
- 今回も Endpoint や Channel は作成しません。
参照したサイト・書籍
Spring Integration Reference Manual - 21. Mail Support
http://docs.spring.io/spring-integration/reference/html/mail.html#mail-inboundGreenMail
http://www.icegreen.com/greenmail/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 のメールからメール本文を取得する方法を参照しました。
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 インターフェースを利用します。
how to use spring send email with attachment use InputStream?
http://stackoverflow.com/questions/5677490/how-to-use-spring-send-email-with-attachment-use-inputstreamMimeMessageHelper::addInline(String contentId, InputStreamSource inputStreamSource, String contentType)
で第2引数の InputStreamSource インターフェースのインスタンスを生成する方法を参照しました。
Pop3MailReciever is not deleting messages
http://stackoverflow.com/questions/32327646/pop3mailreciever-is-not-deleting-messages- Pop3MailReciever クラスで POP3 でメール受信後に削除する方法を調査していた時に参照しました。
How do I manually autowire a bean with Spring?
http://stackoverflow.com/questions/11965600/how-do-i-manually-autowire-a-bean-with-spring- クラスを自分で Bean にする方法を参照しました。
Add Bean Programatically to Spring Web App Context
http://stackoverflow.com/questions/4540713/add-bean-programatically-to-spring-web-app-context- 自分で生成した Bean を ApplicationContext に登録する方法を参照しました。
Spring Framework Reference Documentation - 10. Spring Expression Language (SpEL)
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html- SpEL で正規表現を使う方法を調べた時に参照しました。
目次
手順
作成するバッチの仕様
以下の仕様のバッチを作成します。
- Spring Boot の ApplicationRunner インターフェースを利用したバッチとして実装します。
- POP3 の処理は Spring Integration の Mail-Receiving Channel Adapter に書かれている Pop3MailReceiver クラスを利用します。
- メールは multipart でないメールも multipart のメールも処理できるようにします。
- 以下の処理で実装します。
- POP3 サーバからメール一覧を受信します。
- メールを1件ずつ処理し、Subject とメール本文を標準出力に出力します。
- 受信したメールを削除します。
RecvMailUsingPOP3BatchRunner クラスを作成する
メール処理に必要なライブラリをダウンロードするために、build.gradle を リンク先の内容 に変更します。
Gradle projects View の左上にある「Refresh all Gradle projects」ボタンをクリックして build.gradle を反映します。
src/main/java/ksbysample/batch/integration の下に recvmailusingpop3batch パッケージを作成します。
src/main/resources の下に ksbysample/batch/integration/recvmailusingpop3batch ディレクトリを作成します。
src/main/resources/ksbysample/batch/integration/recvmailusingpop3batch の下に recvmailusingpop3batch.properties を作成し、リンク先の内容 に変更します。
src/main/java/ksbysample/batch/integration/recvmailusingpop3batch の下に RecvMailUsingPOP3BatchRunner.java を作成し、リンク先の内容 に変更します。
テストクラスを作成して動作確認する
GreenMail で SMTP サーバ、POP3 サーバを起動・終了するためのクラスを作成します。src/test/java/ksbysample の下に common.test.rule.mail パッケージを作成します。
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 の下にコピーし、リンク先の内容 に変更します。
@Component アノテーションを付加した MailServerResource クラスが Bean として自動生成されるよう ComponentScan のルートパッケージを変更します。src/main/java/ksbysample/batch/integration の下の Application.java を リンク先の内容 に変更します。
テストクラスを作成します。RecvMailUsingPOP3BatchRunner.java のソース上で Ctrl+Shift+T を押下してコンテキストメニューを表示した後、「Create New Test…」メニューをクリックします。
「Create Test」ダイアログが表示されます。画面下半分に表示されているメソッド一覧から run メソッドのチェックボックスをチェックした後、「OK」ボタンをクリックします。
「Choose Destination Directory」ダイアログが表示されます。src\test\java のディレクトリを選択した後、「OK」ボタンをクリックします。
src/test/java/ksbysample/batch/integration/recvmailusingpop3batch/ の下に RecvMailUsingPOP3BatchRunnerTest.java が作成されますので、リンク先の内容 に変更します。
RecvMailUsingPOP3BatchRunnerTest.java の中で
@Autowired private JavaMailSender mailSender;
を記述したので、src/main/resources の下に application.properties を作成し、リンク先の内容 に変更します。テストを実行します。run メソッドの左側に表示されているアイコンをクリックしてメニューを表示した後、「Run ‘run()'」メニューをクリックします。
テストは正常に終了し、1回目の run メソッド実行ではメールの内容が出力され ( この時受信したメールが削除されます )、2回目の run メソッド実行ではメールがないので何も出力されませんでした。
特定の 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 == '件名'");
で「件名が “件名” に一致するもの」という条件になります。
テストを実行して動作を確認します。以下のように「件名が “件名” に一致するもの」だけが表示されました。
次に 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 で終わるもの」だけが表示されました。
ソースコード
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 プロジェクトで作成していたものと比較して、以下の点を追加・変更します。
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
初版発行。