]> git.proxmox.com Git - proxmox.git/blame - proxmox/src/tools/email.rs
email: add small function to send multi-part emails using sendmail
[proxmox.git] / proxmox / src / tools / email.rs
CommitLineData
66004f22
HL
1//! Email related utilities.
2
3use std::process::{Command, Stdio};
4use anyhow::{bail, Error};
5use std::io::Write;
6use chrono::{DateTime, Local};
7use 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.
13pub 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)]
114mod 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}