use lettre::{ message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor, }; use crate::{config::Config, error::AppError}; pub struct EmailService { mailer: AsyncSmtpTransport, from: String, } impl EmailService { pub fn new(config: &Config) -> Result { let creds = Credentials::new(config.smtp_user.clone(), config.smtp_pass.clone()); let mailer = AsyncSmtpTransport::::starttls_relay(&config.smtp_host) .map_err(|e| AppError::Internal(anyhow::anyhow!("SMTP config error: {e}")))? .port(config.smtp_port) .credentials(creds) .build(); Ok(Self { mailer, from: config.email_from.clone(), }) } pub async fn send_password_reset(&self, to: &str, reset_link: &str) -> Result<(), AppError> { let body = format!( "Você solicitou a redefinição de senha do MeowSpool.\n\nClique no link abaixo para redefinir sua senha (válido por 1 hora):\n\n{reset_link}\n\nSe você não solicitou isso, ignore este email." ); self.send(to, "Redefinição de senha — MeowSpool", &body).await } pub async fn send_email_verification(&self, to: &str, verify_link: &str) -> Result<(), AppError> { let body = format!( "Bem-vindo ao MeowSpool! Confirme seu endereço de email clicando no link abaixo:\n\n{verify_link}\n\nO link expira em 24 horas." ); self.send(to, "Confirme seu email — MeowSpool", &body).await } async fn send(&self, to: &str, subject: &str, body: &str) -> Result<(), AppError> { let email = Message::builder() .from( self.from .parse() .map_err(|e| AppError::Internal(anyhow::anyhow!("Invalid from address: {e}")))?, ) .to(to .parse() .map_err(|e| AppError::Internal(anyhow::anyhow!("Invalid to address: {e}")))?) .subject(subject) .header(ContentType::TEXT_PLAIN) .body(body.to_string()) .map_err(|e| AppError::Internal(anyhow::anyhow!("Email build error: {e}")))?; AsyncTransport::send(&self.mailer, email) .await .map_err(|e| AppError::Internal(anyhow::anyhow!("SMTP send error: {e}")))?; Ok(()) } }