From afc83df73dee3f18ec5b94294004dba4e3647022 Mon Sep 17 00:00:00 2001 From: Nour AL KOTOB Date: Mon, 25 Sep 2023 15:31:24 +0200 Subject: [PATCH] NXP-32079: Amazon SES MailSender package --- ci/Jenkinsfiles/build.groovy | 1 + .../ecm/blob/s3/S3BlobStoreConfiguration.java | 23 +--- .../ecm/core/storage/sql/S3BinaryManager.java | 25 +--- .../ecm/automation/core/mail/Mailer.java | 1 - .../platform/nuxeo-mail-amazon-ses/pom.xml | 30 +++++ .../nuxeo/mail/amazon/ses/SESMailSender.java | 118 ++++++++++++++++++ .../src/main/resources/META-INF/MANIFEST.MF | 5 + .../org/nuxeo/mail/amazon/ses/SESFeature.java | 58 +++++++++ .../mail/amazon/ses/TestSESMailSender.java | 90 +++++++++++++ .../src/test/resources/META-INF/MANIFEST.MF | 5 + .../test-ses-override-default-sender.xml | 8 ++ .../test-ses-wrong-sender-contrib.xml | 16 +++ .../java/org/nuxeo/mail/BlobDataSource.java | 4 + .../org/nuxeo/mail/MailSenderDescriptor.java | 12 ++ .../org/nuxeo/mail/MimeMessageHelper.java | 100 +++++++++++++++ .../java/org/nuxeo/mail/SMTPMailSender.java | 64 +--------- .../org/nuxeo/mail/TestMimeMessageHelper.java | 84 +++++++++++++ modules/platform/pom.xml | 1 + .../runtime/aws/AWSConfigurationService.java | 10 ++ .../aws/AWSConfigurationServiceImpl.java | 23 ++++ .../nuxeo-mail-amazon-ses-package/pom.xml | 20 +++ .../install/templates/ses/nuxeo.defaults | 4 + .../ses/nxserver/config/ses-sender-config.xml | 10 ++ .../src/main/resources/package.xml | 14 +++ packages/pom.xml | 1 + pom.xml | 10 ++ 26 files changed, 632 insertions(+), 105 deletions(-) create mode 100644 modules/platform/nuxeo-mail-amazon-ses/pom.xml create mode 100644 modules/platform/nuxeo-mail-amazon-ses/src/main/java/org/nuxeo/mail/amazon/ses/SESMailSender.java create mode 100755 modules/platform/nuxeo-mail-amazon-ses/src/main/resources/META-INF/MANIFEST.MF create mode 100644 modules/platform/nuxeo-mail-amazon-ses/src/test/java/org/nuxeo/mail/amazon/ses/SESFeature.java create mode 100644 modules/platform/nuxeo-mail-amazon-ses/src/test/java/org/nuxeo/mail/amazon/ses/TestSESMailSender.java create mode 100755 modules/platform/nuxeo-mail-amazon-ses/src/test/resources/META-INF/MANIFEST.MF create mode 100644 modules/platform/nuxeo-mail-amazon-ses/src/test/resources/OSGI-INF/test-ses-override-default-sender.xml create mode 100644 modules/platform/nuxeo-mail-amazon-ses/src/test/resources/OSGI-INF/test-ses-wrong-sender-contrib.xml create mode 100644 modules/platform/nuxeo-mail/src/main/java/org/nuxeo/mail/MimeMessageHelper.java create mode 100644 modules/platform/nuxeo-mail/src/test/java/org/nuxeo/mail/TestMimeMessageHelper.java create mode 100644 packages/nuxeo-mail-amazon-ses-package/pom.xml create mode 100644 packages/nuxeo-mail-amazon-ses-package/src/main/resources/install/templates/ses/nuxeo.defaults create mode 100644 packages/nuxeo-mail-amazon-ses-package/src/main/resources/install/templates/ses/nxserver/config/ses-sender-config.xml create mode 100644 packages/nuxeo-mail-amazon-ses-package/src/main/resources/package.xml diff --git a/ci/Jenkinsfiles/build.groovy b/ci/Jenkinsfiles/build.groovy index b030e98e33e..8358d7f6fcd 100644 --- a/ci/Jenkinsfiles/build.groovy +++ b/ci/Jenkinsfiles/build.groovy @@ -219,6 +219,7 @@ pipeline { AWS_REGION = 'eu-west-3' AWS_ROLE_ARN = 'arn:aws:iam::783725821734:role/nuxeo-s3directupload-role' AWS_CREDENTIALS_SECRET = 'aws-credentials' + AWS_SES_MAIL_SENDER = 'platform@hyland.com' GITHUB_WORKFLOW_DOCKER_SCAN = 'docker-image-scan.yaml' } diff --git a/modules/core/nuxeo-core-binarymanager-cloud/nuxeo-core-binarymanager-s3/src/main/java/org/nuxeo/ecm/blob/s3/S3BlobStoreConfiguration.java b/modules/core/nuxeo-core-binarymanager-cloud/nuxeo-core-binarymanager-s3/src/main/java/org/nuxeo/ecm/blob/s3/S3BlobStoreConfiguration.java index c836aa3dbf1..d2d0dfc3f5a 100644 --- a/modules/core/nuxeo-core-binarymanager-cloud/nuxeo-core-binarymanager-s3/src/main/java/org/nuxeo/ecm/blob/s3/S3BlobStoreConfiguration.java +++ b/modules/core/nuxeo-core-binarymanager-cloud/nuxeo-core-binarymanager-s3/src/main/java/org/nuxeo/ecm/blob/s3/S3BlobStoreConfiguration.java @@ -44,7 +44,6 @@ import java.util.concurrent.Executors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.nuxeo.common.Environment; import org.nuxeo.ecm.blob.CloudBlobStoreConfiguration; import org.nuxeo.ecm.core.api.NuxeoException; import org.nuxeo.ecm.core.storage.sql.S3Utils; @@ -464,11 +463,6 @@ public class S3BlobStoreConfiguration extends CloudBlobStoreConfiguration { } protected ClientConfiguration getClientConfiguration() { - boolean proxyDisabled = Framework.isBooleanPropertyTrue(DISABLE_PROXY_PROPERTY); - String proxyHost = Framework.getProperty(Environment.NUXEO_HTTP_PROXY_HOST); - String proxyPort = Framework.getProperty(Environment.NUXEO_HTTP_PROXY_PORT); - String proxyLogin = Framework.getProperty(Environment.NUXEO_HTTP_PROXY_LOGIN); - String proxyPassword = Framework.getProperty(Environment.NUXEO_HTTP_PROXY_PASSWORD); int maxConnections = getIntProperty(CONNECTION_MAX_PROPERTY); int maxErrorRetry = getIntProperty(CONNECTION_RETRY_PROPERTY); int connectionTimeout = getIntProperty(CONNECTION_TIMEOUT_PROPERTY); @@ -476,20 +470,6 @@ public class S3BlobStoreConfiguration extends CloudBlobStoreConfiguration { String userAgentPrefix = getProperty(USER_AGENT_PREFIX_PROPERTY); String userAgentSuffix = getProperty(USER_AGENT_SUFFIX_PROPERTY); ClientConfiguration clientConfiguration = new ClientConfiguration(); - if (!proxyDisabled) { - if (isNotBlank(proxyHost)) { - clientConfiguration.setProxyHost(proxyHost); - } - if (isNotBlank(proxyPort)) { - clientConfiguration.setProxyPort(Integer.parseInt(proxyPort)); - } - if (isNotBlank(proxyLogin)) { - clientConfiguration.setProxyUsername(proxyLogin); - } - if (proxyPassword != null) { // could be blank - clientConfiguration.setProxyPassword(proxyPassword); - } - } if (maxConnections > 0) { clientConfiguration.setMaxConnections(maxConnections); } @@ -510,6 +490,9 @@ public class S3BlobStoreConfiguration extends CloudBlobStoreConfiguration { } AWSConfigurationService service = Framework.getService(AWSConfigurationService.class); if (service != null) { + if (Framework.isBooleanPropertyFalse(DISABLE_PROXY_PROPERTY)) { + service.configureProxy(clientConfiguration); + } service.configureSSL(clientConfiguration); } return clientConfiguration; diff --git a/modules/core/nuxeo-core-binarymanager-cloud/nuxeo-core-binarymanager-s3/src/main/java/org/nuxeo/ecm/core/storage/sql/S3BinaryManager.java b/modules/core/nuxeo-core-binarymanager-cloud/nuxeo-core-binarymanager-s3/src/main/java/org/nuxeo/ecm/core/storage/sql/S3BinaryManager.java index f6651f4d3d4..5dfc5837a69 100644 --- a/modules/core/nuxeo-core-binarymanager-cloud/nuxeo-core-binarymanager-s3/src/main/java/org/nuxeo/ecm/core/storage/sql/S3BinaryManager.java +++ b/modules/core/nuxeo-core-binarymanager-cloud/nuxeo-core-binarymanager-s3/src/main/java/org/nuxeo/ecm/core/storage/sql/S3BinaryManager.java @@ -47,7 +47,7 @@ import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.nuxeo.common.Environment; + import org.nuxeo.ecm.blob.AbstractBinaryGarbageCollector; import org.nuxeo.ecm.blob.AbstractCloudBinaryManager; import org.nuxeo.ecm.blob.s3.S3ManagedTransfer; @@ -249,12 +249,6 @@ public class S3BinaryManager extends AbstractCloudBinaryManager implements S3Man String awsSecret = getProperty(AWS_SECRET_PROPERTY); String awsToken = getProperty(AWS_SESSION_TOKEN_PROPERTY); - boolean proxyDisabled = Framework.isBooleanPropertyTrue(DISABLE_PROXY_PROPERTY); - String proxyHost = Framework.getProperty(Environment.NUXEO_HTTP_PROXY_HOST); - String proxyPort = Framework.getProperty(Environment.NUXEO_HTTP_PROXY_PORT); - String proxyLogin = Framework.getProperty(Environment.NUXEO_HTTP_PROXY_LOGIN); - String proxyPassword = Framework.getProperty(Environment.NUXEO_HTTP_PROXY_PASSWORD); - int maxConnections = getIntProperty(CONNECTION_MAX_PROPERTY); int maxErrorRetry = getIntProperty(CONNECTION_RETRY_PROPERTY); int connectionTimeout = getIntProperty(CONNECTION_TIMEOUT_PROPERTY); @@ -297,20 +291,6 @@ public class S3BinaryManager extends AbstractCloudBinaryManager implements S3Man // set up client configuration clientConfiguration = new ClientConfiguration(); - if (!proxyDisabled) { - if (isNotBlank(proxyHost)) { - clientConfiguration.setProxyHost(proxyHost); - } - if (isNotBlank(proxyPort)) { - clientConfiguration.setProxyPort(Integer.parseInt(proxyPort)); - } - if (isNotBlank(proxyLogin)) { - clientConfiguration.setProxyUsername(proxyLogin); - } - if (proxyPassword != null) { // could be blank - clientConfiguration.setProxyPassword(proxyPassword); - } - } if (maxConnections > 0) { clientConfiguration.setMaxConnections(maxConnections); } @@ -332,6 +312,9 @@ public class S3BinaryManager extends AbstractCloudBinaryManager implements S3Man AWSConfigurationService service = Framework.getService(AWSConfigurationService.class); if (service != null) { + if (Framework.isBooleanPropertyFalse(DISABLE_PROXY_PROPERTY)) { + service.configureProxy(clientConfiguration); + } service.configureSSL(clientConfiguration); } diff --git a/modules/platform/nuxeo-automation/nuxeo-automation-core/src/main/java/org/nuxeo/ecm/automation/core/mail/Mailer.java b/modules/platform/nuxeo-automation/nuxeo-automation-core/src/main/java/org/nuxeo/ecm/automation/core/mail/Mailer.java index b3bac971602..d2ebecaa7e1 100644 --- a/modules/platform/nuxeo-automation/nuxeo-automation-core/src/main/java/org/nuxeo/ecm/automation/core/mail/Mailer.java +++ b/modules/platform/nuxeo-automation/nuxeo-automation-core/src/main/java/org/nuxeo/ecm/automation/core/mail/Mailer.java @@ -34,7 +34,6 @@ import javax.mail.Transport; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeMessage; -import org.nuxeo.ecm.core.api.NuxeoException; import org.nuxeo.mail.MailException; import org.nuxeo.mail.MailMessage; import org.nuxeo.mail.MailSender; diff --git a/modules/platform/nuxeo-mail-amazon-ses/pom.xml b/modules/platform/nuxeo-mail-amazon-ses/pom.xml new file mode 100644 index 00000000000..03ef29af0af --- /dev/null +++ b/modules/platform/nuxeo-mail-amazon-ses/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + + + org.nuxeo.ecm.platform + nuxeo-platform-parent + 2021.46-SNAPSHOT + + + nuxeo-mail-amazon-ses + Nuxeo Mail Amazon SES + Nuxeo Mail addon for Amazon SES + + + + org.nuxeo.ecm.platform + nuxeo-mail + + + org.nuxeo.runtime + nuxeo-runtime-aws + + + com.amazonaws + aws-java-sdk-ses + + + + diff --git a/modules/platform/nuxeo-mail-amazon-ses/src/main/java/org/nuxeo/mail/amazon/ses/SESMailSender.java b/modules/platform/nuxeo-mail-amazon-ses/src/main/java/org/nuxeo/mail/amazon/ses/SESMailSender.java new file mode 100644 index 00000000000..b2af2ced889 --- /dev/null +++ b/modules/platform/nuxeo-mail-amazon-ses/src/main/java/org/nuxeo/mail/amazon/ses/SESMailSender.java @@ -0,0 +1,118 @@ +/* + * (C) Copyright 2023 Nuxeo (http://nuxeo.com/) and others. + * + * 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.nuxeo.mail.amazon.ses; + +import static org.nuxeo.mail.MailConstants.CONFIGURATION_MAIL_FROM; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.nuxeo.mail.MailException; +import org.nuxeo.mail.MailMessage; +import org.nuxeo.mail.MailSender; +import org.nuxeo.mail.MailSenderDescriptor; +import org.nuxeo.mail.MimeMessageHelper; +import org.nuxeo.runtime.api.Framework; +import org.nuxeo.runtime.aws.AWSConfigurationService; +import org.nuxeo.runtime.aws.NuxeoAWSCredentialsProvider; +import org.nuxeo.runtime.aws.NuxeoAWSRegionProvider; + +import com.amazonaws.ClientConfiguration; +import com.amazonaws.services.simpleemail.AmazonSimpleEmailService; +import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClientBuilder; +import com.amazonaws.services.simpleemail.model.AmazonSimpleEmailServiceException; +import com.amazonaws.services.simpleemail.model.RawMessage; +import com.amazonaws.services.simpleemail.model.SendRawEmailRequest; + +/** + * Implementation of {@link MailSender} building {@link RawMessage}s and sending them via Amazon SES. + * + * @since 2023.4 + */ +public class SESMailSender implements MailSender { + + private static final Logger log = LogManager.getLogger(SESMailSender.class); + + protected static final String AWS_CONFIGURATION_ID_KEY = "awsConfigurationId"; + + protected final String defaultMailFrom; + + protected final AmazonSimpleEmailService client; + + public SESMailSender(MailSenderDescriptor descriptor) { + var configurationId = descriptor.getProperties().get(AWS_CONFIGURATION_ID_KEY); + defaultMailFrom = descriptor.getProperties().get(CONFIGURATION_MAIL_FROM); + var credentialsProvider = new NuxeoAWSCredentialsProvider(configurationId); + var regionProvider = new NuxeoAWSRegionProvider(configurationId); + + var clientConfiguration = new ClientConfiguration(); + var awsConfigurationService = Framework.getService(AWSConfigurationService.class); + awsConfigurationService.configureSSL(clientConfiguration); + awsConfigurationService.configureProxy(clientConfiguration); + + client = AmazonSimpleEmailServiceClientBuilder.standard() + .withClientConfiguration(clientConfiguration) + .withCredentials(credentialsProvider) + .withRegion(regionProvider.getRegion()) + .build(); + } + + @Override + public void sendMail(MailMessage message) { + try { + var mimeMessage = buildMimeMessage(message); + + var outputStream = new ByteArrayOutputStream(); + mimeMessage.writeTo(outputStream); + var rawMessage = new RawMessage(ByteBuffer.wrap(outputStream.toByteArray())); + var sendRawEmailRequest = new SendRawEmailRequest(rawMessage); + + var response = client.sendRawEmail(sendRawEmailRequest); + log.debug("Successfully sent mail with Amazon SES, messageId: {}", response.getMessageId()); + } catch (MessagingException | IOException | AmazonSimpleEmailServiceException e) { + throw new MailException("An error occurred while sending a mail with Amazon SES", e); + } + } + + protected MimeMessage buildMimeMessage(MailMessage message) throws MessagingException { + var effectiveMessage = setMissingMandatoryValues(message); + return MimeMessageHelper.composeMimeMessage(effectiveMessage); + } + + protected MailMessage setMissingMandatoryValues(MailMessage message) { + boolean addFrom = message.getFroms().isEmpty(); + boolean addContent = message.getContent() == null; + if (addFrom || addContent) { + var builder = new MailMessage.Builder(message); + if (addFrom) { + builder.from(defaultMailFrom); + } + if (addContent) { + builder.content(""); + } + return builder.build(); + } + return message; + } + +} diff --git a/modules/platform/nuxeo-mail-amazon-ses/src/main/resources/META-INF/MANIFEST.MF b/modules/platform/nuxeo-mail-amazon-ses/src/main/resources/META-INF/MANIFEST.MF new file mode 100755 index 00000000000..19d60bcc4ae --- /dev/null +++ b/modules/platform/nuxeo-mail-amazon-ses/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,5 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Nuxeo Mail for Amazon SES +Bundle-SymbolicName: org.nuxeo.mail.amazon.ses;singleton:=true +Bundle-Vendor: Nuxeo diff --git a/modules/platform/nuxeo-mail-amazon-ses/src/test/java/org/nuxeo/mail/amazon/ses/SESFeature.java b/modules/platform/nuxeo-mail-amazon-ses/src/test/java/org/nuxeo/mail/amazon/ses/SESFeature.java new file mode 100644 index 00000000000..a1a54060168 --- /dev/null +++ b/modules/platform/nuxeo-mail-amazon-ses/src/test/java/org/nuxeo/mail/amazon/ses/SESFeature.java @@ -0,0 +1,58 @@ +/* + * (C) Copyright 2023 Nuxeo (http://nuxeo.com/) and others. + * + * 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.nuxeo.mail.amazon.ses; + +import static com.amazonaws.SDKGlobalConfiguration.ACCESS_KEY_ENV_VAR; +import static com.amazonaws.SDKGlobalConfiguration.ALTERNATE_SECRET_KEY_ENV_VAR; +import static com.amazonaws.SDKGlobalConfiguration.AWS_REGION_ENV_VAR; +import static org.apache.commons.lang3.StringUtils.isNoneBlank; +import static org.junit.Assume.assumeTrue; +import static org.nuxeo.mail.MailConstants.CONFIGURATION_MAIL_FROM; + +import org.nuxeo.runtime.api.Framework; +import org.nuxeo.runtime.test.runner.Deploy; +import org.nuxeo.runtime.test.runner.Features; +import org.nuxeo.runtime.test.runner.FeaturesRunner; +import org.nuxeo.runtime.test.runner.RunnerFeature; +import org.nuxeo.runtime.test.runner.RuntimeFeature; +import org.nuxeo.runtime.test.runner.RuntimeHarness; + +/** + * @since 2023.4 + */ +@Features(RuntimeFeature.class) +@Deploy("org.nuxeo.runtime.aws") +@Deploy("org.nuxeo.mail") +@Deploy("org.nuxeo.mail.amazon.ses") +public class SESFeature implements RunnerFeature { + + protected static final String AWS_SES_MAIL_SENDER_ENV_VAR = "AWS_SES_MAIL_SENDER"; + + @Override + public void start(FeaturesRunner runner) throws Exception { + String verifiedSender = System.getenv(AWS_SES_MAIL_SENDER_ENV_VAR); + assumeTrue("AWS credentials, region and a verified mail are missing in test configuration", + isNoneBlank(verifiedSender, System.getenv(ACCESS_KEY_ENV_VAR), + System.getenv(ALTERNATE_SECRET_KEY_ENV_VAR), System.getenv(AWS_REGION_ENV_VAR))); + + Framework.getProperties().setProperty(CONFIGURATION_MAIL_FROM, verifiedSender); + RuntimeHarness harness = runner.getFeature(RuntimeFeature.class).getHarness(); + harness.deployContrib("org.nuxeo.mail.amazon.ses.test", "OSGI-INF/test-ses-override-default-sender.xml"); + } + +} diff --git a/modules/platform/nuxeo-mail-amazon-ses/src/test/java/org/nuxeo/mail/amazon/ses/TestSESMailSender.java b/modules/platform/nuxeo-mail-amazon-ses/src/test/java/org/nuxeo/mail/amazon/ses/TestSESMailSender.java new file mode 100644 index 00000000000..520ec87d492 --- /dev/null +++ b/modules/platform/nuxeo-mail-amazon-ses/src/test/java/org/nuxeo/mail/amazon/ses/TestSESMailSender.java @@ -0,0 +1,90 @@ +/* + * (C) Copyright 2023 Nuxeo (http://nuxeo.com/) and others. + * + * 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.nuxeo.mail.amazon.ses; + +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import javax.inject.Inject; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.nuxeo.mail.MailException; +import org.nuxeo.mail.MailMessage; +import org.nuxeo.mail.MailService; +import org.nuxeo.runtime.test.runner.Deploy; +import org.nuxeo.runtime.test.runner.Features; +import org.nuxeo.runtime.test.runner.FeaturesRunner; + +import com.amazonaws.services.simpleemail.model.AmazonSimpleEmailServiceException; + +/** + * @since 2023.4 + */ +@RunWith(FeaturesRunner.class) +@Features(SESFeature.class) +public class TestSESMailSender { + + protected static final String SES_SUCCESS_SIMULATOR_MAIL = "success@simulator.amazonses.com"; + + @Inject + protected MailService mailService; + + @Test + public void testSendMailWithDefaultFrom() { + var mailMessage = getBaseMessageBuilder().build(); // no explicit from + + // If no exception is thrown, the message has been handled successfully + mailService.sendMail(mailMessage); + } + + @Test + public void testSendMailWithExplicitFrom() { + var mailMessage = getBaseMessageBuilder().from("foo@bar.com").build(); + + // The explicit from should be used and fail as it is not verified at SES + assertSESFail(mailMessage); + } + + @Test + public void testSendMailMalformed() { + var mailMessage = getBaseMessageBuilder().cc("malformedAddress").build(); + + // Should fail as the recipients can't hold malformed addresses + assertSESFail(mailMessage); + } + + @Test + @Deploy("org.nuxeo.mail.amazon.ses.test:OSGI-INF/test-ses-wrong-sender-contrib.xml") + public void testSendMailWithWrongCredentials() { + var mailMessage = getBaseMessageBuilder().senderName("wrongSender").build(); + + // Credentials from the wrongSender were successfully used + assertSESFail(mailMessage); + } + + protected MailMessage.Builder getBaseMessageBuilder() { + return new MailMessage.Builder(SES_SUCCESS_SIMULATOR_MAIL); + } + + protected void assertSESFail(MailMessage mailMessage) { + var t = assertThrows("An error occurred while sending a mail", MailException.class, + () -> mailService.sendMail(mailMessage)); + assertTrue(t.getCause() instanceof AmazonSimpleEmailServiceException); + } +} diff --git a/modules/platform/nuxeo-mail-amazon-ses/src/test/resources/META-INF/MANIFEST.MF b/modules/platform/nuxeo-mail-amazon-ses/src/test/resources/META-INF/MANIFEST.MF new file mode 100755 index 00000000000..b1e3e84bd1f --- /dev/null +++ b/modules/platform/nuxeo-mail-amazon-ses/src/test/resources/META-INF/MANIFEST.MF @@ -0,0 +1,5 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Nuxeo Mail for Amazon SES Test +Bundle-SymbolicName: org.nuxeo.mail.amazon.ses.test;singleton:=true +Bundle-Vendor: Nuxeo diff --git a/modules/platform/nuxeo-mail-amazon-ses/src/test/resources/OSGI-INF/test-ses-override-default-sender.xml b/modules/platform/nuxeo-mail-amazon-ses/src/test/resources/OSGI-INF/test-ses-override-default-sender.xml new file mode 100644 index 00000000000..7478655c601 --- /dev/null +++ b/modules/platform/nuxeo-mail-amazon-ses/src/test/resources/OSGI-INF/test-ses-override-default-sender.xml @@ -0,0 +1,8 @@ + + + + + ${mail.from} + + + diff --git a/modules/platform/nuxeo-mail-amazon-ses/src/test/resources/OSGI-INF/test-ses-wrong-sender-contrib.xml b/modules/platform/nuxeo-mail-amazon-ses/src/test/resources/OSGI-INF/test-ses-wrong-sender-contrib.xml new file mode 100644 index 00000000000..8397641e29b --- /dev/null +++ b/modules/platform/nuxeo-mail-amazon-ses/src/test/resources/OSGI-INF/test-ses-wrong-sender-contrib.xml @@ -0,0 +1,16 @@ + + + org.nuxeo.mail.sender.amazon.ses.test + + + wrongConfig + ${mail.from} + + + + + foo + bar + + + diff --git a/modules/platform/nuxeo-mail/src/main/java/org/nuxeo/mail/BlobDataSource.java b/modules/platform/nuxeo-mail/src/main/java/org/nuxeo/mail/BlobDataSource.java index ff9b23b2027..de3ce8aee78 100644 --- a/modules/platform/nuxeo-mail/src/main/java/org/nuxeo/mail/BlobDataSource.java +++ b/modules/platform/nuxeo-mail/src/main/java/org/nuxeo/mail/BlobDataSource.java @@ -31,6 +31,10 @@ import org.nuxeo.ecm.core.api.Blob; */ public class BlobDataSource implements DataSource { + public Blob getBlob() { + return blob; + } + protected Blob blob; public BlobDataSource(Blob blob) { diff --git a/modules/platform/nuxeo-mail/src/main/java/org/nuxeo/mail/MailSenderDescriptor.java b/modules/platform/nuxeo-mail/src/main/java/org/nuxeo/mail/MailSenderDescriptor.java index dc6e2f813b5..f3158a76f56 100644 --- a/modules/platform/nuxeo-mail/src/main/java/org/nuxeo/mail/MailSenderDescriptor.java +++ b/modules/platform/nuxeo-mail/src/main/java/org/nuxeo/mail/MailSenderDescriptor.java @@ -46,6 +46,18 @@ public class MailSenderDescriptor implements Descriptor { return name; } + public String getName() { + return name; + } + + public Class getKlass() { + return klass; + } + + public Map getProperties() { + return properties; + } + public MailSender newInstance() { if (!MailSender.class.isAssignableFrom(klass)) { throw new IllegalArgumentException( diff --git a/modules/platform/nuxeo-mail/src/main/java/org/nuxeo/mail/MimeMessageHelper.java b/modules/platform/nuxeo-mail/src/main/java/org/nuxeo/mail/MimeMessageHelper.java new file mode 100644 index 00000000000..6bb41d7752f --- /dev/null +++ b/modules/platform/nuxeo-mail/src/main/java/org/nuxeo/mail/MimeMessageHelper.java @@ -0,0 +1,100 @@ +/* + * (C) Copyright 2023 Nuxeo (http://nuxeo.com/) and others. + * + * 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.nuxeo.mail; + +import java.util.List; + +import javax.activation.DataHandler; +import javax.mail.Address; +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.internet.AddressException; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; + +import org.nuxeo.ecm.core.api.Blob; + +/** + * Helper to convert {@link MailMessage}s to {@link MimeMessage}. + * + * @since 2023.4 + */ +public class MimeMessageHelper { + + public static MimeMessage composeMimeMessage(MailMessage msg) throws MessagingException { + return composeMimeMessage(msg, null); + } + + public static MimeMessage composeMimeMessage(MailMessage msg, Session session) throws MessagingException { + var mail = new MimeMessage(session); + Object content = msg.getContent(); + String contentType = msg.getContentType(); + if (msg.hasAttachments()) { + MimeBodyPart body = null; + if (content != null) { + body = new MimeBodyPart(); + body.setContent(msg.getContent(), contentType); + } + MimeMultipart bodyParts = assembleMultiPart(body, msg.getAttachments()); + mail.setContent(bodyParts); + } else if (content != null) { + mail.setContent(msg.getContent(), contentType); + } + return fillDetails(msg, mail); + } + + protected static MimeMultipart assembleMultiPart(MimeBodyPart body, List attachments) + throws MessagingException { + var mp = new MimeMultipart(); + if (body != null) { + mp.addBodyPart(body); + } + for (Blob blob : attachments) { + var a = new MimeBodyPart(); + a.setDataHandler(new DataHandler(new BlobDataSource(blob))); + a.setFileName(blob.getFilename()); + mp.addBodyPart(a); + } + return mp; + } + + protected static MimeMessage fillDetails(MailMessage msg, MimeMessage mimeMsg) throws MessagingException { + mimeMsg.addFrom(toAddresses(msg.getFroms())); + mimeMsg.setRecipients(MimeMessage.RecipientType.TO, toAddresses(msg.getTos())); + mimeMsg.setRecipients(MimeMessage.RecipientType.CC, toAddresses(msg.getCcs())); + mimeMsg.setRecipients(MimeMessage.RecipientType.BCC, toAddresses(msg.getBccs())); + mimeMsg.setReplyTo(toAddresses(msg.getReplyTos())); + mimeMsg.setSentDate(msg.getDate()); + mimeMsg.setSubject(msg.getSubject(), msg.getSubjectCharset().name()); + + return mimeMsg; + } + + protected static Address[] toAddresses(List list) { + return list.stream().map(a -> { + try { + return new InternetAddress(a); + } catch (AddressException e) { + throw new MailException("Could not parse mail address: " + a, e); + } + }).toArray(InternetAddress[]::new); + } + +} diff --git a/modules/platform/nuxeo-mail/src/main/java/org/nuxeo/mail/SMTPMailSender.java b/modules/platform/nuxeo-mail/src/main/java/org/nuxeo/mail/SMTPMailSender.java index 9b45550fc8f..ff778336a36 100644 --- a/modules/platform/nuxeo-mail/src/main/java/org/nuxeo/mail/SMTPMailSender.java +++ b/modules/platform/nuxeo-mail/src/main/java/org/nuxeo/mail/SMTPMailSender.java @@ -18,20 +18,10 @@ package org.nuxeo.mail; import static org.nuxeo.mail.MailConstants.CONFIGURATION_MAIL_FROM; -import java.util.List; - -import javax.activation.DataHandler; -import javax.mail.Address; import javax.mail.MessagingException; import javax.mail.Session; import javax.mail.Transport; -import javax.mail.internet.AddressException; -import javax.mail.internet.InternetAddress; -import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeMessage; -import javax.mail.internet.MimeMultipart; - -import org.nuxeo.ecm.core.api.Blob; /** * Default implementation of {@link MailSender} building {@link MimeMessage}s and sending via SMTP protocol. @@ -60,61 +50,9 @@ public class SMTPMailSender implements MailSender { } } - protected MimeMessage composeMimeMessage(MailMessage msg) throws MessagingException { - var mail = new MimeMessage(session); - Object content = msg.getContent(); - String contentType = msg.getContentType(); - if (msg.hasAttachments()) { - MimeBodyPart body = null; - if (content != null) { - body = new MimeBodyPart(); - body.setContent(msg.getContent(), contentType); - } - MimeMultipart bodyParts = assembleMultiPart(body, msg.getAttachments()); - mail.setContent(bodyParts); - } else if (content != null) { - mail.setContent(msg.getContent(), contentType); - } - - return mail; - } - - protected MimeMultipart assembleMultiPart(MimeBodyPart body, List attachments) throws MessagingException { - var mp = new MimeMultipart(); - if (body != null) { - mp.addBodyPart(body); - } - for (Blob blob : attachments) { - MimeBodyPart a = new MimeBodyPart(); - a.setDataHandler(new DataHandler(new BlobDataSource(blob))); - a.setFileName(blob.getFilename()); - mp.addBodyPart(a); - } - return mp; - } - - protected Address[] toAddresses(List list) { - return list.stream().map(a -> { - try { - return new InternetAddress(a); - } catch (AddressException e) { - throw new MailException("Could not parse mail address: " + a, e); - } - }).toArray(InternetAddress[]::new); - } - protected MimeMessage buildMimeMessage(MailMessage message) throws MessagingException { var effectiveMessage = message.getFroms().isEmpty() ? addFroms(message) : message; - MimeMessage mimeMsg = composeMimeMessage(effectiveMessage); - - mimeMsg.addFrom(toAddresses(effectiveMessage.getFroms())); - mimeMsg.setRecipients(MimeMessage.RecipientType.TO, toAddresses(effectiveMessage.getTos())); - mimeMsg.setRecipients(MimeMessage.RecipientType.CC, toAddresses(effectiveMessage.getCcs())); - mimeMsg.setRecipients(MimeMessage.RecipientType.BCC, toAddresses(effectiveMessage.getBccs())); - mimeMsg.setReplyTo(toAddresses(effectiveMessage.getReplyTos())); - mimeMsg.setSentDate(effectiveMessage.getDate()); - mimeMsg.setSubject(effectiveMessage.getSubject(), effectiveMessage.getSubjectCharset().toString()); - return mimeMsg; + return MimeMessageHelper.composeMimeMessage(effectiveMessage, session); } protected MailMessage addFroms(MailMessage message) { diff --git a/modules/platform/nuxeo-mail/src/test/java/org/nuxeo/mail/TestMimeMessageHelper.java b/modules/platform/nuxeo-mail/src/test/java/org/nuxeo/mail/TestMimeMessageHelper.java new file mode 100644 index 00000000000..f56f7187921 --- /dev/null +++ b/modules/platform/nuxeo-mail/src/test/java/org/nuxeo/mail/TestMimeMessageHelper.java @@ -0,0 +1,84 @@ +/* + * (C) Copyright 2023 Nuxeo (http://nuxeo.com/) and others. + * + * 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.nuxeo.mail; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; + +import javax.mail.BodyPart; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMultipart; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.nuxeo.ecm.core.api.Blobs; +import org.nuxeo.runtime.test.runner.Features; +import org.nuxeo.runtime.test.runner.FeaturesRunner; +import org.nuxeo.runtime.test.runner.RuntimeFeature; + +/** + * @since 2023.4 + */ +@RunWith(FeaturesRunner.class) +@Features(RuntimeFeature.class) +public class TestMimeMessageHelper { + + @Test + public void testMailMessageToMimeMessageConversion() throws MessagingException, IOException { + var blob = Blobs.createBlob("def", "text/plain", "UTF-8", "abc"); + var mailMessage = new MailMessage.Builder("to@nx.com", "to-other@nx.com").from("from@nx.com") + .cc("cc@nx.com") + .bcc("bcc@nx.com") + .replyTo("reply@nx.com") + .attachments(blob) + .subject("test") + .content("some content") + .build(); + + var mimeMessage = MimeMessageHelper.composeMimeMessage(mailMessage); + + var tos = mimeMessage.getRecipients(Message.RecipientType.TO); + assertEquals(2, tos.length); + assertEquals("to@nx.com", tos[0].toString()); + assertEquals("to-other@nx.com", tos[1].toString()); + assertEquals("from@nx.com", mimeMessage.getFrom()[0].toString()); + assertEquals("cc@nx.com", mimeMessage.getRecipients(Message.RecipientType.CC)[0].toString()); + assertEquals("bcc@nx.com", mimeMessage.getRecipients(Message.RecipientType.BCC)[0].toString()); + assertEquals("reply@nx.com", mimeMessage.getReplyTo()[0].toString()); + assertEquals("test", mimeMessage.getSubject()); + assertTrue(mimeMessage.getDataHandler().getContentType().startsWith("multipart/mixed")); + + var multipart = (MimeMultipart) mimeMessage.getContent(); + assertEquals(2, multipart.getCount()); + + BodyPart body = multipart.getBodyPart(0); + assertEquals("text/plain; charset=utf-8", body.getDataHandler().getContentType()); + assertEquals("some content", body.getContent()); + + BodyPart attachment = multipart.getBodyPart(1); + var dataSource = (BlobDataSource) attachment.getDataHandler().getDataSource(); + var stringBlob = dataSource.getBlob(); + assertEquals("def", stringBlob.getString()); + assertEquals("text/plain", stringBlob.getMimeType()); + assertEquals("UTF-8", stringBlob.getEncoding()); + assertEquals("abc", stringBlob.getFilename()); + } +} diff --git a/modules/platform/pom.xml b/modules/platform/pom.xml index 0fa2fb40067..30fb4b30330 100644 --- a/modules/platform/pom.xml +++ b/modules/platform/pom.xml @@ -34,6 +34,7 @@ nuxeo-liveconnect nuxeo-localconf-simple nuxeo-mail + nuxeo-mail-amazon-ses nuxeo-multi-tenant-core nuxeo-permissions nuxeo-platform-3d diff --git a/modules/runtime/nuxeo-runtime-aws/src/main/java/org/nuxeo/runtime/aws/AWSConfigurationService.java b/modules/runtime/nuxeo-runtime-aws/src/main/java/org/nuxeo/runtime/aws/AWSConfigurationService.java index 543b7fdbffb..c22378c0e54 100644 --- a/modules/runtime/nuxeo-runtime-aws/src/main/java/org/nuxeo/runtime/aws/AWSConfigurationService.java +++ b/modules/runtime/nuxeo-runtime-aws/src/main/java/org/nuxeo/runtime/aws/AWSConfigurationService.java @@ -66,6 +66,16 @@ public interface AWSConfigurationService { */ void configureSSL(String id, ClientConfiguration config); + /** + * Enriches the given {@link ClientConfiguration} with default proxy configuration. + * + * @param config the configuration to enrich + * @since 2023.4 + */ + default void configureProxy(ClientConfiguration config) { + throw new UnsupportedOperationException(); + } + /** * Gets the AWS Region for the default configuration. * diff --git a/modules/runtime/nuxeo-runtime-aws/src/main/java/org/nuxeo/runtime/aws/AWSConfigurationServiceImpl.java b/modules/runtime/nuxeo-runtime-aws/src/main/java/org/nuxeo/runtime/aws/AWSConfigurationServiceImpl.java index 80be367062c..22b506b7ec9 100644 --- a/modules/runtime/nuxeo-runtime-aws/src/main/java/org/nuxeo/runtime/aws/AWSConfigurationServiceImpl.java +++ b/modules/runtime/nuxeo-runtime-aws/src/main/java/org/nuxeo/runtime/aws/AWSConfigurationServiceImpl.java @@ -36,6 +36,8 @@ import javax.net.ssl.SSLContext; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.ssl.SSLContextBuilder; import org.apache.http.ssl.SSLContexts; +import org.nuxeo.common.Environment; +import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.model.DefaultComponent; import com.amazonaws.ClientConfiguration; @@ -102,6 +104,27 @@ public class AWSConfigurationServiceImpl extends DefaultComponent implements AWS } } + @Override + public void configureProxy(ClientConfiguration config) { + String proxyHost = Framework.getProperty(Environment.NUXEO_HTTP_PROXY_HOST); + String proxyPort = Framework.getProperty(Environment.NUXEO_HTTP_PROXY_PORT); + String proxyLogin = Framework.getProperty(Environment.NUXEO_HTTP_PROXY_LOGIN); + String proxyPassword = Framework.getProperty(Environment.NUXEO_HTTP_PROXY_PASSWORD); + + if (isNotBlank(proxyHost)) { + config.setProxyHost(proxyHost); + } + if (isNotBlank(proxyPort)) { + config.setProxyPort(Integer.parseInt(proxyPort)); + } + if (isNotBlank(proxyLogin)) { + config.setProxyUsername(proxyLogin); + } + if (proxyPassword != null) { // could be blank + config.setProxyPassword(proxyPassword); + } + } + protected SSLContext getSSLContext(AWSConfigurationDescriptor descriptor) { if (descriptor == null) { return null; diff --git a/packages/nuxeo-mail-amazon-ses-package/pom.xml b/packages/nuxeo-mail-amazon-ses-package/pom.xml new file mode 100644 index 00000000000..4123d73caa5 --- /dev/null +++ b/packages/nuxeo-mail-amazon-ses-package/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + org.nuxeo.packages + nuxeo-packages + 2021.46-SNAPSHOT + + + nuxeo-mail-amazon-ses-package + zip + Nuxeo Mail Amazon SES Package + + + + org.nuxeo.ecm.platform + nuxeo-mail-amazon-ses + + + diff --git a/packages/nuxeo-mail-amazon-ses-package/src/main/resources/install/templates/ses/nuxeo.defaults b/packages/nuxeo-mail-amazon-ses-package/src/main/resources/install/templates/ses/nuxeo.defaults new file mode 100644 index 00000000000..e79c6d06c4a --- /dev/null +++ b/packages/nuxeo-mail-amazon-ses-package/src/main/resources/install/templates/ses/nuxeo.defaults @@ -0,0 +1,4 @@ +## DO NOT EDIT THIS FILE, USE nuxeo.conf ## +ses.target=. +nuxeo.template.includes=aws +nuxeo.ses.aws.configuration.id= diff --git a/packages/nuxeo-mail-amazon-ses-package/src/main/resources/install/templates/ses/nxserver/config/ses-sender-config.xml b/packages/nuxeo-mail-amazon-ses-package/src/main/resources/install/templates/ses/nxserver/config/ses-sender-config.xml new file mode 100644 index 00000000000..93cb5da063d --- /dev/null +++ b/packages/nuxeo-mail-amazon-ses-package/src/main/resources/install/templates/ses/nxserver/config/ses-sender-config.xml @@ -0,0 +1,10 @@ + + + org.nuxeo.mail.sender.default.config + + + ${nuxeo.ses.aws.configuration.id} + ${mail.from} + + + diff --git a/packages/nuxeo-mail-amazon-ses-package/src/main/resources/package.xml b/packages/nuxeo-mail-amazon-ses-package/src/main/resources/package.xml new file mode 100644 index 00000000000..031f5d3e7cf --- /dev/null +++ b/packages/nuxeo-mail-amazon-ses-package/src/main/resources/package.xml @@ -0,0 +1,14 @@ + + + Nuxeo Mail Amazon SES Addon + +

The Mail Amazon SES addon allows users to send mails through Amazon SES (Simple Email Service).

+
+ Nuxeo + + @DISTRIBUTION_NAME@ + @VERSION@ + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0 +
diff --git a/packages/pom.xml b/packages/pom.xml index 04b88f1fa86..fd23729679a 100644 --- a/packages/pom.xml +++ b/packages/pom.xml @@ -31,6 +31,7 @@ nuxeo-keycloak-package nuxeo-lang-ext-incomplete-package nuxeo-liveconnect-package + nuxeo-mail-amazon-ses-package nuxeo-microsoft-azure-package nuxeo-multi-tenant-package nuxeo-openid-login-package diff --git a/pom.xml b/pom.xml index 1504dc93ef3..84cb9d1a8a9 100644 --- a/pom.xml +++ b/pom.xml @@ -626,6 +626,11 @@ test ${nuxeo.platform.version} + + org.nuxeo.ecm.platform + nuxeo-mail-amazon-ses + ${nuxeo.platform.version} + org.nuxeo.ecm.platform nuxeo-platform @@ -4509,6 +4514,11 @@ aws-java-sdk-sts ${aws.version} + + com.amazonaws + aws-java-sdk-ses + ${aws.version} + com.google.cloud google-cloud-storage -- 2.41.0