Compare commits
5 Commits
fff9362ea8
...
f04fb14192
Author | SHA1 | Date | |
---|---|---|---|
f04fb14192 | |||
fb0ddf391f | |||
d417a46a06 | |||
c541119cf0 | |||
54798b7554 |
@ -1,5 +1,7 @@
|
||||
SPRING_PROFILES_ACTIVE=dev
|
||||
|
||||
APP_ENCRYPTION_SECRET=123456789
|
||||
|
||||
DB_NAME=EXAMPLE_DB
|
||||
DB_USER=EXAMPLE
|
||||
DB_PASSWORD=SECRET
|
||||
|
@ -39,19 +39,19 @@ public class SendEmailUseCase {
|
||||
public void handle(EmailDTO emailDTO) {
|
||||
String to = emailConfigurationPort.getDefaultRecipient();
|
||||
|
||||
Email email = Email.builder()
|
||||
.from(emailDTO.from())
|
||||
.to(to)
|
||||
.subject(emailDTO.subject())
|
||||
.body(emailDTO.body())
|
||||
.build();
|
||||
Email email = Email.create(emailDTO.from(), to, emailDTO.subject(), emailDTO.body());
|
||||
|
||||
emailValidatorService.validate(email);
|
||||
|
||||
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);
|
||||
log.info("Email successfully sent and persisted to repository for recipient {}", to);
|
||||
}
|
||||
|
@ -26,13 +26,6 @@
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Swagger/OpenAPI -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>${springdoc.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
|
@ -1,6 +1,9 @@
|
||||
info:
|
||||
app:
|
||||
version: @project.version@
|
||||
app:
|
||||
encryption:
|
||||
secret: ${APP_ENCRYPTION_SECRET}
|
||||
spring:
|
||||
application:
|
||||
name: restemailbridge
|
||||
@ -26,6 +29,7 @@ springdoc:
|
||||
path: /v3/api-docs
|
||||
swagger-ui:
|
||||
path: /swagger-ui
|
||||
show-actuator: true
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
|
@ -13,6 +13,7 @@ services:
|
||||
DB_PORT: ${DB_PORT}
|
||||
DB_USER: ${DB_USER}
|
||||
DB_PASSWORD: ${DB_PASSWORD}
|
||||
APP_ENCRYPTION_SECRET: ${APP_ENCRYPTION_SECRET}
|
||||
GMAIL_OAUTH_CLIENT_ID: ${GMAIL_OAUTH_CLIENT_ID}
|
||||
GMAIL_OAUTH_CLIENT_SECRET: ${GMAIL_OAUTH_CLIENT_SECRET}
|
||||
networks:
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.pablotj.restemailbridge.domain.model;
|
||||
|
||||
import java.time.Instant;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@ -7,9 +8,30 @@ import lombok.Getter;
|
||||
@Builder
|
||||
public class Email {
|
||||
|
||||
private String from;
|
||||
private String to;
|
||||
private String subject;
|
||||
private String body;
|
||||
private final String from;
|
||||
private final String to;
|
||||
private final String subject;
|
||||
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>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- Swagger/OpenAPI -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>${springdoc.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.apis</groupId>
|
||||
<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;
|
||||
|
||||
import com.pablotj.restemailbridge.domain.model.EmailStatus;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Convert;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.Instant;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
@Entity
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
@Table(name = "MAIL")
|
||||
@Getter
|
||||
@Setter
|
||||
@ -19,15 +28,29 @@ public class MailJpa {
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(length = 100, nullable = false)
|
||||
@Column(length = 200, nullable = false)
|
||||
@Convert(converter = EncryptionConverter.class)
|
||||
private String sender;
|
||||
|
||||
@Column(length = 100, nullable = false)
|
||||
@Column(length = 200, nullable = false)
|
||||
private String recipient;
|
||||
|
||||
@Column(length = 50, nullable = false)
|
||||
@Column(length = 150, nullable = false)
|
||||
@Convert(converter = EncryptionConverter.class)
|
||||
private String subjet;
|
||||
|
||||
@Column(length = 40000, nullable = false)
|
||||
@Column(length = 7000, nullable = false)
|
||||
@Convert(converter = EncryptionConverter.class)
|
||||
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.setSubjet(email.getSubject());
|
||||
mailJpa.setBody(email.getBody());
|
||||
|
||||
mailJpa.setStatus(email.getStatus());
|
||||
mailJpa.setErrorDescription(email.getErrorDescription());
|
||||
springDataMailRepository.save(mailJpa);
|
||||
|
||||
return email;
|
||||
|
@ -3,6 +3,13 @@ package com.pablotj.restemailbridge.infrastructure.rest;
|
||||
import com.pablotj.restemailbridge.application.dto.EmailDTO;
|
||||
import com.pablotj.restemailbridge.application.usecase.SendEmailUseCase;
|
||||
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 org.springframework.http.ResponseEntity;
|
||||
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.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
|
||||
@RequestMapping("/v1/mail")
|
||||
@Tag(name = "Mail API", description = "Endpoints for sending emails")
|
||||
public class MailController {
|
||||
|
||||
private final SendEmailUseCase sendEmailUseCase;
|
||||
|
||||
/**
|
||||
* Creates a new {@link MailController} instance.
|
||||
*
|
||||
* @param sendEmailUseCase the use case responsible for sending emails
|
||||
*/
|
||||
public MailController(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
|
||||
@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) {
|
||||
sendEmailUseCase.handle(new EmailDTO(request.from(), request.subject(), request.body()));
|
||||
return ResponseEntity.ok().build();
|
||||
|
@ -1,12 +1,26 @@
|
||||
package com.pablotj.restemailbridge.infrastructure.rest.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
@Schema(description = "Request payload to send an email")
|
||||
public record SendMailRequest(
|
||||
@NotBlank @Email @Length(min = 4, max = 100) String from,
|
||||
@NotBlank @Length(min=1, max = 30) String subject,
|
||||
@NotBlank @Length(min=1, max = 4000) String body
|
||||
@NotBlank(message = "{email.from.blank}")
|
||||
@Email(message = "{email.from.invalid}")
|
||||
@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,
|
||||
body VARCHAR(255) NOT NULL,
|
||||
recipient VARCHAR(255) NOT NULL,
|
||||
sender VARCHAR(255) NOT NULL,
|
||||
subject VARCHAR(255) NOT NULL
|
||||
id bigint generated by default as identity primary key,
|
||||
body varchar(7000) not null,
|
||||
recipient varchar(200) not null,
|
||||
sender varchar(200) 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