EmailService.java
package com.albertsilva.dev.asjcatalog.service;
import java.util.concurrent.CompletableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring6.SpringTemplateEngine;
import com.albertsilva.dev.asjcatalog.domain.recovery.Email;
import com.albertsilva.dev.asjcatalog.domain.user.User;
import com.albertsilva.dev.asjcatalog.dto.email.request.EmailRegisterRequest;
import com.albertsilva.dev.asjcatalog.repository.EmailRepository;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
/**
* Serviço que monta e envia os e-mails transacionais do sistema (ativação de
* conta e recuperação de senha) e grava um registro de cada envio em
* {@code tb_email}. É chamado apenas por {@code AccountService}, por meio dos
* métodos {@code ...Async}.
*
* <p>
* <b>Dependências externas:</b> {@code JavaMailSender} (SMTP configurado por
* {@code spring.mail.*}), {@code SpringTemplateEngine} (templates Thymeleaf
* {@code activate_user_by_email_template} e
* {@code reset_password_email_template}) e {@code EmailRepository}. Os links
* enviados usam a propriedade {@code frontend.url}.
* </p>
*
* <p>
* <b>Métodos {@code ...Async}:</b> anotados com {@code @Async} e retornando
* {@link CompletableFuture}. O projeto não possui {@code @EnableAsync} em
* nenhuma classe; portanto, provavelmente a anotação não tem efeito e o envio
* ocorre na thread e na transação de quem chama (o comportamento em execução
* não foi verificado). Os wrappers capturam qualquer {@link Exception}, a
* registram em log e a devolvem no futuro; como {@code AccountService} descarta
* esse futuro, falhas de envio são apenas logadas.
* </p>
*
* <p>
* <b>Transações:</b> nenhum método declara {@code @Transactional}. O
* {@code save} do registro usa a transação de quem chama, se existir; caso
* contrário, o próprio repositório abre uma para a gravação.
* </p>
*
* <p>
* <b>Registro em {@code tb_email}:</b> só é gravado depois de o envio ter
* sucesso. O status fica sempre {@code PENDING} (nenhum código o altera), o
* remetente registrado é fixo ({@code asjcatalog@gmail.com}, diferente do
* {@code From} da mensagem) e o conteúdo registrado é apenas um rótulo fixo
* ("Confirmação de Cadastro" ou "Redefinição de Senha"), não o corpo HTML.
* </p>
*/
@Service
public class EmailService {
private static final Logger logger = LoggerFactory.getLogger(EmailService.class);
@Value("${frontend.url}")
private String frontendUrl;
/** Valor de {@code backend.url}. Injetado, mas não utilizado por nenhum método desta classe. */
@Value("${backend.url}")
private String backendUrl;
private final JavaMailSender emailSender;
private final SpringTemplateEngine templateEngine;
private final EmailRepository emailRepository;
/**
* Construtor para injeção de dependências.
*
* @param emailSender o serviço de envio de e-mails
* @param templateEngine o mecanismo de template para geração de conteúdo de
* e-mail
* @param emailRepository o repositório para persistência de registros de e-mail
*/
public EmailService(JavaMailSender emailSender, SpringTemplateEngine templateEngine,
EmailRepository emailRepository) {
this.emailSender = emailSender;
this.templateEngine = templateEngine;
this.emailRepository = emailRepository;
}
/**
* Envia o e-mail de ativação e devolve o resultado em um
* {@link CompletableFuture}.
*
* <p>
* Chama {@link #sendActivationEmail(String, String, String)}. Qualquer
* {@link Exception} (falha de template, de montagem da mensagem, de SMTP ou
* de gravação do registro) é registrada em log e devolvida como futuro
* falho; em caso de sucesso, devolve um futuro concluído. Anotado com
* {@code @Async}, mas sem {@code @EnableAsync} no projeto a anotação
* provavelmente não tem efeito (ver documentação da classe).
* </p>
*
* @param name nome do destinatário (usado na saudação)
* @param email endereço de e-mail do destinatário
* @param activationToken valor do token de ativação, embutido no link
* @return futuro concluído em caso de sucesso, ou falho com a exceção ocorrida
*/
@Async
public CompletableFuture<Void> sendActivationEmailAsync(String name, String email, String activationToken) {
try {
sendActivationEmail(name, email, activationToken);
return CompletableFuture.completedFuture(null);
} catch (Exception ex) {
logger.error("Erro ao enviar email de ativação para {}", email, ex);
return CompletableFuture.failedFuture(ex);
}
}
/**
* Envia o e-mail de recuperação de senha e devolve o resultado em um
* {@link CompletableFuture}. Segue o mesmo contrato de
* {@link #sendActivationEmailAsync(String, String, String)}: qualquer
* {@link Exception} é registrada em log e devolvida como futuro falho.
*
* <p>
* O {@code User} é usado apenas para ler e-mail e primeiro nome.
* </p>
*
* @param user usuário que solicitou a recuperação de senha
* @param token valor do token de recuperação, embutido no link
* @return futuro concluído em caso de sucesso, ou falho com a exceção ocorrida
*/
@Async
public CompletableFuture<Void> sendPasswordRecoveryEmailAsync(User user, String token) {
try {
sendPasswordRecoveryEmail(user, token);
return CompletableFuture.completedFuture(null);
} catch (Exception ex) {
logger.error("Erro ao enviar email de recuperação de senha para {}", user.getEmail(), ex);
return CompletableFuture.failedFuture(ex);
}
}
/**
* Monta e envia, de forma síncrona, o e-mail de ativação.
*
* <p>
* Renderiza o template {@code activate_user_by_email_template} com o nome, o
* título, o texto e o link {@code frontend.url + "/activate-account?token=" +
* token}; monta a mensagem HTML (assunto "Confirmação de Cadastro", remetente
* {@code nao-responder@asjcatalog.com.br}, logo inline); envia por
* {@code JavaMailSender}; e, depois do envio, grava um registro em
* {@code tb_email} (ver documentação da classe). Se algo falhar antes da
* gravação, nenhum registro é criado.
* </p>
*
* @param name nome do destinatário
* @param email endereço de e-mail do destinatário
* @param activationToken valor do token de ativação
* @throws MessagingException se a criação ou o preenchimento da mensagem
* falhar (falhas de envio do
* {@code JavaMailSender} são exceções não
* verificadas)
*/
public void sendActivationEmail(String name, String email, String activationToken) throws MessagingException {
MimeMessage message = emailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, MimeMessageHelper.MULTIPART_MODE_MIXED_RELATED, "UTF-8");
Context context = new Context();
context.setVariable("nome", name);
context.setVariable("titulo", "Bem-vindo ao ASJ Catalog, " + name + "!");
context.setVariable("texto",
"Estamos felizes em tê-lo(a) conosco. Para começar a usar o ASJ Catalog, confirme seu cadastro clicando no link abaixo.");
context.setVariable("linkConfirmacao", frontendUrl + "/activate-account?token=" + activationToken);
String htmlBody = templateEngine.process("activate_user_by_email_template", context);
helper.setTo(email);
helper.setText(htmlBody, true);
helper.setSubject("Confirmação de Cadastro");
helper.setFrom("nao-responder@asjcatalog.com.br");
helper.addInline("logo", new ClassPathResource("/static/image/logo-ASJ-Catalog-favicon.ico"));
emailSender.send(message);
logger.info("Email de ativação enviado para {}", email);
EmailRegisterRequest registerMail = new EmailRegisterRequest("asjcatalog@gmail.com", email,
"Confirmação de Cadastro");
registerEmailLog(registerMail);
}
/**
* Monta e envia, de forma síncrona, o e-mail de recuperação de senha.
*
* <p>
* Renderiza o template {@code reset_password_email_template} com o nome, o
* token, o título e o link {@code frontend.url + "/reset-password?token=" +
* token}; monta a mensagem HTML (assunto "Redefinição de Senha", mesmo
* remetente e logo inline); envia; e grava o registro em {@code tb_email}. A
* variável {@code texto} é preenchida com a mensagem de orientação
* para redefinição de senha, utilizada pelo template.
* </p>
*
* @param user usuário que solicitou a recuperação (e-mail e primeiro nome)
* @param token valor do token de recuperação
* @throws MessagingException se a criação ou o preenchimento da mensagem
* falhar
*/
public void sendPasswordRecoveryEmail(User user, String token) throws MessagingException {
MimeMessage message = emailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, MimeMessageHelper.MULTIPART_MODE_MIXED_RELATED, "UTF-8");
helper.setTo(user.getEmail());
helper.setSubject("Redefinição de Senha");
Context context = new Context();
context.setVariable("nome", user.getFirstName());
context.setVariable("token", token);
context.setVariable("titulo", "Redefinição de Senha");
context.setVariable("texto", "Recebemos uma solicitação para redefinir a senha da sua conta. Para criar uma nova senha, clique no botão abaixo. Se você não fez essa solicitação, ignore este e-mail.");
context.setVariable("linkRedefinicaoSenha", frontendUrl + "/reset-password?token=" + token);
String htmlBody = templateEngine.process("reset_password_email_template", context);
helper.setText(htmlBody, true);
helper.setFrom("nao-responder@asjcatalog.com.br");
helper.addInline("logo", new ClassPathResource("/static/image/logo-ASJ-Catalog-favicon.ico"));
emailSender.send(message);
EmailRegisterRequest registerMail = new EmailRegisterRequest("asjcatalog@gmail.com", user.getEmail(),
"Redefinição de Senha");
registerEmailLog(registerMail);
}
/**
* Grava em {@code tb_email} o registro de um e-mail já enviado.
*
* <p>
* Cria a entidade {@code Email} a partir do DTO (status inicial
* {@code PENDING}, data de criação atual) e a salva. Não atualiza o status
* depois.
* </p>
*
* @param dataRegisterMail remetente, destinatário e conteúdo a registrar
*/
private void registerEmailLog(EmailRegisterRequest dataRegisterMail) {
Email email = new Email(dataRegisterMail);
emailRepository.save(email);
}
}