Compare commits
5 Commits
fff9362ea8
...
f04fb14192
Author | SHA1 | Date | |
---|---|---|---|
f04fb14192 | |||
fb0ddf391f | |||
d417a46a06 | |||
c541119cf0 | |||
54798b7554 |
@ -1,5 +1,7 @@
|
|||||||
SPRING_PROFILES_ACTIVE=dev
|
SPRING_PROFILES_ACTIVE=dev
|
||||||
|
|
||||||
|
APP_ENCRYPTION_SECRET=123456789
|
||||||
|
|
||||||
DB_NAME=EXAMPLE_DB
|
DB_NAME=EXAMPLE_DB
|
||||||
DB_USER=EXAMPLE
|
DB_USER=EXAMPLE
|
||||||
DB_PASSWORD=SECRET
|
DB_PASSWORD=SECRET
|
||||||
|
@ -39,19 +39,19 @@ public class SendEmailUseCase {
|
|||||||
public void handle(EmailDTO emailDTO) {
|
public void handle(EmailDTO emailDTO) {
|
||||||
String to = emailConfigurationPort.getDefaultRecipient();
|
String to = emailConfigurationPort.getDefaultRecipient();
|
||||||
|
|
||||||
Email email = Email.builder()
|
Email email = Email.create(emailDTO.from(), to, emailDTO.subject(), emailDTO.body());
|
||||||
.from(emailDTO.from())
|
|
||||||
.to(to)
|
|
||||||
.subject(emailDTO.subject())
|
|
||||||
.body(emailDTO.body())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
emailValidatorService.validate(email);
|
emailValidatorService.validate(email);
|
||||||
|
|
||||||
log.info("Sending email from {} to {}", emailDTO.from(), to);
|
log.info("Sending email from {} to {}", emailDTO.from(), to);
|
||||||
|
|
||||||
email = emailService.sendEmail(email);
|
try {
|
||||||
|
email = emailService.sendEmail(email);
|
||||||
|
email.markAsSent();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error sending email", e);
|
||||||
|
email.markAsFailed(e.getMessage());
|
||||||
|
}
|
||||||
emailRepository.save(email);
|
emailRepository.save(email);
|
||||||
log.info("Email successfully sent and persisted to repository for recipient {}", to);
|
log.info("Email successfully sent and persisted to repository for recipient {}", to);
|
||||||
}
|
}
|
||||||
|
@ -26,13 +26,6 @@
|
|||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Swagger/OpenAPI -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springdoc</groupId>
|
|
||||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
|
||||||
<version>${springdoc.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.slf4j</groupId>
|
<groupId>org.slf4j</groupId>
|
||||||
<artifactId>slf4j-api</artifactId>
|
<artifactId>slf4j-api</artifactId>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
info:
|
info:
|
||||||
app:
|
app:
|
||||||
version: @project.version@
|
version: @project.version@
|
||||||
|
app:
|
||||||
|
encryption:
|
||||||
|
secret: ${APP_ENCRYPTION_SECRET}
|
||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: restemailbridge
|
name: restemailbridge
|
||||||
@ -26,6 +29,7 @@ springdoc:
|
|||||||
path: /v3/api-docs
|
path: /v3/api-docs
|
||||||
swagger-ui:
|
swagger-ui:
|
||||||
path: /swagger-ui
|
path: /swagger-ui
|
||||||
|
show-actuator: true
|
||||||
|
|
||||||
server:
|
server:
|
||||||
port: 8080
|
port: 8080
|
||||||
|
@ -13,6 +13,7 @@ services:
|
|||||||
DB_PORT: ${DB_PORT}
|
DB_PORT: ${DB_PORT}
|
||||||
DB_USER: ${DB_USER}
|
DB_USER: ${DB_USER}
|
||||||
DB_PASSWORD: ${DB_PASSWORD}
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
|
APP_ENCRYPTION_SECRET: ${APP_ENCRYPTION_SECRET}
|
||||||
GMAIL_OAUTH_CLIENT_ID: ${GMAIL_OAUTH_CLIENT_ID}
|
GMAIL_OAUTH_CLIENT_ID: ${GMAIL_OAUTH_CLIENT_ID}
|
||||||
GMAIL_OAUTH_CLIENT_SECRET: ${GMAIL_OAUTH_CLIENT_SECRET}
|
GMAIL_OAUTH_CLIENT_SECRET: ${GMAIL_OAUTH_CLIENT_SECRET}
|
||||||
networks:
|
networks:
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.pablotj.restemailbridge.domain.model;
|
package com.pablotj.restemailbridge.domain.model;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
@ -7,9 +8,30 @@ import lombok.Getter;
|
|||||||
@Builder
|
@Builder
|
||||||
public class Email {
|
public class Email {
|
||||||
|
|
||||||
private String from;
|
private final String from;
|
||||||
private String to;
|
private final String to;
|
||||||
private String subject;
|
private final String subject;
|
||||||
private String body;
|
private final String body;
|
||||||
|
private EmailStatus status;
|
||||||
|
private final Instant createdAt;
|
||||||
|
private String errorDescription;
|
||||||
|
|
||||||
}
|
public void markAsSent() {
|
||||||
|
this.status = EmailStatus.SENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markAsFailed(String errorDescription) {
|
||||||
|
this.status = EmailStatus.FAILED;
|
||||||
|
this.errorDescription = errorDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Email create(String from, String to, String subject, String body) {
|
||||||
|
return Email.builder()
|
||||||
|
.from(from)
|
||||||
|
.to(to)
|
||||||
|
.subject(subject)
|
||||||
|
.body(body)
|
||||||
|
.status(EmailStatus.PENDING)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.pablotj.restemailbridge.domain.model;
|
||||||
|
|
||||||
|
public enum EmailStatus {
|
||||||
|
SENT, FAILED, PENDING
|
||||||
|
}
|
@ -74,6 +74,14 @@
|
|||||||
<version>1.39.0</version>
|
<version>1.39.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Swagger/OpenAPI -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
|
<version>${springdoc.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.apis</groupId>
|
<groupId>com.google.apis</groupId>
|
||||||
<artifactId>google-api-services-gmail</artifactId>
|
<artifactId>google-api-services-gmail</artifactId>
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
package com.pablotj.restemailbridge.infrastructure.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableJpaAuditing
|
||||||
|
public class JpaConfig { }
|
@ -0,0 +1,25 @@
|
|||||||
|
package com.pablotj.restemailbridge.infrastructure.config;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.LocaleResolver;
|
||||||
|
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class LocaleConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public LocaleResolver localeResolver() {
|
||||||
|
AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
|
||||||
|
resolver.setDefaultLocale(Locale.of("en"));
|
||||||
|
resolver.setSupportedLocales(
|
||||||
|
List.of(Locale.of("es"),
|
||||||
|
Locale.of("en"),
|
||||||
|
Locale.of("gl"))
|
||||||
|
);
|
||||||
|
return resolver;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
package com.pablotj.restemailbridge.infrastructure.config;
|
||||||
|
|
||||||
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.support.ResourceBundleMessageSource;
|
||||||
|
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class MessageConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public MessageSource messageSource() {
|
||||||
|
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
|
||||||
|
messageSource.setBasename("i18n/messages");
|
||||||
|
messageSource.setDefaultEncoding("UTF-8");
|
||||||
|
messageSource.setUseCodeAsDefaultMessage(false);
|
||||||
|
return messageSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public LocalValidatorFactoryBean validator(MessageSource messageSource) {
|
||||||
|
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
|
||||||
|
bean.setValidationMessageSource(messageSource);
|
||||||
|
return bean;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package com.pablotj.restemailbridge.infrastructure.config;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.models.OpenAPI;
|
||||||
|
import io.swagger.v3.oas.models.info.Contact;
|
||||||
|
import io.swagger.v3.oas.models.info.Info;
|
||||||
|
import io.swagger.v3.oas.models.info.License;
|
||||||
|
import io.swagger.v3.oas.models.parameters.Parameter;
|
||||||
|
import io.swagger.v3.oas.models.media.StringSchema;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springdoc.core.customizers.OpenApiCustomizer;
|
||||||
|
@Configuration
|
||||||
|
public class OpenApiConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public OpenAPI customOpenAPI() {
|
||||||
|
return new OpenAPI()
|
||||||
|
.info(new Info()
|
||||||
|
.title("Rest Email Bridge API")
|
||||||
|
.version("v1")
|
||||||
|
.description("API for sending and managing emails")
|
||||||
|
.license(new License().name("Apache 2.0").url("https://www.apache.org/licenses/LICENSE-2.0"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public OpenApiCustomizer globalHeaderCustomizer() {
|
||||||
|
return openApi -> openApi.getPaths().values().forEach(pathItem ->
|
||||||
|
pathItem.readOperations().forEach(operation ->
|
||||||
|
operation.addParametersItem(
|
||||||
|
new Parameter()
|
||||||
|
.in("header")
|
||||||
|
.name("Accept-Language")
|
||||||
|
.description("Language for messages (en, es, gl)")
|
||||||
|
.required(false)
|
||||||
|
.schema(new StringSchema()
|
||||||
|
._default("en")
|
||||||
|
.addEnumItem("en")
|
||||||
|
.addEnumItem("es")
|
||||||
|
.addEnumItem("gl"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package com.pablotj.restemailbridge.infrastructure.persistence;
|
||||||
|
|
||||||
|
import jakarta.persistence.AttributeConverter;
|
||||||
|
import jakarta.persistence.Converter;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
|
||||||
|
@Converter
|
||||||
|
public class EncryptionConverter implements AttributeConverter<String, String> {
|
||||||
|
|
||||||
|
|
||||||
|
@Value("${app.encryption.secret}")
|
||||||
|
private String secret;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String convertToDatabaseColumn(String attribute) {
|
||||||
|
return attribute == null ? null : new EncryptionUtils(secret).encrypt(attribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String convertToEntityAttribute(String dbData) {
|
||||||
|
return dbData == null ? null : new EncryptionUtils(secret).decrypt(dbData);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
package com.pablotj.restemailbridge.infrastructure.persistence;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.spec.GCMParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
public class EncryptionUtils {
|
||||||
|
|
||||||
|
private static final String ALGORITHM = "AES";
|
||||||
|
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
|
||||||
|
private static final int TAG_LENGTH_BIT = 128;
|
||||||
|
private static final int IV_LENGTH_BYTE = 12;
|
||||||
|
private static final SecureRandom secureRandom = new SecureRandom();
|
||||||
|
|
||||||
|
private final SecretKey secretKey;
|
||||||
|
|
||||||
|
public EncryptionUtils(String secret) {
|
||||||
|
if (secret == null || secret.getBytes(StandardCharsets.UTF_8).length != 32) {
|
||||||
|
throw new IllegalArgumentException("Secret key must be 32 bytes for AES-256");
|
||||||
|
}
|
||||||
|
this.secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), ALGORITHM);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String encrypt(String plainText) {
|
||||||
|
try {
|
||||||
|
byte[] iv = new byte[IV_LENGTH_BYTE];
|
||||||
|
secureRandom.nextBytes(iv);
|
||||||
|
|
||||||
|
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||||
|
GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, iv);
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec);
|
||||||
|
|
||||||
|
byte[] cipherText = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
// Guardamos IV + ciphertext juntos
|
||||||
|
ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + cipherText.length);
|
||||||
|
byteBuffer.put(iv);
|
||||||
|
byteBuffer.put(cipherText);
|
||||||
|
|
||||||
|
return Base64.getEncoder().encodeToString(byteBuffer.array());
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to encrypt text", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String decrypt(String base64CipherText) {
|
||||||
|
try {
|
||||||
|
byte[] cipherMessage = Base64.getDecoder().decode(base64CipherText);
|
||||||
|
ByteBuffer byteBuffer = ByteBuffer.wrap(cipherMessage);
|
||||||
|
|
||||||
|
byte[] iv = new byte[IV_LENGTH_BYTE];
|
||||||
|
byteBuffer.get(iv);
|
||||||
|
|
||||||
|
byte[] cipherText = new byte[byteBuffer.remaining()];
|
||||||
|
byteBuffer.get(cipherText);
|
||||||
|
|
||||||
|
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||||
|
GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, iv);
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
|
||||||
|
|
||||||
|
byte[] plainText = cipher.doFinal(cipherText);
|
||||||
|
return new String(plainText, StandardCharsets.UTF_8);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to decrypt text", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,24 @@
|
|||||||
package com.pablotj.restemailbridge.infrastructure.persistence;
|
package com.pablotj.restemailbridge.infrastructure.persistence;
|
||||||
|
|
||||||
|
import com.pablotj.restemailbridge.domain.model.EmailStatus;
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Convert;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.EntityListeners;
|
||||||
|
import jakarta.persistence.EnumType;
|
||||||
|
import jakarta.persistence.Enumerated;
|
||||||
import jakarta.persistence.GeneratedValue;
|
import jakarta.persistence.GeneratedValue;
|
||||||
import jakarta.persistence.GenerationType;
|
import jakarta.persistence.GenerationType;
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.Instant;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
import org.springframework.data.annotation.CreatedDate;
|
||||||
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
@Table(name = "MAIL")
|
@Table(name = "MAIL")
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@ -19,15 +28,29 @@ public class MailJpa {
|
|||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@Column(length = 100, nullable = false)
|
@Column(length = 200, nullable = false)
|
||||||
|
@Convert(converter = EncryptionConverter.class)
|
||||||
private String sender;
|
private String sender;
|
||||||
|
|
||||||
@Column(length = 100, nullable = false)
|
@Column(length = 200, nullable = false)
|
||||||
private String recipient;
|
private String recipient;
|
||||||
|
|
||||||
@Column(length = 50, nullable = false)
|
@Column(length = 150, nullable = false)
|
||||||
|
@Convert(converter = EncryptionConverter.class)
|
||||||
private String subjet;
|
private String subjet;
|
||||||
|
|
||||||
@Column(length = 40000, nullable = false)
|
@Column(length = 7000, nullable = false)
|
||||||
|
@Convert(converter = EncryptionConverter.class)
|
||||||
private String body;
|
private String body;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private EmailStatus status;
|
||||||
|
|
||||||
|
@CreatedDate
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Instant createdAt;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private String errorDescription;
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,8 @@ public class MailRepositoryAdapter implements EmailRepository {
|
|||||||
mailJpa.setRecipient(email.getTo());
|
mailJpa.setRecipient(email.getTo());
|
||||||
mailJpa.setSubjet(email.getSubject());
|
mailJpa.setSubjet(email.getSubject());
|
||||||
mailJpa.setBody(email.getBody());
|
mailJpa.setBody(email.getBody());
|
||||||
|
mailJpa.setStatus(email.getStatus());
|
||||||
|
mailJpa.setErrorDescription(email.getErrorDescription());
|
||||||
springDataMailRepository.save(mailJpa);
|
springDataMailRepository.save(mailJpa);
|
||||||
|
|
||||||
return email;
|
return email;
|
||||||
|
@ -3,6 +3,13 @@ package com.pablotj.restemailbridge.infrastructure.rest;
|
|||||||
import com.pablotj.restemailbridge.application.dto.EmailDTO;
|
import com.pablotj.restemailbridge.application.dto.EmailDTO;
|
||||||
import com.pablotj.restemailbridge.application.usecase.SendEmailUseCase;
|
import com.pablotj.restemailbridge.application.usecase.SendEmailUseCase;
|
||||||
import com.pablotj.restemailbridge.infrastructure.rest.dto.SendMailRequest;
|
import com.pablotj.restemailbridge.infrastructure.rest.dto.SendMailRequest;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.ExampleObject;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@ -10,18 +17,76 @@ import org.springframework.web.bind.annotation.RequestBody;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST controller responsible for handling email-related requests.
|
||||||
|
* <p>
|
||||||
|
* Exposes endpoints under {@code /v1/mail} to send emails through the system.
|
||||||
|
* Delegates business logic to the {@link SendEmailUseCase}.
|
||||||
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/v1/mail")
|
@RequestMapping("/v1/mail")
|
||||||
|
@Tag(name = "Mail API", description = "Endpoints for sending emails")
|
||||||
public class MailController {
|
public class MailController {
|
||||||
|
|
||||||
private final SendEmailUseCase sendEmailUseCase;
|
private final SendEmailUseCase sendEmailUseCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link MailController} instance.
|
||||||
|
*
|
||||||
|
* @param sendEmailUseCase the use case responsible for sending emails
|
||||||
|
*/
|
||||||
public MailController(SendEmailUseCase sendEmailUseCase) {
|
public MailController(SendEmailUseCase sendEmailUseCase) {
|
||||||
this.sendEmailUseCase = sendEmailUseCase;
|
this.sendEmailUseCase = sendEmailUseCase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a new email using the provided request data.
|
||||||
|
* <p>
|
||||||
|
* The request payload is validated using {@link jakarta.validation.Valid}.
|
||||||
|
*
|
||||||
|
* @param request the email request containing sender, subject and body
|
||||||
|
* @return {@link ResponseEntity} with HTTP 200 (OK) if the email is sent successfully
|
||||||
|
*/
|
||||||
@PostMapping
|
@PostMapping
|
||||||
|
@Operation(
|
||||||
|
summary = "Send an email",
|
||||||
|
description = "Sends an email using the provided sender, subject, and body.",
|
||||||
|
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = "application/json",
|
||||||
|
examples = @ExampleObject(
|
||||||
|
name = "Basic email",
|
||||||
|
value = "{ \"from\": \"user@example.com\", \"subject\": \"Hello\", \"body\": \"Hi there!\" }"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@ApiResponses(value = {
|
||||||
|
@ApiResponse(responseCode = "200", description = "Email sent successfully"),
|
||||||
|
@ApiResponse(responseCode = "400", description = "Invalid request payload"),
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "401",
|
||||||
|
description = "Unauthorized – missing or invalid authentication token",
|
||||||
|
content = @Content(schema = @Schema(hidden = true))
|
||||||
|
),
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "403",
|
||||||
|
description = "Forbidden – the authenticated user cannot send to the specified recipient",
|
||||||
|
content = @Content(schema = @Schema(hidden = true))
|
||||||
|
),
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "422",
|
||||||
|
description = "Unprocessable Entity – domain validation failed (e.g. invalid email address, business rule violation)",
|
||||||
|
content = @Content(
|
||||||
|
mediaType = "application/json",
|
||||||
|
schema = @Schema(
|
||||||
|
example = "{ \"error\": \"Invalid recipient domain\" }"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
@ApiResponse(responseCode = "500", description = "Unexpected server error")
|
||||||
|
})
|
||||||
public ResponseEntity<Void> send(@Valid @RequestBody SendMailRequest request) {
|
public ResponseEntity<Void> send(@Valid @RequestBody SendMailRequest request) {
|
||||||
sendEmailUseCase.handle(new EmailDTO(request.from(), request.subject(), request.body()));
|
sendEmailUseCase.handle(new EmailDTO(request.from(), request.subject(), request.body()));
|
||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
|
@ -1,12 +1,26 @@
|
|||||||
package com.pablotj.restemailbridge.infrastructure.rest.dto;
|
package com.pablotj.restemailbridge.infrastructure.rest.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.Email;
|
import jakarta.validation.constraints.Email;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import org.hibernate.validator.constraints.Length;
|
import org.hibernate.validator.constraints.Length;
|
||||||
|
|
||||||
|
@Schema(description = "Request payload to send an email")
|
||||||
public record SendMailRequest(
|
public record SendMailRequest(
|
||||||
@NotBlank @Email @Length(min = 4, max = 100) String from,
|
@NotBlank(message = "{email.from.blank}")
|
||||||
@NotBlank @Length(min=1, max = 30) String subject,
|
@Email(message = "{email.from.invalid}")
|
||||||
@NotBlank @Length(min=1, max = 4000) String body
|
@Length(min = 4, max = 100, message = "email.from.length}")
|
||||||
|
@Schema(description = "Sender email address", example = "user@example.com")
|
||||||
|
String from,
|
||||||
|
|
||||||
|
@NotBlank(message = "{email.subject.blank}")
|
||||||
|
@Length(min=1, max = 30, message = "{email.subject.length}")
|
||||||
|
@Schema(description = "Email subject", example = "Welcome to RestEmailBridge")
|
||||||
|
String subject,
|
||||||
|
|
||||||
|
@NotBlank(message = "{email.body.blank}")
|
||||||
|
@Length(min=1, max = 4000, message = "{email.body.length}")
|
||||||
|
@Schema(description = "Email body content", example = "Hello, thanks for signing up!")
|
||||||
|
String body
|
||||||
) {
|
) {
|
||||||
}
|
}
|
@ -1,8 +1,18 @@
|
|||||||
CREATE TABLE restemailbridge.mail
|
create table mail
|
||||||
(
|
(
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id bigint generated by default as identity primary key,
|
||||||
body VARCHAR(255) NOT NULL,
|
body varchar(7000) not null,
|
||||||
recipient VARCHAR(255) NOT NULL,
|
recipient varchar(200) not null,
|
||||||
sender VARCHAR(255) NOT NULL,
|
sender varchar(200) not null,
|
||||||
subject VARCHAR(255) NOT NULL
|
subjet varchar(150) not null,
|
||||||
|
created_at timestamp,
|
||||||
|
status varchar
|
||||||
|
constraint check_status
|
||||||
|
check (((status)::text = 'PENDING'::text) OR ((status)::text = 'SENT'::text) OR
|
||||||
|
((status)::text = 'FAILED'::text)),
|
||||||
|
error_description varchar(10000),
|
||||||
|
constraint check_error_description
|
||||||
|
check ((((status)::text = 'FAILED'::text) AND
|
||||||
|
((error_description IS NOT NULL) OR ((error_description)::text <> ''::text))) OR
|
||||||
|
(((status)::text <> 'FAILED'::text) AND (error_description IS NULL)))
|
||||||
);
|
);
|
@ -0,0 +1,9 @@
|
|||||||
|
email.from.invalid=Sender email must be valid
|
||||||
|
email.from.blank=Sender email cannot be blank
|
||||||
|
email.from.length=Sender email must be between 4 and 100 characters
|
||||||
|
|
||||||
|
email.subject.blank=Subject cannot be blank
|
||||||
|
email.subject.length=Subject must be between 1 and 30 characters
|
||||||
|
|
||||||
|
email.body.blank=Body cannot be blank
|
||||||
|
email.body.length=Body must be between 1 and 4000 characters
|
@ -0,0 +1,9 @@
|
|||||||
|
email.from.invalid=El remitente debe ser un correo válido
|
||||||
|
email.from.blank=El remitente no puede estar vacío
|
||||||
|
email.from.length=El remitente debe tener entre 4 y 100 caracteres
|
||||||
|
|
||||||
|
email.subject.blank=El asunto no puede estar vacío
|
||||||
|
email.subject.length=El asunto debe tener entre 1 y 30 caracteres
|
||||||
|
|
||||||
|
email.body.blank=El cuerpo no puede estar vacío
|
||||||
|
email.body.length=El cuerpo debe tener entre 1 y 4000 caracteres
|
@ -0,0 +1,9 @@
|
|||||||
|
email.from.invalid=O remitente debe ser un correo válido
|
||||||
|
email.from.blank=O remitente non pode estar baleiro
|
||||||
|
email.from.length=O remitente debe ter entre 4 e 100 caracteres
|
||||||
|
|
||||||
|
email.subject.blank=O asunto non pode estar baleiro
|
||||||
|
email.subject.length=O asunto debe ter entre 1 e 30 caracteres
|
||||||
|
|
||||||
|
email.body.blank=O corpo non pode estar baleiro
|
||||||
|
email.body.length=O corpo debe ter entre 1 e 4000 caracteres
|
Loading…
x
Reference in New Issue
Block a user