]> git.proxmox.com Git - proxmox.git/blame - proxmox-sys/src/email.rs
sys: email: use `epoch_to_rfc2822` from proxmox_time
[proxmox.git] / proxmox-sys / src / email.rs
CommitLineData
66004f22
HL
1//! Email related utilities.
2
bbc94222
WB
3use std::io::Write;
4use std::process::{Command, Stdio};
66004f22 5
d20d9ec1 6use anyhow::{bail, format_err, Error};
336dab01 7
66004f22
HL
8/// Sends multi-part mail with text and/or html to a list of recipients
9///
bcdcb181
GG
10/// Includes the header `Auto-Submitted: auto-generated`, so that auto-replies
11/// (i.e. OOO replies) won't trigger.
66004f22 12/// ``sendmail`` is used for sending the mail.
bbc94222 13pub fn sendmail(
48d049d4 14 mailto: &[&str],
bbc94222
WB
15 subject: &str,
16 text: Option<&str>,
17 html: Option<&str>,
18 mailfrom: Option<&str>,
19 author: Option<&str>,
20) -> Result<(), Error> {
757031ef
WB
21 use std::fmt::Write as _;
22
66004f22
HL
23 if mailto.is_empty() {
24 bail!("At least one recipient has to be specified!")
25 }
66004f22 26 let mailfrom = mailfrom.unwrap_or("root");
66004f22
HL
27 let recipients = mailto.join(",");
28 let author = author.unwrap_or("Proxmox Backup Server");
29
336dab01 30 let now = proxmox_time::epoch_i64();
66004f22
HL
31
32 let mut sendmail_process = match Command::new("/usr/sbin/sendmail")
33 .arg("-B")
34 .arg("8BITMIME")
35 .arg("-f")
36 .arg(mailfrom)
37 .arg("--")
a7f40023 38 .args(mailto)
66004f22 39 .stdin(Stdio::piped())
bbc94222
WB
40 .spawn()
41 {
66004f22 42 Err(err) => bail!("could not spawn sendmail process: {}", err),
bbc94222 43 Ok(process) => process,
66004f22
HL
44 };
45 let mut is_multipart = false;
46 if let (Some(_), Some(_)) = (text, html) {
47 is_multipart = true;
48 }
49
50 let mut body = String::new();
557cce7a 51 let boundary = format!("----_=_NextPart_001_{}", now);
66004f22
HL
52 if is_multipart {
53 body.push_str("Content-Type: multipart/alternative;\n");
757031ef 54 let _ = writeln!(body, "\tboundary=\"{}\"", boundary);
66004f22
HL
55 body.push_str("MIME-Version: 1.0\n");
56 } else if !subject.is_ascii() {
57 body.push_str("MIME-Version: 1.0\n");
58 }
59 if !subject.is_ascii() {
757031ef 60 let _ = writeln!(body, "Subject: =?utf-8?B?{}?=", base64::encode(subject));
66004f22 61 } else {
757031ef 62 let _ = writeln!(body, "Subject: {}", subject);
66004f22 63 }
757031ef
WB
64 let _ = writeln!(body, "From: {} <{}>", author, mailfrom);
65 let _ = writeln!(body, "To: {}", &recipients);
dc72878d 66 let rfc2822_date = proxmox_time::epoch_to_rfc2822(now)?;
757031ef 67 let _ = writeln!(body, "Date: {}", rfc2822_date);
5517d6f8
GG
68 body.push_str("Auto-Submitted: auto-generated;\n");
69
66004f22
HL
70 if is_multipart {
71 body.push('\n');
72 body.push_str("This is a multi-part message in MIME format.\n");
757031ef 73 let _ = write!(body, "\n--{}\n", boundary);
66004f22
HL
74 }
75 if let Some(text) = text {
76 body.push_str("Content-Type: text/plain;\n");
77 body.push_str("\tcharset=\"UTF-8\"\n");
78 body.push_str("Content-Transfer-Encoding: 8bit\n");
79 body.push('\n');
80 body.push_str(text);
81 if is_multipart {
757031ef 82 let _ = write!(body, "\n--{}\n", boundary);
66004f22
HL
83 }
84 }
85 if let Some(html) = html {
86 body.push_str("Content-Type: text/html;\n");
87 body.push_str("\tcharset=\"UTF-8\"\n");
88 body.push_str("Content-Transfer-Encoding: 8bit\n");
89 body.push('\n');
90 body.push_str(html);
91 if is_multipart {
757031ef 92 let _ = write!(body, "\n--{}--", boundary);
66004f22
HL
93 }
94 }
95
bbc94222
WB
96 if let Err(err) = sendmail_process
97 .stdin
98 .take()
99 .unwrap()
100 .write_all(body.as_bytes())
101 {
66004f22
HL
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
d20d9ec1
LW
113/// Forwards an email message to a given list of recipients.
114///
115/// ``sendmail`` is used for sending the mail, thus `message` must be
116/// compatible with that (the message is piped into stdin unmodified).
117pub fn forward(
118 mailto: &[&str],
119 mailfrom: &str,
120 message: &[u8],
121 uid: Option<u32>,
122) -> Result<(), Error> {
123 use std::os::unix::process::CommandExt;
124
125 if mailto.is_empty() {
126 bail!("At least one recipient has to be specified!")
127 }
128
129 let mut builder = Command::new("/usr/sbin/sendmail");
130
131 builder
132 .args([
133 "-N", "never", // never send DSN (avoid mail loops)
134 "-f", mailfrom, "--",
135 ])
136 .args(mailto)
137 .stdin(Stdio::piped())
138 .stdout(Stdio::null())
139 .stderr(Stdio::null());
140
141 if let Some(uid) = uid {
142 builder.uid(uid);
143 }
144
145 let mut process = builder
146 .spawn()
147 .map_err(|err| format_err!("could not spawn sendmail process: {err}"))?;
148
149 process
150 .stdin
151 .take()
152 .unwrap()
153 .write_all(message)
154 .map_err(|err| format_err!("couldn't write to sendmail stdin: {err}"))?;
155
156 process
157 .wait()
158 .map_err(|err| format_err!("sendmail did not exit successfully: {err}"))?;
159
160 Ok(())
161}
162
66004f22
HL
163#[cfg(test)]
164mod test {
ec3965fd 165 use crate::email::sendmail;
66004f22 166
66004f22 167 #[test]
02acce2d 168 fn email_without_recipients() {
66004f22 169 let result = sendmail(
48d049d4 170 &[],
66004f22
HL
171 "Subject2",
172 None,
173 Some("<b>HTML</b>"),
174 None,
bbc94222
WB
175 Some("test1"),
176 );
66004f22
HL
177 assert!(result.is_err());
178 }
bbc94222 179}