1use 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)); let exit_flag_clone = Arc::clone(&exit_flag); let (tx, rx) = channel();
41 thread::spawn(move || {
42 let res = mailer.test_connection();
43 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 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 mailer.send(&email)?;
89 Ok(())
90 }
91 fn create_email(&self, email_type: Email) -> Result<Message, EmailError> {
95 let from = Mailbox::new(None, self.from.parse()?);
97 let to = match email_type {
99 Email::Warning => &self.from,
100 Email::DeadMan => &self.to,
101 };
102
103 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 let mut email_builder = Message::builder().from(from);
113
114 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 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 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 let attachment_part =
150 Attachment::new(filename.to_string()).body(filebody, content_type);
151
152 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 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 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 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 let client = config.check_smtp_connection();
235
236 assert!(client.is_err());
237 }
238}