EmailService.java
package com.wavii.service;
import com.resend.Resend;
import com.resend.core.exception.ResendException;
import com.resend.services.emails.model.CreateEmailOptions;
import com.resend.services.emails.model.CreateEmailResponse;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
/**
* Servicio para el envío de correos electrónicos.
* Maneja el envío de verificaciones, recuperaciones de contraseña y notificaciones.
*
* @author eduglezexp
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class EmailService {
private final JavaMailSender mailSender;
@Value("${wavii.app.base-url:http://localhost:8080}")
private String baseUrl;
@Value("${spring.mail.username}")
private String fromEmail;
@Value("${resend.api-key:}")
private String resendApiKey;
@Value("${resend.from:${spring.mail.username}}")
private String resendFrom;
@Value("${resend.reply-to:}")
private String resendReplyTo;
/**
* Envía un email para verificar la cuenta del usuario.
*
* @param toEmail Email del destinatario.
* @param userName Nombre del usuario.
* @param token Token de verificación.
*/
@Async
public void sendVerificationEmail(String toEmail, String userName, String token) {
String verificationLink = baseUrl + "/api/auth/verify-email?token=" + token;
String html = buildHtml(
"Verifica tu cuenta en Wavii",
"Hola <strong>" + userName + "</strong>,",
"Gracias por registrarte en Wavii. Haz clic en el botón de abajo para verificar tu correo electrónico y activar tu cuenta.",
verificationLink,
"Verificar mi cuenta",
"Este enlace expirará en 24 horas."
);
sendHtmlEmail(toEmail, "Verifica tu cuenta en Wavii", html);
}
/**
* Envía un email para restablecer la contraseña del usuario.
*
* @param toEmail Email del destinatario.
* @param userName Nombre del usuario.
* @param token Token de recuperación.
*/
@Async
public void sendPasswordResetEmail(String toEmail, String userName, String token) {
String resetLink = baseUrl + "/api/auth/reset-password?token=" + token;
String html = buildHtml(
"Restablece tu contraseña de Wavii",
"Hola <strong>" + userName + "</strong>,",
"Hemos recibido una solicitud para restablecer la contraseña de tu cuenta. Haz clic en el botón de abajo para crear una nueva contraseña.",
resetLink,
"Restablecer contraseña",
"Este enlace expirará en 1 hora. Si no solicitaste este cambio, ignora este correo."
);
sendHtmlEmail(toEmail, "Restablece tu contrasena de Wavii", html);
}
/**
* Envía un email informando que la verificación de profesor ha sido aprobada.
*
* @param toEmail Email del destinatario.
* @param userName Nombre del usuario.
*/
@Async
public void sendVerificationApprovedEmail(String toEmail, String userName) {
String html = buildInfoHtml(
"¡Ya eres Profesor Certificado en Wavii!",
"Hola <strong>" + userName + "</strong>,",
"Tu documentación ha sido revisada y aprobada por nuestro equipo. " +
"Ya tienes la insignia de <strong>Profesor Certificado</strong> en tu perfil de Wavii.",
"Puedes abrir la app y ver tu insignia en la sección de perfil."
);
sendHtmlEmail(toEmail, "Tu verificacion ha sido aprobada - Wavii", html);
}
/**
* Envía un email informando que la verificación de profesor ha sido rechazada.
*
* @param toEmail Email del destinatario.
* @param userName Nombre del usuario.
*/
@Async
public void sendVerificationRejectedEmail(String toEmail, String userName) {
String html = buildInfoHtml(
"Sobre tu verificación en Wavii",
"Hola <strong>" + userName + "</strong>,",
"Hemos revisado tu documentación pero no hemos podido verificarla en este momento. " +
"Tu cuenta sigue activa como Profesor con acceso completo a Scholar. " +
"Puedes volver a intentarlo en cualquier momento desde tu perfil.",
"Si tienes dudas, escríbenos a soporte@wavii.app."
);
sendHtmlEmail(toEmail, "Sobre tu verificación en Wavii", html);
}
/**
* Envía una notificación genérica relacionada con una clase por email.
*
* @param toEmail Email del destinatario.
* @param userName Nombre del destinatario.
* @param title Título del mensaje.
* @param body Cuerpo del mensaje.
*/
@Async
public void sendClassNotificationEmail(String toEmail, String userName, String title, String body) {
String html = buildInfoHtml(
title,
"Hola <strong>" + userName + "</strong>,",
body,
"Puedes revisar los detalles dentro de la app de Wavii."
);
sendHtmlEmail(toEmail, title + " - Wavii", html);
}
/**
* Envía un email de prueba para validar la configuración SMTP.
*
* @param to Email del destinatario.
*/
public void sendTestEmail(String to) {
String html = buildHtml(
"Test de SMTP — Wavii",
"Hola,",
"Este es un correo de prueba para verificar que el servidor SMTP está configurado correctamente.",
baseUrl,
"Ir a Wavii",
"Puedes ignorar este correo."
);
sendHtmlEmail(to, "Test SMTP Wavii", html);
log.info("[EMAIL] Test email enviado a {}", to);
}
private void sendHtmlEmail(String to, String subject, String htmlBody) {
if (hasText(resendApiKey)) {
try {
Resend resend = new Resend(resendApiKey);
var builder = CreateEmailOptions.builder()
.from(resendFrom)
.to(to)
.subject(subject)
.html(htmlBody);
if (hasText(resendReplyTo)) {
builder.replyTo(resendReplyTo);
}
CreateEmailResponse data = resend.emails().send(builder.build());
log.info("[EMAIL] Enviado por Resend a {} — {} ({})", to, subject, data.getId());
return;
} catch (ResendException | RuntimeException e) {
log.error("[EMAIL] Error enviando por Resend a {}: {}", to, e.getMessage());
}
}
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(fromEmail);
if (hasText(resendReplyTo)) {
helper.setReplyTo(resendReplyTo);
}
helper.setTo(to);
helper.setSubject(subject);
helper.setText(htmlBody, true);
mailSender.send(message);
log.info("[EMAIL] Enviado a {} — {}", to, subject);
} catch (MessagingException e) {
log.error("[EMAIL] Error al enviar a {}: {}", to, e.getMessage());
}
}
private boolean hasText(String value) {
return value != null && !value.isBlank();
}
private String buildInfoHtml(String title, String greeting, String body, String footer) {
return """
<!DOCTYPE html>
<html lang="es">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"></head>
<body style="margin:0;padding:0;background:#FFF7ED;font-family:'Segoe UI',Arial,sans-serif;">
<table width="100%%" cellpadding="0" cellspacing="0" style="background:#FFF7ED;padding:40px 0;">
<tr><td align="center">
<table width="560" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:16px;overflow:hidden;box-shadow:0 4px 24px rgba(0,0,0,0.08);">
<tr><td style="background:#FF7A00;padding:32px;text-align:center;">
<h1 style="color:#ffffff;margin:0;font-size:32px;font-weight:900;letter-spacing:-1px;">Wavii</h1>
<p style="color:rgba(255,255,255,0.85);margin:6px 0 0;font-size:14px;">Tu música. Tu ritmo. Tu comunidad.</p>
</td></tr>
<tr><td style="padding:40px 48px;">
<h2 style="color:#1A1A2E;font-size:22px;margin:0 0 16px;">%s</h2>
<p style="color:#1A1A2E;font-size:16px;margin:0 0 12px;">%s</p>
<p style="color:#666680;font-size:15px;line-height:1.6;margin:0 0 24px;">%s</p>
<p style="color:#999999;font-size:13px;text-align:center;margin:0;">%s</p>
</td></tr>
<tr><td style="background:#F9F9F9;padding:20px 48px;text-align:center;border-top:1px solid #EEEEEE;">
<p style="color:#AAAAAA;font-size:12px;margin:0;">© 2026 Wavii · wavii.app</p>
</td></tr>
</table>
</td></tr>
</table>
</body>
</html>
""".formatted(title, greeting, body, footer);
}
private String buildHtml(String title, String greeting, String body, String link, String btnText, String footer) {
return """
<!DOCTYPE html>
<html lang="es">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"></head>
<body style="margin:0;padding:0;background:#FFF7ED;font-family:'Segoe UI',Arial,sans-serif;">
<table width="100%%" cellpadding="0" cellspacing="0" style="background:#FFF7ED;padding:40px 0;">
<tr><td align="center">
<table width="560" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:16px;overflow:hidden;box-shadow:0 4px 24px rgba(0,0,0,0.08);">
<!-- Header naranja -->
<tr><td style="background:#FF7A00;padding:32px;text-align:center;">
<h1 style="color:#ffffff;margin:0;font-size:32px;font-weight:900;letter-spacing:-1px;">Wavii</h1>
<p style="color:rgba(255,255,255,0.85);margin:6px 0 0;font-size:14px;">Tu música. Tu ritmo. Tu comunidad.</p>
</td></tr>
<!-- Cuerpo -->
<tr><td style="padding:40px 48px;">
<h2 style="color:#1A1A2E;font-size:22px;margin:0 0 16px;">%s</h2>
<p style="color:#1A1A2E;font-size:16px;margin:0 0 12px;">%s</p>
<p style="color:#666680;font-size:15px;line-height:1.6;margin:0 0 32px;">%s</p>
<div style="text-align:center;margin-bottom:32px;">
<a href="%s" style="display:inline-block;background:#FF7A00;color:#ffffff;text-decoration:none;padding:16px 40px;border-radius:12px;font-size:16px;font-weight:700;letter-spacing:0.3px;">%s</a>
</div>
<p style="color:#999999;font-size:13px;text-align:center;margin:0;">%s</p>
</td></tr>
<!-- Footer -->
<tr><td style="background:#F9F9F9;padding:20px 48px;text-align:center;border-top:1px solid #EEEEEE;">
<p style="color:#AAAAAA;font-size:12px;margin:0;">© 2026 Wavii · wavii.app</p>
</td></tr>
</table>
</td></tr>
</table>
</body>
</html>
""".formatted(title, greeting, body, link, btnText, footer);
}
}