]>
Commit | Line | Data |
---|---|---|
66004f22 HL |
1 | //! Email related utilities. |
2 | ||
3 | use std::process::{Command, Stdio}; | |
4 | use anyhow::{bail, Error}; | |
5 | use std::io::Write; | |
6 | use chrono::{DateTime, Local}; | |
7 | use crate::tools::time::time; | |
8 | ||
9 | ||
10 | /// Sends multi-part mail with text and/or html to a list of recipients | |
11 | /// | |
12 | /// ``sendmail`` is used for sending the mail. | |
13 | pub fn sendmail(mailto: Vec<&str>, | |
14 | subject: &str, | |
15 | text: Option<&str>, | |
16 | html: Option<&str>, | |
17 | mailfrom: Option<&str>, | |
18 | author: Option<&str>) -> Result<(), Error> { | |
19 | let mail_regex = regex::Regex::new(r"^[a-zA-Z\.0-9-]+@[a-zA-Z\.0-9-]+$").unwrap(); | |
20 | ||
21 | if mailto.is_empty() { | |
22 | bail!("At least one recipient has to be specified!") | |
23 | } | |
24 | ||
25 | for recipient in &mailto { | |
26 | if !mail_regex.is_match(recipient) { | |
27 | bail!("'{}' is not a valid email address", recipient) | |
28 | } | |
29 | } | |
30 | ||
31 | let mailfrom = mailfrom.unwrap_or("root"); | |
32 | if !mailfrom.eq("root") && !mail_regex.is_match(mailfrom) { | |
33 | bail!("'{}' is not a valid email address", mailfrom) | |
34 | } | |
35 | ||
36 | let recipients = mailto.join(","); | |
37 | let author = author.unwrap_or("Proxmox Backup Server"); | |
38 | ||
39 | let now: DateTime<Local> = Local::now(); | |
40 | ||
41 | let mut sendmail_process = match Command::new("/usr/sbin/sendmail") | |
42 | .arg("-B") | |
43 | .arg("8BITMIME") | |
44 | .arg("-f") | |
45 | .arg(mailfrom) | |
46 | .arg("--") | |
47 | .arg(&recipients) | |
48 | .stdin(Stdio::piped()) | |
49 | .spawn() { | |
50 | Err(err) => bail!("could not spawn sendmail process: {}", err), | |
51 | Ok(process) => process | |
52 | }; | |
53 | let mut is_multipart = false; | |
54 | if let (Some(_), Some(_)) = (text, html) { | |
55 | is_multipart = true; | |
56 | } | |
57 | ||
58 | let mut body = String::new(); | |
59 | let boundary = format!("----_=_NextPart_001_{}", time()?); | |
60 | if is_multipart { | |
61 | body.push_str("Content-Type: multipart/alternative;\n"); | |
62 | body.push_str(&format!("\tboundary=\"{}\"\n", boundary)); | |
63 | body.push_str("MIME-Version: 1.0\n"); | |
64 | } else if !subject.is_ascii() { | |
65 | body.push_str("MIME-Version: 1.0\n"); | |
66 | } | |
67 | if !subject.is_ascii() { | |
68 | body.push_str(&format!("Subject: =?utf-8?B?{}?=\n", base64::encode(subject))); | |
69 | } else { | |
70 | body.push_str(&format!("Subject: {}\n", subject)); | |
71 | } | |
72 | body.push_str(&format!("From: {} <{}>\n", author, mailfrom)); | |
73 | body.push_str(&format!("To: {}\n", &recipients)); | |
74 | body.push_str(&format!("Date: {}\n", now.to_rfc2822())); | |
75 | if is_multipart { | |
76 | body.push('\n'); | |
77 | body.push_str("This is a multi-part message in MIME format.\n"); | |
78 | body.push_str(&format!("\n--{}\n", boundary)); | |
79 | } | |
80 | if let Some(text) = text { | |
81 | body.push_str("Content-Type: text/plain;\n"); | |
82 | body.push_str("\tcharset=\"UTF-8\"\n"); | |
83 | body.push_str("Content-Transfer-Encoding: 8bit\n"); | |
84 | body.push('\n'); | |
85 | body.push_str(text); | |
86 | if is_multipart { | |
87 | body.push_str(&format!("\n--{}\n", boundary)); | |
88 | } | |
89 | } | |
90 | if let Some(html) = html { | |
91 | body.push_str("Content-Type: text/html;\n"); | |
92 | body.push_str("\tcharset=\"UTF-8\"\n"); | |
93 | body.push_str("Content-Transfer-Encoding: 8bit\n"); | |
94 | body.push('\n'); | |
95 | body.push_str(html); | |
96 | if is_multipart { | |
97 | body.push_str(&format!("\n--{}--", boundary)); | |
98 | } | |
99 | } | |
100 | ||
101 | if let Err(err) = sendmail_process.stdin.take().unwrap().write_all(body.as_bytes()) { | |
102 | bail!("couldn't write to sendmail stdin: {}", err) | |
103 | }; | |
104 | ||
105 | // wait() closes stdin of the child | |
106 | if let Err(err) = sendmail_process.wait() { | |
107 | bail!("sendmail did not exit successfully: {}", err) | |
108 | } | |
109 | ||
110 | Ok(()) | |
111 | } | |
112 | ||
113 | #[cfg(test)] | |
114 | mod test { | |
115 | use crate::tools::email::sendmail; | |
116 | ||
117 | #[test] | |
118 | fn test1() { | |
119 | let result = sendmail( | |
120 | vec!["somenotvalidemail!", "somealmostvalid email"], | |
121 | "Subject1", | |
122 | Some("TEXT"), | |
123 | Some("<b>HTML</b>"), | |
124 | Some("bim@bam.bum"), | |
125 | Some("test1")); | |
126 | assert!(result.is_err()); | |
127 | } | |
128 | ||
129 | #[test] | |
130 | fn test2() { | |
131 | let result = sendmail( | |
132 | vec![], | |
133 | "Subject2", | |
134 | None, | |
135 | Some("<b>HTML</b>"), | |
136 | None, | |
137 | Some("test1")); | |
138 | assert!(result.is_err()); | |
139 | } | |
140 | ||
141 | #[test] | |
142 | fn test3() { | |
143 | let result = sendmail( | |
144 | vec!["a@b.c"], | |
145 | "Subject3", | |
146 | None, | |
147 | Some("<b>HTML</b>"), | |
148 | Some("notv@lid.com!"), | |
149 | Some("test1")); | |
150 | assert!(result.is_err()); | |
151 | } | |
152 | } |