Skip to content
This repository was archived by the owner on Jun 15, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cloud/start-server.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ docker run -d --rm \
-e POSTGRES_HOST=$POSTGRES_HOST \
-e CORS_ALLOWED_ORIGINS=$CORS_ALLOWED_ORIGINS \
-e SERVER_SERVLET_CONTEXT_PATH=$SERVER_SERVLET_CONTEXT_PATH \
-e MAILGUN_APIKEY=$MAILGUN_APIKEY \
-e SECURITY_KEY=$SECURITY_KEY \
-e BUILD=ghcr.io/ricardo-campos-org/react-typescript-todolist/server:$PR \
ghcr.io/ricardo-campos-org/react-typescript-todolist/server:$PR
4 changes: 0 additions & 4 deletions server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>

<!-- Database -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,29 @@
package br.com.tasknoteapp.server.service;

import br.com.tasknoteapp.server.entity.UserEntity;
import br.com.tasknoteapp.server.exception.MailServiceException;
import br.com.tasknoteapp.server.templates.MailgunTemplate;
import br.com.tasknoteapp.server.templates.MailgunTemplateEmailChanged;
import br.com.tasknoteapp.server.templates.MailgunTemplateResetPwd;
import br.com.tasknoteapp.server.templates.MailgunTemplateResetPwdConfirm;
import br.com.tasknoteapp.server.templates.MailgunTemplateSignUp;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Base64;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.restclient.RestTemplateBuilder;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.client.RestClient;

