1use std::fs;
4use std::io::{Error as IoError, ErrorKind as IoErrorKind};
5
6use lettre::{
7 address::AddressError,
8 error::Error as LettreError,
9 message::{
10 header::{ContentType, ContentTypeErr},
11 Attachment, Mailbox, MultiPart, SinglePart,
12 },
13 transport::smtp::{
14 self,
15 authentication::Credentials,
16 client::{Tls, TlsParameters},
17 },
18 Message, SmtpTransport, Transport,
19};
20use thiserror::Error;
21
22use crate::config::{attachment_path, Config, ConfigError, Email};
23
24#[derive(Error, Debug)]
26pub enum EmailError {
27 #[error(transparent)]
29 TlsError(#[from] smtp::Error),
30
31 #[error(transparent)]
33 EmailError(#[from] AddressError),
34
35 #[error(transparent)]
37 BuilderError(#[from] LettreError),
38
39 #[error(transparent)]
41 IoError(#[from] IoError),
42
43 #[error(transparent)]
45 InvalidContent(#[from] ContentTypeErr),
46
47 #[error(transparent)]
49 AttachmentPath(#[from] ConfigError),
50}
51
52impl Config {
53 pub fn send_email(&self, email_type: Email) -> Result<(), EmailError> {
66 let email = self.create_email(email_type)?;
67
68 let creds = Credentials::new(self.username.clone(), self.password.clone());
70 let tls = TlsParameters::new_rustls(self.smtp_server.clone())?;
71 let mailer = SmtpTransport::relay(&self.smtp_server)?
72 .port(self.smtp_port)
73 .credentials(creds)
74 .tls(Tls::Required(tls))
75 .build();
76
77 mailer.send(&email)?;
79 Ok(())
80 }
81 fn create_email(&self, email_type: Email) -> Result<Message, EmailError> {
85 let from = Mailbox::new(None, self.from.parse()?);
87 let to = match email_type {
89 Email::Warning => &self.from,
90 Email::DeadMan => &self.to,
91 };
92
93 let mailboxes: Result<Vec<Mailbox>, AddressError> = to
95 .split(',')
96 .map(str::trim)
97 .map(|addr| addr.parse::<Mailbox>())
98 .collect();
99 let mailboxes = mailboxes?;
100
101 let mut email_builder = Message::builder().from(from);
103
104 for mbox in mailboxes {
106 email_builder = email_builder.to(mbox);
107 }
108
109 let email_builder = match email_type {
110 Email::Warning => email_builder.subject(&self.subject_warning),
111 Email::DeadMan => email_builder.subject(&self.subject),
112 };
113
114 let text_part =
116 SinglePart::builder()
117 .header(ContentType::TEXT_PLAIN)
118 .body(match email_type {
119 Email::Warning => self.message_warning.clone(),
120 Email::DeadMan => self.message.clone(),
121 });
122
123 if let Email::DeadMan = email_type {
125 if let Some(attachment) = &self.attachment {
126 let attachment_path = attachment_path(self)?;
127 let filename = attachment_path
128 .file_name()
129 .ok_or_else(|| IoError::new(IoErrorKind::NotFound, "Failed to get filename"))?
130 .to_string_lossy();
131 let filebody = fs::read(attachment)?;
132 let content_type = ContentType::parse(
133 mime_guess::from_path(attachment)
134 .first_or_octet_stream()
135 .as_ref(),
136 )?;
137
138 let attachment_part =
140 Attachment::new(filename.to_string()).body(filebody, content_type);
141
142 let email = email_builder.multipart(
144 MultiPart::mixed()
145 .singlepart(text_part)
146 .singlepart(attachment_part),
147 )?;
148 return Ok(email);
149 }
150 }
151
152 let email = email_builder.singlepart(text_part)?;
154 Ok(email)
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 fn get_test_config() -> Config {
163 Config {
164 username: "user@example.com".to_string(),
165 password: "password".to_string(),
166 smtp_server: "smtp.example.com".to_string(),
167 smtp_port: 587,
168 message: "This is a test message".to_string(),
169 message_warning: "This is a test warning message".to_string(),
170 subject: "Test Subject".to_string(),
171 subject_warning: "Test Warning Subject".to_string(),
172 to: "recipient@example.com, recipient2@example.com".to_string(),
173 from: "sender@example.com".to_string(),
174 attachment: None,
175 timer_warning: 60,
176 timer_dead_man: 120,
177 web_password: "password".to_string(),
178 cookie_exp_days: 7,
179 log_level: None,
180 }
181 }
182
183 #[test]
184 fn test_create_email_without_attachment() {
185 let config = get_test_config();
186 let email_result = config.create_email(Email::Warning);
187 assert!(email_result.is_ok());
188 let email_result = config.create_email(Email::DeadMan);
189 assert!(email_result.is_ok());
190 }
191
192 #[test]
193 fn test_create_email_with_attachment() {
194 let mut config = get_test_config();
195 config.attachment = Some("Cargo.toml".into());
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}