use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::Arc;
use uuid::Uuid;
use chrono::{DateTime, Utc};
use paladin_ports::output::notification_port::{
NotificationDeliveryPort, NotificationTemplatePort, NotificationPortResult,
NotificationDeliveryResult, BulkDeliveryResult, DeliveryCapabilities,
NotificationPortError,
};
use crate::core::platform::container::notification::{
Notification, NotificationContent, NotificationChannel, NotificationPriority,
NotificationRecipient, NotificationTemplate, NotificationStatus,
};
pub struct EmailNotificationService {
delivery_port: Arc<dyn NotificationDeliveryPort>,
template_port: Option<Arc<dyn NotificationTemplatePort>>,
}
impl EmailNotificationService {
pub fn new(
delivery_port: Arc<dyn NotificationDeliveryPort>,
template_port: Option<Arc<dyn NotificationTemplatePort>>,
) -> Self {
Self {
delivery_port,
template_port,
}
}
pub async fn send_simple_email(
&self,
to: String,
subject: String,
body: String,
priority: Option<NotificationPriority>,
) -> NotificationPortResult<NotificationDeliveryResult> {
let content = NotificationContent::new(subject, body, "email".to_string());
let recipient = NotificationRecipient::Email(to);
let notification = Notification::new(
recipient,
content,
NotificationChannel::Email,
priority.unwrap_or(NotificationPriority::Normal),
).map_err(|e| NotificationPortError::ValidationError(e.to_string()))?;
self.delivery_port.deliver_notification(notification).await
}
pub async fn send_templated_email(
&self,
to: String,
template_id: String,
variables: HashMap<String, serde_json::Value>,
priority: Option<NotificationPriority>,
) -> NotificationPortResult<NotificationDeliveryResult> {
let template_port = self.template_port.as_ref()
.ok_or_else(|| NotificationPortError::ConfigurationError(
"No template port configured".to_string()
))?;
let content = template_port.render_template(&template_id, variables).await?;
let recipient = NotificationRecipient::Email(to);
let notification = Notification::new(
recipient,
content,
NotificationChannel::Email,
priority.unwrap_or(NotificationPriority::Normal),
).map_err(|e| NotificationPortError::ValidationError(e.to_string()))?;
self.delivery_port.deliver_notification(notification).await
}
pub async fn send_bulk_emails(
&self,
emails: Vec<EmailRequest>,
) -> NotificationPortResult<BulkDeliveryResult> {
let mut notifications = Vec::new();
for email_req in emails {
let content = if let Some(template_id) = email_req.template_id {
let template_port = self.template_port.as_ref()
.ok_or_else(|| NotificationPortError::ConfigurationError(
"No template port configured for templated email".to_string()
))?;
template_port.render_template(&template_id, email_req.template_variables.unwrap_or_default()).await?
} else {
NotificationContent::new(
email_req.subject.unwrap_or_default(),
email_req.body.unwrap_or_default(),
"email".to_string(),
)
};
let recipient = NotificationRecipient::Email(email_req.to);
let notification = Notification::new(
recipient,
content,
NotificationChannel::Email,
email_req.priority.unwrap_or(NotificationPriority::Normal),
).map_err(|e| NotificationPortError::ValidationError(e.to_string()))?;
notifications.push(notification);
}
self.delivery_port.deliver_bulk(notifications).await
}
pub async fn send_email_with_attachments(
&self,
to: String,
subject: String,
body: String,
attachments: Vec<EmailAttachment>,
priority: Option<NotificationPriority>,
) -> NotificationPortResult<NotificationDeliveryResult> {
let capabilities = self.delivery_port.capabilities();
if !capabilities.supports_attachments {
return Err(NotificationPortError::ConfigurationError(
"Email delivery port does not support attachments".to_string()
));
}
let mut content = NotificationContent::new(subject, body, "email".to_string());
for attachment in attachments {
let notification_attachment = crate::core::platform::container::notification::NotificationAttachment::new(
attachment.filename,
attachment.content_type,
attachment.data,
);
content.add_attachment(notification_attachment);
}
let recipient = NotificationRecipient::Email(to);
let notification = Notification::new(
recipient,
content,
NotificationChannel::Email,
priority.unwrap_or(NotificationPriority::Normal),
).map_err(|e| NotificationPortError::ValidationError(e.to_string()))?;
self.delivery_port.deliver_notification(notification).await
}
pub async fn create_email_template(
&self,
template: EmailTemplate,
) -> NotificationPortResult<String> {
let template_port = self.template_port.as_ref()
.ok_or_else(|| NotificationPortError::ConfigurationError(
"No template port configured".to_string()
))?;
let notification_template = NotificationTemplate::new(
template.id,
template.name,
NotificationChannel::Email,
template.body_template,
template.variables,
);
template_port.create_template(notification_template).await
}
pub fn validate_email(&self, email: &str) -> bool {
email.contains('@') && email.len() > 3 && email.contains('.')
}
pub async fn health_check(&self) -> bool {
self.delivery_port.health_check().await
}
pub fn get_capabilities(&self) -> EmailServiceCapabilities {
let delivery_caps = self.delivery_port.capabilities();
EmailServiceCapabilities {
supports_templates: self.template_port.is_some(),
supports_attachments: delivery_caps.supports_attachments,
supports_bulk: delivery_caps.supports_bulk,
supports_rich_content: delivery_caps.supports_rich_content,
max_attachment_size: delivery_caps.max_attachment_size,
rate_limit: delivery_caps.rate_limit,
}
}
}
#[derive(Debug, Clone)]
pub struct EmailRequest {
pub to: String,
pub subject: Option<String>,
pub body: Option<String>,
pub template_id: Option<String>,
pub template_variables: Option<HashMap<String, serde_json::Value>>,
pub priority: Option<NotificationPriority>,
}
#[derive(Debug, Clone)]
pub struct EmailAttachment {
pub filename: String,
pub content_type: String,
pub data: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct EmailTemplate {
pub id: String,
pub name: String,
pub subject_template: Option<String>,
pub body_template: String,
pub variables: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct EmailServiceCapabilities {
pub supports_templates: bool,
pub supports_attachments: bool,
pub supports_bulk: bool,
pub supports_rich_content: bool,
pub max_attachment_size: Option<usize>,
pub rate_limit: Option<u32>,
}
pub struct EmailNotificationServiceFactory;
impl EmailNotificationServiceFactory {
pub fn create_with_delivery_port(
delivery_port: Arc<dyn NotificationDeliveryPort>,
) -> EmailNotificationService {
EmailNotificationService::new(delivery_port, None)
}
pub fn create_with_ports(
delivery_port: Arc<dyn NotificationDeliveryPort>,
template_port: Arc<dyn NotificationTemplatePort>,
) -> EmailNotificationService {
EmailNotificationService::new(delivery_port, Some(template_port))
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockEmailDeliveryPort;
#[async_trait]
impl NotificationDeliveryPort for MockEmailDeliveryPort {
fn channel(&self) -> NotificationChannel {
NotificationChannel::Email
}
fn can_handle(&self, notification: &Notification) -> bool {
notification.channel == NotificationChannel::Email
}
async fn deliver_notification(&self, notification: Notification) -> NotificationPortResult<NotificationDeliveryResult> {
Ok(NotificationDeliveryResult {
notification_id: notification.id,
status: NotificationStatus::Sent,
external_id: Some("mock-id".to_string()),
processing_time_ms: 100,
error_message: None,
delivered_at: Utc::now(),
channel: NotificationChannel::Email,
metadata: HashMap::new(),
})
}
async fn health_check(&self) -> bool {
true
}
fn capabilities(&self) -> DeliveryCapabilities {
DeliveryCapabilities {
supports_bulk: true,
supports_receipts: true,
supports_attachments: true,
supports_rich_content: true,
supports_templates: false,
max_attachment_size: Some(25 * 1024 * 1024),
rate_limit: Some(100),
}
}
}
#[tokio::test]
async fn test_send_simple_email() {
let delivery_port = Arc::new(MockEmailDeliveryPort);
let service = EmailNotificationService::new(delivery_port, None);
let result = service.send_simple_email(
"test@example.com".to_string(),
"Test Subject".to_string(),
"Test Body".to_string(),
Some(NotificationPriority::High),
).await;
assert!(result.is_ok());
let delivery_result = result.unwrap();
assert_eq!(delivery_result.status, NotificationStatus::Sent);
}
#[test]
fn test_email_validation() {
let delivery_port = Arc::new(MockEmailDeliveryPort);
let service = EmailNotificationService::new(delivery_port, None);
assert!(service.validate_email("test@example.com"));
assert!(!service.validate_email("invalid-email"));
assert!(!service.validate_email("@example.com"));
}
#[tokio::test]
async fn test_health_check() {
let delivery_port = Arc::new(MockEmailDeliveryPort);
let service = EmailNotificationService::new(delivery_port, None);
assert!(service.health_check().await);
}
#[test]
fn test_capabilities() {
let delivery_port = Arc::new(MockEmailDeliveryPort);
let service = EmailNotificationService::new(delivery_port, None);
let capabilities = service.get_capabilities();
assert!(!capabilities.supports_templates); assert!(capabilities.supports_attachments);
assert!(capabilities.supports_bulk);
}
}