Skip to main content

dead_man_switch/
email.rs

1//! Email sending capabilities of the Dead Man's Switch.
2
3use crate::config::{attachment_path, Config, Email};
4use crate::error::{AddressError, EmailError};
5
6use lettre::{
7    message::{header::ContentType, Attachment, Mailbox, MultiPart, SinglePart},
8    transport::smtp::{
9        authentication::Credentials,
10        client::{Tls, TlsParameters},
11    },
12    Message, SmtpTransport, Transport,
13};
14use std::fs;
15use std::io::{Error as IoError, ErrorKind as IoErrorKind};
16use std::sync::atomic::{AtomicBool, Ordering};
17use std::sync::mpsc::channel;
18use std::sync::Arc;
19use std::thread;
20use std::time::Duration;
21
22impl Config {
23    pub fn setup_smtp_client(&self) -> Result<SmtpTransport, EmailError> {
24        let creds = Credentials::new(self.username.clone(), self.password.clone());
25        let tls = TlsParameters::new_rustls(self.smtp_server.clone())?;
26        let mailer = SmtpTransport::relay(&self.smtp_server)?
27            .port(self.smtp_port)
28            .credentials(creds)
29            .tls(Tls::Required(tls))
30            .build();
31
32        Ok(mailer)
33    }
34
35    pub fn check_smtp_connection(&self) -> Result<(), EmailError> {
36        let mailer = self.setup_smtp_client()?;
37        let exit_flag = Arc::new(AtomicBool::new(false)); // owned by the main thread
38        let exit_flag_clone = Arc::clone(&exit_flag); // moved into the spawned thread
39
40        let (tx, rx) = channel();
41        thread::spawn(move || {
42            let res = mailer.test_connection();
43            // suppress the send if exit_flag_clone is true
44            if !exit_flag_clone.load(Ordering::SeqCst) {
45                let _ = tx.send(res);
46            }
47        });
48
49        let timeout = Duration::from_secs(self.smtp_check_timeout.unwrap_or(5));
50        match rx.recv_timeout(timeout) {
51            Ok(Ok(true)) => Ok(()),
52            Ok(Ok(false)) => {
53                exit_flag.store(true, Ordering::SeqCst);
54                Err(EmailError::Timeout)
55            }
56            Ok(Err(e)) => {
57                exit_flag.store(true, Ordering::SeqCst);
58                Err(EmailError::SmtpError(e))
59            }
60            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
61                exit_flag.store(true, Ordering::SeqCst);
62                Err(EmailError::Timeout)
63            }
64            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
65                exit_flag.store(true, Ordering::SeqCst);
66                Err(EmailError::Disconnected)
67            }
68        }
69    }
70
71    /// Send the email using the provided configuration.
72    ///
73    /// # Errors
74    ///
75    /// - If the email fails to send.
76    /// - If the email cannot be created.
77    /// - If the attachment cannot be read.
78    ///
79    /// # Notes
80    ///
81    /// If the attachment MIME type cannot be determined, it will default to
82    /// `application/octet-stream`.
83    pub fn send_email(&self, email_type: Email) -> Result<(), EmailError> {
84        let email = self.create_email(email_type)?;
85        let mailer = self.setup_smtp_client()?;
86
87        // Send the email
88        mailer.send(&email)?;
89        Ok(())
90    }
91    /// Create the email to send.
92    ///
93    /// If an attachment is provided, the email will be created with the attachment.
94    fn create_email(&self, email_type: Email) -> Result<Message, EmailError> {
95        // Guaranteed config values
96        let from = Mailbox::new(None, self.from.parse()?);
97        // Adjust the email to based on the email type
98        let to = match email_type {
99            Email::Warning => &self.from,
100            Email::DeadMan => &self.to,
101        };
102
103        // parse the comma‐separated list into a Vec<Mailbox>
104        let mailboxes: Result<Vec<Mailbox>, AddressError> = to
105            .split(',')
106            .map(str::trim)
107            .map(|addr| addr.parse::<Mailbox>())
108            .collect();
109        let mailboxes = mailboxes?;
110
111        // Adjust the email builder based on the email type
112        let mut email_builder = Message::builder().from(from);
113
114        // Add recipients
115        for mbox in mailboxes {
116            email_builder = email_builder.to(mbox);
117        }
118
119        let email_builder = match email_type {
120            Email::Warning => email_builder.subject(&self.subject_warning),
121            Email::DeadMan => email_builder.subject(&self.subject),
122        };
123
124        // Prepare the email body
125        let text_part =
126            SinglePart::builder()
127                .header(ContentType::TEXT_PLAIN)
128                .body(match email_type {
129                    Email::Warning => self.message_warning.clone(),
130                    Email::DeadMan => self.message.clone(),
131                });
132
133        // Conditionally add the attachment for DeadMan email type
134        if let Email::DeadMan = email_type {
135            if let Some(attachment) = &self.attachment {
136                let attachment_path = attachment_path(self)?;
137                let filename = attachment_path
138                    .file_name()
139                    .ok_or_else(|| IoError::new(IoErrorKind::NotFound, "Failed to get filename"))?
140                    .to_string_lossy();
141                let filebody = fs::read(attachment)?;
142                let content_type = ContentType::parse(
143                    mime_guess::from_path(attachment)
144                        .first_or_octet_stream()
145                        .as_ref(),
146                )?;
147
148                // Create the attachment part
149                let attachment_part =
150                    Attachment::new(filename.to_string()).body(filebody, content_type);
151
152                // Construct and return the email with the attachment
153                let email = email_builder.multipart(
154                    MultiPart::mixed()
155                        .singlepart(text_part)
156                        .singlepart(attachment_part),
157                )?;
158                return Ok(email);
159            }
160        }
161
162        // For Warning email type or DeadMan without an attachment
163        let email = email_builder.singlepart(text_part)?;
164        Ok(email)
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    fn get_test_config() -> Config {
173        Config {
174            username: "user@example.com".to_string(),
175            password: "password".to_string(),
176            smtp_server: "smtp.example.com".to_string(),
177            smtp_port: 587,
178            smtp_check_timeout: Some(5),
179            message: "This is a test message".to_string(),
180            message_warning: "This is a test warning message".to_string(),
181            subject: "Test Subject".to_string(),
182            subject_warning: "Test Warning Subject".to_string(),
183            to: "recipient@example.com, recipient2@example.com".to_string(),
184            from: "sender@example.com".to_string(),
185            attachment: None,
186            timer_warning: 60,
187            timer_dead_man: 120,
188            web_password: "password".to_string(),
189            cookie_exp_days: 7,
190            log_level: None,
191        }
192    }
193
194    #[test]
195    fn test_create_email_without_attachment() {
196        let config = get_test_config();
197        let email_result = config.create_email(Email::Warning);
198        assert!(email_result.is_ok());
199        let email_result = config.create_email(Email::DeadMan);
200        assert!(email_result.is_ok());
201    }
202
203    #[test]
204    fn test_create_email_with_attachment() {
205        let mut config = get_test_config();
206        // Assuming there's a test file at this path
207        config.attachment = Some("Cargo.toml".into());
208        let email_result = config.create_email(Email::Warning);
209        assert!(email_result.is_ok());
210        let email_result = config.create_email(Email::DeadMan);
211        assert!(email_result.is_ok());
212    }
213
214    #[test]
215    fn test_setup_smtp_client() {
216        let config = Config::default();
217
218        // placeholder: just verifying function signature at present
219        let client = config.setup_smtp_client();
220
221        assert!(client.is_ok());
222    }
223
224    #[test]
225    fn test_check_smtp_connection() {
226        let mut config = Config::default();
227        config.username = "test_username".to_string();
228        config.password = "test_password".to_string();
229        config.smtp_server = "test_smtp_server".to_string();
230        config.smtp_port = 432;
231        config.smtp_check_timeout = Some(1);
232
233        // placeholder: just verifying function signature at present
234        let client = config.check_smtp_connection();
235
236        assert!(client.is_err());
237    }
238}