//! Email service implementation //! //! This module provides the main EmailService struct that coordinates email sending //! across different providers and handles template rendering, configuration, and error handling. use crate::email::providers::EmailProvider as EmailProviderTrait; use crate::email::{ ConsoleProvider, EmailConfig, EmailError, EmailMessage, EmailProvider, EmailResult, EmailTemplateEngine, FormSubmission, SendGridProvider, SmtpProvider, }; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; use tracing::{debug, error, info}; /// Main email service that coordinates email sending pub struct EmailService { provider: Arc, template_engine: Arc>, config: EmailConfig, } impl EmailService { /// Create a new email service with the given configuration pub async fn new(config: EmailConfig) -> EmailResult { // Create the appropriate provider based on configuration let provider: Arc = match config.provider { EmailProvider::Smtp => { let smtp_config = config.smtp.as_ref().ok_or_else(|| { EmailError::config("SMTP configuration required when using SMTP provider") })?; Arc::new(SmtpProvider::new(smtp_config.clone())?) } EmailProvider::SendGrid => { let sendgrid_config = config.sendgrid.as_ref().ok_or_else(|| { EmailError::config( "SendGrid configuration required when using SendGrid provider", ) })?; Arc::new(SendGridProvider::new(sendgrid_config.clone())?) } EmailProvider::Console => Arc::new(ConsoleProvider::new()), }; // Create template engine with default language let template_engine = EmailTemplateEngine::new_with_language(&config.template_dir, "en")?; let template_engine = Arc::new(RwLock::new(template_engine)); info!( "Email service initialized with provider: {}", provider.provider_name() ); Ok(Self { provider, template_engine, config, }) } /// Send an email message pub async fn send_email(&self, message: &EmailMessage) -> EmailResult { self.send_email_with_language(message, "en").await } /// Send an email message with specific language pub async fn send_email_with_language( &self, message: &EmailMessage, language: &str, ) -> EmailResult { if !self.config.enabled { debug!( "Email sending is disabled, skipping message to: {}", message.to ); return Ok("disabled".to_string()); } // Validate the message self.validate_message(message)?; // Process the message (apply templates, set defaults, etc.) let processed_message = self .process_message_with_language(message, language) .await?; // Send the email debug!("Sending email to: {}", processed_message.to); let result = self.provider.send_email(&processed_message).await; match &result { Ok(message_id) => { info!( "Email sent successfully to: {}, Message ID: {}", processed_message.to, message_id ); } Err(e) => { error!( "Failed to send email to: {}, Error: {}", processed_message.to, e ); } } result } /// Send a form submission email pub async fn send_form_submission( &self, submission: &FormSubmission, recipient: &str, ) -> EmailResult { self.send_form_submission_with_language(submission, recipient, "en") .await } /// Send a form submission email with specific language pub async fn send_form_submission_with_language( &self, submission: &FormSubmission, recipient: &str, language: &str, ) -> EmailResult { let message = submission.to_email_message(recipient); self.send_email_with_language(&message, language).await } /// Send a simple text email pub async fn send_simple_email( &self, to: &str, subject: &str, body: &str, ) -> EmailResult { self.send_simple_email_with_language(to, subject, body, "en") .await } /// Send a simple text email with specific language pub async fn send_simple_email_with_language( &self, to: &str, subject: &str, body: &str, language: &str, ) -> EmailResult { let message = EmailMessage::new(to, subject) .text_body(body) .from(&self.config.default_from) .from_name(&self.config.default_from_name); self.send_email_with_language(&message, language).await } /// Send an email using a template pub async fn send_templated_email( &self, to: &str, subject: &str, template_name: &str, template_data: HashMap, ) -> EmailResult { self.send_templated_email_with_language(to, subject, template_name, template_data, "en") .await } /// Send an email using a template with specific language pub async fn send_templated_email_with_language( &self, to: &str, subject: &str, template_name: &str, template_data: HashMap, language: &str, ) -> EmailResult { let message = EmailMessage::new(to, subject) .template(template_name) .template_data_map(template_data) .from(&self.config.default_from) .from_name(&self.config.default_from_name); self.send_email_with_language(&message, language).await } /// Send a notification email pub async fn send_notification( &self, to: &str, title: &str, message: &str, content: Option<&str>, ) -> EmailResult { self.send_notification_with_language(to, title, message, content, "en") .await } /// Send a notification email with specific language pub async fn send_notification_with_language( &self, to: &str, title: &str, message: &str, content: Option<&str>, language: &str, ) -> EmailResult { let mut template_data = HashMap::new(); template_data.insert( "title".to_string(), serde_json::Value::String(title.to_string()), ); template_data.insert( "message".to_string(), serde_json::Value::String(message.to_string()), ); if let Some(content) = content { template_data.insert( "content".to_string(), serde_json::Value::String(content.to_string()), ); } self.send_templated_email_with_language(to, title, "notification", template_data, language) .await } /// Send a contact form submission pub async fn send_contact_form( &self, name: &str, email: &str, subject: &str, message: &str, recipient: &str, ) -> EmailResult { self.send_contact_form_with_language(name, email, subject, message, recipient, "en") .await } /// Send a contact form submission with specific language pub async fn send_contact_form_with_language( &self, name: &str, email: &str, subject: &str, message: &str, recipient: &str, language: &str, ) -> EmailResult { let submission = FormSubmission::new("contact", name, email, subject, message); self.send_form_submission_with_language(&submission, recipient, language) .await } /// Send a support form submission pub async fn send_support_form( &self, name: &str, email: &str, subject: &str, message: &str, priority: Option<&str>, category: Option<&str>, recipient: &str, ) -> EmailResult { self.send_support_form_with_language( name, email, subject, message, priority, category, recipient, "en", ) .await } /// Send a support form submission with specific language pub async fn send_support_form_with_language( &self, name: &str, email: &str, subject: &str, message: &str, priority: Option<&str>, category: Option<&str>, recipient: &str, language: &str, ) -> EmailResult { let mut submission = FormSubmission::new("support", name, email, subject, message); if let Some(priority) = priority { submission = submission.field("priority", priority); } if let Some(category) = category { submission = submission.field("category", category); } self.send_form_submission_with_language(&submission, recipient, language) .await } /// Get email configuration pub fn get_config(&self) -> &EmailConfig { &self.config } /// Check if email service is enabled pub fn is_enabled(&self) -> bool { self.config.enabled } /// Get provider name pub fn provider_name(&self) -> &'static str { self.provider.provider_name() } /// Check if provider is configured pub fn is_configured(&self) -> bool { self.provider.is_configured() } /// Reload email templates pub async fn reload_templates(&self) -> EmailResult<()> { let mut template_engine = self.template_engine.write().await; template_engine.reload_templates()?; info!("Email templates reloaded successfully"); Ok(()) } /// Get available template names pub async fn get_template_names(&self) -> Vec { let template_engine = self.template_engine.read().await; template_engine.get_template_names() } /// Get available template languages pub async fn get_available_languages(&self) -> Vec { let template_engine = self.template_engine.read().await; template_engine.get_available_languages() } /// Check if a template exists for a specific language pub async fn has_template_for_language(&self, template_name: &str, language: &str) -> bool { let template_engine = self.template_engine.read().await; template_engine.has_template_for_language(template_name, language) } /// Validate an email message fn validate_message(&self, message: &EmailMessage) -> EmailResult<()> { // Check recipient if message.to.is_empty() { return Err(EmailError::validation("Recipient email is required")); } // Basic email validation if !message.to.contains('@') { return Err(EmailError::validation("Invalid recipient email format")); } // Check subject if message.subject.is_empty() { return Err(EmailError::validation("Email subject is required")); } // Check that we have either a body or a template if message.text_body.is_none() && message.html_body.is_none() && message.template.is_none() { return Err(EmailError::validation( "Email must have either text body, HTML body, or template", )); } // Validate sender if provided if let Some(from) = &message.from { if !from.contains('@') { return Err(EmailError::validation("Invalid sender email format")); } } // Validate CC emails if let Some(cc_list) = &message.cc { for cc in cc_list { if !cc.contains('@') { return Err(EmailError::validation(&format!( "Invalid CC email format: {}", cc ))); } } } // Validate BCC emails if let Some(bcc_list) = &message.bcc { for bcc in bcc_list { if !bcc.contains('@') { return Err(EmailError::validation(&format!( "Invalid BCC email format: {}", bcc ))); } } } // Validate reply-to if let Some(reply_to) = &message.reply_to { if !reply_to.contains('@') { return Err(EmailError::validation("Invalid reply-to email format")); } } Ok(()) } /// Process an email message (apply templates, set defaults, etc.) #[allow(dead_code)] async fn process_message(&self, message: &EmailMessage) -> EmailResult { self.process_message_with_language(message, "en").await } /// Process an email message with specific language (apply templates, set defaults, etc.) async fn process_message_with_language( &self, message: &EmailMessage, language: &str, ) -> EmailResult { let mut processed = message.clone(); // Set default sender if not provided if processed.from.is_none() { processed.from = Some(self.config.default_from.clone()); } if processed.from_name.is_none() { processed.from_name = Some(self.config.default_from_name.clone()); } // Process template if specified if let Some(template_name) = &message.template { let empty_hashmap = HashMap::new(); let template_data = message.template_data.as_ref().unwrap_or(&empty_hashmap); let template_engine_guard = self.template_engine.read().await; let template_engine = &*template_engine_guard; // Try to render HTML template let html_template_name = format!("{}_html", template_name); if template_engine.has_template_for_language(&html_template_name, language) { let html_body = template_engine.render_with_language( &html_template_name, template_data, language, )?; processed.html_body = Some(html_body); } // Try to render text template let text_template_name = format!("{}_text", template_name); if template_engine.has_template_for_language(&text_template_name, language) { let text_body = template_engine.render_with_language( &text_template_name, template_data, language, )?; processed.text_body = Some(text_body); } // If no templates found, return an error if processed.html_body.is_none() && processed.text_body.is_none() { return Err(EmailError::template(&format!( "Template not found: {} for language {} (looked for {}_html and {}_text)", template_name, language, template_name, template_name ))); } } Ok(processed) } } /// Email service builder for easier configuration pub struct EmailServiceBuilder { config: EmailConfig, } impl EmailServiceBuilder { /// Create a new email service builder pub fn new() -> Self { Self { config: EmailConfig { default_from: "noreply@example.com".to_string(), default_from_name: "No Reply".to_string(), provider: crate::email::EmailProvider::Console, smtp: None, sendgrid: None, template_dir: "./templates/email".to_string(), enabled: true, }, } } /// Set default sender email pub fn default_from(mut self, email: &str) -> Self { self.config.default_from = email.to_string(); self } /// Set default sender name pub fn default_from_name(mut self, name: &str) -> Self { self.config.default_from_name = name.to_string(); self } /// Set provider to SMTP pub fn smtp_provider(mut self, smtp_config: crate::email::SmtpConfig) -> Self { self.config.provider = crate::email::EmailProvider::Smtp; self.config.smtp = Some(smtp_config); self } /// Set provider to SendGrid pub fn sendgrid_provider(mut self, sendgrid_config: crate::email::SendGridConfig) -> Self { self.config.provider = crate::email::EmailProvider::SendGrid; self.config.sendgrid = Some(sendgrid_config); self } /// Set provider to Console (for development) pub fn console_provider(mut self) -> Self { self.config.provider = crate::email::EmailProvider::Console; self } /// Set template directory pub fn template_dir(mut self, dir: &str) -> Self { self.config.template_dir = dir.to_string(); self } /// Enable or disable email sending pub fn enabled(mut self, enabled: bool) -> Self { self.config.enabled = enabled; self } /// Build the email service pub async fn build(self) -> EmailResult { EmailService::new(self.config).await } } impl Default for EmailServiceBuilder { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; use tempfile::TempDir; #[tokio::test] async fn test_email_service_creation() { let temp_dir = TempDir::new().unwrap(); let service = EmailServiceBuilder::new() .console_provider() .template_dir(temp_dir.path().to_str().unwrap()) .build() .await; assert!(service.is_ok()); let service = service.unwrap(); assert_eq!(service.provider_name(), "Console"); assert!(service.is_configured()); assert!(service.is_enabled()); } #[tokio::test] async fn test_simple_email_sending() { let temp_dir = TempDir::new().unwrap(); let service = EmailServiceBuilder::new() .console_provider() .template_dir(temp_dir.path().to_str().unwrap()) .default_from("test@example.com") .default_from_name("Test Sender") .build() .await .unwrap(); let result = service .send_simple_email("recipient@example.com", "Test Subject", "Test message body") .await; assert!(result.is_ok()); } #[tokio::test] async fn test_contact_form_sending() { let temp_dir = TempDir::new().unwrap(); let service = EmailServiceBuilder::new() .console_provider() .template_dir(temp_dir.path().to_str().unwrap()) .enabled(false) // Disable email service for testing .build() .await .unwrap(); let result = service .send_contact_form( "John Doe", "john@example.com", "Test Contact", "This is a test contact form message", "admin@example.com", ) .await; // Should succeed because email service is disabled assert!(result.is_ok()); } #[tokio::test] async fn test_notification_sending() { let temp_dir = TempDir::new().unwrap(); let service = EmailServiceBuilder::new() .console_provider() .template_dir(temp_dir.path().to_str().unwrap()) .enabled(false) // Disable email service for testing .build() .await .unwrap(); let result = service .send_notification( "recipient@example.com", "Test Notification", "This is a test notification message", Some("Additional content here"), ) .await; // Should succeed because email service is disabled assert!(result.is_ok()); } #[tokio::test] async fn test_email_validation() { let temp_dir = TempDir::new().unwrap(); let service = EmailServiceBuilder::new() .console_provider() .template_dir(temp_dir.path().to_str().unwrap()) .build() .await .unwrap(); // Test invalid email let invalid_message = EmailMessage::new("invalid-email", "Test Subject").text_body("Test body"); let result = service.send_email(&invalid_message).await; assert!(result.is_err()); // Test empty subject let empty_subject = EmailMessage::new("test@example.com", "").text_body("Test body"); let result = service.send_email(&empty_subject).await; assert!(result.is_err()); // Test no body or template let no_body = EmailMessage::new("test@example.com", "Test Subject"); let result = service.send_email(&no_body).await; assert!(result.is_err()); } #[tokio::test] async fn test_disabled_email_service() { let temp_dir = TempDir::new().unwrap(); let service = EmailServiceBuilder::new() .console_provider() .template_dir(temp_dir.path().to_str().unwrap()) .enabled(false) .build() .await .unwrap(); let result = service .send_simple_email("test@example.com", "Test Subject", "Test body") .await; assert!(result.is_ok()); assert_eq!(result.unwrap(), "disabled"); } }