/** This service handles email messages for Mailgun. */
@Service
public class MailgunEmailService {

private static final Logger logger = LoggerFactory.getLogger(MailgunEmailService.class.getName());
private final RestTemplate restTemplate;
private final RestClient restClient;
private final String targetEnv;
private final String domain;
private String senderEmail;
Expand All @@ -42,23 +35,37 @@ public class MailgunEmailService {
* @param domain The domain to send email from.
* @param sender The from option.
* @param targetEnv The environment.
* @param templateBuilder The template builder.
* @param restClientBuilder The rest client builder.
*/
public MailgunEmailService(
@Value("${mailgun.api-key}") String apiKey,
@Value("${mailgun.domain}") String domain,
@Value("${mailgun.sender-email}") String sender,
@Value("${br.com.tasknote.server.target-env}") String targetEnv,
RestTemplateBuilder templateBuilder) {
RestClient.Builder restClientBuilder) {
this.domain = domain;
this.senderEmail = sender;
this.targetEnv = targetEnv;
this.restTemplate =
templateBuilder
.connectTimeout(Duration.ofSeconds(5))
.readTimeout(Duration.ofSeconds(10))
.defaultHeader(HttpHeaders.AUTHORIZATION, basicAuth("api", apiKey))
.build();

if (apiKey != null && apiKey.length() > 6) {
logger.info(
"Mailgun API Key loaded: {}...{}",
apiKey.substring(0, 3),
apiKey.substring(apiKey.length() - 3));
} else {
logger.warn("Mailgun API Key is missing or too short!");
}

this.restClient = restClientBuilder
.baseUrl("https://api.mailgun.net/v3/" + domain)
.defaultStatusHandler(HttpStatusCode::isError, (request, response) -> {
logger.error(
"Mailgun API Error: {} {}",
response.getStatusCode(),
response.getStatusText());
})
.defaultHeaders(headers -> headers.setBasicAuth("api", apiKey))
.build();
}

/**
Expand Down Expand Up @@ -141,16 +148,11 @@ public void sendEmailChangedNotification(UserEntity user, String oldEmail) {
*
* @param to The target email address.
* @param subject The message subject.
* @param textBody The message text body to be displayed.
* @param htmlBody The message html body to be rendered.
* @param template The mailgun template.
*/
private void sendEmail(String to, String subject, MailgunTemplate template) {
String url = "https://api.mailgun.net/v3/" + domain + "/messages";
String from = "TaskNote App <" + senderEmail + ">";

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

MultiValueMap<String, String> mailData = new LinkedMultiValueMap<>();
mailData.add("from", from);
mailData.add("to", to);
Expand All @@ -164,26 +166,22 @@ private void sendEmail(String to, String subject, MailgunTemplate template) {
logger.info("JSON template variables: {}", template.getVariableValuesJson());
}

HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(mailData, headers);

try {
ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);

if (!response.getStatusCode().is2xxSuccessful()) {
throw new MailServiceException("Failed to send email: " + response.getStatusCode());
}
restClient.post()
.uri("/messages")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(mailData)
.retrieve()
.toBodilessEntity();

logger.info("Email message send successfully.");
} catch (HttpClientErrorException ex) {
logger.error("Unable to send email: {} - {}", ex.getMessage(), ex.getCause());
logger.error("Unable to send email: {} - {}", ex.getMessage(), ex.getStatusCode());
} catch (Exception ex) {
logger.error("Unexpected error sending email: {}", ex.getMessage());
}
}

private String basicAuth(String username, String password) {
String auth = username + ":" + password;
return "Basic " + Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
}

private String getBaseUrl() {
if ("development".equals(targetEnv) || Objects.isNull(targetEnv)) {
return "http://localhost:5000";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
Expand All @@ -13,34 +12,43 @@
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.restclient.RestTemplateBuilder;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.client.RestClient;

/** Test class generated by Copilot. I had to tweak it, to make it work. */
/** Test class for MailgunEmailService using RestClient. */
@ExtendWith(MockitoExtension.class)
class MailgunEmailServiceTest {

@Mock private RestTemplate restTemplate;
@Mock private RestTemplateBuilder restTemplateBuilder;
@Mock private RestClient restClient;
@Mock private RestClient.Builder restClientBuilder;
@Mock private RestClient.RequestBodyUriSpec requestBodyUriSpec;
@Mock private RestClient.RequestBodySpec requestBodySpec;
@Mock private RestClient.ResponseSpec responseSpec;

private MailgunEmailService mailgunEmailService;

@BeforeEach
void setUp() {
when(restTemplateBuilder.connectTimeout(any())).thenReturn(restTemplateBuilder);
when(restTemplateBuilder.readTimeout(any())).thenReturn(restTemplateBuilder);
when(restTemplateBuilder.defaultHeader(any(), any())).thenReturn(restTemplateBuilder);
when(restTemplateBuilder.build()).thenReturn(restTemplate);
when(restClientBuilder.baseUrl(anyString())).thenReturn(restClientBuilder);
when(restClientBuilder.defaultStatusHandler(any(), any())).thenReturn(restClientBuilder);
when(restClientBuilder.defaultHeaders(any())).thenReturn(restClientBuilder);
when(restClientBuilder.build()).thenReturn(restClient);

String apiKey = "abx123";
String domain = "domain.com";
String sender = "no-reply@domain.com";
String target = "development";
mailgunEmailService =
new MailgunEmailService(apiKey, domain, sender, target, restTemplateBuilder);
new MailgunEmailService(apiKey, domain, sender, target, restClientBuilder);
}

private void setupMockChain() {
when(restClient.post()).thenReturn(requestBodyUriSpec);
when(requestBodyUriSpec.uri(anyString())).thenReturn(requestBodySpec);
when(requestBodySpec.contentType(any())).thenReturn(requestBodySpec);
when(requestBodySpec.body(any())).thenReturn(requestBodySpec);
when(requestBodySpec.retrieve()).thenReturn(responseSpec);
}

@Test
Expand All @@ -49,25 +57,23 @@ void testSendResetPassword() {
user.setEmail("test@example.com");
user.setResetToken("reset-token");

when(restTemplate.postForEntity(anyString(), any(), eq(String.class)))
.thenReturn(ResponseEntity.ok("Success"));
setupMockChain();

mailgunEmailService.sendResetPassword(user);

verify(restTemplate, times(1)).postForEntity(anyString(), any(), eq(String.class));
verify(restClient, times(1)).post();
}

@Test
void testSendPasswordResetConfirmation() {
UserEntity user = new UserEntity();
user.setEmail("test@example.com");

when(restTemplate.postForEntity(anyString(), any(), eq(String.class)))
.thenReturn(ResponseEntity.ok("Success"));
setupMockChain();

mailgunEmailService.sendPasswordResetConfirmation(user);

verify(restTemplate, times(1)).postForEntity(anyString(), any(), eq(String.class));
verify(restClient, times(1)).post();
}

@Test
Expand All @@ -76,12 +82,11 @@ void testSendNewUser() {
user.setEmail("test@example.com");
user.setEmailUuid(java.util.UUID.randomUUID());

when(restTemplate.postForEntity(anyString(), any(), eq(String.class)))
.thenReturn(ResponseEntity.ok("Success"));
setupMockChain();

mailgunEmailService.sendNewUser(user);

verify(restTemplate, times(1)).postForEntity(anyString(), any(), eq(String.class));
verify(restClient, times(1)).post();
}

@Test
Expand All @@ -90,26 +95,26 @@ void testSendEmailHandlesHttpClientErrorException() {
user.setEmail("test@example.com");
user.setResetToken("reset-token");

when(restTemplate.postForEntity(anyString(), any(), eq(String.class)))
setupMockChain();
when(responseSpec.toBodilessEntity())
.thenThrow(new HttpClientErrorException(HttpStatusCode.valueOf(400)));

mailgunEmailService.sendResetPassword(user);

verify(restTemplate, times(1)).postForEntity(anyString(), any(), eq(String.class));
verify(restClient, times(1)).post();
}

@Test
void testSendEmailChanged() {
UserEntity user = new UserEntity();
user.setEmail("test@example.com");

when(restTemplate.postForEntity(anyString(), any(), eq(String.class)))
.thenReturn(ResponseEntity.ok("Success"));
setupMockChain();

String oldEmail = "old@example.com";

mailgunEmailService.sendEmailChangedNotification(user, oldEmail);

verify(restTemplate, times(1)).postForEntity(anyString(), any(), eq(String.class));
verify(restClient, times(1)).post();
}
}
Loading