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}