this repo has no description
1use async_trait::async_trait; 2use std::process::Stdio; 3use tokio::io::AsyncWriteExt; 4use tokio::process::Command; 5 6use super::types::{NotificationChannel, QueuedNotification}; 7 8#[async_trait] 9pub trait NotificationSender: Send + Sync { 10 fn channel(&self) -> NotificationChannel; 11 async fn send(&self, notification: &QueuedNotification) -> Result<(), SendError>; 12} 13 14#[derive(Debug, thiserror::Error)] 15pub enum SendError { 16 #[error("Failed to spawn sendmail process: {0}")] 17 ProcessSpawn(#[from] std::io::Error), 18 19 #[error("Sendmail exited with non-zero status: {0}")] 20 SendmailFailed(String), 21 22 #[error("Channel not configured: {0:?}")] 23 NotConfigured(NotificationChannel), 24 25 #[error("External service error: {0}")] 26 ExternalService(String), 27} 28 29pub struct EmailSender { 30 from_address: String, 31 from_name: String, 32 sendmail_path: String, 33} 34 35impl EmailSender { 36 pub fn new(from_address: String, from_name: String) -> Self { 37 Self { 38 from_address, 39 from_name, 40 sendmail_path: std::env::var("SENDMAIL_PATH").unwrap_or_else(|_| "/usr/sbin/sendmail".to_string()), 41 } 42 } 43 44 pub fn from_env() -> Option<Self> { 45 let from_address = std::env::var("MAIL_FROM_ADDRESS").ok()?; 46 let from_name = std::env::var("MAIL_FROM_NAME").unwrap_or_else(|_| "BSPDS".to_string()); 47 Some(Self::new(from_address, from_name)) 48 } 49 50 fn format_email(&self, notification: &QueuedNotification) -> String { 51 let subject = notification.subject.as_deref().unwrap_or("Notification"); 52 let from_header = if self.from_name.is_empty() { 53 self.from_address.clone() 54 } else { 55 format!("{} <{}>", self.from_name, self.from_address) 56 }; 57 58 format!( 59 "From: {}\r\nTo: {}\r\nSubject: {}\r\nContent-Type: text/plain; charset=utf-8\r\nMIME-Version: 1.0\r\n\r\n{}", 60 from_header, 61 notification.recipient, 62 subject, 63 notification.body 64 ) 65 } 66} 67 68#[async_trait] 69impl NotificationSender for EmailSender { 70 fn channel(&self) -> NotificationChannel { 71 NotificationChannel::Email 72 } 73 74 async fn send(&self, notification: &QueuedNotification) -> Result<(), SendError> { 75 let email_content = self.format_email(notification); 76 77 let mut child = Command::new(&self.sendmail_path) 78 .arg("-t") 79 .arg("-oi") 80 .stdin(Stdio::piped()) 81 .stdout(Stdio::piped()) 82 .stderr(Stdio::piped()) 83 .spawn()?; 84 85 if let Some(mut stdin) = child.stdin.take() { 86 stdin.write_all(email_content.as_bytes()).await?; 87 } 88 89 let output = child.wait_with_output().await?; 90 91 if !output.status.success() { 92 let stderr = String::from_utf8_lossy(&output.stderr); 93 return Err(SendError::SendmailFailed(stderr.to_string())); 94 } 95 96 Ok(()) 97 } 98}