This is an automated email from the ASF dual-hosted git repository. olli pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-clam.git
commit 96701940c35f6a4270ee3b6119f72a04155ed132 Author: Oliver Lietz <[email protected]> AuthorDate: Sun Aug 30 21:52:07 2020 +0200 SLING-9710 Provide a Mail Sending Scan Result Handler --- bnd.bnd | 13 ++ pom.xml | 45 ++++- .../internal/MailSendingScanResultHandler.java | 196 +++++++++++++++++++++ .../MailSendingScanResultHandlerConfiguration.java | 105 +++++++++++ .../it/tests/MailSendingScanResultHandlerIT.java | 174 ++++++++++++++++++ src/test/resources/password | 1 + 6 files changed, 533 insertions(+), 1 deletion(-) diff --git a/bnd.bnd b/bnd.bnd index e36a0d6..aecb1cd 100644 --- a/bnd.bnd +++ b/bnd.bnd @@ -1,3 +1,16 @@ +DynamicImport-Package:\ + javax.mail.*,\ + org.apache.commons.lang3.*,\ + org.apache.sling.commons.messaging.*,\ + org.thymeleaf.* + +Import-Package:\ + javax.mail.*;resolution:=optional,\ + org.apache.commons.lang3.*;resolution:=optional,\ + org.apache.sling.commons.messaging.*;resolution:=optional,\ + org.thymeleaf.*;resolution:=optional,\ + * + -removeheaders:\ Include-Resource,\ Private-Package diff --git a/pom.xml b/pom.xml index 52c552a..eb13797 100644 --- a/pom.xml +++ b/pom.xml @@ -136,6 +136,12 @@ <artifactId>javax.servlet-api</artifactId> <scope>provided</scope> </dependency> + <dependency> + <groupId>jakarta.mail</groupId> + <artifactId>jakarta.mail-api</artifactId> + <version>1.6.5</version> + <scope>provided</scope> + </dependency> <!-- ok io/http --> <dependency> <groupId>org.apache.servicemix.bundles</groupId> @@ -188,6 +194,18 @@ <version>2.5</version> <scope>provided</scope> </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-email</artifactId> + <version>1.5</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + <version>3.9</version> + <scope>provided</scope> + </dependency> <!-- Apache Felix --> <dependency> <groupId>org.apache.felix</groupId> @@ -241,6 +259,18 @@ </dependency> <dependency> <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.commons.messaging</artifactId> + <version>1.0.0</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.commons.messaging.mail</artifactId> + <version>1.0.0</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.sling</groupId> <artifactId>org.apache.sling.commons.threads</artifactId> <version>3.2.18</version> <scope>provided</scope> @@ -266,9 +296,16 @@ <dependency> <groupId>org.apache.sling</groupId> <artifactId>org.apache.sling.testing.paxexam</artifactId> - <version>3.1.0</version> + <version>3.1.1-SNAPSHOT</version> <scope>test</scope> </dependency> + <!-- Thymeleaf --> + <dependency> + <groupId>org.thymeleaf</groupId> + <artifactId>thymeleaf</artifactId> + <version>3.0.11.RELEASE</version> + <scope>provided</scope> + </dependency> <!-- nullability --> <dependency> <groupId>org.jetbrains</groupId> @@ -306,6 +343,12 @@ <scope>test</scope> </dependency> <dependency> + <groupId>com.icegreen</groupId> + <artifactId>greenmail</artifactId> + <version>1.5.11</version> + <scope>test</scope> + </dependency> + <dependency> <groupId>org.ops4j.pax.exam</groupId> <artifactId>pax-exam</artifactId> <version>${org.ops4j.pax.exam.version}</version> diff --git a/src/main/java/org/apache/sling/clam/result/internal/MailSendingScanResultHandler.java b/src/main/java/org/apache/sling/clam/result/internal/MailSendingScanResultHandler.java new file mode 100644 index 0000000..e9070cb --- /dev/null +++ b/src/main/java/org/apache/sling/clam/result/internal/MailSendingScanResultHandler.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.sling.clam.result.internal; + +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +import javax.mail.internet.MimeMessage; + +import org.apache.commons.lang3.StringUtils; +import org.apache.sling.clam.result.JcrPropertyScanResultHandler; +import org.apache.sling.commons.clam.ScanResult; +import org.apache.sling.commons.messaging.mail.MailService; +import org.apache.sling.commons.messaging.mail.MessageBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.osgi.framework.Constants; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Modified; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.osgi.service.component.annotations.ReferencePolicyOption; +import org.osgi.service.metatype.annotations.Designate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.thymeleaf.ITemplateEngine; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.TemplateSpec; +import org.thymeleaf.context.Context; +import org.thymeleaf.context.IContext; +import org.thymeleaf.templatemode.TemplateMode; + +import static org.apache.sling.clam.internal.ClamUtil.properties; + +@Component( + configurationPolicy = ConfigurationPolicy.REQUIRE, + property = { + Constants.SERVICE_DESCRIPTION + "=Apache Sling Clam Mail Sending Scan Result Handler", + Constants.SERVICE_VENDOR + "=The Apache Software Foundation" + } +) +@Designate( + ocd = MailSendingScanResultHandlerConfiguration.class, + factory = true +) +public class MailSendingScanResultHandler implements JcrPropertyScanResultHandler { + + @Reference( + policy = ReferencePolicy.DYNAMIC, + policyOption = ReferencePolicyOption.GREEDY + ) + private volatile MailService mailService; + + private MailSendingScanResultHandlerConfiguration configuration; + + private final ITemplateEngine templateEngine = new TemplateEngine(); + + private final Logger logger = LoggerFactory.getLogger(MailSendingScanResultHandler.class); + + public MailSendingScanResultHandler() { + } + + @Activate + private void activate(final MailSendingScanResultHandlerConfiguration configuration) { + logger.debug("activating"); + this.configuration = configuration; + } + + @Modified + private void modified(final MailSendingScanResultHandlerConfiguration configuration) { + logger.debug("modifying"); + this.configuration = configuration; + } + + @Deactivate + private void deactivate() { + logger.debug("deactivating"); + this.configuration = null; + } + + @Override + public void handleJcrPropertyScanResult(@NotNull ScanResult scanResult, @NotNull String path, int propertyType, @Nullable String userId) { + if (checkPublish(scanResult)) { + final Map<String, Object> properties = properties(path, userId, scanResult); + try { + sendMail(properties); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + @Override + public void handleJcrPropertyScanResult(@NotNull ScanResult scanResult, @NotNull String path, int index, int propertyType, @Nullable String userId) { + if (checkPublish(scanResult)) { + final Map<String, Object> properties = properties(path, index, userId, scanResult); + try { + sendMail(properties); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + private boolean checkPublish(final ScanResult scanResult) { + return !scanResult.isOk() || configuration.result_status_ok_send(); + } + + private void sendMail(final Map<String, Object> properties) throws Exception { + final MailService mailService = this.mailService; + final MessageBuilder builder = mailService.getMessageBuilder(); + + final String from = configuration.mail_from(); + final String[] to = configuration.mail_to(); + final String[] cc = configuration.mail_cc(); + final String[] bcc = configuration.mail_bcc(); + final String[] replyTo = configuration.mail_replyTo(); + final String subject = configuration.mail_subject(); + final String text = configuration.mail_text(); + final String html = configuration.mail_html(); + + if (StringUtils.isNotBlank(from)) { + builder.from(from); + } + for (final String address : to) { + if (StringUtils.isNotBlank(address)) { + builder.to(address); + } + } + for (final String address : cc) { + if (StringUtils.isNotBlank(address)) { + builder.cc(address); + } + } + for (final String address : bcc) { + if (StringUtils.isNotBlank(address)) { + builder.bcc(address); + } + } + for (final String address : replyTo) { + if (StringUtils.isNotBlank(address)) { + builder.replyTo(address); + } + } + if (StringUtils.isNotBlank(subject)) { + final IContext context = new Context(Locale.ENGLISH, properties); + final TemplateSpec templateSpec = new TemplateSpec(subject, TemplateMode.TEXT); + final String s = templateEngine.process(templateSpec, context); + builder.subject(s); + } + if (StringUtils.isNotBlank(text)) { + final IContext context = new Context(Locale.ENGLISH, properties); + final TemplateSpec templateSpec = new TemplateSpec(text, TemplateMode.TEXT); + final String t = templateEngine.process(templateSpec, context); + builder.text(t); + } + if (StringUtils.isNotBlank(html)) { + final IContext context = new Context(Locale.ENGLISH, properties); + final TemplateSpec templateSpec = new TemplateSpec(html, TemplateMode.HTML); + final String h = templateEngine.process(templateSpec, context); + builder.html(h); + } + final MimeMessage message = builder.build(); + logger.debug("sending scan result mail: {}", properties); + final CompletableFuture<Void> future = mailService.sendMessage(message); + future.whenComplete((v, e) -> { + if (Objects.nonNull(e)) { + logger.error("sending scan result mail failed", e); + } else { + logger.debug("sending scan result mail succeeded"); + } + }); + } + +} diff --git a/src/main/java/org/apache/sling/clam/result/internal/MailSendingScanResultHandlerConfiguration.java b/src/main/java/org/apache/sling/clam/result/internal/MailSendingScanResultHandlerConfiguration.java new file mode 100644 index 0000000..f319285 --- /dev/null +++ b/src/main/java/org/apache/sling/clam/result/internal/MailSendingScanResultHandlerConfiguration.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.sling.clam.result.internal; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +@ObjectClassDefinition( + name = "Apache Sling Clam Mail Sending Scan Result Handler", + description = "Sends JCR property scan results via mail" +) +@SuppressWarnings("java:S100") +@interface MailSendingScanResultHandlerConfiguration { + + @AttributeDefinition( + name = "Mail From", + description = "Mail From address", + required = false + ) + String mail_from(); + + @AttributeDefinition( + name = "Mail To", + description = "Mail To addresses" + ) + String[] mail_to() default {}; + + @AttributeDefinition( + name = "Mail CC", + description = "Mail CC addresses" + ) + String[] mail_cc() default {}; + + @AttributeDefinition( + name = "Mail BCC", + description = "Mail BCC addresses" + ) + String[] mail_bcc() default {}; + + @AttributeDefinition( + name = "Mail Reply-To", + description = "Mail Reply-To addresses" + ) + String[] mail_replyTo() default {}; + + @AttributeDefinition( + name = "Mail Subject", + description = "Mail Subject template (available variables: path, index, message, status, userId, started, size, timestamp)", + required = false + ) + String mail_subject() default "Clam scan result: [(${status})] for [(${path})][# th:if=\"${index}\"] [(${index})][/]"; + + @AttributeDefinition( + name = "Mail Text", + description = "Mail Text template (available variables: path, index, message, status, userId, started, size, timestamp)", + required = false + ) // newlines get lost in scr and metatype XML + String mail_text() default "status: [(${status})]\n" + + "message: [(${message})]\n" + + "path: [(${path})]\n" + + "[# th:if=\"${index}\"]index: [(${index})][/]\n" + + "size: [(${size})]\n" + + "[# th:if=\"${userId}\"]userId: [(${userId})][/]\n" + + "started: [(${#dates.formatISO(new java.util.Date(started))})]\n" + + "timestamp: [(${#dates.formatISO(new java.util.Date(timestamp))})]\n"; + + @AttributeDefinition( + name = "Mail HTML", + description = "Mail HTML template (available variables: path, index, message, status, userId, started, size, timestamp)", + required = false + ) + String mail_html(); + + @AttributeDefinition( + name = "send status ok", + description = "Send scan results with status OK also" + ) + boolean result_status_ok_send() default false; + + @AttributeDefinition( + name = "Mail Service target", + description = "Filter expression to target a Mail Service", + required = false + ) + String mailService_target(); + + String webconsole_configurationFactory_nameHint() default "{mail.to}:{result.status.ok.send}:{mail.subject}"; + +} diff --git a/src/test/java/org/apache/sling/clam/it/tests/MailSendingScanResultHandlerIT.java b/src/test/java/org/apache/sling/clam/it/tests/MailSendingScanResultHandlerIT.java new file mode 100644 index 0000000..8111932 --- /dev/null +++ b/src/test/java/org/apache/sling/clam/it/tests/MailSendingScanResultHandlerIT.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.sling.clam.it.tests; + +import java.security.Security; +import java.util.Objects; + +import javax.inject.Inject; +import javax.mail.internet.MimeMessage; + +import com.icegreen.greenmail.util.DummySSLSocketFactory; +import com.icegreen.greenmail.util.GreenMail; +import com.icegreen.greenmail.util.ServerSetup; +import org.apache.commons.mail.util.MimeMessageParser; +import org.apache.sling.clam.jcr.NodeDescendingJcrPropertyDigger; +import org.apache.sling.clam.result.JcrPropertyScanResultHandler; +import org.apache.sling.resource.presence.ResourcePresence; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.Configuration; +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerClass; +import org.ops4j.pax.exam.util.Filter; +import org.ops4j.pax.exam.util.PathUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.truth.Truth.assertThat; +import static org.apache.sling.testing.paxexam.SlingOptions.greenmail; +import static org.apache.sling.testing.paxexam.SlingOptions.slingCommonsMessagingMail; +import static org.apache.sling.testing.paxexam.SlingOptions.slingResourcePresence; +import static org.apache.sling.testing.paxexam.SlingOptions.slingStarterContent; +import static org.apache.sling.testing.paxexam.SlingOptions.thymeleaf; +import static org.ops4j.pax.exam.CoreOptions.mavenBundle; +import static org.ops4j.pax.exam.CoreOptions.options; +import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.factoryConfiguration; +import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.newConfiguration; + +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerClass.class) +public class MailSendingScanResultHandlerIT extends ClamTestSupport { + + @Inject + @Filter("(service.pid=org.apache.sling.clam.result.internal.MailSendingScanResultHandler)") + private JcrPropertyScanResultHandler jcrPropertyScanResultHandler; + + @Inject + @Filter(value = "(path=/content/starter)", timeout = 300000) + private ResourcePresence resourcePresence; + + @Inject + private NodeDescendingJcrPropertyDigger nodeDescendingJcrPropertyDigger; + + private GreenMail greenMail; + + private final Logger logger = LoggerFactory.getLogger(MailSendingScanResultHandlerIT.class); + + @Configuration + public Option[] configuration() { + final int port = findFreePort(); + final String path = String.format("%s/src/test/resources/password", PathUtils.getBaseDir()); + return options( + baseConfiguration(), + clamdConfiguration(), + slingResourcePresence(), + factoryConfiguration("org.apache.sling.resource.presence.internal.ResourcePresenter") + .put("path", "/content/starter") + .asOption(), + newConfiguration("org.apache.sling.clam.result.internal.MailSendingScanResultHandler") + .put("mail.from", "[email protected]") + .put("mail.to", "[email protected]") + .put("mail.cc", "[email protected]") + .put("mail.bcc", "[email protected]") + .put("mail.replyTo", "[email protected]") + .put("result.status.ok.send", true) + .asOption(), + slingStarterContent(), + // Commons Messaging Mail + factoryConfiguration("org.apache.sling.commons.messaging.mail.internal.SimpleMessageIdProvider") + .put("host", "localhost") + .asOption(), + factoryConfiguration("org.apache.sling.commons.messaging.mail.internal.SimpleMailService") + .put("mail.smtps.from", "[email protected]") + .put("mail.smtps.host", "localhost") + .put("mail.smtps.port", port) + .put("username", "username") + .put("password", "OEKPFL5cVJRqVjh4QaDZhvBiqv8wgWBMJ8PGbYHTqev046oV6888mna9w1mIGCXK") + .asOption(), + slingCommonsMessagingMail(), + // Commons Crypto + factoryConfiguration("org.apache.sling.commons.crypto.jasypt.internal.JasyptStandardPBEStringCryptoService") + .put("algorithm", "PBEWITHHMACSHA512ANDAES_256") + .asOption(), + factoryConfiguration("org.apache.sling.commons.crypto.jasypt.internal.JasyptRandomIvGeneratorRegistrar") + .put("algorithm", "SHA1PRNG") + .asOption(), + factoryConfiguration("org.apache.sling.commons.crypto.internal.FilePasswordProvider") + .put("path", path) + .asOption(), + // Thymeleaf + thymeleaf(), + // testing – mail + greenmail(), + mavenBundle().groupId("org.apache.commons").artifactId("commons-email").versionAsInProject() + ); + } + + @Before + public void setUp() throws Exception { + if (Objects.isNull(greenMail)) { + // set up GreenMail server + Security.setProperty("ssl.SocketFactory.provider", DummySSLSocketFactory.class.getName()); + final org.osgi.service.cm.Configuration[] configurations = configurationAdmin.listConfigurations("(service.factoryPid=org.apache.sling.commons.messaging.mail.internal.SimpleMailService)"); + final org.osgi.service.cm.Configuration configuration = configurations[0]; + final int port = (int) configuration.getProperties().get("mail.smtps.port"); + final ServerSetup serverSetup = new ServerSetup(port, "127.0.0.1", "smtps"); + greenMail = new GreenMail(serverSetup); + greenMail.setUser("username", "password"); + greenMail.start(); + } + } + + @After + public void tearDown() { + if (Objects.nonNull(greenMail)) { + greenMail.stop(); + greenMail = null; + } + } + + @Test + public void testJcrPropertyScanResultHandler() { + assertThat(jcrPropertyScanResultHandler).isNotNull(); + } + + @Test + public void testSentResults() throws Exception { + digBinaries(nodeDescendingJcrPropertyDigger, "/content/starter"); + greenMail.waitForIncomingEmail(360000, 24); + final MimeMessage[] messages = greenMail.getReceivedMessages(); + assertThat(messages.length).isEqualTo(24); + for (final MimeMessage message : messages) { + assertThat(message.getSubject()).startsWith("Clam scan result: OK for /content/starter/"); + final MimeMessageParser parser = new MimeMessageParser(message).parse(); + final String text = parser.getPlainContent(); + assertThat(text).contains("status: OK"); + assertThat(text).contains("message: "); + assertThat(text).contains("path: /content/starter/"); + assertThat(text).contains("started: "); + assertThat(text).contains("timestamp: "); + } + } + +} diff --git a/src/test/resources/password b/src/test/resources/password new file mode 100644 index 0000000..ad66ce8 --- /dev/null +++ b/src/test/resources/password @@ -0,0 +1 @@ ++AQ?aDes!'DBMkrCi:FE6q\sOn=Pbmn=PK8n=PK? \ No newline at end of file
