diff --git a/backend/common/src/main/java/dev/rheinsw/shared/mail/MailServiceClient.java b/backend/common/src/main/java/dev/rheinsw/shared/mail/MailServiceClient.java deleted file mode 100644 index 9c8430b..0000000 --- a/backend/common/src/main/java/dev/rheinsw/shared/mail/MailServiceClient.java +++ /dev/null @@ -1,46 +0,0 @@ -package dev.rheinsw.shared.mail; - -import dev.rheinsw.shared.mail.dto.MailRequest; -import lombok.RequiredArgsConstructor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -/** - * @author Thatsaphorn Atchariyaphap - * @since 22.04.25 - */ -@Service -@RequiredArgsConstructor -public class MailServiceClient { - - private static final Logger log = LoggerFactory.getLogger(MailServiceClient.class); - - private final RestTemplate restTemplate; - - private static final String MAIL_ENDPOINT = "http://gateway/api/mail"; - - @Async - public void sendMail(String email, String subject, String userMessage) { - MailRequest request = new MailRequest(email, subject, userMessage); - postEmail(request); - } - - private void postEmail(MailRequest request) { - try { - HttpHeaders headers = new HttpHeaders(); - - HttpEntity entity = new HttpEntity<>(request, headers); - - restTemplate.postForEntity(MAIL_ENDPOINT + "/send", entity, String.class); - } catch (Exception e) { - log.error("Failed to send email to {}: {}", request.getTo(), e.getMessage()); - } - } - -} diff --git a/backend/common/src/test/java/dev/rheinsw/shared/mail/MailServiceClientTest.java b/backend/common/src/test/java/dev/rheinsw/shared/mail/MailServiceClientTest.java deleted file mode 100644 index 6708ae1..0000000 --- a/backend/common/src/test/java/dev/rheinsw/shared/mail/MailServiceClientTest.java +++ /dev/null @@ -1,83 +0,0 @@ -package dev.rheinsw.shared.mail; - -import dev.rheinsw.shared.mail.dto.MailRequest; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.springframework.http.HttpEntity; -import org.springframework.web.client.RestTemplate; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.mockito.Mockito.*; - -class MailServiceClientTest { - - @Mock - private RestTemplate restTemplate; - - @InjectMocks - private MailServiceClient mailServiceClient; - - @Captor - private ArgumentCaptor> httpEntityCaptor; - - private AutoCloseable closeable; - - @BeforeEach - void setUp() { - closeable = MockitoAnnotations.openMocks(this); - } - - @Test - void sendMail_shouldSendCorrectRequest() { - // Arrange - String email = "user@example.com"; - String subject = "Test Subject"; - String message = "This is a test message."; - - // Act - mailServiceClient.sendMail(email, subject, message); - - // Assert - verify(restTemplate).postForEntity( - eq("http://gateway/api/mail/send"), - httpEntityCaptor.capture(), - eq(String.class) - ); - - MailRequest captured = httpEntityCaptor.getValue().getBody(); // extract the MailRequest - assert captured != null; - assert captured.getTo().equals(email); - assert captured.getSubject().equals(subject); - assert captured.getMessage().equals(message); - } - - @Test - void sendMail_shouldHandleExceptionDuringPost() { - // Arrange - String email = "user@example.com"; - String subject = "Test Subject"; - String message = "This is a test message."; - - doThrow(new RuntimeException("Simulated error")).when(restTemplate).postForEntity( - anyString(), - any(), - eq(String.class) - ); - - // Act & Assert - assertDoesNotThrow(() -> mailServiceClient.sendMail(email, subject, message), - "sendMail should handle exception internally and not throw it"); - } - - - @AfterEach - void tearDown() throws Exception { - closeable.close(); - } -} diff --git a/backend/server/pom.xml b/backend/server/pom.xml index 60305af..30a9f91 100644 --- a/backend/server/pom.xml +++ b/backend/server/pom.xml @@ -49,7 +49,10 @@ org.springframework.cloud spring-cloud-starter-netflix-eureka-client - + + org.springframework.boot + spring-boot-starter-mail + org.springframework.boot spring-boot-starter-data-jpa diff --git a/backend/server/src/main/java/dev/rheinsw/server/controller/contact/ContactController.java b/backend/server/src/main/java/dev/rheinsw/server/contact/controller/ContactController.java similarity index 85% rename from backend/server/src/main/java/dev/rheinsw/server/controller/contact/ContactController.java rename to backend/server/src/main/java/dev/rheinsw/server/contact/controller/ContactController.java index 351d99c..5cb2f44 100644 --- a/backend/server/src/main/java/dev/rheinsw/server/controller/contact/ContactController.java +++ b/backend/server/src/main/java/dev/rheinsw/server/contact/controller/ContactController.java @@ -1,7 +1,7 @@ -package dev.rheinsw.server.controller.contact; +package dev.rheinsw.server.contact.controller; -import dev.rheinsw.server.domain.contact.model.ContactRequestDto; -import dev.rheinsw.server.usecase.contact.SubmitContactUseCase; +import dev.rheinsw.server.contact.domain.model.ContactRequestDto; +import dev.rheinsw.server.contact.usecase.SubmitContactUseCase; import lombok.AllArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/backend/server/src/main/java/dev/rheinsw/server/domain/contact/model/ContactRequest.java b/backend/server/src/main/java/dev/rheinsw/server/contact/domain/model/ContactRequest.java similarity index 97% rename from backend/server/src/main/java/dev/rheinsw/server/domain/contact/model/ContactRequest.java rename to backend/server/src/main/java/dev/rheinsw/server/contact/domain/model/ContactRequest.java index 02c085e..aa3cac1 100644 --- a/backend/server/src/main/java/dev/rheinsw/server/domain/contact/model/ContactRequest.java +++ b/backend/server/src/main/java/dev/rheinsw/server/contact/domain/model/ContactRequest.java @@ -1,4 +1,4 @@ -package dev.rheinsw.server.domain.contact.model; +package dev.rheinsw.server.contact.domain.model; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/backend/server/src/main/java/dev/rheinsw/server/domain/contact/model/ContactRequestDto.java b/backend/server/src/main/java/dev/rheinsw/server/contact/domain/model/ContactRequestDto.java similarity index 89% rename from backend/server/src/main/java/dev/rheinsw/server/domain/contact/model/ContactRequestDto.java rename to backend/server/src/main/java/dev/rheinsw/server/contact/domain/model/ContactRequestDto.java index 3a4cba5..d7e1d35 100644 --- a/backend/server/src/main/java/dev/rheinsw/server/domain/contact/model/ContactRequestDto.java +++ b/backend/server/src/main/java/dev/rheinsw/server/contact/domain/model/ContactRequestDto.java @@ -1,4 +1,4 @@ -package dev.rheinsw.server.domain.contact.model; +package dev.rheinsw.server.contact.domain.model; import dev.rheinsw.shared.transport.Dto; diff --git a/backend/server/src/main/java/dev/rheinsw/server/domain/contact/config/HCaptchaConfig.java b/backend/server/src/main/java/dev/rheinsw/server/contact/domain/model/HCaptchaConfig.java similarity index 88% rename from backend/server/src/main/java/dev/rheinsw/server/domain/contact/config/HCaptchaConfig.java rename to backend/server/src/main/java/dev/rheinsw/server/contact/domain/model/HCaptchaConfig.java index 4c36fbe..1146be4 100644 --- a/backend/server/src/main/java/dev/rheinsw/server/domain/contact/config/HCaptchaConfig.java +++ b/backend/server/src/main/java/dev/rheinsw/server/contact/domain/model/HCaptchaConfig.java @@ -1,4 +1,4 @@ -package dev.rheinsw.server.domain.contact.config; +package dev.rheinsw.server.contact.domain.model; import lombok.Getter; import lombok.Setter; @@ -15,5 +15,4 @@ import org.springframework.context.annotation.Configuration; @ConfigurationProperties(prefix = "hcaptcha") public class HCaptchaConfig { private String secret; - } diff --git a/backend/server/src/main/java/dev/rheinsw/server/repository/contact/ContactRequestsRepo.java b/backend/server/src/main/java/dev/rheinsw/server/contact/repository/ContactRequestsRepo.java similarity index 67% rename from backend/server/src/main/java/dev/rheinsw/server/repository/contact/ContactRequestsRepo.java rename to backend/server/src/main/java/dev/rheinsw/server/contact/repository/ContactRequestsRepo.java index 94249ce..cc88de9 100644 --- a/backend/server/src/main/java/dev/rheinsw/server/repository/contact/ContactRequestsRepo.java +++ b/backend/server/src/main/java/dev/rheinsw/server/contact/repository/ContactRequestsRepo.java @@ -1,6 +1,6 @@ -package dev.rheinsw.server.repository.contact; +package dev.rheinsw.server.contact.repository; -import dev.rheinsw.server.domain.contact.model.ContactRequest; +import dev.rheinsw.server.contact.domain.model.ContactRequest; import org.springframework.data.jpa.repository.JpaRepository; /** diff --git a/backend/server/src/main/java/dev/rheinsw/server/usecase/contact/SubmitContactUseCase.java b/backend/server/src/main/java/dev/rheinsw/server/contact/usecase/SubmitContactUseCase.java similarity index 67% rename from backend/server/src/main/java/dev/rheinsw/server/usecase/contact/SubmitContactUseCase.java rename to backend/server/src/main/java/dev/rheinsw/server/contact/usecase/SubmitContactUseCase.java index 7a5ae81..bd8150a 100644 --- a/backend/server/src/main/java/dev/rheinsw/server/usecase/contact/SubmitContactUseCase.java +++ b/backend/server/src/main/java/dev/rheinsw/server/contact/usecase/SubmitContactUseCase.java @@ -1,6 +1,6 @@ -package dev.rheinsw.server.usecase.contact; +package dev.rheinsw.server.contact.usecase; -import dev.rheinsw.server.domain.contact.model.ContactRequestDto; +import dev.rheinsw.server.contact.domain.model.ContactRequestDto; import org.springframework.http.ResponseEntity; /** diff --git a/backend/server/src/main/java/dev/rheinsw/server/usecase/contact/SubmitContactUseCaseImpl.java b/backend/server/src/main/java/dev/rheinsw/server/contact/usecase/SubmitContactUseCaseImpl.java similarity index 68% rename from backend/server/src/main/java/dev/rheinsw/server/usecase/contact/SubmitContactUseCaseImpl.java rename to backend/server/src/main/java/dev/rheinsw/server/contact/usecase/SubmitContactUseCaseImpl.java index 504ceb2..20cd777 100644 --- a/backend/server/src/main/java/dev/rheinsw/server/usecase/contact/SubmitContactUseCaseImpl.java +++ b/backend/server/src/main/java/dev/rheinsw/server/contact/usecase/SubmitContactUseCaseImpl.java @@ -1,10 +1,11 @@ -package dev.rheinsw.server.usecase.contact; +package dev.rheinsw.server.contact.usecase; -import dev.rheinsw.server.domain.contact.model.ContactRequest; -import dev.rheinsw.server.domain.contact.model.ContactRequestDto; -import dev.rheinsw.server.repository.contact.ContactRequestsRepo; -import dev.rheinsw.shared.mail.MailServiceClient; -import dev.rheinsw.server.controller.contact.HCaptchaValidator; +import dev.rheinsw.server.contact.domain.model.ContactRequest; +import dev.rheinsw.server.contact.domain.model.ContactRequestDto; +import dev.rheinsw.server.contact.repository.ContactRequestsRepo; +import dev.rheinsw.server.contact.util.HCaptchaValidator; +import dev.rheinsw.server.mail.domain.MailRequest; +import dev.rheinsw.server.mail.usecase.SendMailUseCase; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; @@ -24,12 +25,14 @@ public class SubmitContactUseCaseImpl implements SubmitContactUseCase { private final HCaptchaValidator captchaValidator; private final ContactRequestsRepo contactRepository; - private final MailServiceClient mailServiceClient; + private final SendMailUseCase sendMailUseCase; // Inject SendMailUseCase - public SubmitContactUseCaseImpl(HCaptchaValidator captchaValidator, ContactRequestsRepo contactRepository, MailServiceClient mailServiceClient) { + public SubmitContactUseCaseImpl(HCaptchaValidator captchaValidator, + ContactRequestsRepo contactRepository, + SendMailUseCase sendMailUseCase) { this.captchaValidator = captchaValidator; this.contactRepository = contactRepository; - this.mailServiceClient = mailServiceClient; + this.sendMailUseCase = sendMailUseCase; } @Override @@ -59,7 +62,13 @@ public class SubmitContactUseCaseImpl implements SubmitContactUseCase { contactRepository.save(message); - notifyContactAndTeam(request); + // Send notifications + try { + notifyContactAndTeam(request); + } catch (Exception e) { + log.error("Error sending email notifications: ", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error sending notifications"); + } return ResponseEntity.ok("Contact form submitted successfully"); } @@ -83,7 +92,12 @@ public class SubmitContactUseCaseImpl implements SubmitContactUseCase { Rhein Software """.formatted(request.name(), request.message()); - mailServiceClient.sendMail(request.email(), userSubject, userBody); + // Send confirmation email to user + MailRequest userMailRequest = new MailRequest(); + userMailRequest.setTo(request.email()); + userMailRequest.setSubject(userSubject); + userMailRequest.setMessage(userBody); + sendMailUseCase.execute(userMailRequest); // Team notification String teamSubject = "Neue Kontaktanfrage"; @@ -105,10 +119,15 @@ public class SubmitContactUseCaseImpl implements SubmitContactUseCase { request.message() ); - mailServiceClient.sendMail("rhein.software@gmail.com", teamSubject, teamBody); + // Send team notification email + MailRequest teamMailRequest = new MailRequest(); + teamMailRequest.setTo("rhein.software@gmail.com"); + teamMailRequest.setSubject(teamSubject); + teamMailRequest.setMessage(teamBody); + sendMailUseCase.execute(teamMailRequest); } private String safe(String value) { return value != null ? value : "-"; } -} \ No newline at end of file +} diff --git a/backend/server/src/main/java/dev/rheinsw/server/controller/contact/HCaptchaValidator.java b/backend/server/src/main/java/dev/rheinsw/server/contact/util/HCaptchaValidator.java similarity index 93% rename from backend/server/src/main/java/dev/rheinsw/server/controller/contact/HCaptchaValidator.java rename to backend/server/src/main/java/dev/rheinsw/server/contact/util/HCaptchaValidator.java index b9f90b7..c95c7f0 100644 --- a/backend/server/src/main/java/dev/rheinsw/server/controller/contact/HCaptchaValidator.java +++ b/backend/server/src/main/java/dev/rheinsw/server/contact/util/HCaptchaValidator.java @@ -1,6 +1,6 @@ -package dev.rheinsw.server.controller.contact; +package dev.rheinsw.server.contact.util; -import dev.rheinsw.server.domain.contact.config.HCaptchaConfig; +import dev.rheinsw.server.contact.domain.model.HCaptchaConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; diff --git a/backend/server/src/main/java/dev/rheinsw/server/mail/controller/MailController.java b/backend/server/src/main/java/dev/rheinsw/server/mail/controller/MailController.java new file mode 100644 index 0000000..298abf8 --- /dev/null +++ b/backend/server/src/main/java/dev/rheinsw/server/mail/controller/MailController.java @@ -0,0 +1,27 @@ +package dev.rheinsw.server.mail.controller; + +import dev.rheinsw.server.mail.usecase.SendMailUseCase; +import dev.rheinsw.server.mail.domain.MailRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Thatsaphorn Atchariyaphap + * @since 04.05.25 + */ +@RestController +@RequestMapping("/mail") +@RequiredArgsConstructor +public class MailController { + + private final SendMailUseCase sendMailUseCase; + + @PostMapping("/send") + public ResponseEntity sendEmail(@RequestBody MailRequest request) { + return sendMailUseCase.execute(request); + } +} diff --git a/backend/common/src/main/java/dev/rheinsw/shared/mail/dto/MailRequest.java b/backend/server/src/main/java/dev/rheinsw/server/mail/domain/MailRequest.java similarity index 89% rename from backend/common/src/main/java/dev/rheinsw/shared/mail/dto/MailRequest.java rename to backend/server/src/main/java/dev/rheinsw/server/mail/domain/MailRequest.java index 299a629..70f5699 100644 --- a/backend/common/src/main/java/dev/rheinsw/shared/mail/dto/MailRequest.java +++ b/backend/server/src/main/java/dev/rheinsw/server/mail/domain/MailRequest.java @@ -1,4 +1,4 @@ -package dev.rheinsw.shared.mail.dto; +package dev.rheinsw.server.mail.domain; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/backend/server/src/main/java/dev/rheinsw/server/mail/usecase/ISendMailUseCase.java b/backend/server/src/main/java/dev/rheinsw/server/mail/usecase/ISendMailUseCase.java new file mode 100644 index 0000000..51402b3 --- /dev/null +++ b/backend/server/src/main/java/dev/rheinsw/server/mail/usecase/ISendMailUseCase.java @@ -0,0 +1,12 @@ +package dev.rheinsw.server.mail.usecase; + +import dev.rheinsw.server.mail.domain.MailRequest; +import org.springframework.http.ResponseEntity; + +/** + * @author Thatsaphorn Atchariyaphap + * @since 04.05.25 + */ +public interface ISendMailUseCase { + ResponseEntity execute(MailRequest request); +} diff --git a/backend/server/src/main/java/dev/rheinsw/server/mail/usecase/SendMailUseCase.java b/backend/server/src/main/java/dev/rheinsw/server/mail/usecase/SendMailUseCase.java new file mode 100644 index 0000000..50080fb --- /dev/null +++ b/backend/server/src/main/java/dev/rheinsw/server/mail/usecase/SendMailUseCase.java @@ -0,0 +1,46 @@ +package dev.rheinsw.server.mail.usecase; + + +import dev.rheinsw.server.mail.domain.MailRequest; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import org.springframework.http.ResponseEntity; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; +import org.springframework.mail.javamail.JavaMailSender; + +/** + * @author Thatsaphorn Atchariyaphap + * @since 04.05.25 + */ +@Service +public class SendMailUseCase implements ISendMailUseCase { + private final JavaMailSender mailSender; + + public SendMailUseCase(JavaMailSender mailSender) { + this.mailSender = mailSender; + } + + @Override + public ResponseEntity execute(MailRequest request) { + try { + sendEmail(request); + return ResponseEntity.ok("Email sent successfully"); + } catch (Exception e) { + return ResponseEntity.status(500).body("Failed to send email: " + e.getMessage()); + } + } + + private void sendEmail(MailRequest request) throws MessagingException { + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true); + + helper.setFrom("noreply@rhein-software.dev"); + helper.setTo(request.getTo()); + helper.setSubject(request.getSubject()); + helper.setText(request.getMessage(), false); + + mailSender.send(message); + } + +} diff --git a/backend/server/src/main/resources/application.yml b/backend/server/src/main/resources/application.yml index 43158ce..d4e1daf 100644 --- a/backend/server/src/main/resources/application.yml +++ b/backend/server/src/main/resources/application.yml @@ -17,6 +17,19 @@ spring: hibernate: format_sql: true + mail: + host: smtp.resend.com + port: 587 + username: resend + password: re_JnLD5ndg_GnKtXcTqskXm1bg7Wxnghna3 + properties: + mail: + smtp: + auth: true + starttls: + enable: true + default-encoding: UTF-8 + eureka: client: service-url: