diff --git a/refarch-integrations/README.md b/refarch-integrations/README.md index db260585..280a3158 100644 --- a/refarch-integrations/README.md +++ b/refarch-integrations/README.md @@ -6,6 +6,8 @@ Collection of different integration which can be used as is in RefArch projects. - [s3-integration](./refarch-s3-integration/README.md): For CRUD operations on a s3 storage. Also used for file handling in other integrations. +- [email-integration](./refarch-email-integration/README.md): For sending text and html emails with attachments. Uses + s3-integration for file handling. ## Naming conventions diff --git a/refarch-integrations/pom.xml b/refarch-integrations/pom.xml index 81d66bfa..f4ca8ab1 100644 --- a/refarch-integrations/pom.xml +++ b/refarch-integrations/pom.xml @@ -19,6 +19,7 @@ refarch-s3-integration + refarch-email-integration diff --git a/refarch-integrations/refarch-email-integration/README.md b/refarch-integrations/refarch-email-integration/README.md new file mode 100644 index 00000000..bfb2d331 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/README.md @@ -0,0 +1,34 @@ +# RefArch email integration + +Integration for sending text and html emails with attachments. Uses [s3-integration](../refarch-s3-integration) for file +handling. + +## Usage + +```xml + + + + de.muenchen.refarch + refarch-email-integration-starter + ... + + +``` + +and a [s3-integration starter](../refarch-s3-integration/README.md#usage). + +## Configuration + +### refarch-email-integration-starter + +| Property | Description | Example | +|-----------------------------------------|---------------------------------------------------|------------------------| +| `spring.mail.host` | Host of smtp server used for sending mails. | `mail.example.com` | +| `spring.mail.port` | Host of smtp server used for sending mails. | `1025` | +| `spring.mail.username` | Username of smtp server. | | +| `spring.mail.password` | Password of smtp server. | | +| `refarch.mail.from-address` | Default from address used when sending mails. | `test@example.com` | +| `refarch.mail.default-reply-to-address` | Default reply to address used when sending mails. | `no_reply@example.com` | + +In addition, properties of selected [s3-integration starter](../refarch-s3-integration/README.md#usage). diff --git a/refarch-integrations/refarch-email-integration/pom.xml b/refarch-integrations/refarch-email-integration/pom.xml new file mode 100644 index 00000000..adf903c4 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + + de.muenchen.refarch + refarch-integrations + 1.1.0-SNAPSHOT + + + refarch-email-integration + refarch-email-integration + 1.1.0-SNAPSHOT + pom + + + 3.0.4 + 1.4.0 + + + + refarch-email + refarch-email-integration-core + refarch-email-integration-starter + refarch-email-integration-rest-example + refarch-email-integration-java-example + + diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/pom.xml b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/pom.xml new file mode 100644 index 00000000..6e18f227 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + refarch-email-integration + de.muenchen.refarch + 1.1.0-SNAPSHOT + + + refarch-email-integration-core + + + + de.muenchen.refarch + refarch-s3-integration-client-core + ${project.version} + + + de.muenchen.refarch + refarch-email-starter + ${project.version} + + + org.springframework + spring-web + + + org.apache.pdfbox + jbig2-imageio + ${jbig2-imageio.version} + + + com.github.jai-imageio + jai-imageio-jpeg2000 + ${jai-imageio-jpeg2000.version} + + + + diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/adapter/out/mail/MailAdapter.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/adapter/out/mail/MailAdapter.java new file mode 100644 index 00000000..ab4c8ecc --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/adapter/out/mail/MailAdapter.java @@ -0,0 +1,26 @@ +package de.muenchen.refarch.email.integration.adapter.out.mail; + +import de.muenchen.refarch.email.api.EmailApi; +import de.muenchen.refarch.email.integration.application.port.out.MailOutPort; +import de.muenchen.refarch.email.model.Mail; +import freemarker.template.TemplateException; +import jakarta.mail.MessagingException; +import java.io.IOException; +import java.util.Map; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class MailAdapter implements MailOutPort { + + private final EmailApi emailApi; + + @Override + public void sendMail(Mail mail, String logoPath) throws MessagingException { + this.emailApi.sendMail(mail, logoPath); + } + + @Override + public String getBodyFromTemplate(String templateName, Map content) throws TemplateException, IOException { + return this.emailApi.getBodyFromTemplate(templateName, content); + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/adapter/out/s3/S3Adapter.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/adapter/out/s3/S3Adapter.java new file mode 100644 index 00000000..35714783 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/adapter/out/s3/S3Adapter.java @@ -0,0 +1,73 @@ +package de.muenchen.refarch.email.integration.adapter.out.s3; + +import de.muenchen.refarch.email.integration.application.port.out.LoadMailAttachmentOutPort; +import de.muenchen.refarch.email.integration.domain.exception.LoadAttachmentError; +import de.muenchen.refarch.email.model.FileAttachment; +import de.muenchen.refarch.integration.s3.client.exception.DocumentStorageClientErrorException; +import de.muenchen.refarch.integration.s3.client.exception.DocumentStorageException; +import de.muenchen.refarch.integration.s3.client.exception.DocumentStorageServerErrorException; +import de.muenchen.refarch.integration.s3.client.repository.DocumentStorageFileRepository; +import de.muenchen.refarch.integration.s3.client.repository.DocumentStorageFolderRepository; +import de.muenchen.refarch.integration.s3.client.service.FileValidationService; +import jakarta.mail.util.ByteArrayDataSource; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FilenameUtils; + +@Slf4j +@RequiredArgsConstructor +public class S3Adapter implements LoadMailAttachmentOutPort { + + private final DocumentStorageFileRepository documentStorageFileRepository; + private final DocumentStorageFolderRepository documentStorageFolderRepository; + private final FileValidationService fileValidationService; + + @Override + public List loadAttachments(final List filePaths) { + final List attachments = new ArrayList<>(); + filePaths.forEach(path -> { + if (path.endsWith("/")) { + attachments.addAll(getFilesFromFolder(path)); + } else { + attachments.add(getFile(path)); + } + }); + return attachments; + } + + private List getFilesFromFolder(final String folderPath) { + try { + final List contents = new ArrayList<>(); + final Set filepath; + filepath = documentStorageFolderRepository.getAllFilesInFolderRecursively(folderPath); + if (Objects.isNull(filepath)) + throw new LoadAttachmentError("An folder could not be loaded from url: " + folderPath); + filepath.forEach(file -> contents.add(getFile(file))); + return contents; + } catch (final DocumentStorageException | DocumentStorageServerErrorException | DocumentStorageClientErrorException e) { + throw new LoadAttachmentError("An folder could not be loaded from path: " + folderPath); + } + } + + private FileAttachment getFile(final String filePath) { + try { + final byte[] bytes; + bytes = this.documentStorageFileRepository.getFile(filePath, 3); + final String mimeType = fileValidationService.detectFileType(bytes); + final String filename = FilenameUtils.getName(filePath); + final ByteArrayDataSource file = new ByteArrayDataSource(bytes, mimeType); + + // check if mimeType exists + if (!fileValidationService.isSupported(mimeType)) + throw new LoadAttachmentError("The type of this file is not supported: " + filePath); + + return new FileAttachment(filename, file); + } catch (final DocumentStorageException | DocumentStorageServerErrorException | DocumentStorageClientErrorException e) { + throw new LoadAttachmentError("An file could not be loaded from path: " + filePath); + } + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/port/in/SendMailInPort.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/port/in/SendMailInPort.java new file mode 100644 index 00000000..33bd8a94 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/port/in/SendMailInPort.java @@ -0,0 +1,13 @@ +package de.muenchen.refarch.email.integration.application.port.in; + +import de.muenchen.refarch.email.integration.domain.model.TemplateMail; +import de.muenchen.refarch.email.integration.domain.model.TextMail; +import jakarta.validation.Valid; + +public interface SendMailInPort { + + void sendMailWithText(@Valid final TextMail mail); + + void sendMailWithTemplate(@Valid final TemplateMail mail); + +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/port/out/LoadMailAttachmentOutPort.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/port/out/LoadMailAttachmentOutPort.java new file mode 100644 index 00000000..f56157a4 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/port/out/LoadMailAttachmentOutPort.java @@ -0,0 +1,8 @@ +package de.muenchen.refarch.email.integration.application.port.out; + +import de.muenchen.refarch.email.model.FileAttachment; +import java.util.List; + +public interface LoadMailAttachmentOutPort { + List loadAttachments(final List filePaths); +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/port/out/MailOutPort.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/port/out/MailOutPort.java new file mode 100644 index 00000000..20f8ecb3 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/port/out/MailOutPort.java @@ -0,0 +1,15 @@ +package de.muenchen.refarch.email.integration.application.port.out; + +import de.muenchen.refarch.email.model.Mail; +import freemarker.template.TemplateException; +import jakarta.mail.MessagingException; +import java.io.IOException; +import java.util.Map; + +public interface MailOutPort { + + void sendMail(Mail mail, String logoPath) throws MessagingException; + + String getBodyFromTemplate(String templateName, Map content) throws TemplateException, IOException; + +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/usecase/SendMailUseCase.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/usecase/SendMailUseCase.java new file mode 100644 index 00000000..7b460417 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/usecase/SendMailUseCase.java @@ -0,0 +1,88 @@ +package de.muenchen.refarch.email.integration.application.usecase; + +import de.muenchen.refarch.email.integration.application.port.in.SendMailInPort; +import de.muenchen.refarch.email.integration.application.port.out.LoadMailAttachmentOutPort; +import de.muenchen.refarch.email.integration.application.port.out.MailOutPort; +import de.muenchen.refarch.email.integration.domain.exception.TemplateError; +import de.muenchen.refarch.email.integration.domain.model.BasicMail; +import de.muenchen.refarch.email.integration.domain.model.TemplateMail; +import de.muenchen.refarch.email.integration.domain.model.TextMail; +import de.muenchen.refarch.email.model.FileAttachment; +import de.muenchen.refarch.email.model.Mail; +import freemarker.template.TemplateException; +import jakarta.mail.MessagingException; +import jakarta.validation.Valid; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.mail.MailSendException; +import org.springframework.validation.annotation.Validated; + +@Slf4j +@RequiredArgsConstructor +@Validated +public class SendMailUseCase implements SendMailInPort { + + private final LoadMailAttachmentOutPort loadAttachmentOutPort; + private final MailOutPort mailOutPort; + + /** + * Send a mail. + * + * @param mail mail that is sent + */ + @Override + public void sendMailWithText(@Valid final TextMail mail) { + Mail mailModel = createMail(mail, mail.getBody(), false); + + this.sendMail(mailModel, null); + } + + @Override + public void sendMailWithTemplate(@Valid final TemplateMail mail) throws TemplateError { + // get body from template + try { + Map content = new HashMap<>(mail.getContent()); + String body = this.mailOutPort.getBodyFromTemplate(mail.getTemplate(), content); + + Mail mailModel = createMail(mail, body, true); + + this.sendMail(mailModel, "templates/email-logo.png"); + + } catch (IOException ioException) { + throw new TemplateError("The template " + mail.getTemplate() + " could not be loaded"); + } catch (TemplateException templateException) { + throw new TemplateError(templateException.getMessage()); + } + } + + private Mail createMail(final BasicMail mail, final String body, final boolean htmlBody) { + // load Attachments + List attachments = loadAttachmentOutPort.loadAttachments(mail.getFilePaths()); + + // send mail + return new Mail( + mail.getReceivers(), + mail.getReceiversCc(), + mail.getReceiversBcc(), + mail.getSubject(), + body, + htmlBody, + null, + mail.getReplyTo(), + attachments); + } + + private void sendMail(Mail mailModel, String logoPath) throws MailSendException { + try { + this.mailOutPort.sendMail(mailModel, logoPath); + } catch (final MessagingException ex) { + log.error("Sending mail failed with exception: {}", ex.getMessage()); + throw new MailSendException(ex.getMessage()); + } + } + +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/exception/LoadAttachmentError.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/exception/LoadAttachmentError.java new file mode 100644 index 00000000..ba854d6b --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/exception/LoadAttachmentError.java @@ -0,0 +1,7 @@ +package de.muenchen.refarch.email.integration.domain.exception; + +public class LoadAttachmentError extends Error { + public LoadAttachmentError(final String message) { + super(message); + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/exception/TemplateError.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/exception/TemplateError.java new file mode 100644 index 00000000..7189207f --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/exception/TemplateError.java @@ -0,0 +1,7 @@ +package de.muenchen.refarch.email.integration.domain.exception; + +public class TemplateError extends Error { + public TemplateError(final String message) { + super(message); + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/model/BasicMail.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/model/BasicMail.java new file mode 100644 index 00000000..824e9720 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/model/BasicMail.java @@ -0,0 +1,41 @@ +package de.muenchen.refarch.email.integration.domain.model; + +import jakarta.validation.constraints.NotBlank; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class BasicMail { + /** + * Receiver addresses of the mail, comma separated. + */ + @NotBlank(message = "No receivers given") + private String receivers; + + /** + * CC-Receiver addresses of the mail, comma separated. + */ + private String receiversCc; + + /** + * BCC-Receiver addresses of the mail, comma separated. + */ + private String receiversBcc; + + /** + * Subject of the mail. + */ + @NotBlank(message = "No subject given") + private String subject; + + /** + * Reply to address + */ + private String replyTo; + /** + * List of attachment paths. + */ + private List filePaths; +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/model/TemplateMail.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/model/TemplateMail.java new file mode 100644 index 00000000..56ca2683 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/model/TemplateMail.java @@ -0,0 +1,32 @@ +package de.muenchen.refarch.email.integration.domain.model; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import java.util.List; +import java.util.Map; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public class TemplateMail extends BasicMail { + + /** + * Template of the mail. + */ + @NotBlank(message = "No template given") + private final String template; + + /** + * Bottom body of the mail. + */ + @NotEmpty(message = "No content given") + private final Map content; + + public TemplateMail(String receivers, String receiversCc, String receiversBcc, String subject, String replyTo, List filePaths, + String template, Map content) { + super(receivers, receiversCc, receiversBcc, subject, replyTo, filePaths); + this.template = template; + this.content = content; + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/model/TextMail.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/model/TextMail.java new file mode 100644 index 00000000..315a381c --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/model/TextMail.java @@ -0,0 +1,27 @@ +package de.muenchen.refarch.email.integration.domain.model; + +import jakarta.validation.constraints.NotBlank; +import java.util.List; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +/** + * Object contains all the information needed to send a mail. + */ +@EqualsAndHashCode(callSuper = true) +@Getter +public class TextMail extends BasicMail { + + /** + * Body of the mail. + */ + @NotBlank(message = "No body given") + private final String body; + + public TextMail(String receivers, String receiversCc, String receiversBcc, String subject, String body, String replyTo, + List filePaths) { + super(receivers, receiversCc, receiversBcc, subject, replyTo, filePaths); + this.body = body; + } + +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/java/de/muenchen/refarch/email/integration/adapter/out/mail/MailAdapterTest.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/java/de/muenchen/refarch/email/integration/adapter/out/mail/MailAdapterTest.java new file mode 100644 index 00000000..d39bc472 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/java/de/muenchen/refarch/email/integration/adapter/out/mail/MailAdapterTest.java @@ -0,0 +1,49 @@ +package de.muenchen.refarch.email.integration.adapter.out.mail; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.anyMap; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import de.muenchen.refarch.email.api.EmailApi; +import de.muenchen.refarch.email.model.Mail; +import freemarker.template.TemplateException; +import jakarta.mail.MessagingException; +import java.io.IOException; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class MailAdapterTest { + + private final EmailApi emailApi = mock(EmailApi.class); + + @Test + void sendMail() throws MessagingException { + final MailAdapter mailAdapter = new MailAdapter(emailApi); + final Mail mail = new Mail( + "receivers", + "receiversCc", + "receiversBcc", + "subject", + "body", + false, + null, + "replyTo", + null); + mailAdapter.sendMail(mail, "logoPath"); + verify(emailApi).sendMail(mail, "logoPath"); + } + + @Test + void getBodyFromTemplate() throws TemplateException, IOException { + final MailAdapter mailAdapter = new MailAdapter(emailApi); + when(emailApi.getBodyFromTemplate(anyString(), anyMap())).thenReturn("generated body"); + String body = mailAdapter.getBodyFromTemplate("template", Map.of("key", "value")); + + assertThat(body).isEqualTo("generated body"); + verify(emailApi).getBodyFromTemplate("template", Map.of("key", "value")); + } + +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/java/de/muenchen/refarch/email/integration/adapter/out/s3/S3AdapterTest.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/java/de/muenchen/refarch/email/integration/adapter/out/s3/S3AdapterTest.java new file mode 100644 index 00000000..cff7d114 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/java/de/muenchen/refarch/email/integration/adapter/out/s3/S3AdapterTest.java @@ -0,0 +1,80 @@ +package de.muenchen.refarch.email.integration.adapter.out.s3; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import de.muenchen.refarch.email.integration.domain.exception.LoadAttachmentError; +import de.muenchen.refarch.email.model.FileAttachment; +import de.muenchen.refarch.integration.s3.client.exception.DocumentStorageClientErrorException; +import de.muenchen.refarch.integration.s3.client.exception.DocumentStorageException; +import de.muenchen.refarch.integration.s3.client.exception.DocumentStorageServerErrorException; +import de.muenchen.refarch.integration.s3.client.repository.DocumentStorageFileRepository; +import de.muenchen.refarch.integration.s3.client.repository.DocumentStorageFolderRepository; +import de.muenchen.refarch.integration.s3.client.service.FileValidationService; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ClassPathResource; +import org.springframework.util.unit.DataSize; + +@Slf4j +class S3AdapterTest { + private final DocumentStorageFileRepository documentStorageFileRepository = mock(DocumentStorageFileRepository.class); + private final DocumentStorageFolderRepository documentStorageFolderRepository = mock(DocumentStorageFolderRepository.class); + private final FileValidationService fileValidationService = new FileValidationService(null, DataSize.ofMegabytes(50), DataSize.ofMegabytes(110)); + + private S3Adapter s3Adapter; + + @BeforeEach + void setup() { + s3Adapter = new S3Adapter(documentStorageFileRepository, documentStorageFolderRepository, fileValidationService); + } + + @Test + void testLoadAttachment_DocumentStorageException() + throws DocumentStorageException, DocumentStorageClientErrorException, DocumentStorageServerErrorException { + final String path = "path/to/some-file.txt"; + + // DocumentStorageException + when(documentStorageFileRepository.getFile(eq(path), anyInt())) + .thenThrow(new DocumentStorageException("Some error", new RuntimeException("Some error"))); + assertThatThrownBy(() -> s3Adapter.loadAttachments(List.of(path))) + .isInstanceOf(LoadAttachmentError.class); + } + + @Test + void testLoadAttachment_Success() throws DocumentStorageException, DocumentStorageClientErrorException, DocumentStorageServerErrorException { + final Map files = Map.of( + "test-logo.png", "image/png", + "test-pdf.pdf", "application/pdf", + "test-word.docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"); + + for (final Map.Entry file : files.entrySet()) { + try { + final String path = "files/" + file.getKey(); + final byte[] testFile = new ClassPathResource(path).getInputStream().readAllBytes(); + when(documentStorageFileRepository.getFile(anyString(), anyInt())).thenReturn(testFile); + + final List fileAttachment = this.s3Adapter.loadAttachments(List.of(path)); + + assertThat(Arrays.equals(testFile, fileAttachment.getFirst().file().getInputStream().readAllBytes())).isTrue(); + assertThat(file.getKey()).isEqualTo(fileAttachment.getFirst().fileName()); + assertThat(file.getValue()).isEqualTo(fileAttachment.getFirst().file().getContentType()); + } catch (final IOException e) { + log.warn("Could not read file: {}", file); + fail(e.getMessage()); + } + } + } + +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/java/de/muenchen/refarch/email/integration/application/usecase/SendMailUseCaseTest.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/java/de/muenchen/refarch/email/integration/application/usecase/SendMailUseCaseTest.java new file mode 100644 index 00000000..f43cda9f --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/java/de/muenchen/refarch/email/integration/application/usecase/SendMailUseCaseTest.java @@ -0,0 +1,142 @@ +package de.muenchen.refarch.email.integration.application.usecase; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.catchThrowableOfType; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyMap; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import de.muenchen.refarch.email.integration.application.port.in.SendMailInPort; +import de.muenchen.refarch.email.integration.application.port.out.LoadMailAttachmentOutPort; +import de.muenchen.refarch.email.integration.application.port.out.MailOutPort; +import de.muenchen.refarch.email.integration.domain.exception.TemplateError; +import de.muenchen.refarch.email.integration.domain.model.TemplateMail; +import de.muenchen.refarch.email.integration.domain.model.TextMail; +import de.muenchen.refarch.email.model.FileAttachment; +import de.muenchen.refarch.email.model.Mail; +import freemarker.template.TemplateException; +import jakarta.mail.MessagingException; +import jakarta.mail.util.ByteArrayDataSource; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.mail.MailSendException; + +class SendMailUseCaseTest { + + private final LoadMailAttachmentOutPort loadMailAttachmentOutPort = mock(LoadMailAttachmentOutPort.class); + private final MailOutPort mailOutPort = mock(MailOutPort.class); + private final TextMail mail = new TextMail( + "mailReceiver1@muenchen.de,mailReceiver2@muenchen.de", + "receiverCC@muenchen.de", + "receiverBCC@muenchen.de", + "Test Mail", + "This is a test mail", + "test@muenchen.de", + List.of("folder/file.txt")); + private final TemplateMail templateMail = new TemplateMail( + "mailReceiver1@muenchen.de,mailReceiver2@muenchen.de", + "receiverCC@muenchen.de", + "receiverBCC@muenchen.de", + "Test Mail", + "test@muenchen.de", + List.of("folder/file.txt"), + "template", + Map.of("mail", Map.of())); + private SendMailInPort sendMailInPort; + + @BeforeEach + void setUp() { + this.sendMailInPort = new SendMailUseCase(loadMailAttachmentOutPort, mailOutPort); + } + + @Test + void sendMail() throws MessagingException { + sendMailInPort.sendMailWithText(mail); + final Mail mailOutModel = new Mail( + mail.getReceivers(), + mail.getReceiversCc(), + mail.getReceiversBcc(), + mail.getSubject(), + mail.getBody(), + false, + null, + mail.getReplyTo(), + List.of()); + verify(mailOutPort).sendMail(mailOutModel, null); + } + + @Test + void sendMailWithAttachments() throws MessagingException { + final FileAttachment fileAttachment = new FileAttachment("test.txt", new ByteArrayDataSource("Anhang Inhalt".getBytes(), "text/plain")); + when(loadMailAttachmentOutPort.loadAttachments(List.of("folder/file.txt"))).thenReturn(List.of(fileAttachment)); + + sendMailInPort.sendMailWithText(mail); + final Mail mailOutModel = new Mail( + mail.getReceivers(), + mail.getReceiversCc(), + mail.getReceiversBcc(), + mail.getSubject(), + mail.getBody(), + false, + null, + mail.getReplyTo(), + List.of(fileAttachment)); + verify(mailOutPort).sendMail(mailOutModel, null); + } + + @Test + void sendMailThrowsMailSendException() throws MessagingException { + doThrow(new MessagingException("Test Exception")).when(mailOutPort).sendMail(any(), any()); + assertThatThrownBy(() -> sendMailInPort.sendMailWithText(mail)).isInstanceOf(MailSendException.class); + } + + @Test + void sendMailWithTemplate() throws MessagingException, TemplateException, IOException { + when(mailOutPort.getBodyFromTemplate(anyString(), anyMap())).thenReturn("generated body"); + sendMailInPort.sendMailWithTemplate(templateMail); + final Mail mailOutModel = new Mail( + mail.getReceivers(), + mail.getReceiversCc(), + mail.getReceiversBcc(), + mail.getSubject(), + "generated body", + true, + null, + mail.getReplyTo(), + List.of()); + verify(mailOutPort).sendMail(mailOutModel, "templates/email-logo.png"); + } + + @Test + void sendMailWithTemplateThrowsIOException() throws TemplateException, IOException { + doThrow(new IOException("IO Exception")).when(mailOutPort).getBodyFromTemplate(anyString(), anyMap()); + TemplateError error = catchThrowableOfType(() -> sendMailInPort.sendMailWithTemplate(templateMail), TemplateError.class); + + String expectedMessage = "The template " + templateMail.getTemplate() + " could not be loaded"; + String actualMessage = error.getMessage(); + + assertThat(actualMessage).isEqualTo(expectedMessage); + } + + @Test + void sendMailWithTemplateThrowsTemplateException() throws TemplateException, IOException { + TemplateException templateException = mock(TemplateException.class); + when(templateException.getMessage()).thenReturn("Template Exception Message"); + doThrow(templateException).when(mailOutPort).getBodyFromTemplate(anyString(), anyMap()); + TemplateError error = catchThrowableOfType(() -> sendMailInPort.sendMailWithTemplate(templateMail), TemplateError.class); + + String expectedMessage = "Template Exception Message"; + String actualMessage = error.getMessage(); + + assertThat(actualMessage).isEqualTo(expectedMessage); + } + +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/files/test-logo.png b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/files/test-logo.png new file mode 100644 index 00000000..1db73ea0 Binary files /dev/null and b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/files/test-logo.png differ diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/files/test-pdf.pdf b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/files/test-pdf.pdf new file mode 100644 index 00000000..9ee1f262 Binary files /dev/null and b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/files/test-pdf.pdf differ diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/files/test-word.docx b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/files/test-word.docx new file mode 100644 index 00000000..43f35986 Binary files /dev/null and b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/files/test-word.docx differ diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/logback-test.xml b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/logback-test.xml new file mode 100644 index 00000000..7beed25b --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/logback-test.xml @@ -0,0 +1,29 @@ + + + + + + + + + + %date{yyyy.MM.dd HH:mm:ss.SSS} | %highlight(%level) | [%thread] | %cyan(%logger{0}) | [%file : %line] - + %msg%n + + + + + + + + + + + + + + + diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/pom.xml b/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/pom.xml new file mode 100644 index 00000000..b9d98efb --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + de.muenchen.refarch + refarch-email-integration + 1.1.0-SNAPSHOT + + + refarch-email-integration-java-example + + + de.muenchen.refarch + refarch-email-integration-starter + ${project.version} + + + de.muenchen.refarch + refarch-s3-integration-java-client-starter + ${project.version} + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/java/de/muenchen/refarch/email/integration/EmailJavaExampleApplication.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/java/de/muenchen/refarch/email/integration/EmailJavaExampleApplication.java new file mode 100644 index 00000000..ceb02d27 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/java/de/muenchen/refarch/email/integration/EmailJavaExampleApplication.java @@ -0,0 +1,23 @@ +package de.muenchen.refarch.email.integration; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; + +@SpringBootApplication +@RequiredArgsConstructor +public class EmailJavaExampleApplication { + private final TestService testService; + + public static void main(final String[] args) { + SpringApplication.run(EmailJavaExampleApplication.class, args); + } + + @EventListener(ApplicationReadyEvent.class) + void sendTestMail() { + this.testService.testSendMail(); + System.exit(0); + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/java/de/muenchen/refarch/email/integration/TestService.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/java/de/muenchen/refarch/email/integration/TestService.java new file mode 100644 index 00000000..eebbf175 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/java/de/muenchen/refarch/email/integration/TestService.java @@ -0,0 +1,40 @@ +package de.muenchen.refarch.email.integration; + +import de.muenchen.refarch.email.integration.application.port.in.SendMailInPort; +import de.muenchen.refarch.email.integration.domain.model.TextMail; +import de.muenchen.refarch.integration.s3.client.repository.DocumentStorageFileRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class TestService { + private final SendMailInPort sendMailInPort; + private final DocumentStorageFileRepository documentStorageFileRepository; + + void testSendMail() { + this.uploadTestFile(); + TextMail mail = new TextMail( + "test.receiver@muenchen.de", + null, + null, + "Test", + "This is a test", + null, + List.of("/test/test-pdf.pdf")); + sendMailInPort.sendMailWithText(mail); + log.info("Test mail sent"); + } + + @SneakyThrows + void uploadTestFile() { + ClassPathResource resource = new ClassPathResource("/files/test-pdf.pdf"); + documentStorageFileRepository.updateFile("/test/test-pdf.pdf", resource.getContentAsByteArray(), 1); + log.info("Test file uploaded"); + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/resources/application-local.yml b/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/resources/application-local.yml new file mode 100644 index 00000000..7af049c8 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/resources/application-local.yml @@ -0,0 +1,17 @@ +server: + port: 8087 +spring: + mail: + host: localhost + port: 1025 + username: test@muenchen.de + password: secret +refarch: + mail: + from-address: test@muenchen.de + default-reply-to-address: no_reply@muenchen.de + s3: + bucket-name: test-bucket + access-key: minio + secret-key: Test1234 + url: http://localhost:9000 diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/resources/files/test-pdf.pdf b/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/resources/files/test-pdf.pdf new file mode 100644 index 00000000..9ee1f262 Binary files /dev/null and b/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/resources/files/test-pdf.pdf differ diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/pom.xml b/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/pom.xml new file mode 100644 index 00000000..cc507097 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + de.muenchen.refarch + refarch-email-integration + 1.1.0-SNAPSHOT + + + refarch-email-integration-rest-example + + + de.muenchen.refarch + refarch-email-integration-starter + ${project.version} + + + de.muenchen.refarch + refarch-s3-integration-rest-client-starter + ${project.version} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/java/de/muenchen/refarch/email/integration/EmailRestExampleApplication.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/java/de/muenchen/refarch/email/integration/EmailRestExampleApplication.java new file mode 100644 index 00000000..fee5c8b9 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/java/de/muenchen/refarch/email/integration/EmailRestExampleApplication.java @@ -0,0 +1,23 @@ +package de.muenchen.refarch.email.integration; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; + +@SpringBootApplication +@RequiredArgsConstructor +public class EmailRestExampleApplication { + private final TestService testService; + + public static void main(final String[] args) { + SpringApplication.run(EmailRestExampleApplication.class, args); + } + + @EventListener(ApplicationReadyEvent.class) + void sendTestMail() { + this.testService.testSendMail(); + System.exit(0); + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/java/de/muenchen/refarch/email/integration/TestService.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/java/de/muenchen/refarch/email/integration/TestService.java new file mode 100644 index 00000000..eebbf175 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/java/de/muenchen/refarch/email/integration/TestService.java @@ -0,0 +1,40 @@ +package de.muenchen.refarch.email.integration; + +import de.muenchen.refarch.email.integration.application.port.in.SendMailInPort; +import de.muenchen.refarch.email.integration.domain.model.TextMail; +import de.muenchen.refarch.integration.s3.client.repository.DocumentStorageFileRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class TestService { + private final SendMailInPort sendMailInPort; + private final DocumentStorageFileRepository documentStorageFileRepository; + + void testSendMail() { + this.uploadTestFile(); + TextMail mail = new TextMail( + "test.receiver@muenchen.de", + null, + null, + "Test", + "This is a test", + null, + List.of("/test/test-pdf.pdf")); + sendMailInPort.sendMailWithText(mail); + log.info("Test mail sent"); + } + + @SneakyThrows + void uploadTestFile() { + ClassPathResource resource = new ClassPathResource("/files/test-pdf.pdf"); + documentStorageFileRepository.updateFile("/test/test-pdf.pdf", resource.getContentAsByteArray(), 1); + log.info("Test file uploaded"); + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/resources/application-local.yml b/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/resources/application-local.yml new file mode 100644 index 00000000..f7438700 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/resources/application-local.yml @@ -0,0 +1,19 @@ +server: + port: 8087 +spring: + mail: + host: localhost + port: 1025 + username: test@muenchen.de + password: secret +refarch: + mail: + from-address: test@muenchen.de + default-reply-to-address: no_reply@muenchen.de + s3: + client: + document-storage-url: http://localhost:8086 + enable-security: true +SSO_ISSUER_URL: http://keycloak:8100/auth/realms/local_realm +SSO_S3_CLIENT_ID: local +SSO_S3_CLIENT_SECRET: client_secret diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/resources/files/test-pdf.pdf b/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/resources/files/test-pdf.pdf new file mode 100644 index 00000000..9ee1f262 Binary files /dev/null and b/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/resources/files/test-pdf.pdf differ diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-starter/pom.xml b/refarch-integrations/refarch-email-integration/refarch-email-integration-starter/pom.xml new file mode 100644 index 00000000..c9a1d6e6 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-starter/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + de.muenchen.refarch + refarch-email-integration + 1.1.0-SNAPSHOT + + + refarch-email-integration-starter + + + de.muenchen.refarch + refarch-email-integration-core + ${project.version} + + + org.springframework.boot + spring-boot + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter + + + diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-starter/src/main/java/de/muenchen/refarch/email/integration/configuration/MailAutoConfiguration.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-starter/src/main/java/de/muenchen/refarch/email/integration/configuration/MailAutoConfiguration.java new file mode 100644 index 00000000..f4200f60 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-starter/src/main/java/de/muenchen/refarch/email/integration/configuration/MailAutoConfiguration.java @@ -0,0 +1,45 @@ +package de.muenchen.refarch.email.integration.configuration; + +import de.muenchen.refarch.email.api.EmailApi; +import de.muenchen.refarch.email.integration.adapter.out.mail.MailAdapter; +import de.muenchen.refarch.email.integration.adapter.out.s3.S3Adapter; +import de.muenchen.refarch.email.integration.application.port.in.SendMailInPort; +import de.muenchen.refarch.email.integration.application.port.out.LoadMailAttachmentOutPort; +import de.muenchen.refarch.email.integration.application.port.out.MailOutPort; +import de.muenchen.refarch.email.integration.application.usecase.SendMailUseCase; +import de.muenchen.refarch.integration.s3.client.repository.DocumentStorageFileRepository; +import de.muenchen.refarch.integration.s3.client.repository.DocumentStorageFolderRepository; +import de.muenchen.refarch.integration.s3.client.service.FileValidationService; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.mail.MailProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +@EnableConfigurationProperties({ MailProperties.class }) +public class MailAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public SendMailInPort getSendMailPathsInPort(final LoadMailAttachmentOutPort loadAttachmentPort, final MailOutPort mailOutPort) { + return new SendMailUseCase(loadAttachmentPort, mailOutPort); + } + + @Bean + @ConditionalOnMissingBean + public LoadMailAttachmentOutPort getLoadMailAttachmentPort( + final DocumentStorageFileRepository documentStorageFileRepository, + final DocumentStorageFolderRepository documentStorageFolderRepository, + final FileValidationService fileValidationService) { + return new S3Adapter(documentStorageFileRepository, documentStorageFolderRepository, fileValidationService); + } + + @Bean + @ConditionalOnMissingBean + public MailOutPort getMailPort(final EmailApi emailApi) { + return new MailAdapter(emailApi); + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/refarch-integrations/refarch-email-integration/refarch-email-integration-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..c5a1177d --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +de.muenchen.refarch.email.integration.configuration.MailAutoConfiguration diff --git a/refarch-integrations/refarch-email-integration/refarch-email/pom.xml b/refarch-integrations/refarch-email-integration/refarch-email/pom.xml new file mode 100644 index 00000000..653e19b9 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + de.muenchen.refarch + refarch-email-integration + 1.1.0-SNAPSHOT + + + refarch-email + pom + + + refarch-email-api + refarch-email-starter + + + diff --git a/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/pom.xml b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/pom.xml new file mode 100644 index 00000000..5e2c9da9 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + de.muenchen.refarch + refarch-email + 1.1.0-SNAPSHOT + + + refarch-email-api + + + + org.springframework.boot + spring-boot-starter-mail + + + org.apache.commons + commons-lang3 + + + jakarta.validation + jakarta.validation-api + 3.1.0 + + + com.googlecode.owasp-java-html-sanitizer + owasp-java-html-sanitizer + 20240325.1 + + + + org.springframework.boot + spring-boot-starter-freemarker + + + org.springframework + spring-webmvc + + + + + + diff --git a/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/api/EmailApi.java b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/api/EmailApi.java new file mode 100644 index 00000000..f94ad523 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/api/EmailApi.java @@ -0,0 +1,22 @@ +package de.muenchen.refarch.email.api; + +import de.muenchen.refarch.email.model.Mail; +import freemarker.template.TemplateException; +import jakarta.mail.MessagingException; +import jakarta.validation.Valid; +import java.io.IOException; +import java.util.Map; + +public interface EmailApi { + + void sendMail(@Valid Mail mail) throws MessagingException; + + void sendMailWithDefaultLogo(@Valid Mail mail) throws MessagingException; + + void sendMail(@Valid Mail mail, String logoPath) throws MessagingException; + + String getBodyFromTemplate(String templateName, Map content) throws IOException, TemplateException; + + String getEmailBodyFromTemplate(String templatePath, Map content); + +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/impl/EmailApiImpl.java b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/impl/EmailApiImpl.java new file mode 100644 index 00000000..c8eb2121 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/impl/EmailApiImpl.java @@ -0,0 +1,133 @@ +package de.muenchen.refarch.email.impl; + +import de.muenchen.refarch.email.api.EmailApi; +import de.muenchen.refarch.email.model.Mail; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; +import jakarta.validation.Valid; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.owasp.html.PolicyFactory; +import org.owasp.html.Sanitizers; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.ui.freemarker.FreeMarkerTemplateUtils; +import org.springframework.util.FileCopyUtils; +import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; + +@Slf4j +@RequiredArgsConstructor +public class EmailApiImpl implements EmailApi { + + private final JavaMailSender mailSender; + private final ResourceLoader resourceLoader; + private final FreeMarkerConfigurer freeMarkerConfigurer; + private final String fromAddress; + private final String defaultReplyToAddress; + // use a prepackaged sanitizer to prevent XSS + private final PolicyFactory policy = Sanitizers.BLOCKS + .and(Sanitizers.FORMATTING) + .and(Sanitizers.LINKS) + .and(Sanitizers.TABLES); + + @Override + public void sendMail(@Valid Mail mail) throws MessagingException { + this.sendMail(mail, null); + } + + @Override + public void sendMailWithDefaultLogo(@Valid Mail mail) throws MessagingException { + this.sendMail(mail, "bausteine/mail/email-logo.png"); + } + + @Override + public void sendMail(@Valid Mail mail, String logoPath) throws MessagingException { + final MimeMessage mimeMessage = this.mailSender.createMimeMessage(); + + mimeMessage.setRecipients(Message.RecipientType.TO, InternetAddress.parse(mail.receivers())); + + if (mail.hasReceiversCc()) { + mimeMessage.setRecipients(Message.RecipientType.CC, InternetAddress.parse(mail.receiversCc())); + } + if (mail.hasReceiversBcc()) { + mimeMessage.setRecipients(Message.RecipientType.BCC, InternetAddress.parse(mail.receiversBcc())); + } + + if (mail.hasReplyTo()) { + mimeMessage.setReplyTo(InternetAddress.parse(mail.replyTo())); + } else if (defaultReplyToAddress != null) { + mimeMessage.setReplyTo(InternetAddress.parse(defaultReplyToAddress)); + } + + final var helper = new MimeMessageHelper(mimeMessage, true, StandardCharsets.UTF_8.name()); + + helper.setSubject(mail.subject()); + helper.setText(mail.body(), mail.htmlBody()); + // use custom sender + helper.setFrom(mail.hasSender() ? mail.sender() : this.fromAddress); + + // mail attachments + if (mail.hasAttachment()) { + for (val attachment : mail.attachments()) { + helper.addAttachment(attachment.fileName(), attachment.file()); + } + } + + // logo + if (logoPath != null) { + final Resource logo = this.getRessourceFromClassPath(logoPath); + helper.addInline("logo", logo); + } + + this.mailSender.send(mimeMessage); + log.info("Mail {} sent to {}.", mail.subject(), mail.receivers()); + } + + @Override + public String getBodyFromTemplate(String templateName, Map content) throws IOException, TemplateException { + Template template = freeMarkerConfigurer.getConfiguration().getTemplate(templateName); + return FreeMarkerTemplateUtils.processTemplateIntoString(template, content); + } + + @Override + public String getEmailBodyFromTemplate(String templatePath, Map content) { + String mailTemplate = this.getTemplate(templatePath); + for (val entry : content.entrySet()) { + // make sure inputs are sanitized to prevent XSS + final String value = policy.sanitize(entry.getValue()); + // Make sure new lines are converted to
tags + mailTemplate = mailTemplate.replaceAll(entry.getKey(), value.replaceAll("(\r\n|\n\r|\r|\n)", "
")); + } + return mailTemplate; + } + + private String getTemplate(String templatePath) { + final Resource resource = this.getRessourceFromClassPath(templatePath); + if (!resource.exists()) { + log.error("Email Template not found: {}", templatePath); + throw new RuntimeException("Email Template not found: " + templatePath); + } + + try { + byte[] byteArray = FileCopyUtils.copyToByteArray(resource.getInputStream()); + return new String(byteArray, StandardCharsets.UTF_8); + } catch (Exception e) { + log.warn("Failed to load file: {}", templatePath); + throw new RuntimeException("Failed to load file: " + templatePath, e); + } + } + + private Resource getRessourceFromClassPath(String path) { + return resourceLoader.getResource("classpath:" + path); + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/model/FileAttachment.java b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/model/FileAttachment.java new file mode 100644 index 00000000..284e839b --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/model/FileAttachment.java @@ -0,0 +1,8 @@ +package de.muenchen.refarch.email.model; + +import jakarta.mail.util.ByteArrayDataSource; + +public record FileAttachment( + String fileName, + ByteArrayDataSource file) { +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/model/Mail.java b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/model/Mail.java new file mode 100644 index 00000000..7bdb35d3 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/model/Mail.java @@ -0,0 +1,37 @@ +package de.muenchen.refarch.email.model; + +import jakarta.validation.constraints.NotBlank; +import java.util.List; +import org.apache.commons.lang3.StringUtils; + +public record Mail( + @NotBlank String receivers, + String receiversCc, + String receiversBcc, + @NotBlank String subject, + @NotBlank String body, + boolean htmlBody, + String sender, + String replyTo, + List attachments) { + + public boolean hasAttachment() { + return attachments != null && !attachments.isEmpty(); + } + + public boolean hasReplyTo() { + return StringUtils.isNotBlank(this.replyTo); + } + + public boolean hasReceiversCc() { + return StringUtils.isNotBlank(this.receiversCc); + } + + public boolean hasReceiversBcc() { + return StringUtils.isNotBlank(this.receiversBcc); + } + + public boolean hasSender() { + return StringUtils.isNotBlank(this.sender); + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/test/java/de/muenchen/refarch/email/EmailApiImplTest.java b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/test/java/de/muenchen/refarch/email/EmailApiImplTest.java new file mode 100644 index 00000000..c622ed23 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/test/java/de/muenchen/refarch/email/EmailApiImplTest.java @@ -0,0 +1,356 @@ +package de.muenchen.refarch.email; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import de.muenchen.refarch.email.api.EmailApi; +import de.muenchen.refarch.email.impl.EmailApiImpl; +import de.muenchen.refarch.email.model.FileAttachment; +import de.muenchen.refarch.email.model.Mail; +import freemarker.template.Configuration; +import freemarker.template.TemplateException; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMultipart; +import jakarta.mail.util.ByteArrayDataSource; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; + +class EmailApiImplTest { + + private final JavaMailSender javaMailSender = mock(JavaMailSender.class); + private final ResourceLoader resourceLoader = mock(ResourceLoader.class); + private final FreeMarkerConfigurer freeMarkerConfigurer = mock(FreeMarkerConfigurer.class); + // test data + private final String receiver = "mailReceiver1@muenchen.de,mailReceiver2@muenchen.de"; + private final String receiverCC = "receiverCC@muenchen.de"; + private final String receiverBCC = "receiverBCC@muenchen.de"; + private final String subject = "Test Mail"; + private final String body = "This is a test mail"; + private final String replyTo = "test@muenchen.de"; + private final String defaultReplyTo = "noreply@muenchen.de"; + private final String sender = "some-custom-sender@muenchen.de"; + private EmailApi emailApi; + + @BeforeEach + void setUp() { + when(this.javaMailSender.createMimeMessage()).thenReturn(new MimeMessage((Session) null)); + this.emailApi = new EmailApiImpl(this.javaMailSender, this.resourceLoader, freeMarkerConfigurer, "test@muenchen.de", defaultReplyTo); + } + + @Test + void testSendMail() throws MessagingException, IOException { + final Mail mail = new Mail( + this.receiver, + null, + null, + this.subject, + this.body, + false, + null, + null, + null); + this.emailApi.sendMail(mail); + + final ArgumentCaptor messageArgumentCaptor = ArgumentCaptor.forClass(MimeMessage.class); + verify(this.javaMailSender).send(messageArgumentCaptor.capture()); + + assertThat(messageArgumentCaptor.getValue().getAllRecipients()).hasSize(2); + assertThat(messageArgumentCaptor.getValue().getSubject()).isEqualTo(this.subject); + assertThat(messageArgumentCaptor.getValue().getReplyTo()).hasSize(1); + assertThat(messageArgumentCaptor.getValue().getReplyTo()).contains(new InternetAddress(this.defaultReplyTo)); + final MimeMultipart content = (MimeMultipart) messageArgumentCaptor.getValue().getContent(); + assertThat(content.getContentType()).contains("multipart/mixed"); + } + + @Test + void testSendMailNoDefaultReplyTo() throws MessagingException, IOException { + var customAddress = new InternetAddress("custom.test@muenchen.de"); + + final Mail mail = new Mail( + this.receiver, + null, + null, + this.subject, + this.body, + false, + null, + null, + null); + new EmailApiImpl(this.javaMailSender, this.resourceLoader, freeMarkerConfigurer, customAddress.getAddress(), null).sendMail(mail); + + final ArgumentCaptor messageArgumentCaptor = ArgumentCaptor.forClass(MimeMessage.class); + verify(this.javaMailSender).send(messageArgumentCaptor.capture()); + + assertThat(messageArgumentCaptor.getValue().getAllRecipients()).hasSize(2); + assertThat(messageArgumentCaptor.getValue().getSubject()).isEqualTo(this.subject); + assertThat(messageArgumentCaptor.getValue().getReplyTo()).hasSize(1); + assertThat(messageArgumentCaptor.getValue().getReplyTo()).contains(new InternetAddress(customAddress.getAddress())); + final MimeMultipart content = (MimeMultipart) messageArgumentCaptor.getValue().getContent(); + assertThat(content.getContentType()).contains("multipart/mixed"); + } + + @Test + void testSendMailWithOptions() throws MessagingException, IOException { + final Mail mail = new Mail( + this.receiver, + this.receiverCC, + this.receiverBCC, + this.subject, + this.body, + false, + this.sender, + this.replyTo, + null); + this.emailApi.sendMail(mail); + + final ArgumentCaptor messageArgumentCaptor = ArgumentCaptor.forClass(MimeMessage.class); + verify(this.javaMailSender).send(messageArgumentCaptor.capture()); + + assertThat(messageArgumentCaptor.getValue().getAllRecipients()).hasSize(4); + assertThat(messageArgumentCaptor.getValue().getAllRecipients()).containsAll( + List.of(new InternetAddress("mailReceiver1@muenchen.de"), new InternetAddress("mailReceiver2@muenchen.de"), + new InternetAddress(this.receiverCC), new InternetAddress(this.receiverBCC))); + assertThat(messageArgumentCaptor.getValue().getSubject()).isEqualTo(this.subject); + assertThat(messageArgumentCaptor.getValue().getReplyTo()).hasSize(1); + assertThat(messageArgumentCaptor.getValue().getReplyTo()).contains(new InternetAddress(this.replyTo)); + assertThat(messageArgumentCaptor.getValue().getFrom()).contains(new InternetAddress(this.sender)); + final MimeMultipart content = (MimeMultipart) messageArgumentCaptor.getValue().getContent(); + assertThat(content.getContentType()).contains("multipart/mixed"); + } + + @Test + void sendMailWithAttachments() throws MessagingException, IOException { + final Mail mail = new Mail( + this.receiver, + null, + null, + this.subject, + this.body, + false, + null, + null, + List.of( + new FileAttachment( + "Testanhang", + new ByteArrayDataSource("FooBar".getBytes(), "text/plain")))); + this.emailApi.sendMail(mail); + + final ArgumentCaptor messageArgumentCaptor = ArgumentCaptor.forClass(MimeMessage.class); + verify(this.javaMailSender).send(messageArgumentCaptor.capture()); + + assertThat(messageArgumentCaptor.getValue().getAllRecipients()).hasSize(2); + assertThat(messageArgumentCaptor.getValue().getSubject()).isEqualTo(this.subject); + final MimeMultipart content = (MimeMultipart) messageArgumentCaptor.getValue().getContent(); + assertThat(content.getContentType()).contains("multipart/mixed"); + } + + @Test + void sendMailWithMultipleReplyToAddresses() throws MessagingException, IOException { + var reply1 = new InternetAddress("address1@muenchen.de"); + var reply2 = new InternetAddress("address2@muenchen.de"); + + final Mail mail = new Mail( + this.receiver, + null, + null, + this.subject, + this.body, + false, + null, + reply1.getAddress() + "," + reply2.getAddress(), + null); + this.emailApi.sendMail(mail); + + final ArgumentCaptor messageArgumentCaptor = ArgumentCaptor.forClass(MimeMessage.class); + verify(this.javaMailSender).send(messageArgumentCaptor.capture()); + + assertThat(messageArgumentCaptor.getValue().getAllRecipients()).hasSize(2); + assertThat(messageArgumentCaptor.getValue().getSubject()).isEqualTo(this.subject); + assertThat(messageArgumentCaptor.getValue().getReplyTo()).hasSize(2); + assertThat(messageArgumentCaptor.getValue().getReplyTo()).containsAll(List.of(reply1, reply2)); + final MimeMultipart content = (MimeMultipart) messageArgumentCaptor.getValue().getContent(); + assertThat(content.getContentType()).contains("multipart/mixed"); + } + + @Test + void sendMailWithDefaultLogo() throws MessagingException, IOException { + when(this.resourceLoader.getResource(anyString())).thenReturn(this.getResourceForText("Default Logo", true)); + + final Mail mail = new Mail( + this.receiver, + null, + null, + this.subject, + this.body, + false, + null, + null, + null); + this.emailApi.sendMailWithDefaultLogo(mail); + + final ArgumentCaptor messageArgumentCaptor = ArgumentCaptor.forClass(MimeMessage.class); + verify(this.javaMailSender).send(messageArgumentCaptor.capture()); + verify(this.resourceLoader).getResource("classpath:bausteine/mail/email-logo.png"); + + assertThat(messageArgumentCaptor.getValue().getAllRecipients()).hasSize(2); + assertThat(messageArgumentCaptor.getValue().getSubject()).isEqualTo(this.subject); + final MimeMultipart content = (MimeMultipart) messageArgumentCaptor.getValue().getContent(); + assertThat(content.getContentType()).contains("multipart/mixed"); + } + + @Test + void sendMailWithCustomLogo() throws MessagingException, IOException { + when(this.resourceLoader.getResource(anyString())).thenReturn(this.getResourceForText("Custom Logo", true)); + + final Mail mail = new Mail( + this.receiver, + null, + null, + this.subject, + this.body, + false, + null, + null, + null); + this.emailApi.sendMail(mail, "some/random/path/on/classpath"); + + final ArgumentCaptor messageArgumentCaptor = ArgumentCaptor.forClass(MimeMessage.class); + verify(this.javaMailSender).send(messageArgumentCaptor.capture()); + verify(this.resourceLoader).getResource("classpath:some/random/path/on/classpath"); + + assertThat(messageArgumentCaptor.getValue().getAllRecipients()).hasSize(2); + assertThat(messageArgumentCaptor.getValue().getSubject()).isEqualTo(this.subject); + final MimeMultipart content = (MimeMultipart) messageArgumentCaptor.getValue().getContent(); + assertThat(content.getContentType()).contains("multipart/mixed"); + } + + @Test + void testGetBodyFromFreemarkerTemplate() throws IOException, TemplateException { + final String templateName = "test-template.ftl"; + Map content = Map.of("data", "test"); + Configuration configuration = new Configuration(Configuration.VERSION_2_3_30); + configuration.setClassForTemplateLoading(this.getClass(), "/templates/"); + when(this.freeMarkerConfigurer.getConfiguration()).thenReturn(configuration); + + final String result = this.emailApi.getBodyFromTemplate(templateName, content); + + assertThat(result.contains("test")).isTrue(); + } + + @Test + void testGetEmailBodyFromTemplate() { + when(this.resourceLoader.getResource(anyString())).thenReturn(this.getResourceForText("This is a test mail", true)); + + final String templatePath = "bausteine/mail/email-logo.png"; + final String result = this.emailApi.getEmailBodyFromTemplate(templatePath, Map.of()); + + assertThat(result).isEqualTo("This is a test mail"); + } + + @Test + void testGetEmailBodyFromTemplateWithContent() { + when(this.resourceLoader.getResource(anyString())).thenReturn(this.getResourceForText("This is a test mail with content", true)); + + final String templatePath = "bausteine/mail/email-logo.png"; + final String result = this.emailApi.getEmailBodyFromTemplate(templatePath, Map.of("content", "some content")); + + assertThat(result).isEqualTo("This is a test mail with some content"); + } + + @Test + void testGetEmailBodyFromTemplateWithContentAndNewLines() { + when(this.resourceLoader.getResource(anyString())).thenReturn(this.getResourceForText("This is a test mail with content", true)); + + final String templatePath = "bausteine/mail/email-logo.png"; + final String result = this.emailApi.getEmailBodyFromTemplate(templatePath, Map.of("content", "some content \n with new line")); + + assertThat(result).isEqualTo("This is a test mail with some content
with new line"); + } + + @Test + void testGetEmailBodyFromTemplateWithContentFailsIfTemplateDoesNotExist() { + when(this.resourceLoader.getResource(anyString())).thenReturn(this.getResourceForText("foo bar", false)); + + final String templatePath = "some/temlate/that/does/not/exist"; + assertThatThrownBy(() -> { + this.emailApi.getEmailBodyFromTemplate(templatePath, Map.of("content", "some content")); + }) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("Email Template not found: " + templatePath); + } + + private Resource getResourceForText(final String text, final boolean resourceExists) { + return new Resource() { + @Override + public boolean exists() { + return resourceExists; + } + + @Override + public URL getURL() throws IOException { + return null; + } + + @Override + public URI getURI() throws IOException { + return null; + } + + @Override + public File getFile() throws IOException { + return null; + } + + @Override + public long contentLength() throws IOException { + return 0; + } + + @Override + public long lastModified() throws IOException { + return 0; + } + + @Override + public Resource createRelative(String relativePath) throws IOException { + return null; + } + + @Override + public String getFilename() { + return "test.txt"; + } + + @Override + public String getDescription() { + return null; + } + + @Override + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(text.getBytes()); + } + }; + } + +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/test/resources/templates/test-template.ftl b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/test/resources/templates/test-template.ftl new file mode 100644 index 00000000..1e47f8a2 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/test/resources/templates/test-template.ftl @@ -0,0 +1,9 @@ + + + + Titel + + +

Zweck dieser E-Mail ist ein ${data}

+ + diff --git a/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/pom.xml b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/pom.xml new file mode 100644 index 00000000..9c2eb0bf --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + de.muenchen.refarch + refarch-email + 1.1.0-SNAPSHOT + + + refarch-email-starter + + + + de.muenchen.refarch + refarch-email-api + ${project.version} + + + org.springframework.boot + spring-boot + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + diff --git a/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/src/main/java/de/muenchen/refarch/email/configuration/EmailAutoConfiguration.java b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/src/main/java/de/muenchen/refarch/email/configuration/EmailAutoConfiguration.java new file mode 100644 index 00000000..976b08cd --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/src/main/java/de/muenchen/refarch/email/configuration/EmailAutoConfiguration.java @@ -0,0 +1,63 @@ +package de.muenchen.refarch.email.configuration; + +import de.muenchen.refarch.email.api.EmailApi; +import de.muenchen.refarch.email.impl.EmailApiImpl; +import de.muenchen.refarch.email.properties.CustomMailProperties; +import jakarta.mail.MessagingException; +import java.util.Properties; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.mail.MailProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.core.io.ResourceLoader; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; +import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; + +@RequiredArgsConstructor +@EnableConfigurationProperties({ MailProperties.class, CustomMailProperties.class }) +public class EmailAutoConfiguration { + + private final MailProperties mailProperties; + private final CustomMailProperties customMailProperties; + + /** + * Configures the {@link JavaMailSender} + * + * @return configured JavaMailSender + */ + @Bean + @ConditionalOnMissingBean + public JavaMailSender getJavaMailSender() throws MessagingException { + final JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost(this.mailProperties.getHost()); + mailSender.setPort(this.mailProperties.getPort()); + mailSender.setProtocol(this.mailProperties.getProtocol()); + mailSender.setUsername(this.mailProperties.getUsername()); + mailSender.setPassword(this.mailProperties.getPassword()); + + final Properties props = mailSender.getJavaMailProperties(); + props.putAll(this.mailProperties.getProperties()); + mailSender.setJavaMailProperties(props); + mailSender.testConnection(); + return mailSender; + } + + @ConditionalOnMissingBean + @Bean + public EmailApi emailApi(final ResourceLoader resourceLoader, final JavaMailSender javaMailSender, + final FreeMarkerConfigurer freeMarkerConfigurer) { + return new EmailApiImpl(javaMailSender, resourceLoader, freeMarkerConfigurer, this.customMailProperties.getFromAddress(), + this.customMailProperties.getDefaultReplyToAddress()); + } + + @Bean + @ConditionalOnMissingBean + public FreeMarkerConfigurer freemarkerConfig() { + FreeMarkerConfigurer freeMarkerConfigurer = new FreeMarkerConfigurer(); + freeMarkerConfigurer.setTemplateLoaderPath("classpath:templates/"); + return freeMarkerConfigurer; + } + +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/src/main/java/de/muenchen/refarch/email/properties/CustomMailProperties.java b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/src/main/java/de/muenchen/refarch/email/properties/CustomMailProperties.java new file mode 100644 index 00000000..af903421 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/src/main/java/de/muenchen/refarch/email/properties/CustomMailProperties.java @@ -0,0 +1,20 @@ +package de.muenchen.refarch.email.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Data +@ConfigurationProperties(prefix = "refarch.mail") +public class CustomMailProperties { + + /** + * Sender mail address. + */ + private String fromAddress; + + /** + * Default Reply-to mail address, e.g. no-reply@domain + */ + private String defaultReplyToAddress; + +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..e67a5bac --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +de.muenchen.refarch.email.configuration.EmailAutoConfiguration