From ba4f52d59cede8881e4e3757b097858163d5230e Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Fri, 22 Aug 2014 16:35:53 +0300 Subject: [PATCH] DSL: Mail namespace & `MailSendingMessageHandler` DSL:Add SMTP Mail Server Polishing - PR Comments DSL: Mail: add `PropertiesConfigurer` lambda * Move `IntegrationFlowTests` to the `flows` package to avoid cross scanning for components * Make `MailTests` as integration test and move `Mail.outboundAdapter` to the `IntegrationFlow` `@Bean` definition --- spring-integration-java-dsl/build.gradle | 4 + .../integration/dsl/mail/Mail.java | 28 +++ .../mail/MailSendingMessageHandlerSpec.java | 84 +++++++++ .../integration/dsl/mail/package-info.java | 4 + .../dsl/support/PropertiesBuilder.java | 38 ++++ .../dsl/support/PropertiesConfigurer.java | 26 +++ .../{ => flows}/IntegrationFlowTests.java | 6 +- .../integration/dsl/test/mail/MailTests.java | 133 +++++++++++++ .../dsl/test/mail/PoorMansMailServer.java | 177 ++++++++++++++++++ 9 files changed, 499 insertions(+), 1 deletion(-) create mode 100644 spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/Mail.java create mode 100644 spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/MailSendingMessageHandlerSpec.java create mode 100644 spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/package-info.java create mode 100644 spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/support/PropertiesBuilder.java create mode 100644 spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/support/PropertiesConfigurer.java rename spring-integration-java-dsl/src/test/java/org/springframework/integration/dsl/test/{ => flows}/IntegrationFlowTests.java (99%) create mode 100644 spring-integration-java-dsl/src/test/java/org/springframework/integration/dsl/test/mail/MailTests.java create mode 100644 spring-integration-java-dsl/src/test/java/org/springframework/integration/dsl/test/mail/PoorMansMailServer.java diff --git a/spring-integration-java-dsl/build.gradle b/spring-integration-java-dsl/build.gradle index c29b359..5f7fcdb 100644 --- a/spring-integration-java-dsl/build.gradle +++ b/spring-integration-java-dsl/build.gradle @@ -30,6 +30,7 @@ ext { ftpServerVersion = '1.0.6' jacocoVersion = '0.7.1.201405082137' jmsApiVersion = '1.1-rev-1' + mailVersion = '1.4.7' slf4jVersion = '1.7.7' springIntegrationVersion = '4.0.3.RELEASE' springBootVersion = '1.1.5.RELEASE' @@ -78,6 +79,7 @@ dependencies { compile("org.springframework.integration:$it:$springIntegrationVersion", optional) } compile ("javax.jms:jms-api:$jmsApiVersion", provided) + compile ("javax.mail:javax.mail-api:$mailVersion", provided) testCompile "org.springframework.integration:spring-integration-test:$springIntegrationVersion" testCompile "de.flapdoodle.embed:de.flapdoodle.embed.mongo:$embedMongoVersion" @@ -90,6 +92,8 @@ dependencies { testRuntime("org.apache.activemq:activemq-kahadb-store:$activeMqVersion") { exclude group: "org.springframework" } + testRuntime "com.sun.mail:mailapi:$mailVersion" + testRuntime "com.sun.mail:smtp:$mailVersion" jacoco "org.jacoco:org.jacoco.agent:$jacocoVersion:runtime" } diff --git a/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/Mail.java b/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/Mail.java new file mode 100644 index 0000000..b82c70e --- /dev/null +++ b/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/Mail.java @@ -0,0 +1,28 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.dsl.mail; + +/** + * @author Gary Russell + * + */ +public class Mail { + + public static MailSendingMessageHandlerSpec outboundAdapter(String host) { + return new MailSendingMessageHandlerSpec(host); + } + +} diff --git a/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/MailSendingMessageHandlerSpec.java b/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/MailSendingMessageHandlerSpec.java new file mode 100644 index 0000000..2f8dfb9 --- /dev/null +++ b/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/MailSendingMessageHandlerSpec.java @@ -0,0 +1,84 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.dsl.mail; + +import java.util.Properties; + +import javax.activation.FileTypeMap; + +import org.springframework.integration.dsl.core.MessageHandlerSpec; +import org.springframework.integration.dsl.support.PropertiesBuilder; +import org.springframework.integration.dsl.support.PropertiesConfigurer; +import org.springframework.integration.mail.MailSendingMessageHandler; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +/** + * @author Gary Russell + * @author Artem Bilan + */ +public class MailSendingMessageHandlerSpec extends + MessageHandlerSpec { + + private final JavaMailSenderImpl sender = new JavaMailSenderImpl(); + + MailSendingMessageHandlerSpec (String host) { + this.sender.setHost(host); + this.target = new MailSendingMessageHandler(this.sender); + } + + public MailSendingMessageHandlerSpec javaMailProperties(Properties javaMailProperties) { + this.sender.setJavaMailProperties(javaMailProperties); + return this; + } + + public MailSendingMessageHandlerSpec javaMailProperties(PropertiesConfigurer propertiesConfigurer) { + PropertiesBuilder properties = new PropertiesBuilder(); + propertiesConfigurer.configure(properties); + return javaMailProperties(properties.get()); + } + + public MailSendingMessageHandlerSpec protocol(String protocol) { + this.sender.setProtocol(protocol); + return this; + } + + public MailSendingMessageHandlerSpec port(int port) { + this.sender.setPort(port); + return this; + } + + public MailSendingMessageHandlerSpec credentials(String username, String password) { + this.sender.setUsername(username); + this.sender.setPassword(password); + return this; + } + + public MailSendingMessageHandlerSpec defaultEncoding(String defaultEncoding) { + this.sender.setDefaultEncoding(defaultEncoding); + return this; + } + + public MailSendingMessageHandlerSpec defaultFileTypeMap(FileTypeMap defaultFileTypeMap) { + this.sender.setDefaultFileTypeMap(defaultFileTypeMap); + return this; + } + + @Override + protected MailSendingMessageHandler doGet() { + throw new UnsupportedOperationException(); + } + +} diff --git a/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/package-info.java b/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/package-info.java new file mode 100644 index 0000000..dd43519 --- /dev/null +++ b/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides Mail Components support for Spring Integration Java DSL. + */ +package org.springframework.integration.dsl.mail; diff --git a/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/support/PropertiesBuilder.java b/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/support/PropertiesBuilder.java new file mode 100644 index 0000000..07bb020 --- /dev/null +++ b/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/support/PropertiesBuilder.java @@ -0,0 +1,38 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.dsl.support; + +import java.util.Properties; + + +/** + * @author Gary Russell + * + */ +public class PropertiesBuilder { + + private final Properties properties = new Properties(); + + public PropertiesBuilder put(Object key, Object value) { + properties.put(key, value); + return this; + } + + public Properties get() { + return this.properties; + } + +} diff --git a/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/support/PropertiesConfigurer.java b/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/support/PropertiesConfigurer.java new file mode 100644 index 0000000..92562f1 --- /dev/null +++ b/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/support/PropertiesConfigurer.java @@ -0,0 +1,26 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.dsl.support; + +/** + * @author Artem Bilan + */ +public interface PropertiesConfigurer { + + void configure(PropertiesBuilder propertiesBuilder); + +} diff --git a/spring-integration-java-dsl/src/test/java/org/springframework/integration/dsl/test/IntegrationFlowTests.java b/spring-integration-java-dsl/src/test/java/org/springframework/integration/dsl/test/flows/IntegrationFlowTests.java similarity index 99% rename from spring-integration-java-dsl/src/test/java/org/springframework/integration/dsl/test/IntegrationFlowTests.java rename to spring-integration-java-dsl/src/test/java/org/springframework/integration/dsl/test/flows/IntegrationFlowTests.java index 390239d..51c8348 100644 --- a/spring-integration-java-dsl/src/test/java/org/springframework/integration/dsl/test/IntegrationFlowTests.java +++ b/spring-integration-java-dsl/src/test/java/org/springframework/integration/dsl/test/flows/IntegrationFlowTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.integration.dsl.test; +package org.springframework.integration.dsl.test.flows; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.endsWith; @@ -86,6 +86,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.Import; import org.springframework.data.mongodb.MongoDbFactory; import org.springframework.data.mongodb.core.SimpleMongoDbFactory; import org.springframework.integration.IntegrationMessageHeaderAccessor; @@ -114,6 +115,8 @@ import org.springframework.integration.dsl.jms.Jms; import org.springframework.integration.dsl.sftp.Sftp; import org.springframework.integration.dsl.support.Pollers; import org.springframework.integration.dsl.support.Transformers; +import org.springframework.integration.dsl.test.TestFtpServer; +import org.springframework.integration.dsl.test.TestSftpServer; import org.springframework.integration.endpoint.MethodInvokingMessageSource; import org.springframework.integration.event.core.MessagingEvent; import org.springframework.integration.event.outbound.ApplicationEventPublishingMessageHandler; @@ -1275,6 +1278,7 @@ public class IntegrationFlowTests { } @Configuration + @Import({TestFtpServer.class, TestSftpServer.class}) @EnableAutoConfiguration @IntegrationComponentScan public static class ContextConfiguration { diff --git a/spring-integration-java-dsl/src/test/java/org/springframework/integration/dsl/test/mail/MailTests.java b/spring-integration-java-dsl/src/test/java/org/springframework/integration/dsl/test/mail/MailTests.java new file mode 100644 index 0000000..3d994df --- /dev/null +++ b/spring-integration-java-dsl/src/test/java/org/springframework/integration/dsl/test/mail/MailTests.java @@ -0,0 +1,133 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.dsl.test.mail; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.endsWith; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.util.Properties; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.config.EnableIntegration; +import org.springframework.integration.dsl.IntegrationFlow; +import org.springframework.integration.dsl.IntegrationFlows; +import org.springframework.integration.dsl.mail.Mail; +import org.springframework.integration.dsl.test.mail.PoorMansMailServer.SmtpServer; +import org.springframework.integration.mail.MailHeaders; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.test.util.TestUtils; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHandler; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.util.SocketUtils; + +/** + * @author Gary Russell + * @author Artem Bilan + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +@DirtiesContext +public class MailTests { + + private final static int port = SocketUtils.findAvailableTcpPort(); + + private static SmtpServer server = PoorMansMailServer.smtp(port); + + + @BeforeClass + public static void setup() throws InterruptedException { + int n = 0; + while (n++ < 100 && !server.isListening()) { + Thread.sleep(100); + } + assertTrue(n < 100); + } + + @AfterClass + public static void tearDown() { + server.stop(); + } + + @Autowired + @Qualifier("sendMailChannel") + private MessageChannel sendMailChannel; + + @Autowired + @Qualifier("sendMailEndpoint.handler") + private MessageHandler sendMailHandler; + + @Test + public void testOutbound() throws Exception { + assertEquals("localhost", TestUtils.getPropertyValue(this.sendMailHandler, "mailSender.host")); + + Properties javaMailProperties = TestUtils.getPropertyValue(this.sendMailHandler, + "mailSender.javaMailProperties", Properties.class); + assertEquals("true", javaMailProperties.getProperty("mail.debug")); + + this.sendMailChannel.send(MessageBuilder.withPayload("foo") + .setHeader(MailHeaders.SUBJECT, "foo") + .setHeader(MailHeaders.FROM, "foo@bar") + .setHeader(MailHeaders.TO, "bar@baz") + .build()); + + int n = 0; + while (n++ < 100 && server.getMessages().size() == 0) { + Thread.sleep(100); + } + + assertTrue(server.getMessages().size() > 0); + String message = server.getMessages().get(0); + assertThat(message, endsWith("foo\n")); + assertThat(message, containsString("foo@bar")); + assertThat(message, containsString("bar@baz")); + assertThat(message, containsString("user:user")); + assertThat(message, containsString("password:pw")); + + } + + @Configuration + @EnableIntegration + public static class ContextConfiguration { + + @Bean + public IntegrationFlow sendMailFlow() { + return IntegrationFlows.from("sendMailChannel") + .handle(Mail.outboundAdapter("localhost") + .port(port) + .credentials("user", "pw") + .protocol("smtp") + .javaMailProperties(p -> p.put("mail.debug", "true")), + e -> e.id("sendMailEndpoint")) + .get(); + } + + } + +} diff --git a/spring-integration-java-dsl/src/test/java/org/springframework/integration/dsl/test/mail/PoorMansMailServer.java b/spring-integration-java-dsl/src/test/java/org/springframework/integration/dsl/test/mail/PoorMansMailServer.java new file mode 100644 index 0000000..776b2bb --- /dev/null +++ b/spring-integration-java-dsl/src/test/java/org/springframework/integration/dsl/test/mail/PoorMansMailServer.java @@ -0,0 +1,177 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.dsl.test.mail; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.net.ServerSocketFactory; + +import org.apache.sshd.common.util.Base64; + +/** + * @author Gary Russell + * + */ +public class PoorMansMailServer { + + public static SmtpServer smtp(int port) { + try { + return new SmtpServer(port); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static class SmtpServer implements Runnable { + + private final ServerSocket socket; + + private final ExecutorService exec = Executors.newCachedThreadPool(); + + private final List messages = new ArrayList(); + + private volatile boolean listening; + + public SmtpServer(int port) throws IOException { + this.socket = ServerSocketFactory.getDefault().createServerSocket(port); + this.listening = true; + exec.execute(this); + } + + public boolean isListening() { + return listening; + } + + public List getMessages() { + return messages; + } + + @Override + public void run() { + try { + while (!socket.isClosed()) { + Socket socket = this.socket.accept(); + exec.execute(new SmtpHandler(socket)); + } + } + catch (IOException e) { + this.listening = false; + } + } + + public void stop() { + try { + this.socket.close(); + } + catch (IOException e) { + e.printStackTrace(); + } + this.exec.shutdownNow(); + } + + public class SmtpHandler implements Runnable { + + private final Socket socket; + + private BufferedWriter writer; + + public SmtpHandler(Socket socket) { + this.socket = socket; + } + + @Override + public void run() { + try { + StringBuilder sb = new StringBuilder(); + BufferedReader reader = new BufferedReader(new InputStreamReader(this.socket.getInputStream())); + this.writer = new BufferedWriter(new OutputStreamWriter(this.socket.getOutputStream())); + write("220 foo SMTP"); + while (!socket.isClosed()) { + String line = reader.readLine(); + if (line.contains("EHLO")) { + write("250-foo hello [0,0,0,0], foo"); + write("250-AUTH LOGIN PLAIN"); + write("250 OK"); + } + else if (line.contains("MAIL FROM")) { + write("250 OK"); + } + else if (line.contains("RCPT TO")) { + write("250 OK"); + } + else if (line.contains("AUTH LOGIN")) { + write("334 VXNlcm5hbWU6"); + } + else if (line.contains("dXNlcg==")) { // base64 'user' + sb.append("user:"); + sb.append((new String(new Base64().decode(line.getBytes())))); + sb.append("\n"); + write("334 UGFzc3dvcmQ6"); + } + else if (line.contains("cHc=")) { // base64 'pw' + sb.append("password:"); + sb.append((new String(new Base64().decode(line.getBytes())))); + sb.append("\n"); + write("235"); + } + else if (line.equals("DATA")) { + write("354"); + } + else if (line.equals(".")) { + write("250"); + } + else if (line.equals("QUIT")) { + write("221"); + socket.close(); + } + else { + sb.append(line); + sb.append("\n"); + } + } + messages.add(sb.toString()); + } + catch (IOException e) { + e.printStackTrace(); + } + } + + private void write(String str) throws IOException { + this.writer.write(str); + this.writer.write("\r\n"); + this.writer.flush(); + } + + + } + + } + + private PoorMansMailServer() { + } + +}