From cd8c3d7327e409d086d972c6a32ef111d340cdd7 Mon Sep 17 00:00:00 2001 From: Johannes Stelzer Date: Sun, 8 Mar 2015 20:21:13 +0100 Subject: [PATCH] Add mail health check Define an additional health indicator for each JavaMailSenderImpl instance in the context. Closes gh-2017 and gh-2617 --- spring-boot-actuator/pom.xml | 5 + .../HealthIndicatorAutoConfiguration.java | 42 +++++- .../actuate/health/MailHealthIndicator.java | 86 +++++++++++ ...itional-spring-configuration-metadata.json | 6 + ...HealthIndicatorAutoConfigurationTests.java | 38 ++++- .../health/MailHealthIndicatorTest.java | 138 ++++++++++++++++++ .../mail/MailSenderAutoConfiguration.java | 6 +- 7 files changed, 316 insertions(+), 5 deletions(-) create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/MailHealthIndicator.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/MailHealthIndicatorTest.java diff --git a/spring-boot-actuator/pom.xml b/spring-boot-actuator/pom.xml index 22d8cb62db4..48a866fda3e 100644 --- a/spring-boot-actuator/pom.xml +++ b/spring-boot-actuator/pom.xml @@ -41,6 +41,11 @@ spring-context + + com.sun.mail + javax.mail + true + io.dropwizard.metrics metrics-core diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/HealthIndicatorAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/HealthIndicatorAutoConfiguration.java index c3a3decbf05..eeb56a73343 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/HealthIndicatorAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/HealthIndicatorAutoConfiguration.java @@ -32,6 +32,7 @@ import org.springframework.boot.actuate.health.DiskSpaceHealthIndicator; import org.springframework.boot.actuate.health.DiskSpaceHealthIndicatorProperties; import org.springframework.boot.actuate.health.HealthAggregator; import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.MailHealthIndicator; import org.springframework.boot.actuate.health.MongoHealthIndicator; import org.springframework.boot.actuate.health.OrderedHealthAggregator; import org.springframework.boot.actuate.health.RabbitHealthIndicator; @@ -48,6 +49,7 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadata; import org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvider; import org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProviders; +import org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoDataAutoConfiguration; import org.springframework.boot.autoconfigure.redis.RedisAutoConfiguration; @@ -57,6 +59,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.mail.javamail.JavaMailSenderImpl; /** * {@link EnableAutoConfiguration Auto-configuration} for {@link HealthIndicator}s. @@ -70,7 +73,8 @@ import org.springframework.data.redis.connection.RedisConnectionFactory; @AutoConfigureBefore({ EndpointAutoConfiguration.class }) @AutoConfigureAfter({ DataSourceAutoConfiguration.class, MongoAutoConfiguration.class, MongoDataAutoConfiguration.class, RedisAutoConfiguration.class, - RabbitAutoConfiguration.class, SolrAutoConfiguration.class }) + RabbitAutoConfiguration.class, SolrAutoConfiguration.class, + MailSenderAutoConfiguration.class }) @EnableConfigurationProperties({ HealthIndicatorAutoConfigurationProperties.class }) public class HealthIndicatorAutoConfiguration { @@ -276,4 +280,40 @@ public class HealthIndicatorAutoConfiguration { } + @Configuration + @ConditionalOnBean(JavaMailSenderImpl.class) + @ConditionalOnProperty(prefix = "management.health.mail", name = "enabled", matchIfMissing = true) + public static class MailHealthIndicatorConfiguration { + + @Autowired + private HealthAggregator healthAggregator; + + @Autowired(required = false) + private Map mailSenders; + + @Bean + @ConditionalOnMissingBean(name = "mailSenderHealthIndicator") + public HealthIndicator mailHealthIndicator() { + if (this.mailSenders.size() == 1) { + JavaMailSenderImpl mailSender = this.mailSenders.values().iterator() + .next(); + return createMailHealthIndicator(mailSender); + } + CompositeHealthIndicator composite = new CompositeHealthIndicator( + this.healthAggregator); + for (Map.Entry entry : this.mailSenders + .entrySet()) { + String name = entry.getKey(); + JavaMailSenderImpl mailSender = entry.getValue(); + composite.addHealthIndicator(name, createMailHealthIndicator(mailSender)); + } + return composite; + } + + private MailHealthIndicator createMailHealthIndicator( + JavaMailSenderImpl mailSender) { + return new MailHealthIndicator(mailSender); + } + } + } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/MailHealthIndicator.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/MailHealthIndicator.java new file mode 100644 index 00000000000..c6d42f42eec --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/MailHealthIndicator.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2015 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.boot.actuate.health; + +import javax.mail.MessagingException; +import javax.mail.NoSuchProviderException; +import javax.mail.Session; +import javax.mail.Transport; + +import org.springframework.boot.actuate.health.Health.Builder; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +/** + * {@link HealthIndicator} for configured smtp server(s). + * + * @author Johannes Stelzer + * @since 1.3.0 + */ +public class MailHealthIndicator extends AbstractHealthIndicator { + + private final JavaMailSenderImpl mailSender; + + public MailHealthIndicator(JavaMailSenderImpl mailSender) { + this.mailSender = mailSender; + } + + @Override + protected void doHealthCheck(Builder builder) throws Exception { + String location = String.format("%s:%d", this.mailSender.getHost(), this.mailSender.getPort()); + builder.withDetail("location", location); + + Transport transport = null; + try { + transport = connectTransport(); + builder.up(); + } + finally { + if (transport != null) { + transport.close(); + } + } + } + + // Copy-paste from JavaMailSenderImpl - see SPR-12799 + private Transport connectTransport() throws MessagingException { + String username = this.mailSender.getUsername(); + String password = this.mailSender.getPassword(); + if ("".equals(username)) { + username = null; + if ("".equals(password)) { + password = null; + } + } + + Transport transport = getTransport(this.mailSender.getSession()); + transport.connect(this.mailSender.getHost(), this.mailSender.getPort(), username, + password); + return transport; + } + + private Transport getTransport(Session session) throws NoSuchProviderException { + String protocol = this.mailSender.getProtocol(); + if (protocol == null) { + protocol = session.getProperty("mail.transport.protocol"); + if (protocol == null) { + protocol = JavaMailSenderImpl.DEFAULT_PROTOCOL; + } + } + return session.getTransport(protocol); + } + +} diff --git a/spring-boot-actuator/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-actuator/src/main/resources/META-INF/additional-spring-configuration-metadata.json index dd95d1badb9..9f4510dd870 100644 --- a/spring-boot-actuator/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-actuator/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -58,6 +58,12 @@ "description": "Enable Solr health check.", "defaultValue": true }, + { + "name": "management.health.mail.enabled", + "type": "java.lang.Boolean", + "description": "Enable Mail health check.", + "defaultValue": true + }, { "name": "spring.git.properties", "type": "java.lang.String", diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthIndicatorAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthIndicatorAutoConfigurationTests.java index 56ac8fbb533..c3428977050 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthIndicatorAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthIndicatorAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * Copyright 2012-2015 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. @@ -27,6 +27,7 @@ import org.springframework.boot.actuate.health.ApplicationHealthIndicator; import org.springframework.boot.actuate.health.DataSourceHealthIndicator; import org.springframework.boot.actuate.health.DiskSpaceHealthIndicator; import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.MailHealthIndicator; import org.springframework.boot.actuate.health.MongoHealthIndicator; import org.springframework.boot.actuate.health.RabbitHealthIndicator; import org.springframework.boot.actuate.health.RedisHealthIndicator; @@ -37,6 +38,7 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; import org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvidersConfiguration; +import org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoDataAutoConfiguration; import org.springframework.boot.autoconfigure.redis.RedisAutoConfiguration; @@ -304,4 +306,38 @@ public class HealthIndicatorAutoConfigurationTests { } + @Test + public void mailHealthIndicator() { + this.context = new AnnotationConfigApplicationContext(); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.mail.host:smtp.acme.org", + "management.health.diskspace.enabled:false"); + this.context.register(MailSenderAutoConfiguration.class, + ManagementServerProperties.class, HealthIndicatorAutoConfiguration.class); + this.context.refresh(); + + Map beans = this.context + .getBeansOfType(HealthIndicator.class); + assertEquals(1, beans.size()); + assertEquals(MailHealthIndicator.class, beans.values().iterator().next() + .getClass()); + } + + @Test + public void notMailHealthIndicator() { + this.context = new AnnotationConfigApplicationContext(); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.mail.host:smtp.acme.org", "management.health.mail.enabled:false", + "management.health.diskspace.enabled:false"); + this.context.register(MailSenderAutoConfiguration.class, + ManagementServerProperties.class, HealthIndicatorAutoConfiguration.class); + this.context.refresh(); + + Map beans = this.context + .getBeansOfType(HealthIndicator.class); + assertEquals(1, beans.size()); + assertEquals(ApplicationHealthIndicator.class, beans.values().iterator().next() + .getClass()); + } + } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/MailHealthIndicatorTest.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/MailHealthIndicatorTest.java new file mode 100644 index 00000000000..102c291d62e --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/MailHealthIndicatorTest.java @@ -0,0 +1,138 @@ +/* + * Copyright 2012-2015 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.boot.actuate.health; + +import java.util.Properties; + +import javax.mail.Address; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.Provider; +import javax.mail.Provider.Type; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.URLName; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link MailHealthIndicator}. + * + * @author Johannes Stelzer + */ +public class MailHealthIndicatorTest { + + private JavaMailSenderImpl mailSender; + private MailHealthIndicator indicator; + + @Before + public void setup() { + Session session = Session.getDefaultInstance(new Properties()); + session.addProvider(new Provider(Type.TRANSPORT, "success", + SuccessTransport.class.getName(), "Test", "1.0.0")); + + session.addProvider(new Provider(Type.TRANSPORT, "fail", FailTransport.class + .getName(), "Test", "1.0.0")); + + session.addProvider(new Provider(Type.TRANSPORT, "failOnClose", + FailOnCloseTransport.class.getName(), "Test", "1.0.0")); + + this.mailSender = mock(JavaMailSenderImpl.class); + when(this.mailSender.getHost()).thenReturn("smtp.acme.org"); + when(this.mailSender.getPort()).thenReturn(25); + when(this.mailSender.getSession()).thenReturn(session); + + this.indicator = new MailHealthIndicator(this.mailSender); + } + + @Test + public void smtpIsUp() { + when(this.mailSender.getProtocol()).thenReturn("success"); + + Health health = this.indicator.health(); + + assertEquals(Status.UP, health.getStatus()); + assertEquals("smtp.acme.org:25", health.getDetails().get("location")); + } + + @Test + public void smtpIsDown() { + when(this.mailSender.getProtocol()).thenReturn("fail"); + + Health health = this.indicator.health(); + + assertEquals(Status.DOWN, health.getStatus()); + assertEquals("smtp.acme.org:25", health.getDetails().get("location")); + } + + @Test + public void unexpectedExceptionOnClose() { + when(this.mailSender.getProtocol()).thenReturn("failOnClose"); + + Health health = this.indicator.health(); + + assertEquals(Status.DOWN, health.getStatus()); + assertEquals("smtp.acme.org:25", health.getDetails().get("location")); + } + + public static class SuccessTransport extends Transport { + public SuccessTransport(Session session, URLName urlname) { + super(session, urlname); + } + + @Override + public synchronized void connect(String host, int port, String user, + String password) throws MessagingException { + } + + @Override + public void sendMessage(Message msg, Address[] addresses) + throws MessagingException { + } + + } + + public static class FailTransport extends SuccessTransport { + public FailTransport(Session session, URLName urlname) { + super(session, urlname); + } + + @Override + public synchronized void connect(String host, int port, String user, + String password) throws MessagingException { + throw new MessagingException("fail on connect"); + } + } + + public static class FailOnCloseTransport extends SuccessTransport { + public FailOnCloseTransport(Session session, URLName urlname) { + super(session, urlname); + } + + @Override + public synchronized void close() throws MessagingException { + throw new MessagingException("fail on close"); + } + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mail/MailSenderAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mail/MailSenderAutoConfiguration.java index cafd4b7270e..44aadecd341 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mail/MailSenderAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mail/MailSenderAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * Copyright 2012-2015 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. @@ -30,7 +30,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.mail.MailSender; -import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.JavaMailSenderImpl; /** @@ -38,6 +37,7 @@ import org.springframework.mail.javamail.JavaMailSenderImpl; * * @author Oliver Gierke * @author Stephane Nicoll + * @author Johannes Stelzer * @since 1.2.0 */ @Configuration @@ -51,7 +51,7 @@ public class MailSenderAutoConfiguration { MailProperties properties; @Bean - public JavaMailSender mailSender() { + public JavaMailSenderImpl mailSender() { JavaMailSenderImpl sender = new JavaMailSenderImpl(); sender.setHost(this.properties.getHost()); if (this.properties.getPort() != null) {