Refactoring + migrate mail package to server.

This commit is contained in:
2025-05-04 19:17:26 +02:00
parent eb17e24511
commit cb4eb80105
16 changed files with 147 additions and 157 deletions

View File

@@ -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<MailRequest> 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());
}
}
}

View File

@@ -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<HttpEntity<MailRequest>> 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();
}
}

View File

@@ -49,7 +49,10 @@
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,4 +1,4 @@
package dev.rheinsw.server.domain.contact.model;
package dev.rheinsw.server.contact.domain.model;
import dev.rheinsw.shared.transport.Dto;

View File

@@ -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;
}

View File

@@ -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;
/**

View File

@@ -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;
/**

View File

@@ -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 : "-";
}
}
}

View File

@@ -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;

View File

@@ -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<String> sendEmail(@RequestBody MailRequest request) {
return sendMailUseCase.execute(request);
}
}

View File

@@ -1,4 +1,4 @@
package dev.rheinsw.shared.mail.dto;
package dev.rheinsw.server.mail.domain;
import lombok.AllArgsConstructor;
import lombok.Data;

View File

@@ -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<String> execute(MailRequest request);
}

View File

@@ -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<String> 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);
}
}

View File

@@ -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: